<commit_before>'use strict';
var api = require('./apiMock');

var convoId = 0;
// mock for main bot object
class Bot {
    // in constructor we pass controller mock instance
    constructor(controller) {
        this.apiResponses = new api();
        this.botkit = {
            tasks: [],
            log: function(){},
            memory_store: {
                users: {},
                channels: {},
                teams: {}
            }
        };
        var self = this;
        // this object will store bot answer for each user separately
        this.detailedAnswers = {
        // "userId": ["answer1","anwer2"]
        };
        // store all open convos
        this.convos = []
        // reference to controller object
        this.controller = controller;
        // bot patterns - migrated from origin botkit
        this.utterances = {
            yes: "yes",
            no: "no"
        }

        this.api = this.buildProxyAPI();

        this.callbacksHashByConvo = {};

        this.botkit.storage = {
            teams: {
                get: function(team_id, cb) {
                    cb(null, self.botkit.memory_store.teams[team_id]);
                },
                save: function(team, cb) {
                    if (team.id) {
                        self.botkit.memory_store.teams[team.id] = team;
                        cb(null, team.id);
                    } else {
                        cb('No ID specified');
                    }
                },
                all: function(cb) {
                    cb(null, self.botkit.memory_store.teams);
                }
            },
            users: {
                get: function(user_id, cb) {
                    cb(null, self.botkit.memory_store.users[user_id]);
                },
                save: function(user, cb) {
                    if (user.id) {
                        self.botkit.memory_store.users[user.id] = user;
                        cb(null, user.id);
                    } else {
                        cb('No ID specified');
                    }
                },
                all: function(cb) {
                    cb(null, self.botkit.memory_store.users);
                }
            },
            channels: {
                get: function(channel_id, cb) {
                    cb(null, self.botkit.memory_store.channels[channel_id]);
                },
                save: function(channel, cb) {
                    if (channel.id) {
                        self.botkit.memory_store.channels[channel.id] = channel;
                        cb(null, channel.id);
                    } else {
                        cb('No ID specified');
                    }
                },
                all: function(cb) {
                    cb(null, self.botkit.memory_store.channels);
                }
            }
        };
    }

    getAPILogByNumber(i) {
        return this.log[i]
    }

    buildProxyAPI() {
        let o = {};
        this.log = []
        o['callAPI'] = (apiName, params, callback) => {
            if (apiName != 'chat.update')
                this.log.push(apiName);
            this.apiResponses.api['callAPI'](apiName, params, callback)
        }
        return o;
    }

    updateApiByKey(key, value) {
        this.apiResponses.updateData(key, value);
    }

    getApiByKey(key) {
        return this.apiResponses.getData(key);
    }

    // reply to user and store reply in array
    reply(message, text, cb) {
        // check if this is first time then create anwers array
        if(message.channel){
            if(!this.detailedAnswers[message.channel]){
                this.detailedAnswers[message.channel] = []
            }
            this.detailedAnswers[message.channel].push(text)
        }
        else{
            if (!this.detailedAnswers[message.user])
                this.detailedAnswers[message.user] = []
            this.detailedAnswers[message.user].push(text)
        }
        if (typeof cb === 'function') {
            cb({}, {})
        }
    }
    // start new conversation
    startConversation(message, callback) {
        // creat enew convo object
        this.convo = new Convo(this);
        // setup convo owner
        this.convo.user = message.user;
        this.convo.sourceMessage.user = message.user;
        this.convo.sourceMessage.channel = message.user;
        // store convo
        this.convos.push(this.convo);
        // invoke callback
        callback(null, this.convo)
    }
    //say mock - without logic
    say(message, callback) {
        if(message.channel){
            if(!this.detailedAnswers[message.channel]){
                this.detailedAnswers[message.channel] = []
            }
            this.detailedAnswers[message.channel].push(message.text)
        }
        else{
            if (!this.detailedAnswers[message.user])
                this.detailedAnswers[message.user] = [];
            this.detailedAnswers[message.user].push(message.text)
        }
        if (typeof callback === 'function') {
            callback(null, {
                message: {}
            })
        }
    }

}
// mock for controller class
class Controller {
    // constructor taks slackUserId and userName
    constructor(userId, userName) {
        this.userName = userName;
        this.user = userId
        // create bot object
        this.bot = new Bot(this);
        //link on bot.storage
        this.storage = this.bot.botkit.storage;
        // store action which we wanna listen
        this.actions = [];
    }
    // allow multiple convos
    // need refactor return promise
    usersInput(userTypes, options) {

        var self = this;
        // return promise which will be rosolved when we need assertion
        return new Promise((resolve, reject) => {
            // get user who first type message to bot chat
            var whoStartConvoWithBotFirst = userTypes.filter((userType) => {
                return userType.first;
            })[0]
            if (!whoStartConvoWithBotFirst)
                throw "initiator missing"
                // save all typers who will take action in conversation
            self.allTypers = userTypes;
            // save main typer ( first typer )
            self.mainTyper = whoStartConvoWithBotFirst;
            // save options
            self.typeOptions = options || {};
            // add resolve to type options
            self.typeOptions.resolve = resolve;
            // get first message which need send to bot
            var msg = whoStartConvoWithBotFirst.messages.shift()
                // correct message format - message must be an object with property text: "Message Text"
            if (typeof(msg) == 'string')
                msg = {
                    text: msg,
                }

            msg.type = (whoStartConvoWithBotFirst.type || 'direct_message');
                // set message user
            msg.user = msg.user || whoStartConvoWithBotFirst.user
            // send first message
            self.initialUserMessage(msg, options)
        })
    }
    initialUserMessage(message, options) {
        var self = this;
        // find action which will handle this message
        var action = self.actions.filter((obj) => {
            if (!Array.isArray(obj.type)) {
                obj.type = obj.type.split(",");
            }
            let matchType = obj.type.indexOf(message.type) > -1;
            // each action has pattern
            let pattern = obj.pattern;
            if (Array.isArray(obj.pattern)) {
                for (let i = 0;i < pattern.length;i++) {
                    if ((message.text || message).match(new RegExp(pattern[i], 'i'))) {
                        message.match = (message.text || message).match(pattern);

                        return (message.text || message).match(new RegExp(pattern[i], 'i')) && matchType;
                    }
                }
            } else {
                if ((message.text || message).match(new RegExp(pattern, 'i'))) {
                    message.match = (message.text || message).match(new RegExp(pattern, 'i')) && matchType;
                }
            }

            return (message.text || message).match(new RegExp(pattern, 'i')) && matchType;
        })[0];
        if (action) {
            // call action callback with bot and new message object
            action.callback(self.bot, {
                user: message.user || self.user,
                is_bot: self.is_bot || false,
                username: self.userName,
                message: message.text,
                text: message.text,
                match: message.match,
                //for controller.on
                channel: message.channel || message.user || self.user
            })
            // if message need assertion resolve promise - this cause test assertion
            if (message.isAssertion)
                if (message.onEvent) {
                    setTimeout(() => {
                        // simple resolve without params if we handle onEvent messages ( when user join to channel and join to team or leave channel )
                        self.typeOptions.resolve()
                    }, 200)
                } else {
                    setTimeout(() => {
                        let result;
                        if (self.bot.detailedAnswers[message.user]) {
                            result = self.bot.detailedAnswers[message.user][self.bot.detailedAnswers[message.user].length - 1];
                        }
                        // resolve with last message
                        self.typeOptions.resolve(result)
                    }, 200)
                }
        } else {
            // if we can`t find action - by default we`ll never came to this code because we have default bot response for all messages.
            throw 'Can`t find action for this user type'
        }
    }
    // mock for hears - when we subscribe for some actions
    hears(regex, type, callbacks) {
        // push to actions array and use in initialUserMessage
        this.actions.push({
            pattern: regex,
            type: type,
            callback: callbacks
        });
    }
    // alias for hears
    on(event, callback) {
        this.hears(event, 'direct_message', callback)
    }
}
// convo object
class Convo {
    // takes bot object in constructor
    constructor(bot) {
        this.id = convoId = convoId + 1;
        this.bot = bot;
        this.sourceMessage = {
            user: ''
        }

    }
    // main ask action which as user some question
    ask(message, callbacks) {
        var self = this;
        // save bot message in user bot`s answers
        if (!self.bot.detailedAnswers[self.user])
        // if answers not exists create answers array for current convo user
            self.bot.detailedAnswers[self.user] = []
            // push bot message to answers array
        self.bot.detailedAnswers[self.user].push(message)
        // find current typer by message.user
        var currentTyper = (self.bot.controller.allTypers || []).filter((typer) => {
            return typer.user == self.user;
        })[0]
        if (currentTyper) {
            // check if user has some message for answer to bot
            if (currentTyper.messages.length) {
                // get last message
                var messageNew = currentTyper.messages.shift()
                if (typeof(callbacks) == 'function' && (messageNew.text || messageNew.file)) {
                    // simple callback
                    callbacks({
                        text: messageNew.text,
                        channel: this.sourceMessage.user
                    }, self)
                } else if (Array.isArray(callbacks)) {
                    // find callback in callbacks array
                    var callback = callbacks.filter((c) => {
                        return (messageNew.text || '').toString().match(c.pattern)
                    })[0]
                    // check that message has text - we can`t send empty messages
                    if (messageNew.text || messageNew.file)
                    // invoke callback
                        if (callback && callback.callback) {
                            if (messageNew.callbackTimeout) {
                                setTimeout(() => {
                                    callback.callback({
                                        text: messageNew.text,
                                        user: self.user
                                    }, self)
                                }, messageNew.callbackTimeout)
                            } else {
                                callback.callback({
                                    text: messageNew.text,
                                    user: self.user,
                                    file: messageNew.file
                                }, self)
                            }
                        } else {
                            // find default callback and invoke
                            var defaultCallback = callbacks.filter((c) => {
                                return c.default
                            })[0]
                            if (defaultCallback) {
                                if (messageNew.callbackTimeout) {
                                    setTimeout(() => {
                                        default_callback.callback({
                                            text: messageNew.text,
                                            user: self.user
                                        }, self)
                                    }, messageNew.callbackTimeout)
                                } else {
                                    defaultCallback.callback({
                                        text: messageNew.text,
                                        user: self.user
                                    }, self)
                                }
                            }
                        }

                }
                // need wait some time before resolve

                if (messageNew.timeout && messageNew.isAssertion) {
                    return setTimeout(() => {
                        self.bot.controller.typeOptions.resolve(self.bot.detailedAnswers[self.user][self.bot.detailedAnswers[self.user].length - 1 - (messageNew.deep || 0)])
                    }, messageNew.timeout)
                }
                if (messageNew.isAssertion && !messageNew.timeout) {
                    setTimeout(() => {
                        self.bot.controller.typeOptions.resolve(self.bot.detailedAnswers[self.user][self.bot.detailedAnswers[self.user].length - 1 - (messageNew.deep || 0)])
                    }, 200)
                }
            } else {
                // last message already typed
            }

        } else {
            // when can`t find typer by slack id
        }
    }
    // simple say mock we just store answer in array
    say(text) {
        var self = this;
        // check if answers array exists for user
        if (!self.bot.detailedAnswers[self.user])
            self.bot.detailedAnswers[self.user] = []
            // push anwser to array
        self.bot.detailedAnswers[self.user].push(text)
    }
    // mock for next no logic
    next() {

    }
    // mock for stop no logic
    stop() {

    }
    // mock for repeat no logic
    repeat() {

    }
    // mock for silentRepeat no logic
    silentRepeat() {

    }
}
module.exports = {
    bot: Bot,
    controller: Controller,
    convo: Convo
}
<commit_msg>Get first match in case we got multiple matches<commit_after>'use strict';
var api = require('./apiMock');

var convoId = 0;
// mock for main bot object
class Bot {
    // in constructor we pass controller mock instance
    constructor(controller) {
        this.apiResponses = new api();
        this.botkit = {
            tasks: [],
            log: function(){},
            memory_store: {
                users: {},
                channels: {},
                teams: {}
            }
        };
        var self = this;
        // this object will store bot answer for each user separately
        this.detailedAnswers = {
        // "userId": ["answer1","anwer2"]
        };
        // store all open convos
        this.convos = []
        // reference to controller object
        this.controller = controller;
        // bot patterns - migrated from origin botkit
        this.utterances = {
            yes: "yes",
            no: "no"
        }

        this.api = this.buildProxyAPI();

        this.callbacksHashByConvo = {};

        this.botkit.storage = {
            teams: {
                get: function(team_id, cb) {
                    cb(null, self.botkit.memory_store.teams[team_id]);
                },
                save: function(team, cb) {
                    if (team.id) {
                        self.botkit.memory_store.teams[team.id] = team;
                        cb(null, team.id);
                    } else {
                        cb('No ID specified');
                    }
                },
                all: function(cb) {
                    cb(null, self.botkit.memory_store.teams);
                }
            },
            users: {
                get: function(user_id, cb) {
                    cb(null, self.botkit.memory_store.users[user_id]);
                },
                save: function(user, cb) {
                    if (user.id) {
                        self.botkit.memory_store.users[user.id] = user;
                        cb(null, user.id);
                    } else {
                        cb('No ID specified');
                    }
                },
                all: function(cb) {
                    cb(null, self.botkit.memory_store.users);
                }
            },
            channels: {
                get: function(channel_id, cb) {
                    cb(null, self.botkit.memory_store.channels[channel_id]);
                },
                save: function(channel, cb) {
                    if (channel.id) {
                        self.botkit.memory_store.channels[channel.id] = channel;
                        cb(null, channel.id);
                    } else {
                        cb('No ID specified');
                    }
                },
                all: function(cb) {
                    cb(null, self.botkit.memory_store.channels);
                }
            }
        };
    }

    getAPILogByNumber(i) {
        return this.log[i]
    }

    buildProxyAPI() {
        let o = {};
        this.log = []
        o['callAPI'] = (apiName, params, callback) => {
            if (apiName != 'chat.update')
                this.log.push(apiName);
            this.apiResponses.api['callAPI'](apiName, params, callback)
        }
        return o;
    }

    updateApiByKey(key, value) {
        this.apiResponses.updateData(key, value);
    }

    getApiByKey(key) {
        return this.apiResponses.getData(key);
    }

    // reply to user and store reply in array
    reply(message, text, cb) {
        // check if this is first time then create anwers array
        if(message.channel){
            if(!this.detailedAnswers[message.channel]){
                this.detailedAnswers[message.channel] = []
            }
            this.detailedAnswers[message.channel].push(text)
        }
        else{
            if (!this.detailedAnswers[message.user])
                this.detailedAnswers[message.user] = []
            this.detailedAnswers[message.user].push(text)
        }
        if (typeof cb === 'function') {
            cb({}, {})
        }
    }
    // start new conversation
    startConversation(message, callback) {
        // creat enew convo object
        this.convo = new Convo(this);
        // setup convo owner
        this.convo.user = message.user;
        this.convo.sourceMessage.user = message.user;
        this.convo.sourceMessage.channel = message.user;
        // store convo
        this.convos.push(this.convo);
        // invoke callback
        callback(null, this.convo)
    }
    //say mock - without logic
    say(message, callback) {
        if(message.channel){
            if(!this.detailedAnswers[message.channel]){
                this.detailedAnswers[message.channel] = []
            }
            this.detailedAnswers[message.channel].push(message.text)
        }
        else{
            if (!this.detailedAnswers[message.user])
                this.detailedAnswers[message.user] = [];
            this.detailedAnswers[message.user].push(message.text)
        }
        if (typeof callback === 'function') {
            callback(null, {
                message: {}
            })
        }
    }

}
// mock for controller class
class Controller {
    // constructor taks slackUserId and userName
    constructor(userId, userName) {
        this.userName = userName;
        this.user = userId
        // create bot object
        this.bot = new Bot(this);
        //link on bot.storage
        this.storage = this.bot.botkit.storage;
        // store action which we wanna listen
        this.actions = [];
    }
    // allow multiple convos
    // need refactor return promise
    usersInput(userTypes, options) {

        var self = this;
        // return promise which will be rosolved when we need assertion
        return new Promise((resolve, reject) => {
            // get user who first type message to bot chat
            var whoStartConvoWithBotFirst = userTypes.filter((userType) => {
                return userType.first;
            })[0]
            if (!whoStartConvoWithBotFirst)
                throw "initiator missing"
                // save all typers who will take action in conversation
            self.allTypers = userTypes;
            // save main typer ( first typer )
            self.mainTyper = whoStartConvoWithBotFirst;
            // save options
            self.typeOptions = options || {};
            // add resolve to type options
            self.typeOptions.resolve = resolve;
            // get first message which need send to bot
            var msg = whoStartConvoWithBotFirst.messages.shift()
                // correct message format - message must be an object with property text: "Message Text"
            if (typeof(msg) == 'string')
                msg = {
                    text: msg,
                }

            msg.type = (whoStartConvoWithBotFirst.type || 'direct_message');
                // set message user
            msg.user = msg.user || whoStartConvoWithBotFirst.user
            // send first message
            self.initialUserMessage(msg, options)
        })
    }
    initialUserMessage(message, options) {
        var self = this;
        // find action which will handle this message
        var matches = [];
        var action = self.actions.filter((obj) => {
            if (!Array.isArray(obj.type)) {
                obj.type = obj.type.split(",");
            }
            let matchType = obj.type.indexOf(message.type) > -1;
            // each action has pattern
            let pattern = obj.pattern;
            if (Array.isArray(obj.pattern)) {
                for (let i = 0;i < pattern.length;i++) {
                    if ((message.text || message).match(new RegExp(pattern[i], 'i'))) {
                        matches.push((message.text || message).match(pattern[i]));

                        return (message.text || message).match(new RegExp(pattern[i], 'i')) && matchType;
                    }
                }
            } else {
                if ((message.text || message).match(new RegExp(pattern, 'i'))) {
                    matches.push((message.text || message).match(new RegExp(pattern, 'i')) && matchType);
                }
            }

            return (message.text || message).match(new RegExp(pattern, 'i')) && matchType;
        })[0];
        if (action) {
            // call action callback with bot and new message object
            action.callback(self.bot, {
                user: message.user || self.user,
                is_bot: self.is_bot || false,
                username: self.userName,
                message: message.text,
                text: message.text,
                match: matches.find((v) => v!=null),
                //for controller.on
                channel: message.channel || message.user || self.user
            })
            // if message need assertion resolve promise - this cause test assertion
            if (message.isAssertion)
                if (message.onEvent) {
                    setTimeout(() => {
                        // simple resolve without params if we handle onEvent messages ( when user join to channel and join to team or leave channel )
                        self.typeOptions.resolve()
                    }, 200)
                } else {
                    setTimeout(() => {
                        let result;
                        if (self.bot.detailedAnswers[message.user]) {
                            result = self.bot.detailedAnswers[message.user][self.bot.detailedAnswers[message.user].length - 1];
                        }
                        // resolve with last message
                        self.typeOptions.resolve(result)
                    }, 200)
                }
        } else {
            // if we can`t find action - by default we`ll never came to this code because we have default bot response for all messages.
            throw 'Can`t find action for this user type'
        }
    }
    // mock for hears - when we subscribe for some actions
    hears(regex, type, callbacks) {
        // push to actions array and use in initialUserMessage
        this.actions.push({
            pattern: regex,
            type: type,
            callback: callbacks
        });
    }
    // alias for hears
    on(event, callback) {
        this.hears(event, 'direct_message', callback)
    }
}
// convo object
class Convo {
    // takes bot object in constructor
    constructor(bot) {
        this.id = convoId = convoId + 1;
        this.bot = bot;
        this.sourceMessage = {
            user: ''
        }

    }
    // main ask action which as user some question
    ask(message, callbacks) {
        var self = this;
        // save bot message in user bot`s answers
        if (!self.bot.detailedAnswers[self.user])
        // if answers not exists create answers array for current convo user
            self.bot.detailedAnswers[self.user] = []
            // push bot message to answers array
        self.bot.detailedAnswers[self.user].push(message)
        // find current typer by message.user
        var currentTyper = (self.bot.controller.allTypers || []).filter((typer) => {
            return typer.user == self.user;
        })[0]
        if (currentTyper) {
            // check if user has some message for answer to bot
            if (currentTyper.messages.length) {
                // get last message
                var messageNew = currentTyper.messages.shift()
                if (typeof(callbacks) == 'function' && (messageNew.text || messageNew.file)) {
                    // simple callback
                    callbacks({
                        text: messageNew.text,
                        channel: this.sourceMessage.user
                    }, self)
                } else if (Array.isArray(callbacks)) {
                    // find callback in callbacks array
                    var callback = callbacks.filter((c) => {
                        return (messageNew.text || '').toString().match(c.pattern)
                    })[0]
                    // check that message has text - we can`t send empty messages
                    if (messageNew.text || messageNew.file)
                    // invoke callback
                        if (callback && callback.callback) {
                            if (messageNew.callbackTimeout) {
                                setTimeout(() => {
                                    callback.callback({
                                        text: messageNew.text,
                                        user: self.user
                                    }, self)
                                }, messageNew.callbackTimeout)
                            } else {
                                callback.callback({
                                    text: messageNew.text,
                                    user: self.user,
                                    file: messageNew.file
                                }, self)
                            }
                        } else {
                            // find default callback and invoke
                            var defaultCallback = callbacks.filter((c) => {
                                return c.default
                            })[0]
                            if (defaultCallback) {
                                if (messageNew.callbackTimeout) {
                                    setTimeout(() => {
                                        default_callback.callback({
                                            text: messageNew.text,
                                            user: self.user
                                        }, self)
                                    }, messageNew.callbackTimeout)
                                } else {
                                    defaultCallback.callback({
                                        text: messageNew.text,
                                        user: self.user
                                    }, self)
                                }
                            }
                        }

                }
                // need wait some time before resolve

                if (messageNew.timeout && messageNew.isAssertion) {
                    return setTimeout(() => {
                        self.bot.controller.typeOptions.resolve(self.bot.detailedAnswers[self.user][self.bot.detailedAnswers[self.user].length - 1 - (messageNew.deep || 0)])
                    }, messageNew.timeout)
                }
                if (messageNew.isAssertion && !messageNew.timeout) {
                    setTimeout(() => {
                        self.bot.controller.typeOptions.resolve(self.bot.detailedAnswers[self.user][self.bot.detailedAnswers[self.user].length - 1 - (messageNew.deep || 0)])
                    }, 200)
                }
            } else {
                // last message already typed
            }

        } else {
            // when can`t find typer by slack id
        }
    }
    // simple say mock we just store answer in array
    say(text) {
        var self = this;
        // check if answers array exists for user
        if (!self.bot.detailedAnswers[self.user])
            self.bot.detailedAnswers[self.user] = []
            // push anwser to array
        self.bot.detailedAnswers[self.user].push(text)
    }
    // mock for next no logic
    next() {

    }
    // mock for stop no logic
    stop() {

    }
    // mock for repeat no logic
    repeat() {

    }
    // mock for silentRepeat no logic
    silentRepeat() {

    }
}
module.exports = {
    bot: Bot,
    controller: Controller,
    convo: Convo
}
<|endoftext|>


<commit_before>
/*
  Session
 */

var app = corejs.app

var _ = require('underscore'),
  util = require('util'),
  crypto = require('crypto'),
  node_uuid = require('node-uuid'),
  slice = Array.prototype.slice,
  IncomingMessage = require('http').IncomingMessage,
  OutgoingMessage = require('http').OutgoingMessage;

var _end = OutgoingMessage.prototype.end;

function Session(config) {

  config = config || {};
  this.app = app;

  // Middleware configuration defaults
  this.config = _.extend({
    guestSessions: false,                 // Guest sessions support
    regenInterval: 5 * 60,                // Interval to regenerate the sessionId
    permanentExpires: 30 * 24 * 3600,     // Permanent sessions timeout
    temporaryExpires: 24 * 3600,          // Temporary (browser) sessions timeout
    guestExpires: 7 * 24 * 3600,          // Guest sessions timeout
    typecastVars: [],                     // Session variables to automatically typecast
    sessCookie: "_sess",                  // Session Cookie
    hashCookie: "_shash",                 // Session hash
    defaultUserAgent: "Mozilla",          // Default User Agent
    salt: "$28b28fc2ebcd355ca1a2be8881e888a.67a42975e1626d59434e576b5c63f3483!" // Session Salt
    }, config);

    if (typeof config.storage == 'object') this.storage = config.storage;
    else if (typeof config.storage == 'string') this.storage = app.getResource('storages/' + config.storage);
    else throw new Error('Session requires a storage');

    this.className = this.constructor.name;

    app.debug('Session enabled');

    corejs.util.onlySetEnumerable(this, ['className', 'storage']);
  }

  Session.prototype.storage = null;

/**
  Creates a session

  @param {object} req
  @param {object} res
  @param {object} data
  @param {boolean} persistent
  @param {function} callback
  @public
*/

Session.prototype.create = function(req, res, data, persistent, callback) {
  var expires, guest, hashes, multi, userAgent, userAgentMd5, self = this;
  guest = null;
  if (persistent == 'guest') {
    guest = true;
    persistent = true;
  }
  this.app.debug( guest ? 'Creating guest session' : 'Creating session' );
  userAgent = req.headers['user-agent'] || this.config.defaultUserAgent;
  userAgentMd5 = this.md5(userAgent);
  hashes = this.createHash(userAgent, guest);

  if (guest) {
    // Guest sessions have their own expiration timeout
    expires = this.config.guestExpires;
  } else {
    // Is the session persistent ? If yes, timeout should be permanentExpires
    // Otherwise, timeout should be temporaryExpires (browser session)
    expires = (persistent ? this.config.permanentExpires : this.config.temporaryExpires);

    data = _.extend(data, {
      fpr: hashes.fingerprint,
      ua_md5: userAgentMd5,
      pers: (persistent ? 1 : 0)
    });
  }

  multi = this.storage.multi();
  if (!guest && req.session.guest && req.hasCookie(this.config.sessCookie)) {
    multi.delete(req.getCookie(this.config.sessCookie));
  }
  multi.setHash(hashes.sessId, data);
  multi.expire(hashes.sessId, expires);

  multi.exec(function(err, replies) {
    if (err) app.log(err);
    else {

      // Expires has been calculated a few lines back
      res.setCookie(self.config.sessCookie, hashes.sessId, {
        expires: expires
      });

      if (guest) {
        data.guest = parseInt(data.guest, 10);
      } else {
        res.setCookie(self.config.hashCookie, hashes.fingerprint, {
          expires: self.config.regenInterval
        });
      }

      data = self.typecast(data);
      req.session = data;
      req.__origSessionState = _.extend({}, data);
      req.__jsonSession = JSON.stringify(data);
      self.app.emit('load_session', hashes.sessId, req.session);
      callback.call(self, req.session, hashes, expires);
    }
  });
}

/**
  Destroys a session

  @param {object} req
  @param {object} res
  @param {function} callback
  @public
*/

Session.prototype.destroy = function(req, res, callback) {
  var fingerprint, sessId, self = this;
  this.app.debug('Destroying session');
  if (req.hasCookie(this.config.sessCookie) && req.session) {
    sessId = req.getCookie(this.config.sessCookie);
    fingerprint = this.getFingerprint(req, sessId);

    if (fingerprint == req.session.fpr) {
      this.storage.delete(sessId, function(err) {
        if (err) app.serverError(res, err);
        else {
          res.removeCookies(self.config.sessCookie, self.config.hashCookie);
          callback.call(self);
        }
      });
    } else {
      res.removeCookies(this.config.sessCookie, this.config.hashCookie);
      this.app.login(res);
    }
  } else {
    this.app.login(res);
  }
}

/**
  Loads the session

  @param {object} req
  @param {object} res
  @param {function} callback
  @private
*/

Session.prototype.loadSession = function(req, res, callback) {
  var fingerprint, sessHash, sessId, self, app;

  if (req.__loadedSession === true) {
    callback.call(this);
    return;
  } else {
    req.__loadedSession = true;
  }

  self = this;
  app = this.app;

  sessId = req.getCookie(this.config.sessCookie);
  sessHash = req.getCookie(this.config.hashCookie);
  fingerprint = self.getFingerprint(req, sessId);

  if (sessId) { // A cookie with sessId exists

    // Get the session data from storage
    this.storage.getHash(sessId, function(err, data) {
      var expires, guest, hashes, multi, newHash, newSess, ua_md5, userAgent;

      // If errors retrieving session data, respond with HTTP/500 & log error
      if (err) { app.serverError(res, err); return;}

      // If it's not a user session, it's a guest session
      guest = (data.user == null);

      if (_.isEmpty(data)) { // If data is empty

        if (self.config.guestSessions) {
          // => Create guest session
          res.removeCookie(self.config.hashCookie);
          self.createGuestSession(req, res, callback);
        } else {
          // => Remove cookies, contain invalid or expired session data
          req.removeCookies(self.config.sessCookie, self.config.hashCookie);
          callback.call(self, {});
        }

      } else { // Data is not empty

        if (guest) data.guest = parseInt(data.guest, 10);
        else  data.pers = parseInt(data.pers, 10);

        data = self.typecast(data);

        if (guest) { // If guest session detected

          app.debug('Loading guest session');
          req.session = data;
          req.__origSessionState = _.extend({}, data);
          req.__jsonSession = JSON.stringify(data);
          app.emit('load_session', sessId, req.session);
          callback.call(self);

          } else if (sessHash) { // If session hash detected

            if (sessHash == fingerprint && sessHash == data.fpr) { // If cookie hash matches session hash

              // Load user session
              app.debug('Loading session');
              req.session = data;
              req.__origSessionState = _.extend({}, data);
              req.__jsonSession = JSON.stringify(data);
              app.emit('load_session', sessId, req.session);
              callback.call(self);

            } else { // Else if session hash doesn't match

              // This could be somebody trying to impersonate as someone else

              // a) Remove the cookies from client, and redirect to login page
              req.removeCookies(self.config.sessCookie, self.config.hashCookie);
              app.login(res);

              // Log message
              var logMessage = util.format("\n\
SECURITY WARNING: %s tried to access session '%s' with hash: '%s'\n\
Request Method: %s,\n\
Request Headers: \n%s\n", req.socket.remoteAddress, sessId, sessHash, req.method, util.inspect(req.headers));

              // b) Log security event
              app.log(logMessage);

              // c) Emit security warning
              app.emit('security_warning', 'session', { // context, data
                sessId: sessId,
                sessHash: sessHash,
                fingerprint: fingerprint,
                sessionData: data
              });

            }

          } else { // Else (session hash not detected)

            userAgent = req.headers['user-agent'] || self.config.defaultUserAgent;
            ua_md5 = self.md5(userAgent);

            if (ua_md5 == data.ua_md5) { // If user agent's md5 matches the one in session
              hashes = self.createHash(userAgent);
              newSess = hashes.sessId;
              newHash = hashes.fingerprint;

              /*
              Is data persistent ? => use permanentExpires
              Otherwise:
              -- Is it a user session ? => use temporaryExpires
              -- Otherwise => use guestExpires
              */

              expires = self.config[(data.pers ? 'permanentExpires' : (data.user ? 'temporaryExpires' : 'guestExpires'))];

              multi = self.storage.multi();
              multi.updateHash(sessId, {fpr: newHash});
              multi.rename(sessId, newSess);
              multi.expire(newSess, expires);

              multi.exec(function(err, replies) {
                if (err) {
                  app.serverError(res, err);
                } else {
                  res.setCookie(self.config.sessCookie, newSess, {
                    expires: expires
                  });
                  res.setCookie(self.config.hashCookie, newHash, {
                    expires: self.config.regenInterval
                  });
                  req.__cookies[self.config.sessCookie.toLowerCase()] = newSess;
                  data.fpr = req.__cookies[self.config.hashCookie.toLowerCase()] = newHash;
                  req.session = data;
                  req.__origSessionState = _.extend({}, data);
                  req.__jsonSession = JSON.stringify(data);
                  app.emit('load_session', sessId, req.session);
                  app.debug('Regenerating session');
                  callback.call(self);
                }
              });

            } else { // Else (if user agent doesn't match the one in session)

              // => Remove cookies from client, and redirect to login
              res.removeCookies(self.config.sessCookie, self.config.hashCookie);
              app.login(res);

            }
          }
        }

      });

    } else if (this.config.guestSessions) { // If guestSessions are enabled

      res.removeCookie(this.config.hashCookie);
      this.createGuestSession(req, res, callback);

    } else { // sessId is not present

      // Remove session hash if it's present
      if (sessHash) res.removeCookie(this.config.hashCookie);

      req.session = req.__origSessionState = {};
      req.__jsonSession = '';

      app.emit('load_session', sessId, req.session);
      callback.call(self);
    }
  }

/**
  Creates a guest session

  @param {object} req
  @param {object} res
  @param {function} callback
  @public
*/

Session.prototype.createGuestSession = function(req, res, callback) {
  var self = this;
  this.create(req, res, {guest: '1'}, "guest", function(data, hashes, expires) {
    return callback.call(self, data, hashes, expires);
  });
}

/**
  Generates a session fingerprint for a given session ID

  @param {object} req
  @param {string} sessId
  @returns {string} fingerprint hash
  @private
*/

Session.prototype.getFingerprint = function(req, sessId) {
  var userAgent = (req.headers['user-agent'] || this.config.defaultUserAgent);
  return this.md5(userAgent + sessId + this.config.salt);
}

/**
  Generates an MD5 hash of a given string

  @param {string} string
  @returns {string} md5 hash
  @private
*/

Session.prototype.md5 = function(string) {
  return crypto.createHash('md5').update(string).digest('hex');
}

/**
  Creates a session hash

  @param {string} userAgent
  @param {boolean} guest
  @returns {object}
  @private
*/

Session.prototype.createHash = function(userAgent, guest) {
    var sessId = this.md5(node_uuid());
    if (guest) {
      return {sessId: sessId};
    } else {
      var fingerprint = this.md5(userAgent + sessId + this.config.salt);
      return {sessId: sessId, fingerprint: fingerprint};
    }
}

/**
  Performs automatic type coercion on session data.

  The session variables that will be converted, are specified in the `typecastVars` array.

  @param {object} data
  @returns {object} with data converted
  @private
*/

Session.prototype.typecast = function(data) {
  var tvars = this.config.typecastVars;
  for (var key,i=0; i < tvars.length; i++) {
    key = tvars[i];
    if (data[key] != null) data[key] = corejs.util.typecast(data[key]);
  }
  return data;
}

/*
--------------------------------------
IncomingMessage prototype augmentation
--------------------------------------
*/

/**
  Saves the session if it has changed

  @param {function} callback
  @private
*/

IncomingMessage.prototype.saveSessionState = function(callback) {

  var expires,
  app = this.app;

  if (! app.supports.session) {
    callback.call(app);
    return;
  }

  var self = this,
  session = this.session,
  multi = app.session.storage.multi(),
  sessId = this.getCookie(app.session.config.sessCookie);

  if (session.user != null) {
    expires = (session.pers ? app.session.config.permanentExpires : app.session.config.temporaryExpires);
  } else {
    expires = app.session.config.guestExpires;
  }

  multi.setHash(sessId, session);

  for (var key in this.__origSessionState) {
    if (session[key] == null) multi.deleteFromHash(sessId, key);
  }

  multi.expire(sessId, expires);

  multi.exec(function(err, replies) {
    if (err) {
      app.serverError(self.response, util.format("Unable to save session state: %s" + err.toString()));
    } else {
      callback.call(self, app);
    }
  });
}

/**
  Checks if the session has changed

  @returns {boolean}
  @private
*/

IncomingMessage.prototype.sessionChanged = function() {
  if (!this.app.supports.session) return false;
  var curSessionJson = JSON.stringify(this.session);
  return (this.hasCookie(this.app.session.config.sessCookie)
  && curSessionJson !== this.__jsonSession
  && curSessionJson !== '{}');
}

/*
--------------------------------------
OutgoingMessage prototype augmentation
--------------------------------------
*/

/**
  Replaces the default end method, providing support for

  @params {*mixed}
*/

OutgoingMessage.prototype.end = function() {
  var args = slice.call(arguments, 0),
  self = this,
  req = this.request,
  app = this.app;

  if (typeof app == 'object' && app.supports.session && req.sessionChanged()) {
    req.saveSessionState(function() {
      _end.apply(self, args);
    });
  } else {
    _end.apply(this, args);
  }
}

module.exports = Session;
<commit_msg>Refactored session middleware<commit_after>
/*
  Session
 */

var app = corejs.app;

var _ = require('underscore'),
  util = require('util'),
  crypto = require('crypto'),
  node_uuid = require('node-uuid'),
  slice = Array.prototype.slice,
  IncomingMessage = require('http').IncomingMessage,
  OutgoingMessage = require('http').OutgoingMessage;

var _end = OutgoingMessage.prototype.end;

function Session(config) {

  config = config || {};

  // Middleware configuration defaults
  this.config = _.extend({
    guestSessions: false,                 // Guest sessions support
    regenInterval: 5 * 60,                // Interval to regenerate the sessionId
    permanentExpires: 30 * 24 * 3600,     // Permanent sessions timeout
    temporaryExpires: 24 * 3600,          // Temporary (browser) sessions timeout
    guestExpires: 7 * 24 * 3600,          // Guest sessions timeout
    typecastVars: [],                     // Session variables to automatically typecast
    sessCookie: "_sess",                  // Session Cookie
    hashCookie: "_shash",                 // Session hash
    defaultUserAgent: "Mozilla",          // Default User Agent
    salt: "$28b28fc2ebcd355ca1a2be8881e888a.67a42975e1626d59434e576b5c63f3483!" // Session Salt
    }, config);

    if (typeof config.storage == 'object') this.storage = config.storage;
    else if (typeof config.storage == 'string') this.storage = app.getResource('storages/' + config.storage);
    else throw new Error('Session requires a storage');

    this.className = this.constructor.name;

    app.debug('Session enabled');

    corejs.util.onlySetEnumerable(this, ['className', 'storage']);
  }

  Session.prototype.storage = null;

/**
  Creates a session

  @param {object} req
  @param {object} res
  @param {object} data
  @param {boolean} persistent
  @param {function} callback
  @public
*/

Session.prototype.create = function(req, res, data, persistent, callback) {
  var expires, guest, hashes, multi, userAgent, userAgentMd5, self = this;
  guest = null;
  if (persistent == 'guest') {
    guest = true;
    persistent = true;
  }
  app.debug( guest ? 'Creating guest session' : 'Creating session' );
  userAgent = req.headers['user-agent'] || this.config.defaultUserAgent;
  userAgentMd5 = this.md5(userAgent);
  hashes = this.createHash(userAgent, guest);

  if (guest) {
    // Guest sessions have their own expiration timeout
    expires = this.config.guestExpires;
  } else {
    // Is the session persistent ? If yes, timeout should be permanentExpires
    // Otherwise, timeout should be temporaryExpires (browser session)
    expires = (persistent ? this.config.permanentExpires : this.config.temporaryExpires);

    data = _.extend(data, {
      fpr: hashes.fingerprint,
      ua_md5: userAgentMd5,
      pers: (persistent ? 1 : 0)
    });
  }

  multi = this.storage.multi();
  if (!guest && req.session.guest && req.hasCookie(this.config.sessCookie)) {
    multi.delete(req.getCookie(this.config.sessCookie));
  }
  multi.setHash(hashes.sessId, data);
  multi.expire(hashes.sessId, expires);

  multi.exec(function(err, replies) {
    if (err) app.log(err);
    else {

      // Expires has been calculated a few lines back
      res.setCookie(self.config.sessCookie, hashes.sessId, {
        expires: expires
      });

      if (guest) {
        data.guest = parseInt(data.guest, 10);
      } else {
        res.setCookie(self.config.hashCookie, hashes.fingerprint, {
          expires: self.config.regenInterval
        });
      }

      data = self.typecast(data);
      req.session = data;
      req.__origSessionState = _.extend({}, data);
      req.__jsonSession = JSON.stringify(data);
      app.emit('load_session', hashes.sessId, req.session);
      callback.call(self, req.session, hashes, expires);
    }
  });
}

/**
  Destroys a session

  @param {object} req
  @param {object} res
  @param {function} callback
  @public
*/

Session.prototype.destroy = function(req, res, callback) {
  var fingerprint, sessId, self = this;
  app.debug('Destroying session');
  if (req.hasCookie(this.config.sessCookie) && req.session) {
    sessId = req.getCookie(this.config.sessCookie);
    fingerprint = this.getFingerprint(req, sessId);

    if (fingerprint == req.session.fpr) {
      this.storage.delete(sessId, function(err) {
        if (err) app.serverError(res, err);
        else {
          res.removeCookies(self.config.sessCookie, self.config.hashCookie);
          callback.call(self);
        }
      });
    } else {
      res.removeCookies(this.config.sessCookie, this.config.hashCookie);
      app.login(res);
    }
  } else {
    app.login(res);
  }
}

/**
  Loads the session

  @param {object} req
  @param {object} res
  @param {function} callback
  @private
*/

Session.prototype.loadSession = function(req, res, callback) {
  var fingerprint, sessHash, sessId, self;

  if (req.__loadedSession === true) {
    callback.call(this);
    return;
  } else {
    req.__loadedSession = true;
  }

  self = this;

  sessId = req.getCookie(this.config.sessCookie);
  sessHash = req.getCookie(this.config.hashCookie);
  fingerprint = self.getFingerprint(req, sessId);

  if (sessId) { // A cookie with sessId exists

    // Get the session data from storage
    this.storage.getHash(sessId, function(err, data) {
      var expires, guest, hashes, multi, newHash, newSess, ua_md5, userAgent;

      // If errors retrieving session data, respond with HTTP/500 & log error
      if (err) { app.serverError(res, err); return;}

      // If it's not a user session, it's a guest session
      guest = (data.user == null);

      if (_.isEmpty(data)) { // If data is empty

        if (self.config.guestSessions) {
          // => Create guest session
          res.removeCookie(self.config.hashCookie);
          self.createGuestSession(req, res, callback);
        } else {
          // => Remove cookies, contain invalid or expired session data
          req.removeCookies(self.config.sessCookie, self.config.hashCookie);
          callback.call(self, {});
        }

      } else { // Data is not empty

        if (guest) data.guest = parseInt(data.guest, 10);
        else  data.pers = parseInt(data.pers, 10);

        data = self.typecast(data);

        if (guest) { // If guest session detected

          app.debug('Loading guest session');
          req.session = data;
          req.__origSessionState = _.extend({}, data);
          req.__jsonSession = JSON.stringify(data);
          app.emit('load_session', sessId, req.session);
          callback.call(self);

          } else if (sessHash) { // If session hash detected

            if (sessHash == fingerprint && sessHash == data.fpr) { // If cookie hash matches session hash

              // Load user session
              app.debug('Loading session');
              req.session = data;
              req.__origSessionState = _.extend({}, data);
              req.__jsonSession = JSON.stringify(data);
              app.emit('load_session', sessId, req.session);
              callback.call(self);

            } else { // Else if session hash doesn't match

              // This could be somebody trying to impersonate as someone else

              // a) Remove the cookies from client, and redirect to login page
              req.removeCookies(self.config.sessCookie, self.config.hashCookie);
              app.login(res);

              // Log message
              var logMessage = util.format("\n\
SECURITY WARNING: %s tried to access session '%s' with hash: '%s'\n\
Request Method: %s,\n\
Request Headers: \n%s\n", req.socket.remoteAddress, sessId, sessHash, req.method, util.inspect(req.headers));

              // b) Log security event
              app.log(logMessage);

              // c) Emit security warning
              app.emit('security_warning', 'session', { // context, data
                sessId: sessId,
                sessHash: sessHash,
                fingerprint: fingerprint,
                sessionData: data
              });

            }

          } else { // Else (session hash not detected)

            userAgent = req.headers['user-agent'] || self.config.defaultUserAgent;
            ua_md5 = self.md5(userAgent);

            if (ua_md5 == data.ua_md5) { // If user agent's md5 matches the one in session
              hashes = self.createHash(userAgent);
              newSess = hashes.sessId;
              newHash = hashes.fingerprint;

              /*
              Is data persistent ? => use permanentExpires
              Otherwise:
              -- Is it a user session ? => use temporaryExpires
              -- Otherwise => use guestExpires
              */

              expires = self.config[(data.pers ? 'permanentExpires' : (data.user ? 'temporaryExpires' : 'guestExpires'))];

              multi = self.storage.multi();
              multi.updateHash(sessId, {fpr: newHash});
              multi.rename(sessId, newSess);
              multi.expire(newSess, expires);

              multi.exec(function(err, replies) {
                if (err) {
                  app.serverError(res, err);
                } else {
                  res.setCookie(self.config.sessCookie, newSess, {
                    expires: expires
                  });
                  res.setCookie(self.config.hashCookie, newHash, {
                    expires: self.config.regenInterval
                  });
                  req.__cookies[self.config.sessCookie.toLowerCase()] = newSess;
                  data.fpr = req.__cookies[self.config.hashCookie.toLowerCase()] = newHash;
                  req.session = data;
                  req.__origSessionState = _.extend({}, data);
                  req.__jsonSession = JSON.stringify(data);
                  app.emit('load_session', sessId, req.session);
                  app.debug('Regenerating session');
                  callback.call(self);
                }
              });

            } else { // Else (if user agent doesn't match the one in session)

              // => Remove cookies from client, and redirect to login
              res.removeCookies(self.config.sessCookie, self.config.hashCookie);
              app.login(res);

            }
          }
        }

      });

    } else if (this.config.guestSessions) { // If guestSessions are enabled

      res.removeCookie(this.config.hashCookie);
      this.createGuestSession(req, res, callback);

    } else { // sessId is not present

      // Remove session hash if it's present
      if (sessHash) res.removeCookie(this.config.hashCookie);

      req.session = req.__origSessionState = {};
      req.__jsonSession = '';

      app.emit('load_session', sessId, req.session);
      callback.call(self);
    }
  }

/**
  Creates a guest session

  @param {object} req
  @param {object} res
  @param {function} callback
  @public
*/

Session.prototype.createGuestSession = function(req, res, callback) {
  var self = this;
  this.create(req, res, {guest: '1'}, "guest", function(data, hashes, expires) {
    return callback.call(self, data, hashes, expires);
  });
}

/**
  Generates a session fingerprint for a given session ID

  @param {object} req
  @param {string} sessId
  @returns {string} fingerprint hash
  @private
*/

Session.prototype.getFingerprint = function(req, sessId) {
  var userAgent = (req.headers['user-agent'] || this.config.defaultUserAgent);
  return this.md5(userAgent + sessId + this.config.salt);
}

/**
  Generates an MD5 hash of a given string

  @param {string} string
  @returns {string} md5 hash
  @private
*/

Session.prototype.md5 = function(string) {
  return crypto.createHash('md5').update(string).digest('hex');
}

/**
  Creates a session hash

  @param {string} userAgent
  @param {boolean} guest
  @returns {object}
  @private
*/

Session.prototype.createHash = function(userAgent, guest) {
    var sessId = this.md5(node_uuid());
    if (guest) {
      return {sessId: sessId};
    } else {
      var fingerprint = this.md5(userAgent + sessId + this.config.salt);
      return {sessId: sessId, fingerprint: fingerprint};
    }
}

/**
  Performs automatic type coercion on session data.

  The session variables that will be converted, are specified in the `typecastVars` array.

  @param {object} data
  @returns {object} with data converted
  @private
*/

Session.prototype.typecast = function(data) {
  var tvars = this.config.typecastVars;
  for (var key,i=0; i < tvars.length; i++) {
    key = tvars[i];
    if (data[key] != null) data[key] = corejs.util.typecast(data[key]);
  }
  return data;
}

/*
--------------------------------------
IncomingMessage prototype augmentation
--------------------------------------
*/

/**
  Saves the session if it has changed

  @param {function} callback
  @private
*/

IncomingMessage.prototype.saveSessionState = function(callback) {

  var expires;

  if (! app.supports.session) {
    callback.call(app);
    return;
  }

  var self = this,
  session = this.session,
  multi = app.session.storage.multi(),
  sessId = this.getCookie(app.session.config.sessCookie);

  if (session.user != null) {
    expires = (session.pers ? app.session.config.permanentExpires : app.session.config.temporaryExpires);
  } else {
    expires = app.session.config.guestExpires;
  }

  multi.setHash(sessId, session);

  for (var key in this.__origSessionState) {
    if (session[key] == null) multi.deleteFromHash(sessId, key);
  }

  multi.expire(sessId, expires);

  multi.exec(function(err, replies) {
    if (err) {
      app.serverError(self.response, util.format("Unable to save session state: %s" + err.toString()));
    } else {
      callback.call(self, app);
    }
  });
}

/**
  Checks if the session has changed

  @returns {boolean}
  @private
*/

IncomingMessage.prototype.sessionChanged = function() {
  if (!app.supports.session) return false;
  var curSessionJson = JSON.stringify(this.session);
  return (this.hasCookie(app.session.config.sessCookie)
  && curSessionJson !== this.__jsonSession
  && curSessionJson !== '{}');
}

/*
--------------------------------------
OutgoingMessage prototype augmentation
--------------------------------------
*/

/**
  Replaces the default end method, providing support for

  @params {*mixed}
*/

OutgoingMessage.prototype.end = function() {
  var args = slice.call(arguments, 0),
  self = this,
  req = this.request;

  if (app.supports.session && req.sessionChanged()) {
    req.saveSessionState(function() {
      _end.apply(self, args);
    });
  } else {
    _end.apply(this, args);
  }
}

module.exports = Session;
<|endoftext|>


<commit_before>/*
	---
	description: Scrollbar element
	authors: [moolego,r2d2]
	requires:
	- core:1.2.1: '*'

	...
 */
/*
	Class: UI.Scroll
	Manage scrolls for views.

	Extend:
	<UI.Component>

	Arguments:
	options

	Options:
	- width - (integer) The scollbar track width
	- maxThumbSize - (integer)
	- wheel - (integer) The scroll increment

	Example:
	(start code)
	var scrollbar = new UI.Scroll({
	container	: this.content
	});
	(end)

	Implied global:
	- MooLego - UI
	- MooTools -Class
	- Javascript - document

	Credits:
	based on Valerio's Mootools scrollbar plugin.
	found in upload folder of mootools website
*/
UI.VirtualScroll = new Class({

	Extends: UI.Component,

	options: {
		name: 'scrollbar',
		klass: 'ui-scrollbar',
		type: 'track',

		//maxThumbSize: 8,
		minThumbSize: 8,
		wheel: 16,
		//autoHide: 1000,
		size: 8
	},
	/*
		Constructor: initialize
			Construtor

		Arguments:
			options - (object) options

		See also:
			<UI.Component::initialize>
	 */
	initialize: function(options){

		this.parent(options);

		this.bound = {
			start: this.start.bind(this),
			end: this.end.bind(this),
			drag: this.drag.bind(this),
			wheel: this.wheel.bind(this),
			//page: this.page.bind(this)
		};

		//console.log('initalize',this.options.container,'before');

		this.container = this.options.container;
		this.position = {};
		this.mouse = {};
		this.update();
		this.attachEvent();
	},
	/*
		Method: _initElement
			private method

			Make a  Text and set the fade Fx

		Return:
			(void)

		See also:
			<UI.Component::_initElement>
	 */
	_initElement: function(){
		if (!this.options.container)
			return;

		this.options.width = this.options.size;

		this.parent();

		this.element.inject(this.options.container,'before');

		this.thumb = new UI.Component({
			name: 'thumb',
			klass: 'ui-thumb'
		}).inject(this.element);

		this.index = 0;
	},

	update: function(){
		var opts = this.options,
			containerSize = opts.virtual.containerSize;

		this.element.setStyle('height', containerSize + 'px');

		this.containerSize = containerSize;

		this.thumbSize = containerSize / opts.virtual.scrollHeight;

		this.thumbSize = parseInt(this.thumbSize * containerSize, 10);

		this.thumbPos = this.index * opts.virtual.elSize * containerSize;
		this.thumbPos = this.thumbPos / opts.virtual.scrollHeight;

		if (this.thumbSize < opts.minThumbSize)
			this.thumbSize = opts.minThumbSize;

		if (this.thumbSize < containerSize) {
			this.thumb.setSize(opts.width, this.thumbSize);
			this.thumb.element.setStyle('visibility', 'visible');
		}
		else
			this.thumb.element.setStyle('visibility', 'hidden');

		if(opts.virtual.listLength < opts.virtual.countLoad * 3)
			thumbPos = this.options.container.scrollTop;

		this.thumb.element.setStyle('top', this.thumbPos.limit(0, containerSize - this.thumbSize) + 'px');

	},

	attachEvent: function(){
		this.thumb.element.addEvent('mousedown', this.bound.start);

		if (this.options.wheel) {
			this.options.container.addEvent('mousewheel', this.bound.wheel);
		}
		this.element.addEvent('mouseup', this.bound.page);
	},

	wheel: function(event){
		var opts = this.options.virtual,
			thumbSize = this.thumb.getSize().y,
			thumbPos = (opts.elSize * this.containerSize * this.index) / (opts.listLength * opts.elSize);
			countLoad = opts.countLoad;

		if (this.index < 0)
			this.index = 0;

		if (event.wheel > 0) {
			if (event.target.getPrevious())
				event.target.getPrevious().fireEvent('mouseenter');

			for (var i = 1; i <= opts.sensibility; i++) {
				this.index--;

				if (this.index + countLoad < opts.listLength - countLoad && this.index + 1 > countLoad)
					this.fireEvent('scrolling', {ev: event, index: this.index});
				else
					this.container.scrollTop -= opts.scrollSensibility;
			}
		}
		else if (this.index < opts.listLength) {
			if (event.target.getNext())
				event.target.getNext().fireEvent('mouseenter');

			for (var j = 1; j <= opts.sensibility; j++) {
				if(this.index >= opts.listLength - (countLoad * 2))
					this.container.scrollTop += opts.scrollSensibility;
				else{
					this.index++;
					this.container.scrollTop += opts.scrollSensibility;
				}

				if (this.index > countLoad && this.index < opts.listLength - (countLoad * 2))
					this.fireEvent('scrolling', {ev: event, index: this.index});
			}
		}

		if(opts.listLength < countLoad * 3)
			thumbPos = this.options.container.scrollTop;

		this.thumb.element.setStyle('top', thumbPos.limit(0, this.container.getSize().y - this.thumbSize) + 'px');

		event.stop();
	},

	start: function(event){
		this.mouse.start = event.page.y;
		this.position.start = this.thumb.element.getStyle('top').toInt();

		document.addEvent('mousemove', this.bound.drag);
		document.addEvent('mouseup', this.bound.end);
		this.thumb.element.addEvent('mouseup', this.bound.end);

		event.stop();
	},

	end: function(event){
		document.removeEvent('mousemove', this.bound.drag);
		document.removeEvent('mouseup', this.bound.end);
		this.thumb.element.removeEvent('mouseup', this.bound.end);
		this.fireEvent('dragCompplete', this.index);
		event.stop();
	},

	toBottom: function(){
		var opts = this.options.virtual,
			countLoad = opts.countLoad;

		this.container.scrollTop = (countLoad * 3) * opts.elSize;
	},

	drag: function(event){
		var opts = this.options.virtual,
			thumbSize = this.thumb.getSize().y,
			countLoad = opts.countLoad,
			evPos = event.page.y - this.container.getPosition().y,
			newIndex = evPos * (opts.listLength * opts.elSize);

		this.index = parseInt( (newIndex / this.content.getSize().y) / opts.elSize, 10 );

		if (evPos + thumbSize >= this.container.getSize().y) // bottom limit
			this.index = opts.listLength;
		else if (evPos <= 0 || this.index <= countLoad) // top limit
			this.index = 0;

		this.thumb.element.setStyle('top', evPos.limit(0, this.container.getSize().y - thumbSize) + 'px');

		this.fireEvent('drag', this.index);

		event.stop();
	}

});<commit_msg>Work on virtual scroll<commit_after>/*
	---
	description: Scrollbar element
	authors: [moolego,r2d2]
	requires:
	- core:1.2.1: '*'

	...
 */
/*
	Class: UI.Scroll
	Manage scrolls for views.

	Extend:
	<UI.Component>

	Arguments:
	options

	Options:
	- width - (integer) The scollbar track width
	- maxThumbSize - (integer)
	- wheel - (integer) The scroll increment

	Example:
	(start code)
	var scrollbar = new UI.Scroll({
	container	: this.content
	});
	(end)

	Implied global:
	- MooLego - UI
	- MooTools -Class
	- Javascript - document

	Credits:
	based on Valerio's Mootools scrollbar plugin.
	found in upload folder of mootools website
*/
UI.VirtualScroll = new Class({

	Extends: UI.Component,

	options: {
		name: 'scrollbar',
		klass: 'ui-scrollbar',
		type: 'track',

		//maxThumbSize: 8,
		minThumbSize: 8,
		wheel: 16,
		//autoHide: 1000,
		size: 8
	},
	/*
		Constructor: initialize
			Construtor

		Arguments:
			options - (object) options

		See also:
			<UI.Component::initialize>
	 */
	initialize: function(options){

		this.parent(options);

		this.bound = {
			start: this.start.bind(this),
			end: this.end.bind(this),
			drag: this.drag.bind(this),
			wheel: this.wheel.bind(this),
			//page: this.page.bind(this)
		};

		//console.log('initalize',this.options.container,'before');

		this.container = this.options.container;
		this.position = {};
		this.mouse = {};
		this.update();
		this.attachEvent();
	},
	/*
		Method: _initElement
			private method

			Make a  Text and set the fade Fx

		Return:
			(void)

		See also:
			<UI.Component::_initElement>
	 */
	_initElement: function(){
		if (!this.options.container)
			return;

		this.options.width = this.options.size;

		this.parent();

		this.element.inject(this.options.container,'before');

		this.thumb = new UI.Component({
			name: 'thumb',
			klass: 'ui-thumb'
		}).inject(this.element);

		this.index = 0;
	},

	update: function(){
		var opts = this.options,
			containerSize = opts.virtual.containerSize;

		this.element.setStyle('height', containerSize + 'px');

		this.containerSize = containerSize;

		this.thumbSize = containerSize / opts.virtual.scrollHeight;
		this.thumbSize = parseInt(this.thumbSize * containerSize, 10);

		this.thumbPos = this.index * opts.virtual.elSize * containerSize;
		this.thumbPos = this.thumbPos / (opts.virtual.listLength * opts.virtual.elSize);

		/*this.thumbPos = this.index * opts.virtual.elSize * containerSize;
		this.thumbPos = this.thumbPos / opts.virtual.scrollHeight;*/

		if (this.thumbSize < opts.minThumbSize)
			this.thumbSize = opts.minThumbSize;

		if (this.thumbSize < containerSize) {
			this.thumb.setSize(opts.width, this.thumbSize);
			this.thumb.element.setStyle('visibility', 'visible');
		}
		else
			this.thumb.element.setStyle('visibility', 'hidden');

		if(opts.virtual.listLength < opts.virtual.countLoad * 3){
			thumbPos = 0;
			this.options.container.scrollTop = 0
		}

		this.thumb.element.setStyle('top', this.thumbPos.limit(0, containerSize - this.thumbSize) + 'px');

	},

	attachEvent: function(){
		this.thumb.element.addEvent('mousedown', this.bound.start);

		if (this.options.wheel) {
			this.options.container.addEvent('mousewheel', this.bound.wheel);
		}
		this.element.addEvent('mouseup', this.bound.page);
	},

	wheel: function(event){
		var opts = this.options.virtual,
			thumbSize = this.thumb.getSize().y,
			thumbPos = (opts.elSize * this.containerSize * this.index) / (opts.listLength * opts.elSize);
			countLoad = opts.countLoad;

		if (this.index < 0)
			this.index = 0;

		if (event.wheel > 0) {
			if (event.target.getPrevious())
				event.target.getPrevious().fireEvent('mouseenter');

			for (var i = 1; i <= opts.sensibility; i++) {
				this.index--;

				if (this.index + countLoad < opts.listLength - countLoad && this.index + 1 > countLoad)
					this.fireEvent('scrolling', {ev: event, index: this.index});
				else
					this.container.scrollTop -= opts.scrollSensibility;
			}
		}
		else if (this.index < opts.listLength) {
			if (event.target.getNext())
				event.target.getNext().fireEvent('mouseenter');

			for (var j = 1; j <= opts.sensibility; j++) {
				if(this.index >= opts.listLength - (countLoad * 2))
					this.container.scrollTop += opts.scrollSensibility;
				else{
					this.index++;
					this.container.scrollTop += opts.scrollSensibility;
				}

				if (this.index > countLoad && this.index < opts.listLength - (countLoad * 2))
					this.fireEvent('scrolling', {ev: event, index: this.index});
			}
		}

		if(opts.listLength < countLoad * 3)
			thumbPos = this.options.container.scrollTop;

		this.thumb.element.setStyle('top', thumbPos.limit(0, this.container.getSize().y - this.thumbSize) + 'px');

		event.stop();
	},

	start: function(event){
		this.mouse.start = event.page.y;
		this.position.start = this.thumb.element.getStyle('top').toInt();

		document.addEvent('mousemove', this.bound.drag);
		document.addEvent('mouseup', this.bound.end);
		this.thumb.element.addEvent('mouseup', this.bound.end);

		event.stop();
	},

	end: function(event){
		document.removeEvent('mousemove', this.bound.drag);
		document.removeEvent('mouseup', this.bound.end);
		this.thumb.element.removeEvent('mouseup', this.bound.end);
		this.fireEvent('dragCompplete', this.index);
		event.stop();
	},

	toBottom: function(){
		var opts = this.options.virtual,
			countLoad = opts.countLoad;

		this.container.scrollTop = (countLoad * 3) * opts.elSize;
	},

	drag: function(event){
		var opts = this.options.virtual,
			thumbSize = this.thumb.getSize().y,
			countLoad = opts.countLoad,
			evPos = event.page.y - this.container.getPosition().y,
			newIndex = evPos * (opts.listLength * opts.elSize);

		this.index = parseInt( (newIndex / this.content.getSize().y) / opts.elSize, 10 );

		if (evPos + thumbSize >= this.container.getSize().y) // bottom limit
			this.index = opts.listLength;
		else if (evPos <= 0 || this.index <= countLoad) // top limit
			this.index = 0;

		this.thumb.element.setStyle('top', evPos.limit(0, this.container.getSize().y - thumbSize) + 'px');

		this.fireEvent('drag', this.index);

		event.stop();
	}

});<|endoftext|>


<commit_before>/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 *
 * $LastChangedDate: 2007-07-21 18:45:56 -0500 (Sat, 21 Jul 2007) $
 * $Rev: 2447 $
 *
 * Version 2.1.1
 */
(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+'style="display:block;position:absolute;z-index:-1;'+(s.opacity!==false?'filter:Alpha(Opacity=\'0\');':'')+'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+'"/>';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})(jQuery);<commit_msg>[微件]删除重复文件<commit_after><|endoftext|>


<commit_before>'use strict';

zr.controller('NavbarController', ['$scope', 'drive', 'zrdb', '$location', '$timeout', 'config',
	function ($scope, drive, zrdb, $location, $timeout, config) {
		gapi.client.setApiKey(config.simpleApiKey);

		 $scope.clientId = config.appId;
		 $scope.scopes = config.scopes;

		 $scope.activeTournaments = [];
		 zrdb.getAllResources('tournament').then(function(data) {
			var allTournaments = data.data.rows;
			var now = Date.now();
			for(var i = allTournaments.length; i--; ) {
				var t = allTournaments[i];
				if(t.tournamentEndDate > now) {
					$scope.activeTournaments.push(t);
				}
			}
		 });

		 var firstTime = true;
		 $scope.getUser = function() {
			drive.getUser(function(data) {
				$timeout(function() {
			 		if(data && data.displayName) {
			 			$scope.displayName = data.displayName;
			 		}
			 		else {
			 			$scope.displayName = null;
			 			if(firstTime) {
							$timeout(function() {
								gapi.signin.render('login-button', {
									'width': 'wide'
								});
							});
							firstTime = false;
			 			}
			 		}
			 		$scope.showLogin = true;
				});
			 });
		 };
		 $scope.getUser();

		$scope.newProject = function() {
			drive.newProject();
		};

		$scope.openProject = function() {
			drive.openProject(function(id, name) {
				$timeout(function() {
					$location.url('/ide/' + id + '/');
				});
			});
		};

		$scope.signOut = function() {
			gapi.auth.signOut();
			$location.path('/');
		}

		//Give global reference for login callback
		zr.navbarScope = $scope;
}]);
<commit_msg>Don't render auth button until auth has failed once<commit_after>'use strict';

zr.controller('NavbarController', ['$scope', 'drive', 'zrdb', '$location', '$timeout', 'config',
	function ($scope, drive, zrdb, $location, $timeout, config) {
		gapi.client.setApiKey(config.simpleApiKey);

		 $scope.clientId = config.appId;
		 $scope.scopes = config.scopes;

		 $scope.activeTournaments = [];
		 zrdb.getAllResources('tournament').then(function(data) {
			var allTournaments = data.data.rows;
			var now = Date.now();
			for(var i = allTournaments.length; i--; ) {
				var t = allTournaments[i];
				if(t.tournamentEndDate > now) {
					$scope.activeTournaments.push(t);
				}
			}
		 });

		 var firstTime = true;
		 $scope.getUser = function() {
			drive.getUser(function(data) {
				$timeout(function() {
			 		if(data && data.displayName) {
			 			$scope.displayName = data.displayName;
			 		}
			 		else {
			 			$scope.displayName = null;
			 			if(firstTime) {
							$timeout(function() {
								gapi.signin.render('login-button', {
									'width': 'wide'
								});
							},0.05);
							firstTime = false;
			 			}
			 		}
			 		$scope.showLogin = true;
				});
			 });
		 };
		 $scope.getUser();

		$scope.newProject = function() {
			drive.newProject();
		};

		$scope.openProject = function() {
			drive.openProject(function(id, name) {
				$timeout(function() {
					$location.url('/ide/' + id + '/');
				});
			});
		};

		$scope.signOut = function() {
			gapi.auth.signOut();
			$location.path('/');
		}

		//Give global reference for login callback
		zr.navbarScope = $scope;
}]);
<|endoftext|>


<commit_before>// be aware that the months are numbered 0 through 11 numerically in JS
var progress_start = new Date(2014, 3, 1);
var progress_end = new Date(2014, 6 , 2);
var today = new Date();
var progress_position_pct = (today.getTime() - progress_start.getTime()) / (progress_end.getTime() - progress_start.getTime());
// check boundaries (0 - 100%)
progress_position_pct = (progress_position_pct > 1.0 || progress_position_pct < 0.0) ? Math.floor(Math.abs(progress_position_pct)) : progress_position_pct;

function calc_position(object)
{
  switch(object.type)
  {
    case 'div':
    var progress_duration_px = $(object.contId).width() - $(object.objId).width();
    $(object.objId).css('marginLeft', Math.round(progress_duration_px * progress_position_pct))
    break;
    case 'progressbar':
    $(object.objId).css('width', Math.round(progress_position_pct * 100) + '%');
    $(object.objId).find('span').text(Math.round(progress_position_pct * 100) + '% complete');
    break;
  }
}

function add_tooltips(blog_posts, contId)
{
  $('[data-toggle="tooltip"]').remove();
  // sort posts by time (reverse)
  blog_posts.sort(function(a,b){
    return a.date.getTime() - b.date.getTime();
  });

  blog_posts.forEach(function(post){
    var progress_position_pct = (post.date.getTime() - progress_start.getTime()) / (progress_end.getTime() - progress_start.getTime());
    // check boundaries (0 - 100%)
    progress_position_pct = (progress_position_pct > 1.0 || progress_position_pct < 0.0) ? Math.floor(Math.abs(progress_position_pct)) : progress_position_pct;
    var pos = Math.round($(contId).width() * progress_position_pct);
    // check if pos is occupied and move the next one at least a couple of pixels forward
    console.log(pos);
    if($('a[data-toggle="tooltip"]').length > 0)
    {
      pos = (pos - $('a[data-toggle="tooltip"]:first').position().left < 5) ? $('a[data-toggle="tooltip"]:first').position().left + 7 : pos;
      console.log('pos: ' + pos + ' / ' + $('a[data-toggle="tooltip"]:first').position().left);
    }

    $('').prependTo(contId).click(function(){
      window.location.href = post.url;
    });
  });
  $('[data-toggle="tooltip"]').tooltip();
};

$(function(){
        $(window).on('resize load', function(){
          calc_position({ objId: '#bus', contId: '#bus-container', type: 'div' });
          calc_position({ objId: '#progress_bar .progress-bar', type: 'progressbar' });
          // add_tooltips(blog_posts, '#progress_bar_wrapper');
        });
      });
<commit_msg>bus.js update<commit_after>// be aware that the months are numbered 0 through 11 numerically in JS
var progress_start = new Date(2014, 6, 7);
var progress_end = new Date(2014, 7 , 18);
var today = new Date();
var progress_position_pct = (today.getTime() - progress_start.getTime()) / (progress_end.getTime() - progress_start.getTime());
// check boundaries (0 - 100%)
progress_position_pct = (progress_position_pct > 1.0 || progress_position_pct < 0.0) ? Math.floor(Math.abs(progress_position_pct)) : progress_position_pct;

function calc_position(object)
{
  switch(object.type)
  {
    case 'div':
    var progress_duration_px = $(object.contId).width() - $(object.objId).width();
    $(object.objId).css('marginLeft', Math.round(progress_duration_px * progress_position_pct))
    break;
    case 'progressbar':
    $(object.objId).css('width', Math.round(progress_position_pct * 100) + '%');
    $(object.objId).find('span').text(Math.round(progress_position_pct * 100) + '% complete');
    break;
  }
}

function add_tooltips(blog_posts, contId)
{
  $('[data-toggle="tooltip"]').remove();
  // sort posts by time (reverse)
  blog_posts.sort(function(a,b){
    return a.date.getTime() - b.date.getTime();
  });

  blog_posts.forEach(function(post){
    var progress_position_pct = (post.date.getTime() - progress_start.getTime()) / (progress_end.getTime() - progress_start.getTime());
    // check boundaries (0 - 100%)
    progress_position_pct = (progress_position_pct > 1.0 || progress_position_pct < 0.0) ? Math.floor(Math.abs(progress_position_pct)) : progress_position_pct;
    var pos = Math.round($(contId).width() * progress_position_pct);
    // check if pos is occupied and move the next one at least a couple of pixels forward
    console.log(pos);
    if($('a[data-toggle="tooltip"]').length > 0)
    {
      pos = (pos - $('a[data-toggle="tooltip"]:first').position().left < 5) ? $('a[data-toggle="tooltip"]:first').position().left + 7 : pos;
      console.log('pos: ' + pos + ' / ' + $('a[data-toggle="tooltip"]:first').position().left);
    }

    $('').prependTo(contId).click(function(){
      window.location.href = post.url;
    });
  });
  $('[data-toggle="tooltip"]').tooltip();
};

$(function(){
        $(window).on('resize load', function(){
          calc_position({ objId: '#bus', contId: '#bus-container', type: 'div' });
          calc_position({ objId: '#progress_bar .progress-bar', type: 'progressbar' });
          // add_tooltips(blog_posts, '#progress_bar_wrapper');
        });
      });
<|endoftext|>


<commit_before>dojo.provide("dijit.Tooltip");

dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare(
	"dijit._MasterTooltip",
	[dijit._Widget, dijit._Templated],
	{
		// summary
		//		Internal widget that holds the actual tooltip markup,
		//		which occurs once per page.
		//		Called by Tooltip widgets which are just containers to hold
		//		the markup

		// duration: Integer
		//		Milliseconds to fade in/fade out
		duration: 200,

		templatePath: dojo.moduleUrl("dijit", "templates/Tooltip.html"),

		postCreate: function(){
			dojo.body().appendChild(this.domNode);

			this.bgIframe = new dijit.BackgroundIframe(this.domNode);

			// Setup fade-in and fade-out functions.
			this.fadeIn = dojo.fadeIn({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onShow") }),
			this.fadeOut = dojo.fadeOut({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onHide") });

		},

		show: function(/*String*/ innerHTML, /*DomNode*/ aroundNode){
			// summary:
			//	Display tooltip w/specified contents to right specified node
			//	(To left if there's no space on the right, or if LTR==right)

			if(this.aroundNode && this.aroundNode === aroundNode){
				return;
			}

			if(this.fadeOut.status() == "playing"){
				// previous tooltip is being hidden; wait until the hide completes then show new one
				this._onDeck=arguments;
				return;
			}
			this.containerNode.innerHTML=innerHTML;

			// Firefox bug. when innerHTML changes to be shorter than previous
			// one, the node size will not be updated until it moves.
			this.domNode.style.top = (this.domNode.offsetTop + 1) + "px";

			// position the element and change CSS according to position
			var align = this.isLeftToRight() ? {'BR': 'BL', 'BL': 'BR'} : {'BL': 'BR', 'BR': 'BL'};
			var pos = dijit.placeOnScreenAroundElement(this.domNode, aroundNode, align);
			this.domNode.className="dijitTooltip dijitTooltip" + (pos.corner=='BL' ? "Right" : "Left");

			// show it
			dojo.style(this.domNode, "opacity", 0);
			this.fadeIn.play();
			this.isShowingNow = true;
			this.aroundNode = aroundNode;
		},

		_onShow: function(){
			if(dojo.isIE){
				// the arrow won't show up on a node w/an opacity filter
				this.domNode.style.filter="";
			}
		},

		hide: function(aroundNode){
			// summary: hide the tooltip
			if(!this.aroundNode || this.aroundNode !== aroundNode){
				return;
			}
			if(this._onDeck){
				// this hide request is for a show() that hasn't even started yet;
				// just cancel the pending show()
				this._onDeck=null;
				return;
			}
			this.fadeIn.stop();
			this.isShowingNow = false;
			this.aroundNode = null;
			this.fadeOut.play();
		},

		_onHide: function(){
			this.domNode.style.cssText="";	// to position offscreen again
			if(this._onDeck){
				// a show request has been queued up; do it now
				this.show.apply(this, this._onDeck);
				this._onDeck=null;
			}
		}

	}
);

// Make a single tooltip markup on the page that is reused as appropriate
dojo.addOnLoad(function(){
	dijit.MasterTooltip = new dijit._MasterTooltip();
});

dojo.declare(
	"dijit.Tooltip",
	dijit._Widget,
	{
		// summary
		//		Pops up a tooltip (a help message) when you hover over a node.

		// label: String
		//		Text to display in the tooltip.
		//		Specified as innerHTML when creating the widget from markup.
		label: "",

		// showDelay: Integer
		//		Number of milliseconds to wait after hovering over/focusing on the object, before
		//		the tooltip is displayed.
		showDelay: 400,

		// connectId: String
		//		Id of domNode to attach the tooltip to.
		//		(When user hovers over specified dom node, the tooltip will appear.)
		connectId: "",

		postCreate: function(){
			this.srcNodeRef.style.display="none";

			this._connectNode = dojo.byId(this.connectId);

			dojo.forEach(["onMouseOver", "onMouseOut", "onFocus", "onBlur", "onHover", "onUnHover"], function(event){
				this.connect(this._connectNode, event.toLowerCase(), "_"+event);
			}, this);
		},

		_onMouseOver: function(/*Event*/ e){
			this._onHover(e);
		},

		_onMouseOut: function(/*Event*/ e){
			if(dojo.isDescendant(e.relatedTarget, this._connectNode)){
				// false event; just moved from target to target child; ignore.
				return;
			}
			this._onUnHover(e);
		},

		_onFocus: function(/*Event*/ e){
			this._focus = true;
			this._onHover(e);
		},

		_onBlur: function(/*Event*/ e){
			this._focus = false;
			this._onUnHover(e);
		},

		_onHover: function(/*Event*/ e){
			if(!this._showTimer){
				this._showTimer = setTimeout(dojo.hitch(this, "open"), this.showDelay);
			}
		},

		_onUnHover: function(/*Event*/ e){
			// keep a tooltip open if the associated element has focus
			if(this._focus){ return; }
			if(this._showTimer){
				clearTimeout(this._showTimer);
				delete this._showTimer;
			}
			this.close();
		},

		open: function(){
			// summary: display the tooltip; usually not called directly.
			if(this._showTimer){
				clearTimeout(this._showTimer);
				delete this._showTimer;
			}
			dijit.MasterTooltip.show(this.label || this.domNode.innerHTML, this._connectNode);
		},

		close: function(){
			// summary: hide the tooltip; usually not called directly.
			dijit.MasterTooltip.hide(this._connectNode);
		},

		uninitialize: function(){
			this.close();
		}
	}
);
<commit_msg>BiDi workaround for IE. Fixes #3893<commit_after>dojo.provide("dijit.Tooltip");

dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare(
	"dijit._MasterTooltip",
	[dijit._Widget, dijit._Templated],
	{
		// summary
		//		Internal widget that holds the actual tooltip markup,
		//		which occurs once per page.
		//		Called by Tooltip widgets which are just containers to hold
		//		the markup

		// duration: Integer
		//		Milliseconds to fade in/fade out
		duration: 200,

		templatePath: dojo.moduleUrl("dijit", "templates/Tooltip.html"),

		postCreate: function(){
			dojo.body().appendChild(this.domNode);

			this.bgIframe = new dijit.BackgroundIframe(this.domNode);

			// Setup fade-in and fade-out functions.
			this.fadeIn = dojo.fadeIn({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onShow") });
			this.fadeOut = dojo.fadeOut({ node: this.domNode, duration: this.duration, onEnd: dojo.hitch(this, "_onHide") });

		},

		show: function(/*String*/ innerHTML, /*DomNode*/ aroundNode){
			// summary:
			//	Display tooltip w/specified contents to right specified node
			//	(To left if there's no space on the right, or if LTR==right)

			if(this.aroundNode && this.aroundNode === aroundNode){
				return;
			}

			if(this.fadeOut.status() == "playing"){
				// previous tooltip is being hidden; wait until the hide completes then show new one
				this._onDeck=arguments;
				return;
			}
			this.containerNode.innerHTML=innerHTML;

			// Firefox bug. when innerHTML changes to be shorter than previous
			// one, the node size will not be updated until it moves.
			this.domNode.style.top = (this.domNode.offsetTop + 1) + "px";

			// position the element and change CSS according to position
			var align = this.isLeftToRight() ? {'BR': 'BL', 'BL': 'BR'} : {'BL': 'BR', 'BR': 'BL'};
			var pos = dijit.placeOnScreenAroundElement(this.domNode, aroundNode, align);
			this.domNode.className="dijitTooltip dijitTooltip" + (pos.corner=='BL' ? "Right" : "Left");//FIXME: might overwrite class

			// show it
			dojo.style(this.domNode, "opacity", 0);
			this.fadeIn.play();
			this.isShowingNow = true;
			this.aroundNode = aroundNode;
		},

		_onShow: function(){
			if(dojo.isIE){
				// the arrow won't show up on a node w/an opacity filter
				this.domNode.style.filter="";
			}
		},

		hide: function(aroundNode){
			// summary: hide the tooltip
			if(!this.aroundNode || this.aroundNode !== aroundNode){
				return;
			}
			if(this._onDeck){
				// this hide request is for a show() that hasn't even started yet;
				// just cancel the pending show()
				this._onDeck=null;
				return;
			}
			this.fadeIn.stop();
			this.isShowingNow = false;
			this.aroundNode = null;
			this.fadeOut.play();
		},

		_onHide: function(){
			this.domNode.style.cssText="";	// to position offscreen again
			if(this._onDeck){
				// a show request has been queued up; do it now
				this.show.apply(this, this._onDeck);
				this._onDeck=null;
			}
		}

	}
);

// Make a single tooltip markup on the page that is reused as appropriate
dojo.addOnLoad(function(){
	dijit.MasterTooltip = new dijit._MasterTooltip();
});

dojo.declare(
	"dijit.Tooltip",
	dijit._Widget,
	{
		// summary
		//		Pops up a tooltip (a help message) when you hover over a node.

		// label: String
		//		Text to display in the tooltip.
		//		Specified as innerHTML when creating the widget from markup.
		label: "",

		// showDelay: Integer
		//		Number of milliseconds to wait after hovering over/focusing on the object, before
		//		the tooltip is displayed.
		showDelay: 400,

		// connectId: String
		//		Id of domNode to attach the tooltip to.
		//		(When user hovers over specified dom node, the tooltip will appear.)
		connectId: "",

		postCreate: function(){
			this.srcNodeRef.style.display="none";

			this._connectNode = dojo.byId(this.connectId);

			if(dojo.isIE){
				// BiDi workaround
				this._connectNode.style.zoom = 1;
			}

			dojo.forEach(["onMouseOver", "onMouseOut", "onFocus", "onBlur", "onHover", "onUnHover"], function(event){
				this.connect(this._connectNode, event.toLowerCase(), "_"+event);
			}, this);
		},

		_onMouseOver: function(/*Event*/ e){
			this._onHover(e);
		},

		_onMouseOut: function(/*Event*/ e){
			if(dojo.isDescendant(e.relatedTarget, this._connectNode)){
				// false event; just moved from target to target child; ignore.
				return;
			}
			this._onUnHover(e);
		},

		_onFocus: function(/*Event*/ e){
			this._focus = true;
			this._onHover(e);
		},

		_onBlur: function(/*Event*/ e){
			this._focus = false;
			this._onUnHover(e);
		},

		_onHover: function(/*Event*/ e){
			if(!this._showTimer){
				this._showTimer = setTimeout(dojo.hitch(this, "open"), this.showDelay);
			}
		},

		_onUnHover: function(/*Event*/ e){
			// keep a tooltip open if the associated element has focus
			if(this._focus){ return; }
			if(this._showTimer){
				clearTimeout(this._showTimer);
				delete this._showTimer;
			}
			this.close();
		},

		open: function(){
			// summary: display the tooltip; usually not called directly.
			if(this._showTimer){
				clearTimeout(this._showTimer);
				delete this._showTimer;
			}
			dijit.MasterTooltip.show(this.label || this.domNode.innerHTML, this._connectNode);
		},

		close: function(){
			// summary: hide the tooltip; usually not called directly.
			dijit.MasterTooltip.hide(this._connectNode);
		},

		uninitialize: function(){
			this.close();
		}
	}
);
<|endoftext|>