diff --git a/.codeclimate.yml b/.codeclimate.yml index 2d3659da46..81b8bd3c4c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -5,4 +5,4 @@ languages: PHP: true exclude_paths: - "public/vendor/*" -- "tests/*" \ No newline at end of file +- "test/*" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..0799652254 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,20 @@ +node_modules/ +public/src/nodebb.min.js +*.sublime-project +*.sublime-workspace +.project +.vagrant +.DS_Store +logs/ +/public/templates +/public/uploads +/public/sounds +/public/vendor +/public/nodebb.min.js +/public/acp.min.js +/public/src/modules/string.js +.idea/ +.vscode/ +*.ipr +*.iws +/coverage diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000..ccbf4944a3 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,112 @@ +{ + "extends": "airbnb", + + "rules": { + "handle-callback-err": [ "error","^(e$|(e|(.*(_e|E)))rr)" ], + "linebreak-style": "off", + "one-var": "off", + "no-undef": "off", + "max-len": "off", + "no-new": "off", + "max-nested-callbacks": "off", + "no-mixed-requires": "off", + "brace-style": "off", + "max-statements-per-line": "off", + "no-unused-vars": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-concat": "off", + "require-jsdoc": "off", + "eqeqeq": "off", + "camelcase": "off", + "no-negated-condition": "off", + "one-var-declaration-per-line": "off", + "new-cap": "off", + "no-lonely-if": "off", + "radix": "off", + "no-else-return": "off", + "no-useless-escape": "off", + "block-scoped-var": "off", + "operator-assignment": "off", + "default-case": "off", + "yoda": "off", + "no-use-before-define": "off", + "no-loop-func": "off", + "no-void": "off", + "valid-jsdoc": "off", + "o-eq-null": "off", + "no-cond-assign": "off", + "no-eq-null": "off", + "no-redeclare": "off", + "no-unreachable": "off", + "no-nested-ternary": "off", + "operator-linebreak": "off", + "guard-for-in": "off", + "no-unneeded-ternary": "off", + "no-sequences": "off", + "no-extend-native": "off", + "no-shadow-restricted-names": "off", + "no-extra-boolean-cast": "off", + "no-undef-init": "off", + "no-script-url": "off", + "no-path-concat": "off", + "no-unused-expressions": "off", + "no-restricted-module": "off", + "no-return-assign": "off", + "no-restricted-modules": "off", + "no-tabs": "off", + "indent": "off", + "func-names": "off", + "prefer-arrow-callback": "off", + "object-curly-spacing": "off", + "no-var": "off", + "no-shadow": "off", + "prefer-template": "off", + "padded-blocks": "off", + "eol-last": "off", + "lines-around-directive": "off", + "space-before-blocks": "off", + "no-restricted-syntax": "off", + "vars-on-top": "off", + "no-prototype-builtins": "off", + "object-shorthand": "off", + "no-param-reassign": "off", + "consistent-return": "off", + "strict": "off", + "comma-dangle": "off", + "no-multi-spaces": "off", + "quotes": "off", + "keyword-spacing": "off", + "no-plusplus": "off", + "no-mixed-operators": "off", + "semi": "off", + "comma-spacing": "off", + "global-require": "off", + "no-trailing-spaces": "off", + "key-spacing": "off", + "import/newline-after-import": "off", + "no-underscore-dangle": "off", + "prefer-spread": "off", + "no-multiple-empty-lines": "off", + "spaced-comment": "off", + "prefer-rest-params": "off", + "space-in-parens": "off", + "block-spacing": "off", + "quote-props": "off", + "no-console": "off", + "space-unary-ops": "off", + "import/no-dynamic-require": "off", + "semi-spacing": "off", + "no-bitwise": "off", + "no-empty": "off", + "array-bracket-spacin": "off", + "dot-notation": "off", + "func-call-spacing": "off", + "newline-per-chained-call": "off", + "newline-per-chained-call": "off", + "array-bracket-spacing": "off", + "object-property-newline": "off", + "no-continue": "off", + "no-extra-semi": "off", + "no-spaced-func": "off" + } +} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..28dd965678 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +Please include the following information when submitting a bug report/issue: + +* NodeBB version and git hash (to find your git hash, execute `git rev-parse HEAD` from the main NodeBB directory) +* Database (mongo or redis) and it's version. +* Exact steps to cause this issue + 1. First I did this... + 2. Then, I clicked on this item... +* What you expected + * e.g. I expected *abc* to *xyz* +* What happened instead + * e.g. Instead, I got *zyx* and NodeBB set fire to my house + +Thank you! diff --git a/.gitignore b/.gitignore index a8ea9cb1eb..510c95a201 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ npm-debug.log node_modules/ sftp-config.json config.json +jsconfig.json public/src/nodebb.min.js !src/views/config.json public/css/*.css @@ -15,6 +16,7 @@ provision.sh *.komodoproject .DS_Store feeds/recent.rss +.eslintcache logs/ @@ -50,4 +52,4 @@ tx.exe .transifexrc ##Coverage output -coverage \ No newline at end of file +coverage diff --git a/.travis.yml b/.travis.yml index ffd3716c2a..e89064595e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,15 @@ before_install: - "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list" - "sudo apt-get update" - "sudo apt-get install mongodb-org-server" - - npm i --production - - node app --setup="{\"url\":\"http://127.0.0.1:4567/\",\"secret\":\"abcdef\",\"database\":\"mongo\",\"mongo:host\":\"127.0.0.1\",\"mongo:port\":27017,\"mongo:username\":\"\",\"mongo:password\":\"\",\"mongo:database\":0,\"redis:host\":\"127.0.0.1\",\"redis:port\":6379,\"redis:password\":\"\",\"redis:database\":0,\"admin:username\":\"admin\",\"admin:email\":\"test@example.org\",\"admin:password\":\"abcdef\",\"admin:password:confirm\":\"abcdef\"}" --ci="{\"host\":\"127.0.0.1\",\"port\":27017,\"database\":0}" -before_script: + - "npm i --production" + - sh -c "if [ '$DB' = 'mongodb' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567/\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"mongo\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":27017,\\\"database\\\":0}\"; fi" + - sh -c "if [ '$DB' = 'redis' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567/\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"redis\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":6379,\\\"database\\\":0}\"; fi" +before_script: - "until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done" language: node_js env: - - CXX=g++-4.8 + - CXX=g++-4.8 DB=mongodb + - CXX=g++-4.8 DB=redis addons: apt: sources: @@ -19,11 +21,9 @@ addons: packages: - g++-4.8 node_js: - - "4.2" - - "4.1" - - "4.0" - - "0.11" - - "0.10" + - "6" + - "5" + - "4" branches: only: - master \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index a6aca2083a..9f1585f301 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,16 +6,17 @@ var fork = require('child_process').fork, incomplete = []; -module.exports = function(grunt) { +module.exports = function (grunt) { + var args = []; + if (!grunt.option('verbose')) { + args.push('--log-level=info'); + } + function update(action, filepath, target) { - var args = [], + var updateArgs = args.slice(), fromFile = '', compiling = '', time = Date.now(); - - if (!grunt.option('verbose')) { - args.push('--log-level=info'); - } if (target === 'lessUpdated_Client') { fromFile = ['js', 'tpl', 'acpLess']; @@ -33,17 +34,17 @@ module.exports = function(grunt) { fromFile = ['clientLess', 'acpLess', 'js', 'tpl']; } - fromFile = fromFile.filter(function(ext) { + fromFile = fromFile.filter(function (ext) { return incomplete.indexOf(ext) === -1; }); - args.push('--from-file=' + fromFile.join(',')); + updateArgs.push('--from-file=' + fromFile.join(',')); incomplete.push(compiling); worker.kill(); - worker = fork('app.js', args, { env: env }); + worker = fork('app.js', updateArgs, { env: env }); - worker.on('message', function() { + worker.on('message', function () { if (incomplete.length) { incomplete = []; @@ -101,6 +102,6 @@ module.exports = function(grunt) { env.NODE_ENV = 'development'; - worker = fork('app.js', [], { env: env }); + worker = fork('app.js', args, { env: env }); grunt.event.on('watch', update); }; \ No newline at end of file diff --git a/README.md b/README.md index 200a444b6d..70e2c9773b 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,13 @@ Additional functionality is enabled through the use of third-party plugins. [![](http://i.imgur.com/LmHtPhob.png)](http://i.imgur.com/LmHtPho.png) [![](http://i.imgur.com/paiJPJkb.jpg)](http://i.imgur.com/paiJPJk.jpg) -[![](http://i.imgur.com/8OLssij.png)](http://i.imgur.com/8OLssij.png) -[![](http://i.imgur.com/JKOc0LZ.png)](http://i.imgur.com/JKOc0LZ.png) +[![](http://i.imgur.com/HwNEXGu.png)](http://i.imgur.com/HwNEXGu.png) +[![](http://i.imgur.com/II1byYs.png)](http://i.imgur.com/II1byYs.png) + + ## How can I follow along/contribute? -* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/NodeBB/NodeBB/wiki/Version-History-%26-Roadmap) * If you are a developer, feel free to check out the source and submit pull requests. We also have a wide array of [plugins](http://community.nodebb.org/category/7/nodebb-plugins) which would be a great starting point for learning the codebase. * If you are a designer, [NodeBB needs themes](http://community.nodebb.org/category/10/nodebb-themes)! NodeBB's theming system allows extention of the base templates as well as styling via LESS or CSS. NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether. * If you know languages other than English you can help us translate NodeBB. We use [Transifex](https://www.transifex.com/projects/p/nodebb/) for internationalization. diff --git a/app.js b/app.js index ed5238285e..82ffd2f7b1 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ /* NodeBB - A better forum platform for the modern web https://github.com/NodeBB/NodeBB/ - Copyright (C) 2013-2014 NodeBB Inc. + Copyright (C) 2013-2016 NodeBB Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -36,7 +36,7 @@ global.env = process.env.NODE_ENV || 'production'; winston.remove(winston.transports.Console); winston.add(winston.transports.Console, { colorize: true, - timestamp: function() { + timestamp: function () { var date = new Date(); return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; }, @@ -51,13 +51,14 @@ if (nconf.get('config')) { configFile = path.resolve(__dirname, nconf.get('config')); } -var configExists = file.existsSync(configFile); +var configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); loadConfig(); +versionCheck(); if (!process.send) { // If run using `node app`, log GNU copyright info along with server info - winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-2014 NodeBB Inc.'); + winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-' + (new Date()).getFullYear() + ' NodeBB Inc.'); winston.info('This program comes with ABSOLUTELY NO WARRANTY.'); winston.info('This is free software, and you are welcome to redistribute it under certain conditions.'); winston.info(''); @@ -103,6 +104,10 @@ function loadConfig() { nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path'))); nconf.set('core_templates_path', path.join(__dirname, 'src/views')); nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); + + if (nconf.get('url')) { + nconf.set('url_parsed', url.parse(nconf.get('url'))); + } } @@ -113,15 +118,18 @@ function start() { if (!nconf.get('upload_path')) { nconf.set('upload_path', '/public/uploads'); } + if (!nconf.get('sessionKey')) { + nconf.set('sessionKey', 'express.sid'); + } // Parse out the relative_url and other goodies from the configured URL var urlObject = url.parse(nconf.get('url')); var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; nconf.set('base_url', urlObject.protocol + '//' + urlObject.host); - nconf.set('secure', urlObject.protocol === 'https'); + nconf.set('secure', urlObject.protocol === 'https:'); nconf.set('use_port', !!urlObject.port); nconf.set('relative_path', relativePath); - nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || 4567); - nconf.set('upload_url', '/uploads/'); + nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); + nconf.set('upload_url', nconf.get('upload_path').replace(/^\/public/, '')); if (nconf.get('isPrimary') === 'true') { winston.info('Time: %s', (new Date()).toString()); @@ -138,7 +146,7 @@ function start() { process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); process.on('SIGHUP', restart); - process.on('message', function(message) { + process.on('message', function (message) { if (typeof message !== 'object') { return; } @@ -165,7 +173,7 @@ function start() { } }); - process.on('uncaughtException', function(err) { + process.on('uncaughtException', function (err) { winston.error(err.stack); console.log(err.stack); @@ -176,16 +184,21 @@ function start() { async.waterfall([ async.apply(db.init), async.apply(db.checkCompatibility), - function(next) { + function (next) { require('./src/meta').configs.init(next); }, - function(next) { - require('./src/meta').dependencies.check(next); + function (next) { + if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) { + require('./src/meta').dependencies.check(next); + } else { + winston.warn('[init] Dependency checking skipped!'); + setImmediate(next); + } }, - function(next) { + function (next) { require('./src/upgrade').check(next); }, - function(next) { + function (next) { var webserver = require('./src/webserver'); require('./src/socket.io').init(webserver.server); @@ -196,7 +209,7 @@ function start() { webserver.listen(); } - ], function(err) { + ], function (err) { if (err) { switch(err.message) { case 'schema-out-of-date': @@ -207,6 +220,10 @@ function start() { winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:'); winston.warn(' ./nodebb upgrade'); break; + case 'dependencies-missing': + winston.warn('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:'); + winston.warn(' ./nodebb upgrade'); + break; default: if (err.stacktrace !== false) { winston.error(err.stack); @@ -234,7 +251,7 @@ function setup() { install.setup(function (err, data) { var separator = ' '; if (process.stdout.columns > 10) { - for(var x=0,cols=process.stdout.columns-10;xAnnouncements regarding our community

\n", "bgColor": "#fda34b", "color": "#fff", "icon" : "fa-bullhorn", @@ -10,6 +11,7 @@ { "name": "General Discussion", "description": "A place to talk about whatever you want", + "descriptionParsed": "

A place to talk about whatever you want

\n", "bgColor": "#59b3d0", "color": "#fff", "icon" : "fa-comments-o", @@ -18,6 +20,7 @@ { "name": "Blogs", "description": "Blog posts from individual members", + "descriptionParsed": "

Blog posts from individual members

\n", "bgColor": "#86ba4b", "color": "#fff", "icon" : "fa-newspaper-o", @@ -26,6 +29,7 @@ { "name": "Comments & Feedback", "description": "Got a question? Ask away!", + "descriptionParsed": "

Got a question? Ask away!

\n", "bgColor": "#e95c5a", "color": "#fff", "icon" : "fa-question", diff --git a/install/data/defaults.json b/install/data/defaults.json index 5041ff83ea..c471db6b89 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -9,6 +9,8 @@ "maximumPostLength": 32767, "minimumTagsPerTopic": 0, "maximumTagsPerTopic": 5, + "minimumTagLength": 3, + "maximumTagLength": 15, "allowGuestSearching": 0, "allowTopicsThumbnail": 0, "registrationType": "normal", @@ -29,6 +31,9 @@ "profileImageDimension": 128, "requireEmailConfirmation": 0, "allowProfileImageUploads": 1, - "teaserPost": "last", - "allowPrivateGroups": 1 -} \ No newline at end of file + "teaserPost": "last-reply", + "allowPrivateGroups": 1, + "unreadCutoff": 2, + "bookmarkThreshold": 5, + "topicsPerList": 20 +} diff --git a/install/data/footer.json b/install/data/footer.json index 7c71cad648..12528110c6 100644 --- a/install/data/footer.json +++ b/install/data/footer.json @@ -2,7 +2,7 @@ { "widget": "html", "data" : { - "html": "", + "html": "", "title":"", "container":"" } diff --git a/install/databases.js b/install/databases.js index 8c5a76555b..c314ad75ef 100644 --- a/install/databases.js +++ b/install/databases.js @@ -9,7 +9,7 @@ var questions = { mongo: require('../src/database/mongo').questions }; -module.exports = function(config, callback) { +module.exports = function (config, callback) { async.waterfall([ function (next) { process.stdout.write('\n'); @@ -74,7 +74,7 @@ function saveDatabaseConfig(config, databaseConfig, callback) { } var allQuestions = questions.redis.concat(questions.mongo); - for (var x=0; x 1 ? true : false; process.env.port = ports[index]; var worker = fork('app.js', [], { @@ -207,21 +205,22 @@ function getPorts() { return port; } -Loader.restart = function() { +Loader.restart = function () { killWorkers(); - + nconf.remove('file'); + nconf.use('file', { file: path.join(__dirname, '/config.json') }); Loader.start(); }; -Loader.reload = function() { - workers.forEach(function(worker) { +Loader.reload = function () { + workers.forEach(function (worker) { worker.send({ action: 'reload' }); }); }; -Loader.stop = function() { +Loader.stop = function () { killWorkers(); // Clean up the pidfile @@ -229,15 +228,15 @@ Loader.stop = function() { }; function killWorkers() { - workers.forEach(function(worker) { + workers.forEach(function (worker) { worker.suicide = true; worker.kill(); }); } -Loader.notifyWorkers = function(msg, worker_pid) { +Loader.notifyWorkers = function (msg, worker_pid) { worker_pid = parseInt(worker_pid, 10); - workers.forEach(function(worker) { + workers.forEach(function (worker) { if (parseInt(worker.pid, 10) !== worker_pid) { try { worker.send(msg); @@ -248,7 +247,7 @@ Loader.notifyWorkers = function(msg, worker_pid) { }); }; -fs.open(path.join(__dirname, 'config.json'), 'r', function(err) { +fs.open(path.join(__dirname, 'config.json'), 'r', function (err) { if (!err) { if (nconf.get('daemon') !== 'false' && nconf.get('daemon') !== false) { if (file.existsSync(pidFilePath)) { @@ -273,7 +272,7 @@ fs.open(path.join(__dirname, 'config.json'), 'r', function(err) { Loader.init, Loader.displayStartupMessages, Loader.start - ], function(err) { + ], function (err) { if (err) { console.log('[loader] Error during startup: ' + err.message); } diff --git a/minifier.js b/minifier.js index 50106e2957..25c0177175 100644 --- a/minifier.js +++ b/minifier.js @@ -12,18 +12,18 @@ var Minifier = { /* Javascript */ Minifier.js.minify = function (scripts, minify, callback) { - scripts = scripts.filter(function(file) { + scripts = scripts.filter(function (file) { return file && file.endsWith('.js'); }); - async.filter(scripts, function(script, next) { - file.exists(script, function(exists) { + async.filter(scripts, function (script, next) { + file.exists(script, function (exists) { if (!exists) { console.warn('[minifier] file not found, ' + script); } next(exists); }); - }, function(scripts) { + }, function (scripts) { if (minify) { minifyScripts(scripts, callback); } else { @@ -32,10 +32,10 @@ Minifier.js.minify = function (scripts, minify, callback) { }); }; -process.on('message', function(payload) { +process.on('message', function (payload) { switch(payload.action) { case 'js': - Minifier.js.minify(payload.scripts, payload.minify, function(minified/*, sourceMap*/) { + Minifier.js.minify(payload.scripts, payload.minify, function (minified/*, sourceMap*/) { process.send({ type: 'end', // sourceMap: sourceMap, @@ -65,7 +65,7 @@ function minifyScripts(scripts, callback) { } function concatenateScripts(scripts, callback) { - async.map(scripts, fs.readFile, function(err, scripts) { + async.map(scripts, fs.readFile, function (err, scripts) { if (err) { process.send({ type: 'error', diff --git a/nodebb b/nodebb index 417c6a9d80..53ae28db22 100755 --- a/nodebb +++ b/nodebb @@ -1,14 +1,25 @@ #!/usr/bin/env node -var colors = require('colors'), - cproc = require('child_process'), - argv = require('minimist')(process.argv.slice(2)), - fs = require('fs'), - path = require('path'), - request = require('request'), - semver = require('semver'), - prompt = require('prompt'), - async = require('async'); +try { + var colors = require('colors'), + cproc = require('child_process'), + argv = require('minimist')(process.argv.slice(2)), + fs = require('fs'), + path = require('path'), + request = require('request'), + semver = require('semver'), + prompt = require('prompt'), + async = require('async'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + process.stdout.write('NodeBB could not be started because it\'s dependencies have not been installed.\n'); + process.stdout.write('Please ensure that you have executed "npm install --production" prior to running NodeBB.\n\n'); + process.stdout.write('For more information, please see: https://docs.nodebb.org/en/latest/installing/os.html\n\n'); + process.stdout.write('Could not start: ' + e.code + '\n'); + + process.exit(1); + } +} var getRunningPid = function(callback) { fs.readFile(__dirname + '/pidfile', { @@ -118,12 +129,13 @@ var getRunningPid = function(callback) { version: async.apply(getCurrentVersion) }), function(payload, next) { - if (!payload.plugins.length) { + var toCheck = Object.keys(payload.plugins); + + if (!toCheck.length) { process.stdout.write('OK'.green + '\n'.reset); return next(null, []); // no extraneous plugins installed } - var toCheck = Object.keys(payload.plugins); request({ method: 'GET', url: 'https://packages.nodebb.org/api/v1/suggest?version=' + payload.version + '&package[]=' + toCheck.join('&package[]='), @@ -195,7 +207,7 @@ var getRunningPid = function(callback) { description: 'Proceed with upgrade (y|n)?'.reset, type: 'string' }, function(err, result) { - if (result.upgrade === 'y' || result.upgrade === 'yes') { + if (['y', 'Y', 'yes', 'YES'].indexOf(result.upgrade) !== -1) { process.stdout.write('\nUpgrading packages...'); var args = ['npm', 'i']; found.forEach(function(suggestObj) { diff --git a/package.json b/package.json index 151660ddf2..43a6f46567 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.0.2", + "version": "1.2.1", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -11,30 +11,36 @@ "main": "app.js", "scripts": { "start": "node loader.js", - "test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./tests -t 10000" + "lint": "eslint --cache .", + "pretest": "npm run lint", + "test": "istanbul cover _mocha test", + "test-windows": "_mocha test" }, "dependencies": { "async": "~1.5.0", "autoprefixer": "^6.2.3", "bcryptjs": "~2.3.0", "body-parser": "^1.9.0", + "chart.js": "^2.1.0", "colors": "^1.1.0", "compression": "^1.1.0", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", "connect-mongo": "~1.1.0", "connect-multiparty": "^2.0.0", - "connect-redis": "~3.0.2", + "connect-redis": "~3.1.0", "cookie-parser": "^1.3.3", "cron": "^1.0.5", "csurf": "^1.6.1", "daemon": "~1.1.0", - "express": "^4.9.5", + "express": "^4.14.0", "express-session": "^1.8.2", "express-useragent": "0.2.4", "html-to-text": "2.0.0", "ip": "1.1.2", "jimp": "0.2.21", + "jquery": "^3.1.0", + "json-2-csv": "^2.0.22", "less": "^2.0.0", "logrotate-stream": "^0.2.3", "lru-cache": "4.0.0", @@ -43,40 +49,43 @@ "mkdirp": "~0.5.0", "mongodb": "~2.1.3", "morgan": "^1.3.2", + "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "3.0.18", - "nodebb-plugin-dbsearch": "1.0.1", - "nodebb-plugin-emoji-extended": "1.0.3", - "nodebb-plugin-markdown": "5.0.1", - "nodebb-plugin-mentions": "1.0.21", + "nodebb-plugin-composer-default": "4.2.9", + "nodebb-plugin-dbsearch": "1.0.2", + "nodebb-plugin-emoji-extended": "1.1.1", + "nodebb-plugin-emoji-one": "1.1.5", + "nodebb-plugin-markdown": "6.0.2", + "nodebb-plugin-mentions": "1.1.3", "nodebb-plugin-soundpack-default": "0.1.6", - "nodebb-plugin-spam-be-gone": "0.4.6", - "nodebb-rewards-essentials": "0.0.8", - "nodebb-theme-lavender": "3.0.9", - "nodebb-theme-persona": "4.0.115", - "nodebb-theme-vanilla": "5.0.61", - "nodebb-widget-essentials": "2.0.9", + "nodebb-plugin-spam-be-gone": "0.4.10", + "nodebb-rewards-essentials": "0.0.9", + "nodebb-theme-lavender": "3.0.15", + "nodebb-theme-persona": "4.1.63", + "nodebb-theme-vanilla": "5.1.43", + "nodebb-widget-essentials": "2.0.12", "nodemailer": "2.0.0", "nodemailer-sendmail-transport": "1.0.0", "nodemailer-smtp-transport": "^2.4.1", "passport": "^0.3.0", "passport-local": "1.0.0", "postcss": "^5.0.13", + "promise-polyfill": "^6.0.2", "prompt": "^1.0.0", - "redis": "~2.4.2", + "redis": "~2.6.2", "request": "^2.44.0", "rimraf": "~2.5.0", "rss": "^1.0.0", - "semver": "^5.0.1", + "semver": "^5.1.0", "serve-favicon": "^2.1.5", "sitemap": "^1.4.0", - "socket.io": "^1.4.0", + "socket.io": "^1.4.8", "socket.io-client": "^1.4.0", - "socket.io-redis": "^1.0.0", + "socket.io-redis": "1.1.1", "socketio-wildcard": "~0.3.0", "string": "^3.0.0", "templates.js": "0.3.4", - "toobusy-js": "^0.4.2", + "toobusy-js": "^0.5.1", "uglify-js": "^2.6.0", "underscore": "^1.8.3", "underscore.deep": "^0.5.1", @@ -85,16 +94,21 @@ "xregexp": "~3.1.0" }, "devDependencies": { + "eslint": "^3.7.1", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^1.16.0", + "eslint-plugin-jsx-a11y": "^2.2.3", + "eslint-plugin-react": "^6.4.1", "grunt": "~0.4.5", "grunt-contrib-watch": "^1.0.0", "istanbul": "^0.4.2", - "mocha": "~1.13.0" + "mocha": "~3.1.0" }, "bugs": { "url": "https://github.com/NodeBB/NodeBB/issues" }, "engines": { - "node": ">=0.10" + "node": ">=4" }, "maintainers": [ { diff --git a/public/503.html b/public/503.html index 124e92ef26..119c710ab7 100644 --- a/public/503.html +++ b/public/503.html @@ -1,7 +1,8 @@ Excessive Load Warning - + + ").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,o=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return n?this.floating?a&&"right"===a||"down"===o?2:1:o&&("down"===o?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return this.floating&&o?"right"===o&&s||"left"===o&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(o=t(l[s]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&h.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",a),c.push({item:h,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t(" ",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,o,a,r,h,l,c,u,d,p,f=null,g=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],g=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[g].containerCache.over||(this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1);else{for(a=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],o=this.items.length-1;o>=0;o--)t.contains(this.containers[g].element[0],this.items[o].item[0])&&this.items[o].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[o].top,this.items[o].height))&&(u=this.items[o].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[o][l]-c)&&(d=!0,u+=this.items[o][l]),a>Math.abs(u-c)&&(a=Math.abs(u-c),r=this.items[o],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[g])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[g].element,!0),this._trigger("change",s,this._uiHash()),this.containers[g]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[g],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("
    ").addClass("ui-autocomplete ui-front").appendTo(this._appendTo()).menu({role:null}).hide().data("ui-menu"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur});var i=this.menu.element[0];e(t.target).closest(".ui-menu-item").length||this._delay(function(){var t=this;this.document.one("mousedown",function(s){s.target===t.element[0]||s.target===i||e.contains(i,s.target)||t.close()})})},menufocus:function(t,i){if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),this.document.one("mousemove",function(){e(t.target).trigger(t.originalEvent)}),undefined;var s=i.item.data("ui-autocomplete-item");!1!==this._trigger("focus",t,{item:s})?t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value):this.liveRegion.text(s.value)},menuselect:function(e,t){var i=t.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",e,{item:i})&&this._value(i.value),this.term=this._value(),this.close(e),this.selectedItem=i}}),this.liveRegion=e("",{role:"status","aria-live":"polite"}).addClass("ui-helper-hidden-accessible").insertBefore(this.element),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(e,t){this._super(e,t),"source"===e&&this._initSource(),"appendTo"===e&&this.menu.element.appendTo(this._appendTo()),"disabled"===e&&t&&this.xhr&&this.xhr.abort()},_appendTo:function(){var t=this.options.appendTo;return t&&(t=t.jquery||t.nodeType?e(t):this.document.find(t).eq(0)),t||(t=this.element.closest(".ui-front")),t.length||(t=this.document[0].body),t},_initSource:function(){var t,i,s=this;e.isArray(this.options.source)?(t=this.options.source,this.source=function(i,s){s(e.ui.autocomplete.filter(t,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(t,n){s.xhr&&s.xhr.abort(),s.xhr=e.ajax({url:i,data:t,dataType:"json",success:function(e){n(e)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(e){clearTimeout(this.searching),this.searching=this._delay(function(){this.term!==this._value()&&(this.selectedItem=null,this.search(null,e))},this.options.delay)},search:function(e,t){return e=null!=e?e:this._value(),this.term=this._value(),e.length").append(e("").text(i.label)).appendTo(t)},_move:function(e,t){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(e)||this.menu.isLastItem()&&/^next/.test(e)?(this._value(this.term),this.menu.blur(),undefined):(this.menu[e](t),undefined):(this.search(null,t),undefined)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(e,t){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(e,t),t.preventDefault())}}),e.extend(e.ui.autocomplete,{escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,i){var s=RegExp(e.ui.autocomplete.escapeRegex(i),"i");return e.grep(t,function(e){return s.test(e.label||e.value||e)})}}),e.widget("ui.autocomplete",e.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(e){return e+(e>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var t;this._superApply(arguments),this.options.disabled||this.cancelSearch||(t=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.text(t))}})})(jQuery);(function(e,t){function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},e.extend(this._defaults,this.regional[""]),this.dpDiv=a(e("
    "))}function a(t){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return t.delegate(i,"mouseout",function(){e(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",function(){e.datepicker._isDisabledDatepicker(n.inline?t.parent()[0]:n.input[0])||(e(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),e(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).addClass("ui-datepicker-next-hover"))})}function s(t,i){e.extend(t,i);for(var a in i)null==i[a]&&(t[a]=i[a]);return t}e.extend(e.ui,{datepicker:{version:"1.10.4"}});var n,r="datepicker";e.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return s(this._defaults,e||{}),this},_attachDatepicker:function(t,i){var a,s,n;a=t.nodeName.toLowerCase(),s="div"===a||"span"===a,t.id||(this.uuid+=1,t.id="dp"+this.uuid),n=this._newInst(e(t),s),n.settings=e.extend({},i||{}),"input"===a?this._connectDatepicker(t,n):s&&this._inlineDatepicker(t,n)},_newInst:function(t,i){var s=t[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:s,input:t,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?a(e("
    ")):this.dpDiv}},_connectDatepicker:function(t,i){var a=e(t);i.append=e([]),i.trigger=e([]),a.hasClass(this.markerClassName)||(this._attachments(a,i),a.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp),this._autoSize(i),e.data(t,r,i),i.settings.disabled&&this._disableDatepicker(t))},_attachments:function(t,i){var a,s,n,r=this._get(i,"appendText"),o=this._get(i,"isRTL");i.append&&i.append.remove(),r&&(i.append=e(""+r+""),t[o?"before":"after"](i.append)),t.unbind("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),a=this._get(i,"showOn"),("focus"===a||"both"===a)&&t.focus(this._showDatepicker),("button"===a||"both"===a)&&(s=this._get(i,"buttonText"),n=this._get(i,"buttonImage"),i.trigger=e(this._get(i,"buttonImageOnly")?e("").addClass(this._triggerClass).attr({src:n,alt:s,title:s}):e("").addClass(this._triggerClass).html(n?e("").attr({src:n,alt:s,title:s}):s)),t[o?"before":"after"](i.trigger),i.trigger.click(function(){return e.datepicker._datepickerShowing&&e.datepicker._lastInput===t[0]?e.datepicker._hideDatepicker():e.datepicker._datepickerShowing&&e.datepicker._lastInput!==t[0]?(e.datepicker._hideDatepicker(),e.datepicker._showDatepicker(t[0])):e.datepicker._showDatepicker(t[0]),!1}))},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t,i,a,s,n=new Date(2009,11,20),r=this._get(e,"dateFormat");r.match(/[DM]/)&&(t=function(e){for(i=0,a=0,s=0;e.length>s;s++)e[s].length>i&&(i=e[s].length,a=s);return a},n.setMonth(t(this._get(e,r.match(/MM/)?"monthNames":"monthNamesShort"))),n.setDate(t(this._get(e,r.match(/DD/)?"dayNames":"dayNamesShort"))+20-n.getDay())),e.input.attr("size",this._formatDate(e,n).length)}},_inlineDatepicker:function(t,i){var a=e(t);a.hasClass(this.markerClassName)||(a.addClass(this.markerClassName).append(i.dpDiv),e.data(t,r,i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(t),i.dpDiv.css("display","block"))},_dialogDatepicker:function(t,i,a,n,o){var u,c,h,l,d,p=this._dialogInst;return p||(this.uuid+=1,u="dp"+this.uuid,this._dialogInput=e(""),this._dialogInput.keydown(this._doKeyDown),e("body").append(this._dialogInput),p=this._dialogInst=this._newInst(this._dialogInput,!1),p.settings={},e.data(this._dialogInput[0],r,p)),s(p.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(p,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(c=document.documentElement.clientWidth,h=document.documentElement.clientHeight,l=document.documentElement.scrollLeft||document.body.scrollLeft,d=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[c/2-100+l,h/2-150+d]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),p.settings.onSelect=a,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),e.blockUI&&e.blockUI(this.dpDiv),e.data(this._dialogInput[0],r,p),this},_destroyDatepicker:function(t){var i,a=e(t),s=e.data(t,r);a.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),e.removeData(t,r),"input"===i?(s.append.remove(),s.trigger.remove(),a.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):("div"===i||"span"===i)&&a.removeClass(this.markerClassName).empty())},_enableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().removeClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}))},_disableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().addClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}),this._disabledInputs[this._disabledInputs.length]=t)},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;this._disabledInputs.length>t;t++)if(this._disabledInputs[t]===e)return!0;return!1},_getInst:function(t){try{return e.data(t,r)}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(i,a,n){var r,o,u,c,h=this._getInst(i);return 2===arguments.length&&"string"==typeof a?"defaults"===a?e.extend({},e.datepicker._defaults):h?"all"===a?e.extend({},h.settings):this._get(h,a):null:(r=a||{},"string"==typeof a&&(r={},r[a]=n),h&&(this._curInst===h&&this._hideDatepicker(),o=this._getDateDatepicker(i,!0),u=this._getMinMaxDate(h,"min"),c=this._getMinMaxDate(h,"max"),s(h.settings,r),null!==u&&r.dateFormat!==t&&r.minDate===t&&(h.settings.minDate=this._formatDate(h,u)),null!==c&&r.dateFormat!==t&&r.maxDate===t&&(h.settings.maxDate=this._formatDate(h,c)),"disabled"in r&&(r.disabled?this._disableDatepicker(i):this._enableDatepicker(i)),this._attachments(e(i),h),this._autoSize(h),this._setDate(h,o),this._updateAlternate(h),this._updateDatepicker(h)),t)},_changeDatepicker:function(e,t,i){this._optionDatepicker(e,t,i)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var i=this._getInst(e);i&&(this._setDate(i,t),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(e,t){var i=this._getInst(e);return i&&!i.inline&&this._setDateFromField(i,t),i?this._getDate(i):null},_doKeyDown:function(t){var i,a,s,n=e.datepicker._getInst(t.target),r=!0,o=n.dpDiv.is(".ui-datepicker-rtl");if(n._keyEvent=!0,e.datepicker._datepickerShowing)switch(t.keyCode){case 9:e.datepicker._hideDatepicker(),r=!1;break;case 13:return s=e("td."+e.datepicker._dayOverClass+":not(."+e.datepicker._currentClass+")",n.dpDiv),s[0]&&e.datepicker._selectDay(t.target,n.selectedMonth,n.selectedYear,s[0]),i=e.datepicker._get(n,"onSelect"),i?(a=e.datepicker._formatDate(n),i.apply(n.input?n.input[0]:null,[a,n])):e.datepicker._hideDatepicker(),!1;case 27:e.datepicker._hideDatepicker();break;case 33:e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 34:e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 35:(t.ctrlKey||t.metaKey)&&e.datepicker._clearDate(t.target),r=t.ctrlKey||t.metaKey;break;case 36:(t.ctrlKey||t.metaKey)&&e.datepicker._gotoToday(t.target),r=t.ctrlKey||t.metaKey;break;case 37:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?1:-1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 38:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,-7,"D"),r=t.ctrlKey||t.metaKey;break;case 39:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?-1:1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 40:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,7,"D"),r=t.ctrlKey||t.metaKey;break;default:r=!1}else 36===t.keyCode&&t.ctrlKey?e.datepicker._showDatepicker(this):r=!1;r&&(t.preventDefault(),t.stopPropagation())},_doKeyPress:function(i){var a,s,n=e.datepicker._getInst(i.target);return e.datepicker._get(n,"constrainInput")?(a=e.datepicker._possibleChars(e.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==i.charCode?i.keyCode:i.charCode),i.ctrlKey||i.metaKey||" ">s||!a||a.indexOf(s)>-1):t},_doKeyUp:function(t){var i,a=e.datepicker._getInst(t.target);if(a.input.val()!==a.lastVal)try{i=e.datepicker.parseDate(e.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,e.datepicker._getFormatConfig(a)),i&&(e.datepicker._setDateFromField(a),e.datepicker._updateAlternate(a),e.datepicker._updateDatepicker(a))}catch(s){}return!0},_showDatepicker:function(t){if(t=t.target||t,"input"!==t.nodeName.toLowerCase()&&(t=e("input",t.parentNode)[0]),!e.datepicker._isDisabledDatepicker(t)&&e.datepicker._lastInput!==t){var i,a,n,r,o,u,c;i=e.datepicker._getInst(t),e.datepicker._curInst&&e.datepicker._curInst!==i&&(e.datepicker._curInst.dpDiv.stop(!0,!0),i&&e.datepicker._datepickerShowing&&e.datepicker._hideDatepicker(e.datepicker._curInst.input[0])),a=e.datepicker._get(i,"beforeShow"),n=a?a.apply(t,[t,i]):{},n!==!1&&(s(i.settings,n),i.lastVal=null,e.datepicker._lastInput=t,e.datepicker._setDateFromField(i),e.datepicker._inDialog&&(t.value=""),e.datepicker._pos||(e.datepicker._pos=e.datepicker._findPos(t),e.datepicker._pos[1]+=t.offsetHeight),r=!1,e(t).parents().each(function(){return r|="fixed"===e(this).css("position"),!r}),o={left:e.datepicker._pos[0],top:e.datepicker._pos[1]},e.datepicker._pos=null,i.dpDiv.empty(),i.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),e.datepicker._updateDatepicker(i),o=e.datepicker._checkOffset(i,o,r),i.dpDiv.css({position:e.datepicker._inDialog&&e.blockUI?"static":r?"fixed":"absolute",display:"none",left:o.left+"px",top:o.top+"px"}),i.inline||(u=e.datepicker._get(i,"showAnim"),c=e.datepicker._get(i,"duration"),i.dpDiv.zIndex(e(t).zIndex()+1),e.datepicker._datepickerShowing=!0,e.effects&&e.effects.effect[u]?i.dpDiv.show(u,e.datepicker._get(i,"showOptions"),c):i.dpDiv[u||"show"](u?c:null),e.datepicker._shouldFocusInput(i)&&i.input.focus(),e.datepicker._curInst=i))}},_updateDatepicker:function(t){this.maxRows=4,n=t,t.dpDiv.empty().append(this._generateHTML(t)),this._attachHandlers(t),t.dpDiv.find("."+this._dayOverClass+" a").mouseover();var i,a=this._getNumberOfMonths(t),s=a[1],r=17;t.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),s>1&&t.dpDiv.addClass("ui-datepicker-multi-"+s).css("width",r*s+"em"),t.dpDiv[(1!==a[0]||1!==a[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),t.dpDiv[(this._get(t,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),t===e.datepicker._curInst&&e.datepicker._datepickerShowing&&e.datepicker._shouldFocusInput(t)&&t.input.focus(),t.yearshtml&&(i=t.yearshtml,setTimeout(function(){i===t.yearshtml&&t.yearshtml&&t.dpDiv.find("select.ui-datepicker-year:first").replaceWith(t.yearshtml),i=t.yearshtml=null},0))},_shouldFocusInput:function(e){return e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&!e.input.is(":focus")},_checkOffset:function(t,i,a){var s=t.dpDiv.outerWidth(),n=t.dpDiv.outerHeight(),r=t.input?t.input.outerWidth():0,o=t.input?t.input.outerHeight():0,u=document.documentElement.clientWidth+(a?0:e(document).scrollLeft()),c=document.documentElement.clientHeight+(a?0:e(document).scrollTop());return i.left-=this._get(t,"isRTL")?s-r:0,i.left-=a&&i.left===t.input.offset().left?e(document).scrollLeft():0,i.top-=a&&i.top===t.input.offset().top+o?e(document).scrollTop():0,i.left-=Math.min(i.left,i.left+s>u&&u>s?Math.abs(i.left+s-u):0),i.top-=Math.min(i.top,i.top+n>c&&c>n?Math.abs(n+o):0),i},_findPos:function(t){for(var i,a=this._getInst(t),s=this._get(a,"isRTL");t&&("hidden"===t.type||1!==t.nodeType||e.expr.filters.hidden(t));)t=t[s?"previousSibling":"nextSibling"];return i=e(t).offset(),[i.left,i.top]},_hideDatepicker:function(t){var i,a,s,n,o=this._curInst;!o||t&&o!==e.data(t,r)||this._datepickerShowing&&(i=this._get(o,"showAnim"),a=this._get(o,"duration"),s=function(){e.datepicker._tidyDialog(o)},e.effects&&(e.effects.effect[i]||e.effects[i])?o.dpDiv.hide(i,e.datepicker._get(o,"showOptions"),a,s):o.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?a:null,s),i||s(),this._datepickerShowing=!1,n=this._get(o,"onClose"),n&&n.apply(o.input?o.input[0]:null,[o.input?o.input.val():"",o]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),e.blockUI&&(e.unblockUI(),e("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(t){if(e.datepicker._curInst){var i=e(t.target),a=e.datepicker._getInst(i[0]);(i[0].id!==e.datepicker._mainDivId&&0===i.parents("#"+e.datepicker._mainDivId).length&&!i.hasClass(e.datepicker.markerClassName)&&!i.closest("."+e.datepicker._triggerClass).length&&e.datepicker._datepickerShowing&&(!e.datepicker._inDialog||!e.blockUI)||i.hasClass(e.datepicker.markerClassName)&&e.datepicker._curInst!==a)&&e.datepicker._hideDatepicker()}},_adjustDate:function(t,i,a){var s=e(t),n=this._getInst(s[0]);this._isDisabledDatepicker(s[0])||(this._adjustInstDate(n,i+("M"===a?this._get(n,"showCurrentAtPos"):0),a),this._updateDatepicker(n))},_gotoToday:function(t){var i,a=e(t),s=this._getInst(a[0]);this._get(s,"gotoCurrent")&&s.currentDay?(s.selectedDay=s.currentDay,s.drawMonth=s.selectedMonth=s.currentMonth,s.drawYear=s.selectedYear=s.currentYear):(i=new Date,s.selectedDay=i.getDate(),s.drawMonth=s.selectedMonth=i.getMonth(),s.drawYear=s.selectedYear=i.getFullYear()),this._notifyChange(s),this._adjustDate(a)},_selectMonthYear:function(t,i,a){var s=e(t),n=this._getInst(s[0]);n["selected"+("M"===a?"Month":"Year")]=n["draw"+("M"===a?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(n),this._adjustDate(s)},_selectDay:function(t,i,a,s){var n,r=e(t);e(s).hasClass(this._unselectableClass)||this._isDisabledDatepicker(r[0])||(n=this._getInst(r[0]),n.selectedDay=n.currentDay=e("a",s).html(),n.selectedMonth=n.currentMonth=i,n.selectedYear=n.currentYear=a,this._selectDate(t,this._formatDate(n,n.currentDay,n.currentMonth,n.currentYear)))},_clearDate:function(t){var i=e(t);this._selectDate(i,"")},_selectDate:function(t,i){var a,s=e(t),n=this._getInst(s[0]);i=null!=i?i:this._formatDate(n),n.input&&n.input.val(i),this._updateAlternate(n),a=this._get(n,"onSelect"),a?a.apply(n.input?n.input[0]:null,[i,n]):n.input&&n.input.trigger("change"),n.inline?this._updateDatepicker(n):(this._hideDatepicker(),this._lastInput=n.input[0],"object"!=typeof n.input[0]&&n.input.focus(),this._lastInput=null)},_updateAlternate:function(t){var i,a,s,n=this._get(t,"altField");n&&(i=this._get(t,"altFormat")||this._get(t,"dateFormat"),a=this._getDate(t),s=this.formatDate(i,a,this._getFormatConfig(t)),e(n).each(function(){e(this).val(s)}))},noWeekends:function(e){var t=e.getDay();return[t>0&&6>t,""]},iso8601Week:function(e){var t,i=new Date(e.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),t=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((t-i)/864e5)/7)+1},parseDate:function(i,a,s){if(null==i||null==a)throw"Invalid arguments";if(a="object"==typeof a?""+a:a+"",""===a)return null;var n,r,o,u,c=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,l="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),d=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,p=(s?s.dayNames:null)||this._defaults.dayNames,g=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,m=(s?s.monthNames:null)||this._defaults.monthNames,f=-1,_=-1,v=-1,k=-1,y=!1,b=function(e){var t=i.length>n+1&&i.charAt(n+1)===e;return t&&n++,t},D=function(e){var t=b(e),i="@"===e?14:"!"===e?20:"y"===e&&t?4:"o"===e?3:2,s=RegExp("^\\d{1,"+i+"}"),n=a.substring(c).match(s);if(!n)throw"Missing number at position "+c;return c+=n[0].length,parseInt(n[0],10)},w=function(i,s,n){var r=-1,o=e.map(b(i)?n:s,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)});if(e.each(o,function(e,i){var s=i[1];return a.substr(c,s.length).toLowerCase()===s.toLowerCase()?(r=i[0],c+=s.length,!1):t}),-1!==r)return r+1;throw"Unknown name at position "+c},M=function(){if(a.charAt(c)!==i.charAt(n))throw"Unexpected literal at position "+c;c++};for(n=0;i.length>n;n++)if(y)"'"!==i.charAt(n)||b("'")?M():y=!1;else switch(i.charAt(n)){case"d":v=D("d");break;case"D":w("D",d,p);break;case"o":k=D("o");break;case"m":_=D("m");break;case"M":_=w("M",g,m);break;case"y":f=D("y");break;case"@":u=new Date(D("@")),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"!":u=new Date((D("!")-this._ticksTo1970)/1e4),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"'":b("'")?M():y=!0;break;default:M()}if(a.length>c&&(o=a.substr(c),!/^\s+/.test(o)))throw"Extra/unparsed characters found in date: "+o;if(-1===f?f=(new Date).getFullYear():100>f&&(f+=(new Date).getFullYear()-(new Date).getFullYear()%100+(l>=f?0:-100)),k>-1)for(_=1,v=k;;){if(r=this._getDaysInMonth(f,_-1),r>=v)break;_++,v-=r}if(u=this._daylightSavingAdjust(new Date(f,_-1,v)),u.getFullYear()!==f||u.getMonth()+1!==_||u.getDate()!==v)throw"Invalid date";return u},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(e,t,i){if(!t)return"";var a,s=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,n=(i?i.dayNames:null)||this._defaults.dayNames,r=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,o=(i?i.monthNames:null)||this._defaults.monthNames,u=function(t){var i=e.length>a+1&&e.charAt(a+1)===t;return i&&a++,i},c=function(e,t,i){var a=""+t;if(u(e))for(;i>a.length;)a="0"+a;return a},h=function(e,t,i,a){return u(e)?a[t]:i[t]},l="",d=!1;if(t)for(a=0;e.length>a;a++)if(d)"'"!==e.charAt(a)||u("'")?l+=e.charAt(a):d=!1;else switch(e.charAt(a)){case"d":l+=c("d",t.getDate(),2);break;case"D":l+=h("D",t.getDay(),s,n);break;case"o":l+=c("o",Math.round((new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime()-new Date(t.getFullYear(),0,0).getTime())/864e5),3);break;case"m":l+=c("m",t.getMonth()+1,2);break;case"M":l+=h("M",t.getMonth(),r,o);break;case"y":l+=u("y")?t.getFullYear():(10>t.getYear()%100?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=1e4*t.getTime()+this._ticksTo1970;break;case"'":u("'")?l+="'":d=!0;break;default:l+=e.charAt(a)}return l},_possibleChars:function(e){var t,i="",a=!1,s=function(i){var a=e.length>t+1&&e.charAt(t+1)===i;return a&&t++,a};for(t=0;e.length>t;t++)if(a)"'"!==e.charAt(t)||s("'")?i+=e.charAt(t):a=!1;else switch(e.charAt(t)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":s("'")?i+="'":a=!0;break;default:i+=e.charAt(t)}return i},_get:function(e,i){return e.settings[i]!==t?e.settings[i]:this._defaults[i]},_setDateFromField:function(e,t){if(e.input.val()!==e.lastVal){var i=this._get(e,"dateFormat"),a=e.lastVal=e.input?e.input.val():null,s=this._getDefaultDate(e),n=s,r=this._getFormatConfig(e);try{n=this.parseDate(i,a,r)||s}catch(o){a=t?"":a}e.selectedDay=n.getDate(),e.drawMonth=e.selectedMonth=n.getMonth(),e.drawYear=e.selectedYear=n.getFullYear(),e.currentDay=a?n.getDate():0,e.currentMonth=a?n.getMonth():0,e.currentYear=a?n.getFullYear():0,this._adjustInstDate(e)}},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(t,i,a){var s=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},n=function(i){try{return e.datepicker.parseDate(e.datepicker._get(t,"dateFormat"),i,e.datepicker._getFormatConfig(t))}catch(a){}for(var s=(i.toLowerCase().match(/^c/)?e.datepicker._getDate(t):null)||new Date,n=s.getFullYear(),r=s.getMonth(),o=s.getDate(),u=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,c=u.exec(i);c;){switch(c[2]||"d"){case"d":case"D":o+=parseInt(c[1],10);break;case"w":case"W":o+=7*parseInt(c[1],10);break;case"m":case"M":r+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r));break;case"y":case"Y":n+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r))}c=u.exec(i)}return new Date(n,r,o)},r=null==i||""===i?a:"string"==typeof i?n(i):"number"==typeof i?isNaN(i)?a:s(i):new Date(i.getTime());return r=r&&"Invalid Date"==""+r?a:r,r&&(r.setHours(0),r.setMinutes(0),r.setSeconds(0),r.setMilliseconds(0)),this._daylightSavingAdjust(r)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,i){var a=!t,s=e.selectedMonth,n=e.selectedYear,r=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=r.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=r.getMonth(),e.drawYear=e.selectedYear=e.currentYear=r.getFullYear(),s===e.selectedMonth&&n===e.selectedYear||i||this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(a?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&""===e.input.val()?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(t){var i=this._get(t,"stepMonths"),a="#"+t.id.replace(/\\\\/g,"\\");t.dpDiv.find("[data-handler]").map(function(){var t={prev:function(){e.datepicker._adjustDate(a,-i,"M")},next:function(){e.datepicker._adjustDate(a,+i,"M")},hide:function(){e.datepicker._hideDatepicker()},today:function(){e.datepicker._gotoToday(a)},selectDay:function(){return e.datepicker._selectDay(a,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return e.datepicker._selectMonthYear(a,this,"M"),!1},selectYear:function(){return e.datepicker._selectMonthYear(a,this,"Y"),!1}};e(this).bind(this.getAttribute("data-event"),t[this.getAttribute("data-handler")])})},_generateHTML:function(e){var t,i,a,s,n,r,o,u,c,h,l,d,p,g,m,f,_,v,k,y,b,D,w,M,C,x,I,N,T,A,E,S,Y,F,P,O,j,K,R,H=new Date,W=this._daylightSavingAdjust(new Date(H.getFullYear(),H.getMonth(),H.getDate())),L=this._get(e,"isRTL"),U=this._get(e,"showButtonPanel"),B=this._get(e,"hideIfNoPrevNext"),z=this._get(e,"navigationAsDateFormat"),q=this._getNumberOfMonths(e),G=this._get(e,"showCurrentAtPos"),J=this._get(e,"stepMonths"),Q=1!==q[0]||1!==q[1],V=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),$=this._getMinMaxDate(e,"min"),X=this._getMinMaxDate(e,"max"),Z=e.drawMonth-G,et=e.drawYear;if(0>Z&&(Z+=12,et--),X)for(t=this._daylightSavingAdjust(new Date(X.getFullYear(),X.getMonth()-q[0]*q[1]+1,X.getDate())),t=$&&$>t?$:t;this._daylightSavingAdjust(new Date(et,Z,1))>t;)Z--,0>Z&&(Z=11,et--);for(e.drawMonth=Z,e.drawYear=et,i=this._get(e,"prevText"),i=z?this.formatDate(i,this._daylightSavingAdjust(new Date(et,Z-J,1)),this._getFormatConfig(e)):i,a=this._canAdjustMonth(e,-1,et,Z)?"
    "+i+"":B?"":""+i+"",s=this._get(e,"nextText"),s=z?this.formatDate(s,this._daylightSavingAdjust(new Date(et,Z+J,1)),this._getFormatConfig(e)):s,n=this._canAdjustMonth(e,1,et,Z)?""+s+"":B?"":""+s+"",r=this._get(e,"currentText"),o=this._get(e,"gotoCurrent")&&e.currentDay?V:W,r=z?this.formatDate(r,o,this._getFormatConfig(e)):r,u=e.inline?"":"",c=U?"
    "+(L?u:"")+(this._isInRange(e,o)?"":"")+(L?"":u)+"
    ":"",h=parseInt(this._get(e,"firstDay"),10),h=isNaN(h)?0:h,l=this._get(e,"showWeek"),d=this._get(e,"dayNames"),p=this._get(e,"dayNamesMin"),g=this._get(e,"monthNames"),m=this._get(e,"monthNamesShort"),f=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),v=this._get(e,"selectOtherMonths"),k=this._getDefaultDate(e),y="",D=0;q[0]>D;D++){for(w="",this.maxRows=4,M=0;q[1]>M;M++){if(C=this._daylightSavingAdjust(new Date(et,Z,e.selectedDay)),x=" ui-corner-all",I="",Q){if(I+="
    "}for(I+="
    "+(/all|left/.test(x)&&0===D?L?n:a:"")+(/all|right/.test(x)&&0===D?L?a:n:"")+this._generateMonthYearHeader(e,Z,et,$,X,D>0||M>0,g,m)+"
    "+"",N=l?"":"",b=0;7>b;b++)T=(b+h)%7,N+="=5?" class='ui-datepicker-week-end'":"")+">"+""+p[T]+"";for(I+=N+"",A=this._getDaysInMonth(et,Z),et===e.selectedYear&&Z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,A)),E=(this._getFirstDayOfMonth(et,Z)-h+7)%7,S=Math.ceil((E+A)/7),Y=Q?this.maxRows>S?this.maxRows:S:S,this.maxRows=Y,F=this._daylightSavingAdjust(new Date(et,Z,1-E)),P=0;Y>P;P++){for(I+="",O=l?"":"",b=0;7>b;b++)j=f?f.apply(e.input?e.input[0]:null,[F]):[!0,""],K=F.getMonth()!==Z,R=K&&!v||!j[0]||$&&$>F||X&&F>X,O+="",F.setDate(F.getDate()+1),F=this._daylightSavingAdjust(F);I+=O+""}Z++,Z>11&&(Z=0,et++),I+="
    "+this._get(e,"weekHeader")+"
    "+this._get(e,"calculateWeek")(F)+""+(K&&!_?" ":R?""+F.getDate()+"":""+F.getDate()+"")+"
    "+(Q?"
    "+(q[0]>0&&M===q[1]-1?"
    ":""):""),w+=I}y+=w}return y+=c,e._keyEvent=!1,y},_generateMonthYearHeader:function(e,t,i,a,s,n,r,o){var u,c,h,l,d,p,g,m,f=this._get(e,"changeMonth"),_=this._get(e,"changeYear"),v=this._get(e,"showMonthAfterYear"),k="
    ",y="";if(n||!f)y+=""+r[t]+"";else{for(u=a&&a.getFullYear()===i,c=s&&s.getFullYear()===i,y+=""}if(v||(k+=y+(!n&&f&&_?"":" ")),!e.yearshtml)if(e.yearshtml="",n||!_)k+=""+i+"";else{for(l=this._get(e,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(e){var t=e.match(/c[+\-].*/)?i+parseInt(e.substring(1),10):e.match(/[+\-].*/)?d+parseInt(e,10):parseInt(e,10); -return isNaN(t)?d:t},g=p(l[0]),m=Math.max(g,p(l[1]||"")),g=a?Math.max(g,a.getFullYear()):g,m=s?Math.min(m,s.getFullYear()):m,e.yearshtml+="",k+=e.yearshtml,e.yearshtml=null}return k+=this._get(e,"yearSuffix"),v&&(k+=(!n&&f&&_?"":" ")+y),k+="
    "},_adjustInstDate:function(e,t,i){var a=e.drawYear+("Y"===i?t:0),s=e.drawMonth+("M"===i?t:0),n=Math.min(e.selectedDay,this._getDaysInMonth(a,s))+("D"===i?t:0),r=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(a,s,n)));e.selectedDay=r.getDate(),e.drawMonth=e.selectedMonth=r.getMonth(),e.drawYear=e.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),s=i&&i>t?i:t;return a&&s>a?a:s},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,a){var s=this._getNumberOfMonths(e),n=this._daylightSavingAdjust(new Date(i,a+(0>t?t:s[0]*s[1]),1));return 0>t&&n.setDate(this._getDaysInMonth(n.getFullYear(),n.getMonth())),this._isInRange(e,n)},_isInRange:function(e,t){var i,a,s=this._getMinMaxDate(e,"min"),n=this._getMinMaxDate(e,"max"),r=null,o=null,u=this._get(e,"yearRange");return u&&(i=u.split(":"),a=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=a),i[1].match(/[+\-].*/)&&(o+=a)),(!s||t.getTime()>=s.getTime())&&(!n||t.getTime()<=n.getTime())&&(!r||t.getFullYear()>=r)&&(!o||o>=t.getFullYear())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,a){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var s=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(a,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),s,this._getFormatConfig(e))}}),e.fn.datepicker=function(t){if(!this.length)return this;e.datepicker.initialized||(e(document).mousedown(e.datepicker._checkExternalClick),e.datepicker.initialized=!0),0===e("#"+e.datepicker._mainDivId).length&&e("body").append(e.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof t||"isDisabled"!==t&&"getDate"!==t&&"widget"!==t?"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof t?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this].concat(i)):e.datepicker._attachDatepicker(this,t)}):e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i))},e.datepicker=new i,e.datepicker.initialized=!1,e.datepicker.uuid=(new Date).getTime(),e.datepicker.version="1.10.4"})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.4",defaultElement:"
      ",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,l=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:l=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}l&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()"))}function s(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,n)}function n(){t.datepicker._isDisabledDatepicker(c.inline?c.dpDiv.parent()[0]:c.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function o(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}t.ui=t.ui||{},t.ui.version="1.12.0";var a=0,r=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},l=e.split(".")[0];e=e.split(".")[1];var h=l+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][h.toLowerCase()]=function(e){return!!t.data(e,h)},t[l]=t[l]||{},n=t[l][e],o=t[l][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:l,widgetName:e,widgetFullName:h}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=r.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=r.call(arguments,1),l=this;return o?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(l=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(l=i&&i.jquery?l.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
      ",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=a++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var l=s.match(/^([\w:-]*)\s*(.*)$/),h=l[1]+o.eventNamespace,c=l[2];c?n.on(h,c,r):i.on(h,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o,a=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;o=function(){var e=t("
      ").css("position","absolute").appendTo("body").offset({top:1.5,left:1.5}),i=1.5===e.offset().top;return e.remove(),o=function(){return i},i},t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
      "),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widthi?"left":e>0?"right":"center",vertical:0>o?"top":s>0?"bottom":"middle"};u>g&&g>r(e+i)&&(l.horizontal="center"),d>m&&m>r(s+o)&&(l.vertical="middle"),l.important=a(r(e),r(i))>a(r(s),r(o))?"horizontal":"vertical",n.using.call(this,t,l)}),c.offset(t.extend(I,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,o=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-o-n;e.collisionWidth>o?l>0&&0>=h?(i=t.left+l+e.collisionWidth-o-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+o-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=a(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,o=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-o-n;e.collisionHeight>o?l>0&&0>=h?(i=t.top+l+e.collisionHeight-o-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+o-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=a(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,a=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-a-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-a-o,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,a=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-a-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-a-o,(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());var l=!1;t(document).on("mouseup",function(){l=!1}),t.widget("ui.mouse",{version:"1.12.0",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!l){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,s=1===e.which,n="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return s&&!n&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),l=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)if(e.originalEvent.altKey||e.originalEvent.ctrlKey||e.originalEvent.metaKey||e.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,l=!1,e.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.ui.plugin={add:function(e,i,s){var n,o=t.ui[e].prototype;for(n in s)o.plugins[n]=o.plugins[n]||[],o.plugins[n].push([i,s[n]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;o.length>n;n++)t.options[o[n][0]]&&o[n][1].apply(t.element,i)}},t.ui.safeActiveElement=function(t){var e;try{e=t.activeElement}catch(i){e=t.body}return e||(e=t.body),e.nodeName||(e=t.body),e},t.ui.safeBlur=function(e){e&&"body"!==e.nodeName.toLowerCase()&&t(e).trigger("blur")},t.widget("ui.draggable",t.ui.mouse,{version:"1.12.0",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"===this.options.helper&&this._setPositionRelative(),this.options.addClasses&&this._addClass("ui-draggable"),this._setHandleClassName(),this._mouseInit()},_setOption:function(t,e){this._super(t,e),"handle"===t&&(this._removeHandleClassName(),this._setHandleClassName())},_destroy:function(){return(this.helper||this.element).is(".ui-draggable-dragging")?(this.destroyOnClear=!0,void 0):(this._removeHandleClassName(),this._mouseDestroy(),void 0)},_mouseCapture:function(e){var i=this.options;return this._blurActiveElement(e),this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(this._blockFrames(i.iframeFix===!0?"iframe":i.iframeFix),!0):!1)},_blockFrames:function(e){this.iframeBlocks=this.document.find(e).map(function(){var e=t(this);return t("
      ").css("position","absolute").appendTo(e.parent()).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(e){var i=t.ui.safeActiveElement(this.document[0]),s=t(e.target);this._getHandle(e)&&s.closest(i).length||t.ui.safeBlur(i)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=this.helper.parents().filter(function(){return"fixed"===t(this).css("position")}).length>0,this.positionAbs=this.element.offset(),this._refreshOffsets(e),this.originalPosition=this.position=this._generatePosition(e,!1),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_refreshOffsets:function(t){this.offset={top:this.positionAbs.top-this.margins.top,left:this.positionAbs.left-this.margins.left,scroll:!1,parent:this._getParentOffset(),relative:this._getRelativeOffset()},this.offset.click={left:t.pageX-this.offset.left,top:t.pageY-this.offset.top}},_mouseDrag:function(e,i){if(this.hasFixedAncestor&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e,!0),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp(new t.Event("mouseup",e)),!1;this.position=s.position}return this.helper[0].style.left=this.position.left+"px",this.helper[0].style.top=this.position.top+"px",t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1},_mouseUp:function(e){return this._unblockFrames(),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),this.handleElement.is(e.target)&&this.element.trigger("focus"),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp(new t.Event("mouseup",{target:this.element[0]})):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_setHandleClassName:function(){this.handleElement=this.options.handle?this.element.find(this.options.handle):this.element,this._addClass(this.handleElement,"ui-draggable-handle")},_removeHandleClassName:function(){this._removeClass(this.handleElement,"ui-draggable-handle")},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper),n=s?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return n.parents("body").length||n.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s&&n[0]===this.element[0]&&this._setPositionRelative(),n[0]===this.element[0]||/(fixed|absolute)/.test(n.css("position"))||n.css("position","absolute"),n},_setPositionRelative:function(){/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative")},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_isRootNode:function(t){return/(html|body)/i.test(t.tagName)||t===this.document[0]},_getParentOffset:function(){var e=this.offsetParent.offset(),i=this.document[0];return"absolute"===this.cssPosition&&this.scrollParent[0]!==i&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),this._isRootNode(this.offsetParent[0])&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"!==this.cssPosition)return{top:0,left:0};var t=this.element.position(),e=this._isRootNode(this.scrollParent[0]);return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+(e?0:this.scrollParent.scrollTop()),left:t.left-(parseInt(this.helper.css("left"),10)||0)+(e?0:this.scrollParent.scrollLeft())}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options,o=this.document[0];return this.relativeContainer=null,n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):"document"===n.containment?(this.containment=[0,0,t(o).width()-this.helperProportions.width-this.margins.left,(t(o).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):n.containment.constructor===Array?(this.containment=n.containment,void 0):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e=/(scroll|auto)/.test(i.css("overflow")),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relativeContainer=i),void 0):(this.containment=null,void 0)},_convertPositionTo:function(t,e){e||(e=this.position);var i="absolute"===t?1:-1,s=this._isRootNode(this.scrollParent[0]);return{top:e.top+this.offset.relative.top*i+this.offset.parent.top*i-("fixed"===this.cssPosition?-this.offset.scroll.top:s?0:this.offset.scroll.top)*i,left:e.left+this.offset.relative.left*i+this.offset.parent.left*i-("fixed"===this.cssPosition?-this.offset.scroll.left:s?0:this.offset.scroll.left)*i}},_generatePosition:function(t,e){var i,s,n,o,a=this.options,r=this._isRootNode(this.scrollParent[0]),l=t.pageX,h=t.pageY;return r&&this.offset.scroll||(this.offset.scroll={top:this.scrollParent.scrollTop(),left:this.scrollParent.scrollLeft()}),e&&(this.containment&&(this.relativeContainer?(s=this.relativeContainer.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,t.pageX-this.offset.click.lefti[2]&&(l=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),a.grid&&(n=a.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/a.grid[1])*a.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-a.grid[1]:n+a.grid[1]:n,o=a.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/a.grid[0])*a.grid[0]:this.originalPageX,l=i?o-this.offset.click.left>=i[0]||o-this.offset.click.left>i[2]?o:o-this.offset.click.left>=i[0]?o-a.grid[0]:o+a.grid[0]:o),"y"===a.axis&&(l=this.originalPageX),"x"===a.axis&&(h=this.originalPageY)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:r?0:this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:r?0:this.offset.scroll.left)} +},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s,this],!0),/^(drag|start|stop)/.test(e)&&(this.positionAbs=this._convertPositionTo("absolute"),s.offset=this.positionAbs),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i,s){var n=t.extend({},i,{item:s.element});s.sortables=[],t(s.options.connectToSortable).each(function(){var i=t(this).sortable("instance");i&&!i.options.disabled&&(s.sortables.push(i),i.refreshPositions(),i._trigger("activate",e,n))})},stop:function(e,i,s){var n=t.extend({},i,{item:s.element});s.cancelHelperRemoval=!1,t.each(s.sortables,function(){var t=this;t.isOver?(t.isOver=0,s.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,n))})},drag:function(e,i,s){t.each(s.sortables,function(){var n=!1,o=this;o.positionAbs=s.positionAbs,o.helperProportions=s.helperProportions,o.offset.click=s.offset.click,o._intersectsWith(o.containerCache)&&(n=!0,t.each(s.sortables,function(){return this.positionAbs=s.positionAbs,this.helperProportions=s.helperProportions,this.offset.click=s.offset.click,this!==o&&this._intersectsWith(this.containerCache)&&t.contains(o.element[0],this.element[0])&&(n=!1),n})),n?(o.isOver||(o.isOver=1,s._parent=i.helper.parent(),o.currentItem=i.helper.appendTo(o.element).data("ui-sortable-item",!0),o.options._helper=o.options.helper,o.options.helper=function(){return i.helper[0]},e.target=o.currentItem[0],o._mouseCapture(e,!0),o._mouseStart(e,!0,!0),o.offset.click.top=s.offset.click.top,o.offset.click.left=s.offset.click.left,o.offset.parent.left-=s.offset.parent.left-o.offset.parent.left,o.offset.parent.top-=s.offset.parent.top-o.offset.parent.top,s._trigger("toSortable",e),s.dropped=o.element,t.each(s.sortables,function(){this.refreshPositions()}),s.currentItem=s.element,o.fromOutside=s),o.currentItem&&(o._mouseDrag(e),i.position=o.position)):o.isOver&&(o.isOver=0,o.cancelHelperRemoval=!0,o.options._revert=o.options.revert,o.options.revert=!1,o._trigger("out",e,o._uiHash(o)),o._mouseStop(e,!0),o.options.revert=o.options._revert,o.options.helper=o.options._helper,o.placeholder&&o.placeholder.remove(),i.helper.appendTo(s._parent),s._refreshOffsets(e),i.position=s._generatePosition(e,!0),s._trigger("fromSortable",e),s.dropped=!1,t.each(s.sortables,function(){this.refreshPositions()}))})}}),t.ui.plugin.add("draggable","cursor",{start:function(e,i,s){var n=t("body"),o=s.options;n.css("cursor")&&(o._cursor=n.css("cursor")),n.css("cursor",o.cursor)},stop:function(e,i,s){var n=s.options;n._cursor&&t("body").css("cursor",n._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("opacity")&&(o._opacity=n.css("opacity")),n.css("opacity",o.opacity)},stop:function(e,i,s){var n=s.options;n._opacity&&t(i.helper).css("opacity",n._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(e,i,s){var n=s.options,o=!1,a=s.scrollParentNotHidden[0],r=s.document[0];a!==r&&"HTML"!==a.tagName?(n.axis&&"x"===n.axis||(s.overflowOffset.top+a.offsetHeight-e.pageY=0;d--)l=s.snapElements[d].left-s.margins.left,h=l+s.snapElements[d].width,c=s.snapElements[d].top-s.margins.top,u=c+s.snapElements[d].height,l-g>_||m>h+g||c-g>b||v>u+g||!t.contains(s.snapElements[d].item.ownerDocument,s.snapElements[d].item)?(s.snapElements[d].snapping&&s.options.snap.release&&s.options.snap.release.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=!1):("inner"!==f.snapMode&&(n=g>=Math.abs(c-b),o=g>=Math.abs(u-v),a=g>=Math.abs(l-_),r=g>=Math.abs(h-m),n&&(i.position.top=s._convertPositionTo("relative",{top:c-s.helperProportions.height,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l-s.helperProportions.width}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h}).left)),p=n||o||a||r,"outer"!==f.snapMode&&(n=g>=Math.abs(c-v),o=g>=Math.abs(u-b),a=g>=Math.abs(l-m),r=g>=Math.abs(h-_),n&&(i.position.top=s._convertPositionTo("relative",{top:c,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u-s.helperProportions.height,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h-s.helperProportions.width}).left)),!s.snapElements[d].snapping&&(n||o||a||r||p)&&s.options.snap.snap&&s.options.snap.snap.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=n||o||a||r||p)}}),t.ui.plugin.add("draggable","stack",{start:function(e,i,s){var n,o=s.options,a=t.makeArray(t(o.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});a.length&&(n=parseInt(t(a[0]).css("zIndex"),10)||0,t(a).each(function(e){t(this).css("zIndex",n+e)}),this.css("zIndex",n+a.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("zIndex")&&(o._zIndex=n.css("zIndex")),n.css("zIndex",o.zIndex)},stop:function(e,i,s){var n=s.options;n._zIndex&&t(i.helper).css("zIndex",n._zIndex)}}),t.ui.draggable,t.widget("ui.droppable",{version:"1.12.0",widgetEventPrefix:"drop",options:{accept:"*",addClasses:!0,greedy:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],void 0):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},this._addToManager(i.scope),i.addClasses&&this._addClass("ui-droppable")},_addToManager:function(e){t.ui.ddmanager.droppables[e]=t.ui.ddmanager.droppables[e]||[],t.ui.ddmanager.droppables[e].push(this)},_splice:function(t){for(var e=0;t.length>e;e++)t[e]===this&&t.splice(e,1)},_destroy:function(){var e=t.ui.ddmanager.droppables[this.options.scope];this._splice(e)},_setOption:function(e,i){if("accept"===e)this.accept=t.isFunction(i)?i:function(t){return t.is(i)};else if("scope"===e){var s=t.ui.ddmanager.droppables[this.options.scope];this._splice(s),this._addToManager(i)}this._super(e,i)},_activate:function(e){var i=t.ui.ddmanager.current;this._addActiveClass(),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this._removeActiveClass(),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._addHoverClass(),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._removeHoverClass(),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var i=t(this).droppable("instance");return i.options.greedy&&!i.options.disabled&&i.options.scope===s.options.scope&&i.accept.call(i.element[0],s.currentItem||s.element)&&h(s,t.extend(i,{offset:i.element.offset()}),i.options.tolerance,e)?(n=!0,!1):void 0}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this._removeActiveClass(),this._removeHoverClass(),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}},_addHoverClass:function(){this._addClass("ui-droppable-hover")},_removeHoverClass:function(){this._removeClass("ui-droppable-hover")},_addActiveClass:function(){this._addClass("ui-droppable-active")},_removeActiveClass:function(){this._removeClass("ui-droppable-active")}});var h=t.ui.intersect=function(){function t(t,e,i){return t>=e&&e+i>t}return function(e,i,s,n){if(!i.offset)return!1;var o=(e.positionAbs||e.position.absolute).left+e.margins.left,a=(e.positionAbs||e.position.absolute).top+e.margins.top,r=o+e.helperProportions.width,l=a+e.helperProportions.height,h=i.offset.left,c=i.offset.top,u=h+i.proportions().width,d=c+i.proportions().height;switch(s){case"fit":return o>=h&&u>=r&&a>=c&&d>=l;case"intersect":return o+e.helperProportions.width/2>h&&u>r-e.helperProportions.width/2&&a+e.helperProportions.height/2>c&&d>l-e.helperProportions.height/2;case"pointer":return t(n.pageY,c,i.proportions().height)&&t(n.pageX,h,i.proportions().width);case"touch":return(a>=c&&d>=a||l>=c&&d>=l||c>a&&l>d)&&(o>=h&&u>=o||r>=h&&u>=r||h>o&&r>u);default:return!1}}}();t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,o=t.ui.ddmanager.droppables[e.options.scope]||[],a=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;o.length>s;s++)if(!(o[s].options.disabled||e&&!o[s].accept.call(o[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===o[s].element[0]){o[s].proportions().height=0;continue t}o[s].visible="none"!==o[s].element.css("display"),o[s].visible&&("mousedown"===a&&o[s]._activate.call(o[s],i),o[s].offset=o[s].element.offset(),o[s].proportions({width:o[s].element[0].offsetWidth,height:o[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&h(e,this,this.options.tolerance,i)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").on("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,o,a=h(e,this,this.options.tolerance,i),r=!a&&this.isover?"isout":a&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,o=this.element.parents(":data(ui-droppable)").filter(function(){return t(this).droppable("instance").options.scope===n}),o.length&&(s=t(o[0]).droppable("instance"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").off("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}},t.uiBackCompat!==!1&&t.widget("ui.droppable",t.ui.droppable,{options:{hoverClass:!1,activeClass:!1},_addActiveClass:function(){this._super(),this.options.activeClass&&this.element.addClass(this.options.activeClass)},_removeActiveClass:function(){this._super(),this.options.activeClass&&this.element.removeClass(this.options.activeClass)},_addHoverClass:function(){this._super(),this.options.hoverClass&&this.element.addClass(this.options.hoverClass)},_removeHoverClass:function(){this._super(),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass)}}),t.ui.droppable,t.widget("ui.resizable",t.ui.mouse,{version:"1.12.0",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,classes:{"ui-resizable-se":"ui-icon ui-icon-gripsmall-diagonal-se"},containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_num:function(t){return parseFloat(t)||0},_isNumber:function(t){return!isNaN(parseFloat(t))},_hasScroll:function(e,i){if("hidden"===t(e).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",n=!1;return e[s]>0?!0:(e[s]=1,n=e[s]>0,e[s]=0,n)},_create:function(){var e,i=this.options,s=this;this._addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!i.aspectRatio,aspectRatio:i.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:i.helper||i.ghost||i.animate?i.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/^(canvas|textarea|input|select|button|img)$/i)&&(this.element.wrap(t("
      ").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,e={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(e),this.originalElement.css("margin",0),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css(e),this._proportionallyResize()),this._setupHandles(),i.autoHide&&t(this.element).on("mouseenter",function(){i.disabled||(s._removeClass("ui-resizable-autohide"),s._handles.show())}).on("mouseleave",function(){i.disabled||s.resizing||(s._addClass("ui-resizable-autohide"),s._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeData("resizable").removeData("ui-resizable").off(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;default:}},_setupHandles:function(){var e,i,s,n,o,a=this.options,r=this;if(this.handles=a.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=t(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),s=this.handles.split(","),this.handles={},i=0;s.length>i;i++)e=t.trim(s[i]),n="ui-resizable-"+e,o=t("
      "),this._addClass(o,"ui-resizable-handle "+n),o.css({zIndex:a.zIndex}),this.handles[e]=".ui-resizable-"+e,this.element.append(o);this._renderAxis=function(e){var i,s,n,o;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String?this.handles[i]=this.element.children(this.handles[i]).first().show():(this.handles[i].jquery||this.handles[i].nodeType)&&(this.handles[i]=t(this.handles[i]),this._on(this.handles[i],{mousedown:r._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(s=t(this.handles[i],this.element),o=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,o),this._proportionallyResize()),this._handles=this._handles.add(this.handles[i])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){r.resizing||(this.className&&(o=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),r.axis=o&&o[1]?o[1]:"se")}),a.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._handles.remove()},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(e){var i,s,n,o=this.options,a=this.element;return this.resizing=!0,this._renderProxy(),i=this._num(this.helper.css("left")),s=this._num(this.helper.css("top")),o.containment&&(i+=t(o.containment).scrollLeft()||0,s+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:i,top:s},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:a.width(),height:a.height()},this.originalSize=this._helper?{width:a.outerWidth(),height:a.outerHeight()}:{width:a.width(),height:a.height()},this.sizeDiff={width:a.outerWidth()-a.width(),height:a.outerHeight()-a.height()},this.originalPosition={left:i,top:s},this.originalMousePosition={left:e.pageX,top:e.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,n=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===n?this.axis+"-resize":n),this._addClass("ui-resizable-resizing"),this._propagate("start",e),!0},_mouseDrag:function(e){var i,s,n=this.originalMousePosition,o=this.axis,a=e.pageX-n.left||0,r=e.pageY-n.top||0,l=this._change[o];return this._updatePrevProperties(),l?(i=l.apply(this,[e,a,r]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",e,this.ui()),this._applyChanges()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,o,a,r,l,h=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&this._hasScroll(i[0],"left")?0:c.sizeDiff.height,o=s?0:c.sizeDiff.width,a={width:c.helper.width()-o,height:c.helper.height()-n},r=parseFloat(c.element.css("left"))+(c.position.left-c.originalPosition.left)||null,l=parseFloat(c.element.css("top"))+(c.position.top-c.originalPosition.top)||null,h.animate||this.element.css(t.extend(a,{top:l,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!h.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px"),this.helper.css(t),t},_updateVirtualBoundaries:function(t){var e,i,s,n,o,a=this.options;o={minWidth:this._isNumber(a.minWidth)?a.minWidth:0,maxWidth:this._isNumber(a.maxWidth)?a.maxWidth:1/0,minHeight:this._isNumber(a.minHeight)?a.minHeight:0,maxHeight:this._isNumber(a.maxHeight)?a.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,s=o.minWidth/this.aspectRatio,i=o.maxHeight*this.aspectRatio,n=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),s>o.minHeight&&(o.minHeight=s),o.maxWidth>i&&(o.maxWidth=i),o.maxHeight>n&&(o.maxHeight=n)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),this._isNumber(t.left)&&(this.position.left=t.left),this._isNumber(t.top)&&(this.position.top=t.top),this._isNumber(t.height)&&(this.size.height=t.height),this._isNumber(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,i=this.size,s=this.axis;return this._isNumber(t.height)?t.width=t.height*this.aspectRatio:this._isNumber(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===s&&(t.left=e.left+(i.width-t.width),t.top=null),"nw"===s&&(t.top=e.top+(i.height-t.height),t.left=e.left+(i.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,i=this.axis,s=this._isNumber(t.width)&&e.maxWidth&&e.maxWidtht.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,l=this.originalPosition.top+this.originalSize.height,h=/sw|nw|w/.test(i),c=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&h&&(t.left=r-e.minWidth),s&&h&&(t.left=r-e.maxWidth),a&&c&&(t.top=l-e.minHeight),n&&c&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];4>e;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;this._proportionallyResizeElements.length>e;e++)t=this._proportionallyResizeElements[e],this.outerDimensions||(this.outerDimensions=this._getPaddingPlusBorderDimensions(t)),t.css({height:i.height()-this.outerDimensions.height||0,width:i.width()-this.outerDimensions.width||0})},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("
      "),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).resizable("instance"),s=i.options,n=i._proportionallyResizeElements,o=n.length&&/textarea/i.test(n[0].nodeName),a=o&&i._hasScroll(n[0],"left")?0:i.sizeDiff.height,r=o?0:i.sizeDiff.width,l={width:i.size.width-r,height:i.size.height-a},h=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,c=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(l,c&&h?{top:c,left:h}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var e,i,s,n,o,a,r,l=t(this).resizable("instance"),h=l.options,c=l.element,u=h.containment,d=u instanceof t?u.get(0):/parent/.test(u)?c.parent().get(0):u;d&&(l.containerElement=t(d),/document/.test(u)||u===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(e=t(d),i=[],t(["Top","Right","Left","Bottom"]).each(function(t,s){i[t]=l._num(e.css("padding"+s))}),l.containerOffset=e.offset(),l.containerPosition=e.position(),l.containerSize={height:e.innerHeight()-i[3],width:e.innerWidth()-i[1]},s=l.containerOffset,n=l.containerSize.height,o=l.containerSize.width,a=l._hasScroll(d,"left")?d.scrollWidth:o,r=l._hasScroll(d)?d.scrollHeight:n,l.parentData={element:d,left:s.left,top:s.top,width:a,height:r}))},resize:function(e){var i,s,n,o,a=t(this).resizable("instance"),r=a.options,l=a.containerOffset,h=a.position,c=a._aspectRatio||e.shiftKey,u={top:0,left:0},d=a.containerElement,p=!0;d[0]!==document&&/static/.test(d.css("position"))&&(u=l),h.left<(a._helper?l.left:0)&&(a.size.width=a.size.width+(a._helper?a.position.left-l.left:a.position.left-u.left),c&&(a.size.height=a.size.width/a.aspectRatio,p=!1),a.position.left=r.helper?l.left:0),h.top<(a._helper?l.top:0)&&(a.size.height=a.size.height+(a._helper?a.position.top-l.top:a.position.top),c&&(a.size.width=a.size.height*a.aspectRatio,p=!1),a.position.top=a._helper?l.top:0),n=a.containerElement.get(0)===a.element.parent().get(0),o=/relative|absolute/.test(a.containerElement.css("position")),n&&o?(a.offset.left=a.parentData.left+a.position.left,a.offset.top=a.parentData.top+a.position.top):(a.offset.left=a.element.offset().left,a.offset.top=a.element.offset().top),i=Math.abs(a.sizeDiff.width+(a._helper?a.offset.left-u.left:a.offset.left-l.left)),s=Math.abs(a.sizeDiff.height+(a._helper?a.offset.top-u.top:a.offset.top-l.top)),i+a.size.width>=a.parentData.width&&(a.size.width=a.parentData.width-i,c&&(a.size.height=a.size.width/a.aspectRatio,p=!1)),s+a.size.height>=a.parentData.height&&(a.size.height=a.parentData.height-s,c&&(a.size.width=a.size.height*a.aspectRatio,p=!1)),p||(a.position.left=a.prevPosition.left,a.position.top=a.prevPosition.top,a.size.width=a.prevSize.width,a.size.height=a.prevSize.height)},stop:function(){var e=t(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.containerPosition,o=e.containerElement,a=t(e.helper),r=a.offset(),l=a.outerWidth()-e.sizeDiff.width,h=a.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:l,height:h}),e._helper&&!i.animate&&/static/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:l,height:h})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).resizable("instance"),i=e.options;t(i.alsoResize).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseFloat(e.width()),height:parseFloat(e.height()),left:parseFloat(e.css("left")),top:parseFloat(e.css("top"))})})},resize:function(e,i){var s=t(this).resizable("instance"),n=s.options,o=s.originalSize,a=s.originalPosition,r={height:s.size.height-o.height||0,width:s.size.width-o.width||0,top:s.position.top-a.top||0,left:s.position.left-a.left||0};t(n.alsoResize).each(function(){var e=t(this),s=t(this).data("ui-resizable-alsoresize"),n={},o=e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(s[e]||0)+(r[e]||0);i&&i>=0&&(n[e]=i||null)}),e.css(n)})},stop:function(){t(this).removeData("ui-resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).resizable("instance"),i=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:i.height,width:i.width,margin:0,left:0,top:0}),e._addClass(e.ghost,"ui-resizable-ghost"),t.uiBackCompat!==!1&&"string"==typeof e.options.ghost&&e.ghost.addClass(this.options.ghost),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).resizable("instance");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).resizable("instance");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e,i=t(this).resizable("instance"),s=i.options,n=i.size,o=i.originalSize,a=i.originalPosition,r=i.axis,l="number"==typeof s.grid?[s.grid,s.grid]:s.grid,h=l[0]||1,c=l[1]||1,u=Math.round((n.width-o.width)/h)*h,d=Math.round((n.height-o.height)/c)*c,p=o.width+u,f=o.height+d,g=s.maxWidth&&p>s.maxWidth,m=s.maxHeight&&f>s.maxHeight,_=s.minWidth&&s.minWidth>p,v=s.minHeight&&s.minHeight>f;s.grid=l,_&&(p+=h),v&&(f+=c),g&&(p-=h),m&&(f-=c),/^(se|s|e)$/.test(r)?(i.size.width=p,i.size.height=f):/^(ne)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.top=a.top-d):/^(sw)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.left=a.left-u):((0>=f-c||0>=p-h)&&(e=i._getPaddingPlusBorderDimensions(this)),f-c>0?(i.size.height=f,i.position.top=a.top-d):(f=c-e.height,i.size.height=f,i.position.top=a.top+o.height-f),p-h>0?(i.size.width=p,i.position.left=a.left-u):(p=h-e.width,i.size.width=p,i.position.left=a.left+o.width-p)) +}}),t.ui.resizable,t.widget("ui.selectable",t.ui.mouse,{version:"1.12.0",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e=this;this._addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e.elementPos=t(e.element[0]).offset(),e.selectees=t(e.options.filter,e.element[0]),e._addClass(e.selectees,"ui-selectee"),e.selectees.each(function(){var i=t(this),s=i.offset(),n={left:s.left-e.elementPos.left,top:s.top-e.elementPos.top};t.data(this,"selectable-item",{element:this,$element:i,left:n.left,top:n.top,right:n.left+i.outerWidth(),bottom:n.top+i.outerHeight(),startselected:!1,selected:i.hasClass("ui-selected"),selecting:i.hasClass("ui-selecting"),unselecting:i.hasClass("ui-unselecting")})})},this.refresh(),this._mouseInit(),this.helper=t("
      "),this._addClass(this.helper,"ui-selectable-helper")},_destroy:function(){this.selectees.removeData("selectable-item"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.elementPos=t(this.element[0]).offset(),this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(i._removeClass(s.$element,"ui-selected"),s.selected=!1,i._addClass(s.$element,"ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),i._removeClass(n.$element,s?"ui-unselecting":"ui-selected")._addClass(n.$element,s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):void 0}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,o=this.opos[0],a=this.opos[1],r=e.pageX,l=e.pageY;return o>r&&(i=r,r=o,o=i),a>l&&(i=l,l=a,a=i),this.helper.css({left:o,top:a,width:r-o,height:l-a}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),h=!1,c={};i&&i.element!==s.element[0]&&(c.left=i.left+s.elementPos.left,c.right=i.right+s.elementPos.left,c.top=i.top+s.elementPos.top,c.bottom=i.bottom+s.elementPos.top,"touch"===n.tolerance?h=!(c.left>r||o>c.right||c.top>l||a>c.bottom):"fit"===n.tolerance&&(h=c.left>o&&r>c.right&&c.top>a&&l>c.bottom),h?(i.selected&&(s._removeClass(i.$element,"ui-selected"),i.selected=!1),i.unselecting&&(s._removeClass(i.$element,"ui-unselecting"),i.unselecting=!1),i.selecting||(s._addClass(i.$element,"ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,s._addClass(i.$element,"ui-selected"),i.selected=!0):(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,i.startselected&&(s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(s._removeClass(i.$element,"ui-selected"),i.selected=!1,s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-selecting")._addClass(s.$element,"ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}}),t.widget("ui.sortable",t.ui.mouse,{version:"1.12.0",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return t>=e&&e+i>t},_isFloating:function(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))},_create:function(){this.containerCache={},this._addClass("ui-sortable"),this.refresh(),this.offset=this.element.offset(),this._mouseInit(),this._setHandleClassName(),this.ready=!0},_setOption:function(t,e){this._super(t,e),"handle"===t&&this._setHandleClassName()},_setHandleClassName:function(){var e=this;this._removeClass(this.element.find(".ui-sortable-handle"),"ui-sortable-handle"),t.each(this.items,function(){e._addClass(this.instance.options.handle?this.item.find(this.instance.options.handle):this.item,"ui-sortable-handle")})},_destroy:function(){this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):void 0}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this._addClass(this.helper,"ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?(this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,l=r+t.height,h=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+h>r&&l>s+h,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&l>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var e,i,s="x"===this.options.axis||this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top,t.height),n="y"===this.options.axis||this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left,t.width),o=s&&n;return o?(e=this._getDragVerticalDirection(),i=this._getDragHorizontalDirection(),this.floating?"right"===i||"down"===e?2:1:e&&("down"===e?2:1)):!1},_intersectsWithSides:function(t){var e=this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),i=this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),s=this._getDragVerticalDirection(),n=this._getDragHorizontalDirection();return this.floating&&n?"right"===n&&i||"left"===n&&!i:s&&("down"===s&&e||"up"===s&&!e)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this._setHandleClassName(),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],l=[],h=this._connectWith();if(h&&e)for(s=h.length-1;s>=0;s--)for(o=t(h[s],this.document[0]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&l.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(l.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=l.length-1;s>=0;s--)l[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,l,h,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i],this.document[0]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,h=r.length;h>s;s++)l=t(r[s]),l.data(this.widgetName+"-item",a),c.push({item:l,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.floating=this.items.length?"x"===this.options.axis||this._isFloating(this.items[0].item):!1,this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]);return e._addClass(n,"ui-sortable-placeholder",i||e.currentItem[0].className)._removeClass(n,"ui-sortable-helper"),"tbody"===s?e._createTrPlaceholder(e.currentItem.find("tr").eq(0),t("",e.document[0]).appendTo(n)):"tr"===s?e._createTrPlaceholder(e.currentItem,n):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_createTrPlaceholder:function(e,i){var s=this;e.children().each(function(){t(" ",s.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(i)})},_contactContainers:function(e){var i,s,n,o,a,r,l,h,c,u,d=null,p=null;for(i=this.containers.length-1;i>=0;i--)if(!t.contains(this.currentItem[0],this.containers[i].element[0]))if(this._intersectsWith(this.containers[i].containerCache)){if(d&&t.contains(this.containers[i].element[0],d.element[0]))continue;d=this.containers[i],p=i}else this.containers[i].containerCache.over&&(this.containers[i]._trigger("out",e,this._uiHash(this)),this.containers[i].containerCache.over=0);if(d)if(1===this.containers.length)this.containers[p].containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1);else{for(n=1e4,o=null,c=d.floating||this._isFloating(this.currentItem),a=c?"left":"top",r=c?"width":"height",u=c?"pageX":"pageY",s=this.items.length-1;s>=0;s--)t.contains(this.containers[p].element[0],this.items[s].item[0])&&this.items[s].item[0]!==this.currentItem[0]&&(l=this.items[s].item.offset()[a],h=!1,e[u]-l>this.items[s][r]/2&&(h=!0),n>Math.abs(e[u]-l)&&(n=Math.abs(e[u]-l),o=this.items[s],this.direction=h?"up":"down"));if(!o&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[p])return this.currentContainer.containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash()),this.currentContainer.containerCache.over=1),void 0;o?this._rearrange(e,o,null,!0):this._rearrange(e,null,this.containers[p].element,!0),this._trigger("change",e,this._uiHash()),this.containers[p]._trigger("change",e,this._uiHash(this)),this.currentContainer=this.containers[p],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===this.document[0].body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,"document"===n.containment?this.document.width():this.window.width()-this.helperProportions.width-this.margins.left,("document"===n.containment?this.document.height()||document.body.parentNode.scrollHeight:this.window.height()||this.document[0].body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():l?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():l?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!this.cancelHelperRemoval},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}}),t.widget("ui.menu",{version:"1.12.0",defaultElement:"
        ",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",n=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t)) +},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,l=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=l.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=l.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n;this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t("
          ").appendTo(this._appendTo()).menu({role:null}).hide().menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(e){e.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,this.element[0]!==t.ui.safeActiveElement(this.document[0])&&this.element.trigger("focus")})},menufocus:function(e,i){var s,n;return this.isNewMenu&&(this.isNewMenu=!1,e.originalEvent&&/^mouse/.test(e.originalEvent.type))?(this.menu.blur(),this.document.one("mousemove",function(){t(e.target).trigger(e.originalEvent)}),void 0):(n=i.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",e,{item:n})&&e.originalEvent&&/^key/.test(e.originalEvent.type)&&this._value(n.value),s=i.item.attr("aria-label")||n.value,s&&t.trim(s).length&&(this.liveRegion.children().hide(),t("
          ").text(s).appendTo(this.liveRegion)),void 0)},menuselect:function(e,i){var s=i.item.data("ui-autocomplete-item"),n=this.previous;this.element[0]!==t.ui.safeActiveElement(this.document[0])&&(this.element.trigger("focus"),this.previous=n,this._delay(function(){this.previous=n,this.selectedItem=s})),!1!==this._trigger("select",e,{item:s})&&this._value(s.value),this.term=this._value(),this.close(e),this.selectedItem=s}}),this.liveRegion=t("
          ",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(e){var i=this.menu.element[0];return e.target===this.element[0]||e.target===i||t.contains(i,e.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var e=this.options.appendTo;return e&&(e=e.jquery||e.nodeType?t(e):this.document.find(e).eq(0)),e&&e[0]||(e=this.element.closest(".ui-front, dialog")),e.length||(e=this.document[0].body),e},_initSource:function(){var e,i,s=this;t.isArray(this.options.source)?(e=this.options.source,this.source=function(i,s){s(t.ui.autocomplete.filter(e,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(e,n){s.xhr&&s.xhr.abort(),s.xhr=t.ajax({url:i,data:e,dataType:"json",success:function(t){n(t)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(t){clearTimeout(this.searching),this.searching=this._delay(function(){var e=this.term===this._value(),i=this.menu.element.is(":visible"),s=t.altKey||t.ctrlKey||t.metaKey||t.shiftKey;(!e||e&&!i&&!s)&&(this.selectedItem=null,this.search(null,t))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length").append(t("
          ").text(i.label)).appendTo(e)},_move:function(t,e){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),this.menu.blur(),void 0):(this.menu[t](e),void 0):(this.search(null,e),void 0)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),t.extend(t.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(e,i){var s=RegExp(t.ui.autocomplete.escapeRegex(i),"i");return t.grep(e,function(t){return s.test(t.label||t.value||t)})}}),t.widget("ui.autocomplete",t.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(t>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var i;this._superApply(arguments),this.options.disabled||this.cancelSearch||(i=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.children().hide(),t("
          ").text(i).appendTo(this.liveRegion))}}),t.ui.autocomplete,t.extend(t.ui,{datepicker:{version:"1.12.0"}});var c;t.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(t){return o(this._defaults,t||{}),this},_attachDatepicker:function(e,i){var s,n,o;s=e.nodeName.toLowerCase(),n="div"===s||"span"===s,e.id||(this.uuid+=1,e.id="dp"+this.uuid),o=this._newInst(t(e),n),o.settings=t.extend({},i||{}),"input"===s?this._connectDatepicker(e,o):n&&this._inlineDatepicker(e,o)},_newInst:function(e,i){var n=e[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:n,input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?s(t("
          ")):this.dpDiv}},_connectDatepicker:function(e,i){var s=t(e);i.append=t([]),i.trigger=t([]),s.hasClass(this.markerClassName)||(this._attachments(s,i),s.addClass(this.markerClassName).on("keydown",this._doKeyDown).on("keypress",this._doKeyPress).on("keyup",this._doKeyUp),this._autoSize(i),t.data(e,"datepicker",i),i.settings.disabled&&this._disableDatepicker(e))},_attachments:function(e,i){var s,n,o,a=this._get(i,"appendText"),r=this._get(i,"isRTL");i.append&&i.append.remove(),a&&(i.append=t(""+a+""),e[r?"before":"after"](i.append)),e.off("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),s=this._get(i,"showOn"),("focus"===s||"both"===s)&&e.on("focus",this._showDatepicker),("button"===s||"both"===s)&&(n=this._get(i,"buttonText"),o=this._get(i,"buttonImage"),i.trigger=t(this._get(i,"buttonImageOnly")?t("").addClass(this._triggerClass).attr({src:o,alt:n,title:n}):t("").addClass(this._triggerClass).html(o?t("").attr({src:o,alt:n,title:n}):n)),e[r?"before":"after"](i.trigger),i.trigger.on("click",function(){return t.datepicker._datepickerShowing&&t.datepicker._lastInput===e[0]?t.datepicker._hideDatepicker():t.datepicker._datepickerShowing&&t.datepicker._lastInput!==e[0]?(t.datepicker._hideDatepicker(),t.datepicker._showDatepicker(e[0])):t.datepicker._showDatepicker(e[0]),!1}))},_autoSize:function(t){if(this._get(t,"autoSize")&&!t.inline){var e,i,s,n,o=new Date(2009,11,20),a=this._get(t,"dateFormat");a.match(/[DM]/)&&(e=function(t){for(i=0,s=0,n=0;t.length>n;n++)t[n].length>i&&(i=t[n].length,s=n);return s},o.setMonth(e(this._get(t,a.match(/MM/)?"monthNames":"monthNamesShort"))),o.setDate(e(this._get(t,a.match(/DD/)?"dayNames":"dayNamesShort"))+20-o.getDay())),t.input.attr("size",this._formatDate(t,o).length)}},_inlineDatepicker:function(e,i){var s=t(e);s.hasClass(this.markerClassName)||(s.addClass(this.markerClassName).append(i.dpDiv),t.data(e,"datepicker",i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(e),i.dpDiv.css("display","block"))},_dialogDatepicker:function(e,i,s,n,a){var r,l,h,c,u,d=this._dialogInst;return d||(this.uuid+=1,r="dp"+this.uuid,this._dialogInput=t(""),this._dialogInput.on("keydown",this._doKeyDown),t("body").append(this._dialogInput),d=this._dialogInst=this._newInst(this._dialogInput,!1),d.settings={},t.data(this._dialogInput[0],"datepicker",d)),o(d.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(d,i):i,this._dialogInput.val(i),this._pos=a?a.length?a:[a.pageX,a.pageY]:null,this._pos||(l=document.documentElement.clientWidth,h=document.documentElement.clientHeight,c=document.documentElement.scrollLeft||document.body.scrollLeft,u=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[l/2-100+c,h/2-150+u]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),d.settings.onSelect=s,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),t.blockUI&&t.blockUI(this.dpDiv),t.data(this._dialogInput[0],"datepicker",d),this},_destroyDatepicker:function(e){var i,s=t(e),n=t.data(e,"datepicker");s.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),t.removeData(e,"datepicker"),"input"===i?(n.append.remove(),n.trigger.remove(),s.removeClass(this.markerClassName).off("focus",this._showDatepicker).off("keydown",this._doKeyDown).off("keypress",this._doKeyPress).off("keyup",this._doKeyUp)):("div"===i||"span"===i)&&s.removeClass(this.markerClassName).empty(),c===n&&(c=null))},_enableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!1,o.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}))},_disableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!0,o.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().addClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}),this._disabledInputs[this._disabledInputs.length]=e)},_isDisabledDatepicker:function(t){if(!t)return!1;for(var e=0;this._disabledInputs.length>e;e++)if(this._disabledInputs[e]===t)return!0;return!1},_getInst:function(e){try{return t.data(e,"datepicker")}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(e,i,s){var n,a,r,l,h=this._getInst(e);return 2===arguments.length&&"string"==typeof i?"defaults"===i?t.extend({},t.datepicker._defaults):h?"all"===i?t.extend({},h.settings):this._get(h,i):null:(n=i||{},"string"==typeof i&&(n={},n[i]=s),h&&(this._curInst===h&&this._hideDatepicker(),a=this._getDateDatepicker(e,!0),r=this._getMinMaxDate(h,"min"),l=this._getMinMaxDate(h,"max"),o(h.settings,n),null!==r&&void 0!==n.dateFormat&&void 0===n.minDate&&(h.settings.minDate=this._formatDate(h,r)),null!==l&&void 0!==n.dateFormat&&void 0===n.maxDate&&(h.settings.maxDate=this._formatDate(h,l)),"disabled"in n&&(n.disabled?this._disableDatepicker(e):this._enableDatepicker(e)),this._attachments(t(e),h),this._autoSize(h),this._setDate(h,a),this._updateAlternate(h),this._updateDatepicker(h)),void 0)},_changeDatepicker:function(t,e,i){this._optionDatepicker(t,e,i)},_refreshDatepicker:function(t){var e=this._getInst(t);e&&this._updateDatepicker(e)},_setDateDatepicker:function(t,e){var i=this._getInst(t);i&&(this._setDate(i,e),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(t,e){var i=this._getInst(t);return i&&!i.inline&&this._setDateFromField(i,e),i?this._getDate(i):null},_doKeyDown:function(e){var i,s,n,o=t.datepicker._getInst(e.target),a=!0,r=o.dpDiv.is(".ui-datepicker-rtl");if(o._keyEvent=!0,t.datepicker._datepickerShowing)switch(e.keyCode){case 9:t.datepicker._hideDatepicker(),a=!1;break;case 13:return n=t("td."+t.datepicker._dayOverClass+":not(."+t.datepicker._currentClass+")",o.dpDiv),n[0]&&t.datepicker._selectDay(e.target,o.selectedMonth,o.selectedYear,n[0]),i=t.datepicker._get(o,"onSelect"),i?(s=t.datepicker._formatDate(o),i.apply(o.input?o.input[0]:null,[s,o])):t.datepicker._hideDatepicker(),!1;case 27:t.datepicker._hideDatepicker();break;case 33:t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 34:t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&t.datepicker._clearDate(e.target),a=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&t.datepicker._gotoToday(e.target),a=e.ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?1:-1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,-7,"D"),a=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?-1:1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,7,"D"),a=e.ctrlKey||e.metaKey;break;default:a=!1}else 36===e.keyCode&&e.ctrlKey?t.datepicker._showDatepicker(this):a=!1;a&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(e){var i,s,n=t.datepicker._getInst(e.target);return t.datepicker._get(n,"constrainInput")?(i=t.datepicker._possibleChars(t.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),e.ctrlKey||e.metaKey||" ">s||!i||i.indexOf(s)>-1):void 0},_doKeyUp:function(e){var i,s=t.datepicker._getInst(e.target);if(s.input.val()!==s.lastVal)try{i=t.datepicker.parseDate(t.datepicker._get(s,"dateFormat"),s.input?s.input.val():null,t.datepicker._getFormatConfig(s)),i&&(t.datepicker._setDateFromField(s),t.datepicker._updateAlternate(s),t.datepicker._updateDatepicker(s))}catch(n){}return!0},_showDatepicker:function(i){if(i=i.target||i,"input"!==i.nodeName.toLowerCase()&&(i=t("input",i.parentNode)[0]),!t.datepicker._isDisabledDatepicker(i)&&t.datepicker._lastInput!==i){var s,n,a,r,l,h,c;s=t.datepicker._getInst(i),t.datepicker._curInst&&t.datepicker._curInst!==s&&(t.datepicker._curInst.dpDiv.stop(!0,!0),s&&t.datepicker._datepickerShowing&&t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])),n=t.datepicker._get(s,"beforeShow"),a=n?n.apply(i,[i,s]):{},a!==!1&&(o(s.settings,a),s.lastVal=null,t.datepicker._lastInput=i,t.datepicker._setDateFromField(s),t.datepicker._inDialog&&(i.value=""),t.datepicker._pos||(t.datepicker._pos=t.datepicker._findPos(i),t.datepicker._pos[1]+=i.offsetHeight),r=!1,t(i).parents().each(function(){return r|="fixed"===t(this).css("position"),!r}),l={left:t.datepicker._pos[0],top:t.datepicker._pos[1]},t.datepicker._pos=null,s.dpDiv.empty(),s.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),t.datepicker._updateDatepicker(s),l=t.datepicker._checkOffset(s,l,r),s.dpDiv.css({position:t.datepicker._inDialog&&t.blockUI?"static":r?"fixed":"absolute",display:"none",left:l.left+"px",top:l.top+"px"}),s.inline||(h=t.datepicker._get(s,"showAnim"),c=t.datepicker._get(s,"duration"),s.dpDiv.css("z-index",e(t(i))+1),t.datepicker._datepickerShowing=!0,t.effects&&t.effects.effect[h]?s.dpDiv.show(h,t.datepicker._get(s,"showOptions"),c):s.dpDiv[h||"show"](h?c:null),t.datepicker._shouldFocusInput(s)&&s.input.trigger("focus"),t.datepicker._curInst=s))}},_updateDatepicker:function(e){this.maxRows=4,c=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e);var i,s=this._getNumberOfMonths(e),o=s[1],a=17,r=e.dpDiv.find("."+this._dayOverClass+" a");r.length>0&&n.apply(r.get(0)),e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),o>1&&e.dpDiv.addClass("ui-datepicker-multi-"+o).css("width",a*o+"em"),e.dpDiv[(1!==s[0]||1!==s[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),e.dpDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e===t.datepicker._curInst&&t.datepicker._datepickerShowing&&t.datepicker._shouldFocusInput(e)&&e.input.trigger("focus"),e.yearshtml&&(i=e.yearshtml,setTimeout(function(){i===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),i=e.yearshtml=null},0))},_shouldFocusInput:function(t){return t.input&&t.input.is(":visible")&&!t.input.is(":disabled")&&!t.input.is(":focus")},_checkOffset:function(e,i,s){var n=e.dpDiv.outerWidth(),o=e.dpDiv.outerHeight(),a=e.input?e.input.outerWidth():0,r=e.input?e.input.outerHeight():0,l=document.documentElement.clientWidth+(s?0:t(document).scrollLeft()),h=document.documentElement.clientHeight+(s?0:t(document).scrollTop());return i.left-=this._get(e,"isRTL")?n-a:0,i.left-=s&&i.left===e.input.offset().left?t(document).scrollLeft():0,i.top-=s&&i.top===e.input.offset().top+r?t(document).scrollTop():0,i.left-=Math.min(i.left,i.left+n>l&&l>n?Math.abs(i.left+n-l):0),i.top-=Math.min(i.top,i.top+o>h&&h>o?Math.abs(o+r):0),i},_findPos:function(e){for(var i,s=this._getInst(e),n=this._get(s,"isRTL");e&&("hidden"===e.type||1!==e.nodeType||t.expr.filters.hidden(e));)e=e[n?"previousSibling":"nextSibling"];return i=t(e).offset(),[i.left,i.top]},_hideDatepicker:function(e){var i,s,n,o,a=this._curInst;!a||e&&a!==t.data(e,"datepicker")||this._datepickerShowing&&(i=this._get(a,"showAnim"),s=this._get(a,"duration"),n=function(){t.datepicker._tidyDialog(a)},t.effects&&(t.effects.effect[i]||t.effects[i])?a.dpDiv.hide(i,t.datepicker._get(a,"showOptions"),s,n):a.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?s:null,n),i||n(),this._datepickerShowing=!1,o=this._get(a,"onClose"),o&&o.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),t.blockUI&&(t.unblockUI(),t("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(t){t.dpDiv.removeClass(this._dialogClass).off(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(t.datepicker._curInst){var i=t(e.target),s=t.datepicker._getInst(i[0]);(i[0].id!==t.datepicker._mainDivId&&0===i.parents("#"+t.datepicker._mainDivId).length&&!i.hasClass(t.datepicker.markerClassName)&&!i.closest("."+t.datepicker._triggerClass).length&&t.datepicker._datepickerShowing&&(!t.datepicker._inDialog||!t.blockUI)||i.hasClass(t.datepicker.markerClassName)&&t.datepicker._curInst!==s)&&t.datepicker._hideDatepicker()}},_adjustDate:function(e,i,s){var n=t(e),o=this._getInst(n[0]);this._isDisabledDatepicker(n[0])||(this._adjustInstDate(o,i+("M"===s?this._get(o,"showCurrentAtPos"):0),s),this._updateDatepicker(o))},_gotoToday:function(e){var i,s=t(e),n=this._getInst(s[0]);this._get(n,"gotoCurrent")&&n.currentDay?(n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear):(i=new Date,n.selectedDay=i.getDate(),n.drawMonth=n.selectedMonth=i.getMonth(),n.drawYear=n.selectedYear=i.getFullYear()),this._notifyChange(n),this._adjustDate(s)},_selectMonthYear:function(e,i,s){var n=t(e),o=this._getInst(n[0]);o["selected"+("M"===s?"Month":"Year")]=o["draw"+("M"===s?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(o),this._adjustDate(n)},_selectDay:function(e,i,s,n){var o,a=t(e);t(n).hasClass(this._unselectableClass)||this._isDisabledDatepicker(a[0])||(o=this._getInst(a[0]),o.selectedDay=o.currentDay=t("a",n).html(),o.selectedMonth=o.currentMonth=i,o.selectedYear=o.currentYear=s,this._selectDate(e,this._formatDate(o,o.currentDay,o.currentMonth,o.currentYear)))},_clearDate:function(e){var i=t(e);this._selectDate(i,"")},_selectDate:function(e,i){var s,n=t(e),o=this._getInst(n[0]);i=null!=i?i:this._formatDate(o),o.input&&o.input.val(i),this._updateAlternate(o),s=this._get(o,"onSelect"),s?s.apply(o.input?o.input[0]:null,[i,o]):o.input&&o.input.trigger("change"),o.inline?this._updateDatepicker(o):(this._hideDatepicker(),this._lastInput=o.input[0],"object"!=typeof o.input[0]&&o.input.trigger("focus"),this._lastInput=null)},_updateAlternate:function(e){var i,s,n,o=this._get(e,"altField");o&&(i=this._get(e,"altFormat")||this._get(e,"dateFormat"),s=this._getDate(e),n=this.formatDate(i,s,this._getFormatConfig(e)),t(o).val(n))},noWeekends:function(t){var e=t.getDay();return[e>0&&6>e,""]},iso8601Week:function(t){var e,i=new Date(t.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),e=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((e-i)/864e5)/7)+1},parseDate:function(e,i,s){if(null==e||null==i)throw"Invalid arguments";if(i="object"==typeof i?""+i:i+"",""===i)return null;var n,o,a,r,l=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,c="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),u=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,d=(s?s.dayNames:null)||this._defaults.dayNames,p=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,f=(s?s.monthNames:null)||this._defaults.monthNames,g=-1,m=-1,_=-1,v=-1,b=!1,y=function(t){var i=e.length>n+1&&e.charAt(n+1)===t;return i&&n++,i},w=function(t){var e=y(t),s="@"===t?14:"!"===t?20:"y"===t&&e?4:"o"===t?3:2,n="y"===t?s:1,o=RegExp("^\\d{"+n+","+s+"}"),a=i.substring(l).match(o);if(!a)throw"Missing number at position "+l;return l+=a[0].length,parseInt(a[0],10)},k=function(e,s,n){var o=-1,a=t.map(y(e)?n:s,function(t,e){return[[e,t]]}).sort(function(t,e){return-(t[1].length-e[1].length)});if(t.each(a,function(t,e){var s=e[1];return i.substr(l,s.length).toLowerCase()===s.toLowerCase()?(o=e[0],l+=s.length,!1):void 0}),-1!==o)return o+1;throw"Unknown name at position "+l},x=function(){if(i.charAt(l)!==e.charAt(n))throw"Unexpected literal at position "+l;l++};for(n=0;e.length>n;n++)if(b)"'"!==e.charAt(n)||y("'")?x():b=!1;else switch(e.charAt(n)){case"d":_=w("d");break;case"D":k("D",u,d);break;case"o":v=w("o");break;case"m":m=w("m");break;case"M":m=k("M",p,f);break;case"y":g=w("y");break;case"@":r=new Date(w("@")),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate(); +break;case"!":r=new Date((w("!")-this._ticksTo1970)/1e4),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate();break;case"'":y("'")?x():b=!0;break;default:x()}if(i.length>l&&(a=i.substr(l),!/^\s+/.test(a)))throw"Extra/unparsed characters found in date: "+a;if(-1===g?g=(new Date).getFullYear():100>g&&(g+=(new Date).getFullYear()-(new Date).getFullYear()%100+(c>=g?0:-100)),v>-1)for(m=1,_=v;;){if(o=this._getDaysInMonth(g,m-1),o>=_)break;m++,_-=o}if(r=this._daylightSavingAdjust(new Date(g,m-1,_)),r.getFullYear()!==g||r.getMonth()+1!==m||r.getDate()!==_)throw"Invalid date";return r},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(t,e,i){if(!e)return"";var s,n=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,o=(i?i.dayNames:null)||this._defaults.dayNames,a=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,r=(i?i.monthNames:null)||this._defaults.monthNames,l=function(e){var i=t.length>s+1&&t.charAt(s+1)===e;return i&&s++,i},h=function(t,e,i){var s=""+e;if(l(t))for(;i>s.length;)s="0"+s;return s},c=function(t,e,i,s){return l(t)?s[e]:i[e]},u="",d=!1;if(e)for(s=0;t.length>s;s++)if(d)"'"!==t.charAt(s)||l("'")?u+=t.charAt(s):d=!1;else switch(t.charAt(s)){case"d":u+=h("d",e.getDate(),2);break;case"D":u+=c("D",e.getDay(),n,o);break;case"o":u+=h("o",Math.round((new Date(e.getFullYear(),e.getMonth(),e.getDate()).getTime()-new Date(e.getFullYear(),0,0).getTime())/864e5),3);break;case"m":u+=h("m",e.getMonth()+1,2);break;case"M":u+=c("M",e.getMonth(),a,r);break;case"y":u+=l("y")?e.getFullYear():(10>e.getFullYear()%100?"0":"")+e.getFullYear()%100;break;case"@":u+=e.getTime();break;case"!":u+=1e4*e.getTime()+this._ticksTo1970;break;case"'":l("'")?u+="'":d=!0;break;default:u+=t.charAt(s)}return u},_possibleChars:function(t){var e,i="",s=!1,n=function(i){var s=t.length>e+1&&t.charAt(e+1)===i;return s&&e++,s};for(e=0;t.length>e;e++)if(s)"'"!==t.charAt(e)||n("'")?i+=t.charAt(e):s=!1;else switch(t.charAt(e)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":n("'")?i+="'":s=!0;break;default:i+=t.charAt(e)}return i},_get:function(t,e){return void 0!==t.settings[e]?t.settings[e]:this._defaults[e]},_setDateFromField:function(t,e){if(t.input.val()!==t.lastVal){var i=this._get(t,"dateFormat"),s=t.lastVal=t.input?t.input.val():null,n=this._getDefaultDate(t),o=n,a=this._getFormatConfig(t);try{o=this.parseDate(i,s,a)||n}catch(r){s=e?"":s}t.selectedDay=o.getDate(),t.drawMonth=t.selectedMonth=o.getMonth(),t.drawYear=t.selectedYear=o.getFullYear(),t.currentDay=s?o.getDate():0,t.currentMonth=s?o.getMonth():0,t.currentYear=s?o.getFullYear():0,this._adjustInstDate(t)}},_getDefaultDate:function(t){return this._restrictMinMax(t,this._determineDate(t,this._get(t,"defaultDate"),new Date))},_determineDate:function(e,i,s){var n=function(t){var e=new Date;return e.setDate(e.getDate()+t),e},o=function(i){try{return t.datepicker.parseDate(t.datepicker._get(e,"dateFormat"),i,t.datepicker._getFormatConfig(e))}catch(s){}for(var n=(i.toLowerCase().match(/^c/)?t.datepicker._getDate(e):null)||new Date,o=n.getFullYear(),a=n.getMonth(),r=n.getDate(),l=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,h=l.exec(i);h;){switch(h[2]||"d"){case"d":case"D":r+=parseInt(h[1],10);break;case"w":case"W":r+=7*parseInt(h[1],10);break;case"m":case"M":a+=parseInt(h[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a));break;case"y":case"Y":o+=parseInt(h[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a))}h=l.exec(i)}return new Date(o,a,r)},a=null==i||""===i?s:"string"==typeof i?o(i):"number"==typeof i?isNaN(i)?s:n(i):new Date(i.getTime());return a=a&&"Invalid Date"==""+a?s:a,a&&(a.setHours(0),a.setMinutes(0),a.setSeconds(0),a.setMilliseconds(0)),this._daylightSavingAdjust(a)},_daylightSavingAdjust:function(t){return t?(t.setHours(t.getHours()>12?t.getHours()+2:0),t):null},_setDate:function(t,e,i){var s=!e,n=t.selectedMonth,o=t.selectedYear,a=this._restrictMinMax(t,this._determineDate(t,e,new Date));t.selectedDay=t.currentDay=a.getDate(),t.drawMonth=t.selectedMonth=t.currentMonth=a.getMonth(),t.drawYear=t.selectedYear=t.currentYear=a.getFullYear(),n===t.selectedMonth&&o===t.selectedYear||i||this._notifyChange(t),this._adjustInstDate(t),t.input&&t.input.val(s?"":this._formatDate(t))},_getDate:function(t){var e=!t.currentYear||t.input&&""===t.input.val()?null:this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return e},_attachHandlers:function(e){var i=this._get(e,"stepMonths"),s="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){t.datepicker._adjustDate(s,-i,"M")},next:function(){t.datepicker._adjustDate(s,+i,"M")},hide:function(){t.datepicker._hideDatepicker()},today:function(){t.datepicker._gotoToday(s)},selectDay:function(){return t.datepicker._selectDay(s,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return t.datepicker._selectMonthYear(s,this,"M"),!1},selectYear:function(){return t.datepicker._selectMonthYear(s,this,"Y"),!1}};t(this).on(this.getAttribute("data-event"),e[this.getAttribute("data-handler")])})},_generateHTML:function(t){var e,i,s,n,o,a,r,l,h,c,u,d,p,f,g,m,_,v,b,y,w,k,x,C,D,T,I,M,P,S,N,H,z,A,O,W,E,F,L,R=new Date,Y=this._daylightSavingAdjust(new Date(R.getFullYear(),R.getMonth(),R.getDate())),B=this._get(t,"isRTL"),j=this._get(t,"showButtonPanel"),q=this._get(t,"hideIfNoPrevNext"),K=this._get(t,"navigationAsDateFormat"),U=this._getNumberOfMonths(t),V=this._get(t,"showCurrentAtPos"),X=this._get(t,"stepMonths"),$=1!==U[0]||1!==U[1],G=this._daylightSavingAdjust(t.currentDay?new Date(t.currentYear,t.currentMonth,t.currentDay):new Date(9999,9,9)),J=this._getMinMaxDate(t,"min"),Q=this._getMinMaxDate(t,"max"),Z=t.drawMonth-V,te=t.drawYear;if(0>Z&&(Z+=12,te--),Q)for(e=this._daylightSavingAdjust(new Date(Q.getFullYear(),Q.getMonth()-U[0]*U[1]+1,Q.getDate())),e=J&&J>e?J:e;this._daylightSavingAdjust(new Date(te,Z,1))>e;)Z--,0>Z&&(Z=11,te--);for(t.drawMonth=Z,t.drawYear=te,i=this._get(t,"prevText"),i=K?this.formatDate(i,this._daylightSavingAdjust(new Date(te,Z-X,1)),this._getFormatConfig(t)):i,s=this._canAdjustMonth(t,-1,te,Z)?""+i+"":q?"":""+i+"",n=this._get(t,"nextText"),n=K?this.formatDate(n,this._daylightSavingAdjust(new Date(te,Z+X,1)),this._getFormatConfig(t)):n,o=this._canAdjustMonth(t,1,te,Z)?""+n+"":q?"":""+n+"",a=this._get(t,"currentText"),r=this._get(t,"gotoCurrent")&&t.currentDay?G:Y,a=K?this.formatDate(a,r,this._getFormatConfig(t)):a,l=t.inline?"":"",h=j?"
          "+(B?l:"")+(this._isInRange(t,r)?"":"")+(B?"":l)+"
          ":"",c=parseInt(this._get(t,"firstDay"),10),c=isNaN(c)?0:c,u=this._get(t,"showWeek"),d=this._get(t,"dayNames"),p=this._get(t,"dayNamesMin"),f=this._get(t,"monthNames"),g=this._get(t,"monthNamesShort"),m=this._get(t,"beforeShowDay"),_=this._get(t,"showOtherMonths"),v=this._get(t,"selectOtherMonths"),b=this._getDefaultDate(t),y="",k=0;U[0]>k;k++){for(x="",this.maxRows=4,C=0;U[1]>C;C++){if(D=this._daylightSavingAdjust(new Date(te,Z,t.selectedDay)),T=" ui-corner-all",I="",$){if(I+="
          "}for(I+="
          "+(/all|left/.test(T)&&0===k?B?o:s:"")+(/all|right/.test(T)&&0===k?B?s:o:"")+this._generateMonthYearHeader(t,Z,te,J,Q,k>0||C>0,f,g)+"
          "+"",M=u?"":"",w=0;7>w;w++)P=(w+c)%7,M+="";for(I+=M+"",S=this._getDaysInMonth(te,Z),te===t.selectedYear&&Z===t.selectedMonth&&(t.selectedDay=Math.min(t.selectedDay,S)),N=(this._getFirstDayOfMonth(te,Z)-c+7)%7,H=Math.ceil((N+S)/7),z=$?this.maxRows>H?this.maxRows:H:H,this.maxRows=z,A=this._daylightSavingAdjust(new Date(te,Z,1-N)),O=0;z>O;O++){for(I+="",W=u?"":"",w=0;7>w;w++)E=m?m.apply(t.input?t.input[0]:null,[A]):[!0,""],F=A.getMonth()!==Z,L=F&&!v||!E[0]||J&&J>A||Q&&A>Q,W+="",A.setDate(A.getDate()+1),A=this._daylightSavingAdjust(A);I+=W+""}Z++,Z>11&&(Z=0,te++),I+="
          "+this._get(t,"weekHeader")+"=5?" class='ui-datepicker-week-end'":"")+">"+""+p[P]+"
          "+this._get(t,"calculateWeek")(A)+""+(F&&!_?" ":L?""+A.getDate()+"":""+A.getDate()+"")+"
          "+($?"
          "+(U[0]>0&&C===U[1]-1?"
          ":""):""),x+=I}y+=x}return y+=h,t._keyEvent=!1,y},_generateMonthYearHeader:function(t,e,i,s,n,o,a,r){var l,h,c,u,d,p,f,g,m=this._get(t,"changeMonth"),_=this._get(t,"changeYear"),v=this._get(t,"showMonthAfterYear"),b="
          ",y="";if(o||!m)y+=""+a[e]+"";else{for(l=s&&s.getFullYear()===i,h=n&&n.getFullYear()===i,y+=""}if(v||(b+=y+(!o&&m&&_?"":" ")),!t.yearshtml)if(t.yearshtml="",o||!_)b+=""+i+"";else{for(u=this._get(t,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(t){var e=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?d+parseInt(t,10):parseInt(t,10);return isNaN(e)?d:e},f=p(u[0]),g=Math.max(f,p(u[1]||"")),f=s?Math.max(f,s.getFullYear()):f,g=n?Math.min(g,n.getFullYear()):g,t.yearshtml+="",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),v&&(b+=(!o&&m&&_?"":" ")+y),b+="
          "},_adjustInstDate:function(t,e,i){var s=t.selectedYear+("Y"===i?e:0),n=t.selectedMonth+("M"===i?e:0),o=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),a=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,o)));t.selectedDay=a.getDate(),t.drawMonth=t.selectedMonth=a.getMonth(),t.drawYear=t.selectedYear=a.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),s=this._getMinMaxDate(t,"max"),n=i&&i>e?i:e;return s&&n>s?s:n},_notifyChange:function(t){var e=this._get(t,"onChangeMonthYear");e&&e.apply(t.input?t.input[0]:null,[t.selectedYear,t.selectedMonth+1,t])},_getNumberOfMonths:function(t){var e=this._get(t,"numberOfMonths");return null==e?[1,1]:"number"==typeof e?[1,e]:e},_getMinMaxDate:function(t,e){return this._determineDate(t,this._get(t,e+"Date"),null)},_getDaysInMonth:function(t,e){return 32-this._daylightSavingAdjust(new Date(t,e,32)).getDate()},_getFirstDayOfMonth:function(t,e){return new Date(t,e,1).getDay()},_canAdjustMonth:function(t,e,i,s){var n=this._getNumberOfMonths(t),o=this._daylightSavingAdjust(new Date(i,s+(0>e?e:n[0]*n[1]),1));return 0>e&&o.setDate(this._getDaysInMonth(o.getFullYear(),o.getMonth())),this._isInRange(t,o)},_isInRange:function(t,e){var i,s,n=this._getMinMaxDate(t,"min"),o=this._getMinMaxDate(t,"max"),a=null,r=null,l=this._get(t,"yearRange");return l&&(i=l.split(":"),s=(new Date).getFullYear(),a=parseInt(i[0],10),r=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(a+=s),i[1].match(/[+\-].*/)&&(r+=s)),(!n||e.getTime()>=n.getTime())&&(!o||e.getTime()<=o.getTime())&&(!a||e.getFullYear()>=a)&&(!r||r>=e.getFullYear())},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return e="string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),{shortYearCutoff:e,dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);var n=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),n,this._getFormatConfig(t))}}),t.fn.datepicker=function(e){if(!this.length)return this;t.datepicker.initialized||(t(document).on("mousedown",t.datepicker._checkExternalClick),t.datepicker.initialized=!0),0===t("#"+t.datepicker._mainDivId).length&&t("body").append(t.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!==e&&"getDate"!==e&&"widget"!==e?"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof e?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this].concat(i)):t.datepicker._attachDatepicker(this,e)}):t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i))},t.datepicker=new i,t.datepicker.initialized=!1,t.datepicker.uuid=(new Date).getTime(),t.datepicker.version="1.12.0",t.datepicker}); \ No newline at end of file diff --git a/public/vendor/jquery/textcomplete/jquery.textcomplete.js b/public/vendor/jquery/textcomplete/jquery.textcomplete.js index ad1d508450..b4ccd18cfc 100644 --- a/public/vendor/jquery/textcomplete/jquery.textcomplete.js +++ b/public/vendor/jquery/textcomplete/jquery.textcomplete.js @@ -17,7 +17,7 @@ * Repository: https://github.com/yuku-t/jquery-textcomplete * License: MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE) * Author: Yuku Takahashi - * Version: 1.3.1 + * Version: 1.7.3 */ if (typeof jQuery === 'undefined') { @@ -137,10 +137,6 @@ if (typeof jQuery === 'undefined') { return Object.prototype.toString.call(obj) === '[object String]'; }; - var isFunction = function (obj) { - return Object.prototype.toString.call(obj) === '[object Function]'; - }; - var uniqueId = 0; function Completer(element, option) { @@ -148,32 +144,46 @@ if (typeof jQuery === 'undefined') { this.id = 'textcomplete' + uniqueId++; this.strategies = []; this.views = []; - this.option = $.extend({}, Completer._getDefaults(), option); + this.option = $.extend({}, Completer.defaults, option); if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') { throw new Error('textcomplete must be called on a Textarea or a ContentEditable.'); } - if (element === document.activeElement) { + // use ownerDocument to fix iframe / IE issues + if (element === element.ownerDocument.activeElement) { // element has already been focused. Initialize view objects immediately. this.initialize() } else { // Initialize view objects lazily. var self = this; this.$el.one('focus.' + this.id, function () { self.initialize(); }); + + // Special handling for CKEditor: lazy init on instance load + if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) { + CKEDITOR.on("instanceReady", function(event) { + event.editor.once("focus", function(event2) { + // replace the element with the Iframe element and flag it as CKEditor + self.$el = $(event.editor.editable().$); + if (!self.option.adapter) { + self.option.adapter = $.fn.textcomplete['CKEditor']; + self.option.ckeditor_instance = event.editor; + } + self.initialize(); + }); + }); + } } } - Completer._getDefaults = function () { - if (!Completer.DEFAULTS) { - Completer.DEFAULTS = { - appendTo: $('body'), - zIndex: '100' - }; - } - - return Completer.DEFAULTS; - } + Completer.defaults = { + appendTo: 'body', + className: '', // deprecated option + dropdownClassName: 'dropdown-menu textcomplete-dropdown', + maxCount: 10, + zIndex: '100', + rightEdgeOffset: 30 + }; $.extend(Completer.prototype, { // Public properties @@ -185,12 +195,26 @@ if (typeof jQuery === 'undefined') { adapter: null, dropdown: null, $el: null, + $iframe: null, // Public methods // -------------- initialize: function () { var element = this.$el.get(0); + + // check if we are in an iframe + // we need to alter positioning logic if using an iframe + if (this.$el.prop('ownerDocument') !== document && window.frames.length) { + for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) { + if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) { + this.$iframe = $(window.frames[iframeIndex].frameElement); + break; + } + } + } + + // Initialize view objects. this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option); var Adapter, viewName; @@ -282,7 +306,7 @@ if (typeof jQuery === 'undefined') { var strategy = this.strategies[i]; var context = strategy.context(text); if (context || context === '') { - var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match; + var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match; if (isString(context)) { text = context; } var match = text.match(matchRegexp); if (match) { return [strategy, match[strategy.index], match]; } @@ -400,7 +424,7 @@ if (typeof jQuery === 'undefined') { var $parent = option.appendTo; if (!($parent instanceof $)) { $parent = $($parent); } var $el = $('
            ') - .addClass('dropdown-menu textcomplete-dropdown') + .addClass(option.dropdownClassName) .attr('id', 'textcomplete-dropdown-' + option._oid) .css({ display: 'none', @@ -423,7 +447,7 @@ if (typeof jQuery === 'undefined') { footer: null, header: null, id: null, - maxCount: 10, + maxCount: null, placement: '', shown: false, data: [], // Shown zipped data. @@ -446,8 +470,8 @@ if (typeof jQuery === 'undefined') { render: function (zippedData) { var contentsHtml = this._buildContents(zippedData); - var unzippedData = $.map(this.data, function (d) { return d.value; }); - if (this.data.length) { + var unzippedData = $.map(zippedData, function (d) { return d.value; }); + if (zippedData.length) { var strategy = zippedData[0].strategy; if (strategy.id) { this.$el.attr('data-strategy', strategy.id); @@ -481,7 +505,7 @@ if (typeof jQuery === 'undefined') { return false; if($(this).css('position') === 'fixed') { pos.top -= $window.scrollTop(); - pos.left -= $window.scrollLeft(); + pos.left -= $window.scrollLeft(); position = 'fixed'; return false; } @@ -786,7 +810,10 @@ if (typeof jQuery === 'undefined') { var windowScrollBottom = $window.scrollTop() + $window.height(); var height = this.$el.height(); if ((this.$el.position().top + height) > windowScrollBottom) { - this.$el.offset({top: windowScrollBottom - height}); + // only do this if we are not in an iframe + if (!this.completer.$iframe) { + this.$el.offset({top: windowScrollBottom - height}); + } } }, @@ -795,9 +822,15 @@ if (typeof jQuery === 'undefined') { // to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping // (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right // edge, move left. We don't know how far to move left, so just keep nudging a bit. - var tolerance = 30; // pixels. Make wider than vertical scrollbar because we might not be able to use that space. - while (this.$el.offset().left + this.$el.width() > $window.width() - tolerance) { - this.$el.offset({left: this.$el.offset().left - tolerance}); + var tolerance = this.option.rightEdgeOffset; // pixels. Make wider than vertical scrollbar because we might not be able to use that space. + var lastOffset = this.$el.offset().left, offset; + var width = this.$el.width(); + var maxLeft = $window.width() - tolerance; + while (lastOffset + width > maxLeft) { + this.$el.offset({left: lastOffset - tolerance}); + offset = this.$el.offset().left; + if (offset >= lastOffset) { break; } + lastOffset = offset; } }, @@ -1002,6 +1035,7 @@ if (typeof jQuery === 'undefined') { case 13: // ENTER case 40: // DOWN case 38: // UP + case 27: // ESC return true; } if (clickEvent.ctrlKey) switch (clickEvent.keyCode) { @@ -1035,12 +1069,14 @@ if (typeof jQuery === 'undefined') { var pre = this.getTextFromHeadToCaret(); var post = this.el.value.substring(this.el.selectionEnd); var newSubstr = strategy.replace(value, e); + var regExp; if (typeof newSubstr !== 'undefined') { if ($.isArray(newSubstr)) { post = newSubstr[1] + post; newSubstr = newSubstr[0]; } - pre = pre.replace(strategy.match, newSubstr); + regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match; + pre = pre.replace(regExp, newSubstr); this.$el.val(pre + post); this.el.selectionStart = this.el.selectionEnd = pre.length; } @@ -1056,9 +1092,29 @@ if (typeof jQuery === 'undefined') { _getCaretRelativePosition: function () { var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart); return { - top: p.top + parseInt(this.$el.css('line-height'), 10) - this.$el.scrollTop(), - left: p.left - this.$el.scrollLeft() + top: p.top + this._calculateLineHeight() - this.$el.scrollTop(), + left: p.left - this.$el.scrollLeft(), + lineHeight: this._calculateLineHeight() }; + }, + + _calculateLineHeight: function () { + var lineHeight = parseInt(this.$el.css('line-height'), 10); + if (isNaN(lineHeight)) { + // http://stackoverflow.com/a/4515470/1297336 + var parentNode = this.el.parentNode; + var temp = document.createElement(this.el.nodeName); + var style = this.el.style; + temp.setAttribute( + 'style', + 'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize + ); + temp.innerHTML = 'test'; + parentNode.appendChild(temp); + lineHeight = temp.clientHeight; + parentNode.removeChild(temp); + } + return lineHeight; } }); @@ -1087,12 +1143,14 @@ if (typeof jQuery === 'undefined') { var pre = this.getTextFromHeadToCaret(); var post = this.el.value.substring(pre.length); var newSubstr = strategy.replace(value, e); + var regExp; if (typeof newSubstr !== 'undefined') { if ($.isArray(newSubstr)) { post = newSubstr[1] + post; newSubstr = newSubstr[0]; } - pre = pre.replace(strategy.match, newSubstr); + regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match; + pre = pre.replace(regExp, newSubstr); this.$el.val(pre + post); this.el.focus(); var range = this.el.createTextRange(); @@ -1138,30 +1196,35 @@ if (typeof jQuery === 'undefined') { // When an dropdown item is selected, it is executed. select: function (value, strategy, e) { var pre = this.getTextFromHeadToCaret(); - var sel = window.getSelection() + // use ownerDocument instead of window to support iframes + var sel = this.el.ownerDocument.getSelection(); + var range = sel.getRangeAt(0); var selection = range.cloneRange(); selection.selectNodeContents(range.startContainer); var content = selection.toString(); var post = content.substring(range.startOffset); var newSubstr = strategy.replace(value, e); + var regExp; if (typeof newSubstr !== 'undefined') { if ($.isArray(newSubstr)) { post = newSubstr[1] + post; newSubstr = newSubstr[0]; } - pre = pre.replace(strategy.match, newSubstr); + regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match; + pre = pre.replace(regExp, newSubstr) + .replace(/ $/, " "); //   necessary at least for CKeditor to not eat spaces range.selectNodeContents(range.startContainer); range.deleteContents(); // create temporary elements - var preWrapper = document.createElement("div"); + var preWrapper = this.el.ownerDocument.createElement("div"); preWrapper.innerHTML = pre; - var postWrapper = document.createElement("div"); + var postWrapper = this.el.ownerDocument.createElement("div"); postWrapper.innerHTML = post; // create the fragment thats inserted - var fragment = document.createDocumentFragment(); + var fragment = this.el.ownerDocument.createDocumentFragment(); var childNode; var lastOfPre; while (childNode = preWrapper.firstChild) { @@ -1194,8 +1257,8 @@ if (typeof jQuery === 'undefined') { // // Dropdown's position will be decided using the result. _getCaretRelativePosition: function () { - var range = window.getSelection().getRangeAt(0).cloneRange(); - var node = document.createElement('span'); + var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange(); + var node = this.el.ownerDocument.createElement('span'); range.insertNode(node); range.selectNodeContents(node); range.deleteContents(); @@ -1204,6 +1267,17 @@ if (typeof jQuery === 'undefined') { position.left -= this.$el.offset().left; position.top += $node.height() - this.$el.offset().top; position.lineHeight = $node.height(); + + // special positioning logic for iframes + // this is typically used for contenteditables such as tinymce or ckeditor + if (this.completer.$iframe) { + var iframePosition = this.completer.$iframe.offset(); + position.top += iframePosition.top; + position.left += iframePosition.left; + //subtract scrollTop from element in iframe + position.top -= this.$el.scrollTop(); + } + $node.remove(); return position; }, @@ -1217,7 +1291,7 @@ if (typeof jQuery === 'undefined') { // this.getTextFromHeadToCaret() // // => ' wor' // not 'hello wor' getTextFromHeadToCaret: function () { - var range = window.getSelection().getRangeAt(0); + var range = this.el.ownerDocument.getSelection().getRangeAt(0); var selection = range.cloneRange(); selection.selectNodeContents(range.startContainer); return selection.toString().substring(0, range.startOffset); @@ -1227,6 +1301,39 @@ if (typeof jQuery === 'undefined') { $.fn.textcomplete.ContentEditable = ContentEditable; }(jQuery); +// NOTE: TextComplete plugin has contenteditable support but it does not work +// fine especially on old IEs. +// Any pull requests are REALLY welcome. + ++function ($) { + 'use strict'; + + // CKEditor adapter + // ======================= + // + // Adapter for CKEditor, based on contenteditable elements. + function CKEditor (element, completer, option) { + this.initialize(element, completer, option); + } + + $.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, { + _bindEvents: function () { + var $this = this; + this.option.ckeditor_instance.on('key', function(event) { + var domEvent = event.data; + $this._onKeyup(domEvent); + if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) { + return false; + } + }, null, null, 1); // 1 = Priority = Important! + // we actually also need the native event, as the CKEditor one is happening to late + this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this)); + }, +}); + + $.fn.textcomplete.CKEditor = CKEditor; +}(jQuery); + // The MIT License (MIT) // // Copyright (c) 2015 Jonathan Ong me@jongleberry.com @@ -1248,7 +1355,7 @@ if (typeof jQuery === 'undefined') { // // https://github.com/component/textarea-caret-position -(function () { +(function ($) { // The properties that we copy into a mirrored div. // Note that some browsers, such as Firefox, @@ -1369,13 +1476,9 @@ function getCaretCoordinates(element, position, options) { return coordinates; } -if (typeof module != 'undefined' && typeof module.exports != 'undefined') { - module.exports = getCaretCoordinates; -} else if(isBrowser){ - window.$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates; -} +$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates; -}()); +}(jQuery)); return jQuery; -})); \ No newline at end of file +})); diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js new file mode 100644 index 0000000000..a430a45768 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js @@ -0,0 +1,20 @@ +// Azerbaijani shortened +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "", + suffixFromNow: "", + seconds: '1 dəq', + minute: '1 dəq', + minutes: '%d dəq', + hour: '1 saat', + hours: '%d saat', + day: '1 gün', + days: '%d gün', + month: '1 ay', + months: '%d ay', + year: '1 il', + years: '%d il', + wordSeparator: '', + numbers: [] +}; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.az.js b/public/vendor/jquery/timeago/locales/jquery.timeago.az.js new file mode 100644 index 0000000000..1e04a23a83 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.az.js @@ -0,0 +1,20 @@ +// Azerbaijani +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'əvvəl', + suffixFromNow: 'sonra', + seconds: 'saniyələr', + minute: '1 dəqiqə', + minutes: '%d dəqiqə', + hour: '1 saat', + hours: '%d saat', + day: '1 gün', + days: '%d gün', + month: '1 ay', + months: '%d ay', + year: '1 il', + years: '%d il', + wordSeparator: '', + numbers: [] +}; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js index 10f158de08..09427ec976 100644 --- a/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js @@ -4,17 +4,17 @@ jQuery.timeago.settings.strings = { prefixFromNow: null, suffixAgo: "", suffixFromNow: "", - seconds: "sec", - minute: "1min", - minutes: "%dmin", + seconds: "s", + minute: "1m", + minutes: "%dm", hour: "1h", hours: "%dh", - day: "1d", - days: "%dd", - month: "1Mon", - months: "%dMon", - year: "1Jhr", - years: "%dJhr", + day: "1T.", + days: "%dT.", + month: "1Mt.", + months: "%dMt.", + year: "1J.", + years: "%dJ.", wordSeparator: " ", numbers: [] }; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js b/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js index fd81f275d0..95a7cd2a7a 100644 --- a/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js @@ -11,8 +11,8 @@ jQuery.timeago.settings.strings = { hours: "約 %d 時間", day: "約 1 日", days: "約 %d 日", - month: "約 1 月", - months: "約 %d 月", + month: "約 1 ヶ月", + months: "約 %d ヶ月", year: "約 1 年", years: "約 %d 年", wordSeparator: "" diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js b/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js new file mode 100644 index 0000000000..eb02391563 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js @@ -0,0 +1,20 @@ +//Latvian +jQuery.timeago.settings.strings = { + prefixAgo: "pirms", + prefixFromNow: null, + suffixAgo: null, + suffixFromNow: "no šī brīža", + seconds: "%d sek.", + minute: "min.", + minutes: "%d min.", + hour: "st.", + hours: "%d st.", + day: "1 d.", + days: "%d d.", + month: "mēnesis.", + months: "%d mēnesis.", + year: "gads", + years: "%d gads", + wordSeparator: " ", + numbers: [] +}; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js b/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js index 57d4f6020c..b8ab587d82 100644 --- a/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js @@ -28,7 +28,7 @@ }, month: "en mesec", months: function (value) { - return numpf(value, ["%d mescov", "%d mesec", "%d mesca", "%d mesce"]); + return numpf(value, ["%d mesecev", "%d mesec", "%d meseca", "%d mesece"]); }, year: "eno leto", years: function (value) { diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js b/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js new file mode 100644 index 0000000000..c75a972e77 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js @@ -0,0 +1,49 @@ +// Serbian +(function () { + var numpf; + + numpf = function (n, f, s, t) { + var n10; + n10 = n % 10; + if (n10 === 1 && (n === 1 || n > 20)) { + return f; + } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) { + return s; + } else { + return t; + } + }; + + jQuery.timeago.settings.strings = { + prefixAgo: "пре", + prefixFromNow: "за", + suffixAgo: null, + suffixFromNow: null, + second: "секунд", + seconds: function (value) { + return numpf(value, "%d секунд", "%d секунде", "%d секунди"); + }, + minute: "један минут", + minutes: function (value) { + return numpf(value, "%d минут", "%d минута", "%d минута"); + }, + hour: "један сат", + hours: function (value) { + return numpf(value, "%d сат", "%d сата", "%d сати"); + }, + day: "један дан", + days: function (value) { + return numpf(value, "%d дан", "%d дана", "%d дана"); + }, + month: "месец дана", + months: function (value) { + return numpf(value, "%d месец", "%d месеца", "%d месеци"); + }, + year: "годину дана", + years: function (value) { + return numpf(value, "%d годину", "%d године", "%d година"); + }, + wordSeparator: " " + }; + +}).call(this); diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js new file mode 100644 index 0000000000..ebc2277b49 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js @@ -0,0 +1,20 @@ +// Turkish shortened +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "", + suffixFromNow: "", + seconds: "1sn", + minute: "1d", + minutes: "%dd", + hour: "1s", + hours: "%ds", + day: "1g", + days: "%dg", + month: "1ay", + months: "%day", + year: "1y", + years: "%dy", + wordSeparator: " ", + numbers: [] +}; diff --git a/public/vendor/mousetrap/mousetrap.js b/public/vendor/mousetrap/mousetrap.js deleted file mode 100644 index 01709ffd9a..0000000000 --- a/public/vendor/mousetrap/mousetrap.js +++ /dev/null @@ -1,9 +0,0 @@ -/* mousetrap v1.4.6 craig.is/killing/mice */ -(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;gg||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;cn?t:n>e?e:n}function t(n){return 100*(-1+n)}function e(n,e,r){var i;return i="translate3d"===c.positionUsing?{transform:"translate3d("+t(n)+"%,0,0)"}:"translate"===c.positionUsing?{transform:"translate("+t(n)+"%,0)"}:{"margin-left":t(n)+"%"},i.transition="all "+e+"ms "+r,i}function r(n,t){var e="string"==typeof n?n:o(n);return e.indexOf(" "+t+" ")>=0}function i(n,t){var e=o(n),i=e+t;r(e,t)||(n.className=i.substring(1))}function s(n,t){var e,i=o(n);r(n,t)&&(e=i.replace(" "+t+" "," "),n.className=e.substring(1,e.length-1))}function o(n){return(" "+(n.className||"")+" ").replace(/\s+/gi," ")}function a(n){n&&n.parentNode&&n.parentNode.removeChild(n)}var u={};u.version="0.1.6";var c=u.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
            '};u.configure=function(n){var t,e;for(t in n)e=n[t],void 0!==e&&n.hasOwnProperty(t)&&(c[t]=e);return this},u.status=null,u.set=function(t){var r=u.isStarted();t=n(t,c.minimum,1),u.status=1===t?null:t;var i=u.render(!r),s=i.querySelector(c.barSelector),o=c.speed,a=c.easing;return i.offsetWidth,l(function(n){""===c.positionUsing&&(c.positionUsing=u.getPositioningCSS()),f(s,e(t,o,a)),1===t?(f(i,{transition:"none",opacity:1}),i.offsetWidth,setTimeout(function(){f(i,{transition:"all "+o+"ms linear",opacity:0}),setTimeout(function(){u.remove(),n()},o)},o)):setTimeout(n,o)}),this},u.isStarted=function(){return"number"==typeof u.status},u.start=function(){u.status||u.set(0);var n=function(){setTimeout(function(){u.status&&(u.trickle(),n())},c.trickleSpeed)};return c.trickle&&n(),this},u.done=function(n){return n||u.status?u.inc(.3+.5*Math.random()).set(1):this},u.inc=function(t){var e=u.status;return e?("number"!=typeof t&&(t=(1-e)*n(Math.random()*e,.1,.95)),e=n(e+t,0,.994),u.set(e)):u.start()},u.trickle=function(){return u.inc(Math.random()*c.trickleRate)},function(){var n=0,t=0;u.promise=function(e){return e&&"resolved"!=e.state()?(0==t&&u.start(),n++,t++,e.always(function(){t--,0==t?(n=0,u.done()):u.set((n-t)/n)}),this):this}}(),u.render=function(n){if(u.isRendered())return document.getElementById("nprogress");i(document.documentElement,"nprogress-busy");var e=document.createElement("div");e.id="nprogress",e.innerHTML=c.template;var r,s=e.querySelector(c.barSelector),o=n?"-100":t(u.status||0),l=document.querySelector(c.parent);return f(s,{transition:"all 0 linear",transform:"translate3d("+o+"%,0,0)"}),c.showSpinner||(r=e.querySelector(c.spinnerSelector),r&&a(r)),l!=document.body&&i(l,"nprogress-custom-parent"),l.appendChild(e),e},u.remove=function(){s(document.documentElement,"nprogress-busy"),s(document.querySelector(c.parent),"nprogress-custom-parent");var n=document.getElementById("nprogress");n&&a(n)},u.isRendered=function(){return!!document.getElementById("nprogress")},u.getPositioningCSS=function(){var n=document.body.style,t="WebkitTransform"in n?"Webkit":"MozTransform"in n?"Moz":"msTransform"in n?"ms":"OTransform"in n?"O":"";return t+"Perspective"in n?"translate3d":t+"Transform"in n?"translate":"margin"};var l=function(){function n(){var e=t.shift();e&&e(n)}var t=[];return function(e){t.push(e),1==t.length&&n()}}(),f=function(){function n(n){return n.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(n,t){return t.toUpperCase()})}function t(n){var t=document.body.style;if(n in t)return n;for(var e,r=i.length,s=n.charAt(0).toUpperCase()+n.slice(1);r--;)if(e=i[r]+s,e in t)return e;return n}function e(e){return e=n(e),s[e]||(s[e]=t(e))}function r(n,t,r){t=e(t),n.style[t]=r}var i=["Webkit","O","Moz","ms"],s={};return function(n,t){var e,i,s=arguments;if(2==s.length)for(e in t)i=t[e],void 0!==i&&t.hasOwnProperty(e)&&r(n,e,i);else r(n,s[1],s[2])}}();return u}); \ No newline at end of file diff --git a/public/vendor/requirejs/require.js b/public/vendor/requirejs/require.js index f04b8c3f7d..857eb5b700 100644 --- a/public/vendor/requirejs/require.js +++ b/public/vendor/requirejs/require.js @@ -1,36 +1,36 @@ /* - RequireJS 2.1.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. - Available via the MIT or new BSD license. - see: http://github.com/jrburke/requirejs for details + RequireJS 2.2.0 Copyright jQuery Foundation and other contributors. + Released under MIT license, http://github.com/requirejs/requirejs/LICENSE */ var requirejs,require,define; -(function(ba){function J(b){return"[object Function]"===N.call(b)}function K(b){return"[object Array]"===N.call(b)}function z(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(J(n)){if(this.events.error&&this.map.isDefine||h.onError!==ca)try{e=k.execCb(c,n,b,e)}catch(d){a=d}else e=k.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!== -this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&!this.ignore&&(r[c]=e,h.onResourceLoad))h.onResourceLoad(k,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete= -!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=l(a.prefix);this.depMaps.push(d);u(d,"defined",v(this,function(e){var n,d;d=this.map.name;var g=this.map.parentMap?this.map.parentMap.name:null,C=k.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,g,!0)})||""),e=l(a.prefix+"!"+d,this.map.parentMap),u(e,"defined",v(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})), -d=m(q,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",v(this,function(a){this.emit("error",a)}));d.enable()}}else n=v(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=v(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];H(q,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),n.fromText=v(this,function(e,c){var d=a.name,g=l(d),i=Q;c&&(e=c);i&&(Q=!1);s(g);t(j.config,b)&&(j.config[d]=j.config[b]);try{h.exec(e)}catch(D){return w(B("fromtexteval", -"fromText eval for "+b+" failed: "+D,D,[b]))}i&&(Q=!0);this.depMaps.push(g);k.completeLoad(d);C([d],n)}),e.load(a.name,C,n,j)}));k.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;z(this.depMaps,v(this,function(a,b){var c,e;if("string"===typeof a){a=l(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(P,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;u(a,"defined",v(this,function(a){this.defineDep(b, -a);this.check()}));this.errback&&u(a,"error",v(this,this.errback))}c=a.id;e=q[c];!t(P,c)&&(e&&!e.enabled)&&k.enable(a,this)}));H(this.pluginMaps,v(this,function(a){var b=m(q,a.id);b&&!b.enabled&&k.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){z(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};k={config:j,contextName:b,registry:q,defined:r,urlFetched:V,defQueue:I,Module:$,makeModuleMap:l, -nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.pkgs,c=j.shim,e={paths:!0,config:!0,map:!0};H(a,function(a,b){e[b]?"map"===b?(j.map||(j.map={}),S(j[b],a,!0,!0)):S(j[b],a,!0):j[b]=a});a.shim&&(H(a.shim,function(a,b){K(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=k.makeShimExports(a);c[b]=a}),j.shim=c);a.packages&&(z(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name, -location:a.location||a.name,main:(a.main||"main").replace(ka,"").replace(fa,"")}}),j.pkgs=b);H(q,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=l(b))});if(a.deps||a.callback)k.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,f){function d(e,c,g){var i,j;f.enableBuildCallback&&(c&&J(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(J(c))return w(B("requireargs", -"Invalid require call"),g);if(a&&t(P,e))return P[e](q[a.id]);if(h.get)return h.get(k,e,a,d);i=l(e,a,!1,!0);i=i.id;return!t(r,i)?w(B("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[i]}M();k.nextTick(function(){M();j=s(l(null,a));j.skipMap=f.skipMap;j.init(e,c,g,{enabled:!0});E()});return d}f=f||{};S(d,{isBrowser:A,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];if(-1!==f&&(!("."===g||".."===g)||1g.attachEvent.toString().indexOf("[native code"))&&!Z?(Q=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,E?y.insertBefore(g,E):y.appendChild(g), -M=null,g;if(ea)try{importScripts(d),b.completeLoad(c)}catch(l){b.onError(B("importscripts","importScripts failed for "+c+" at "+d,l,[c]))}};A&&O(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return s=L,u.baseUrl||(F=s.split("/"),s=F.pop(),ga=F.length?F.join("/")+"/":"./",u.baseUrl=ga),s=s.replace(fa,""),h.jsExtRegExp.test(s)&&(s=L),u.deps=u.deps?u.deps.concat(s):[s],!0});define=function(b,c,d){var h,g;"string"!==typeof b&&(d=c,c=b,b=null); -K(c)||(d=c,c=null);!c&&J(d)&&(c=[],d.length&&(d.toString().replace(ma,"").replace(na,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(Q){if(!(h=M))R&&"interactive"===R.readyState||O(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return R=b}),h=R;h&&(b||(b=h.getAttribute("data-requiremodule")),g=G[h.getAttribute("data-requirecontext")])}(g?g.defQueue:U).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)}; -h(u)}})(this); \ No newline at end of file +(function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!== +ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c); +this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})|| +""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0=== +a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a, +b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id); +b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b= +a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&& +(p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs", +"Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){}, +0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&& +(d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this); diff --git a/public/vendor/tinycon/tinycon.js b/public/vendor/tinycon/tinycon.js index 3317cc0d03..3e3657cdf8 100644 --- a/public/vendor/tinycon/tinycon.js +++ b/public/vendor/tinycon/tinycon.js @@ -1,188 +1,200 @@ /*! * Tinycon - A small library for manipulating the Favicon * Tom Moor, http://tommoor.com - * Copyright (c) 2012 Tom Moor - * MIT Licensed - * @version 0.6.1 + * Copyright (c) 2015 Tom Moor + * @license MIT Licensed + * @version 0.6.4 */ (function(){ - var Tinycon = {}; - var currentFavicon = null; - var originalFavicon = null; - var originalTitle = document.title; - var faviconImage = null; - var canvas = null; - var options = {}; - var r = window.devicePixelRatio || 1; - var size = 16 * r; - var defaults = { - width: 7, - height: 9, - font: 9 * r + 'px arial', - colour: '#ffffff', - background: '#F03D25', - fallback: true, - crossOrigin: true, - abbreviate: true - }; + var Tinycon = {}; + var currentFavicon = null; + var originalFavicon = null; + var faviconImage = null; + var canvas = null; + var options = {}; + var r = window.devicePixelRatio || 1; + var size = 16 * r; + var defaults = { + width: 7, + height: 9, + font: 10 * r + 'px arial', + color: '#ffffff', + background: '#F03D25', + fallback: true, + crossOrigin: true, + abbreviate: true + }; - var ua = (function () { - var agent = navigator.userAgent.toLowerCase(); - // New function has access to 'agent' via closure - return function (browser) { - return agent.indexOf(browser) !== -1; - }; - }()); + var ua = (function () { + var agent = navigator.userAgent.toLowerCase(); + // New function has access to 'agent' via closure + return function (browser) { + return agent.indexOf(browser) !== -1; + }; + }()); - var browser = { - ie: ua('msie'), - chrome: ua('chrome'), - webkit: ua('chrome') || ua('safari'), - safari: ua('safari') && !ua('chrome'), - mozilla: ua('mozilla') && !ua('chrome') && !ua('safari') - }; + var browser = { + ie: ua('trident'), + chrome: ua('chrome'), + webkit: ua('chrome') || ua('safari'), + safari: ua('safari') && !ua('chrome'), + mozilla: ua('mozilla') && !ua('chrome') && !ua('safari') + }; - // private methods - var getFaviconTag = function(){ + // private methods + var getFaviconTag = function(){ - var links = document.getElementsByTagName('link'); + var links = document.getElementsByTagName('link'); - for(var i=0, len=links.length; i < len; i++) { - if ((links[i].getAttribute('rel') || '').match(/\bicon\b/)) { - return links[i]; - } - } + for(var i=0, len=links.length; i < len; i++) { + if ((links[i].getAttribute('rel') || '').match(/\bicon\b/i)) { + return links[i]; + } + } - return false; - }; + return false; + }; - var removeFaviconTag = function(){ + var removeFaviconTag = function(){ - var links = document.getElementsByTagName('link'); - var head = document.getElementsByTagName('head')[0]; + var links = document.getElementsByTagName('link'); + var head = document.getElementsByTagName('head')[0]; - for(var i=0, len=links.length; i < len; i++) { - var exists = (typeof(links[i]) !== 'undefined'); - if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/)) { - head.removeChild(links[i]); - } - } - }; + for(var i=0, len=links.length; i < len; i++) { + var exists = (typeof(links[i]) !== 'undefined'); + if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/i)) { + head.removeChild(links[i]); + } + } + }; - var getCurrentFavicon = function(){ + var getCurrentFavicon = function(){ - if (!originalFavicon || !currentFavicon) { - var tag = getFaviconTag(); - originalFavicon = currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico'; - } + if (!originalFavicon || !currentFavicon) { + var tag = getFaviconTag(); + currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico'; + if (!originalFavicon) { + originalFavicon = currentFavicon; + } + } - return currentFavicon; - }; + return currentFavicon; + }; - var getCanvas = function (){ + var getCanvas = function (){ - if (!canvas) { - canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - } + if (!canvas) { + canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + } - return canvas; - }; + return canvas; + }; - var setFaviconTag = function(url){ - removeFaviconTag(); + var setFaviconTag = function(url){ + if(url){ + removeFaviconTag(); - var link = document.createElement('link'); - link.type = 'image/x-icon'; - link.rel = 'icon'; - link.href = url; - document.getElementsByTagName('head')[0].appendChild(link); - }; + var link = document.createElement('link'); + link.type = 'image/x-icon'; + link.rel = 'icon'; + link.href = url; + document.getElementsByTagName('head')[0].appendChild(link); + } + }; - var log = function(message){ - if (window.console) window.console.log(message); - }; + var log = function(message){ + if (window.console) window.console.log(message); + }; - var drawFavicon = function(label, colour) { + var drawFavicon = function(label, color) { - // fallback to updating the browser title if unsupported - if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') { - return updateTitle(label); - } + // fallback to updating the browser title if unsupported + if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') { + return updateTitle(label); + } - var context = getCanvas().getContext("2d"); - var colour = colour || '#000000'; - var src = getCurrentFavicon(); + var context = getCanvas().getContext("2d"); + var color = color || '#000000'; + var src = getCurrentFavicon(); - faviconImage = document.createElement('img'); - faviconImage.onload = function() { + faviconImage = document.createElement('img'); + faviconImage.onload = function() { - // clear canvas - context.clearRect(0, 0, size, size); + // clear canvas + context.clearRect(0, 0, size, size); - // draw the favicon - context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size); + // draw the favicon + context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size); - // draw bubble over the top - if ((label + '').length > 0) drawBubble(context, label, colour); + // draw bubble over the top + if ((label + '').length > 0) drawBubble(context, label, color); - // refresh tag in page - refreshFavicon(); - }; + // refresh tag in page + refreshFavicon(); + }; - // allow cross origin resource requests if the image is not a data:uri - // as detailed here: https://github.com/mrdoob/three.js/issues/1305 - if (!src.match(/^data/) && options.crossOrigin) { - faviconImage.crossOrigin = 'anonymous'; - } + // allow cross origin resource requests if the image is not a data:uri + // as detailed here: https://github.com/mrdoob/three.js/issues/1305 + if (!src.match(/^data/) && options.crossOrigin) { + faviconImage.crossOrigin = 'anonymous'; + } - faviconImage.src = src; - }; + faviconImage.src = src; + }; - var updateTitle = function(label) { + var updateTitle = function(label) { - if (options.fallback) { - if ((label + '').length > 0) { - document.title = '(' + label + ') ' + originalTitle; - } else { - document.title = originalTitle; - } - } - }; + if (options.fallback) { + // Grab the current title that we can prefix with the label + var originalTitle = document.title; - var drawBubble = function(context, label, colour) { + // Strip out the old label if there is one + if (originalTitle[0] === '(') { + originalTitle = originalTitle.slice(originalTitle.indexOf(' ')); + } - // automatic abbreviation for long (>2 digits) numbers - if (typeof label == 'number' && label > 99 && options.abbreviate) { - label = abbreviateNumber(label); - } + if ((label + '').length > 0) { + document.title = '(' + label + ') ' + originalTitle; + } else { + document.title = originalTitle; + } + } + }; - // bubble needs to be larger for double digits - var len = (label + '').length-1; + var drawBubble = function(context, label, color) { - var width = options.width * r + (6 * r * len), - height = options.height * r; + // automatic abbreviation for long (>2 digits) numbers + if (typeof label == 'number' && label > 99 && options.abbreviate) { + label = abbreviateNumber(label); + } - var top = size - height, + // bubble needs to be larger for double digits + var len = (label + '').length-1; + + var width = options.width * r + (6 * r * len), + height = options.height * r; + + var top = size - height, left = size - width - r, bottom = 16 * r, right = 16 * r, radius = 2 * r; - // webkit seems to render fonts lighter than firefox - context.font = (browser.webkit ? 'bold ' : '') + options.font; - context.fillStyle = options.background; - context.strokeStyle = options.background; - context.lineWidth = r; + // webkit seems to render fonts lighter than firefox + context.font = (browser.webkit ? 'bold ' : '') + options.font; + context.fillStyle = options.background; + context.strokeStyle = options.background; + context.lineWidth = r; - // bubble - context.beginPath(); + // bubble + context.beginPath(); context.moveTo(left + radius, top); - context.quadraticCurveTo(left, top, left, top + radius); - context.lineTo(left, bottom - radius); + context.quadraticCurveTo(left, top, left, top + radius); + context.lineTo(left, bottom - radius); context.quadraticCurveTo(left, bottom, left + radius, bottom); context.lineTo(right - radius, bottom); context.quadraticCurveTo(right, bottom, right, bottom - radius); @@ -191,77 +203,85 @@ context.closePath(); context.fill(); - // bottom shadow - context.beginPath(); - context.strokeStyle = "rgba(0,0,0,0.3)"; - context.moveTo(left + radius / 2.0, bottom); - context.lineTo(right - radius / 2.0, bottom); - context.stroke(); + // bottom shadow + context.beginPath(); + context.strokeStyle = "rgba(0,0,0,0.3)"; + context.moveTo(left + radius / 2.0, bottom); + context.lineTo(right - radius / 2.0, bottom); + context.stroke(); - // label - context.fillStyle = options.colour; - context.textAlign = "right"; - context.textBaseline = "top"; + // label + context.fillStyle = options.color; + context.textAlign = "right"; + context.textBaseline = "top"; - // unfortunately webkit/mozilla are a pixel different in text positioning - context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r); - }; + // unfortunately webkit/mozilla are a pixel different in text positioning + context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r); + }; - var refreshFavicon = function(){ - // check support - if (!getCanvas().getContext) return; + var refreshFavicon = function(){ + // check support + if (!getCanvas().getContext) return; - setFaviconTag(getCanvas().toDataURL()); - }; + setFaviconTag(getCanvas().toDataURL()); + }; - var abbreviateNumber = function(label) { - var metricPrefixes = [ - ['G', 1000000000], - ['M', 1000000], - ['k', 1000] - ]; + var abbreviateNumber = function(label) { + var metricPrefixes = [ + ['G', 1000000000], + ['M', 1000000], + ['k', 1000] + ]; - for(var i = 0; i < metricPrefixes.length; ++i) { - if (label >= metricPrefixes[i][1]) { - label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0]; - break; - } - } + for(var i = 0; i < metricPrefixes.length; ++i) { + if (label >= metricPrefixes[i][1]) { + label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0]; + break; + } + } - return label; - }; + return label; + }; - var round = function (value, precision) { - var number = new Number(value); - return number.toFixed(precision); - }; + var round = function (value, precision) { + var number = new Number(value); + return number.toFixed(precision); + }; - // public methods - Tinycon.setOptions = function(custom){ - options = {}; + // public methods + Tinycon.setOptions = function(custom){ + options = {}; - for(var key in defaults){ - options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key]; - } - return this; - }; + // account for deprecated UK English spelling + if (custom.colour) { + custom.color = custom.colour; + } - Tinycon.setImage = function(url){ - currentFavicon = url; - refreshFavicon(); - return this; - }; + for(var key in defaults){ + options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key]; + } + return this; + }; - Tinycon.setBubble = function(label, colour) { - label = label || ''; - drawFavicon(label, colour); - return this; - }; + Tinycon.setImage = function(url){ + currentFavicon = url; + refreshFavicon(); + return this; + }; - Tinycon.reset = function(){ - setFaviconTag(originalFavicon); - }; + Tinycon.setBubble = function(label, color) { + label = label || ''; + drawFavicon(label, color); + return this; + }; - Tinycon.setOptions(defaults); - window.Tinycon = Tinycon; -})(); \ No newline at end of file + Tinycon.reset = function(){ + currentFavicon = originalFavicon; + setFaviconTag(originalFavicon); + }; + + Tinycon.setOptions(defaults); + + window.Tinycon = Tinycon; + +})(); diff --git a/src/analytics.js b/src/analytics.js index c1ede42eba..ab834b75b2 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -6,7 +6,7 @@ var winston = require('winston'); var db = require('./database'); -(function(Analytics) { +(function (Analytics) { var counters = {}; var pageViews = 0; @@ -15,24 +15,24 @@ var db = require('./database'); var isCategory = /^(?:\/api)?\/category\/(\d+)/; - new cronJob('*/10 * * * *', function() { + new cronJob('*/10 * * * *', function () { Analytics.writeData(); }, null, true); - Analytics.increment = function(keys) { + Analytics.increment = function (keys) { keys = Array.isArray(keys) ? keys : [keys]; - keys.forEach(function(key) { + keys.forEach(function (key) { counters[key] = counters[key] || 0; ++counters[key]; }); }; - Analytics.pageView = function(payload) { + Analytics.pageView = function (payload) { ++pageViews; if (payload.ip) { - db.sortedSetScore('ip:recent', payload.ip, function(err, score) { + db.sortedSetScore('ip:recent', payload.ip, function (err, score) { if (err) { return; } @@ -58,7 +58,7 @@ var db = require('./database'); } }; - Analytics.writeData = function() { + Analytics.writeData = function () { var today = new Date(); var month = new Date(); var dbQueue = []; @@ -92,14 +92,14 @@ var db = require('./database'); } } - async.parallel(dbQueue, function(err) { + async.parallel(dbQueue, function (err) { if (err) { winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); } }); }; - Analytics.getHourlyStatsForSet = function(set, hour, numHours, callback) { + Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) { var terms = {}, hoursArr = []; @@ -111,19 +111,19 @@ var db = require('./database'); hour.setHours(hour.getHours() - 1, 0, 0, 0); } - db.sortedSetScores(set, hoursArr, function(err, counts) { + db.sortedSetScores(set, hoursArr, function (err, counts) { if (err) { return callback(err); } - hoursArr.forEach(function(term, index) { + hoursArr.forEach(function (term, index) { terms[term] = parseInt(counts[index], 10) || 0; }); var termsArr = []; hoursArr.reverse(); - hoursArr.forEach(function(hour) { + hoursArr.forEach(function (hour) { termsArr.push(terms[hour]); }); @@ -131,36 +131,36 @@ var db = require('./database'); }); }; - Analytics.getDailyStatsForSet = function(set, day, numDays, callback) { + Analytics.getDailyStatsForSet = function (set, day, numDays, callback) { var daysArr = []; day = new Date(day); - day.setDate(day.getDate()+1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values + day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values day.setHours(0, 0, 0, 0); - async.whilst(function() { + async.whilst(function () { return numDays--; - }, function(next) { - Analytics.getHourlyStatsForSet(set, day.getTime()-(1000*60*60*24*numDays), 24, function(err, day) { + }, function (next) { + Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) { if (err) { return next(err); } - daysArr.push(day.reduce(function(cur, next) { - return cur+next; + daysArr.push(day.reduce(function (cur, next) { + return cur + next; })); next(); }); - }, function(err) { + }, function (err) { callback(err, daysArr); }); }; - Analytics.getUnwrittenPageviews = function() { + Analytics.getUnwrittenPageviews = function () { return pageViews; }; - Analytics.getMonthlyPageViews = function(callback) { + Analytics.getMonthlyPageViews = function (callback) { var thisMonth = new Date(); var lastMonth = new Date(); thisMonth.setMonth(thisMonth.getMonth(), 1); @@ -170,7 +170,7 @@ var db = require('./database'); var values = [thisMonth.getTime(), lastMonth.getTime()]; - db.sortedSetScores('analytics:pageviews:month', values, function(err, scores) { + db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { if (err) { return callback(err); } @@ -178,7 +178,7 @@ var db = require('./database'); }); }; - Analytics.getCategoryAnalytics = function(cid, callback) { + Analytics.getCategoryAnalytics = function (cid, callback) { async.parallel({ 'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24), 'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30), @@ -187,4 +187,11 @@ var db = require('./database'); }, callback); }; + Analytics.getErrorAnalytics = function (callback) { + async.parallel({ + 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), + 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) + }, callback); + }; + }(exports)); \ No newline at end of file diff --git a/src/batch.js b/src/batch.js index 1a425e1a21..ca0944b80d 100644 --- a/src/batch.js +++ b/src/batch.js @@ -6,17 +6,17 @@ var async = require('async'), db = require('./database'), utils = require('../public/src/utils'); -(function(Batch) { +(function (Batch) { var DEFAULT_BATCH_SIZE = 100; - Batch.processSortedSet = function(setKey, process, options, callback) { + Batch.processSortedSet = function (setKey, process, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } - callback = typeof callback === 'function' ? callback : function(){}; + callback = typeof callback === 'function' ? callback : function () {}; options = options || {}; if (typeof process !== 'function') { @@ -29,7 +29,7 @@ var async = require('async'), } // custom done condition - options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function(){}; + options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function (){}; var batch = options.batch || DEFAULT_BATCH_SIZE; var start = 0; @@ -37,11 +37,11 @@ var async = require('async'), var done = false; async.whilst( - function() { + function () { return !done; }, - function(next) { - db.getSortedSetRange(setKey, start, stop, function(err, ids) { + function (next) { + db.getSortedSetRange(setKey, start, stop, function (err, ids) { if (err) { return next(err); } @@ -49,7 +49,7 @@ var async = require('async'), done = true; return next(); } - process(ids, function(err) { + process(ids, function (err) { if (err) { return next(err); } @@ -63,4 +63,52 @@ var async = require('async'), ); }; + Batch.processArray = function (array, process, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + callback = typeof callback === 'function' ? callback : function () {}; + options = options || {}; + + if (!Array.isArray(array) || !array.length) { + return callback(); + } + if (typeof process !== 'function') { + return callback(new Error('[[error:process-not-a-function]]')); + } + + var batch = options.batch || DEFAULT_BATCH_SIZE; + var start = 0; + var done = false; + + async.whilst( + function () { + return !done; + }, + function (next) { + var currentBatch = array.slice(start, start + batch); + if (!currentBatch.length) { + done = true; + return next(); + } + process(currentBatch, function (err) { + if (err) { + return next(err); + } + start = start + batch; + if (options.interval) { + setTimeout(next, options.interval); + } else { + next(); + } + }); + }, + function (err) { + callback(err); + } + ); + }; + }(exports)); diff --git a/src/categories.js b/src/categories.js index cd2b18a787..4e2c435f8e 100644 --- a/src/categories.js +++ b/src/categories.js @@ -9,7 +9,7 @@ var Groups = require('./groups'); var plugins = require('./plugins'); var privileges = require('./privileges'); -(function(Categories) { +(function (Categories) { require('./categories/data')(Categories); require('./categories/create')(Categories); @@ -20,11 +20,11 @@ var privileges = require('./privileges'); require('./categories/recentreplies')(Categories); require('./categories/update')(Categories); - Categories.exists = function(cid, callback) { + Categories.exists = function (cid, callback) { db.isSortedSetMember('categories:cid', cid, callback); }; - Categories.getCategoryById = function(data, callback) { + Categories.getCategoryById = function (data, callback) { var category; async.waterfall([ function (next) { @@ -35,15 +35,19 @@ var privileges = require('./privileges'); return next(new Error('[[error:invalid-cid]]')); } category = categories[0]; - if (parseInt(data.uid, 10)) { - Categories.markAsRead([data.cid], data.uid); - } async.parallel({ - topics: function(next) { + topics: function (next) { Categories.getCategoryTopics(data, next); }, - isIgnored: function(next) { + topicCount: function (next) { + if (Array.isArray(data.set)) { + db.sortedSetIntersectCard(data.set, next); + } else { + next(null, category.topic_count); + } + }, + isIgnored: function (next) { Categories.isIgnored([data.cid], data.uid, next); } }, next); @@ -52,6 +56,7 @@ var privileges = require('./privileges'); category.topics = results.topics.topics; category.nextStart = results.topics.nextStart; category.isIgnored = results.isIgnored[0]; + category.topic_count = results.topicCount; plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, next); }, @@ -61,24 +66,24 @@ var privileges = require('./privileges'); ], callback); }; - Categories.isIgnored = function(cids, uid, callback) { - user.getIgnoredCategories(uid, function(err, ignoredCids) { + Categories.isIgnored = function (cids, uid, callback) { + user.getIgnoredCategories(uid, function (err, ignoredCids) { if (err) { return callback(err); } - cids = cids.map(function(cid) { + cids = cids.map(function (cid) { return ignoredCids.indexOf(cid.toString()) !== -1; }); callback(null, cids); }); }; - Categories.getPageCount = function(cid, uid, callback) { + Categories.getPageCount = function (cid, uid, callback) { async.parallel({ topicCount: async.apply(Categories.getCategoryField, cid, 'topic_count'), settings: async.apply(user.getSettings, uid) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -91,8 +96,8 @@ var privileges = require('./privileges'); }); }; - Categories.getAllCategories = function(uid, callback) { - db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) { + Categories.getAllCategories = function (uid, callback) { + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { if (err || !Array.isArray(cids) || !cids.length) { return callback(err, []); } @@ -101,22 +106,22 @@ var privileges = require('./privileges'); }); }; - Categories.getCategoriesByPrivilege = function(set, uid, privilege, callback) { + Categories.getCategoriesByPrivilege = function (set, uid, privilege, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange(set, 0, -1, next); }, - function(cids, next) { + function (cids, next) { privileges.categories.filterCids(privilege, cids, uid, next); }, - function(cids, next) { + function (cids, next) { Categories.getCategories(cids, uid, next); } ], callback); }; - Categories.getModerators = function(cid, callback) { - Groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, function(err, uids) { + Categories.getModerators = function (cid, callback) { + Groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, function (err, uids) { if (err || !Array.isArray(uids) || !uids.length) { return callback(err, []); } @@ -126,7 +131,7 @@ var privileges = require('./privileges'); }; - Categories.getCategories = function(cids, uid, callback) { + Categories.getCategories = function (cids, uid, callback) { if (!Array.isArray(cids)) { return callback(new Error('[[error:invalid-cid]]')); } @@ -136,19 +141,19 @@ var privileges = require('./privileges'); } async.parallel({ - categories: function(next) { + categories: function (next) { Categories.getCategoriesData(cids, next); }, - children: function(next) { + children: function (next) { Categories.getChildren(cids, uid, next); }, - parents: function(next) { + parents: function (next) { Categories.getParents(cids, next); }, - hasRead: function(next) { + hasRead: function (next) { Categories.hasReadCategories(cids, uid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -156,7 +161,7 @@ var privileges = require('./privileges'); var categories = results.categories; var hasRead = results.hasRead; uid = parseInt(uid, 10); - for(var i=0; i 2; - - res.render('chats', room); + room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10); + room.users = data.users.filter(function (user) { + return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; }); + + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; + room.rooms = recentChats.rooms; + room.uid = uid; + room.userslug = req.params.userslug; + room.nextStart = recentChats.nextStart; + room.title = room.roomName; + room.breadcrumbs = helpers.buildBreadcrumbs([ + {text: username, url: '/user/' + req.params.userslug}, + {text: '[[pages:chats]]', url: '/user/' + req.params.userslug + '/chats'}, + {text: room.roomName} + ]); + room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0; + room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000; + room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; + + res.render('chats', room); }); }; +chatsController.redirectToChat = function (req, res, next) { + var roomid = parseInt(req.params.roomid, 10); + if (!req.uid) { + return next(); + } + user.getUserField(req.uid, 'userslug', function (err, userslug) { + if (err || !userslug) { + return next(err); + } + + if (!roomid) { + return helpers.redirect(res, '/user/' + userslug + '/chats'); + } + helpers.redirect(res, '/user/' + userslug + '/chats/' + roomid); + }); +}; + + + module.exports = chatsController; \ No newline at end of file diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 09e824ecd9..3fe2c57092 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -10,12 +10,13 @@ var user = require('../../user'); var meta = require('../../meta'); var plugins = require('../../plugins'); var helpers = require('../helpers'); +var groups = require('../../groups'); var accountHelpers = require('./helpers'); var editController = {}; -editController.get = function(req, res, callback) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function(err, userData) { +editController.get = function (req, res, callback) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function (err, userData) { if (err || !userData) { return callback(err); } @@ -25,15 +26,21 @@ editController.get = function(req, res, callback) { userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10); userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads) === 1; userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1; - + + userData.groups = userData.groups.filter(function (group) { + return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users'; + }); + userData.groups.forEach(function (group) { + group.selected = group.name === userData.groupTitle; + }); userData.title = '[[pages:account/edit, ' + userData.username + ']]'; userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]); userData.editButtons = []; - plugins.fireHook('filter:user.account.edit', userData, function(err, userData) { + plugins.fireHook('filter:user.account.edit', userData, function (err, userData) { if (err) { - return next(err); + return callback(err); } res.render('account/edit', userData); @@ -41,20 +48,20 @@ editController.get = function(req, res, callback) { }); }; -editController.password = function(req, res, next) { +editController.password = function (req, res, next) { renderRoute('password', req, res, next); }; -editController.username = function(req, res, next) { +editController.username = function (req, res, next) { renderRoute('username', req, res, next); }; -editController.email = function(req, res, next) { +editController.email = function (req, res, next) { renderRoute('email', req, res, next); }; function renderRoute(name, req, res, next) { - getUserData(req, next, function(err, userData) { + getUserData(req, next, function (err, userData) { if (err) { return next(err); } @@ -80,17 +87,17 @@ function renderRoute(name, req, res, next) { function getUserData(req, next, callback) { var userData; async.waterfall([ - function(next) { + function (next) { accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, - function(data, next) { + function (data, next) { userData = data; if (!userData) { return next(); } db.getObjectField('user:' + userData.uid, 'password', next); } - ], function(err, password) { + ], function (err, password) { if (err) { return callback(err); } @@ -106,10 +113,10 @@ editController.uploadPicture = function (req, res, next) { var updateUid; async.waterfall([ - function(next) { + function (next) { user.getUidByUserslug(req.params.userslug, next); }, - function(uid, next) { + function (uid, next) { updateUid = uid; if (parseInt(req.uid, 10) === parseInt(uid, 10)) { return next(null, true); @@ -117,16 +124,18 @@ editController.uploadPicture = function (req, res, next) { user.isAdminOrGlobalMod(req.uid, next); }, - function(isAllowed, next) { + function (isAllowed, next) { if (!isAllowed) { return helpers.notAllowed(req, res); } - + user.uploadPicture(updateUid, userPhoto, next); } - ], function(err, image) { - fs.unlink(userPhoto.path, function(err) { - winston.error('unable to delete picture ' + userPhoto.path, err); + ], function (err, image) { + fs.unlink(userPhoto.path, function (err) { + if (err) { + winston.warn('[user/picture] Unable to delete picture ' + userPhoto.path, err); + } }); if (err) { return next(err); @@ -136,13 +145,13 @@ editController.uploadPicture = function (req, res, next) { }); }; -editController.uploadCoverPicture = function(req, res, next) { +editController.uploadCoverPicture = function (req, res, next) { var params = JSON.parse(req.body.params); user.updateCoverPicture({ file: req.files.files[0], uid: params.uid - }, function(err, image) { + }, function (err, image) { if (err) { return next(err); } diff --git a/src/controllers/accounts/follow.js b/src/controllers/accounts/follow.js index f9dc72c6f3..7d1e91b5e2 100644 --- a/src/controllers/accounts/follow.js +++ b/src/controllers/accounts/follow.js @@ -1,44 +1,52 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - user = require('../../user'), - helpers = require('../helpers'), - accountHelpers = require('./helpers'); +var user = require('../../user'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); +var pagination = require('../../pagination'); var followController = {}; -followController.getFollowing = function(req, res, next) { +followController.getFollowing = function (req, res, next) { getFollow('account/following', 'following', req, res, next); }; -followController.getFollowers = function(req, res, next) { +followController.getFollowers = function (req, res, next) { getFollow('account/followers', 'followers', req, res, next); }; function getFollow(tpl, name, req, res, callback) { var userData; + var page = parseInt(req.query.page, 10) || 1; + var resultsPerPage = 50; + var start = Math.max(0, page - 1) * resultsPerPage; + var stop = start + resultsPerPage - 1; + async.waterfall([ - function(next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, - function(data, next) { + function (data, next) { userData = data; if (!userData) { return callback(); } var method = name === 'following' ? 'getFollowing' : 'getFollowers'; - user[method](userData.uid, 0, 49, next); + user[method](userData.uid, start, stop, next); } - ], function(err, users) { + ], function (err, users) { if (err) { return callback(err); } userData.users = users; - userData.nextStart = 50; userData.title = '[[pages:' + tpl + ', ' + userData.username + ']]'; + var count = name === 'following' ? userData.followingCount : userData.followerCount; + var pageCount = Math.ceil(count / resultsPerPage); + userData.pagination = pagination.create(page, pageCount); userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:' + name + ']]'}]); res.render(tpl, userData); diff --git a/src/controllers/accounts/groups.js b/src/controllers/accounts/groups.js index e19034c908..038d63de1d 100644 --- a/src/controllers/accounts/groups.js +++ b/src/controllers/accounts/groups.js @@ -1,21 +1,21 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - groups = require('../../groups'), - helpers = require('../helpers'), - accountHelpers = require('./helpers'); +var groups = require('../../groups'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); var groupsController = {}; -groupsController.get = function(req, res, callback) { +groupsController.get = function (req, res, callback) { var userData; var groupsData; async.waterfall([ function (next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, function (_userData, next) { userData = _userData; @@ -27,19 +27,19 @@ groupsController.get = function(req, res, callback) { }, function (_groupsData, next) { groupsData = _groupsData[0]; - var groupNames = groupsData.filter(Boolean).map(function(group) { + var groupNames = groupsData.filter(Boolean).map(function (group) { return group.name; }); groups.getMemberUsers(groupNames, 0, 3, next); }, function (members, next) { - groupsData.forEach(function(group, index) { + groupsData.forEach(function (group, index) { group.members = members[index]; }); next(); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 8acbdc08c8..1046529442 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -3,16 +3,17 @@ var async = require('async'); var validator = require('validator'); +var winston = require('winston'); var user = require('../../user'); var groups = require('../../groups'); -var plugins =require('../../plugins'); +var plugins = require('../../plugins'); var meta = require('../../meta'); var utils = require('../../../public/src/utils'); var helpers = {}; -helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { +helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { async.waterfall([ function (next) { user.getUidByUserslug(userslug, next); @@ -23,28 +24,34 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { } async.parallel({ - userData : function(next) { + userData : function (next) { user.getUserData(uid, next); }, - userSettings : function(next) { + userSettings : function (next) { user.getSettings(uid, next); }, - isAdmin : function(next) { + isAdmin : function (next) { user.isAdministrator(callerUID, next); }, - isGlobalModerator: function(next) { + isGlobalModerator: function (next) { user.isGlobalModerator(callerUID, next); }, - ips: function(next) { + isFollowing: function (next) { + user.isFollowing(callerUID, uid, next); + }, + ips: function (next) { user.getIPs(uid, 4, next); }, - profile_links: function(next) { + profile_links: function (next) { plugins.fireHook('filter:user.profileLinks', [], next); }, - groups: function(next) { + profile_menu: function (next) { + plugins.fireHook('filter:user.profileMenu', {uid: uid, callerUID: callerUID, links: []}, next); + }, + groups: function (next) { groups.getUserGroups([uid], next); }, - sso: function(next) { + sso: function (next) { plugins.fireHook('filter:auth.list', {uid: uid, associations: []}, next); } }, next); @@ -80,35 +87,44 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { userData.ips = results.ips; } + if (!isAdmin && !isGlobalModerator) { + userData.moderationNote = undefined; + } + userData.uid = userData.uid; userData.yourid = callerUID; userData.theirid = userData.uid; userData.isAdmin = isAdmin; userData.isGlobalModerator = isGlobalModerator; + userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator; userData.canBan = isAdmin || isGlobalModerator; userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1); userData.isSelf = isSelf; + userData.isFollowing = results.isFollowing; userData.showHidden = isSelf || isAdmin || isGlobalModerator; userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : []; userData.disableSignatures = meta.config.disableSignatures !== undefined && parseInt(meta.config.disableSignatures, 10) === 1; userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10); - userData.profile_links = filterLinks(results.profile_links, isSelf); + userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf); + userData.sso = results.sso.associations; userData.status = user.getStatus(userData); userData.banned = parseInt(userData.banned, 10) === 1; - userData.website = validator.escape(userData.website || ''); + userData.website = validator.escape(String(userData.website || '')); userData.websiteLink = !userData.website.startsWith('http') ? 'http://' + userData.website : userData.website; userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), ''); userData.followingCount = parseInt(userData.followingCount, 10) || 0; userData.followerCount = parseInt(userData.followerCount, 10) || 0; - userData.email = validator.escape(userData.email || ''); - userData.fullname = validator.escape(userData.fullname || ''); - userData.location = validator.escape(userData.location || ''); - userData.signature = validator.escape(userData.signature || ''); - userData.aboutme = validator.escape(userData.aboutme || ''); + userData.email = validator.escape(String(userData.email || '')); + userData.fullname = validator.escape(String(userData.fullname || '')); + userData.location = validator.escape(String(userData.location || '')); + userData.signature = validator.escape(String(userData.signature || '')); + userData.aboutme = validator.escape(String(userData.aboutme || '')); + userData.birthday = validator.escape(String(userData.birthday || '')); + userData.moderationNote = validator.escape(String(userData.moderationNote || '')); userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid); userData['cover:position'] = userData['cover:position'] || '50% 50%'; @@ -121,55 +137,13 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { }; -helpers.getBaseUser = function(userslug, callerUID, callback) { - async.waterfall([ - function (next) { - user.getUidByUserslug(userslug, next); - }, - function (uid, next) { - if (!uid) { - return callback(null, null); - } - - async.parallel({ - user: function(next) { - user.getUserFields(uid, ['uid', 'username', 'userslug', 'picture', 'cover:url', 'cover:position', 'status', 'lastonline'], next); - }, - isAdmin: function(next) { - user.isAdministrator(callerUID, next); - }, - isGlobalModerator: function(next) { - user.isGlobalModerator(callerUID, next); - }, - profile_links: function(next) { - plugins.fireHook('filter:user.profileLinks', [], next); - } - }, next); - }, - function (results, next) { - if (!results.user) { - return callback(); - } - - results.user.yourid = callerUID; - results.user.theirid = results.user.uid; - results.user.status = user.getStatus(results.user); - results.user.isSelf = parseInt(callerUID, 10) === parseInt(results.user.uid, 10); - results.user.showHidden = results.user.isSelf || results.isAdmin || results.isGlobalModerator; - results.user.profile_links = filterLinks(results.profile_links, results.user.isSelf); - - results.user['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - results.user['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; - results.user['cover:url'] = results.user['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(results.user.uid); - results.user['cover:position'] = results.user['cover:position'] || '50% 50%'; - - next(null, results.user); - } - ], callback); +helpers.getBaseUser = function (userslug, callerUID, callback) { + winston.warn('helpers.getBaseUser deprecated please use helpers.getUserDataByUserSlug'); + helpers.getUserDataByUserSlug(userslug, callerUID, callback); }; function filterLinks(links, self) { - return links.filter(function(link) { + return links.filter(function (link) { return link && (link.public || self); }); } diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js new file mode 100644 index 0000000000..77ab2f275e --- /dev/null +++ b/src/controllers/accounts/info.js @@ -0,0 +1,45 @@ +'use strict'; + +var async = require('async'); + +var user = require('../../user'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); + +var infoController = {}; + +infoController.get = function (req, res, callback) { + var userData; + async.waterfall([ + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); + }, + function (_userData, next) { + userData = _userData; + if (!userData) { + return callback(); + } + async.parallel({ + history: async.apply(user.getModerationHistory, userData.uid), + sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID), + usernames: async.apply(user.getHistory, 'user:' + userData.uid + ':usernames'), + emails: async.apply(user.getHistory, 'user:' + userData.uid + ':emails') + }, next); + } + ], function (err, data) { + if (err) { + return callback(err); + } + + userData.history = data.history; + userData.sessions = data.sessions; + userData.usernames = data.usernames; + userData.emails = data.emails; + userData.title = '[[pages:account/info]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:account_info]]'}]); + + res.render('account/info', userData); + }); +}; + +module.exports = infoController; \ No newline at end of file diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index aa60892f47..91b9eb1d07 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -6,8 +6,8 @@ var user = require('../../user'), var notificationsController = {}; -notificationsController.get = function(req, res, next) { - user.notifications.getAll(req.uid, 0, 39, function(err, notifications) { +notificationsController.get = function (req, res, next) { + user.notifications.getAll(req.uid, 0, 39, function (err, notifications) { if (err) { return next(err); } diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 7e1e67a8bd..53ff073dad 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -1,31 +1,31 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - db = require('../../database'), - user = require('../../user'), - posts = require('../../posts'), - topics = require('../../topics'), - pagination = require('../../pagination'), - helpers = require('../helpers'), - accountHelpers = require('./helpers'); +var db = require('../../database'); +var user = require('../../user'); +var posts = require('../../posts'); +var topics = require('../../topics'); +var pagination = require('../../pagination'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); var postsController = {}; -postsController.getFavourites = function(req, res, next) { +postsController.getBookmarks = function (req, res, next) { var data = { - template: 'account/favourites', - set: 'favourites', + template: 'account/bookmarks', + set: 'bookmarks', type: 'posts', - noItemsFoundKey: '[[topic:favourites.has_no_favourites]]', + noItemsFoundKey: '[[topic:bookmarks.has_no_bookmarks]]', method: posts.getPostSummariesFromSet, - crumb: '[[user:favourites]]' + crumb: '[[user:bookmarks]]' }; getFromUserSet(data, req, res, next); }; -postsController.getPosts = function(req, res, next) { +postsController.getPosts = function (req, res, next) { var data = { template: 'account/posts', set: 'posts', @@ -37,7 +37,7 @@ postsController.getPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getUpVotedPosts = function(req, res, next) { +postsController.getUpVotedPosts = function (req, res, next) { var data = { template: 'account/upvoted', set: 'upvote', @@ -49,7 +49,7 @@ postsController.getUpVotedPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getDownVotedPosts = function(req, res, next) { +postsController.getDownVotedPosts = function (req, res, next) { var data = { template: 'account/downvoted', set: 'downvote', @@ -61,7 +61,7 @@ postsController.getDownVotedPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getBestPosts = function(req, res, next) { +postsController.getBestPosts = function (req, res, next) { var data = { template: 'account/best', set: 'posts:votes', @@ -73,7 +73,7 @@ postsController.getBestPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getWatchedTopics = function(req, res, next) { +postsController.getWatchedTopics = function (req, res, next) { var data = { template: 'account/watched', set: 'followed_tids', @@ -85,7 +85,7 @@ postsController.getWatchedTopics = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getTopics = function(req, res, next) { +postsController.getTopics = function (req, res, next) { var data = { template: 'account/topics', set: 'topics', @@ -99,13 +99,13 @@ postsController.getTopics = function(req, res, next) { function getFromUserSet(data, req, res, next) { async.parallel({ - settings: function(next) { + settings: function (next) { user.getSettings(req.uid, next); }, - userData: function(next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + userData: function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); } - }, function(err, results) { + }, function (err, results) { if (err || !results.userData) { return next(err); } @@ -118,19 +118,19 @@ function getFromUserSet(data, req, res, next) { var itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage; async.parallel({ - itemCount: function(next) { + itemCount: function (next) { if (results.settings.usePagination) { db.sortedSetCard(setName, next); } else { next(null, 0); } }, - data: function(next) { + data: function (next) { var start = (page - 1) * itemsPerPage; var stop = start + itemsPerPage - 1; data.method(setName, req.uid, start, stop, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index e52b5f4861..f0fa1e1378 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -14,7 +14,7 @@ var helpers = require('../helpers'); var profileController = {}; -profileController.get = function(req, res, callback) { +profileController.get = function (req, res, callback) { var lowercaseSlug = req.params.userslug.toLowerCase(); if (req.params.userslug !== lowercaseSlug) { @@ -43,18 +43,18 @@ profileController.get = function(req, res, callback) { } async.parallel({ - isFollowing: function(next) { + isFollowing: function (next) { user.isFollowing(req.uid, userData.theirid, next); }, - posts: function(next) { + posts: function (next) { posts.getPostSummariesFromSet('uid:' + userData.theirid + ':posts', req.uid, 0, 9, next); }, - signature: function(next) { + signature: function (next) { posts.parseSignature(userData, req.uid, next); }, - aboutme: function(next) { + aboutme: function (next) { if (userData.aboutme) { - plugins.fireHook('filter:parse.raw', userData.aboutme, next); + plugins.fireHook('filter:parse.aboutme', userData.aboutme, next); } else { next(); } @@ -118,10 +118,13 @@ profileController.get = function(req, res, callback) { } ); } + userData.selectedGroup = userData.groups.find(function (group) { + return group && group.name === userData.groupTitle; + }); plugins.fireHook('filter:user.account', {userData: userData, uid: req.uid}, next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } diff --git a/src/controllers/accounts/session.js b/src/controllers/accounts/session.js index 8fdb180ded..7e31906f26 100644 --- a/src/controllers/accounts/session.js +++ b/src/controllers/accounts/session.js @@ -7,20 +7,27 @@ var user = require('../../user'); var sessionController = {}; -sessionController.revoke = function(req, res, next) { +sessionController.revoke = function (req, res, next) { if (!req.params.hasOwnProperty('uuid')) { return next(); } var _id; - + var uid; async.waterfall([ function (next) { - db.getSortedSetRange('uid:' + req.uid + ':sessions', 0, -1, next); + user.getUidByUserslug(req.params.userslug, next); + }, + function (_uid, next) { + uid = _uid; + if (!uid) { + return next(new Error('[[error:no-session-found]]')); + } + db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1, next); }, function (sids, done) { - async.eachSeries(sids, function(sid, next) { - db.sessionStore.get(sid, function(err, sessionObj) { + async.eachSeries(sids, function (sid, next) { + db.sessionStore.get(sid, function (err, sessionObj) { if (err) { return next(err); } @@ -38,9 +45,9 @@ sessionController.revoke = function(req, res, next) { return next(new Error('[[error:no-session-found]]')); } - user.auth.revokeSession(_id, req.uid, next); + user.auth.revokeSession(_id, uid, next); } - ], function(err) { + ], function (err) { if (err) { return res.status(500).send(err.message); } else { diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index b5b020c118..44499e7e68 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -3,7 +3,6 @@ var async = require('async'); var user = require('../../user'); -var groups = require('../../groups'); var languages = require('../../languages'); var meta = require('../../meta'); var plugins = require('../../plugins'); @@ -17,53 +16,60 @@ var accountHelpers = require('./helpers'); var settingsController = {}; -settingsController.get = function(req, res, callback) { +settingsController.get = function (req, res, callback) { var userData; async.waterfall([ - function(next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, - function(_userData, next) { + function (_userData, next) { userData = _userData; if (!userData) { return callback(); } async.parallel({ - settings: function(next) { + settings: function (next) { user.getSettings(userData.uid, next); }, - userGroups: function(next) { - groups.getUserGroupsFromSet('groups:createtime', [userData.uid], next); - }, - languages: function(next) { + languages: function (next) { languages.list(next); }, - homePageRoutes: function(next) { + homePageRoutes: function (next) { getHomePageRoutes(next); }, - ips: function (next) { - user.getIPs(userData.uid, 4, next); + sounds: function (next) { + meta.sounds.getFiles(next); }, - sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID) + soundsMapping: function (next) { + meta.sounds.getMapping(userData.uid, next); + } }, next); }, - function(results, next) { + function (results, next) { userData.settings = results.settings; - userData.userGroups = results.userGroups[0].filter(function(group) { - return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users'; - }); userData.languages = results.languages; userData.homePageRoutes = results.homePageRoutes; - userData.ips = results.ips; - userData.sessions = results.sessions; + + var soundSettings = { + 'notificationSound': 'notification', + 'incomingChatSound': 'chat-incoming', + 'outgoingChatSound': 'chat-outgoing' + }; + + Object.keys(soundSettings).forEach(function (setting) { + userData[setting] = Object.keys(results.sounds).map(function (name) { + return {name: name, selected: name === results.soundsMapping[soundSettings[setting]]}; + }); + }); + plugins.fireHook('filter:user.customSettings', {settings: results.settings, customSettings: [], uid: req.uid}, next); }, - function(data, next) { + function (data, next) { userData.customSettings = data.customSettings; userData.disableEmailSubscriptions = parseInt(meta.config.disableEmailSubscriptions, 10) === 1; next(); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -96,19 +102,29 @@ settingsController.get = function(req, res, callback) { { "name": "Yeti", "value": "yeti" } ]; - userData.homePageRoutes.forEach(function(route) { + var isCustom = true; + userData.homePageRoutes.forEach(function (route) { route.selected = route.route === userData.settings.homePageRoute; + if (route.selected) { + isCustom = false; + } }); - userData.bootswatchSkinOptions.forEach(function(skin) { + if (isCustom && userData.settings.homePageRoute === 'none') { + isCustom = false; + } + + userData.homePageRoutes.push({ + route: 'custom', + name: 'Custom', + selected: isCustom + }); + + userData.bootswatchSkinOptions.forEach(function (skin) { skin.selected = skin.value === userData.settings.bootswatchSkin; }); - userData.userGroups.forEach(function(group) { - group.selected = group.name === userData.settings.groupTitle; - }); - - userData.languages.forEach(function(language) { + userData.languages.forEach(function (language) { language.selected = language.code === userData.settings.userLang; }); @@ -116,6 +132,8 @@ settingsController.get = function(req, res, callback) { userData.allowUserHomePage = parseInt(meta.config.allowUserHomePage, 10) === 1; + userData.inTopicSearchAvailable = plugins.hasListeners('filter:topic.search'); + userData.title = '[[pages:account/settings]]'; userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:settings]]'}]); @@ -136,46 +154,38 @@ function getHomePageRoutes(callback) { categories.getCategoriesFields(cids, ['name', 'slug'], next); }, function (categoryData, next) { - categoryData = categoryData.map(function(category) { + categoryData = categoryData.map(function (category) { return { route: 'category/' + category.slug, name: 'Category: ' + category.name }; }); - next(null, categoryData); + + categoryData = categoryData || []; + + plugins.fireHook('filter:homepage.get', {routes: [ + { + route: 'categories', + name: 'Categories' + }, + { + route: 'unread', + name: 'Unread' + }, + { + route: 'recent', + name: 'Recent' + }, + { + route: 'popular', + name: 'Popular' + } + ].concat(categoryData)}, next); + }, + function (data, next) { + next(null, data.routes); } - ], function(err, categoryData) { - if (err) { - return callback(err); - } - categoryData = categoryData || []; - - plugins.fireHook('filter:homepage.get', {routes: [ - { - route: 'categories', - name: 'Categories' - }, - { - route: 'recent', - name: 'Recent' - }, - { - route: 'popular', - name: 'Popular' - } - ].concat(categoryData)}, function(err, data) { - if (err) { - return callback(err); - } - - data.routes.push({ - route: 'custom', - name: 'Custom' - }); - - callback(null, data.routes); - }); - }); + ], callback); } diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 2bba60cae6..7f622466cd 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -14,8 +14,9 @@ var adminController = { }, events: require('./admin/events'), logs: require('./admin/logs'), + errors: require('./admin/errors'), database: require('./admin/database'), - postCache: require('./admin/postCache'), + cache: require('./admin/cache'), plugins: require('./admin/plugins'), languages: require('./admin/languages'), settings: require('./admin/settings'), diff --git a/src/controllers/admin/appearance.js b/src/controllers/admin/appearance.js index 8d60efda23..8956bd175d 100644 --- a/src/controllers/admin/appearance.js +++ b/src/controllers/admin/appearance.js @@ -2,7 +2,7 @@ var appearanceController = {}; -appearanceController.get = function(req, res, next) { +appearanceController.get = function (req, res, next) { var term = req.params.term ? req.params.term : 'themes'; res.render('admin/appearance/' + term, {}); diff --git a/src/controllers/admin/blacklist.js b/src/controllers/admin/blacklist.js index 2c0104f742..d70b1d1d79 100644 --- a/src/controllers/admin/blacklist.js +++ b/src/controllers/admin/blacklist.js @@ -4,8 +4,8 @@ var meta = require('../../meta'); var blacklistController = {}; -blacklistController.get = function(req, res, next) { - meta.blacklist.get(function(err, rules) { +blacklistController.get = function (req, res, next) { + meta.blacklist.get(function (err, rules) { if (err) { return next(err); } diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js new file mode 100644 index 0000000000..21ef6ff086 --- /dev/null +++ b/src/controllers/admin/cache.js @@ -0,0 +1,35 @@ +'use strict'; + +var cacheController = {}; + +cacheController.get = function (req, res, next) { + var postCache = require('../../posts/cache'); + var groupCache = require('../../groups').cache; + + var avgPostSize = 0; + var percentFull = 0; + if (postCache.itemCount > 0) { + avgPostSize = parseInt((postCache.length / postCache.itemCount), 10); + percentFull = ((postCache.length / postCache.max) * 100).toFixed(2); + } + + res.render('admin/advanced/cache', { + postCache: { + length: postCache.length, + max: postCache.max, + itemCount: postCache.itemCount, + percentFull: percentFull, + avgPostSize: avgPostSize + }, + groupCache: { + length: groupCache.length, + max: groupCache.max, + itemCount: groupCache.itemCount, + percentFull: ((groupCache.length / groupCache.max) * 100).toFixed(2), + dump: req.query.debug ? JSON.stringify(groupCache.dump(), null, 4) : false + } + }); +}; + + +module.exports = cacheController; \ No newline at end of file diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index 5444a087ea..8a59bb0b4e 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -6,25 +6,30 @@ var categories = require('../../categories'); var privileges = require('../../privileges'); var analytics = require('../../analytics'); var plugins = require('../../plugins'); -var translator = require('../../../public/src/modules/translator') +var translator = require('../../../public/src/modules/translator'); var categoriesController = {}; -categoriesController.get = function(req, res, next) { +categoriesController.get = function (req, res, next) { async.parallel({ category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid), privileges: async.apply(privileges.categories.list, req.params.category_id) - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } + var category = data.category[0]; - plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: data.category[0], privileges: data.privileges }, function(err, data) { + if (!category) { + return next(); + } + + plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: category, privileges: data.privileges }, function (err, data) { if (err) { return next(err); } - data.category.name = translator.escape(data.category.name); + data.category.name = translator.escape(String(data.category.name)); res.render('admin/manage/category', { category: data.category, privileges: data.privileges @@ -33,16 +38,20 @@ categoriesController.get = function(req, res, next) { }); }; -categoriesController.getAll = function(req, res, next) { +categoriesController.getAll = function (req, res, next) { // Categories list will be rendered on client side with recursion, etc. res.render('admin/manage/categories', {}); }; -categoriesController.getAnalytics = function(req, res, next) { +categoriesController.getAnalytics = function (req, res, next) { async.parallel({ name: async.apply(categories.getCategoryField, req.params.category_id, 'name'), analytics: async.apply(analytics.getCategoryAnalytics, req.params.category_id) - }, function(err, data) { + }, function (err, data) { + if (err) { + return next(err); + } + res.render('admin/manage/category-analytics', data); }); }; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index 24a65983f1..22105b6652 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -10,17 +10,17 @@ var plugins = require('../../plugins'); var dashboardController = {}; -dashboardController.get = function(req, res, next) { +dashboardController.get = function (req, res, next) { async.parallel({ - stats: function(next) { + stats: function (next) { getStats(next); }, - notices: function(next) { + notices: function (next) { var notices = [ { done: !meta.reloadRequired, - doneText: 'Reload not required', - notDoneText:'Reload required' + doneText: 'Restart not required', + notDoneText:'Restart required' }, { done: plugins.hasListeners('filter:search.query'), @@ -32,7 +32,7 @@ dashboardController.get = function(req, res, next) { ]; plugins.fireHook('filter:admin.notices', notices, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } @@ -46,19 +46,19 @@ dashboardController.get = function(req, res, next) { function getStats(callback) { async.parallel([ - function(next) { + function (next) { getStatsForSet('ip:recent', 'uniqueIPCount', next); }, - function(next) { + function (next) { getStatsForSet('users:joindate', 'userCount', next); }, - function(next) { + function (next) { getStatsForSet('posts:pid', 'postCount', next); }, - function(next) { + function (next) { getStatsForSet('topics:tid', 'topicCount', next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } @@ -80,23 +80,23 @@ function getStatsForSet(set, field, callback) { var now = Date.now(); async.parallel({ - day: function(next) { + day: function (next) { db.sortedSetCount(set, now - terms.day, '+inf', next); }, - week: function(next) { + week: function (next) { db.sortedSetCount(set, now - terms.week, '+inf', next); }, - month: function(next) { + month: function (next) { db.sortedSetCount(set, now - terms.month, '+inf', next); }, - alltime: function(next) { + alltime: function (next) { getGlobalField(field, next); } }, callback); } function getGlobalField(field, callback) { - db.getObjectField('global', field, function(err, count) { + db.getObjectField('global', field, function (err, count) { callback(err, parseInt(count, 10) || 0); }); } diff --git a/src/controllers/admin/database.js b/src/controllers/admin/database.js index d15db3f09c..5a28b95ec4 100644 --- a/src/controllers/admin/database.js +++ b/src/controllers/admin/database.js @@ -7,18 +7,17 @@ var databaseController = {}; -databaseController.get = function(req, res, next) { +databaseController.get = function (req, res, next) { async.parallel({ - redis: function(next) { + redis: function (next) { if (nconf.get('redis')) { var rdb = require('../../database/redis'); - var cxn = rdb.connect(); - rdb.info(cxn, next); + rdb.info(rdb.client, next); } else { next(); } }, - mongo: function(next) { + mongo: function (next) { if (nconf.get('mongo')) { var mdb = require('../../database/mongo'); mdb.info(mdb.client, next); @@ -26,7 +25,7 @@ databaseController.get = function(req, res, next) { next(); } } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } diff --git a/src/controllers/admin/errors.js b/src/controllers/admin/errors.js new file mode 100644 index 0000000000..4cacd425c9 --- /dev/null +++ b/src/controllers/admin/errors.js @@ -0,0 +1,38 @@ +'use strict'; + +var async = require('async'); +var json2csv = require('json-2-csv').json2csv; + +var meta = require('../../meta'); +var analytics = require('../../analytics'); + +var errorsController = {}; + +errorsController.get = function (req, res, next) { + async.parallel({ + 'not-found': async.apply(meta.errors.get, true), + analytics: async.apply(analytics.getErrorAnalytics) + }, function (err, data) { + if (err) { + return next(err); + } + + res.render('admin/advanced/errors', data); + }); +}; + +errorsController.export = function (req, res, next) { + async.waterfall([ + async.apply(meta.errors.get, false), + async.apply(json2csv) + ], function (err, csv) { + if (err) { + return next(err); + } + + res.set('Content-Type', 'text/csv').set('Content-Disposition', 'attachment; filename="404.csv"').send(csv); + }); +}; + + +module.exports = errorsController; \ No newline at end of file diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js index ceee1e2a70..8a4d63bad1 100644 --- a/src/controllers/admin/events.js +++ b/src/controllers/admin/events.js @@ -1,18 +1,38 @@ 'use strict'; +var async = require('async'); + +var db = require('../../database'); var events = require('../../events'); +var pagination = require('../../pagination'); var eventsController = {}; -eventsController.get = function(req, res, next) { - events.getEvents(0, 19, function(err, events) { +eventsController.get = function (req, res, next) { + + var page = parseInt(req.query.page, 10) || 1; + var itemsPerPage = 20; + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + + async.parallel({ + eventCount: function (next) { + db.sortedSetCard('events:time', next); + }, + events: function (next) { + events.getEvents(start, stop, next); + } + }, function (err, results) { if (err) { return next(err); } + var pageCount = Math.max(1, Math.ceil(results.eventCount / itemsPerPage)); + res.render('admin/advanced/events', { - events: events, + events: results.events, + pagination: pagination.create(page, pageCount), next: 20 }); }); diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js index cb2f830365..1b31a95ff4 100644 --- a/src/controllers/admin/flags.js +++ b/src/controllers/admin/flags.js @@ -1,38 +1,103 @@ "use strict"; var async = require('async'); +var validator = require('validator'); + var posts = require('../../posts'); +var user = require('../../user'); +var categories = require('../../categories'); +var analytics = require('../../analytics'); +var pagination = require('../../pagination'); var flagsController = {}; -flagsController.get = function(req, res, next) { - var sortBy = req.query.sortBy || 'count'; - var byUsername = req.query.byUsername || ''; - var start = 0; - var stop = 19; +var itemsPerPage = 20; - async.waterfall([ - function (next) { - if (byUsername) { - posts.getUserFlags(byUsername, sortBy, req.uid, start, stop, next); - } else { - var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; - posts.getFlags(set, req.uid, start, stop, next); - } - } - ], function (err, posts) { +flagsController.get = function (req, res, next) { + var byUsername = req.query.byUsername || ''; + var cid = req.query.cid || 0; + var sortBy = req.query.sortBy || 'count'; + var page = parseInt(req.query.page, 10) || 1; + + async.parallel({ + categories: function (next) { + categories.buildForSelect(req.uid, next); + }, + flagData: function (next) { + getFlagData(req, res, next); + }, + analytics: function (next) { + analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next); + }, + assignees: async.apply(user.getAdminsandGlobalModsandModerators) + }, function (err, results) { if (err) { return next(err); } + + // Minimise data set for assignees so tjs does less work + results.assignees = results.assignees.map(function (userObj) { + return { + uid: userObj.uid, + username: userObj.username + }; + }); + + // If res.locals.cids is populated, then slim down the categories list + if (res.locals.cids) { + results.categories = results.categories.filter(function (category) { + return res.locals.cids.indexOf(String(category.cid)) !== -1; + }); + } + + var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage)); + + results.categories.forEach(function (category) { + category.selected = parseInt(category.cid, 10) === parseInt(cid, 10); + }); + var data = { - posts: posts, - next: stop + 1, - byUsername: byUsername, + posts: results.flagData.posts, + assignees: results.assignees, + analytics: results.analytics, + categories: results.categories, + byUsername: validator.escape(String(byUsername)), + sortByCount: sortBy === 'count', + sortByTime: sortBy === 'time', + pagination: pagination.create(page, pageCount, req.query), title: '[[pages:flagged-posts]]' }; res.render('admin/manage/flags', data); }); }; +function getFlagData(req, res, callback) { + var sortBy = req.query.sortBy || 'count'; + var byUsername = req.query.byUsername || ''; + var cid = req.query.cid || res.locals.cids || 0; + var page = parseInt(req.query.page, 10) || 1; + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + + var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged']; + + async.waterfall([ + function (next) { + if (byUsername) { + user.getUidByUsername(byUsername, next); + } else { + process.nextTick(next, null, 0); + } + }, + function (uid, next) { + if (uid) { + sets.push('uid:' + uid + ':flag:pids'); + } + + posts.getFlags(sets, cid, req.uid, start, stop, next); + } + ], callback); +} + module.exports = flagsController; diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js index 03b3514327..19aeb4f2f4 100644 --- a/src/controllers/admin/groups.js +++ b/src/controllers/admin/groups.js @@ -12,17 +12,17 @@ var async = require('async'), var groupsController = {}; -groupsController.list = function(req, res, next) { +groupsController.list = function (req, res, next) { var page = parseInt(req.query.page, 10) || 1; var groupsPerPage = 20; var pageCount = 0; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('groups:createtime', 0, -1, next); }, - function(groupNames, next) { - groupNames = groupNames.filter(function(name) { + function (groupNames, next) { + groupNames = groupNames.filter(function (name) { return name.indexOf(':privileges:') === -1 && name !== 'registered-users'; }); pageCount = Math.ceil(groupNames.length / groupsPerPage); @@ -33,10 +33,10 @@ groupsController.list = function(req, res, next) { groupNames = groupNames.slice(start, stop + 1); groups.getGroupsData(groupNames, next); }, - function(groupData, next) { + function (groupData, next) { next(null, {groups: groupData, pagination: pagination.create(page, pageCount)}); } - ], function(err, data) { + ], function (err, data) { if (err) { return next(err); } @@ -49,19 +49,19 @@ groupsController.list = function(req, res, next) { }); }; -groupsController.get = function(req, res, callback) { +groupsController.get = function (req, res, callback) { var groupName = req.params.name; async.waterfall([ - function(next){ + function (next){ groups.exists(groupName, next); }, - function(exists, next) { + function (exists, next) { if (!exists) { return callback(); } groups.get(groupName, {uid: req.uid, truncateUserList: true, userListCount: 20}, next); } - ], function(err, group) { + ], function (err, group) { if (err) { return callback(err); } diff --git a/src/controllers/admin/homepage.js b/src/controllers/admin/homepage.js index f503dd8865..9fdf3a2371 100644 --- a/src/controllers/admin/homepage.js +++ b/src/controllers/admin/homepage.js @@ -10,19 +10,19 @@ var plugins = require('../../plugins'); var homePageController = {}; -homePageController.get = function(req, res, next) { +homePageController.get = function (req, res, next) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange('categories:cid', 0, -1, next); }, - function(cids, next) { + function (cids, next) { privileges.categories.filterCids('find', cids, 0, next); }, - function(cids, next) { + function (cids, next) { categories.getCategoriesFields(cids, ['name', 'slug'], next); }, - function(categoryData, next) { - categoryData = categoryData.map(function(category) { + function (categoryData, next) { + categoryData = categoryData.map(function (category) { return { route: 'category/' + category.slug, name: 'Category: ' + category.name @@ -30,7 +30,7 @@ homePageController.get = function(req, res, next) { }); next(null, categoryData); } - ], function(err, categoryData) { + ], function (err, categoryData) { if (err || !categoryData) { categoryData = []; } @@ -48,7 +48,11 @@ homePageController.get = function(req, res, next) { route: 'popular', name: 'Popular' } - ].concat(categoryData)}, function(err, data) { + ].concat(categoryData)}, function (err, data) { + if (err) { + return next(err); + } + data.routes.push({ route: '', name: 'Custom' diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index 2459ad7140..6c60d6a048 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -13,23 +13,23 @@ var infoController = {}; var info = {}; -infoController.get = function(req, res, next) { +infoController.get = function (req, res, next) { info = {}; pubsub.publish('sync:node:info:start'); - setTimeout(function() { + setTimeout(function () { var data = []; - Object.keys(info).forEach(function(key) { + Object.keys(info).forEach(function (key) { data.push(info[key]); }); - data.sort(function(a, b) { + data.sort(function (a, b) { return (a.os.hostname < b.os.hostname) ? -1 : (a.os.hostname > b.os.hostname) ? 1 : 0; }); res.render('admin/development/info', {info: data, infoJSON: JSON.stringify(data, null, 4), host: os.hostname(), port: nconf.get('port')}); - }, 300); + }, 500); }; -pubsub.on('sync:node:info:start', function() { - getNodeInfo(function(err, data) { +pubsub.on('sync:node:info:start', function () { + getNodeInfo(function (err, data) { if (err) { return winston.error(err); } @@ -37,7 +37,7 @@ pubsub.on('sync:node:info:start', function() { }); }); -pubsub.on('sync:node:info:end', function(data) { +pubsub.on('sync:node:info:end', function (data) { info[data.id] = data.data; }); @@ -57,39 +57,38 @@ function getNodeInfo(callback) { platform: os.platform(), arch: os.arch(), release: os.release(), - load: os.loadavg().map(function(load){ return load.toFixed(2); }).join(', ') + load: os.loadavg().map(function (load){ return load.toFixed(2); }).join(', ') } }; async.parallel({ - pubsub: function(next) { - pubsub.publish('sync:stats:start'); - next(); + stats: function (next) { + rooms.getLocalStats(next); }, - gitInfo: function(next) { + gitInfo: function (next) { getGitInfo(next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } data.git = results.gitInfo; - data.stats = rooms.stats[data.os.hostname + ':' + data.process.port]; + data.stats = results.stats; callback(null, data); }); } function getGitInfo(callback) { function get(cmd, callback) { - exec(cmd, function(err, stdout) { + exec(cmd, function (err, stdout) { callback(err, stdout ? stdout.replace(/\n$/, '') : ''); }); } async.parallel({ - hash: function(next) { + hash: function (next) { get('git rev-parse HEAD', next); }, - branch: function(next) { + branch: function (next) { get('git rev-parse --abbrev-ref HEAD', next); } }, callback); diff --git a/src/controllers/admin/languages.js b/src/controllers/admin/languages.js index 85c6d60484..292cd2a3b4 100644 --- a/src/controllers/admin/languages.js +++ b/src/controllers/admin/languages.js @@ -6,13 +6,13 @@ var meta = require('../../meta'); var languagesController = {}; -languagesController.get = function(req, res, next) { - languages.list(function(err, languages) { +languagesController.get = function (req, res, next) { + languages.list(function (err, languages) { if (err) { return next(err); } - languages.forEach(function(language) { + languages.forEach(function (language) { language.selected = language.code === (meta.config.defaultLang || 'en_GB'); }); diff --git a/src/controllers/admin/logger.js b/src/controllers/admin/logger.js index 45c9f246c9..7ae327a858 100644 --- a/src/controllers/admin/logger.js +++ b/src/controllers/admin/logger.js @@ -2,7 +2,7 @@ var loggerController = {}; -loggerController.get = function(req, res) { +loggerController.get = function (req, res) { res.render('admin/development/logger', {}); }; diff --git a/src/controllers/admin/logs.js b/src/controllers/admin/logs.js index f3ae601dd4..6723d3795f 100644 --- a/src/controllers/admin/logs.js +++ b/src/controllers/admin/logs.js @@ -6,8 +6,8 @@ var meta = require('../../meta'); var logsController = {}; -logsController.get = function(req, res, next) { - meta.logs.get(function(err, logs) { +logsController.get = function (req, res, next) { + meta.logs.get(function (err, logs) { if (err) { return next(err); } diff --git a/src/controllers/admin/navigation.js b/src/controllers/admin/navigation.js index 463c525eff..423f21721c 100644 --- a/src/controllers/admin/navigation.js +++ b/src/controllers/admin/navigation.js @@ -2,14 +2,14 @@ var navigationController = {}; -navigationController.get = function(req, res, next) { - require('../../navigation/admin').getAdmin(function(err, data) { +navigationController.get = function (req, res, next) { + require('../../navigation/admin').getAdmin(function (err, data) { if (err) { return next(err); } - data.enabled.forEach(function(enabled, index) { + data.enabled.forEach(function (enabled, index) { enabled.index = index; enabled.selected = index === 0; }); diff --git a/src/controllers/admin/plugins.js b/src/controllers/admin/plugins.js index a4733e4c51..f1a72720ac 100644 --- a/src/controllers/admin/plugins.js +++ b/src/controllers/admin/plugins.js @@ -5,10 +5,10 @@ var plugins = require('../../plugins'); var pluginsController = {}; -pluginsController.get = function(req, res, next) { +pluginsController.get = function (req, res, next) { async.parallel({ - compatible: function(next) { - plugins.list(function(err, plugins) { + compatible: function (next) { + plugins.list(function (err, plugins) { if (err || !Array.isArray(plugins)) { plugins = []; } @@ -16,8 +16,8 @@ pluginsController.get = function(req, res, next) { next(null, plugins); }); }, - all: function(next) { - plugins.list(false, function(err, plugins) { + all: function (next) { + plugins.list(false, function (err, plugins) { if (err || !Array.isArray(plugins)) { plugins = []; } @@ -25,22 +25,28 @@ pluginsController.get = function(req, res, next) { next(null, plugins); }); } - }, function(err, payload) { + }, function (err, payload) { if (err) { return next(err); } - var compatiblePkgNames = payload.compatible.map(function(pkgData) { + var compatiblePkgNames = payload.compatible.map(function (pkgData) { return pkgData.name; }); res.render('admin/extend/plugins' , { - installed: payload.compatible.filter(function(plugin) { + installed: payload.compatible.filter(function (plugin) { return plugin.installed; }), - download: payload.compatible.filter(function(plugin) { + upgradeCount: payload.compatible.reduce(function (count, current) { + if (current.installed && current.outdated) { + ++count; + } + return count; + }, 0), + download: payload.compatible.filter(function (plugin) { return !plugin.installed; }), - incompatible: payload.all.filter(function(plugin) { + incompatible: payload.all.filter(function (plugin) { return compatiblePkgNames.indexOf(plugin.name) === -1; }) }); diff --git a/src/controllers/admin/postCache.js b/src/controllers/admin/postCache.js deleted file mode 100644 index bbfd222586..0000000000 --- a/src/controllers/admin/postCache.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -var postCacheController = {}; - -postCacheController.get = function(req, res, next) { - var cache = require('../../posts/cache'); - var avgPostSize = 0; - var percentFull = 0; - if (cache.itemCount > 0) { - avgPostSize = parseInt((cache.length / cache.itemCount), 10); - percentFull = ((cache.length / cache.max) * 100).toFixed(2); - } - - res.render('admin/advanced/post-cache', { - cache: { - length: cache.length, - max: cache.max, - itemCount: cache.itemCount, - percentFull: percentFull, - avgPostSize: avgPostSize - } - }); -}; - - -module.exports = postCacheController; \ No newline at end of file diff --git a/src/controllers/admin/rewards.js b/src/controllers/admin/rewards.js index 063abb6807..8ff05c75b3 100644 --- a/src/controllers/admin/rewards.js +++ b/src/controllers/admin/rewards.js @@ -2,8 +2,8 @@ var rewardsController = {}; -rewardsController.get = function(req, res, next) { - require('../../rewards/admin').get(function(err, data) { +rewardsController.get = function (req, res, next) { + require('../../rewards/admin').get(function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index bf0975058a..a6afb80cfc 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -4,7 +4,7 @@ var settingsController = {}; var async = require('async'), meta = require('../../meta'); -settingsController.get = function(req, res, next) { +settingsController.get = function (req, res, next) { var term = req.params.term ? req.params.term : 'general'; switch (req.params.term) { @@ -19,29 +19,41 @@ settingsController.get = function(req, res, next) { function renderEmail(req, res, next) { - var fs = require('fs'), - path = require('path'), - utils = require('../../../public/src/utils'); + var fs = require('fs'); + var path = require('path'); + var utils = require('../../../public/src/utils'); var emailsPath = path.join(__dirname, '../../../public/templates/emails'); - utils.walk(emailsPath, function(err, emails) { - async.map(emails, function(email, next) { + utils.walk(emailsPath, function (err, emails) { + if (err) { + return next(err); + } + + async.map(emails, function (email, next) { var path = email.replace(emailsPath, '').substr(1).replace('.tpl', ''); - fs.readFile(email, function(err, original) { + fs.readFile(email, function (err, original) { + if (err) { + return next(err); + } + var text = meta.config['email:custom:' + path] ? meta.config['email:custom:' + path] : original.toString(); - next(err, { + next(null, { path: path, fullpath: email, text: text, original: original.toString() }); }); - }, function(err, emails) { + }, function (err, emails) { + if (err) { + return next(err); + } + res.render('admin/settings/email', { emails: emails, - sendable: emails.filter(function(email) { + sendable: emails.filter(function (email) { return email.path.indexOf('_plaintext') === -1 && email.path.indexOf('partials') === -1; }) }); diff --git a/src/controllers/admin/social.js b/src/controllers/admin/social.js index d8f87af060..11c7982701 100644 --- a/src/controllers/admin/social.js +++ b/src/controllers/admin/social.js @@ -5,8 +5,8 @@ var social = require('../../social'); var socialController = {}; -socialController.get = function(req, res, next) { - social.getPostSharing(function(err, posts) { +socialController.get = function (req, res, next) { + social.getPostSharing(function (err, posts) { if (err) { return next(err); } diff --git a/src/controllers/admin/sounds.js b/src/controllers/admin/sounds.js index 6e7ebf3f19..801a2067ac 100644 --- a/src/controllers/admin/sounds.js +++ b/src/controllers/admin/sounds.js @@ -4,9 +4,13 @@ var meta = require('../../meta'); var soundsController = {}; -soundsController.get = function(req, res, next) { - meta.sounds.getFiles(function(err, sounds) { - sounds = Object.keys(sounds).map(function(name) { +soundsController.get = function (req, res, next) { + meta.sounds.getFiles(function (err, sounds) { + if (err) { + return next(err); + } + + sounds = Object.keys(sounds).map(function (name) { return { name: name }; diff --git a/src/controllers/admin/tags.js b/src/controllers/admin/tags.js index 22e3b32d67..a645e2ef11 100644 --- a/src/controllers/admin/tags.js +++ b/src/controllers/admin/tags.js @@ -4,8 +4,8 @@ var topics = require('../../topics'); var tagsController = {}; -tagsController.get = function(req, res, next) { - topics.getTags(0, 199, function(err, tags) { +tagsController.get = function (req, res, next) { + topics.getTags(0, 199, function (err, tags) { if (err) { return next(err); } diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js index e5ef8a9343..4f6f3e1f3b 100644 --- a/src/controllers/admin/themes.js +++ b/src/controllers/admin/themes.js @@ -5,9 +5,9 @@ var file = require('../../file'); var themesController = {}; -themesController.get = function(req, res, next) { +themesController.get = function (req, res, next) { var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme); - file.exists(themeDir, function(exists) { + file.exists(themeDir, function (exists) { if (!exists) { return next(); } diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index aea79e3934..d1664cb8f1 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -1,26 +1,26 @@ "use strict"; -var fs = require('fs'), - path = require('path'), - async = require('async'), - nconf = require('nconf'), - winston = require('winston'), - file = require('../../file'), - image = require('../../image'), - plugins = require('../../plugins'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var nconf = require('nconf'); +var winston = require('winston'); +var file = require('../../file'); +var image = require('../../image'); +var plugins = require('../../plugins'); +var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; var uploadsController = {}; -uploadsController.uploadCategoryPicture = function(req, res, next) { +uploadsController.uploadCategoryPicture = function (req, res, next) { var uploadedFile = req.files.files[0]; - var allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/svg+xml'], - params = null; + var params = null; try { params = JSON.parse(req.body.params); } catch (e) { - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -28,19 +28,19 @@ uploadsController.uploadCategoryPicture = function(req, res, next) { return next(e); } - if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { + if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { var filename = 'category-' + params.cid + path.extname(uploadedFile.name); uploadImage(filename, 'category', uploadedFile, req, res, next); } }; -uploadsController.uploadFavicon = function(req, res, next) { +uploadsController.uploadFavicon = function (req, res, next) { var uploadedFile = req.files.files[0]; var allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { - file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function(err, image) { - fs.unlink(uploadedFile.path, function(err) { + file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function (err, image) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -54,15 +54,19 @@ uploadsController.uploadFavicon = function(req, res, next) { } }; -uploadsController.uploadTouchIcon = function(req, res, next) { +uploadsController.uploadTouchIcon = function (req, res, next) { var uploadedFile = req.files.files[0], allowedTypes = ['image/png'], sizes = [36, 48, 72, 96, 144, 192]; if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { - file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function(err, imageObj) { + file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function (err, imageObj) { + if (err) { + return next(err); + } + // Resize the image into squares for use as touch icons at various DPIs - async.each(sizes, function(size, next) { + async.each(sizes, function (size, next) { async.series([ async.apply(file.saveFileToLocal, 'touchicon-' + size + '.png', 'system', uploadedFile.path), async.apply(image.resizeImage, { @@ -72,8 +76,8 @@ uploadsController.uploadTouchIcon = function(req, res, next) { height: size }) ], next); - }, function(err) { - fs.unlink(uploadedFile.path, function(err) { + }, function (err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -89,14 +93,14 @@ uploadsController.uploadTouchIcon = function(req, res, next) { } }; -uploadsController.uploadLogo = function(req, res, next) { +uploadsController.uploadLogo = function (req, res, next) { upload('site-logo', req, res, next); }; -uploadsController.uploadSound = function(req, res, next) { +uploadsController.uploadSound = function (req, res, next) { var uploadedFile = req.files.files[0]; - file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function(err) { + file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function (err) { if (err) { return next(err); } @@ -110,7 +114,7 @@ uploadsController.uploadSound = function(req, res, next) { fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file'); } - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { return next(err); } @@ -120,14 +124,18 @@ uploadsController.uploadSound = function(req, res, next) { }); }; -uploadsController.uploadDefaultAvatar = function(req, res, next) { +uploadsController.uploadDefaultAvatar = function (req, res, next) { upload('avatar-default', req, res, next); }; +uploadsController.uploadOgImage = function (req, res, next) { + upload('og:image', req, res, next); +}; + function upload(name, req, res, next) { var uploadedFile = req.files.files[0]; - var allowedTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif']; - if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { + + if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { var filename = name + path.extname(uploadedFile.name); uploadImage(filename, 'system', uploadedFile, req, res, next); } @@ -135,7 +143,7 @@ function upload(name, req, res, next) { function validateUpload(req, res, next, uploadedFile, allowedTypes) { if (allowedTypes.indexOf(uploadedFile.type) === -1) { - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -150,7 +158,7 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) { function uploadImage(filename, folder, uploadedFile, req, res, next) { function done(err, image) { - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 7e5bd530d6..426d8b1c19 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -1,116 +1,92 @@ "use strict"; var async = require('async'); +var validator = require('validator'); + var user = require('../../user'); var meta = require('../../meta'); var db = require('../../database'); var pagination = require('../../pagination'); - +var events = require('../../events'); +var plugins = require('../../plugins'); var usersController = {}; -usersController.search = function(req, res, next) { +var userFields = ['uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', + 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed']; + +usersController.search = function (req, res, next) { res.render('admin/manage/users', { search_display: '', users: [] }); }; -usersController.sortByJoinDate = function(req, res, next) { - getUsers('users:joindate', 'latest', req, res, next); +usersController.sortByJoinDate = function (req, res, next) { + getUsers('users:joindate', 'latest', undefined, undefined, req, res, next); }; -usersController.notValidated = function(req, res, next) { - getUsers('users:notvalidated', 'notvalidated', req, res, next); +usersController.notValidated = function (req, res, next) { + getUsers('users:notvalidated', 'notvalidated', undefined, undefined, req, res, next); }; -usersController.noPosts = function(req, res, next) { - getUsersByScore('users:postcount', 'noposts', 0, 0, req, res, next); +usersController.noPosts = function (req, res, next) { + getUsers('users:postcount', 'noposts', '-inf', 0, req, res, next); }; -usersController.inactive = function(req, res, next) { +usersController.flagged = function (req, res, next) { + getUsers('users:flags', 'mostflags', 1, '+inf', req, res, next); +}; + +usersController.inactive = function (req, res, next) { var timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3); var cutoff = Date.now() - timeRange; - getUsersByScore('users:online', 'inactive', '-inf', cutoff, req, res, next); + getUsers('users:online', 'inactive', '-inf', cutoff, req, res, next); }; -function getUsersByScore(set, section, min, max, req, res, callback) { - var page = parseInt(req.query.page, 10) || 1; - var resultsPerPage = 25; - var start = Math.max(0, page - 1) * resultsPerPage; - var count = 0; - - async.waterfall([ - function (next) { - async.parallel({ - count: function (next) { - db.sortedSetCount(set, min, max, next); - }, - uids: function (next) { - db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next); - } - }, next); - }, - function (results, next) { - count = results.count; - user.getUsers(results.uids, req.uid, next); - } - ], function(err, users) { - if (err) { - return callback(err); - } - users = users.filter(function(user) { - return user && parseInt(user.uid, 10); - }); - var data = { - users: users, - page: page, - pageCount: Math.ceil(count / resultsPerPage) - }; - data[section] = true; - render(req, res, data); - }); -} - -usersController.banned = function(req, res, next) { - getUsers('users:banned', 'banned', req, res, next); +usersController.banned = function (req, res, next) { + getUsers('users:banned', 'banned', undefined, undefined, req, res, next); }; -usersController.registrationQueue = function(req, res, next) { +usersController.registrationQueue = function (req, res, next) { var page = parseInt(req.query.page, 10) || 1; var itemsPerPage = 20; var start = (page - 1) * 20; var stop = start + itemsPerPage - 1; var invitations; + async.parallel({ - registrationQueueCount: function(next) { + registrationQueueCount: function (next) { db.sortedSetCard('registration:queue', next); }, - users: function(next) { + users: function (next) { user.getRegistrationQueue(start, stop, next); }, - invites: function(next) { + customHeaders: function (next) { + plugins.fireHook('filter:admin.registrationQueue.customHeaders', {headers: []}, next); + }, + invites: function (next) { async.waterfall([ - function(next) { + function (next) { user.getAllInvites(next); }, - function(_invitations, next) { + function (_invitations, next) { invitations = _invitations; - async.map(invitations, function(invites, next) { + async.map(invitations, function (invites, next) { user.getUserField(invites.uid, 'username', next); }, next); }, - function(usernames, next) { - invitations.forEach(function(invites, index) { + function (usernames, next) { + invitations.forEach(function (invites, index) { invites.username = usernames[index]; }); - async.map(invitations, function(invites, next) { + async.map(invitations, function (invites, next) { async.map(invites.invitations, user.getUsernameByEmail, next); }, next); }, - function(usernames, next) { - invitations.forEach(function(invites, index) { - invites.invitations = invites.invitations.map(function(email, i) { + function (usernames, next) { + invitations.forEach(function (invites, index) { + invites.invitations = invites.invitations.map(function (email, i) { return { email: email, username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i] @@ -121,35 +97,53 @@ usersController.registrationQueue = function(req, res, next) { } ], next); } - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } var pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage)); data.pagination = pagination.create(page, pageCount); + data.customHeaders = data.customHeaders.headers; res.render('admin/manage/registration', data); }); }; -function getUsers(set, section, req, res, next) { +function getUsers(set, section, min, max, req, res, next) { var page = parseInt(req.query.page, 10) || 1; - var resultsPerPage = 25; + var resultsPerPage = 50; var start = Math.max(0, page - 1) * resultsPerPage; var stop = start + resultsPerPage - 1; + var byScore = min !== undefined && max !== undefined; async.parallel({ - count: function(next) { - db.sortedSetCard(set, next); + count: function (next) { + if (byScore) { + db.sortedSetCount(set, min, max, next); + } else { + db.sortedSetCard(set, next); + } }, - users: function(next) { - user.getUsersFromSet(set, req.uid, start, stop, next); + users: function (next) { + async.waterfall([ + function (next) { + if (byScore) { + db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next); + } else { + user.getUidsFromSet(set, start, stop, next); + } + }, + function (uids, next) { + user.getUsersWithFields(uids, userFields, req.uid, next); + } + ], next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } - results.users = results.users.filter(function(user) { + results.users = results.users.filter(function (user) { + user.email = validator.escape(String(user.email || '')); return user && parseInt(user.uid, 10); }); var data = { @@ -166,11 +160,23 @@ function render(req, res, data) { data.search_display = 'hidden'; data.pagination = pagination.create(data.page, data.pageCount, req.query); data.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1; + + var registrationType = meta.config.registrationType; + + data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.adminInviteOnly = registrationType === 'admin-invite-only'; + res.render('admin/manage/users', data); } -usersController.getCSV = function(req, res, next) { - user.getUsersCSV(function(err, data) { +usersController.getCSV = function (req, res, next) { + events.log({ + type: 'getUsersCSV', + uid: req.user.uid, + ip: req.ip + }); + + user.getUsersCSV(function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/admin/widgets.js b/src/controllers/admin/widgets.js index 8dd93fbead..c2d0d1e667 100644 --- a/src/controllers/admin/widgets.js +++ b/src/controllers/admin/widgets.js @@ -2,8 +2,8 @@ var widgetsController = {}; -widgetsController.get = function(req, res, next) { - require('../../widgets/admin').get(function(err, data) { +widgetsController.get = function (req, res, next) { + require('../../widgets/admin').get(function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/api.js b/src/controllers/api.js index 77ae7131ad..36261a23c1 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -12,16 +12,17 @@ var categories = require('../categories'); var privileges = require('../privileges'); var plugins = require('../plugins'); var widgets = require('../widgets'); +var accountHelpers = require('../controllers/accounts/helpers'); var apiController = {}; -apiController.getConfig = function(req, res, next) { +apiController.getConfig = function (req, res, next) { var config = {}; config.environment = process.env.NODE_ENV; config.relative_path = nconf.get('relative_path'); config.version = nconf.get('version'); - config.siteTitle = validator.escape(meta.config.title || meta.config.browserTitle || 'NodeBB'); - config.browserTitle = validator.escape(meta.config.browserTitle || meta.config.title || 'NodeBB'); + config.siteTitle = validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')); + config.browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')); config.titleLayout = (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'); config.showSiteTitle = parseInt(meta.config.showSiteTitle, 10) === 1; config.minimumTitleLength = meta.config.minimumTitleLength; @@ -51,7 +52,7 @@ apiController.getConfig = function(req, res, next) { config['theme:id'] = meta.config['theme:id']; config['theme:src'] = meta.config['theme:src']; config.defaultLang = meta.config.defaultLang || 'en_GB'; - config.userLang = req.query.lang || config.defaultLang; + config.userLang = req.query.lang ? validator.escape(String(req.query.lang)) : config.defaultLang; config.loggedIn = !!req.user; config['cache-buster'] = meta.config['cache-buster'] || ''; config.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1; @@ -66,27 +67,22 @@ apiController.getConfig = function(req, res, next) { if (!req.user) { return next(null, config); } - user.getSettings(req.uid, function(err, settings) { - if (err) { - return next(err); - } - config.usePagination = settings.usePagination; - config.topicsPerPage = settings.topicsPerPage; - config.postsPerPage = settings.postsPerPage; - config.notificationSounds = settings.notificationSounds; - config.userLang = req.query.lang || settings.userLang || config.defaultLang; - config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; - config.topicPostSort = settings.topicPostSort || config.topicPostSort; - config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; - config.topicSearchEnabled = settings.topicSearchEnabled || false; - config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin; - next(null, config); - }); + user.getSettings(req.uid, next); }, - function (config, next) { + function (settings, next) { + config.usePagination = settings.usePagination; + config.topicsPerPage = settings.topicsPerPage; + config.postsPerPage = settings.postsPerPage; + config.userLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.userLang || config.defaultLang; + config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; + config.topicPostSort = settings.topicPostSort || config.topicPostSort; + config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; + config.topicSearchEnabled = settings.topicSearchEnabled || false; + config.delayImageLoading = settings.delayImageLoading !== undefined ? settings.delayImageLoading : true; + config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin; plugins.fireHook('filter:config.get', config, next); } - ], function(err, config) { + ], function (err, config) { if (err) { return next(err); } @@ -100,7 +96,7 @@ apiController.getConfig = function(req, res, next) { }; -apiController.renderWidgets = function(req, res, next) { +apiController.renderWidgets = function (req, res, next) { var areas = { template: req.query.template, locations: req.query.locations, @@ -116,10 +112,11 @@ apiController.renderWidgets = function(req, res, next) { template: areas.template, url: areas.url, locations: areas.locations, + isMobile: req.query.isMobile === 'true' }, req, res, - function(err, widgets) { + function (err, widgets) { if (err) { return next(err); } @@ -127,105 +124,158 @@ apiController.renderWidgets = function(req, res, next) { }); }; -apiController.getObject = function(req, res, next) { - apiController.getObjectByType(req.uid, req.params.type, req.params.id, function(err, results) { - if (err) { - return next(err); +apiController.getPostData = function (pid, uid, callback) { + async.parallel({ + privileges: function (next) { + privileges.posts.get([pid], uid, next); + }, + post: function (next) { + posts.getPostData(pid, next); + } + }, function (err, results) { + if (err || !results.post) { + return callback(err); } - res.json(results); + var post = results.post; + var privileges = results.privileges[0]; + + if (!privileges.read || !privileges['topics:read']) { + return callback(); + } + + post.ip = privileges.isAdminOrMod ? post.ip : undefined; + var selfPost = uid && uid === parseInt(post.uid, 10); + if (post.deleted && !(privileges.isAdminOrMod || selfPost)) { + post.content = '[[topic:post_is_deleted]]'; + } + callback(null, post); }); }; -apiController.getObjectByType = function(uid, type, id, callback) { - var methods = { - post: { - canRead: privileges.posts.can, - data: posts.getPostData +apiController.getTopicData = function (tid, uid, callback) { + async.parallel({ + privileges: function (next) { + privileges.topics.get(tid, uid, next); }, - topic: { - canRead: privileges.topics.can, - data: topics.getTopicData - }, - category: { - canRead: privileges.categories.can, - data: categories.getCategoryData + topic: function (next) { + topics.getTopicData(tid, next); } + }, function (err, results) { + if (err || !results.topic) { + return callback(err); + } + + if (!results.privileges.read || !results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { + return callback(); + } + callback(null, results.topic); + }); +}; + +apiController.getCategoryData = function (cid, uid, callback) { + async.parallel({ + privileges: function (next) { + privileges.categories.get(cid, uid, next); + }, + category: function (next) { + categories.getCategoryData(cid, next); + } + }, function (err, results) { + if (err || !results.category) { + return callback(err); + } + + if (!results.privileges.read) { + return callback(); + } + callback(null, results.category); + }); +}; + + +apiController.getObject = function (req, res, next) { + var methods = { + post: apiController.getPostData, + topic: apiController.getTopicData, + category: apiController.getCategoryData }; - - if (!methods[type]) { - return callback(); + var method = methods[req.params.type]; + if (!method) { + return next(); } + method(req.params.id, req.uid, function (err, result) { + if (err || !result) { + return next(err); + } + res.json(result); + }); +}; + +apiController.getCurrentUser = function (req, res, next) { + if (!req.uid) { + return res.status(401).json('not-authorized'); + } async.waterfall([ function (next) { - methods[type].canRead('read', id, uid, next); + user.getUserField(req.uid, 'userslug', next); }, - function (canRead, next) { - if (!canRead) { - return next(new Error('[[error:no-privileges]]')); + function (userslug, next) { + accountHelpers.getUserDataByUserSlug(userslug, req.uid, next); + } + ], function (err, userData) { + if (err) { + return next(err); + } + res.json(userData); + }); +}; + +apiController.getUserByUID = function (req, res, next) { + byType('uid', req, res, next); +}; + +apiController.getUserByUsername = function (req, res, next) { + byType('username', req, res, next); +}; + +apiController.getUserByEmail = function (req, res, next) { + byType('email', req, res, next); +}; + +function byType(type, req, res, next) { + apiController.getUserDataByField(req.uid, type, req.params[type], function (err, data) { + if (err || !data) { + return next(err); + } + res.json(data); + }); +} + +apiController.getUserDataByField = function (callerUid, field, fieldValue, callback) { + async.waterfall([ + function (next) { + if (field === 'uid') { + next(null, fieldValue); + } else if (field === 'username') { + user.getUidByUsername(fieldValue, next); + } else if (field === 'email') { + user.getUidByEmail(fieldValue, next); + } else { + next(); } - methods[type].data(id, next); - } - ], callback); -}; - -apiController.getUserByUID = function(req, res, next) { - var uid = req.params.uid ? req.params.uid : 0; - - apiController.getUserDataByUID(req.uid, uid, function(err, data) { - if (err) { - return next(err); - } - res.json(data); - }); -}; - -apiController.getUserByUsername = function(req, res, next) { - var username = req.params.username ? req.params.username : 0; - - apiController.getUserDataByUsername(req.uid, username, function(err, data) { - if (err) { - return next(err); - } - res.json(data); - }); -}; - -apiController.getUserByEmail = function(req, res, next) { - var email = req.params.email ? req.params.email : 0; - - apiController.getUserDataByEmail(req.uid, email, function(err, data) { - if (err) { - return next(err); - } - res.json(data); - }); -}; - -apiController.getUserDataByUsername = function(callerUid, username, callback) { - async.waterfall([ - function(next) { - user.getUidByUsername(username, next); }, - function(uid, next) { + function (uid, next) { + if (!uid) { + return next(); + } apiController.getUserDataByUID(callerUid, uid, next); } ], callback); }; -apiController.getUserDataByEmail = function(callerUid, email, callback) { - async.waterfall([ - function(next) { - user.getUidByEmail(email, next); - }, - function(uid, next) { - apiController.getUserDataByUID(callerUid, uid, next); - } - ], callback); -}; - -apiController.getUserDataByUID = function(callerUid, uid, callback) { +apiController.getUserDataByUID = function (callerUid, uid, callback) { if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) { return callback(new Error('[[error:no-privileges]]')); } @@ -237,7 +287,7 @@ apiController.getUserDataByUID = function(callerUid, uid, callback) { async.parallel({ userData: async.apply(user.getUserData, uid), settings: async.apply(user.getSettings, uid) - }, function(err, results) { + }, function (err, results) { if (err || !results.userData) { return callback(err || new Error('[[error:no-user]]')); } @@ -249,8 +299,8 @@ apiController.getUserDataByUID = function(callerUid, uid, callback) { }); }; -apiController.getModerators = function(req, res, next) { - categories.getModerators(req.params.cid, function(err, moderators) { +apiController.getModerators = function (req, res, next) { + categories.getModerators(req.params.cid, function (err, moderators) { if (err) { return next(err); } @@ -259,7 +309,7 @@ apiController.getModerators = function(req, res, next) { }; -apiController.getRecentPosts = function(req, res, next) { +apiController.getRecentPosts = function (req, res, next) { posts.getRecentPosts(req.uid, 0, 19, req.params.term, function (err, data) { if (err) { return next(err); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 8db2c9e580..0111f62573 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -6,6 +6,7 @@ var passport = require('passport'); var nconf = require('nconf'); var validator = require('validator'); var _ = require('underscore'); +var url = require('url'); var db = require('../database'); var meta = require('../meta'); @@ -16,7 +17,7 @@ var Password = require('../password'); var authenticationController = {}; -authenticationController.register = function(req, res, next) { +authenticationController.register = function (req, res, next) { var registrationType = meta.config.registrationType || 'normal'; if (registrationType === 'disabled') { @@ -32,14 +33,14 @@ authenticationController.register = function(req, res, next) { } async.waterfall([ - function(next) { - if (registrationType === 'invite-only') { + function (next) { + if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') { user.verifyInvitation(userData, next); } else { next(); } }, - function(next) { + function (next) { if (!userData.email) { return next(new Error('[[error:invalid-email]]')); } @@ -54,18 +55,28 @@ authenticationController.register = function(req, res, next) { user.isPasswordValid(userData.password, next); }, - function(next) { + function (next) { res.locals.processLogin = true; // set it to false in plugin if you wish to just register only plugins.fireHook('filter:register.check', {req: req, res: res, userData: userData}, next); }, - function(data, next) { - if (registrationType === 'normal' || registrationType === 'invite-only') { + function (data, next) { + if (registrationType === 'normal' || registrationType === 'invite-only' || registrationType === 'admin-invite-only') { registerAndLoginUser(req, res, userData, next); } else if (registrationType === 'admin-approval') { addToApprovalQueue(req, userData, next); + } else if (registrationType === 'admin-approval-ip') { + db.sortedSetCard('ip:' + req.ip + ':uid', function (err, count) { + if (err) { + next(err); + } else if (count) { + addToApprovalQueue(req, userData, next); + } else { + registerAndLoginUser(req, res, userData, next); + } + }); } } - ], function(err, data) { + ], function (err, data) { if (err) { return res.status(400).send(err.message); } @@ -81,10 +92,31 @@ authenticationController.register = function(req, res, next) { function registerAndLoginUser(req, res, userData, callback) { var uid; async.waterfall([ - function(next) { + function (next) { + plugins.fireHook('filter:register.interstitial', { + userData: userData, + interstitials: [] + }, function (err, data) { + if (err) { + return next(err); + } + + // If interstitials are found, save registration attempt into session and abort + var deferRegistration = data.interstitials.length; + + if (!deferRegistration) { + return next(); + } else { + userData.register = true; + req.session.registration = userData; + return res.json({ referrer: nconf.get('relative_path') + '/register/complete' }); + } + }); + }, + function (next) { user.create(userData, next); }, - function(_uid, next) { + function (_uid, next) { uid = _uid; if (res.locals.processLogin) { authenticationController.doLogin(req, uid, next); @@ -92,7 +124,7 @@ function registerAndLoginUser(req, res, userData, callback) { next(); } }, - function(next) { + function (next) { user.deleteInvitationKey(userData.email); plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next); } @@ -101,20 +133,76 @@ function registerAndLoginUser(req, res, userData, callback) { function addToApprovalQueue(req, userData, callback) { async.waterfall([ - function(next) { + function (next) { userData.ip = req.ip; user.addToApprovalQueue(userData, next); }, - function(next) { + function (next) { next(null, {message: '[[register:registration-added-to-queue]]'}); } ], callback); } -authenticationController.login = function(req, res, next) { +authenticationController.registerComplete = function (req, res, next) { + // For the interstitials that respond, execute the callback with the form body + plugins.fireHook('filter:register.interstitial', { + userData: req.session.registration, + interstitials: [] + }, function (err, data) { + if (err) { + return next(err); + } + + var callbacks = data.interstitials.reduce(function (memo, cur) { + if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') { + memo.push(async.apply(cur.callback, req.session.registration, req.body)); + } + + return memo; + }, []); + + var done = function () { + delete req.session.registration; + + if (req.session.returnTo) { + res.redirect(req.session.returnTo); + } else { + res.redirect(nconf.get('relative_path') + '/'); + } + }; + + async.parallel(callbacks, function (err) { + if (err) { + req.flash('error', err.message); + return res.redirect(nconf.get('relative_path') + '/register/complete'); + } + + if (req.session.registration.register === true) { + res.locals.processLogin = true; + registerAndLoginUser(req, res, req.session.registration, done); + } else { + // Clear registration data in session + done(); + } + }); + }); +}; + +authenticationController.registerAbort = function (req, res) { + // End the session and redirect to home + req.session.destroy(function () { + res.redirect(nconf.get('relative_path') + '/'); + }); +}; + +authenticationController.login = function (req, res, next) { // Handle returnTo data if (req.body.hasOwnProperty('returnTo') && !req.session.returnTo) { - req.session.returnTo = req.body.returnTo; + // As req.body is data obtained via userland, it is untrusted, restrict to internal links only + var parsed = url.parse(req.body.returnTo); + var isInternal = utils.isInternalURI(url.parse(req.body.returnTo), nconf.get('url_parsed'), nconf.get('relative_path')); + + req.session.returnTo = isInternal ? req.body.returnTo : nconf.get('url'); } if (plugins.hasListeners('action:auth.overrideLogin')) { @@ -124,7 +212,7 @@ authenticationController.login = function(req, res, next) { var loginWith = meta.config.allowLoginWith || 'username-email'; if (req.body.username && utils.isEmailValid(req.body.username) && loginWith.indexOf('email') !== -1) { - user.getUsernameByEmail(req.body.username, function(err, username) { + user.getUsernameByEmail(req.body.username, function (err, username) { if (err) { return next(err); } @@ -139,7 +227,7 @@ authenticationController.login = function(req, res, next) { }; function continueLogin(req, res, next) { - passport.authenticate('local', function(err, userData, info) { + passport.authenticate('local', function (err, userData, info) { if (err) { return res.status(403).send(err.message); } @@ -167,11 +255,15 @@ function continueLogin(req, res, next) { if (passwordExpiry && passwordExpiry < Date.now()) { winston.verbose('[auth] Triggering password reset for uid ' + userData.uid + ' due to password policy'); req.session.passwordExpired = true; - user.reset.generate(userData.uid, function(err, code) { + user.reset.generate(userData.uid, function (err, code) { + if (err) { + return res.status(403).send(err.message); + } + res.status(200).send(nconf.get('relative_path') + '/reset/' + code); }); } else { - authenticationController.doLogin(req, userData.uid, function(err) { + authenticationController.doLogin(req, userData.uid, function (err) { if (err) { return res.status(403).send(err.message); } @@ -189,12 +281,12 @@ function continueLogin(req, res, next) { })(req, res, next); } -authenticationController.doLogin = function(req, uid, callback) { +authenticationController.doLogin = function (req, uid, callback) { if (!uid) { return callback(); } - req.login({uid: uid}, function(err) { + req.login({uid: uid}, function (err) { if (err) { return callback(err); } @@ -203,11 +295,13 @@ authenticationController.doLogin = function(req, uid, callback) { }); }; -authenticationController.onSuccessfulLogin = function(req, uid, callback) { - callback = callback || function() {}; +authenticationController.onSuccessfulLogin = function (req, uid, callback) { + callback = callback || function () {}; var uuid = utils.generateUUID(); req.session.meta = {}; + delete req.session.forceLogin; + // Associate IP used during login with user account user.logIP(uid, req.ip); req.session.meta.ip = req.ip; @@ -228,8 +322,11 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) { }, function (next) { db.setObjectField('uid:' + uid + 'sessionUUID:sessionId', uuid, req.sessionID, next); + }, + function (next) { + user.updateLastOnlineTime(uid, next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -238,7 +335,7 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) { }); }; -authenticationController.localLogin = function(req, username, password, next) { +authenticationController.localLogin = function (req, username, password, next) { if (!username) { return next(new Error('[[error:invalid-username]]')); } @@ -262,11 +359,14 @@ authenticationController.localLogin = function(req, username, password, next) { }, function (next) { async.parallel({ - userData: function(next) { - db.getObjectFields('user:' + uid, ['password', 'banned', 'passwordExpiry'], next); + userData: function (next) { + db.getObjectFields('user:' + uid, ['password', 'passwordExpiry'], next); }, - isAdmin: function(next) { + isAdmin: function (next) { user.isAdministrator(uid, next); + }, + banned: function (next) { + user.isBanned(uid, next); } }, next); }, @@ -278,13 +378,22 @@ authenticationController.localLogin = function(req, username, password, next) { if (!result.isAdmin && parseInt(meta.config.allowLocalLogin, 10) === 0) { return next(new Error('[[error:local-login-disabled]]')); } - if (!userData || !userData.password) { return next(new Error('[[error:invalid-user-data]]')); } - if (userData.banned && parseInt(userData.banned, 10) === 1) { - return next(new Error('[[error:user-banned]]')); + if (result.banned) { + // Retrieve ban reason and show error + return user.getLatestBanInfo(uid, function (err, banInfo) { + if (err) { + next(err); + } else if (banInfo.reason) { + next(new Error('[[error:user-banned-reason, ' + banInfo.reason + ']]')); + } else { + next(new Error('[[error:user-banned]]')); + } + }); } + Password.compare(password, userData.password, next); }, function (passwordMatch, next) { @@ -297,18 +406,19 @@ authenticationController.localLogin = function(req, username, password, next) { ], next); }; -authenticationController.logout = function(req, res, next) { +authenticationController.logout = function (req, res, next) { if (req.user && parseInt(req.user.uid, 10) > 0 && req.sessionID) { var uid = parseInt(req.user.uid, 10); - user.auth.revokeSession(req.sessionID, uid, function(err) { + user.auth.revokeSession(req.sessionID, uid, function (err) { if (err) { return next(err); } req.logout(); + req.session.destroy(); - // action:user.loggedOut deprecated in > v0.9.3 - plugins.fireHook('action:user.loggedOut', {req: req, res: res, uid: uid}); - plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function() { + user.setUserField(uid, 'lastonline', Date.now() - 300000); + + plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function () { res.status(200).send(''); }); }); diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 7ed087c704..9a18e7f1dd 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -1,25 +1,22 @@ "use strict"; - var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); var categories = require('../categories'); var meta = require('../meta'); -var plugins = require('../plugins'); - var helpers = require('./helpers'); var categoriesController = {}; -categoriesController.list = function(req, res, next) { +categoriesController.list = function (req, res, next) { res.locals.metaTags = [{ name: "title", - content: validator.escape(meta.config.title || 'NodeBB') + content: validator.escape(String(meta.config.title || 'NodeBB')) }, { name: "description", - content: validator.escape(meta.config.description || '') + content: validator.escape(String(meta.config.description || '')) }, { property: 'og:title', content: '[[pages:categories]]' @@ -28,14 +25,14 @@ categoriesController.list = function(req, res, next) { content: 'website' }]; - if (meta.config['brand:logo']) { - var brandLogo = meta.config['brand:logo']; - if (!brandLogo.startsWith('http')) { - brandLogo = nconf.get('url') + brandLogo; + var ogImage = meta.config['og:image'] || meta.config['brand:logo'] || ''; + if (ogImage) { + if (!ogImage.startsWith('http')) { + ogImage = nconf.get('url') + ogImage; } res.locals.metaTags.push({ property: 'og:image', - content: brandLogo + content: ogImage }); } @@ -51,33 +48,32 @@ categoriesController.list = function(req, res, next) { categories.flattenCategories(allCategories, categoryData); categories.getRecentTopicReplies(allCategories, req.uid, next); - }, - function (next) { - var data = { - title: '[[pages:categories]]', - categories: categoryData - }; - - if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) { - data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]); - } - - data.categories.forEach(function(category) { - if (category && Array.isArray(category.posts) && category.posts.length) { - category.teaser = { - url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index, - timestampISO: category.posts[0].timestampISO - }; - } - }); - - plugins.fireHook('filter:categories.build', {req: req, res: res, templateData: data}, next); } - ], function(err, data) { + ], function (err) { if (err) { return next(err); } - res.render('categories', data.templateData); + + var data = { + title: '[[pages:categories]]', + categories: categoryData + }; + + if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) { + data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]); + } + + data.categories.forEach(function (category) { + if (category && Array.isArray(category.posts) && category.posts.length) { + category.teaser = { + url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index, + timestampISO: category.posts[0].timestampISO, + pid: category.posts[0].pid + }; + } + }); + + res.render('categories', data); }); }; diff --git a/src/controllers/category.js b/src/controllers/category.js index f80ad122a7..2a660ee63c 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -9,18 +9,18 @@ var privileges = require('../privileges'); var user = require('../user'); var categories = require('../categories'); var meta = require('../meta'); -var plugins = require('../plugins'); var pagination = require('../pagination'); var helpers = require('./helpers'); var utils = require('../../public/src/utils'); var categoryController = {}; -categoryController.get = function(req, res, callback) { +categoryController.get = function (req, res, callback) { var cid = req.params.category_id; var currentPage = parseInt(req.query.page, 10) || 1; var pageCount = 1; var userPrivileges; + var settings; if ((req.params.topic_index && !utils.isNumber(req.params.topic_index)) || !utils.isNumber(cid)) { return callback(); @@ -29,13 +29,13 @@ categoryController.get = function(req, res, callback) { async.waterfall([ function (next) { async.parallel({ - categoryData: function(next) { + categoryData: function (next) { categories.getCategoryFields(cid, ['slug', 'disabled', 'topic_count'], next); }, - privileges: function(next) { + privileges: function (next) { privileges.categories.get(cid, req.uid, next); }, - userSettings: function(next) { + userSettings: function (next) { user.getSettings(req.uid, next); } }, next); @@ -55,7 +55,7 @@ categoryController.get = function(req, res, callback) { return helpers.redirect(res, '/category/' + results.categoryData.slug); } - var settings = results.userSettings; + settings = results.userSettings; var topicIndex = utils.isNumber(req.params.topic_index) ? parseInt(req.params.topic_index, 10) - 1 : 0; var topicCount = parseInt(results.categoryData.topic_count, 10); pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); @@ -76,20 +76,21 @@ categoryController.get = function(req, res, callback) { topicIndex = 0; } - var set = 'cid:' + cid + ':tids', - reverse = false; - - if (settings.categoryTopicSort === 'newest_to_oldest') { + var set = 'cid:' + cid + ':tids'; + var reverse = false; + // `sort` qs has priority over user setting + var sort = req.query.sort || settings.categoryTopicSort; + if (sort === 'newest_to_oldest') { reverse = true; - } else if (settings.categoryTopicSort === 'most_posts') { + } else if (sort === 'most_posts') { reverse = true; set = 'cid:' + cid + ':tids:posts'; } - var start = (currentPage - 1) * settings.topicsPerPage + topicIndex, - stop = start + settings.topicsPerPage - 1; + var start = (currentPage - 1) * settings.topicsPerPage + topicIndex; + var stop = start + settings.topicsPerPage - 1; - next(null, { + var payload = { cid: cid, set: set, reverse: reverse, @@ -97,19 +98,24 @@ categoryController.get = function(req, res, callback) { stop: stop, uid: req.uid, settings: settings - }); - }, - function (payload, next) { - user.getUidByUserslug(req.query.author, function(err, uid) { - payload.targetUid = uid; - if (uid) { - payload.set = 'cid:' + cid + ':uid:' + uid + ':tids'; + }; + + async.waterfall([ + function (next) { + user.getUidByUserslug(req.query.author, next); + }, + function (uid, next) { + payload.targetUid = uid; + if (uid) { + payload.set = 'cid:' + cid + ':uid:' + uid + ':tids'; + } + + if (req.query.tag) { + payload.set = [payload.set, 'tag:' + req.query.tag + ':topics']; + } + categories.getCategoryById(payload, next); } - next(err, payload); - }); - }, - function (payload, next) { - categories.getCategoryById(payload, next); + ], next); }, function (categoryData, next) { @@ -126,7 +132,7 @@ categoryController.get = function(req, res, callback) { url: nconf.get('relative_path') + '/category/' + categoryData.slug } ]; - helpers.buildCategoryBreadcrumbs(categoryData.parentCid, function(err, crumbs) { + helpers.buildCategoryBreadcrumbs(categoryData.parentCid, function (err, crumbs) { if (err) { return next(err); } @@ -140,68 +146,71 @@ categoryController.get = function(req, res, callback) { } var allCategories = []; categories.flattenCategories(allCategories, categoryData.children); - categories.getRecentTopicReplies(allCategories, req.uid, function(err) { + categories.getRecentTopicReplies(allCategories, req.uid, function (err) { next(err, categoryData); }); - }, - function (categoryData, next) { - categoryData.privileges = userPrivileges; - categoryData.showSelect = categoryData.privileges.editable; - - res.locals.metaTags = [ - { - name: 'title', - content: categoryData.name - }, - { - property: 'og:title', - content: categoryData.name - }, - { - name: 'description', - content: categoryData.description - }, - { - property: "og:type", - content: 'website' - } - ]; - - if (categoryData.backgroundImage) { - res.locals.metaTags.push({ - name: 'og:image', - content: categoryData.backgroundImage - }); - } - - res.locals.linkTags = [ - { - rel: 'alternate', - type: 'application/rss+xml', - href: nconf.get('url') + '/category/' + cid + '.rss' - }, - { - rel: 'up', - href: nconf.get('url') - } - ]; - - categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; - categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss'; - categoryData.title = categoryData.name; - categoryData.pagination = pagination.create(currentPage, pageCount); - categoryData.pagination.rel.forEach(function(rel) { - rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href; - res.locals.linkTags.push(rel); - }); - - plugins.fireHook('filter:category.build', {req: req, res: res, templateData: categoryData}, next); } - ], function (err, data) { + ], function (err, categoryData) { if (err) { return callback(err); } - res.render('category', data.templateData); + + categoryData.privileges = userPrivileges; + categoryData.showSelect = categoryData.privileges.editable; + + res.locals.metaTags = [ + { + name: 'title', + content: categoryData.name + }, + { + property: 'og:title', + content: categoryData.name + }, + { + name: 'description', + content: categoryData.description + }, + { + property: "og:type", + content: 'website' + } + ]; + + if (categoryData.backgroundImage) { + res.locals.metaTags.push({ + name: 'og:image', + content: categoryData.backgroundImage + }); + } + + res.locals.linkTags = [ + { + rel: 'alternate', + type: 'application/rss+xml', + href: nconf.get('url') + '/category/' + cid + '.rss' + }, + { + rel: 'up', + href: nconf.get('url') + } + ]; + + if (parseInt(req.uid, 10)) { + categories.markAsRead([cid], req.uid); + } + + categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss'; + categoryData.title = categoryData.name; + pageCount = Math.max(1, Math.ceil(categoryData.topic_count / settings.topicsPerPage)); + categoryData.pagination = pagination.create(currentPage, pageCount, req.query); + categoryData.pagination.rel.forEach(function (rel) { + rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href; + res.locals.linkTags.push(rel); + }); + + res.render('category', categoryData); }); }; diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js index 3275c7929e..7e4fd1ffec 100644 --- a/src/controllers/globalmods.js +++ b/src/controllers/globalmods.js @@ -1,23 +1,12 @@ "use strict"; var user = require('../user'); -var adminFlagsController = require('./admin/flags'); var adminBlacklistController = require('./admin/blacklist'); var globalModsController = {}; -globalModsController.flagged = function(req, res, next) { - user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) { - if (err || !isAdminOrGlobalMod) { - return next(err); - } - - adminFlagsController.get(req, res, next); - }); -}; - -globalModsController.ipBlacklist = function(req, res, next) { - user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) { +globalModsController.ipBlacklist = function (req, res, next) { + user.isAdminOrGlobalMod(req.uid, function (err, isAdminOrGlobalMod) { if (err || !isAdminOrGlobalMod) { return next(err); } diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 837ba3b1f6..53b7064f59 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -1,19 +1,20 @@ "use strict"; -var async = require('async'), - nconf = require('nconf'), - validator = require('validator'), - meta = require('../meta'), - groups = require('../groups'), - user = require('../user'), - helpers = require('./helpers'), - plugins = require('../plugins'), - groupsController = {}; +var async = require('async'); +var nconf = require('nconf'); +var validator = require('validator'); -groupsController.list = function(req, res, next) { +var meta = require('../meta'); +var groups = require('../groups'); +var user = require('../user'); +var helpers = require('./helpers'); + +var groupsController = {}; + +groupsController.list = function (req, res, next) { var sort = req.query.sort || 'alpha'; - groupsController.getGroupsFromSet(req.uid, sort, 0, 14, function(err, data) { + groupsController.getGroupsFromSet(req.uid, sort, 0, 14, function (err, data) { if (err) { return next(err); } @@ -23,7 +24,7 @@ groupsController.list = function(req, res, next) { }); }; -groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) { +groupsController.getGroupsFromSet = function (uid, sort, start, stop, callback) { var set = 'groups:visible:name'; if (sort === 'count') { set = 'groups:visible:memberCount'; @@ -31,7 +32,7 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) { set = 'groups:visible:createtime'; } - groups.getGroupsFromSet(set, uid, start, stop, function(err, groups) { + groups.getGroupsFromSet(set, uid, start, stop, function (err, groups) { if (err) { return callback(err); } @@ -44,25 +45,33 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) { }); }; -groupsController.details = function(req, res, callback) { +groupsController.details = function (req, res, callback) { + var groupName; async.waterfall([ - async.apply(groups.exists, res.locals.groupName), - function (exists, next) { - if (!exists) { + function (next) { + groups.getGroupNameByGroupSlug(req.params.slug, next); + }, + function (_groupName, next) { + groupName = _groupName; + if (!groupName) { return callback(); } - - groups.isHidden(res.locals.groupName, next); + async.parallel({ + exists: async.apply(groups.exists, groupName), + hidden: async.apply(groups.isHidden, groupName) + }, next); }, - function (hidden, next) { - if (!hidden) { + function (results, next) { + if (!results.exists) { + return callback(); + } + if (!results.hidden) { return next(); } - async.parallel({ - isMember: async.apply(groups.isMember, req.uid, res.locals.groupName), - isInvited: async.apply(groups.isInvited, req.uid, res.locals.groupName) - }, function(err, checks) { + isMember: async.apply(groups.isMember, req.uid, groupName), + isInvited: async.apply(groups.isInvited, req.uid, groupName) + }, function (err, checks) { if (err || checks.isMember || checks.isInvited) { return next(err); } @@ -71,55 +80,59 @@ groupsController.details = function(req, res, callback) { }, function (next) { async.parallel({ - group: function(next) { - groups.get(res.locals.groupName, { + group: function (next) { + groups.get(groupName, { uid: req.uid, truncateUserList: true, userListCount: 20 }, next); }, - posts: function(next) { - groups.getLatestMemberPosts(res.locals.groupName, 10, req.uid, next); + posts: function (next) { + groups.getLatestMemberPosts(groupName, 10, req.uid, next); }, - isAdmin: async.apply(user.isAdministrator, req.uid) + isAdmin:function (next) { + user.isAdministrator(req.uid, next); + }, + isGlobalMod: function (next) { + user.isGlobalModerator(req.uid, next); + } }, next); - }, - function (results, next) { - if (!results.group) { - return callback(); - } - results.title = '[[pages:group, ' + results.group.displayName + ']]'; - results.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:groups]]', url: '/groups' }, {text: results.group.displayName}]); - results.allowPrivateGroups = parseInt(meta.config.allowPrivateGroups, 10) === 1; - plugins.fireHook('filter:group.build', {req: req, res: res, templateData: results}, next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } - res.render('groups/details', results.templateData); + if (!results.group) { + return callback(); + } + results.group.isOwner = results.group.isOwner || results.isAdmin || (results.isGlobalMod && !results.group.system); + results.title = '[[pages:group, ' + results.group.displayName + ']]'; + results.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:groups]]', url: '/groups' }, {text: results.group.displayName}]); + results.allowPrivateGroups = parseInt(meta.config.allowPrivateGroups, 10) === 1; + + res.render('groups/details', results); }); }; -groupsController.members = function(req, res, next) { +groupsController.members = function (req, res, next) { var groupName; async.waterfall([ - function(next) { + function (next) { groups.getGroupNameByGroupSlug(req.params.slug, next); }, - function(_groupName, next) { + function (_groupName, next) { groupName = _groupName; user.getUsersFromSet('group:' + groupName + ':members', req.uid, 0, 49, next); }, - ], function(err, users) { + ], function (err, users) { if (err || !groupName) { return next(err); } var breadcrumbs = helpers.buildBreadcrumbs([ {text: '[[pages:groups]]', url: '/groups' }, - {text: validator.escape(groupName), url: '/groups/' + req.params.slug}, + {text: validator.escape(String(groupName)), url: '/groups/' + req.params.slug}, {text: '[[groups:details.members]]'} ]); @@ -132,13 +145,13 @@ groupsController.members = function(req, res, next) { }); }; -groupsController.uploadCover = function(req, res, next) { +groupsController.uploadCover = function (req, res, next) { var params = JSON.parse(req.body.params); groups.updateCover(req.uid, { file: req.files.files[0].path, groupName: params.groupName - }, function(err, image) { + }, function (err, image) { if (err) { return next(err); } diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 058a1849b1..13a91aaa4c 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -1,43 +1,52 @@ 'use strict'; -var nconf = require('nconf'), - async = require('async'), - validator = require('validator'), +var nconf = require('nconf'); +var async = require('async'); +var validator = require('validator'); +var winston = require('winston'); - translator = require('../../public/src/modules/translator'), - categories = require('../categories'), - plugins = require('../plugins'), - meta = require('../meta'); +var categories = require('../categories'); +var plugins = require('../plugins'); +var meta = require('../meta'); var helpers = {}; -helpers.notAllowed = function(req, res, error) { - if (req.uid) { - if (res.locals.isAPI) { - res.status(403).json({ - path: req.path.replace(/^\/api/, ''), - loggedIn: !!req.uid, error: error, - title: '[[global:403.title]]' - }); - } else { - res.status(403).render('403', { - path: req.path, - loggedIn: !!req.uid, error: error, - title: '[[global:403.title]]' - }); +helpers.notAllowed = function (req, res, error) { + plugins.fireHook('filter:helpers.notAllowed', { + req: req, + res: res, + error: error + }, function (err, data) { + if (err) { + return winston.error(err); } - } else { - if (res.locals.isAPI) { - req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, ''); - res.status(401).json('not-authorized'); + if (req.uid) { + if (res.locals.isAPI) { + res.status(403).json({ + path: req.path.replace(/^\/api/, ''), + loggedIn: !!req.uid, error: error, + title: '[[global:403.title]]' + }); + } else { + res.status(403).render('403', { + path: req.path, + loggedIn: !!req.uid, error: error, + title: '[[global:403.title]]' + }); + } } else { - req.session.returnTo = nconf.get('relative_path') + req.url; - res.redirect(nconf.get('relative_path') + '/login'); + if (res.locals.isAPI) { + req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, ''); + res.status(401).json('not-authorized'); + } else { + req.session.returnTo = nconf.get('relative_path') + req.url; + res.redirect(nconf.get('relative_path') + '/login'); + } } - } + }); }; -helpers.redirect = function(res, url) { +helpers.redirect = function (res, url) { if (res.locals.isAPI) { res.status(308).json(url); } else { @@ -45,20 +54,20 @@ helpers.redirect = function(res, url) { } }; -helpers.buildCategoryBreadcrumbs = function(cid, callback) { +helpers.buildCategoryBreadcrumbs = function (cid, callback) { var breadcrumbs = []; - async.whilst(function() { + async.whilst(function () { return parseInt(cid, 10); - }, function(next) { - categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled'], function(err, data) { + }, function (next) { + categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled'], function (err, data) { if (err) { return next(err); } if (!parseInt(data.disabled, 10)) { breadcrumbs.unshift({ - text: validator.escape(data.name), + text: validator.escape(String(data.name)), url: nconf.get('relative_path') + '/category/' + data.slug }); } @@ -66,11 +75,18 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) { cid = data.parentCid; next(); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } + if (!meta.config.homePageRoute && meta.config.homePageCustom) { + breadcrumbs.unshift({ + text: '[[global:header.categories]]', + url: nconf.get('relative_path') + '/categories' + }); + } + breadcrumbs.unshift({ text: '[[global:home]]', url: nconf.get('relative_path') + '/' @@ -80,7 +96,7 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) { }); }; -helpers.buildBreadcrumbs = function(crumbs) { +helpers.buildBreadcrumbs = function (crumbs) { var breadcrumbs = [ { text: '[[global:home]]', @@ -88,7 +104,7 @@ helpers.buildBreadcrumbs = function(crumbs) { } ]; - crumbs.forEach(function(crumb) { + crumbs.forEach(function (crumb) { if (crumb) { if (crumb.url) { crumb.url = nconf.get('relative_path') + crumb.url; @@ -100,14 +116,14 @@ helpers.buildBreadcrumbs = function(crumbs) { return breadcrumbs; }; -helpers.buildTitle = function(pageTitle) { +helpers.buildTitle = function (pageTitle) { var titleLayout = meta.config.titleLayout || '{pageTitle} | {browserTitle}'; - var browserTitle = validator.escape(meta.config.browserTitle || meta.config.title || 'NodeBB'); + var browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')); pageTitle = pageTitle || ''; - var title = titleLayout.replace('{pageTitle}', function() { + var title = titleLayout.replace('{pageTitle}', function () { return pageTitle; - }).replace('{browserTitle}', function() { + }).replace('{browserTitle}', function () { return browserTitle; }); return title; diff --git a/src/controllers/index.js b/src/controllers/index.js index fa35523c2b..6f73886986 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -3,15 +3,16 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); +var winston = require('winston'); var meta = require('../meta'); var user = require('../user'); var plugins = require('../plugins'); -var sitemap = require('../sitemap'); var helpers = require('./helpers'); var Controllers = { topics: require('./topics'), + posts: require('./posts'), categories: require('./categories'), category: require('./category'), unread: require('./unread'), @@ -25,14 +26,16 @@ var Controllers = { authentication: require('./authentication'), api: require('./api'), admin: require('./admin'), - globalMods: require('./globalmods') + globalMods: require('./globalmods'), + mods: require('./mods'), + sitemap: require('./sitemap') }; -Controllers.home = function(req, res, next) { - var route = meta.config.homePageRoute || meta.config.homePageCustom || 'categories'; +Controllers.home = function (req, res, next) { + var route = meta.config.homePageRoute || (meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; - user.getSettings(req.uid, function(err, settings) { + user.getSettings(req.uid, function (err, settings) { if (err) { return next(err); } @@ -48,6 +51,8 @@ Controllers.home = function(req, res, next) { if (route === 'categories' || route === '/') { Controllers.categories.list(req, res, next); + } else if (route === 'unread') { + Controllers.unread.get(req, res, next); } else if (route === 'recent') { Controllers.recent.get(req, res, next); } else if (route === 'popular') { @@ -67,9 +72,9 @@ Controllers.home = function(req, res, next) { }); }; -Controllers.reset = function(req, res, next) { +Controllers.reset = function (req, res, next) { if (req.params.code) { - user.reset.validate(req.params.code, function(err, valid) { + user.reset.validate(req.params.code, function (err, valid) { if (err) { return next(err); } @@ -94,76 +99,144 @@ Controllers.reset = function(req, res, next) { }; -Controllers.login = function(req, res, next) { - var data = {}, - loginStrategies = require('../routes/authentication').getLoginStrategies(), - registrationType = meta.config.registrationType || 'normal'; +Controllers.login = function (req, res, next) { + var data = {}; + var loginStrategies = require('../routes/authentication').getLoginStrategies(); + var registrationType = meta.config.registrationType || 'normal'; + + var allowLoginWith = (meta.config.allowLoginWith || 'username-email'); + + var errorText; + if (req.query.error === 'csrf-invalid') { + errorText = '[[error:csrf-invalid]]'; + } else if (req.query.error) { + errorText = validator.escape(String(req.query.error)); + } data.alternate_logins = loginStrategies.length > 0; data.authentication = loginStrategies; data.allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) === 1 || parseInt(req.query.local, 10) === 1; - data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval'; - data.allowLoginWith = '[[login:' + (meta.config.allowLoginWith || 'username-email') + ']]'; + data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval' || registrationType === 'admin-approval-ip'; + data.allowLoginWith = '[[login:' + allowLoginWith + ']]'; data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:login]]'}]); - data.error = req.flash('error')[0]; + data.error = req.flash('error')[0] || errorText; data.title = '[[pages:login]]'; - res.render('login', data); + if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) { + if (res.locals.isAPI) { + return helpers.redirect(res, { + external: data.authentication[0].url + }); + } else { + return res.redirect(data.authentication[0].url); + } + } + if (req.uid) { + user.getUserFields(req.uid, ['username', 'email'], function (err, user) { + if (err) { + return next(err); + } + data.username = allowLoginWith === 'email' ? user.email : user.username; + data.alternate_logins = []; + res.render('login', data); + }); + } else { + res.render('login', data); + } + }; -Controllers.register = function(req, res, next) { +Controllers.register = function (req, res, next) { var registrationType = meta.config.registrationType || 'normal'; if (registrationType === 'disabled') { return next(); } + var errorText; + if (req.query.error === 'csrf-invalid') { + errorText = '[[error:csrf-invalid]]'; + } + async.waterfall([ - function(next) { + function (next) { if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') { user.verifyInvitation(req.query, next); } else { next(); } }, - function(next) { + function (next) { plugins.fireHook('filter:parse.post', {postData: {content: meta.config.termsOfUse || ''}}, next); - }, - function(tos, next) { - var loginStrategies = require('../routes/authentication').getLoginStrategies(); - var data = { - 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12', - 'alternate_logins': !!loginStrategies.length - }; - - data.authentication = loginStrategies; - - data.minimumUsernameLength = parseInt(meta.config.minimumUsernameLength, 10); - data.maximumUsernameLength = parseInt(meta.config.maximumUsernameLength, 10); - data.minimumPasswordLength = parseInt(meta.config.minimumPasswordLength, 10); - data.termsOfUse = tos.postData.content; - data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]); - data.regFormEntry = []; - data.error = req.flash('error')[0]; - data.title = '[[pages:register]]'; - - plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, next); } - ], function(err, data) { + ], function (err, termsOfUse) { if (err) { return next(err); } - res.render('register', data.templateData); + var loginStrategies = require('../routes/authentication').getLoginStrategies(); + var data = { + 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12', + 'alternate_logins': !!loginStrategies.length + }; + + data.authentication = loginStrategies; + + data.minimumUsernameLength = parseInt(meta.config.minimumUsernameLength, 10); + data.maximumUsernameLength = parseInt(meta.config.maximumUsernameLength, 10); + data.minimumPasswordLength = parseInt(meta.config.minimumPasswordLength, 10); + data.termsOfUse = termsOfUse.postData.content; + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]); + data.regFormEntry = []; + data.error = req.flash('error')[0] || errorText; + data.title = '[[pages:register]]'; + + res.render('register', data); }); }; -Controllers.compose = function(req, res, next) { +Controllers.registerInterstitial = function (req, res, next) { + if (!req.session.hasOwnProperty('registration')) { + return res.redirect(nconf.get('relative_path') + '/register'); + } + + plugins.fireHook('filter:register.interstitial', { + userData: req.session.registration, + interstitials: [] + }, function (err, data) { + if (err) { + return next(err); + } + + if (!data.interstitials.length) { + return next(); + } + + var renders = data.interstitials.map(function (interstitial) { + return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {}); + }); + var errors = req.flash('error'); + + async.parallel(renders, function (err, sections) { + if (err) { + return next(err); + } + + res.render('registerComplete', { + title: '[[pages:registration-complete]]', + errors: errors, + sections: sections + }); + }); + }); +}; + +Controllers.compose = function (req, res, next) { plugins.fireHook('filter:composer.build', { req: req, res: res, next: next, templateData: {} - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } @@ -179,7 +252,7 @@ Controllers.compose = function(req, res, next) { }); }; -Controllers.confirmEmail = function(req, res, next) { +Controllers.confirmEmail = function (req, res) { user.email.confirm(req.params.code, function (err) { res.render('confirm', { error: err ? err.message : '', @@ -188,61 +261,6 @@ Controllers.confirmEmail = function(req, res, next) { }); }; -Controllers.sitemap = {}; -Controllers.sitemap.render = function(req, res, next) { - sitemap.render(function(err, tplData) { - Controllers.render('sitemap', tplData, function(err, xml) { - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); - }); -}; - -Controllers.sitemap.getPages = function(req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getPages(function(err, xml) { - if (err) { - return next(err); - } - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; - -Controllers.sitemap.getCategories = function(req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getCategories(function(err, xml) { - if (err) { - return next(err); - } - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; - -Controllers.sitemap.getTopicPage = function(req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getTopicPage(parseInt(req.params[0], 10), function(err, xml) { - if (err) { - return next(err); - } else if (!xml) { - return next(); - } - - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; - Controllers.robots = function (req, res) { res.set('Content-Type', 'text/plain'); @@ -255,7 +273,7 @@ Controllers.robots = function (req, res) { } }; -Controllers.manifest = function(req, res) { +Controllers.manifest = function (req, res) { var manifest = { name: meta.config.title || 'NodeBB', start_url: nconf.get('relative_path') + '/', @@ -301,13 +319,13 @@ Controllers.manifest = function(req, res) { res.status(200).json(manifest); }; -Controllers.outgoing = function(req, res, next) { - var url = req.query.url, - data = { - url: validator.escape(url), - title: meta.config.title, - breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}]) - }; +Controllers.outgoing = function (req, res) { + var url = req.query.url || ''; + var data = { + outgoing: validator.escape(String(url)), + title: meta.config.title, + breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}]) + }; if (url) { res.render('outgoing', data); @@ -316,11 +334,115 @@ Controllers.outgoing = function(req, res, next) { } }; -Controllers.termsOfUse = function(req, res, next) { +Controllers.termsOfUse = function (req, res, next) { if (!meta.config.termsOfUse) { return next(); } res.render('tos', {termsOfUse: meta.config.termsOfUse}); }; +Controllers.ping = function (req, res) { + res.status(200).send(req.path === '/sping' ? 'healthy' : '200'); +}; + +Controllers.handle404 = function (req, res) { + var relativePath = nconf.get('relative_path'); + var isLanguage = new RegExp('^' + relativePath + '/language/.*/.*.json'); + var isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js'); + + if (plugins.hasListeners('action:meta.override404')) { + return plugins.fireHook('action:meta.override404', { + req: req, + res: res, + error: {} + }); + } + + if (isClientScript.test(req.url)) { + res.type('text/javascript').status(200).send(''); + } else if (isLanguage.test(req.url)) { + res.status(200).json({}); + } else if (req.path.startsWith(relativePath + '/uploads') || (req.get('accept') && req.get('accept').indexOf('text/html') === -1) || req.path === '/favicon.ico') { + meta.errors.log404(req.path || ''); + res.sendStatus(404); + } else if (req.accepts('html')) { + if (process.env.NODE_ENV === 'development') { + winston.warn('Route requested but not found: ' + req.url); + } + + meta.errors.log404(req.path.replace(/^\/api/, '') || ''); + res.status(404); + + var path = String(req.path || ''); + + if (res.locals.isAPI) { + return res.json({path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]'}); + } + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('404', {path: validator.escape(path), title: '[[global:404.title]]'}); + }); + } else { + res.status(404).type('txt').send('Not found'); + } +}; + +Controllers.handleURIErrors = function (err, req, res, next) { + // Handle cases where malformed URIs are passed in + if (err instanceof URIError) { + var tidMatch = req.path.match(/^\/topic\/(\d+)\//); + var cidMatch = req.path.match(/^\/category\/(\d+)\//); + + if (tidMatch) { + res.redirect(nconf.get('relative_path') + tidMatch[0]); + } else if (cidMatch) { + res.redirect(nconf.get('relative_path') + cidMatch[0]); + } else { + winston.warn('[controller] Bad request: ' + req.path); + if (res.locals.isAPI) { + res.status(400).json({ + error: '[[global:400.title]]' + }); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('400', { error: validator.escape(String(err.message)) }); + }); + } + } + + return; + } else { + next(err); + } +}; + +Controllers.handleErrors = function (err, req, res, next) { + switch (err.code) { + case 'EBADCSRFTOKEN': + winston.error(req.path + '\n', err.message); + return res.sendStatus(403); + case 'blacklisted-ip': + return res.status(403).type('text/plain').send(err.message); + } + + if (parseInt(err.status, 10) === 302 && err.path) { + return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path); + } + + winston.error(req.path + '\n', err.stack); + + res.status(err.status || 500); + + var path = String(req.path || ''); + if (res.locals.isAPI) { + res.json({path: validator.escape(path), error: err.message}); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); + }); + } +}; + module.exports = Controllers; diff --git a/src/controllers/mods.js b/src/controllers/mods.js new file mode 100644 index 0000000000..0079412f87 --- /dev/null +++ b/src/controllers/mods.js @@ -0,0 +1,27 @@ +"use strict"; + +var async = require('async'); + +var user = require('../user'); +var adminFlagsController = require('./admin/flags'); + +var modsController = {}; + +modsController.flagged = function (req, res, next) { + async.parallel({ + isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid), + moderatedCids: async.apply(user.getModeratedCids, req.uid) + }, function (err, results) { + if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) { + return next(err); + } + + if (!results.isAdminOrGlobalMod && results.moderatedCids.length) { + res.locals.cids = results.moderatedCids; + } + + adminFlagsController.get(req, res, next); + }); +}; + +module.exports = modsController; diff --git a/src/controllers/popular.js b/src/controllers/popular.js index 813f3527b8..f38edd594c 100644 --- a/src/controllers/popular.js +++ b/src/controllers/popular.js @@ -1,15 +1,15 @@ 'use strict'; -var nconf = require('nconf'), - topics = require('../topics'), - plugins = require('../plugins'), - meta = require('../meta'), - helpers = require('./helpers'); +var nconf = require('nconf'); +var topics = require('../topics'); +var meta = require('../meta'); +var helpers = require('./helpers'); var popularController = {}; -var anonCache = {}, lastUpdateTime = 0; +var anonCache = {}; +var lastUpdateTime = 0; var terms = { daily: 'day', @@ -17,7 +17,7 @@ var terms = { monthly: 'month' }; -popularController.get = function(req, res, next) { +popularController.get = function (req, res, next) { var term = terms[req.params.term]; @@ -39,7 +39,7 @@ popularController.get = function(req, res, next) { } } - topics.getPopular(term, req.uid, meta.config.topicsPerList, function(err, topics) { + topics.getPopular(term, req.uid, meta.config.topicsPerList, function (err, topics) { if (err) { return next(err); } @@ -48,7 +48,8 @@ popularController.get = function(req, res, next) { topics: topics, 'feeds:disableRSS': parseInt(meta.config['feeds:disableRSS'], 10) === 1, rssFeedUrl: nconf.get('relative_path') + '/popular/' + (req.params.term || 'daily') + '.rss', - title: '[[pages:popular-' + term + ']]' + title: '[[pages:popular-' + term + ']]', + term: term }; if (req.path.startsWith('/api/popular') || req.path.startsWith('/popular')) { @@ -66,12 +67,7 @@ popularController.get = function(req, res, next) { lastUpdateTime = Date.now(); } - plugins.fireHook('filter:popular.build', {req: req, res: res, term: term, templateData: data}, function(err, data) { - if (err) { - return next(err); - } - res.render('popular', data.templateData); - }); + res.render('popular', data); }); }; diff --git a/src/controllers/posts.js b/src/controllers/posts.js new file mode 100644 index 0000000000..dae990e171 --- /dev/null +++ b/src/controllers/posts.js @@ -0,0 +1,24 @@ +"use strict"; + +var posts = require('../posts'); +var helpers = require('./helpers'); + +var postsController = {}; + +postsController.redirectToPost = function (req, res, callback) { + var pid = parseInt(req.params.pid, 10); + if (!pid) { + return callback(); + } + + posts.generatePostPath(pid, req.uid, function (err, path) { + if (err || !path) { + return callback(err); + } + + helpers.redirect(res, path); + }); +}; + + +module.exports = postsController; diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 242d26ac12..72c0f45721 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -1,38 +1,69 @@ 'use strict'; -var nconf = require('nconf'); var async = require('async'); +var nconf = require('nconf'); + +var db = require('../database'); +var privileges = require('../privileges'); +var user = require('../user'); var topics = require('../topics'); var meta = require('../meta'); var helpers = require('./helpers'); -var plugins = require('../plugins'); +var pagination = require('../pagination'); var recentController = {}; -recentController.get = function(req, res, next) { - - var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; +recentController.get = function (req, res, next) { + var page = parseInt(req.query.page, 10) || 1; + var pageCount = 1; + var stop = 0; + var topicCount = 0; + var settings; async.waterfall([ function (next) { - topics.getTopicsFromSet('topics:recent', req.uid, 0, stop, next); + async.parallel({ + settings: function (next) { + user.getSettings(req.uid, next); + }, + tids: function (next) { + db.getSortedSetRevRange('topics:recent', 0, 199, next); + } + }, next); }, - function (data, next) { - data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; - data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss'; - data.title = '[[pages:recent]]'; - if (req.path.startsWith('/api/recent') || req.path.startsWith('/recent')) { - data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]); - } + function (results, next) { + settings = results.settings; + privileges.topics.filterTids('read', results.tids, req.uid, next); + }, + function (tids, next) { + var start = Math.max(0, (page - 1) * settings.topicsPerPage); + stop = start + settings.topicsPerPage - 1; - plugins.fireHook('filter:recent.build', {req: req, res: res, templateData: data}, next); + topicCount = tids.length; + pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); + tids = tids.slice(start, stop + 1); + + topics.getTopicsByTids(tids, req.uid, next); } - ], function(err, data) { + ], function (err, topics) { if (err) { return next(err); } - res.render('recent', data.templateData); + + var data = {}; + data.topics = topics; + data.nextStart = stop + 1; + data.set = 'topics:recent'; + data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss'; + data.title = '[[pages:recent]]'; + data.pagination = pagination.create(page, pageCount); + if (req.path.startsWith('/api/recent') || req.path.startsWith('/recent')) { + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]); + } + + res.render('recent', data); }); }; diff --git a/src/controllers/search.js b/src/controllers/search.js index dee14e7c1f..5967cfc88e 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -1,19 +1,19 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - meta = require('../meta'), - plugins = require('../plugins'), - search = require('../search'), - categories = require('../categories'), - pagination = require('../pagination'), - helpers = require('./helpers'); +var meta = require('../meta'); +var plugins = require('../plugins'); +var search = require('../search'); +var categories = require('../categories'); +var pagination = require('../pagination'); +var helpers = require('./helpers'); var searchController = {}; -searchController.search = function(req, res, next) { +searchController.search = function (req, res, next) { if (!plugins.hasListeners('filter:search.query')) { return next(); } @@ -28,7 +28,7 @@ searchController.search = function(req, res, next) { } var data = { - query: req.params.term, + query: req.query.term, searchIn: req.query.in || 'posts', postedBy: req.query.by, categories: req.query.categories, @@ -45,69 +45,30 @@ searchController.search = function(req, res, next) { }; async.parallel({ - categories: async.apply(buildCategories, req.uid), + categories: async.apply(categories.buildForSelect, req.uid), search: async.apply(search.search, data) - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } + + var categoriesData = [ + {value: 'all', text: '[[unread:all_categories]]'}, + {value: 'watched', text: '[[category:watched-categories]]'} + ].concat(results.categories); + var searchData = results.search; - searchData.categories = results.categories; + searchData.categories = categoriesData; searchData.categoriesCount = results.categories.length; searchData.pagination = pagination.create(page, searchData.pageCount, req.query); searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts'; searchData.showAsTopics = req.query.showAs === 'topics'; searchData.title = '[[global:header.search]]'; searchData.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]); - searchData.expandSearch = !req.params.term; + searchData.expandSearch = !req.query.term; - plugins.fireHook('filter:search.build', {data: data, results: searchData}, function(err, data) { - if (err) { - return next(err); - } - res.render('search', data.results); - }); + res.render('search', searchData); }); }; -function buildCategories(uid, callback) { - categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function(err, categories) { - if (err) { - return callback(err); - } - - var categoriesData = [ - {value: 'all', text: '[[unread:all_categories]]'}, - {value: 'watched', text: '[[category:watched-categories]]'} - ]; - - categories = categories.filter(function(category) { - return category && !category.link && !parseInt(category.parentCid, 10); - }); - - categories.forEach(function(category) { - recursive(category, categoriesData, ''); - }); - callback(null, categoriesData); - }); -} - - -function recursive(category, categoriesData, level) { - if (category.link) { - return; - } - - var bullet = level ? '• ' : ''; - - categoriesData.push({ - value: category.cid, - text: level + bullet + category.name - }); - - category.children.forEach(function(child) { - recursive(child, categoriesData, '    ' + level); - }); -} - module.exports = searchController; diff --git a/src/controllers/sitemap.js b/src/controllers/sitemap.js new file mode 100644 index 0000000000..42b0ae1076 --- /dev/null +++ b/src/controllers/sitemap.js @@ -0,0 +1,68 @@ +'use strict'; + +var sitemap = require('../sitemap'); +var meta = require('../meta'); + +var sitemapController = {}; +sitemapController.render = function (req, res, next) { + sitemap.render(function (err, tplData) { + if (err) { + return next(err); + } + + req.app.render('sitemap', tplData, function (err, xml) { + if (err) { + return next(err); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); + }); +}; + +sitemapController.getPages = function (req, res, next) { + if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { + return next(); + } + + sitemap.getPages(function (err, xml) { + if (err) { + return next(err); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); +}; + +sitemapController.getCategories = function (req, res, next) { + if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { + return next(); + } + + sitemap.getCategories(function (err, xml) { + if (err) { + return next(err); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); +}; + +sitemapController.getTopicPage = function (req, res, next) { + if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { + return next(); + } + + sitemap.getTopicPage(parseInt(req.params[0], 10), function (err, xml) { + if (err) { + return next(err); + } else if (!xml) { + return next(); + } + + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); +}; + +module.exports = sitemapController; \ No newline at end of file diff --git a/src/controllers/tags.js b/src/controllers/tags.js index af8f6058c0..a433694220 100644 --- a/src/controllers/tags.js +++ b/src/controllers/tags.js @@ -5,15 +5,16 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); -var meta = require('../meta'); +var user = require('../user'); var topics = require('../topics'); +var pagination = require('../pagination'); var helpers = require('./helpers'); var tagsController = {}; -tagsController.getTag = function(req, res, next) { - var tag = validator.escape(req.params.tag); - var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; +tagsController.getTag = function (req, res, next) { + var tag = validator.escape(String(req.params.tag)); + var page = parseInt(req.query.page, 10) || 1; var templateData = { topics: [], @@ -21,20 +22,34 @@ tagsController.getTag = function(req, res, next) { breadcrumbs: helpers.buildBreadcrumbs([{text: '[[tags:tags]]', url: '/tags'}, {text: tag}]), title: '[[pages:tag, ' + tag + ']]' }; - + var settings; + var topicCount = 0; async.waterfall([ function (next) { - topics.getTagTids(req.params.tag, 0, stop, next); + user.getSettings(req.uid, next); }, - function (tids, next) { - if (Array.isArray(tids) && !tids.length) { - topics.deleteTag(req.params.tag); + function (_settings, next) { + settings = _settings; + var start = Math.max(0, (page - 1) * settings.topicsPerPage); + var stop = start + settings.topicsPerPage - 1; + templateData.nextStart = stop + 1; + async.parallel({ + topicCount: function (next) { + topics.getTagTopicCount(tag, next); + }, + tids: function (next) { + topics.getTagTids(req.params.tag, start, stop, next); + } + }, next); + }, + function (results, next) { + if (Array.isArray(results.tids) && !results.tids.length) { return res.render('tag', templateData); } - - topics.getTopics(tids, req.uid, next); + topicCount = results.topicCount; + topics.getTopics(results.tids, req.uid, next); } - ], function(err, topics) { + ], function (err, topics) { if (err) { return next(err); } @@ -54,20 +69,20 @@ tagsController.getTag = function(req, res, next) { } ]; templateData.topics = topics; - templateData.nextStart = stop + 1; + + var pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); + templateData.pagination = pagination.create(page, pageCount); res.render('tag', templateData); }); }; -tagsController.getTags = function(req, res, next) { - topics.getTags(0, 99, function(err, tags) { +tagsController.getTags = function (req, res, next) { + topics.getTags(0, 99, function (err, tags) { if (err) { return next(err); } - tags = tags.filter(function(tag) { - return tag && tag.score > 0; - }); + tags = tags.filter(Boolean); var data = { tags: tags, nextStart: 100, diff --git a/src/controllers/topics.js b/src/controllers/topics.js index c3c2847778..c739052937 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -17,9 +17,8 @@ var utils = require('../../public/src/utils'); var topicsController = {}; -topicsController.get = function(req, res, callback) { +topicsController.get = function (req, res, callback) { var tid = req.params.topic_id; - var sort = req.query.sort; var currentPage = parseInt(req.query.page, 10) || 1; var pageCount = 1; var userPrivileges; @@ -32,13 +31,13 @@ topicsController.get = function(req, res, callback) { async.waterfall([ function (next) { async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.topics.get(tid, req.uid, next); }, - settings: function(next) { + settings: function (next) { user.getSettings(req.uid, next); }, - topic: function(next) { + topic: function (next) { topics.getTopicData(tid, next); } }, next); @@ -50,14 +49,17 @@ topicsController.get = function(req, res, callback) { userPrivileges = results.privileges; - if (!userPrivileges.read || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { + if (!userPrivileges.read || !userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { return helpers.notAllowed(req, res); } if (!res.locals.isAPI && (!req.params.slug || results.topic.slug !== tid + '/' + req.params.slug) && (results.topic.slug && results.topic.slug !== tid + '/')) { var url = '/topic/' + results.topic.slug; if (req.params.post_index){ - url += '/'+req.params.post_index; + url += '/' + req.params.post_index; + } + if (currentPage > 1) { + url += '?page=' + currentPage; } return helpers.redirect(res, url); } @@ -76,18 +78,13 @@ topicsController.get = function(req, res, callback) { var set = 'tid:' + tid + ':posts'; var reverse = false; - // `sort` qs has priority over user setting + var sort = req.query.sort || settings.topicPostSort; if (sort === 'newest_to_oldest') { reverse = true; } else if (sort === 'most_votes') { reverse = true; set = 'tid:' + tid + ':posts:votes'; - } else if (settings.topicPostSort === 'newest_to_oldest') { - reverse = true; - } else if (settings.topicPostSort === 'most_votes') { - reverse = true; - set = 'tid:' + tid + ':posts:votes'; } var postIndex = 0; @@ -97,7 +94,9 @@ topicsController.get = function(req, res, callback) { req.params.post_index = 0; } if (!settings.usePagination) { - currentPage = 1; + if (req.params.post_index !== 0) { + currentPage = 1; + } if (reverse) { postIndex = Math.max(0, postCount - (req.params.post_index || postCount) - Math.ceil(settings.postsPerPage / 2)); } else { @@ -140,7 +139,7 @@ topicsController.get = function(req, res, callback) { } ]; - helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function(err, crumbs) { + helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function (err, crumbs) { if (err) { return next(err); } @@ -150,7 +149,7 @@ topicsController.get = function(req, res, callback) { }, function (topicData, next) { function findPost(index) { - for(var i=0; i data.pageCount)) { + req.query.page = Math.max(1, Math.min(data.pageCount, page)); + return helpers.redirect(res, '/unread?' + querystring.stringify(req.query)); + } + + data.categories = results.watchedCategories.categories; + data.selectedCategory = results.watchedCategories.selectedCategory; + + if (req.path.startsWith('/api/unread') || req.path.startsWith('/unread')) { + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[unread:title]]'}]); + } + + data.title = '[[pages:unread]]'; + data.filters = [{ + name: '[[unread:all-topics]]', + url: 'unread', + selected: filter === '', + filter: '' + }, { + name: '[[unread:new-topics]]', + url: 'unread/new', + selected: filter === 'new', + filter: 'new' + }, { + name: '[[unread:watched-topics]]', + url: 'unread/watched', + selected: filter === 'watched', + filter: 'watched' + }]; + + data.selectedFilter = data.filters.filter(function (filter) { + return filter && filter.selected; + })[0]; + + data.querystring = cid ? ('?cid=' + validator.escape(String(cid))) : ''; + + res.render('unread', data); }); }; +function getWatchedCategories(uid, selectedCid, callback) { + async.waterfall([ + function (next) { + user.getWatchedCategories(uid, next); + }, + function (cids, next) { + privileges.categories.filterCids('read', cids, uid, next); + }, + function (cids, next) { + categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link', 'color', 'bgColor', 'parentCid'], next); + }, + function (categoryData, next) { + categoryData = categoryData.filter(function (category) { + return category && !category.link; + }); -unreadController.unreadTotal = function(req, res, next) { - topics.getTotalUnread(req.uid, function (err, data) { + var selectedCategory; + categoryData.forEach(function (category) { + category.selected = parseInt(category.cid, 10) === parseInt(selectedCid, 10); + if (category.selected) { + selectedCategory = category; + } + }); + + var categoriesData = []; + var tree = categories.getTree(categoryData, 0); + + tree.forEach(function (category) { + recursive(category, categoriesData, ''); + }); + + next(null, {categories: categoriesData, selectedCategory: selectedCategory}); + } + ], callback); +} + +function recursive(category, categoriesData, level) { + category.level = level; + categoriesData.push(category); + + category.children.forEach(function (child) { + recursive(child, categoriesData, '    ' + level); + }); +} + +unreadController.unreadTotal = function (req, res, next) { + var filter = req.params.filter || ''; + + if (!validFilter[filter]) { + return next(); + } + + topics.getTotalUnread(req.uid, filter, function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 59253c5666..f2896ec8f6 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -6,22 +6,19 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); var winston = require('winston'); +var mime = require('mime'); var meta = require('../meta'); var file = require('../file'); var plugins = require('../plugins'); var image = require('../image'); +var privileges = require('../privileges'); var uploadsController = {}; -uploadsController.upload = function(req, res, filesIterator, next) { +uploadsController.upload = function (req, res, filesIterator) { var files = req.files.files; - if (!req.user && meta.config.allowGuestUploads !== '1') { - deleteTempFiles(files); - return res.status(403).json('[[error:guest-upload-disabled]]'); - } - if (!Array.isArray(files)) { return res.status(500).json('invalid files'); } @@ -30,7 +27,7 @@ uploadsController.upload = function(req, res, filesIterator, next) { files = files[0]; } - async.map(files, filesIterator, function(err, images) { + async.map(files, filesIterator, function (err, images) { deleteTempFiles(files); if (err) { @@ -43,39 +40,104 @@ uploadsController.upload = function(req, res, filesIterator, next) { }); }; -uploadsController.uploadPost = function(req, res, next) { - uploadsController.upload(req, res, function(uploadedFile, next) { +uploadsController.uploadPost = function (req, res, next) { + uploadsController.upload(req, res, function (uploadedFile, next) { var isImage = uploadedFile.type.match(/image./); - if (isImage && plugins.hasListeners('filter:uploadImage')) { - return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: req.uid}, next); + if (isImage) { + uploadAsImage(req, uploadedFile, next); + } else { + uploadAsFile(req, uploadedFile, next); } - - async.waterfall([ - function(next) { - if (isImage) { - file.isFileTypeAllowed(uploadedFile.path, next); - } else { - next(); - } - }, - function (next) { - if (parseInt(meta.config.allowFileUploads, 10) !== 1) { - return next(new Error('[[error:uploads-are-disabled]]')); - } - uploadFile(req.uid, uploadedFile, next); - } - ], next); }, next); }; -uploadsController.uploadThumb = function(req, res, next) { +function uploadAsImage(req, uploadedFile, callback) { + async.waterfall([ + function (next) { + privileges.categories.can('upload:post:image', req.body.cid, req.uid, next); + }, + function (canUpload, next) { + if (!canUpload) { + return next(new Error('[[error:no-privileges]]')); + } + if (plugins.hasListeners('filter:uploadImage')) { + return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: req.uid}, callback); + } + file.isFileTypeAllowed(uploadedFile.path, next); + }, + function (next) { + uploadFile(req.uid, uploadedFile, next); + }, + function (fileObj, next) { + if (parseInt(meta.config.maximumImageWidth, 10) === 0) { + return next(null, fileObj); + } + + resizeImage(fileObj, next); + } + ], callback); +} + +function uploadAsFile(req, uploadedFile, callback) { + async.waterfall([ + function (next) { + privileges.categories.can('upload:post:file', req.body.cid, req.uid, next); + }, + function (canUpload, next) { + if (!canUpload) { + return next(new Error('[[error:no-privileges]]')); + } + if (parseInt(meta.config.allowFileUploads, 10) !== 1) { + return next(new Error('[[error:uploads-are-disabled]]')); + } + uploadFile(req.uid, uploadedFile, next); + } + ], callback); +} + +function resizeImage(fileObj, callback) { + async.waterfall([ + function (next) { + image.size(fileObj.path, next); + }, + function (imageData, next) { + if (imageData.width < (parseInt(meta.config.maximumImageWidth, 10) || 760)) { + return callback(null, fileObj); + } + + var dirname = path.dirname(fileObj.path); + var extname = path.extname(fileObj.path); + var basename = path.basename(fileObj.path, extname); + + image.resizeImage({ + path: fileObj.path, + target: path.join(dirname, basename + '-resized' + extname), + extension: extname, + width: parseInt(meta.config.maximumImageWidth, 10) || 760 + }, next); + }, + function (next) { + + // Return the resized version to the composer/postData + var dirname = path.dirname(fileObj.url); + var extname = path.extname(fileObj.url); + var basename = path.basename(fileObj.url, extname); + + fileObj.url = path.join(dirname, basename + '-resized' + extname); + + next(null, fileObj); + } + ], callback); +} + +uploadsController.uploadThumb = function (req, res, next) { if (parseInt(meta.config.allowTopicsThumbnail, 10) !== 1) { deleteTempFiles(req.files.files); return next(new Error('[[error:topic-thumbnails-are-disabled]]')); } - uploadsController.upload(req, res, function(uploadedFile, next) { - file.isFileTypeAllowed(uploadedFile.path, function(err) { + uploadsController.upload(req, res, function (uploadedFile, next) { + file.isFileTypeAllowed(uploadedFile.path, function (err) { if (err) { return next(err); } @@ -90,7 +152,7 @@ uploadsController.uploadThumb = function(req, res, next) { extension: path.extname(uploadedFile.name), width: size, height: size - }, function(err) { + }, function (err) { if (err) { return next(err); } @@ -105,7 +167,7 @@ uploadsController.uploadThumb = function(req, res, next) { }, next); }; -uploadsController.uploadGroupCover = function(uid, uploadedFile, callback) { +uploadsController.uploadGroupCover = function (uid, uploadedFile, callback) { if (plugins.hasListeners('filter:uploadImage')) { return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: uid}, callback); } @@ -114,7 +176,7 @@ uploadsController.uploadGroupCover = function(uid, uploadedFile, callback) { return plugins.fireHook('filter:uploadFile', {file: uploadedFile, uid: uid}, callback); } - file.isFileTypeAllowed(uploadedFile.path, function(err) { + file.isFileTypeAllowed(uploadedFile.path, function (err) { if (err) { return callback(err); } @@ -138,6 +200,9 @@ function uploadFile(uid, uploadedFile, callback) { if (meta.config.hasOwnProperty('allowedFileExtensions')) { var allowed = file.allowedExtensions(); var extension = path.extname(uploadedFile.name); + if (!extension) { + extension = '.' + mime.extension(uploadedFile.type); + } if (allowed.length > 0 && allowed.indexOf(extension) === -1) { return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]')); } @@ -147,24 +212,31 @@ function uploadFile(uid, uploadedFile, callback) { } function saveFileToLocal(uploadedFile, callback) { + var extension = path.extname(uploadedFile.name); + if (!extension && uploadedFile.type) { + extension = '.' + mime.extension(uploadedFile.type); + } + var filename = uploadedFile.name || 'upload'; - filename = Date.now() + '-' + validator.escape(filename).substr(0, 255); - file.saveFileToLocal(filename, 'files', uploadedFile.path, function(err, upload) { + filename = Date.now() + '-' + validator.escape(filename.replace(extension, '')).substr(0, 255) + extension; + + file.saveFileToLocal(filename, 'files', uploadedFile.path, function (err, upload) { if (err) { return callback(err); } callback(null, { url: nconf.get('relative_path') + upload.url, + path: upload.path, name: uploadedFile.name }); }); } function deleteTempFiles(files) { - async.each(files, function(file, next) { - fs.unlink(file.path, function(err) { + async.each(files, function (file, next) { + fs.unlink(file.path, function (err) { if (err) { winston.error(err); } diff --git a/src/controllers/users.js b/src/controllers/users.js index 31ac0bd1cc..baf18a5b64 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -5,29 +5,79 @@ var user = require('../user'); var meta = require('../meta'); var pagination = require('../pagination'); -var plugins = require('../plugins'); var db = require('../database'); var helpers = require('./helpers'); var usersController = {}; -usersController.getOnlineUsers = function(req, res, next) { + +usersController.index = function (req, res, next) { + var section = req.query.section || 'joindate'; + var sectionToController = { + joindate: usersController.getUsersSortedByJoinDate, + online: usersController.getOnlineUsers, + 'sort-posts': usersController.getUsersSortedByPosts, + 'sort-reputation': usersController.getUsersSortedByReputation, + banned: usersController.getBannedUsers, + flagged: usersController.getFlaggedUsers + }; + + if (req.query.term) { + usersController.search(req, res, next); + } else if (sectionToController[section]) { + sectionToController[section](req, res, next); + } else { + usersController.getUsersSortedByJoinDate(req, res, next); + } +}; + +usersController.search = function (req, res, next) { async.parallel({ - users: function(next) { - usersController.getUsers('users:online', req.uid, req.query.page, next); + search: function (next) { + user.search({ + query: req.query.term, + searchBy: req.query.searchBy || 'username', + page: req.query.page || 1, + sortBy: req.query.sortBy, + onlineOnly: req.query.onlineOnly === 'true', + bannedOnly: req.query.bannedOnly === 'true', + flaggedOnly: req.query.flaggedOnly === 'true' + }, next); }, - guests: function(next) { + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(req.uid, next); + } + }, function (err, results) { + if (err) { + return next(err); + } + + var section = req.query.section || 'joindate'; + + results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod; + results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query); + results.search['section_' + section] = true; + render(req, res, results.search, next); + }); +}; + +usersController.getOnlineUsers = function (req, res, next) { + async.parallel({ + users: function (next) { + usersController.getUsers('users:online', req.uid, req.query, next); + }, + guests: function (next) { require('../socket.io/admin/rooms').getTotalGuestCount(next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } var userData = results.users; var hiddenCount = 0; if (!userData.isAdminOrGlobalMod) { - userData.users = userData.users.filter(function(user) { + userData.users = userData.users.filter(function (user) { if (user && user.status === 'offline') { hiddenCount ++; } @@ -41,23 +91,23 @@ usersController.getOnlineUsers = function(req, res, next) { }); }; -usersController.getUsersSortedByPosts = function(req, res, next) { +usersController.getUsersSortedByPosts = function (req, res, next) { usersController.renderUsersPage('users:postcount', req, res, next); }; -usersController.getUsersSortedByReputation = function(req, res, next) { +usersController.getUsersSortedByReputation = function (req, res, next) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return next(); } usersController.renderUsersPage('users:reputation', req, res, next); }; -usersController.getUsersSortedByJoinDate = function(req, res, next) { +usersController.getUsersSortedByJoinDate = function (req, res, next) { usersController.renderUsersPage('users:joindate', req, res, next); }; -usersController.getBannedUsers = function(req, res, next) { - usersController.getUsers('users:banned', req.uid, req.query.page, function(err, userData) { +usersController.getBannedUsers = function (req, res, next) { + usersController.getUsers('users:banned', req.uid, req.query, function (err, userData) { if (err) { return next(err); } @@ -70,93 +120,103 @@ usersController.getBannedUsers = function(req, res, next) { }); }; -usersController.renderUsersPage = function(set, req, res, next) { - usersController.getUsers(set, req.uid, req.query.page, function(err, userData) { +usersController.getFlaggedUsers = function (req, res, next) { + usersController.getUsers('users:flags', req.uid, req.query, function (err, userData) { if (err) { return next(err); } + + if (!userData.isAdminOrGlobalMod) { + return next(); + } + render(req, res, userData, next); }); }; -usersController.getUsers = function(set, uid, page, callback) { - var setToTitles = { - 'users:postcount': '[[pages:users/sort-posts]]', - 'users:reputation': '[[pages:users/sort-reputation]]', - 'users:joindate': '[[pages:users/latest]]', - 'users:online': '[[pages:users/online]]', - 'users:banned': '[[pages:users/banned]]' +usersController.renderUsersPage = function (set, req, res, next) { + usersController.getUsers(set, req.uid, req.query, function (err, userData) { + if (err) { + return next(err); + } + + render(req, res, userData, next); + }); +}; + +usersController.getUsers = function (set, uid, query, callback) { + var setToData = { + 'users:postcount': {title: '[[pages:users/sort-posts]]', crumb: '[[users:top_posters]]'}, + 'users:reputation': {title: '[[pages:users/sort-reputation]]', crumb: '[[users:most_reputation]]'}, + 'users:joindate': {title: '[[pages:users/latest]]', crumb: '[[global:users]]'}, + 'users:online': {title: '[[pages:users/online]]', crumb: '[[global:online]]'}, + 'users:banned': {title: '[[pages:users/banned]]', crumb: '[[user:banned]]'}, + 'users:flags': {title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]'}, }; - var setToCrumbs = { - 'users:postcount': '[[users:top_posters]]', - 'users:reputation': '[[users:most_reputation]]', - 'users:joindate': '[[global:users]]', - 'users:online': '[[global:online]]', - 'users:banned': '[[user:banned]]' - }; + if (!setToData[set]) { + setToData[set] = {title: '', crumb: ''}; + } - var breadcrumbs = [{text: setToCrumbs[set]}]; + var breadcrumbs = [{text: setToData[set].crumb}]; if (set !== 'users:joindate') { breadcrumbs.unshift({text: '[[global:users]]', url: '/users'}); } - page = parseInt(page, 10) || 1; + var page = parseInt(query.page, 10) || 1; var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 50; var start = Math.max(0, page - 1) * resultsPerPage; var stop = start + resultsPerPage - 1; async.parallel({ - isAdministrator: function(next) { - user.isAdministrator(uid, next); + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(uid, next); }, - isGlobalMod: function(next) { - user.isGlobalModerator(uid, next); - }, - usersData: function(next) { + usersData: function (next) { usersController.getUsersAndCount(set, uid, start, stop, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var pageCount = Math.ceil(results.usersData.count / resultsPerPage); var userData = { - loadmore_display: results.usersData.count > (stop - start + 1) ? 'block' : 'hide', users: results.usersData.users, - pagination: pagination.create(page, pageCount), - title: setToTitles[set] || '[[pages:users/latest]]', + pagination: pagination.create(page, pageCount, query), + userCount: results.usersData.count, + title: setToData[set].title || '[[pages:users/latest]]', breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs), - setName: set, - isAdminOrGlobalMod: results.isAdministrator || results.isGlobalMod + isAdminOrGlobalMod: results.isAdminOrGlobalMod }; - userData['route_' + set] = true; + userData['section_' + (query.section || 'joindate')] = true; callback(null, userData); }); }; -usersController.getUsersAndCount = function(set, uid, start, stop, callback) { +usersController.getUsersAndCount = function (set, uid, start, stop, callback) { async.parallel({ - users: function(next) { + users: function (next) { user.getUsersFromSet(set, uid, start, stop, next); }, - count: function(next) { + count: function (next) { if (set === 'users:online') { var now = Date.now(); db.sortedSetCount('users:online', now - 300000, '+inf', next); } else if (set === 'users:banned') { db.sortedSetCard('users:banned', next); + } else if (set === 'users:flags') { + db.sortedSetCard('users:flags', next); } else { db.getObjectField('global', 'userCount', next); } } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - results.users = results.users.filter(function(user) { + results.users = results.users.filter(function (user) { return user && parseInt(user.uid, 10); }); @@ -165,26 +225,22 @@ usersController.getUsersAndCount = function(set, uid, start, stop, callback) { }; function render(req, res, data, next) { - plugins.fireHook('filter:users.build', {req: req, res: res, templateData: data }, function(err, data) { + var registrationType = meta.config.registrationType; + + data.maximumInvites = meta.config.maximumInvites; + data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.adminInviteOnly = registrationType === 'admin-invite-only'; + data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; + + user.getInvitesNumber(req.uid, function (err, numInvites) { if (err) { return next(err); } - var registrationType = meta.config.registrationType; + res.append('X-Total-Count', data.userCount); + data.invites = numInvites; - data.templateData.maximumInvites = meta.config.maximumInvites; - data.templateData.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; - data.templateData.adminInviteOnly = registrationType === 'admin-invite-only'; - data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - - user.getInvitesNumber(req.uid, function(err, num) { - if (err) { - return next(err); - } - - data.templateData.invites = num; - res.render('users', data.templateData); - }); + res.render('users', data); }); } diff --git a/src/coverPhoto.js b/src/coverPhoto.js index d699ace785..699e4ee374 100644 --- a/src/coverPhoto.js +++ b/src/coverPhoto.js @@ -5,11 +5,11 @@ var meta = require('./meta'); var nconf = require('nconf'); -coverPhoto.getDefaultGroupCover = function(groupName) { +coverPhoto.getDefaultGroupCover = function (groupName) { return getCover('groups', groupName); }; -coverPhoto.getDefaultProfileCover = function(uid) { +coverPhoto.getDefaultProfileCover = function (uid) { return getCover('profile', parseInt(uid, 10)); }; diff --git a/src/database/mongo.js b/src/database/mongo.js index 0e7e8e2d2a..92bfd264dc 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -1,7 +1,7 @@ 'use strict'; -(function(module) { +(function (module) { var winston = require('winston'), async = require('async'), @@ -33,20 +33,21 @@ name: 'mongo:password', description: 'Password of your MongoDB database', hidden: true, - before: function(value) { value = value || nconf.get('mongo:password') || ''; return value; } + default: nconf.get('mongo:password') || '', + before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; } }, { name: "mongo:database", - description: "Which database to use", - 'default': nconf.get('mongo:database') || 0 + description: "MongoDB database name", + 'default': nconf.get('mongo:database') || 'nodebb' } ]; module.helpers = module.helpers || {}; module.helpers.mongo = require('./mongo/helpers'); - module.init = function(callback) { - callback = callback || function() {}; + module.init = function (callback) { + callback = callback || function () {}; try { var sessionStore; mongoClient = require('mongodb').MongoClient; @@ -74,7 +75,7 @@ nconf.set('mongo:port', 27017); } if (!nconf.get('mongo:database')) { - nconf.set('mongo:database', '0'); + nconf.set('mongo:database', 'nodebb'); } var hosts = nconf.get('mongo:host').split(','); @@ -95,7 +96,7 @@ connOptions = _.deepExtend((nconf.get('mongo:options') || {}), connOptions); - mongoClient.connect(connString, connOptions, function(err, _db) { + mongoClient.connect(connString, connOptions, function (err, _db) { if (err) { winston.error("NodeBB could not connect to your Mongo database. Mongo returned the following error: " + err.message); return callback(err); @@ -110,8 +111,13 @@ db: db }); } else { + // Initial Redis database + var rdb = require('./redis'); + // Create a new redis connection and store it in module (skeleton) + rdb.client = rdb.connect(); + module.sessionStore = new sessionStore({ - client: require('./redis').connect(), + client: rdb.client, ttl: 60 * 60 * 24 * 14 }); } @@ -141,7 +147,7 @@ async.apply(createIndex, 'objects', {_key: 1, score: -1}, {background: true}), async.apply(createIndex, 'objects', {_key: 1, value: -1}, {background: true, unique: true, sparse: true}), async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true}) - ], function(err) { + ], function (err) { if (err) { winston.error('Error creating index ' + err.message); } @@ -155,7 +161,7 @@ }); }; - module.checkCompatibility = function(callback) { + module.checkCompatibility = function (callback) { var mongoPkg = require.main.require('./node_modules/mongodb/package.json'), err = semver.lt(mongoPkg.version, '2.0.0') ? new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.') : null; @@ -165,32 +171,32 @@ callback(err); }; - module.info = function(db, callback) { + module.info = function (db, callback) { async.parallel({ - serverStatus: function(next) { + serverStatus: function (next) { db.command({'serverStatus': 1}, next); }, - stats: function(next) { + stats: function (next) { db.command({'dbStats': 1}, next); }, - listCollections: function(next) { - db.listCollections().toArray(function(err, items) { + listCollections: function (next) { + db.listCollections().toArray(function (err, items) { if (err) { return next(err); } - async.map(items, function(collection, next) { + async.map(items, function (collection, next) { db.collection(collection.name).stats(next); }, next); }); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var stats = results.stats; var scale = 1024 * 1024; - results.listCollections = results.listCollections.map(function(collectionInfo) { + results.listCollections = results.listCollections.map(function (collectionInfo) { return { name: collectionInfo.ns, count: collectionInfo.count, @@ -222,7 +228,7 @@ }); }; - module.close = function() { + module.close = function () { db.close(); }; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index dbf294119e..7baf70b401 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -1,20 +1,20 @@ "use strict"; -module.exports = function(db, module) { +module.exports = function (db, module) { var helpers = module.helpers.mongo; - module.setObject = function(key, data, callback) { + module.setObject = function (key, data, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } - db.collection('objects').update({_key: key}, {$set: data}, {upsert: true, w: 1}, function(err) { + db.collection('objects').update({_key: key}, {$set: data}, {upsert: true, w: 1}, function (err) { callback(err); }); }; - module.setObjectField = function(key, field, value, callback) { + module.setObjectField = function (key, field, value, callback) { callback = callback || helpers.noop; if (!field) { return callback(); @@ -25,18 +25,18 @@ module.exports = function(db, module) { module.setObject(key, data, callback); }; - module.getObject = function(key, callback) { + module.getObject = function (key, callback) { if (!key) { return callback(); } db.collection('objects').findOne({_key: key}, {_id: 0, _key: 0}, callback); }; - module.getObjects = function(keys, callback) { + module.getObjects = function (keys, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(null, []); } - db.collection('objects').find({_key: {$in: keys}}, {_id: 0}).toArray(function(err, data) { + db.collection('objects').find({_key: {$in: keys}}, {_id: 0}).toArray(function (err, data) { if (err) { return callback(err); } @@ -44,7 +44,7 @@ module.exports = function(db, module) { var map = helpers.toMap(data); var returnData = []; - for (var i=0; i 0) { pipeline.push({ $limit: limit }); } - pipeline.push({ $project: { _id: 0, value: '$_id.value' }}); + var project = { _id: 0, value: '$_id.value' }; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); - db.collection('objects').aggregate(pipeline, function(err, data) { + db.collection('objects').aggregate(pipeline, function (err, data) { if (err || !data) { return callback(err); } - data = data.map(function(item) { - return item.value; - }); + if (!params.withScores) { + data = data.map(function (item) { + return item.value; + }); + } + callback(null, data); }); } - module.sortedSetIncrBy = function(key, increment, value, callback) { + module.sortedSetIncrBy = function (key, increment, value, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } var data = {}; - value = helpers.fieldToString(value); + value = helpers.valueToString(value); data.score = parseInt(increment, 10); - db.collection('objects').findAndModify({_key: key, value: value}, {}, {$inc: data}, {new: true, upsert: true}, function(err, result) { + db.collection('objects').findAndModify({_key: key, value: value}, {}, {$inc: data}, {new: true, upsert: true}, function (err, result) { // if there is duplicate key error retry the upsert // https://github.com/NodeBB/NodeBB/issues/4467 // https://jira.mongodb.org/browse/SERVER-14322 @@ -535,7 +578,7 @@ module.exports = function(db, module) { }); }; - module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) { + module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) { var query = {_key: key}; if (min !== '-') { query.value = {$gte: min}; @@ -548,18 +591,18 @@ module.exports = function(db, module) { .sort({value: 1}) .skip(start) .limit(count === -1 ? 0 : count) - .toArray(function(err, data) { + .toArray(function (err, data) { if (err) { return callback(err); } - data = data.map(function(item) { + data = data.map(function (item) { return item && item.value; }); callback(err, data); }); }; - module.processSortedSet = function(setKey, process, batch, callback) { + module.processSortedSet = function (setKey, process, batch, callback) { var done = false; var ids = []; var cursor = db.collection('objects').find({_key: setKey}) @@ -568,11 +611,11 @@ module.exports = function(db, module) { .batchSize(batch); async.whilst( - function() { + function () { return !done; }, - function(next) { - cursor.next(function(err, item) { + function (next) { + cursor.next(function (err, item) { if (err) { return next(err); } @@ -586,7 +629,7 @@ module.exports = function(db, module) { return next(null); } - process(ids, function(err) { + process(ids, function (err) { ids = []; return next(err); }); @@ -595,4 +638,99 @@ module.exports = function(db, module) { callback ); }; + + + module.sortedSetIntersectCard = function (keys, callback) { + if (!Array.isArray(keys) || !keys.length) { + return callback(null, 0); + } + + var pipeline = [ + { $match: { _key: {$in: keys}} }, + { $group: { _id: {value: '$value'}, count: {$sum: 1}} }, + { $match: { count: keys.length} }, + { $group: { _id: null, count: { $sum: 1 } } } + ]; + + db.collection('objects').aggregate(pipeline, function (err, data) { + callback(err, Array.isArray(data) && data.length ? data[0].count : 0); + }); + }; + + module.getSortedSetIntersect = function (params, callback) { + params.sort = 1; + getSortedSetRevIntersect(params, callback); + }; + + module.getSortedSetRevIntersect = function (params, callback) { + params.sort = -1; + getSortedSetRevIntersect(params, callback); + }; + + function getSortedSetRevIntersect(params, callback) { + var sets = params.sets; + var start = params.hasOwnProperty('start') ? params.start : 0; + var stop = params.hasOwnProperty('stop') ? params.stop : -1; + var weights = params.weights || []; + var aggregate = {}; + + if (params.aggregate) { + aggregate['$' + params.aggregate.toLowerCase()] = '$score'; + } else { + aggregate.$sum = '$score'; + } + + var limit = stop - start + 1; + if (limit <= 0) { + limit = 0; + } + + var pipeline = [{ $match: { _key: {$in: sets}} }]; + + weights.forEach(function (weight, index) { + if (weight !== 1) { + pipeline.push({ + $project: { + value: 1, + score: { + $cond: { if: { $eq: [ "$_key", sets[index] ] }, then: { $multiply: [ '$score', weight ] }, else: '$score' } + } + } + }); + } + }); + + pipeline.push({ $group: { _id: {value: '$value'}, totalScore: aggregate, count: {$sum: 1}} }); + pipeline.push({ $match: { count: sets.length} }); + pipeline.push({ $sort: { totalScore: params.sort} }); + + if (start) { + pipeline.push({ $skip: start }); + } + + if (limit > 0) { + pipeline.push({ $limit: limit }); + } + + var project = { _id: 0, value: '$_id.value'}; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); + + db.collection('objects').aggregate(pipeline, function (err, data) { + if (err || !data) { + return callback(err); + } + + if (!params.withScores) { + data = data.map(function (item) { + return item.value; + }); + } + + callback(null, data); + }); + } + }; diff --git a/src/database/redis.js b/src/database/redis.js index 239090aa95..3b05148c9e 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -1,6 +1,6 @@ 'use strict'; -(function(module) { +(function (module) { var winston = require('winston'), nconf = require('nconf'), @@ -25,7 +25,8 @@ name: 'redis:password', description: 'Password of your Redis database', hidden: true, - before: function(value) { value = value || nconf.get('redis:password') || ''; return value; } + default: nconf.get('redis:password') || '', + before: function (value) { value = value || nconf.get('redis:password') || ''; return value; } }, { name: "redis:database", @@ -34,7 +35,7 @@ } ]; - module.init = function(callback) { + module.init = function (callback) { try { redis = require('redis'); connectRedis = require('connect-redis')(session); @@ -63,16 +64,19 @@ } }; - module.connect = function(options) { - var redis_socket_or_host = nconf.get('redis:host'), - cxn, dbIdx; - - options = options || {}; + module.connect = function (options) { + var redis_socket_or_host = nconf.get('redis:host'); + var cxn; if (!redis) { redis = require('redis'); } + options = options || {}; + if (nconf.get('redis:password')) { + options.auth_pass = nconf.get('redis:password'); + } + if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) { /* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */ cxn = redis.createClient(nconf.get('redis:host'), options); @@ -90,9 +94,9 @@ cxn.auth(nconf.get('redis:password')); } - dbIdx = parseInt(nconf.get('redis:database'), 10); + var dbIdx = parseInt(nconf.get('redis:database'), 10); if (dbIdx) { - cxn.select(dbIdx, function(error) { + cxn.select(dbIdx, function (error) { if(error) { winston.error("NodeBB could not connect to your Redis database. Redis returned the following error: " + error.message); process.exit(); @@ -103,8 +107,8 @@ return cxn; }; - module.checkCompatibility = function(callback) { - module.info(module.client, function(err, info) { + module.checkCompatibility = function (callback) { + module.info(module.client, function (err, info) { if (err) { return callback(err); } @@ -118,11 +122,11 @@ }); }; - module.close = function() { + module.close = function () { redisClient.quit(); }; - module.info = function(cxn, callback) { + module.info = function (cxn, callback) { cxn.info(function (err, data) { if (err) { return callback(err); diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index ce79d0bc6f..a728852f1a 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -1,50 +1,50 @@ "use strict"; -module.exports = function(redisClient, module) { +module.exports = function (redisClient, module) { var helpers = module.helpers.redis; - module.setObject = function(key, data, callback) { - callback = callback || function() {}; - redisClient.hmset(key, data, function(err) { + module.setObject = function (key, data, callback) { + callback = callback || function () {}; + redisClient.hmset(key, data, function (err) { callback(err); }); }; - module.setObjectField = function(key, field, value, callback) { - callback = callback || function() {}; - redisClient.hset(key, field, value, function(err) { + module.setObjectField = function (key, field, value, callback) { + callback = callback || function () {}; + redisClient.hset(key, field, value, function (err) { callback(err); }); }; - module.getObject = function(key, callback) { + module.getObject = function (key, callback) { redisClient.hgetall(key, callback); }; - module.getObjects = function(keys, callback) { + module.getObjects = function (keys, callback) { helpers.multiKeys(redisClient, 'hgetall', keys, callback); }; - module.getObjectField = function(key, field, callback) { - module.getObjectFields(key, [field], function(err, data) { + module.getObjectField = function (key, field, callback) { + module.getObjectFields(key, [field], function (err, data) { callback(err, data ? data[field] : null); }); }; - module.getObjectFields = function(key, fields, callback) { - module.getObjectsFields([key], fields, function(err, results) { + module.getObjectFields = function (key, fields, callback) { + module.getObjectsFields([key], fields, function (err, results) { callback(err, results ? results[0] : null); }); }; - module.getObjectsFields = function(keys, fields, callback) { + module.getObjectsFields = function (keys, fields, callback) { if (!Array.isArray(fields) || !fields.length) { - return callback(null, keys.map(function() { return {}; })); + return callback(null, keys.map(function () { return {}; })); } var multi = redisClient.multi(); - for(var x=0; x (parseInt(meta.config.maximumGroupNameLength, 10) || 255)) { + return callback(new Error('[[error:group-name-too-long]]')); + } + if (name.indexOf('/') !== -1) { return callback(new Error('[[error:invalid-group-name]]')); } diff --git a/src/groups/delete.js b/src/groups/delete.js index 8e665249a8..0838dd2407 100644 --- a/src/groups/delete.js +++ b/src/groups/delete.js @@ -1,14 +1,14 @@ 'use strict'; -var async = require('async'), - plugins = require('../plugins'), - utils = require('../../public/src/utils'), - db = require('./../database'); +var async = require('async'); +var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); +var db = require('./../database'); -module.exports = function(Groups) { +module.exports = function (Groups) { - Groups.destroy = function(groupName, callback) { - Groups.getGroupsData([groupName], function(err, groupsData) { + Groups.destroy = function (groupName, callback) { + Groups.getGroupsData([groupName], function (err, groupsData) { if (err) { return callback(err); } @@ -16,6 +16,7 @@ module.exports = function(Groups) { return callback(); } var groupObj = groupsData[0]; + plugins.fireHook('action:group.destroy', groupObj); async.parallel([ @@ -29,17 +30,23 @@ module.exports = function(Groups) { async.apply(db.delete, 'group:' + groupName + ':invited'), async.apply(db.delete, 'group:' + groupName + ':owners'), async.apply(db.deleteObjectField, 'groupslug:groupname', utils.slugify(groupName)), - function(next) { - db.getSortedSetRange('groups:createtime', 0, -1, function(err, groups) { + function (next) { + db.getSortedSetRange('groups:createtime', 0, -1, function (err, groups) { if (err) { return next(err); } - async.each(groups, function(group, next) { + async.each(groups, function (group, next) { db.sortedSetRemove('group:' + group + ':members', groupName, next); }, next); }); } - ], callback); + ], function (err) { + if (err) { + return callback(err); + } + Groups.resetCache(); + callback(); + }); }); }; }; diff --git a/src/groups/membership.js b/src/groups/membership.js index 747fa9d3d7..d03dba3e59 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -1,86 +1,112 @@ 'use strict'; -var async = require('async'), - winston = require('winston'), - _ = require('underscore'), +var async = require('async'); +var winston = require('winston'); +var _ = require('underscore'); - user = require('../user'), - utils = require('../../public/src/utils'), - plugins = require('../plugins'), - notifications = require('../notifications'), - db = require('./../database'); +var user = require('../user'); +var utils = require('../../public/src/utils'); +var plugins = require('../plugins'); +var notifications = require('../notifications'); +var db = require('../database'); -module.exports = function(Groups) { - Groups.join = function(groupName, uid, callback) { - function join() { - var tasks = [ - async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid), - async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount') - ]; +var pubsub = require('../pubsub'); +var LRU = require('lru-cache'); - async.waterfall([ - function(next) { - async.parallel({ - isAdmin: function(next) { - user.isAdministrator(uid, next); - }, - isHidden: function(next) { - Groups.isHidden(groupName, next); - } - }, next); - }, - function(results, next) { - if (results.isAdmin) { - tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid)); - } - if (!results.isHidden) { - tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName)); - } - async.parallel(tasks, next); - }, - function(results, next) { - user.setGroupTitle(groupName, uid, next); - }, - function(next) { - plugins.fireHook('action:group.join', { - groupName: groupName, - uid: uid - }); - next(); - } - ], callback); - } +var cache = LRU({ + max: 40000, + maxAge: 1000 * 60 * 60 +}); - callback = callback || function() {}; +module.exports = function (Groups) { + + Groups.cache = cache; + + Groups.join = function (groupName, uid, callback) { + callback = callback || function () {}; if (!groupName) { return callback(new Error('[[error:invalid-data]]')); } - Groups.exists(groupName, function(err, exists) { - if (err) { + async.waterfall([ + function (next) { + Groups.isMember(uid, groupName, next); + }, + function (isMember, next) { + if (isMember) { + return callback(); + } + Groups.exists(groupName, next); + }, + function (exists, next) { + if (exists) { + return next(); + } + Groups.create({ + name: groupName, + description: '', + hidden: 1 + }, function (err) { + if (err && err.message !== '[[error:group-already-exists]]') { + winston.error('[groups.join] Could not create new hidden group: ' + err.message); + return callback(err); + } + next(); + }); + }, + function (next) { + async.parallel({ + isAdmin: function (next) { + user.isAdministrator(uid, next); + }, + isHidden: function (next) { + Groups.isHidden(groupName, next); + } + }, next); + }, + function (results, next) { + var tasks = [ + async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid), + async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount') + ]; + if (results.isAdmin) { + tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid)); + } + if (!results.isHidden) { + tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName)); + } + async.parallel(tasks, next); + }, + function (results, next) { + clearCache(uid, groupName); + setGroupTitleIfNotSet(groupName, uid, next); + }, + function (next) { + plugins.fireHook('action:group.join', { + groupName: groupName, + uid: uid + }); + next(); + } + ], callback); + }; + + function setGroupTitleIfNotSet(groupName, uid, callback) { + if (groupName === 'registered-users' || Groups.isPrivilegeGroup(groupName)) { + return callback(); + } + + db.getObjectField('user:' + uid, 'groupTitle', function (err, currentTitle) { + if (err || (currentTitle || currentTitle === '')) { return callback(err); } - if (exists) { - return join(); - } - - Groups.create({ - name: groupName, - description: '', - hidden: 1 - }, function(err) { - if (err && err.message !== '[[error:group-already-exists]]') { - winston.error('[groups.join] Could not create new hidden group: ' + err.message); - return callback(err); - } - join(); - }); + user.setUserField(uid, 'groupTitle', groupName, callback); }); - }; + } - Groups.requestMembership = function(groupName, uid, callback) { + Groups.requestMembership = function (groupName, uid, callback) { async.waterfall([ async.apply(inviteOrRequestMembership, groupName, uid, 'request'), function (next) { @@ -88,7 +114,7 @@ module.exports = function(Groups) { }, function (username, next) { async.parallel({ - notification: function(next) { + notification: function (next) { notifications.create({ bodyShort: '[[groups:request.notification_title, ' + username + ']]', bodyLong: '[[groups:request.notification_text, ' + username + ', ' + groupName + ']]', @@ -97,7 +123,7 @@ module.exports = function(Groups) { from: uid }, next); }, - owners: function(next) { + owners: function (next) { Groups.getOwners(groupName, next); } }, next); @@ -111,7 +137,7 @@ module.exports = function(Groups) { ], callback); }; - Groups.acceptMembership = function(groupName, uid, callback) { + Groups.acceptMembership = function (groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.waterfall([ async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), @@ -120,7 +146,7 @@ module.exports = function(Groups) { ], callback); }; - Groups.rejectMembership = function(groupName, uid, callback) { + Groups.rejectMembership = function (groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.parallel([ async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), @@ -128,7 +154,7 @@ module.exports = function(Groups) { ], callback); }; - Groups.invite = function(groupName, uid, callback) { + Groups.invite = function (groupName, uid, callback) { async.waterfall([ async.apply(inviteOrRequestMembership, groupName, uid, 'invite'), async.apply(notifications.create, { @@ -138,11 +164,7 @@ module.exports = function(Groups) { path: '/groups/' + utils.slugify(groupName) }), function (notification, next) { - if (!notification) { - return next(); - } - - notifications.push(notification, [uid]); + notifications.push(notification, [uid], next); } ], callback); }; @@ -155,7 +177,7 @@ module.exports = function(Groups) { var set = type === 'invite' ? 'group:' + groupName + ':invited' : 'group:' + groupName + ':pending'; async.waterfall([ - function(next) { + function (next) { async.parallel({ exists: async.apply(Groups.exists, groupName), isMember: async.apply(Groups.isMember, uid, groupName), @@ -163,20 +185,20 @@ module.exports = function(Groups) { isInvited: async.apply(Groups.isInvited, uid, groupName) }, next); }, - function(checks, next) { + function (checks, next) { if (!checks.exists) { return next(new Error('[[error:no-group]]')); } else if (checks.isMember) { - return next(new Error('[[error:group-already-member]]')); + return callback(); } else if (type === 'invite' && checks.isInvited) { - return next(new Error('[[error:group-already-invited]]')); + return callback(); } else if (type === 'request' && checks.isPending) { return next(new Error('[[error:group-already-requested]]')); } db.setAdd(set, uid, next); }, - function(next) { + function (next) { plugins.fireHook(hookName, { groupName: groupName, uid: uid @@ -186,53 +208,68 @@ module.exports = function(Groups) { ], callback); } - Groups.leave = function(groupName, uid, callback) { - callback = callback || function() {}; + Groups.leave = function (groupName, uid, callback) { + callback = callback || function () {}; - var tasks = [ - async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid), - async.apply(db.setRemove, 'group:' + groupName + ':owners', uid), - async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount') - ]; - - async.parallel(tasks, function(err) { - if (err) { - return callback(err); - } - - plugins.fireHook('action:group.leave', { - groupName: groupName, - uid: uid - }); - - Groups.getGroupFields(groupName, ['hidden', 'memberCount'], function(err, groupData) { - if (err || !groupData) { - return callback(err); + async.waterfall([ + function (next) { + Groups.isMember(uid, groupName, next); + }, + function (isMember, next) { + if (!isMember) { + return callback(); } - if (parseInt(groupData.hidden, 10) === 1 && parseInt(groupData.memberCount, 10) === 0) { - Groups.destroy(groupName, callback); + Groups.exists(groupName, next); + }, + function (exists, next) { + if (!exists) { + return callback(); + } + async.parallel([ + async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid), + async.apply(db.setRemove, 'group:' + groupName + ':owners', uid), + async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount') + ], next); + }, + function (results, next) { + clearCache(uid, groupName); + Groups.getGroupFields(groupName, ['hidden', 'memberCount'], next); + }, + function (groupData, next) { + if (!groupData) { + return callback(); + } + if (Groups.isPrivilegeGroup(groupName) && parseInt(groupData.memberCount, 10) === 0) { + Groups.destroy(groupName, next); } else { if (parseInt(groupData.hidden, 10) !== 1) { - db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, callback); + db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, next); } else { - callback(); + next(); } } - }); - }); + }, + function (next) { + plugins.fireHook('action:group.leave', { + groupName: groupName, + uid: uid + }); + next(); + } + ], callback); }; - Groups.leaveAllGroups = function(uid, callback) { + Groups.leaveAllGroups = function (uid, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange('groups:createtime', 0, -1, next); }, - function(groups, next) { - async.each(groups, function(groupName, next) { + function (groups, next) { + async.each(groups, function (groupName, next) { async.parallel([ - function(next) { - Groups.isMember(uid, groupName, function(err, isMember) { + function (next) { + Groups.isMember(uid, groupName, function (err, isMember) { if (!err && isMember) { Groups.leave(groupName, uid, next); } else { @@ -240,7 +277,7 @@ module.exports = function(Groups) { } }); }, - function(next) { + function (next) { Groups.rejectMembership(groupName, uid, next); } ], next); @@ -249,13 +286,13 @@ module.exports = function(Groups) { ], callback); }; - Groups.getMembers = function(groupName, start, stop, callback) { + Groups.getMembers = function (groupName, start, stop, callback) { db.getSortedSetRevRange('group:' + groupName + ':members', start, stop, callback); }; - Groups.getMemberUsers = function(groupNames, start, stop, callback) { - async.map(groupNames, function(groupName, next) { - Groups.getMembers(groupName, start, stop, function(err, uids) { + Groups.getMemberUsers = function (groupNames, start, stop, callback) { + async.map(groupNames, function (groupName, next) { + Groups.getMembers(groupName, start, stop, function (err, uids) { if (err) { return next(err); } @@ -265,36 +302,129 @@ module.exports = function(Groups) { }, callback); }; - Groups.getMembersOfGroups = function(groupNames, callback) { - db.getSortedSetsMembers(groupNames.map(function(name) { + Groups.getMembersOfGroups = function (groupNames, callback) { + db.getSortedSetsMembers(groupNames.map(function (name) { return 'group:' + name + ':members'; }), callback); }; - Groups.isMember = function(uid, groupName, callback) { + Groups.resetCache = function () { + pubsub.publish('group:cache:reset'); + cache.reset(); + }; + + pubsub.on('group:cache:reset', function () { + cache.reset(); + }); + + function clearCache(uid, groupName) { + pubsub.publish('group:cache:del', {uid: uid, groupName: groupName}); + cache.del(uid + ':' + groupName); + } + + pubsub.on('group:cache:del', function (data) { + cache.del(data.uid + ':' + data.groupName); + }); + + Groups.isMember = function (uid, groupName, callback) { if (!uid || parseInt(uid, 10) <= 0) { return callback(null, false); } - db.isSortedSetMember('group:' + groupName + ':members', uid, callback); - }; - Groups.isMembers = function(uids, groupName, callback) { - db.isSortedSetMembers('group:' + groupName + ':members', uids, callback); - }; - - Groups.isMemberOfGroups = function(uid, groups, callback) { - if (!uid || parseInt(uid, 10) <= 0) { - return callback(null, groups.map(function() {return false;})); + var cacheKey = uid + ':' + groupName; + if (cache.has(cacheKey)) { + return process.nextTick(callback, null, cache.get(cacheKey)); } - groups = groups.map(function(groupName) { + + db.isSortedSetMember('group:' + groupName + ':members', uid, function (err, isMember) { + if (err) { + return callback(err); + } + + cache.set(cacheKey, isMember); + callback(null, isMember); + }); + }; + + Groups.isMembers = function (uids, groupName, callback) { + if (!groupName || !uids.length) { + return callback(null, uids.map(function () {return false;})); + } + + var nonCachedUids = []; + uids.forEach(function (uid) { + if (!cache.has(uid + ':' + groupName)) { + nonCachedUids.push(uid); + } + }); + + if (!nonCachedUids.length) { + var result = uids.map(function (uid) { + return cache.get(uid + ':' + groupName); + }); + return process.nextTick(callback, null, result); + } + + db.isSortedSetMembers('group:' + groupName + ':members', nonCachedUids, function (err, isMembers) { + if (err) { + return callback(err); + } + + nonCachedUids.forEach(function (uid, index) { + cache.set(uid + ':' + groupName, isMembers[index]); + }); + + var result = uids.map(function (uid) { + return cache.get(uid + ':' + groupName); + }); + + callback(null, result); + }); + }; + + Groups.isMemberOfGroups = function (uid, groups, callback) { + if (!uid || parseInt(uid, 10) <= 0 || !groups.length) { + return callback(null, groups.map(function () {return false;})); + } + + var nonCachedGroups = []; + + groups.forEach(function (groupName) { + if (!cache.has(uid + ':' + groupName)) { + nonCachedGroups.push(groupName); + } + }); + + // are they all cached? + if (!nonCachedGroups.length) { + var result = groups.map(function (groupName) { + return cache.get(uid + ':' + groupName); + }); + return process.nextTick(callback, null, result); + } + + var nonCachedGroupsMemberSets = nonCachedGroups.map(function (groupName) { return 'group:' + groupName + ':members'; }); - db.isMemberOfSortedSets(groups, uid, callback); + db.isMemberOfSortedSets(nonCachedGroupsMemberSets, uid, function (err, isMembers) { + if (err) { + return callback(err); + } + + nonCachedGroups.forEach(function (groupName, index) { + cache.set(uid + ':' + groupName, isMembers[index]); + }); + + var result = groups.map(function (groupName) { + return cache.get(uid + ':' + groupName); + }); + callback(null, result); + }); }; - Groups.getMemberCount = function(groupName, callback) { - db.getObjectField('group:' + groupName, 'memberCount', function(err, count) { + Groups.getMemberCount = function (groupName, callback) { + db.getObjectField('group:' + groupName, 'memberCount', function (err, count) { if (err) { return callback(err); } @@ -302,8 +432,8 @@ module.exports = function(Groups) { }); }; - Groups.isMemberOfGroupList = function(uid, groupListKey, callback) { - db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) { + Groups.isMemberOfGroupList = function (uid, groupListKey, callback) { + db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) { if (err) { return callback(err); } @@ -312,7 +442,7 @@ module.exports = function(Groups) { return callback(null, false); } - Groups.isMemberOfGroups(uid, groupNames, function(err, isMembers) { + Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) { if (err) { return callback(err); } @@ -322,12 +452,12 @@ module.exports = function(Groups) { }); }; - Groups.isMemberOfGroupsList = function(uid, groupListKeys, callback) { - var sets = groupListKeys.map(function(groupName) { + Groups.isMemberOfGroupsList = function (uid, groupListKeys, callback) { + var sets = groupListKeys.map(function (groupName) { return 'group:' + groupName + ':members'; }); - db.getSortedSetsMembers(sets, function(err, members) { + db.getSortedSetsMembers(sets, function (err, members) { if (err) { return callback(err); } @@ -335,19 +465,19 @@ module.exports = function(Groups) { var uniqueGroups = _.unique(_.flatten(members)); uniqueGroups = Groups.internals.removeEphemeralGroups(uniqueGroups); - Groups.isMemberOfGroups(uid, uniqueGroups, function(err, isMembers) { + Groups.isMemberOfGroups(uid, uniqueGroups, function (err, isMembers) { if (err) { return callback(err); } var map = {}; - uniqueGroups.forEach(function(groupName, index) { + uniqueGroups.forEach(function (groupName, index) { map[groupName] = isMembers[index]; }); - var result = members.map(function(groupNames) { - for (var i=0; i b.slug; - }).sort(function(a, b) { + }).sort(function (a, b) { return b.memberCount - a.memberCount; }); break; case 'date': - groups = groups.sort(function(a, b) { + groups = groups.sort(function (a, b) { return b.createtime - a.createtime; }); break; case 'alpha': // intentional fall-through default: - groups = groups.sort(function(a, b) { + groups = groups.sort(function (a, b) { return a.slug > b.slug ? 1 : -1; }); } @@ -63,7 +63,7 @@ module.exports = function(Groups) { next(null, groups); }; - Groups.searchMembers = function(data, callback) { + Groups.searchMembers = function (data, callback) { function findUids(query, searchBy, callback) { if (!query) { @@ -73,15 +73,15 @@ module.exports = function(Groups) { query = query.toLowerCase(); async.waterfall([ - function(next) { + function (next) { Groups.getMembers(data.groupName, 0, -1, next); }, - function(members, next) { + function (members, next) { user.getUsersFields(members, ['uid'].concat([searchBy]), next); }, - function(users, next) { + function (users, next) { var uids = []; - for(var i=0; i
            +
            diff --git a/src/views/admin/advanced/post-cache.tpl b/src/views/admin/advanced/post-cache.tpl deleted file mode 100644 index 7f688327e4..0000000000 --- a/src/views/admin/advanced/post-cache.tpl +++ /dev/null @@ -1,27 +0,0 @@ - -
            -
            -
            -
            Post Cache
            -
            - -
            - {cache.itemCount}
            - -
            - {cache.avgPostSize}
            - -
            - {cache.length} / {cache.max}
            - -
            -
            - {cache.percentFull}% Full -
            -
            - -
            -
            -
            - -
            diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl index ac6781c66b..07c51bcb73 100644 --- a/src/views/admin/extend/plugins.tpl +++ b/src/views/admin/extend/plugins.tpl @@ -1,7 +1,12 @@
            @@ -15,6 +20,15 @@
        +
        +
          +
          +
          +
            +
            +
            +
              +
                @@ -22,9 +36,6 @@
              -
              -
                -
                diff --git a/src/views/admin/general/dashboard.tpl b/src/views/admin/general/dashboard.tpl index 6ea841352a..4f7775b226 100644 --- a/src/views/admin/general/dashboard.tpl +++ b/src/views/admin/general/dashboard.tpl @@ -95,11 +95,10 @@
                System Control

                - - +

                - Maintenance Mode + Maintenance Mode


                @@ -107,6 +106,13 @@
                +
                +
                Active Users
                +
                +
                +
                +
                +
                Anonymous vs Registered Users
                @@ -146,13 +152,5 @@
                - - -
                -
                Active Users
                -
                -
                -
                -
                \ No newline at end of file diff --git a/src/views/admin/general/sounds.tpl b/src/views/admin/general/sounds.tpl index dfdbae512d..1154f4ec75 100644 --- a/src/views/admin/general/sounds.tpl +++ b/src/views/admin/general/sounds.tpl @@ -1,5 +1,5 @@
                -
                +
                Notifications
                @@ -8,14 +8,14 @@
                - +
                @@ -28,14 +28,14 @@
                - +
                @@ -43,40 +43,29 @@
                - +
                + +
                + + + +
                -
                -
                -
                -
                - - - -
                -
                -
                -
                - - \ No newline at end of file + \ No newline at end of file diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl index 19b3b065cb..ccf93afa3d 100644 --- a/src/views/admin/header.tpl +++ b/src/views/admin/header.tpl @@ -4,7 +4,6 @@ {title} - @@ -15,7 +14,9 @@ var app = { template: "{template.name}", user: JSON.parse('{{userJSON}}'), - config: JSON.parse(decodeURIComponent("{{adminConfigJSON}}")) + config: JSON.parse(decodeURIComponent("{{adminConfigJSON}}")), + flags: {}, + inAdmin: true }; @@ -27,43 +28,23 @@ - - - - - - - + - +
                \ No newline at end of file diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl index d5807ce88e..a14348ba42 100644 --- a/src/views/admin/manage/flags.tpl +++ b/src/views/admin/manage/flags.tpl @@ -1,34 +1,64 @@
                -
                + +
                + +
                +
                +
                +
                +

                + +

                +
                + +
                +
                + + + +
                +
                - -
                -
                - -
                +
                No flagged posts! @@ -36,70 +66,130 @@ -
                -
                -
                -
                - - - +
                + + + [[topic:flag_manage_state_{../flagData.state}open]] +  [[topic:flag_manage_title, {posts.category.name}]] + + +
                -
                - This post has been flagged {posts.flags} time(s): -
                - -
                -
                - - +
                +
                +
                +
                +
                + + + + +
                {../user.icon:text}
                + +
                + + + {../user.username} + +
                +

                {posts.content}

                +
                + + + Posted in {posts.category.name}, • + Read More + + +
                +
                +
                + This post has been flagged {posts.flags} time(s): +
                + +
                +
                + + +
                +
                +
                +
                +
                +
                +
                +
                + + +
                +
                + + +
                +
                + + +
                + +
                +
                +
                +
                [[topic:flag_manage_history]]
                + +
                [[topic:flag_manage_no_history]]
                + +
                  + +
                • +
                  + + + +
                  {../user.icon:text}
                  + + [[topic:flag_manage_history_{posts.flagData.history.type}, {posts.flagData.history.label}]] +
                • + +
                + +
                +
                -
                -
                -
                - -
                -
                -
                Flags Control Panel
                -
                -
                - -
                +
                diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl index d293d8cfa4..486686996d 100644 --- a/src/views/admin/manage/registration.tpl +++ b/src/views/admin/manage/registration.tpl @@ -5,7 +5,7 @@

                There are no users in the registration queue.
                - To enable this feature, go to Settings -> User -> Authentication and set + To enable this feature, go to Settings → User → Authentication and set Registration Type to "Admin Approval".

                @@ -13,8 +13,11 @@ Name Email - IP - Time + IP + Time + + {customHeaders.label} + @@ -35,17 +38,31 @@ {users.email} - + {users.ip} + +
                + + + +
                {users.ipMatch.icon:text}
                + + {users.ipMatch.username} + - + + + + {users.customRows.value} + +
                diff --git a/src/views/admin/manage/tags.tpl b/src/views/admin/manage/tags.tpl index bc47f93731..d60cdc4147 100644 --- a/src/views/admin/manage/tags.tpl +++ b/src/views/admin/manage/tags.tpl @@ -6,7 +6,7 @@ Your forum does not have any topics with tags yet. - +
                @@ -32,9 +32,10 @@
                -
                Modify Tag
                +
                Create & Modify Tags

                Select tags via clicking and/or dragging, use shift to select multiple.

                +
                @@ -48,4 +49,26 @@
                + +
                diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index cfdc7bb72c..aa74b72ed5 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -1,5 +1,6 @@

              @@ -47,100 +60,48 @@ User not found!
              + 3 months 6 months 12 months - -
                + + + + + + + + + + + + + -
                -
                - - - -
                {users.icon:text}
                - -
                - - - Not Validated - - - Admin - Banned -
                -
                +
                + + + - {users.username} ({users.uid})
                - - {users.email} - - - joined
                - login
                - posts {users.postcount} - - -
                {users.flags}
                - - + + + + + + + + - +
                uidusernameemailpostcountreputationflagsjoinedlast onlinebanned
                {users.uid} {users.username} + + + + {users.email}{users.postcount}{users.reputation}{users.flags}0
                - - - - - - - -
                -
                -
                Users Control Panel
                -
                - - Download CSV
                diff --git a/src/views/admin/partials/categories/groups.tpl b/src/views/admin/partials/categories/groups.tpl index aa2da68cfc..495aa9589f 100644 --- a/src/views/admin/partials/categories/groups.tpl +++ b/src/views/admin/partials/categories/groups.tpl @@ -5,12 +5,16 @@ Privileges {groups.displayName} - \ No newline at end of file + diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index b5605c0935..497fa71ac0 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -58,6 +58,7 @@
                +
                diff --git a/src/views/admin/partials/categories/users.tpl b/src/views/admin/partials/categories/users.tpl index 40991fcf3a..772053d5b0 100644 --- a/src/views/admin/partials/categories/users.tpl +++ b/src/views/admin/partials/categories/users.tpl @@ -5,14 +5,18 @@ Privileges {users.username} - \ No newline at end of file + diff --git a/src/views/admin/partials/create_user_modal.tpl b/src/views/admin/partials/create_user_modal.tpl new file mode 100644 index 0000000000..b065479b06 --- /dev/null +++ b/src/views/admin/partials/create_user_modal.tpl @@ -0,0 +1,21 @@ +
                +
                +
                + + +
                +
                + + +
                + +
                + + +
                + +
                + + +
                +
                diff --git a/src/views/admin/partials/installed_plugin_item.tpl b/src/views/admin/partials/installed_plugin_item.tpl index b0399df5e2..3c95bcc2c1 100644 --- a/src/views/admin/partials/installed_plugin_item.tpl +++ b/src/views/admin/partials/installed_plugin_item.tpl @@ -1,5 +1,5 @@ -
              • +
              • Themes diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 8d0f0f853d..712155a30b 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -16,7 +16,7 @@
                + + \ No newline at end of file diff --git a/src/views/admin/partials/temporary-ban.tpl b/src/views/admin/partials/temporary-ban.tpl new file mode 100644 index 0000000000..da8d02c79f --- /dev/null +++ b/src/views/admin/partials/temporary-ban.tpl @@ -0,0 +1,32 @@ + +
                +
                +
                + + +
                +
                +
                +
                + + +
                +
                +
                +
                +
                +
                + + +    + + +
                +
                +
                +

                + Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban. +

                +
                +
                + \ No newline at end of file diff --git a/src/views/admin/partials/widget-settings.tpl b/src/views/admin/partials/widget-settings.tpl new file mode 100644 index 0000000000..dfb7f8f4bc --- /dev/null +++ b/src/views/admin/partials/widget-settings.tpl @@ -0,0 +1,18 @@ +
                + +
                + + + + +
                + +
                + +
                + +
                + +
                + +
                diff --git a/src/views/admin/settings/advanced.tpl b/src/views/admin/settings/advanced.tpl index 3fd617cf83..f845d06a1c 100644 --- a/src/views/admin/settings/advanced.tpl +++ b/src/views/admin/settings/advanced.tpl @@ -88,7 +88,7 @@

                Lowering this value decreases wait times for page loads, but will also show the - "excessive load" message to more users. (Reload required) + "excessive load" message to more users. (Restart required)

                @@ -96,7 +96,7 @@

                Lowering this value causes NodeBB to become more sensitive to spikes in load, but - may also cause the check to become too sensitive. (Reload required) + may also cause the check to become too sensitive. (Restart required)

                diff --git a/src/views/admin/settings/chat.tpl b/src/views/admin/settings/chat.tpl index 3f0fb1e222..5f4d0b8315 100644 --- a/src/views/admin/settings/chat.tpl +++ b/src/views/admin/settings/chat.tpl @@ -12,9 +12,6 @@ -
                - Chat Message Inbox Size
                -
                @@ -25,6 +22,12 @@
                + + +
                + + +
                diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl index 683a13f4cc..3b65cbc0a4 100644 --- a/src/views/admin/settings/email.tpl +++ b/src/views/admin/settings/email.tpl @@ -34,14 +34,14 @@
                -
                +

                Enter the full email address here, especially if you are using a Google Apps managed domain.

                -
                +
                @@ -67,12 +67,14 @@
                Email Testing
                -
                -
                +
                + + +

                The test email will be sent to the currently logged in user's email address. @@ -90,6 +92,17 @@ Disable subscriber notification emails

                + +
                + + +

                + Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). + Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
                + The approximate server time is:
                + The next daily digest is scheduled to be sent +

                +
                diff --git a/src/views/admin/settings/footer.tpl b/src/views/admin/settings/footer.tpl index fc80967c47..af46d0ad59 100644 --- a/src/views/admin/settings/footer.tpl +++ b/src/views/admin/settings/footer.tpl @@ -6,7 +6,7 @@ diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index fa41b38f61..79cba3caf7 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -50,6 +50,7 @@ +
                @@ -61,6 +62,17 @@
                + +
                + +
                + + + + + +
                +
                diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl index 13b5121b24..8e1ee54b3f 100644 --- a/src/views/admin/settings/group.tpl +++ b/src/views/admin/settings/group.tpl @@ -28,6 +28,9 @@

                If enabled, users can create groups (Default: disabled)

                + + + diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index 590f0e18cd..f58f524aca 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -48,6 +48,14 @@ +
                + + +
                +
                + + +
                @@ -83,7 +91,8 @@
                @@ -100,6 +109,10 @@
                +
                + + +
                diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl index 76b053332b..80fbe3074d 100644 --- a/src/views/admin/settings/uploads.tpl +++ b/src/views/admin/settings/uploads.tpl @@ -20,16 +20,20 @@ -
                - -
                -
                - + + +

                + (in pixels, default: 760 pixels, set to 0 to disable) +

                +
                + +
                + +

                + (in kilobytes, default: 2048 KiB) +

                @@ -41,7 +45,7 @@
                - +
                @@ -56,8 +60,6 @@
                - -
                Profile Avatars @@ -91,16 +93,32 @@
                +

                + (in pixels, default: 128 pixels) +

                +

                + (in kilobytes, default: 256 KiB) +

                +

                + (in kilobytes, default: 2,048 KiB) +

                +
                + +
                +
                @@ -119,4 +137,4 @@
                - \ No newline at end of file + diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 55c54149c6..aecdd1a0cb 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -32,26 +32,6 @@ - -
                - - -
                - -
                - - -

                - 0 for no restriction. Admins get infinite invitations
                - Only applicable for "Invite Only" -

                -
                @@ -139,6 +119,33 @@
                User Registration
                +
                + + +

                + Normal - Users can register from the /register page.
                + Admin Approval - User registrations are placed in an approval queue for administrators.
                + Admin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
                + Invite Only - Users can invite others from the users page.
                + Admin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
                + No registration - No user registration.
                +

                +
                +
                + + +

                + 0 for no restriction. Admins get infinite invitations
                + Only applicable for "Invite Only" +

                +
                @@ -254,13 +261,6 @@
                -
                - -
                -
                diff --git a/src/views/emails/notif_chat.tpl b/src/views/emails/notif_chat.tpl index a0669f3bac..f1d5a5fc42 100644 --- a/src/views/emails/notif_chat.tpl +++ b/src/views/emails/notif_chat.tpl @@ -3,7 +3,7 @@

                {summary}:

                {message.content}
                -[[email:notif.chat.cta]] +[[email:notif.chat.cta]] diff --git a/src/views/install/index.tpl b/src/views/install/index.tpl index 75d6c1e16a..cb5b69dbcb 100644 --- a/src/views/install/index.tpl +++ b/src/views/install/index.tpl @@ -7,7 +7,7 @@ NodeBB Web Installer - + @@ -72,9 +72,9 @@
                - +
                -
                +
                @@ -119,7 +119,7 @@

                Congratulations! Your NodeBB has been set-up.

                - +

                @@ -136,7 +136,7 @@
                - + diff --git a/src/views/partials/data/topic.tpl b/src/views/partials/data/topic.tpl index d7fd1deb50..ea6fea2a3c 100644 --- a/src/views/partials/data/topic.tpl +++ b/src/views/partials/data/topic.tpl @@ -1 +1 @@ -data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemscope itemtype="http://schema.org/Comment" \ No newline at end of file +data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-timestamp="{posts.timestamp}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemscope itemtype="http://schema.org/Comment" \ No newline at end of file diff --git a/src/views/partials/fontawesome.tpl b/src/views/partials/fontawesome.tpl index a179bfdd49..f6a84268bc 100644 --- a/src/views/partials/fontawesome.tpl +++ b/src/views/partials/fontawesome.tpl @@ -4,7 +4,737 @@
                - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

                For a full list of icons, please consult: diff --git a/src/views/partials/requirejs-config.tpl b/src/views/partials/requirejs-config.tpl deleted file mode 100644 index 9ad5ea48fc..0000000000 --- a/src/views/partials/requirejs-config.tpl +++ /dev/null @@ -1,13 +0,0 @@ - \ No newline at end of file diff --git a/src/webserver.js b/src/webserver.js index 057c9a5e8a..3436f575c8 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -1,24 +1,35 @@ 'use strict'; -var path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - express = require('express'), - app = express(), - server, - winston = require('winston'), - async = require('async'), +var fs = require('fs'); +var path = require('path'); +var nconf = require('nconf'); +var express = require('express'); +var app = express(); +var server; +var winston = require('winston'); +var async = require('async'); +var flash = require('connect-flash'); +var compression = require('compression'); +var bodyParser = require('body-parser'); +var cookieParser = require('cookie-parser'); +var session = require('express-session'); +var useragent = require('express-useragent'); +var favicon = require('serve-favicon'); - emailer = require('./emailer'), - meta = require('./meta'), - logger = require('./logger'), - plugins = require('./plugins'), - middleware = require('./middleware'), - routes = require('./routes'), - emitter = require('./emitter'), +var db = require('./database'); +var file = require('./file'); +var emailer = require('./emailer'); +var meta = require('./meta'); +var languages = require('./languages'); +var logger = require('./logger'); +var plugins = require('./plugins'); +var routes = require('./routes'); +var auth = require('./routes/authentication'); +var emitter = require('./emitter'); +var templates = require('templates.js'); - helpers = require('../public/src/modules/helpers'); +var helpers = require('../public/src/modules/helpers'); if (nconf.get('ssl')) { server = require('https').createServer({ @@ -31,7 +42,7 @@ if (nconf.get('ssl')) { module.exports.server = server; -server.on('error', function(err) { +server.on('error', function (err) { winston.error(err); if (err.code === 'EADDRINUSE') { winston.error('NodeBB address in use, exiting...'); @@ -42,22 +53,22 @@ server.on('error', function(err) { }); -module.exports.listen = function() { +module.exports.listen = function () { emailer.registerApp(app); - middleware = middleware(app); + setupExpressApp(app); helpers.register(); logger.init(app); - emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() { + emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function () { winston.info('NodeBB Ready'); emitter.emit('nodebb:ready'); listen(); }); - initializeNodeBB(function(err) { + initializeNodeBB(function (err) { if (err) { winston.error(err); process.exit(); @@ -70,8 +81,85 @@ module.exports.listen = function() { }); }; +function setupExpressApp(app) { + var middleware = require('./middleware'); + + var relativePath = nconf.get('relative_path'); + + app.engine('tpl', templates.__express); + app.set('view engine', 'tpl'); + app.set('views', nconf.get('views_dir')); + app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0); + app.use(flash()); + + app.enable('view cache'); + + if (global.env !== 'development') { + app.enable('cache'); + app.enable('minification'); + } + + app.use(compression()); + + setupFavicon(app); + + app.use(relativePath + '/apple-touch-icon', middleware.routeTouchIcon); + + app.use(bodyParser.urlencoded({extended: true})); + app.use(bodyParser.json()); + app.use(cookieParser()); + app.use(useragent.express()); + + app.use(session({ + store: db.sessionStore, + secret: nconf.get('secret'), + key: nconf.get('sessionKey'), + cookie: setupCookie(), + resave: true, + saveUninitialized: true + })); + + app.use(middleware.addHeaders); + app.use(middleware.processRender); + auth.initialize(app, middleware); + + var toobusy = require('toobusy-js'); + toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100); + toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500); +} + +function setupFavicon(app) { + var faviconPath = path.join(nconf.get('base_dir'), 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico'); + if (file.existsSync(faviconPath)) { + app.use(nconf.get('relative_path'), favicon(faviconPath)); + } +} + +function setupCookie() { + var cookie = { + maxAge: 1000 * 60 * 60 * 24 * (parseInt(meta.config.loginDays, 10) || 14) + }; + + if (meta.config.cookieDomain) { + cookie.domain = meta.config.cookieDomain; + } + + if (nconf.get('secure')) { + cookie.secure = true; + } + + var relativePath = nconf.get('relative_path'); + if (relativePath !== '') { + cookie.path = relativePath; + } + + return cookie; +} + function initializeNodeBB(callback) { - var skipJS, skipLess, fromFile = nconf.get('from-file') || ''; + var skipJS; + var fromFile = nconf.get('from-file') || ''; + var middleware = require('./middleware'); if (fromFile.match('js')) { winston.info('[minifier] Minifying client-side JS skipped'); @@ -79,46 +167,41 @@ function initializeNodeBB(callback) { } async.waterfall([ - async.apply(cacheStaticFiles), async.apply(meta.themes.setupPaths), - function(next) { + function (next) { plugins.init(app, middleware, next); }, - function(next) { + async.apply(plugins.fireHook, 'static:assets.prepare', {}), + async.apply(meta.js.bridgeModules, app), + function (next) { async.series([ async.apply(meta.templates.compile), async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'nodebb.min.js'), async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'acp.min.js'), async.apply(meta.css.minify), async.apply(meta.sounds.init), + async.apply(languages.init), async.apply(meta.blacklist.load) ], next); }, - function(results, next) { + function (results, next) { plugins.fireHook('static:app.preload', { app: app, middleware: middleware }, next); }, - function(next) { - routes(app, middleware); + async.apply(plugins.fireHook, 'filter:hotswap.prepare', []), + function (hotswapIds, next) { + routes(app, middleware, hotswapIds); next(); } ], callback); } -function cacheStaticFiles(callback) { - if (global.env === 'development') { - return callback(); - } - - app.enable('cache'); - app.enable('minification'); - callback(); -} - -function listen(callback) { +function listen() { var port = parseInt(nconf.get('port'), 10); + var isSocket = isNaN(port); + var socketPath = isSocket ? nconf.get('port') : ''; if (Array.isArray(port)) { if (!port.length) { @@ -144,18 +227,18 @@ function listen(callback) { winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md'); } - var isSocket = isNaN(port), - args = isSocket ? [port] : [port, nconf.get('bind_address')], - bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port, - oldUmask; - args.push(function(err) { + var args = isSocket ? [socketPath] : [port, nconf.get('bind_address')]; + var bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port; + var oldUmask; + + args.push(function (err) { if (err) { winston.info('[startup] NodeBB was unable to listen on: ' + bind_address); process.exit(); } - winston.info('NodeBB is now listening on: ' + (isSocket ? port : bind_address)); + winston.info('NodeBB is now listening on: ' + (isSocket ? socketPath : bind_address)); if (oldUmask) { process.umask(oldUmask); } @@ -164,11 +247,11 @@ function listen(callback) { // Alter umask if necessary if (isSocket) { oldUmask = process.umask('0000'); - module.exports.testSocket(port, function(err) { + module.exports.testSocket(socketPath, function (err) { if (!err) { server.listen.apply(server, args); } else { - winston.error('[startup] NodeBB was unable to secure domain socket access (' + port + ')'); + winston.error('[startup] NodeBB was unable to secure domain socket access (' + socketPath + ')'); winston.error('[startup] ' + err.message); process.exit(); } @@ -178,15 +261,15 @@ function listen(callback) { } } -module.exports.testSocket = function(socketPath, callback) { +module.exports.testSocket = function (socketPath, callback) { if (typeof socketPath !== 'string') { return callback(new Error('invalid socket path : ' + socketPath)); } var net = require('net'); var file = require('./file'); async.series([ - function(next) { - file.exists(socketPath, function(exists) { + function (next) { + file.exists(socketPath, function (exists) { if (exists) { next(); } else { @@ -194,12 +277,12 @@ module.exports.testSocket = function(socketPath, callback) { } }); }, - function(next) { + function (next) { var testSocket = new net.Socket(); - testSocket.on('error', function(err) { + testSocket.on('error', function (err) { next(err.code !== 'ECONNREFUSED' ? err : null); }); - testSocket.connect({ path: socketPath }, function() { + testSocket.connect({ path: socketPath }, function () { // Something's listening here, abort callback(new Error('port-in-use')); }); diff --git a/src/widgets/admin.js b/src/widgets/admin.js index 110f523e21..fc380804c6 100644 --- a/src/widgets/admin.js +++ b/src/widgets/admin.js @@ -1,14 +1,15 @@ "use strict"; - -var async = require('async'), - plugins = require('../plugins'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var plugins = require('../plugins'); var admin = {}; -admin.get = function(callback) { +admin.get = function (callback) { async.parallel({ - areas: function(next) { + areas: function (next) { var defaultAreas = [ { name: 'Global Sidebar', template: 'global', location: 'sidebar' }, { name: 'Global Header', template: 'global', location: 'header' }, @@ -20,36 +21,36 @@ admin.get = function(callback) { plugins.fireHook('filter:widgets.getAreas', defaultAreas, next); }, - widgets: function(next) { + widgets: function (next) { plugins.fireHook('filter:widgets.getWidgets', [], next); + }, + adminTemplate: function (next) { + fs.readFile(path.resolve(__dirname, '../../public/templates/admin/partials/widget-settings.tpl'), 'utf8', next); } - }, function(err, widgetData) { + }, function (err, widgetData) { if (err) { return callback(err); } widgetData.areas.push({ name: 'Draft Zone', template: 'global', location: 'drafts' }); - async.each(widgetData.areas, function(area, next) { - require('./index').getArea(area.template, area.location, function(err, areaData) { + async.each(widgetData.areas, function (area, next) { + require('./index').getArea(area.template, area.location, function (err, areaData) { area.data = areaData; next(err); }); - - }, function(err) { + }, function (err) { if (err) { return callback(err); } - for (var w in widgetData.widgets) { - if (widgetData.widgets.hasOwnProperty(w)) { - // if this gets anymore complicated, it needs to be a template - widgetData.widgets[w].content += "

                "; - } - } + + widgetData.widgets.forEach(function (w) { + w.content += widgetData.adminTemplate; + }); var templates = [], list = {}, index = 0; - widgetData.areas.forEach(function(area) { + widgetData.areas.forEach(function (area) { if (typeof list[area.template] === 'undefined') { list[area.template] = index; templates.push({ diff --git a/src/widgets/index.js b/src/widgets/index.js index c1421b44b6..eac58e46e3 100644 --- a/src/widgets/index.js +++ b/src/widgets/index.js @@ -1,36 +1,39 @@ "use strict"; -var async = require('async'), - winston = require('winston'), - templates = require('templates.js'), +var async = require('async'); +var winston = require('winston'); +var templates = require('templates.js'); - plugins = require('../plugins'), - translator = require('../../public/src/modules/translator'), - db = require('../database'); +var plugins = require('../plugins'); +var translator = require('../../public/src/modules/translator'); +var db = require('../database'); var widgets = {}; -widgets.render = function(uid, area, req, res, callback) { +widgets.render = function (uid, area, req, res, callback) { if (!area.locations || !area.template) { return callback(new Error('[[error:invalid-data]]')); } - widgets.getAreas(['global', area.template], area.locations, function(err, data) { + widgets.getAreas(['global', area.template], area.locations, function (err, data) { if (err) { return callback(err); } var widgetsByLocation = {}; - async.map(area.locations, function(location, done) { + async.map(area.locations, function (location, done) { widgetsByLocation[location] = data.global[location].concat(data[area.template][location]); if (!widgetsByLocation[location].length) { return done(null, {location: location, widgets: []}); } - async.map(widgetsByLocation[location], function(widget, next) { - if (!widget || !widget.data || (!!widget.data['hide-registered'] && uid !== 0) || (!!widget.data['hide-guests'] && uid === 0)) { + async.map(widgetsByLocation[location], function (widget, next) { + if (!widget || !widget.data || + (!!widget.data['hide-registered'] && uid !== 0) || + (!!widget.data['hide-guests'] && uid === 0) || + (!!widget.data['hide-mobile'] && area.isMobile)) { return next(); } @@ -40,8 +43,8 @@ widgets.render = function(uid, area, req, res, callback) { data: widget.data, req: req, res: res - }, function(err, html) { - if (err) { + }, function (err, html) { + if (err || html === null) { return next(err); } @@ -50,7 +53,7 @@ widgets.render = function(uid, area, req, res, callback) { } if (widget.data.container && widget.data.container.match('{body}')) { - translator.translate(widget.data.title, function(title) { + translator.translate(widget.data.title, function (title) { html = templates.parse(widget.data.container, { title: title, body: html @@ -62,27 +65,27 @@ widgets.render = function(uid, area, req, res, callback) { next(null, {html: html}); } }); - }, function(err, result) { + }, function (err, result) { done(err, {location: location, widgets: result.filter(Boolean)}); }); }, callback); }); }; -widgets.getAreas = function(templates, locations, callback) { - var keys = templates.map(function(tpl) { +widgets.getAreas = function (templates, locations, callback) { + var keys = templates.map(function (tpl) { return 'widgets:' + tpl; }); - db.getObjectsFields(keys, locations, function(err, data) { + db.getObjectsFields(keys, locations, function (err, data) { if (err) { return callback(err); } var returnData = {}; - templates.forEach(function(template, index) { + templates.forEach(function (template, index) { returnData[template] = returnData[template] || {}; - locations.forEach(function(location) { + locations.forEach(function (location) { if (data && data[index] && data[index][location]) { try { returnData[template][location] = JSON.parse(data[index][location]); @@ -100,8 +103,8 @@ widgets.getAreas = function(templates, locations, callback) { }); }; -widgets.getArea = function(template, location, callback) { - db.getObjectField('widgets:' + template, location, function(err, result) { +widgets.getArea = function (template, location, callback) { + db.getObjectField('widgets:' + template, location, function (err, result) { if (err) { return callback(err); } @@ -118,7 +121,7 @@ widgets.getArea = function(template, location, callback) { }); }; -widgets.setArea = function(area, callback) { +widgets.setArea = function (area, callback) { if (!area.location || !area.template) { return callback(new Error('Missing location and template data')); } @@ -126,21 +129,29 @@ widgets.setArea = function(area, callback) { db.setObjectField('widgets:' + area.template, area.location, JSON.stringify(area.widgets), callback); }; -widgets.reset = function(callback) { +widgets.reset = function (callback) { var defaultAreas = [ { name: 'Draft Zone', template: 'global', location: 'header' }, { name: 'Draft Zone', template: 'global', location: 'footer' }, { name: 'Draft Zone', template: 'global', location: 'sidebar' } ]; - plugins.fireHook('filter:widgets.getAreas', defaultAreas, function(err, areas) { + async.parallel({ + areas: function (next) { + plugins.fireHook('filter:widgets.getAreas', defaultAreas, next); + }, + drafts: function (next) { + widgets.getArea('global', 'drafts', next); + } + }, function (err, results) { if (err) { return callback(err); } - var drafts = []; - async.each(areas, function(area, next) { - widgets.getArea(area.template, area.location, function(err, areaData) { + var drafts = results.drafts || []; + + async.each(results.areas, function (area, next) { + widgets.getArea(area.template, area.location, function (err, areaData) { if (err) { return next(err); } @@ -149,7 +160,7 @@ widgets.reset = function(callback) { area.widgets = []; widgets.setArea(area, next); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } diff --git a/tests/.jshintrc b/test/.jshintrc similarity index 100% rename from tests/.jshintrc rename to test/.jshintrc diff --git a/test/categories.js b/test/categories.js new file mode 100644 index 0000000000..86c158eb3e --- /dev/null +++ b/test/categories.js @@ -0,0 +1,179 @@ +'use strict'; +/*global require, after, before*/ + + +var async = require('async'); +var assert = require('assert'); + +var db = require('./mocks/databasemock'); +var Categories = require('../src/categories'); +var Topics = require('../src/topics'); +var User = require('../src/user'); + +describe('Categories', function () { + var categoryObj; + var posterUid; + + before(function (done) { + User.create({username: 'poster'}, function (err, _posterUid) { + if (err) { + return done(err); + } + + posterUid = _posterUid; + + done(); + }); + }); + + describe('.create', function () { + it('should create a new category', function (done) { + + Categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + icon: 'fa-check', + blockclass: 'category-blue', + order: '5' + }, function (err, category) { + assert.equal(err, null); + + categoryObj = category; + done.apply(this, arguments); + }); + }); + }); + + describe('.getCategoryById', function () { + it('should retrieve a newly created category by its ID', function (done) { + Categories.getCategoryById({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':tids', + reverse: true, + start: 0, + stop: -1, + uid: 0 + }, function (err, categoryData) { + assert.equal(err, null); + + assert(categoryData); + assert.equal(categoryObj.name, categoryData.name); + assert.equal(categoryObj.description, categoryData.description); + + done(); + }); + }); + }); + + describe('Categories.getRecentTopicReplies', function () { + it('should not throw', function (done) { + Categories.getCategoryById({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':tids', + reverse: true, + start: 0, + stop: -1, + uid: 0 + }, function (err, categoryData) { + assert.ifError(err); + Categories.getRecentTopicReplies(categoryData, 0, function (err) { + assert.ifError(err); + done(); + }); + }); + }); + }); + + describe('.getCategoryTopics', function () { + it('should return a list of topics', function (done) { + Categories.getCategoryTopics({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':tids', + reverse: true, + start: 0, + stop: 10, + uid: 0 + }, function (err, result) { + assert.equal(err, null); + + assert(Array.isArray(result.topics)); + assert(result.topics.every(function (topic) { + return topic instanceof Object; + })); + + done(); + }); + }); + + it('should return a list of topics by a specific user', function (done) { + Categories.getCategoryTopics({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':uid:' + 1 + ':tids', + reverse: true, + start: 0, + stop: 10, + uid: 0, + targetUid: 1 + }, function (err, result) { + assert.equal(err, null); + assert(Array.isArray(result.topics)); + assert(result.topics.every(function (topic) { + return topic instanceof Object && topic.uid === '1'; + })); + + done(); + }); + }); + }); + + describe('Categories.moveRecentReplies', function () { + var moveCid; + var moveTid; + before(function (done) { + async.parallel({ + category: function (next) { + Categories.create({ + name: 'Test Category 2', + description: 'Test category created by testing script' + }, next); + }, + topic: function (next) { + Topics.post({ + uid: posterUid, + cid: categoryObj.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }, next); + } + }, function (err, results) { + if (err) { + return done(err); + } + moveCid = results.category.cid; + moveTid = results.topic.topicData.tid; + Topics.reply({uid: posterUid, content: 'test post', tid: moveTid}, function (err) { + done(err); + }); + }); + }); + + it('should move posts from one category to another', function (done) { + Categories.moveRecentReplies(moveTid, categoryObj.cid, moveCid, function (err) { + assert.ifError(err); + db.getSortedSetRange('cid:' + categoryObj.cid + ':pids', 0, -1, function (err, pids) { + assert.ifError(err); + assert.equal(pids.length, 0); + db.getSortedSetRange('cid:' + moveCid + ':pids', 0, -1, function (err, pids) { + assert.ifError(err); + assert.equal(pids.length, 2); + done(); + }); + }); + }); + }); + }); + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/tests/database.js b/test/database.js similarity index 74% rename from tests/database.js rename to test/database.js index 3f8c119002..2750973a0c 100644 --- a/tests/database.js +++ b/test/database.js @@ -5,9 +5,9 @@ var assert = require('assert'), db = require('./mocks/databasemock'); -describe('Test database', function() { - it('should work', function(){ - assert.doesNotThrow(function(){ +describe('Test database', function () { + it('should work', function (){ + assert.doesNotThrow(function (){ var db = require('./mocks/databasemock'); }); }); diff --git a/tests/database/hash.js b/test/database/hash.js similarity index 61% rename from tests/database/hash.js rename to test/database/hash.js index 469172ce70..b1fd87390d 100644 --- a/tests/database/hash.js +++ b/test/database/hash.js @@ -5,20 +5,20 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Hash methods', function() { +describe('Hash methods', function () { var testData = { name: 'baris', lastname: 'usakli', age: 99 }; - beforeEach(function(done) { + beforeEach(function (done) { db.setObject('hashTestObject', testData, done); }); - describe('setObject()', function() { - it('should create a object', function(done) { - db.setObject('testObject1', {foo: 'baris', bar: 99}, function(err) { + describe('setObject()', function () { + it('should create a object', function (done) { + db.setObject('testObject1', {foo: 'baris', bar: 99}, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -26,17 +26,17 @@ describe('Hash methods', function() { }); }); - describe('setObjectField()', function() { - it('should create a new object with field', function(done) { - db.setObjectField('testObject2', 'name', 'ginger', function(err) { + describe('setObjectField()', function () { + it('should create a new object with field', function (done) { + db.setObjectField('testObject2', 'name', 'ginger', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should add a new field to an object', function(done) { - db.setObjectField('testObject2', 'type', 'cat', function(err) { + it('should add a new field to an object', function (done) { + db.setObjectField('testObject2', 'type', 'cat', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -44,9 +44,9 @@ describe('Hash methods', function() { }); }); - describe('getObject()', function() { - it('should return falsy if object does not exist', function(done) { - db.getObject('doesnotexist', function(err, data) { + describe('getObject()', function () { + it('should return falsy if object does not exist', function (done) { + db.getObject('doesnotexist', function (err, data) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!data, false); @@ -54,8 +54,8 @@ describe('Hash methods', function() { }); }); - it('should retrieve an object', function(done) { - db.getObject('hashTestObject', function(err, data) { + it('should retrieve an object', function (done) { + db.getObject('hashTestObject', function (err, data) { assert.equal(err, null); assert.equal(data.name, testData.name); assert.equal(data.age, testData.age); @@ -65,16 +65,16 @@ describe('Hash methods', function() { }); }); - describe('getObjects()', function() { - before(function(done) { + describe('getObjects()', function () { + before(function (done) { async.parallel([ async.apply(db.setObject, 'testObject4', {name: 'baris'}), async.apply(db.setObjectField, 'testObject5', 'name', 'ginger') ], done); }); - it('should return 3 objects with correct data', function(done) { - db.getObjects(['testObject4', 'testObject5', 'doesnotexist'], function(err, objects) { + it('should return 3 objects with correct data', function (done) { + db.getObjects(['testObject4', 'testObject5', 'doesnotexist'], function (err, objects) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(objects) && objects.length === 3, true); @@ -86,9 +86,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectField()', function() { - it('should return falsy if object does not exist', function(done) { - db.getObjectField('doesnotexist', 'fieldName', function(err, value) { + describe('getObjectField()', function () { + it('should return falsy if object does not exist', function (done) { + db.getObjectField('doesnotexist', 'fieldName', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!value, false); @@ -96,8 +96,8 @@ describe('Hash methods', function() { }); }); - it('should return falsy if field does not exist', function(done) { - db.getObjectField('hashTestObject', 'fieldName', function(err, value) { + it('should return falsy if field does not exist', function (done) { + db.getObjectField('hashTestObject', 'fieldName', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!value, false); @@ -105,8 +105,8 @@ describe('Hash methods', function() { }); }); - it('should get an objects field', function(done) { - db.getObjectField('hashTestObject', 'lastname', function(err, value) { + it('should get an objects field', function (done) { + db.getObjectField('hashTestObject', 'lastname', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, 'usakli'); @@ -115,9 +115,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectFields()', function() { - it('should return an object with falsy values', function(done) { - db.getObjectFields('doesnotexist', ['field1', 'field2'], function(err, object) { + describe('getObjectFields()', function () { + it('should return an object with falsy values', function (done) { + db.getObjectFields('doesnotexist', ['field1', 'field2'], function (err, object) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(typeof object, 'object'); @@ -127,8 +127,8 @@ describe('Hash methods', function() { }); }); - it('should return an object with correct fields', function(done) { - db.getObjectFields('hashTestObject', ['lastname', 'age', 'field1'], function(err, object) { + it('should return an object with correct fields', function (done) { + db.getObjectFields('hashTestObject', ['lastname', 'age', 'field1'], function (err, object) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(typeof object, 'object'); @@ -140,16 +140,16 @@ describe('Hash methods', function() { }); }); - describe('getObjectsFields()', function() { - before(function(done) { + describe('getObjectsFields()', function () { + before(function (done) { async.parallel([ async.apply(db.setObject, 'testObject8', {name: 'baris', age:99}), async.apply(db.setObject, 'testObject9', {name: 'ginger', age: 3}) ], done); }); - it('should return an array of objects with correct values', function(done) { - db.getObjectsFields(['testObject8', 'testObject9', 'doesnotexist'], ['name', 'age'], function(err, objects) { + it('should return an array of objects with correct values', function (done) { + db.getObjectsFields(['testObject8', 'testObject9', 'doesnotexist'], ['name', 'age'], function (err, objects) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(objects), true); @@ -164,9 +164,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectKeys()', function() { - it('should return an empty array for a object that does not exist', function(done) { - db.getObjectKeys('doesnotexist', function(err, keys) { + describe('getObjectKeys()', function () { + it('should return an empty array for a object that does not exist', function (done) { + db.getObjectKeys('doesnotexist', function (err, keys) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(keys) && keys.length === 0, true); @@ -174,12 +174,12 @@ describe('Hash methods', function() { }); }); - it('should return an array of keys for the object\'s fields', function(done) { - db.getObjectKeys('hashTestObject', function(err, keys) { + it('should return an array of keys for the object\'s fields', function (done) { + db.getObjectKeys('hashTestObject', function (err, keys) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(keys) && keys.length === 3, true); - keys.forEach(function(key) { + keys.forEach(function (key) { assert.notEqual(['name', 'lastname', 'age'].indexOf(key), -1); }); done(); @@ -187,9 +187,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectValues()', function() { - it('should return an empty array for a object that does not exist', function(done) { - db.getObjectValues('doesnotexist', function(err, values) { + describe('getObjectValues()', function () { + it('should return an empty array for a object that does not exist', function (done) { + db.getObjectValues('doesnotexist', function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(values) && values.length === 0, true); @@ -197,23 +197,20 @@ describe('Hash methods', function() { }); }); - it('should return an array of values for the object\'s fields', function(done) { - db.getObjectValues('hashTestObject', function(err, values) { + it('should return an array of values for the object\'s fields', function (done) { + db.getObjectValues('hashTestObject', function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(values) && values.length === 3, true); - values.forEach(function(value) { - assert.notEqual(['baris', 'usakli', 99].indexOf(value), -1); - }); - + assert.deepEqual(['baris', 'usakli', 99].sort(), values.sort()); done(); }); }); }); - describe('isObjectField()', function() { - it('should return false if object does not exist', function(done) { - db.isObjectField('doesnotexist', 'field1', function(err, value) { + describe('isObjectField()', function () { + it('should return false if object does not exist', function (done) { + db.isObjectField('doesnotexist', 'field1', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, false); @@ -221,8 +218,8 @@ describe('Hash methods', function() { }); }); - it('should return false if field does not exist', function(done) { - db.isObjectField('hashTestObject', 'field1', function(err, value) { + it('should return false if field does not exist', function (done) { + db.isObjectField('hashTestObject', 'field1', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, false); @@ -230,8 +227,8 @@ describe('Hash methods', function() { }); }); - it('should return true if field exists', function(done) { - db.isObjectField('hashTestObject', 'name', function(err, value) { + it('should return true if field exists', function (done) { + db.isObjectField('hashTestObject', 'name', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, true); @@ -241,9 +238,9 @@ describe('Hash methods', function() { }); - describe('isObjectFields()', function() { - it('should return an array of false if object does not exist', function(done) { - db.isObjectFields('doesnotexist', ['field1', 'field2'], function(err, values) { + describe('isObjectFields()', function () { + it('should return an array of false if object does not exist', function (done) { + db.isObjectFields('doesnotexist', ['field1', 'field2'], function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [false, false]); @@ -251,8 +248,8 @@ describe('Hash methods', function() { }); }); - it('should return false if field does not exist', function(done) { - db.isObjectFields('hashTestObject', ['name', 'age', 'field1'], function(err, values) { + it('should return false if field does not exist', function (done) { + db.isObjectFields('hashTestObject', ['name', 'age', 'field1'], function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [true, true, false]); @@ -261,16 +258,16 @@ describe('Hash methods', function() { }); }); - describe('deleteObjectField()', function() { - before(function(done) { + describe('deleteObjectField()', function () { + before(function (done) { db.setObject('testObject10', {foo: 'bar', delete: 'this', delete1: 'this', delete2: 'this'}, done); }); - it('should delete an objects field', function(done) { - db.deleteObjectField('testObject10', 'delete', function(err) { + it('should delete an objects field', function (done) { + db.deleteObjectField('testObject10', 'delete', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isObjectField('testObject10', 'delete', function(err, isField) { + db.isObjectField('testObject10', 'delete', function (err, isField) { assert.equal(err, null); assert.equal(isField, false); done(); @@ -278,14 +275,14 @@ describe('Hash methods', function() { }); }); - it('should delete multiple fields of the object', function(done) { - db.deleteObjectFields('testObject10', ['delete1', 'delete2'], function(err) { + it('should delete multiple fields of the object', function (done) { + db.deleteObjectFields('testObject10', ['delete1', 'delete2'], function (err) { assert.ifError(err); assert.equal(arguments.length, 1); async.parallel({ delete1: async.apply(db.isObjectField, 'testObject10', 'delete1'), delete2: async.apply(db.isObjectField, 'testObject10', 'delete2') - }, function(err, results) { + }, function (err, results) { assert.ifError(err); assert.equal(results.delete1, false); assert.equal(results.delete2, false); @@ -295,13 +292,13 @@ describe('Hash methods', function() { }); }); - describe('incrObjectField()', function() { - before(function(done) { + describe('incrObjectField()', function () { + before(function (done) { db.setObject('testObject11', {age: 99}, done); }); - it('should set an objects field to 1 if object does not exist', function(done) { - db.incrObjectField('testObject12', 'field1', function(err, newValue) { + it('should set an objects field to 1 if object does not exist', function (done) { + db.incrObjectField('testObject12', 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 1); @@ -309,8 +306,8 @@ describe('Hash methods', function() { }); }); - it('should increment an object fields by 1 and return it', function(done) { - db.incrObjectField('testObject11', 'age', function(err, newValue) { + it('should increment an object fields by 1 and return it', function (done) { + db.incrObjectField('testObject11', 'age', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 100); @@ -319,13 +316,13 @@ describe('Hash methods', function() { }); }); - describe('decrObjectField()', function() { - before(function(done) { + describe('decrObjectField()', function () { + before(function (done) { db.setObject('testObject13', {age: 99}, done); }); - it('should set an objects field to -1 if object does not exist', function(done) { - db.decrObjectField('testObject14', 'field1', function(err, newValue) { + it('should set an objects field to -1 if object does not exist', function (done) { + db.decrObjectField('testObject14', 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, -1); @@ -333,8 +330,8 @@ describe('Hash methods', function() { }); }); - it('should decrement an object fields by 1 and return it', function(done) { - db.decrObjectField('testObject13', 'age', function(err, newValue) { + it('should decrement an object fields by 1 and return it', function (done) { + db.decrObjectField('testObject13', 'age', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 98); @@ -343,33 +340,41 @@ describe('Hash methods', function() { }); }); - describe('incrObjectFieldBy()', function() { - before(function(done) { + describe('incrObjectFieldBy()', function () { + before(function (done) { db.setObject('testObject15', {age: 100}, done); }); - it('should set an objects field to 5 if object does not exist', function(done) { - db.incrObjectFieldBy('testObject16', 'field1', 5, function(err, newValue) { - assert.equal(err, null); + it('should set an objects field to 5 if object does not exist', function (done) { + db.incrObjectFieldBy('testObject16', 'field1', 5, function (err, newValue) { + assert.ifError(err); assert.equal(arguments.length, 2); assert.equal(newValue, 5); done(); }); }); - it('should increment an object fields by passed in value and return it', function(done) { - db.incrObjectFieldBy('testObject15', 'age', 11, function(err, newValue) { - assert.equal(err, null); + it('should increment an object fields by passed in value and return it', function (done) { + db.incrObjectFieldBy('testObject15', 'age', 11, function (err, newValue) { + assert.ifError(err); assert.equal(arguments.length, 2); assert.equal(newValue, 111); done(); }); }); + + it('should increment an object fields by passed in value and return it', function (done) { + db.incrObjectFieldBy('testObject15', 'age', '11', function (err, newValue) { + assert.ifError(err); + assert.equal(newValue, 122); + done(); + }); + }); }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/keys.js b/test/database/keys.js similarity index 54% rename from tests/database/keys.js rename to test/database/keys.js index 36e5fe5989..0166ced52d 100644 --- a/tests/database/keys.js +++ b/test/database/keys.js @@ -5,22 +5,22 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Key methods', function() { +describe('Key methods', function () { - beforeEach(function(done) { + beforeEach(function (done) { db.set('testKey', 'testValue', done); }); - it('should set a key without error', function(done) { - db.set('testKey', 'testValue', function(err) { + it('should set a key without error', function (done) { + db.set('testKey', 'testValue', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should get a key without error', function(done) { - db.get('testKey', function(err, value) { + it('should get a key without error', function (done) { + db.get('testKey', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(value, 'testValue'); @@ -28,8 +28,8 @@ describe('Key methods', function() { }); }); - it('should return true if key exist', function(done) { - db.exists('testKey', function(err, exists) { + it('should return true if key exist', function (done) { + db.exists('testKey', function (err, exists) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(exists, true); @@ -37,8 +37,8 @@ describe('Key methods', function() { }); }); - it('should return false if key does not exist', function(done) { - db.exists('doesnotexist', function(err, exists) { + it('should return false if key does not exist', function (done) { + db.exists('doesnotexist', function (err, exists) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(exists, false); @@ -46,12 +46,12 @@ describe('Key methods', function() { }); }); - it('should delete a key without error', function(done) { - db.delete('testKey', function(err) { + it('should delete a key without error', function (done) { + db.delete('testKey', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.get('testKey', function(err, value) { + db.get('testKey', function (err, value) { assert.equal(err, null); assert.equal(false, !!value); done(); @@ -59,11 +59,11 @@ describe('Key methods', function() { }); }); - it('should return false if key was deleted', function(done) { - db.delete('testKey', function(err) { + it('should return false if key was deleted', function (done) { + db.delete('testKey', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.exists('testKey', function(err, exists) { + db.exists('testKey', function (err, exists) { assert.equal(err, null); assert.strictEqual(exists, false); done(); @@ -71,29 +71,29 @@ describe('Key methods', function() { }); }); - it('should delete all keys passed in', function(done) { + it('should delete all keys passed in', function (done) { async.parallel([ - function(next) { + function (next) { db.set('key1', 'value1', next); }, - function(next) { + function (next) { db.set('key2', 'value2', next); } - ], function(err) { + ], function (err) { if (err) { return done(err); } - db.deleteAll(['key1', 'key2'], function(err) { + db.deleteAll(['key1', 'key2'], function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); async.parallel({ - key1exists: function(next) { + key1exists: function (next) { db.exists('key1', next); }, - key2exists: function(next) { + key2exists: function (next) { db.exists('key2', next); } - }, function(err, results) { + }, function (err, results) { assert.equal(err, null); assert.equal(results.key1exists, false); assert.equal(results.key2exists, false); @@ -103,17 +103,17 @@ describe('Key methods', function() { }); }); - describe('increment', function() { - it('should initialize key to 1', function(done) { - db.increment('keyToIncrement', function(err, value) { + describe('increment', function () { + it('should initialize key to 1', function (done) { + db.increment('keyToIncrement', function (err, value) { assert.equal(err, null); assert.strictEqual(parseInt(value, 10), 1); done(); }); }); - it('should increment key to 2', function(done) { - db.increment('keyToIncrement', function(err, value) { + it('should increment key to 2', function (done) { + db.increment('keyToIncrement', function (err, value) { assert.equal(err, null); assert.strictEqual(parseInt(value, 10), 2); done(); @@ -121,17 +121,17 @@ describe('Key methods', function() { }); }); - describe('rename', function() { - it('should rename key to new name', function(done) { - db.set('keyOldName', 'renamedKeyValue', function(err) { + describe('rename', function () { + it('should rename key to new name', function (done) { + db.set('keyOldName', 'renamedKeyValue', function (err) { if (err) { return done(err); } - db.rename('keyOldName', 'keyNewName', function(err) { + db.rename('keyOldName', 'keyNewName', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.get('keyNewName', function(err, value) { + db.get('keyNewName', function (err, value) { assert.equal(err, null); assert.equal(value, 'renamedKeyValue'); done(); @@ -142,7 +142,7 @@ describe('Key methods', function() { }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/list.js b/test/database/list.js similarity index 58% rename from tests/database/list.js rename to test/database/list.js index 59fa82aa58..706fc2ef40 100644 --- a/tests/database/list.js +++ b/test/database/list.js @@ -5,11 +5,11 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('List methods', function() { +describe('List methods', function () { - describe('listAppend()', function() { - it('should append to a list', function(done) { - db.listAppend('testList1', 5, function(err) { + describe('listAppend()', function () { + it('should append to a list', function (done) { + db.listAppend('testList1', 5, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -17,47 +17,47 @@ describe('List methods', function() { }); }); - describe('listPrepend()', function() { - it('should prepend to a list', function(done) { - db.listPrepend('testList2', 3, function(err) { + describe('listPrepend()', function () { + it('should prepend to a list', function (done) { + db.listPrepend('testList2', 3, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should prepend 2 more elements to a list', function(done) { + it('should prepend 2 more elements to a list', function (done) { async.series([ - function(next) { + function (next) { db.listPrepend('testList2', 2, next); }, - function(next) { + function (next) { db.listPrepend('testList2', 1, next); } - ], function(err) { + ], function (err) { assert.equal(err, null); done(); }); }); }); - describe('getListRange()', function() { - before(function(done) { + describe('getListRange()', function () { + before(function (done) { async.series([ - function(next) { + function (next) { db.listAppend('testList3', 7, next); }, - function(next) { + function (next) { db.listPrepend('testList3', 3, next); }, - function(next) { + function (next) { db.listAppend('testList4', 5, next); } ], done); }); - it('should return an empty list', function(done) { - db.getListRange('doesnotexist', 0, -1, function(err, list) { + it('should return an empty list', function (done) { + db.getListRange('doesnotexist', 0, -1, function (err, list) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(list), true); @@ -66,8 +66,8 @@ describe('List methods', function() { }); }); - it('should return a list with one element', function(done) { - db.getListRange('testList4', 0, 0, function(err, list) { + it('should return a list with one element', function (done) { + db.getListRange('testList4', 0, 0, function (err, list) { assert.equal(err, null); assert.equal(Array.isArray(list), true); assert.equal(list[0], 5); @@ -75,8 +75,8 @@ describe('List methods', function() { }); }); - it('should return a list with 2 elements 3, 7', function(done) { - db.getListRange('testList3', 0, -1, function(err, list) { + it('should return a list with 2 elements 3, 7', function (done) { + db.getListRange('testList3', 0, -1, function (err, list) { assert.equal(err, null); assert.equal(Array.isArray(list), true); assert.equal(list.length, 2); @@ -86,20 +86,20 @@ describe('List methods', function() { }); }); - describe('listRemoveLast()', function() { - before(function(done) { + describe('listRemoveLast()', function () { + before(function (done) { async.series([ - function(next) { + function (next) { db.listAppend('testList4', 12, next); }, - function(next) { + function (next) { db.listPrepend('testList4', 9, next); } ], done); }); - it('should remove the last element of list and return it', function(done) { - db.listRemoveLast('testList4', function(err, lastElement) { + it('should remove the last element of list and return it', function (done) { + db.listRemoveLast('testList4', function (err, lastElement) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(lastElement, '12'); @@ -108,8 +108,8 @@ describe('List methods', function() { }); }); - describe('listRemoveAll()', function() { - before(function(done) { + describe('listRemoveAll()', function () { + before(function (done) { async.series([ async.apply(db.listAppend, 'testList5', 1), async.apply(db.listAppend, 'testList5', 1), @@ -119,12 +119,13 @@ describe('List methods', function() { ], done); }); - it('should remove all the matching elements of list', function(done) { - db.listRemoveAll('testList5', '1', function(err) { + it('should remove all the matching elements of list', function (done) { + db.listRemoveAll('testList5', '1', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.getListRange('testList5', 0, -1, function(err, list) { + db.getListRange('testList5', 0, -1, function (err, list) { + assert.equal(err, null); assert.equal(Array.isArray(list), true); assert.equal(list.length, 2); assert.equal(list.indexOf('1'), -1); @@ -134,20 +135,21 @@ describe('List methods', function() { }); }); - describe('listTrim()', function() { - it('should trim list to a certain range', function(done) { + describe('listTrim()', function () { + it('should trim list to a certain range', function (done) { var list = ['1', '2', '3', '4', '5']; - async.eachSeries(list, function(value, next) { + async.eachSeries(list, function (value, next) { db.listAppend('testList6', value, next); - }, function(err) { + }, function (err) { if (err) { return done(err); } - db.listTrim('testList6', 0, 2, function(err) { + db.listTrim('testList6', 0, 2, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.getListRange('testList6', 0, -1, function(err, list) { + db.getListRange('testList6', 0, -1, function (err, list) { + assert.equal(err, null); assert.equal(list.length, 3); assert.deepEqual(list, ['1', '2', '3']); done(); @@ -158,7 +160,7 @@ describe('List methods', function() { }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/sets.js b/test/database/sets.js similarity index 56% rename from tests/database/sets.js rename to test/database/sets.js index a7a23f4dd8..4e899c8265 100644 --- a/tests/database/sets.js +++ b/test/database/sets.js @@ -5,19 +5,19 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Set methods', function() { +describe('Set methods', function () { - describe('setAdd()', function() { - it('should add to a set', function(done) { - db.setAdd('testSet1', 5, function(err) { + describe('setAdd()', function () { + it('should add to a set', function (done) { + db.setAdd('testSet1', 5, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should add an array to a set', function(done) { - db.setAdd('testSet1', [1, 2, 3, 4], function(err) { + it('should add an array to a set', function (done) { + db.setAdd('testSet1', [1, 2, 3, 4], function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -25,13 +25,13 @@ describe('Set methods', function() { }); }); - describe('getSetMembers()', function() { - before(function(done) { + describe('getSetMembers()', function () { + before(function (done) { db.setAdd('testSet2', [1,2,3,4,5], done); }); - it('should return an empty set', function(done) { - db.getSetMembers('doesnotexist', function(err, set) { + it('should return an empty set', function (done) { + db.getSetMembers('doesnotexist', function (err, set) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(set), true); @@ -40,11 +40,11 @@ describe('Set methods', function() { }); }); - it('should return a set with all elements', function(done) { - db.getSetMembers('testSet2', function(err, set) { + it('should return a set with all elements', function (done) { + db.getSetMembers('testSet2', function (err, set) { assert.equal(err, null); assert.equal(set.length, 5); - set.forEach(function(value) { + set.forEach(function (value) { assert.notEqual(['1', '2', '3', '4', '5'].indexOf(value), -1); }); @@ -53,9 +53,9 @@ describe('Set methods', function() { }); }); - describe('setsAdd()', function() { - it('should add to multiple sets', function(done) { - db.setsAdd(['set1', 'set2'], 'value', function(err) { + describe('setsAdd()', function () { + it('should add to multiple sets', function (done) { + db.setsAdd(['set1', 'set2'], 'value', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -63,13 +63,13 @@ describe('Set methods', function() { }); }); - describe('getSetsMembers()', function() { - before(function(done) { + describe('getSetsMembers()', function () { + before(function (done) { db.setsAdd(['set3', 'set4'], 'value', done); }); - it('should return members of two sets', function(done) { - db.getSetsMembers(['set3', 'set4'], function(err, sets) { + it('should return members of two sets', function (done) { + db.getSetsMembers(['set3', 'set4'], function (err, sets) { assert.equal(err, null); assert.equal(Array.isArray(sets), true); assert.equal(arguments.length, 2); @@ -81,13 +81,13 @@ describe('Set methods', function() { }); }); - describe('isSetMember()', function() { - before(function(done) { + describe('isSetMember()', function () { + before(function (done) { db.setAdd('testSet3', 5, done); }); - it('should return false if element is not member of set', function(done) { - db.isSetMember('testSet3', 10, function(err, isMember) { + it('should return false if element is not member of set', function (done) { + db.isSetMember('testSet3', 10, function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, false); @@ -95,8 +95,8 @@ describe('Set methods', function() { }); }); - it('should return true if element is a member of set', function(done) { - db.isSetMember('testSet3', 5, function(err, isMember) { + it('should return true if element is a member of set', function (done) { + db.isSetMember('testSet3', 5, function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, true); @@ -105,13 +105,13 @@ describe('Set methods', function() { }); }); - describe('isSetMembers()', function() { - before(function(done) { + describe('isSetMembers()', function () { + before(function (done) { db.setAdd('testSet4', [1, 2, 3, 4, 5], done); }); - it('should return an array of booleans', function(done) { - db.isSetMembers('testSet4', ['1', '2', '10', '3'], function(err, members) { + it('should return an array of booleans', function (done) { + db.isSetMembers('testSet4', ['1', '2', '10', '3'], function (err, members) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(members), true); @@ -121,13 +121,13 @@ describe('Set methods', function() { }); }); - describe('isMemberOfSets()', function() { - before(function(done) { + describe('isMemberOfSets()', function () { + before(function (done) { db.setsAdd(['set1', 'set2'], 'value', done); }); - it('should return an array of booleans', function(done) { - db.isMemberOfSets(['set1', 'testSet1', 'set2', 'doesnotexist'], 'value', function(err, members) { + it('should return an array of booleans', function (done) { + db.isMemberOfSets(['set1', 'testSet1', 'set2', 'doesnotexist'], 'value', function (err, members) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(members), true); @@ -137,13 +137,13 @@ describe('Set methods', function() { }); }); - describe('setCount()', function() { - before(function(done) { + describe('setCount()', function () { + before(function (done) { db.setAdd('testSet5', [1,2,3,4,5], done); }); - it('should return the element count of set', function(done) { - db.setCount('testSet5', function(err, count) { + it('should return the element count of set', function (done) { + db.setCount('testSet5', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(count, 5); @@ -152,8 +152,8 @@ describe('Set methods', function() { }); }); - describe('setsCount()', function() { - before(function(done) { + describe('setsCount()', function () { + before(function (done) { async.parallel([ async.apply(db.setAdd, 'set5', [1,2,3,4,5]), async.apply(db.setAdd, 'set6', 1), @@ -161,8 +161,8 @@ describe('Set methods', function() { ], done); }); - it('should return the element count of sets', function(done) { - db.setsCount(['set5', 'set6', 'set7', 'doesnotexist'], function(err, counts) { + it('should return the element count of sets', function (done) { + db.setsCount(['set5', 'set6', 'set7', 'doesnotexist'], function (err, counts) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(counts), true); @@ -172,17 +172,17 @@ describe('Set methods', function() { }); }); - describe('setRemove()', function() { - before(function(done) { + describe('setRemove()', function () { + before(function (done) { db.setAdd('testSet6', [1, 2], done); }); - it('should remove a element from set', function(done) { - db.setRemove('testSet6', '2', function(err) { + it('should remove a element from set', function (done) { + db.setRemove('testSet6', '2', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isSetMember('testSet6', '2', function(err, isMember) { + db.isSetMember('testSet6', '2', function (err, isMember) { assert.equal(err, null); assert.equal(isMember, false); done(); @@ -191,16 +191,16 @@ describe('Set methods', function() { }); }); - describe('setsRemove()', function() { - before(function(done) { + describe('setsRemove()', function () { + before(function (done) { db.setsAdd(['set1', 'set2'], 'value', done); }); - it('should remove a element from multiple sets', function(done) { - db.setsRemove(['set1', 'set2'], 'value', function(err) { + it('should remove a element from multiple sets', function (done) { + db.setsRemove(['set1', 'set2'], 'value', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isMemberOfSets(['set1', 'set2'], 'value', function(err, members) { + db.isMemberOfSets(['set1', 'set2'], 'value', function (err, members) { assert.equal(err, null); assert.deepEqual(members, [false, false]); done(); @@ -209,17 +209,17 @@ describe('Set methods', function() { }); }); - describe('setRemoveRandom()', function() { - before(function(done) { + describe('setRemoveRandom()', function () { + before(function (done) { db.setAdd('testSet7', [1,2,3,4,5], done); }); - it('should remove a random element from set', function(done) { - db.setRemoveRandom('testSet7', function(err, element) { + it('should remove a random element from set', function (done) { + db.setRemoveRandom('testSet7', function (err, element) { assert.equal(err, null); assert.equal(arguments.length, 2); - db.isSetMember('testSet', element, function(err, ismember) { + db.isSetMember('testSet', element, function (err, ismember) { assert.equal(err, null); assert.equal(ismember, false); done(); @@ -229,7 +229,7 @@ describe('Set methods', function() { }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/sorted.js b/test/database/sorted.js similarity index 50% rename from tests/database/sorted.js rename to test/database/sorted.js index 5c34c380b8..29ed4cf95e 100644 --- a/tests/database/sorted.js +++ b/test/database/sorted.js @@ -5,33 +5,33 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Sorted Set methods', function() { +describe('Sorted Set methods', function () { - before(function(done) { + before(function (done) { async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('sortedSetTest1', [1, 2, 3], ['value1', 'value2', 'value3'], next); }, - function(next) { + function (next) { db.sortedSetAdd('sortedSetTest2', [1, 4], ['value1', 'value4'], next); }, - function(next) { + function (next) { db.sortedSetAdd('sortedSetTest3', [2, 4], ['value2', 'value4'], next); } ], done); }); - describe('sortedSetAdd()', function() { - it('should add an element to a sorted set', function(done) { - db.sortedSetAdd('sorted1', 1, 'value1', function(err) { + describe('sortedSetAdd()', function () { + it('should add an element to a sorted set', function (done) { + db.sortedSetAdd('sorted1', 1, 'value1', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should add two elements to a sorted set', function(done) { - db.sortedSetAdd('sorted2', [1, 2], ['value1', 'value2'], function(err) { + it('should add two elements to a sorted set', function (done) { + db.sortedSetAdd('sorted2', [1, 2], ['value1', 'value2'], function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -39,9 +39,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsAdd()', function() { - it('should add an element to two sorted sets', function(done) { - db.sortedSetsAdd(['sorted1', 'sorted2'], 3, 'value3', function(err) { + describe('sortedSetsAdd()', function () { + it('should add an element to two sorted sets', function (done) { + db.sortedSetsAdd(['sorted1', 'sorted2'], 3, 'value3', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -49,18 +49,18 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRange()', function() { - it('should return the lowest scored element', function(done) { - db.getSortedSetRange('sortedSetTest1', 0, 0, function(err, value) { + describe('getSortedSetRange()', function () { + it('should return the lowest scored element', function (done) { + db.getSortedSetRange('sortedSetTest1', 0, 0, function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); - assert.equal(value, 'value1'); + assert.deepEqual(value, ['value1']); done(); }); }); - it('should return elements sorted by score lowest to highest', function(done) { - db.getSortedSetRange('sortedSetTest1', 0, -1, function(err, values) { + it('should return elements sorted by score lowest to highest', function (done) { + db.getSortedSetRange('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2', 'value3']); @@ -69,18 +69,18 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRange()', function() { - it('should return the highest scored element', function(done) { - db.getSortedSetRevRange('sortedSetTest1', 0, 0, function(err, value) { + describe('getSortedSetRevRange()', function () { + it('should return the highest scored element', function (done) { + db.getSortedSetRevRange('sortedSetTest1', 0, 0, function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); - assert.equal(value, 'value3'); + assert.deepEqual(value, ['value3']); done(); }); }); - it('should return elements sorted by score highest to lowest', function(done) { - db.getSortedSetRevRange('sortedSetTest1', 0, -1, function(err, values) { + it('should return elements sorted by score highest to lowest', function (done) { + db.getSortedSetRevRange('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value3', 'value2', 'value1']); @@ -89,9 +89,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRangeWithScores()', function() { - it('should return array of elements sorted by score lowest to highest with scores', function(done) { - db.getSortedSetRangeWithScores('sortedSetTest1', 0, -1, function(err, values) { + describe('getSortedSetRangeWithScores()', function () { + it('should return array of elements sorted by score lowest to highest with scores', function (done) { + db.getSortedSetRangeWithScores('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value1', score: 1}, {value: 'value2', score: 2}, {value: 'value3', score: 3}]); @@ -100,9 +100,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRangeWithScores()', function() { - it('should return array of elements sorted by score highest to lowest with scores', function(done) { - db.getSortedSetRevRangeWithScores('sortedSetTest1', 0, -1, function(err, values) { + describe('getSortedSetRevRangeWithScores()', function () { + it('should return array of elements sorted by score highest to lowest with scores', function (done) { + db.getSortedSetRevRangeWithScores('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value3', score: 3}, {value: 'value2', score: 2}, {value: 'value1', score: 1}]); @@ -111,9 +111,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRangeByScore()', function() { - it('should get count elements with score between min max sorted by score lowest to highest', function(done) { - db.getSortedSetRangeByScore('sortedSetTest1', 0, -1, '-inf', 2, function(err, values) { + describe('getSortedSetRangeByScore()', function () { + it('should get count elements with score between min max sorted by score lowest to highest', function (done) { + db.getSortedSetRangeByScore('sortedSetTest1', 0, -1, '-inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2']); @@ -122,9 +122,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRangeByScore()', function() { - it('should get count elements with score between max min sorted by score highest to lowest', function(done) { - db.getSortedSetRevRangeByScore('sortedSetTest1', 0, -1, '+inf', 2, function(err, values) { + describe('getSortedSetRevRangeByScore()', function () { + it('should get count elements with score between max min sorted by score highest to lowest', function (done) { + db.getSortedSetRevRangeByScore('sortedSetTest1', 0, -1, '+inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value3', 'value2']); @@ -133,9 +133,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRangeByScoreWithScores()', function() { - it('should get count elements with score between min max sorted by score lowest to highest with scores', function(done) { - db.getSortedSetRangeByScoreWithScores('sortedSetTest1', 0, -1, '-inf', 2, function(err, values) { + describe('getSortedSetRangeByScoreWithScores()', function () { + it('should get count elements with score between min max sorted by score lowest to highest with scores', function (done) { + db.getSortedSetRangeByScoreWithScores('sortedSetTest1', 0, -1, '-inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value1', score: 1}, {value: 'value2', score: 2}]); @@ -144,9 +144,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRangeByScoreWithScores()', function() { - it('should get count elements with score between max min sorted by score highest to lowest', function(done) { - db.getSortedSetRevRangeByScoreWithScores('sortedSetTest1', 0, -1, '+inf', 2, function(err, values) { + describe('getSortedSetRevRangeByScoreWithScores()', function () { + it('should get count elements with score between max min sorted by score highest to lowest', function (done) { + db.getSortedSetRevRangeByScoreWithScores('sortedSetTest1', 0, -1, '+inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value3', score: 3}, {value: 'value2', score: 2}]); @@ -155,9 +155,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetCount()', function() { - it('should return 0 for a sorted set that does not exist', function(done) { - db.sortedSetCount('doesnotexist', 0, 10, function(err, count) { + describe('sortedSetCount()', function () { + it('should return 0 for a sorted set that does not exist', function (done) { + db.sortedSetCount('doesnotexist', 0, 10, function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 0); @@ -165,8 +165,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return number of elements between scores min max inclusive', function(done) { - db.sortedSetCount('sortedSetTest1', '-inf', 2, function(err, count) { + it('should return number of elements between scores min max inclusive', function (done) { + db.sortedSetCount('sortedSetTest1', '-inf', 2, function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 2); @@ -174,8 +174,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return number of elements between scores -inf +inf inclusive', function(done) { - db.sortedSetCount('sortedSetTest1', '-inf', '+inf', function(err, count) { + it('should return number of elements between scores -inf +inf inclusive', function (done) { + db.sortedSetCount('sortedSetTest1', '-inf', '+inf', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 3); @@ -184,9 +184,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetCard()', function() { - it('should return 0 for a sorted set that does not exist', function(done) { - db.sortedSetCard('doesnotexist', function(err, count) { + describe('sortedSetCard()', function () { + it('should return 0 for a sorted set that does not exist', function (done) { + db.sortedSetCard('doesnotexist', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 0); @@ -194,8 +194,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return number of elements in a sorted set', function(done) { - db.sortedSetCard('sortedSetTest1', function(err, count) { + it('should return number of elements in a sorted set', function (done) { + db.sortedSetCard('sortedSetTest1', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 3); @@ -204,9 +204,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsCard()', function() { - it('should return the number of elements in sorted sets', function(done) { - db.sortedSetsCard(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], function(err, counts) { + describe('sortedSetsCard()', function () { + it('should return the number of elements in sorted sets', function (done) { + db.sortedSetsCard(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], function (err, counts) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(counts, [3, 2, 0]); @@ -215,9 +215,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetRank()', function() { - it('should return falsy if sorted set does not exist', function(done) { - db.sortedSetRank('doesnotexist', 'value1', function(err, rank) { + describe('sortedSetRank()', function () { + it('should return falsy if sorted set does not exist', function (done) { + db.sortedSetRank('doesnotexist', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -225,8 +225,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return falsy if element isnt in sorted set', function(done) { - db.sortedSetRank('sortedSetTest1', 'value5', function(err, rank) { + it('should return falsy if element isnt in sorted set', function (done) { + db.sortedSetRank('sortedSetTest1', 'value5', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -234,8 +234,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return the rank of the element in the sorted set sorted by lowest to highest score', function(done) { - db.sortedSetRank('sortedSetTest1', 'value1', function(err, rank) { + it('should return the rank of the element in the sorted set sorted by lowest to highest score', function (done) { + db.sortedSetRank('sortedSetTest1', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(rank, 0); @@ -244,9 +244,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetRevRank()', function() { - it('should return falsy if sorted set doesnot exist', function(done) { - db.sortedSetRevRank('doesnotexist', 'value1', function(err, rank) { + describe('sortedSetRevRank()', function () { + it('should return falsy if sorted set doesnot exist', function (done) { + db.sortedSetRevRank('doesnotexist', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -254,8 +254,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return falsy if element isnt in sorted set', function(done) { - db.sortedSetRevRank('sortedSetTest1', 'value5', function(err, rank) { + it('should return falsy if element isnt in sorted set', function (done) { + db.sortedSetRevRank('sortedSetTest1', 'value5', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -263,8 +263,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return the rank of the element in the sorted set sorted by highest to lowest score', function(done) { - db.sortedSetRevRank('sortedSetTest1', 'value1', function(err, rank) { + it('should return the rank of the element in the sorted set sorted by highest to lowest score', function (done) { + db.sortedSetRevRank('sortedSetTest1', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(rank, 2); @@ -273,9 +273,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsRanks()', function() { - it('should return the ranks of values in sorted sets', function(done) { - db.sortedSetsRanks(['sortedSetTest1', 'sortedSetTest2'], ['value1', 'value4'], function(err, ranks) { + describe('sortedSetsRanks()', function () { + it('should return the ranks of values in sorted sets', function (done) { + db.sortedSetsRanks(['sortedSetTest1', 'sortedSetTest2'], ['value1', 'value4'], function (err, ranks) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(ranks, [0, 1]); @@ -284,9 +284,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetRanks()', function() { - it('should return the ranks of values in a sorted set', function(done) { - db.sortedSetRanks('sortedSetTest1', ['value2', 'value1', 'value3', 'value4'], function(err, ranks) { + describe('sortedSetRanks()', function () { + it('should return the ranks of values in a sorted set', function (done) { + db.sortedSetRanks('sortedSetTest1', ['value2', 'value1', 'value3', 'value4'], function (err, ranks) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(ranks, [1, 0, 2, null]); @@ -295,9 +295,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetScore()', function() { - it('should return falsy if sorted set does not exist', function(done) { - db.sortedSetScore('doesnotexist', 'value1', function(err, score) { + describe('sortedSetScore()', function () { + it('should return falsy if sorted set does not exist', function (done) { + db.sortedSetScore('doesnotexist', 'value1', function (err, score) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!score, false); @@ -305,8 +305,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return falsy if element is not in sorted set', function(done) { - db.sortedSetScore('sortedSetTest1', 'value5', function(err, score) { + it('should return falsy if element is not in sorted set', function (done) { + db.sortedSetScore('sortedSetTest1', 'value5', function (err, score) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!score, false); @@ -314,8 +314,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return the score of an element', function(done) { - db.sortedSetScore('sortedSetTest1', 'value2', function(err, score) { + it('should return the score of an element', function (done) { + db.sortedSetScore('sortedSetTest1', 'value2', function (err, score) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(score, 2); @@ -324,9 +324,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsScore()', function() { - it('should return the scores of value in sorted sets', function(done) { - db.sortedSetsScore(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], 'value1', function(err, scores) { + describe('sortedSetsScore()', function () { + it('should return the scores of value in sorted sets', function (done) { + db.sortedSetsScore(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], 'value1', function (err, scores) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(scores, [1, 1, null]); @@ -335,9 +335,21 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetScores()', function() { - it('should return the scores of value in sorted sets', function(done) { - db.sortedSetScores('sortedSetTest1', ['value2', 'value1', 'doesnotexist'], function(err, scores) { + describe('sortedSetScores()', function () { + before(function (done) { + db.sortedSetAdd('zeroScore', 0, 'value1', done); + }); + + it('should return 0 if score is 0', function (done) { + db.sortedSetScores('zeroScore', ['value1'], function (err, scores) { + assert.ifError(err); + assert.equal(0, scores[0]); + done(); + }); + }); + + it('should return the scores of value in sorted sets', function (done) { + db.sortedSetScores('sortedSetTest1', ['value2', 'value1', 'doesnotexist'], function (err, scores) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(scores, [2, 1, null]); @@ -346,13 +358,13 @@ describe('Sorted Set methods', function() { }); }); - describe('isSortedSetMember()', function() { - before(function(done) { + describe('isSortedSetMember()', function () { + before(function (done) { db.sortedSetAdd('zeroscore', 0, 'itemwithzeroscore', done); }); - it('should return false if sorted set does not exist', function(done) { - db.isSortedSetMember('doesnotexist', 'value1', function(err, isMember) { + it('should return false if sorted set does not exist', function (done) { + db.isSortedSetMember('doesnotexist', 'value1', function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, false); @@ -360,8 +372,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return false if element is not in sorted set', function(done) { - db.isSortedSetMember('sorted2', 'value5', function(err, isMember) { + it('should return false if element is not in sorted set', function (done) { + db.isSortedSetMember('sorted2', 'value5', function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, false); @@ -369,8 +381,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return true if element is in sorted set', function(done) { - db.isSortedSetMember('sortedSetTest1', 'value2', function(err, isMember) { + it('should return true if element is in sorted set', function (done) { + db.isSortedSetMember('sortedSetTest1', 'value2', function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(isMember, true); @@ -378,8 +390,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return true if element is in sorted set with score 0', function(done) { - db.isSortedSetMember('zeroscore', 'itemwithzeroscore', function(err, isMember) { + it('should return true if element is in sorted set with score 0', function (done) { + db.isSortedSetMember('zeroscore', 'itemwithzeroscore', function (err, isMember) { assert.ifError(err); assert.deepEqual(isMember, true); done(); @@ -387,9 +399,9 @@ describe('Sorted Set methods', function() { }); }); - describe('isSortedSetMembers()', function() { - it('should return an array of booleans indicating membership', function(done) { - db.isSortedSetMembers('sortedSetTest1', ['value1', 'value2', 'value5'], function(err, isMembers) { + describe('isSortedSetMembers()', function () { + it('should return an array of booleans indicating membership', function (done) { + db.isSortedSetMembers('sortedSetTest1', ['value1', 'value2', 'value5'], function (err, isMembers) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(isMembers, [true, true, false]); @@ -398,9 +410,9 @@ describe('Sorted Set methods', function() { }); }); - describe('isMemberOfSortedSets', function() { - it('should return true for members false for non members', function(done) { - db.isMemberOfSortedSets(['doesnotexist', 'sortedSetTest1', 'sortedSetTest2'], 'value2', function(err, isMembers) { + describe('isMemberOfSortedSets', function () { + it('should return true for members false for non members', function (done) { + db.isMemberOfSortedSets(['doesnotexist', 'sortedSetTest1', 'sortedSetTest2'], 'value2', function (err, isMembers) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(isMembers, [false, true, false]); @@ -409,13 +421,13 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetsMembers', function() { - it('should return members of multiple sorted sets', function(done) { - db.getSortedSetsMembers(['doesnotexist', 'sortedSetTest1'], function(err, sortedSets) { + describe('getSortedSetsMembers', function () { + it('should return members of multiple sorted sets', function (done) { + db.getSortedSetsMembers(['doesnotexist', 'sortedSetTest1'], function (err, sortedSets) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(sortedSets[0], []); - sortedSets[0].forEach(function(element) { + sortedSets[0].forEach(function (element) { assert.notEqual(['value1', 'value2', 'value3'].indexOf(element), -1); }); @@ -424,9 +436,19 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetUnion()', function() { - it('should return an array of values from both sorted sets sorted by scores lowest to highest', function(done) { - db.getSortedSetUnion(['sortedSetTest2', 'sortedSetTest3'], 0, -1, function(err, values) { + describe('sortedSetUnionCard', function () { + it('should return the number of elements in the union', function (done) { + db.sortedSetUnionCard(['sortedSetTest2', 'sortedSetTest3'], function (err, count) { + assert.ifError(err); + assert.equal(count, 3); + done(); + }); + }); + }); + + describe('getSortedSetUnion()', function () { + it('should return an array of values from both sorted sets sorted by scores lowest to highest', function (done) { + db.getSortedSetUnion({sets: ['sortedSetTest2', 'sortedSetTest3'], start: 0, stop: -1}, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2', 'value4']); @@ -435,9 +457,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevUnion()', function() { - it('should return an array of values from both sorted sets sorted by scores highest to lowest', function(done) { - db.getSortedSetRevUnion(['sortedSetTest2', 'sortedSetTest3'], 0, -1, function(err, values) { + describe('getSortedSetRevUnion()', function () { + it('should return an array of values from both sorted sets sorted by scores highest to lowest', function (done) { + db.getSortedSetRevUnion({sets: ['sortedSetTest2', 'sortedSetTest3'], start: 0, stop: -1}, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value4', 'value2', 'value1']); @@ -446,13 +468,13 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetIncrBy()', function() { - it('should create a sorted set with a field set to 1', function(done) { - db.sortedSetIncrBy('sortedIncr', 1, 'field1', function(err, newValue) { + describe('sortedSetIncrBy()', function () { + it('should create a sorted set with a field set to 1', function (done) { + db.sortedSetIncrBy('sortedIncr', 1, 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 1); - db.sortedSetScore('sortedIncr', 'field1', function(err, score) { + db.sortedSetScore('sortedIncr', 'field1', function (err, score) { assert.equal(err, null); assert.equal(score, 1); done(); @@ -460,12 +482,12 @@ describe('Sorted Set methods', function() { }); }); - it('should increment a field of a sorted set by 5', function(done) { - db.sortedSetIncrBy('sortedIncr', 5, 'field1', function(err, newValue) { + it('should increment a field of a sorted set by 5', function (done) { + db.sortedSetIncrBy('sortedIncr', 5, 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 6); - db.sortedSetScore('sortedIncr', 'field1', function(err, score) { + db.sortedSetScore('sortedIncr', 'field1', function (err, score) { assert.equal(err, null); assert.equal(score, 6); done(); @@ -475,16 +497,16 @@ describe('Sorted Set methods', function() { }); - describe('sortedSetRemove()', function() { - before(function(done) { + describe('sortedSetRemove()', function () { + before(function (done) { db.sortedSetAdd('sorted3', [1, 2], ['value1', 'value2'], done); }); - it('should remove an element from a sorted set', function(done) { - db.sortedSetRemove('sorted3', 'value2', function(err) { + it('should remove an element from a sorted set', function (done) { + db.sortedSetRemove('sorted3', 'value2', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isSortedSetMember('sorted3', 'value2', function(err, isMember) { + db.isSortedSetMember('sorted3', 'value2', function (err, isMember) { assert.equal(err, null); assert.equal(isMember, false); done(); @@ -493,19 +515,19 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsRemove()', function() { - before(function(done) { + describe('sortedSetsRemove()', function () { + before(function (done) { async.parallel([ async.apply(db.sortedSetAdd, 'sorted4', [1,2], ['value1', 'value2']), async.apply(db.sortedSetAdd, 'sorted5', [1,2], ['value1', 'value3']), ], done); }); - it('should remove element from multiple sorted sets', function(done) { - db.sortedSetsRemove(['sorted4', 'sorted5'], 'value1', function(err) { + it('should remove element from multiple sorted sets', function (done) { + db.sortedSetsRemove(['sorted4', 'sorted5'], 'value1', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.sortedSetsScore(['sorted4', 'sorted5'], 'value1', function(err, scores) { + db.sortedSetsScore(['sorted4', 'sorted5'], 'value1', function (err, scores) { assert.equal(err, null); assert.deepEqual(scores, [null, null]); done(); @@ -514,16 +536,16 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsRemoveRangeByScore()', function() { - before(function(done) { + describe('sortedSetsRemoveRangeByScore()', function () { + before(function (done) { db.sortedSetAdd('sorted6', [1,2,3,4,5], ['value1','value2','value3','value4','value5'], done); }); - it('should remove elements with scores between min max inclusive', function(done) { - db.sortedSetsRemoveRangeByScore(['sorted6'], 4, 5, function(err) { + it('should remove elements with scores between min max inclusive', function (done) { + db.sortedSetsRemoveRangeByScore(['sorted6'], 4, 5, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.getSortedSetRange('sorted6', 0, -1, function(err, values) { + db.getSortedSetRange('sorted6', 0, -1, function (err, values) { assert.equal(err, null); assert.deepEqual(values, ['value1', 'value2', 'value3']); done(); @@ -532,8 +554,148 @@ describe('Sorted Set methods', function() { }); }); + describe('getSortedSetIntersect', function () { + before(function (done) { + async.parallel([ + function (next) { + db.sortedSetAdd('interSet1', [1,2,3], ['value1', 'value2', 'value3'], next); + }, + function (next) { + db.sortedSetAdd('interSet2', [4,5,6], ['value2', 'value3', 'value5'], next); + } + ], done); + }); - after(function() { - db.flushdb(); + it('should return the intersection of two sets', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1 + }, function (err, data) { + assert.ifError(err); + assert.deepEqual(['value2', 'value3'], data); + done(); + }); + }); + + it('should return the intersection of two sets with scores', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 6}, {value: 'value3', score: 8}], data); + done(); + }); + }); + + it('should return the intersection of two sets with scores aggregate MIN', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true, + aggregate: 'MIN' + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 2}, {value: 'value3', score: 3}], data); + done(); + }); + }); + + it('should return the intersection of two sets with scores aggregate MAX', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true, + aggregate: 'MAX' + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 4}, {value: 'value3', score: 5}], data); + done(); + }); + }); + + it('should return the intersection with scores modified by weights', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true, + weights: [1, 0.5] + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 4}, {value: 'value3', score: 5.5}], data); + done(); + }); + }); + + it('should return empty array if sets do not exist', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet10', 'interSet12'], + start: 0, + stop: -1 + }, function (err, data) { + assert.ifError(err); + assert.equal(data.length, 0); + done(); + }); + }); + + it('should return empty array if one set does not exist', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet12'], + start: 0, + stop: -1 + }, function (err, data) { + assert.ifError(err); + assert.equal(data.length, 0); + done(); + }); + }); + + }); + + describe('sortedSetIntersectCard', function () { + before(function (done) { + async.parallel([ + function (next) { + db.sortedSetAdd('interCard1', [0, 0, 0], ['value1', 'value2', 'value3'], next); + }, + function (next) { + db.sortedSetAdd('interCard2', [0, 0, 0], ['value2', 'value3', 'value4'], next); + }, + function (next) { + db.sortedSetAdd('interCard3', [0, 0, 0], ['value3', 'value4', 'value5'], next); + }, + function (next) { + db.sortedSetAdd('interCard4', [0, 0, 0], ['value4', 'value5', 'value6'], next); + } + ], done); + }); + + it('should return # of elements in intersection', function (done) { + db.sortedSetIntersectCard(['interCard1', 'interCard2', 'interCard3'], function (err, count) { + assert.ifError(err); + assert.strictEqual(count, 1); + done(); + }); + }); + + it('should return 0 if intersection is empty', function (done) { + db.sortedSetIntersectCard(['interCard1', 'interCard4'], function (err, count) { + assert.ifError(err); + assert.strictEqual(count, 0); + done(); + }); + }); + }); + + + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/groups.js b/test/groups.js similarity index 52% rename from tests/groups.js rename to test/groups.js index 81fb253433..31195719b6 100644 --- a/tests/groups.js +++ b/test/groups.js @@ -1,55 +1,55 @@ 'use strict'; /*global require, before, after*/ -var assert = require('assert'), - async = require('async'), +var assert = require('assert'); +var async = require('async'); - db = require('./mocks/databasemock'), - Groups = require('../src/groups'), - User = require('../src/user'); +var db = require('./mocks/databasemock'); +var Groups = require('../src/groups'); +var User = require('../src/user'); -describe('Groups', function() { - before(function(done) { +describe('Groups', function () { + before(function (done) { + Groups.resetCache(); async.parallel([ - function(next) { + function (next) { // Create a group to play around with Groups.create({ name: 'Test', description: 'Foobar!' }, next); }, - function(next) { + function (next) { // Create a new user User.create({ username: 'testuser', email: 'b@c.com' }, next); }, - function(next) { + function (next) { // Also create a hidden group Groups.join('Hidden', 'Test', next); } ], done); }); - describe('.list()', function() { - it('should list the groups present', function(done) { - Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function(err, groups) { - if (err) return done(err); - + describe('.list()', function () { + it('should list the groups present', function (done) { + Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function (err, groups) { + assert.ifError(err); assert.equal(groups.length, 3); done(); }); }); }); - describe('.get()', function() { - before(function(done) { + describe('.get()', function () { + before(function (done) { Groups.join('Test', 1, done); }); - it('with no options, should show group information', function(done) { - Groups.get('Test', {}, function(err, groupObj) { + it('with no options, should show group information', function (done) { + Groups.get('Test', {}, function (err, groupObj) { if (err) return done(err); assert.equal(typeof groupObj, 'object'); @@ -64,9 +64,9 @@ describe('Groups', function() { }); }); - describe('.search()', function() { - it('should return the "Test" group when searched for', function(done) { - Groups.search('test', {}, function(err, groups) { + describe('.search()', function () { + it('should return the "Test" group when searched for', function (done) { + Groups.search('test', {}, function (err, groups) { if (err) return done(err); assert.equal(1, groups.length); assert.strictEqual('Test', groups[0].name); @@ -75,9 +75,9 @@ describe('Groups', function() { }); }); - describe('.isMember()', function() { - it('should return boolean true when a user is in a group', function(done) { - Groups.isMember(1, 'Test', function(err, isMember) { + describe('.isMember()', function () { + it('should return boolean true when a user is in a group', function (done) { + Groups.isMember(1, 'Test', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, true); @@ -86,8 +86,8 @@ describe('Groups', function() { }); }); - it('should return boolean false when a user is not in a group', function(done) { - Groups.isMember(2, 'Test', function(err, isMember) { + it('should return boolean false when a user is not in a group', function (done) { + Groups.isMember(2, 'Test', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, false); @@ -97,9 +97,9 @@ describe('Groups', function() { }); }); - describe('.isMemberOfGroupList', function() { - it('should report that a user is part of a groupList, if they are', function(done) { - Groups.isMemberOfGroupList(1, 'Hidden', function(err, isMember) { + describe('.isMemberOfGroupList', function () { + it('should report that a user is part of a groupList, if they are', function (done) { + Groups.isMemberOfGroupList(1, 'Hidden', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, true); @@ -108,8 +108,8 @@ describe('Groups', function() { }); }); - it('should report that a user is not part of a groupList, if they are not', function(done) { - Groups.isMemberOfGroupList(2, 'Hidden', function(err, isMember) { + it('should report that a user is not part of a groupList, if they are not', function (done) { + Groups.isMemberOfGroupList(2, 'Hidden', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, false); @@ -119,9 +119,9 @@ describe('Groups', function() { }); }); - describe('.exists()', function() { - it('should verify that the test group exists', function(done) { - Groups.exists('Test', function(err, exists) { + describe('.exists()', function () { + it('should verify that the test group exists', function (done) { + Groups.exists('Test', function (err, exists) { if (err) return done(err); assert.strictEqual(exists, true); @@ -130,8 +130,8 @@ describe('Groups', function() { }); }); - it('should verify that a fake group does not exist', function(done) { - Groups.exists('Derp', function(err, exists) { + it('should verify that a fake group does not exist', function (done) { + Groups.exists('Derp', function (err, exists) { if (err) return done(err); assert.strictEqual(exists, false); @@ -140,8 +140,8 @@ describe('Groups', function() { }); }); - it('should check if group exists using an array', function(done) { - Groups.exists(['Test', 'Derp'], function(err, groupsExists) { + it('should check if group exists using an array', function (done) { + Groups.exists(['Test', 'Derp'], function (err, groupsExists) { if (err) return done(err); assert.strictEqual(groupsExists[0], true); @@ -151,25 +151,33 @@ describe('Groups', function() { }); }); - describe('.create()', function() { - it('should create another group', function(done) { + describe('.create()', function () { + it('should create another group', function (done) { Groups.create({ name: 'foo', description: 'bar' - }, function(err) { + }, function (err) { if (err) return done(err); Groups.get('foo', {}, done); }); }); + + it('should fail to create group with duplicate group name', function (done) { + Groups.create({name: 'foo'}, function (err) { + assert(err); + assert.equal(err.message, '[[error:group-already-exists]]'); + done(); + }); + }); }); - describe('.hide()', function() { - it('should mark the group as hidden', function(done) { - Groups.hide('foo', function(err) { + describe('.hide()', function () { + it('should mark the group as hidden', function (done) { + Groups.hide('foo', function (err) { if (err) return done(err); - Groups.get('foo', {}, function(err, groupObj) { + Groups.get('foo', {}, function (err, groupObj) { if (err) return done(err); assert.strictEqual(true, groupObj.hidden); @@ -180,8 +188,8 @@ describe('Groups', function() { }); }); - describe('.update()', function() { - before(function(done) { + describe('.update()', function () { + before(function (done) { Groups.create({ name: 'updateTestGroup', description: 'bar', @@ -190,13 +198,13 @@ describe('Groups', function() { }, done); }); - it('should change an aspect of a group', function(done) { + it('should change an aspect of a group', function (done) { Groups.update('updateTestGroup', { description: 'baz' - }, function(err) { + }, function (err) { if (err) return done(err); - Groups.get('updateTestGroup', {}, function(err, groupObj) { + Groups.get('updateTestGroup', {}, function (err, groupObj) { if (err) return done(err); assert.strictEqual('baz', groupObj.description); @@ -206,13 +214,13 @@ describe('Groups', function() { }); }); - it('should rename a group if the name was updated', function(done) { + it('should rename a group if the name was updated', function (done) { Groups.update('updateTestGroup', { name: 'updateTestGroup?' - }, function(err) { + }, function (err) { if (err) return done(err); - Groups.get('updateTestGroup?', {}, function(err, groupObj) { + Groups.get('updateTestGroup?', {}, function (err, groupObj) { if (err) return done(err); assert.strictEqual('updateTestGroup?', groupObj.name); @@ -224,16 +232,16 @@ describe('Groups', function() { }); }); - describe('.destroy()', function() { - before(function(done) { + describe('.destroy()', function () { + before(function (done) { Groups.join('foobar?', 1, done); }); - it('should destroy a group', function(done) { - Groups.destroy('foobar?', function(err) { + it('should destroy a group', function (done) { + Groups.destroy('foobar?', function (err) { if (err) return done(err); - Groups.get('foobar?', {}, function(err) { + Groups.get('foobar?', {}, function (err) { assert(err, 'Group still exists!'); done(); @@ -241,8 +249,8 @@ describe('Groups', function() { }); }); - it('should also remove the members set', function(done) { - db.exists('group:foo:members', function(err, exists) { + it('should also remove the members set', function (done) { + db.exists('group:foo:members', function (err, exists) { if (err) return done(err); assert.strictEqual(false, exists); @@ -252,16 +260,17 @@ describe('Groups', function() { }); }); - describe('.join()', function() { - before(function(done) { + describe('.join()', function () { + before(function (done) { Groups.leave('Test', 1, done); }); - it('should add a user to a group', function(done) { - Groups.join('Test', 1, function(err) { + it('should add a user to a group', function (done) { + Groups.join('Test', 1, function (err) { if (err) return done(err); - Groups.isMember(1, 'Test', function(err, isMember) { + Groups.isMember(1, 'Test', function (err, isMember) { + assert.equal(err, null); assert.strictEqual(true, isMember); done(); @@ -270,12 +279,13 @@ describe('Groups', function() { }); }); - describe('.leave()', function() { - it('should remove a user from a group', function(done) { - Groups.leave('Test', 1, function(err) { + describe('.leave()', function () { + it('should remove a user from a group', function (done) { + Groups.leave('Test', 1, function (err) { if (err) return done(err); - Groups.isMember(1, 'Test', function(err, isMember) { + Groups.isMember(1, 'Test', function (err, isMember) { + assert.equal(err, null); assert.strictEqual(false, isMember); done(); @@ -284,20 +294,20 @@ describe('Groups', function() { }); }); - describe('.leaveAllGroups()', function() { - it('should remove a user from all groups', function(done) { - Groups.leaveAllGroups(1, function(err) { + describe('.leaveAllGroups()', function () { + it('should remove a user from all groups', function (done) { + Groups.leaveAllGroups(1, function (err) { if (err) return done(err); var groups = ['Test', 'Hidden']; - async.every(groups, function(group, next) { - Groups.isMember(1, group, function(err, isMember) { + async.every(groups, function (group, next) { + Groups.isMember(1, group, function (err, isMember) { if (err) done(err); else { next(!isMember); } }); - }, function(result) { + }, function (result) { assert(result); done(); @@ -306,12 +316,12 @@ describe('Groups', function() { }); }); - describe('.show()', function() { - it('should make a group visible', function(done) { - Groups.show('Test', function(err) { + describe('.show()', function () { + it('should make a group visible', function (done) { + Groups.show('Test', function (err) { assert.ifError(err); assert.equal(arguments.length, 1); - db.isSortedSetMember('groups:visible:createtime', 'Test', function(err, isMember) { + db.isSortedSetMember('groups:visible:createtime', 'Test', function (err, isMember) { assert.ifError(err); assert.strictEqual(isMember, true); done(); @@ -320,12 +330,12 @@ describe('Groups', function() { }); }); - describe('.hide()', function() { - it('should make a group hidden', function(done) { - Groups.hide('Test', function(err) { + describe('.hide()', function () { + it('should make a group hidden', function (done) { + Groups.hide('Test', function (err) { assert.ifError(err); assert.equal(arguments.length, 1); - db.isSortedSetMember('groups:visible:createtime', 'Test', function(err, isMember) { + db.isSortedSetMember('groups:visible:createtime', 'Test', function (err, isMember) { assert.ifError(err); assert.strictEqual(isMember, false); done(); @@ -334,7 +344,7 @@ describe('Groups', function() { }); }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/messaging.js b/test/messaging.js similarity index 59% rename from tests/messaging.js rename to test/messaging.js index 4758fde40b..9c85259ede 100644 --- a/tests/messaging.js +++ b/test/messaging.js @@ -9,14 +9,18 @@ var assert = require('assert'), Messaging = require('../src/messaging'), testUids; -describe('Messaging Library', function() { - before(function(done) { +describe('Messaging Library', function () { + before(function (done) { // Create 3 users: 1 admin, 2 regular async.parallel([ async.apply(User.create, { username: 'foo', password: 'bar' }), // admin async.apply(User.create, { username: 'baz', password: 'quux' }), // restricted user async.apply(User.create, { username: 'herp', password: 'derp' }) // regular user - ], function(err, uids) { + ], function (err, uids) { + if (err) { + return done(err); + } + testUids = uids; async.parallel([ async.apply(Groups.join, 'administrators', uids[0]), @@ -25,40 +29,40 @@ describe('Messaging Library', function() { }); }); - describe('.canMessage()', function() { - it('should not error out', function(done) { - Messaging.canMessageUser(testUids[1], testUids[2], function(err) { + describe('.canMessage()', function () { + it('should not error out', function (done) { + Messaging.canMessageUser(testUids[1], testUids[2], function (err) { assert.ifError(err); done(); }); }); - it('should allow messages to be sent to an unrestricted user', function(done) { - Messaging.canMessageUser(testUids[1], testUids[2], function(err) { + it('should allow messages to be sent to an unrestricted user', function (done) { + Messaging.canMessageUser(testUids[1], testUids[2], function (err) { assert.ifError(err); done(); }); }); - it('should NOT allow messages to be sent to a restricted user', function(done) { - User.setSetting(testUids[1], 'restrictChat', '1', function() { - Messaging.canMessageUser(testUids[2], testUids[1], function(err) { + it('should NOT allow messages to be sent to a restricted user', function (done) { + User.setSetting(testUids[1], 'restrictChat', '1', function () { + Messaging.canMessageUser(testUids[2], testUids[1], function (err) { assert.strictEqual(err.message, '[[error:chat-restricted]]'); done(); }); }); }); - it('should always allow admins through', function(done) { - Messaging.canMessageUser(testUids[0], testUids[1], function(err) { + it('should always allow admins through', function (done) { + Messaging.canMessageUser(testUids[0], testUids[1], function (err) { assert.ifError(err); done(); }); }); - it('should allow messages to be sent to a restricted user if restricted user follows sender', function(done) { - User.follow(testUids[1], testUids[2], function() { - Messaging.canMessageUser(testUids[2], testUids[1], function(err) { + it('should allow messages to be sent to a restricted user if restricted user follows sender', function (done) { + User.follow(testUids[1], testUids[2], function () { + Messaging.canMessageUser(testUids[2], testUids[1], function (err) { assert.ifError(err); done(); }); @@ -66,7 +70,7 @@ describe('Messaging Library', function() { }); }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000000..b0a5a2aa02 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--reporter dot +--timeout 10000 diff --git a/tests/mocks/databasemock.js b/test/mocks/databasemock.js similarity index 91% rename from tests/mocks/databasemock.js rename to test/mocks/databasemock.js index a895d25c8a..36e696f4d2 100644 --- a/tests/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -3,7 +3,7 @@ * ATTENTION: testing db is flushed before every use! */ -(function(module) { +(function (module) { 'use strict'; /*global require, before*/ @@ -29,22 +29,22 @@ if(!testDbConfig){ errorText = 'test_database is not defined'; winston.info( - '\n===========================================================\n'+ - 'Please, add parameters for test database in config.json\n'+ - 'For example (redis):\n'+ + '\n===========================================================\n' + + 'Please, add parameters for test database in config.json\n' + + 'For example (redis):\n' + '"test_database": {' + '\n' + ' "host": "127.0.0.1",' + '\n' + ' "port": "6379",' + '\n' + ' "password": "",' + '\n' + ' "database": "1"' + '\n' + - '}\n'+ + '}\n' + ' or (mongo):\n' + '"test_database": {' + '\n' + ' "host": "127.0.0.1",' + '\n' + ' "port": "27017",' + '\n' + ' "password": "",' + '\n' + ' "database": "1"' + '\n' + - '}\n'+ + '}\n' + ' or (mongo) in a replicaset' + '\n' + '"test_database": {' + '\n' + ' "host": "127.0.0.1,127.0.0.1,127.0.0.1",' + '\n' + @@ -52,7 +52,7 @@ ' "username": "",' + '\n' + ' "password": "",' + '\n' + ' "database": "nodebb_test"' + '\n' + - '}\n'+ + '}\n' + '===========================================================' ); winston.error(errorText); @@ -73,10 +73,14 @@ var db = require('../../src/database'), meta = require('../../src/meta'); - before(function(done) { - db.init(function(err) { + before(function (done) { + db.init(function (err) { + if (err) { + return done(err); + } + //Clean up - db.flushdb(function(err) { + db.flushdb(function (err) { if(err) { winston.error(err); throw new Error(err); diff --git a/test/notifications.js b/test/notifications.js new file mode 100644 index 0000000000..482af2d9bb --- /dev/null +++ b/test/notifications.js @@ -0,0 +1,102 @@ +'use strict'; +/*global require, after, before*/ + + +var assert = require('assert'); + +var db = require('./mocks/databasemock'); +var user = require('../src/user'); +var notifications = require('../src/notifications'); + +describe('Notifications', function () { + var uid; + var notification; + + before(function (done) { + user.create({username: 'poster'}, function (err, _uid) { + if (err) { + return done(err); + } + + uid = _uid; + done(); + }); + }); + + it('should create a notification', function (done) { + notifications.create({ + bodyShort: 'bodyShort', + nid: 'notification_id' + }, function (err, _notification) { + notification = _notification; + assert.ifError(err); + assert(notification); + db.exists('notifications:' + notification.nid, function (err, exists) { + assert.ifError(err); + assert(exists); + db.isSortedSetMember('notifications', notification.nid, function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }); + }); + }); + + it('should get notifications', function (done) { + notifications.getMultiple([notification.nid], function (err, notificationsData) { + assert.ifError(err); + assert(Array.isArray(notificationsData)); + assert(notificationsData[0]); + assert.equal(notification.nid, notificationsData[0].nid); + done(); + }); + }); + + it('should push a notification to uid', function (done) { + notifications.push(notification, [uid], function (err) { + assert.ifError(err); + setTimeout(function () { + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }, 2000); + }); + }); + + it('should mark a notification read', function (done) { + notifications.markRead(notification.nid, uid, function (err) { + assert.ifError(err); + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, false); + db.isSortedSetMember('uid:' + uid + ':notifications:read', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, true); + done(); + }); + }); + }); + }); + + it('should mark a notification unread', function (done) { + notifications.markUnread(notification.nid, uid, function (err) { + assert.ifError(err); + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, true); + db.isSortedSetMember('uid:' + uid + ':notifications:read', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, false); + done(); + }); + }); + }); + }); + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/test/posts.js b/test/posts.js new file mode 100644 index 0000000000..00c9aa01a3 --- /dev/null +++ b/test/posts.js @@ -0,0 +1,136 @@ +'use strict'; +/*global require, before, after*/ + +var assert = require('assert'); +var async = require('async'); + +var db = require('./mocks/databasemock'); +var topics = require('../src/topics'); +var posts = require('../src/posts'); +var categories = require('../src/categories'); +var user = require('../src/user'); + +describe('Post\'s', function () { + var voterUid; + var voteeUid; + var postData; + + before(function (done) { + async.parallel({ + voterUid: function (next) { + user.create({username: 'upvoter'}, next); + }, + voteeUid: function (next) { + user.create({username: 'upvotee'}, next); + }, + category: function (next) { + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script' + }, next); + } + }, function (err, results) { + if (err) { + return done(err); + } + + voterUid = results.voterUid; + voteeUid = results.voteeUid; + + topics.post({ + uid: results.voteeUid, + cid: results.category.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }, function (err, data) { + if (err) { + return done(err); + } + postData = data.postData; + done(); + }); + }); + }); + + describe('voting', function () { + + it('should upvote a post', function (done) { + posts.upvote(postData.pid, voterUid, function (err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 1); + assert.equal(result.post.downvotes, 0); + assert.equal(result.post.votes, 1); + assert.equal(result.user.reputation, 1); + posts.hasVoted(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.upvoted, true); + assert.equal(data.downvoted, false); + done(); + }); + }); + }); + + it('should unvote a post', function (done) { + posts.unvote(postData.pid, voterUid, function (err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 0); + assert.equal(result.post.downvotes, 0); + assert.equal(result.post.votes, 0); + assert.equal(result.user.reputation, 0); + posts.hasVoted(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.upvoted, false); + assert.equal(data.downvoted, false); + done(); + }); + }); + }); + + it('should downvote a post', function (done) { + posts.downvote(postData.pid, voterUid, function (err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 0); + assert.equal(result.post.downvotes, 1); + assert.equal(result.post.votes, -1); + assert.equal(result.user.reputation, -1); + posts.hasVoted(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.upvoted, false); + assert.equal(data.downvoted, true); + done(); + }); + }); + }); + }); + + describe('bookmarking', function () { + it('should bookmark a post', function (done) { + posts.bookmark(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.isBookmarked, true); + posts.hasBookmarked(postData.pid, voterUid, function (err, hasBookmarked) { + assert.ifError(err); + assert.equal(hasBookmarked, true); + done(); + }); + }); + }); + + it('should unbookmark a post', function (done) { + posts.unbookmark(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.isBookmarked, false); + posts.hasBookmarked([postData.pid], voterUid, function (err, hasBookmarked) { + assert.ifError(err); + assert.equal(hasBookmarked[0], false); + done(); + }); + }); + }); + }); + + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/test/topics.js b/test/topics.js new file mode 100644 index 0000000000..d0af967a0f --- /dev/null +++ b/test/topics.js @@ -0,0 +1,451 @@ +'use strict'; +/*global require, before, beforeEach, after*/ + +var assert = require('assert'); +var validator = require('validator'); +var db = require('./mocks/databasemock'); +var topics = require('../src/topics'); +var categories = require('../src/categories'); +var User = require('../src/user'); +var groups = require('../src/groups'); +var async = require('async'); + +describe('Topic\'s', function () { + var topic, + categoryObj; + + before(function (done) { + var userData = { + username: 'John Smith', + password: 'swordfish', + email: 'john@example.com', + callback: undefined + }; + + User.create({username: userData.username, password: userData.password, email: userData.email}, function (err, uid) { + if (err) { + return done(err); + } + + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + icon: 'fa-check', + blockclass: 'category-blue', + order: '5' + }, function (err, category) { + if (err) { + return done(err); + } + + categoryObj = category; + + topic = { + userId: uid, + categoryId: categoryObj.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }; + done(); + }); + }); + + + }); + + describe('.post', function () { + + it('should create a new topic with proper parameters', function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.equal(err, null, 'was created with error'); + assert.ok(result); + + done(); + }); + }); + + it('should fail to create new topic with invalid user id', function (done) { + topics.post({uid: null, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + + it('should fail to create new topic with empty title', function (done) { + topics.post({uid: topic.userId, title: '', content: topic.content, cid: topic.categoryId}, function (err) { + assert.ok(err); + done(); + }); + }); + + it('should fail to create new topic with empty content', function (done) { + topics.post({uid: topic.userId, title: topic.title, content: '', cid: topic.categoryId}, function (err) { + assert.ok(err); + done(); + }); + }); + + it('should fail to create new topic with non-existant category id', function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: 99}, function (err) { + assert.equal(err.message, '[[error:no-category]]', 'received no error'); + done(); + }); + }); + }); + + describe('.reply', function () { + var newTopic; + var newPost; + + before(function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + if (err) { + return done(err); + } + + newTopic = result.topicData; + newPost = result.postData; + done(); + }); + }); + + it('should create a new reply with proper parameters', function (done) { + topics.reply({uid: topic.userId, content: 'test post', tid: newTopic.tid}, function (err, result) { + assert.equal(err, null, 'was created with error'); + assert.ok(result); + + done(); + }); + }); + + it('should fail to create new reply with invalid user id', function (done) { + topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + + it('should fail to create new reply with empty content', function (done) { + topics.reply({uid: topic.userId, content: '', tid: newTopic.tid}, function (err) { + assert.ok(err); + done(); + }); + }); + + it('should fail to create new reply with invalid topic id', function (done) { + topics.reply({uid: null, content: 'test post', tid: 99}, function (err) { + assert.equal(err.message, '[[error:no-topic]]'); + done(); + }); + }); + }); + + describe('Get methods', function () { + var newTopic; + var newPost; + + before(function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + if (err) { + return done(err); + } + + newTopic = result.topicData; + newPost = result.postData; + done(); + }); + }); + + describe('.getTopicData', function () { + it('should not receive errors', function (done) { + topics.getTopicData(newTopic.tid, done); + }); + }); + + describe('.getTopicWithPosts', function () { + it('should get a topic with posts and other data', function (done) { + topics.getTopicData(newTopic.tid, function (err, topicData) { + if (err) { + return done(err); + } + topics.getTopicWithPosts(topicData, 'tid:' + newTopic.tid + ':posts', topic.userId, 0, -1, false, function (err, data) { + if (err) { + return done(err); + } + assert(data); + assert.equal(data.category.cid, topic.categoryId); + assert.equal(data.unreplied, true); + assert.equal(data.deleted, false); + assert.equal(data.locked, false); + assert.equal(data.pinned, false); + done(); + }); + }); + }); + }); + }); + + describe('Title escaping', function () { + + it('should properly escape topic title', function (done) { + var title = '" new topic test'; + var titleEscaped = validator.escape(title); + topics.post({uid: topic.userId, title: title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.ifError(err); + topics.getTopicData(result.topicData.tid, function (err, topicData) { + assert.ifError(err); + assert.strictEqual(topicData.titleRaw, title); + assert.strictEqual(topicData.title, titleEscaped); + done(); + }); + }); + }); + }); + + describe('.purge/.delete', function () { + var newTopic; + var followerUid; + before(function (done) { + async.waterfall([ + function (next) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.ifError(err); + newTopic = result.topicData; + next(); + }); + }, + function (next) { + User.create({username: 'topicFollower', password: '123456'}, next); + }, + function (_uid, next) { + followerUid = _uid; + topics.follow(newTopic.tid, _uid, next); + } + ], done); + }); + + it('should delete the topic', function (done) { + topics.delete(newTopic.tid, 1, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should purge the topic', function (done) { + topics.purge(newTopic.tid, 1, function (err) { + assert.ifError(err); + db.isSortedSetMember('uid:' + followerUid + ':followed_tids', newTopic.tid, function (err, isMember) { + assert.ifError(err); + assert.strictEqual(false, isMember); + done(); + }); + }); + }); + }); + + describe('.ignore', function (){ + var newTid; + var uid; + var newTopic; + before(function (done){ + uid = topic.userId; + async.waterfall([ + function (done){ + topics.post({uid: topic.userId, title: 'Topic to be ignored', content: 'Just ignore me, please!', cid: topic.categoryId}, function (err, result) { + if (err) { + return done(err); + } + + newTopic = result.topicData; + newTid = newTopic.tid; + done(); + }); + }, + function (done){ + topics.markUnread( newTid, uid, done ); + } + ],done); + }); + + it('should not appear in the unread list', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.getUnreadTopics(0, uid, 0, -1, '', done ); + }, + function (results, done){ + var topics = results.topics; + var tids = topics.map( function (topic){ return topic.tid; } ); + assert.equal(tids.indexOf(newTid), -1, 'The topic appeared in the unread list.'); + done(); + } + ], done); + }); + + it('should not appear as unread in the recent list', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.getLatestTopics( uid, 0, -1, 'year', done ); + }, + function (results, done){ + var topics = results.topics; + var topic; + var i; + for(i = 0; i < topics.length; ++i){ + if( topics[i].tid == newTid ){ + assert.equal(false, topics[i].unread, 'ignored topic was marked as unread in recent list'); + return done(); + } + } + assert.ok(topic, 'topic didn\'t appear in the recent list'); + done(); + } + ], done); + }); + + it('should appear as unread again when marked as reading', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.follow( newTid, uid, done ); + }, + function (done){ + topics.getUnreadTopics(0, uid, 0, -1, '', done ); + }, + function (results, done){ + var topics = results.topics; + var tids = topics.map( function (topic){ return topic.tid; } ); + assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); + done(); + } + ], done); + }); + + it('should appear as unread again when marked as following', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.follow( newTid, uid, done ); + }, + function (done){ + topics.getUnreadTopics(0, uid, 0, -1, '', done ); + }, + function (results, done){ + var topics = results.topics; + var tids = topics.map( function (topic){ return topic.tid; } ); + assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); + done(); + } + ], done); + }); + }); + + + + describe('.fork', function (){ + var newTopic; + var replies = []; + var topicPids; + var originalBookmark = 5; + function postReply( next ){ + topics.reply({uid: topic.userId, content: 'test post ' + replies.length, tid: newTopic.tid}, + function (err, result) { + assert.equal(err, null, 'was created with error'); + assert.ok(result); + replies.push( result ); + next(); + } + ); + } + + before( function (done) { + async.waterfall( + [ + function (next){ + groups.join('administrators', topic.userId, next); + }, + function ( next ){ + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.ifError( err ); + newTopic = result.topicData; + next(); + }); + }, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ + topicPids = replies.map( function ( reply ){ return reply.pid; } ); + topics.setUserBookmark( newTopic.tid, topic.userId, originalBookmark, next ); + }], + done ); + }); + + it('should have 12 replies', function (done) { + assert.equal( 12, replies.length ); + done(); + }); + + it('should not update the user\'s bookmark', function (done){ + async.waterfall([ + function (next){ + topics.createTopicFromPosts( + topic.userId, + 'Fork test, no bookmark update', + topicPids.slice( -2 ), + newTopic.tid, + next ); + }, + function ( forkedTopicData, next){ + topics.getUserBookmark( newTopic.tid, topic.userId, next ); + }, + function ( bookmark, next ){ + assert.equal( originalBookmark, bookmark ); + next(); + } + ],done); + }); + + it('should update the user\'s bookmark ', function (done){ + async.waterfall([ + function (next){ + topics.createTopicFromPosts( + topic.userId, + 'Fork test, no bookmark update', + topicPids.slice( 1, 3 ), + newTopic.tid, + next ); + }, + function ( forkedTopicData, next){ + topics.getUserBookmark( newTopic.tid, topic.userId, next ); + }, + function ( bookmark, next ){ + assert.equal( originalBookmark - 2, bookmark ); + next(); + } + ],done); + }); + }); + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/test/translator.js b/test/translator.js new file mode 100644 index 0000000000..5d8c234747 --- /dev/null +++ b/test/translator.js @@ -0,0 +1,188 @@ +'use strict'; +/*global require*/ + +var assert = require('assert'); +var shim = require('../public/src/modules/translator.js'); +var Translator = shim.Translator; + +require('../src/languages').init(function () {}); + +describe('translator shim', function (){ + describe('.translate()', function (){ + it('should translate correctly', function (done) { + shim.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]', function (translated) { + assert.strictEqual(translated, '(foobar) out of Home'); + done(); + }); + }); + }); +}); + +describe('new Translator(language)', function (){ + describe('.translate()', function (){ + it('should handle basic translations', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:home]]').then(function (translated) { + assert.strictEqual(translated, 'Home'); + done(); + }); + }); + + it('should handle language keys in regular text', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('Let\'s go [[global:home]]').then(function (translated) { + assert.strictEqual(translated, 'Let\'s go Home'); + done(); + }); + }); + + it('should accept a language parameter and adjust accordingly', function (done) { + var translator = new Translator('de'); + + translator.translate('[[global:home]]').then(function (translated) { + assert.strictEqual(translated, 'Übersicht'); + done(); + }); + }); + + it('should handle language keys in regular text with another language specified', function (done) { + var translator = new Translator('de'); + + translator.translate('[[global:home]] test').then(function (translated) { + assert.strictEqual(translated, 'Übersicht test'); + done(); + }); + }); + + it('should handle language keys with parameters', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, 1, 5]]').then(function (translated) { + assert.strictEqual(translated, '1 out of 5'); + done(); + }); + }); + + it('should handle language keys inside language keys', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:outgoing_link_message, [[global:guest]]]]').then(function (translated) { + assert.strictEqual(translated, 'You are now leaving Guest'); + done(); + }); + }); + + it('should handle language keys inside language keys with multiple parameters', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:user_posted_to, [[global:guest]], My Topic]]').then(function (translated) { + assert.strictEqual(translated, 'Guest has posted a reply to: My Topic'); + done(); + }); + }); + + it('should handle language keys inside language keys with all parameters as language keys', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:user_posted_to, [[global:guest]], [[global:guest]]]]').then(function (translated) { + assert.strictEqual(translated, 'Guest has posted a reply to: Guest'); + done(); + }); + }); + + it('should properly handle parameters that contain square brackets', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, [guest], [[global:home]]]]').then(function (translated) { + assert.strictEqual(translated, '[guest] out of Home'); + done(); + }); + }); + + it('should properly handle parameters that contain parentheses', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]').then(function (translated) { + assert.strictEqual(translated, '(foobar) out of Home'); + done(); + }); + }); + + it('should not translate language key parameters with HTML in them', function (done) { + var translator = new Translator('en_GB'); + + var key = '[[global:403.login, test]]'; + translator.translate(key).then(function (translated) { + assert.strictEqual(translated, 'Perhaps you should try logging in?'); + done(); + }); + }); + + it('should properly escape % and ,', function (done) { + var translator = new Translator('en_GB'); + + var title = 'Test 1, 2, 3 % salmon'; + title = title.replace(/%/g, '%').replace(/,/g, ','); + var key = "[[topic:composer.replying_to, " + title + "]]"; + translator.translate(key).then(function (translated) { + assert.strictEqual(translated, 'Replying to Test 1, 2, 3 % salmon'); + done(); + }); + }); + + it('should throw if not passed a language', function (done) { + assert.throws(function () { + new Translator(); + }, /language string/); + done(); + }); + + it('should not translate [[derp] some text', function (done) { + var translator = new Translator('en_GB'); + translator.translate('[[derp] some text').then(function (translated) { + assert.strictEqual('[[derp] some text', translated); + done(); + }); + }); + + it('should not translate [[derp:xyz] some text', function (done) { + var translator = new Translator('en_GB'); + translator.translate('[[derp:xyz] some text').then(function (translated) { + assert.strictEqual('[[derp:xyz] some text', translated); + done(); + }); + }); + + it('should translate [[pages:users/latest]] properly', function (done) { + var translator = new Translator('en_GB'); + translator.translate('[[pages:users/latest]]').then(function (translated) { + assert.strictEqual(translated, 'Latest Users'); + done(); + }); + }); + }); +}); + +describe('Translator.create()', function (){ + it('should return an instance of Translator', function (done) { + var translator = Translator.create('en_GB'); + + assert(translator instanceof Translator); + done(); + }); + it('should return the same object for the same language', function (done) { + var one = Translator.create('de'); + var two = Translator.create('de'); + + assert.strictEqual(one, two); + done(); + }); + it('should default to defaultLang', function (done) { + var translator = Translator.create(); + + assert.strictEqual(translator.lang, 'en_GB'); + done(); + }); +}); diff --git a/tests/user.js b/test/user.js similarity index 52% rename from tests/user.js rename to test/user.js index 570b002808..5e30d2fb5a 100644 --- a/tests/user.js +++ b/test/user.js @@ -17,23 +17,30 @@ var User = require('../src/user'), Meta = require('../src/meta'), Password = require('../src/password'); -describe('User', function() { +describe('User', function () { var userData, testUid, testCid; - before(function(done) { + before(function (done) { + var groups = require('../src/groups'); + groups.resetCache(); + Categories.create({ name: 'Test Category', description: 'A test', order: 1 - }, function(err, categoryObj) { + }, function (err, categoryObj) { + if (err) { + return done(err); + } + testCid = categoryObj.cid; done(); }); }); - beforeEach(function(){ + beforeEach(function () { userData = { username: 'John Smith', fullname: 'John Smith McNamara', @@ -44,9 +51,9 @@ describe('User', function() { }); - describe('.create(), when created', function() { - it('should be created properly', function(done) { - User.create({username: userData.username, password: userData.password, email: userData.email}, function(error,userId){ + describe('.create(), when created', function () { + it('should be created properly', function (done) { + User.create({username: userData.username, password: userData.password, email: userData.email}, function (error,userId){ assert.equal(error, null, 'was created with error'); assert.ok(userId); @@ -55,8 +62,8 @@ describe('User', function() { }); }); - it('should have a valid email, if using an email', function(done) { - User.create({username: userData.username, password: userData.password, email: 'fakeMail'},function(err) { + it('should have a valid email, if using an email', function (done) { + User.create({username: userData.username, password: userData.password, email: 'fakeMail'},function (err) { assert(err); assert.equal(err.message, '[[error:invalid-email]]'); done(); @@ -64,24 +71,27 @@ describe('User', function() { }); }); - describe('.isModerator()', function() { - it('should return false', function(done) { - User.isModerator(testUid, testCid, function(err, isModerator) { + describe('.isModerator()', function () { + it('should return false', function (done) { + User.isModerator(testUid, testCid, function (err, isModerator) { + assert.equal(err, null); assert.equal(isModerator, false); done(); }); }); - it('should return two false results', function(done) { - User.isModerator([testUid, testUid], testCid, function(err, isModerator) { + it('should return two false results', function (done) { + User.isModerator([testUid, testUid], testCid, function (err, isModerator) { + assert.equal(err, null); assert.equal(isModerator[0], false); assert.equal(isModerator[1], false); done(); }); }); - it('should return two false results', function(done) { - User.isModerator(testUid, [testCid, testCid], function(err, isModerator) { + it('should return two false results', function (done) { + User.isModerator(testUid, [testCid, testCid], function (err, isModerator) { + assert.equal(err, null); assert.equal(isModerator[0], false); assert.equal(isModerator[1], false); done(); @@ -89,8 +99,8 @@ describe('User', function() { }); }); - describe('.isReadyToPost()', function() { - it('should error when a user makes two posts in quick succession', function(done) { + describe('.isReadyToPost()', function () { + it('should error when a user makes two posts in quick succession', function (done) { Meta.config = Meta.config || {}; Meta.config.postDelay = '10'; @@ -107,54 +117,54 @@ describe('User', function() { content: 'lorem ipsum', cid: testCid }) - ], function(err) { + ], function (err) { assert(err); done(); }); }); - it('should allow a post if the last post time is > 10 seconds', function(done) { - User.setUserField(testUid, 'lastposttime', +new Date()-(11*1000), function() { + it('should allow a post if the last post time is > 10 seconds', function (done) { + User.setUserField(testUid, 'lastposttime', +new Date() - (11 * 1000), function () { Topics.post({ uid: testUid, title: 'Topic 3', content: 'lorem ipsum', cid: testCid - }, function(err) { + }, function (err) { assert.ifError(err); done(); }); }); }); - it('should error when a new user posts if the last post time is 10 < 30 seconds', function(done) { + it('should error when a new user posts if the last post time is 10 < 30 seconds', function (done) { Meta.config.newbiePostDelay = 30; Meta.config.newbiePostDelayThreshold = 3; - User.setUserField(testUid, 'lastposttime', +new Date()-(20*1000), function() { + User.setUserField(testUid, 'lastposttime', +new Date() - (20 * 1000), function () { Topics.post({ uid: testUid, title: 'Topic 4', content: 'lorem ipsum', cid: testCid - }, function(err) { + }, function (err) { assert(err); done(); }); }); }); - it('should not error if a non-newbie user posts if the last post time is 10 < 30 seconds', function(done) { + it('should not error if a non-newbie user posts if the last post time is 10 < 30 seconds', function (done) { User.setUserFields(testUid, { - lastposttime: +new Date()-(20*1000), + lastposttime: +new Date() - (20 * 1000), reputation: 10 - }, function() { + }, function () { Topics.post({ uid: testUid, title: 'Topic 5', content: 'lorem ipsum', cid: testCid - }, function(err) { + }, function (err) { assert.ifError(err); done(); }); @@ -162,9 +172,9 @@ describe('User', function() { }); }); - describe('.search()', function() { - it('should return an object containing an array of matching users', function(done) { - User.search({query: 'john'}, function(err, searchData) { + describe('.search()', function () { + it('should return an object containing an array of matching users', function (done) { + User.search({query: 'john'}, function (err, searchData) { assert.ifError(err); assert.equal(Array.isArray(searchData.users) && searchData.users.length > 0, true); assert.equal(searchData.users[0].username, 'John Smith'); @@ -173,20 +183,20 @@ describe('User', function() { }); }); - describe('.delete()', function() { + describe('.delete()', function () { var uid; - before(function(done) { - User.create({username: 'usertodelete', password: '123456', email: 'delete@me.com'}, function(err, newUid) { + before(function (done) { + User.create({username: 'usertodelete', password: '123456', email: 'delete@me.com'}, function (err, newUid) { assert.ifError(err); uid = newUid; done(); }); }); - it('should delete a user account', function(done) { - User.delete(1, uid, function(err) { + it('should delete a user account', function (done) { + User.delete(1, uid, function (err) { assert.ifError(err); - User.existsBySlug('usertodelete', function(err, exists) { + User.existsBySlug('usertodelete', function (err, exists) { assert.ifError(err); assert.equal(exists, false); done(); @@ -195,19 +205,19 @@ describe('User', function() { }); }); - describe('passwordReset', function() { + describe('passwordReset', function () { var uid, code; - before(function(done) { - User.create({username: 'resetuser', password: '123456', email: 'reset@me.com'}, function(err, newUid) { + before(function (done) { + User.create({username: 'resetuser', password: '123456', email: 'reset@me.com'}, function (err, newUid) { assert.ifError(err); uid = newUid; done(); }); }); - it('.generate() should generate a new reset code', function(done) { - User.reset.generate(uid, function(err, _code) { + it('.generate() should generate a new reset code', function (done) { + User.reset.generate(uid, function (err, _code) { assert.ifError(err); assert(_code); @@ -216,36 +226,36 @@ describe('User', function() { }); }); - it('.validate() should ensure that this new code is valid', function(done) { - User.reset.validate(code, function(err, valid) { + it('.validate() should ensure that this new code is valid', function (done) { + User.reset.validate(code, function (err, valid) { assert.ifError(err); assert.strictEqual(valid, true); done(); }); }); - it('.validate() should correctly identify an invalid code', function(done) { - User.reset.validate(code + 'abcdef', function(err, valid) { + it('.validate() should correctly identify an invalid code', function (done) { + User.reset.validate(code + 'abcdef', function (err, valid) { assert.ifError(err); assert.strictEqual(valid, false); done(); }); }); - it('.send() should create a new reset code and reset password', function(done) { - User.reset.send('reset@me.com', function(err, code) { + it('.send() should create a new reset code and reset password', function (done) { + User.reset.send('reset@me.com', function (err, code) { assert.ifError(err); done(); }); }); - it('.commit() should update the user\'s password', function(done) { - User.reset.commit(code, 'newpassword', function(err) { + it('.commit() should update the user\'s password', function (done) { + User.reset.commit(code, 'newpassword', function (err) { assert.ifError(err); - db.getObjectField('user:' + uid, 'password', function(err, newPassword) { + db.getObjectField('user:' + uid, 'password', function (err, newPassword) { assert.ifError(err); - Password.compare('newpassword', newPassword, function(err, match) { + Password.compare('newpassword', newPassword, function (err, match) { assert.ifError(err); assert(match); done(); @@ -255,26 +265,26 @@ describe('User', function() { }); }); - describe('hash methods', function() { + describe('hash methods', function () { - it('should return uid from email', function(done) { - User.getUidByEmail('john@example.com', function(err, uid) { + it('should return uid from email', function (done) { + User.getUidByEmail('john@example.com', function (err, uid) { assert.ifError(err); assert.equal(parseInt(uid, 10), parseInt(testUid, 10)); done(); }); }); - it('should return uid from username', function(done) { - User.getUidByUsername('John Smith', function(err, uid) { + it('should return uid from username', function (done) { + User.getUidByUsername('John Smith', function (err, uid) { assert.ifError(err); assert.equal(parseInt(uid, 10), parseInt(testUid, 10)); done(); }); }); - it('should return uid from userslug', function(done) { - User.getUidByUserslug('john-smith', function(err, uid) { + it('should return uid from userslug', function (done) { + User.getUidByUserslug('john-smith', function (err, uid) { assert.ifError(err); assert.equal(parseInt(uid, 10), parseInt(testUid, 10)); done(); @@ -282,7 +292,43 @@ describe('User', function() { }); }); - after(function() { - db.flushdb(); + describe('updateProfile', function () { + var uid; + before(function (done) { + User.create({username: 'updateprofile', email: 'update@me.com'}, function (err, newUid) { + assert.ifError(err); + uid = newUid; + done(); + }); + }); + + it('should update a user\'s profile', function (done) { + var data = { + username: 'updatedUserName', + email: 'updatedEmail@me.com', + fullname: 'updatedFullname', + website: 'http://nodebb.org', + location: 'izmir', + groupTitle: 'testGroup', + birthday: '01/01/1980', + signature: 'nodebb is good' + }; + + User.updateProfile(uid, data, function (err, result) { + assert.ifError(err); + assert(result); + db.getObject('user:' + uid, function (err, userData) { + assert.ifError(err); + Object.keys(data).forEach(function (key) { + assert.equal(data[key], userData[key]); + }); + done(); + }); + }); + }); + }); + + after(function (done) { + db.flushdb(done); }); }); \ No newline at end of file diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000000..d342eca7a5 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,55 @@ +'use strict'; +/*global require*/ + +var assert = require('assert'); +var utils = require('./../public/src/utils.js'); + + +describe('Utility Methods', function () { + describe('username validation', function () { + it('accepts latin-1 characters', function (){ + var username = "John\"'-. Doeäâèéë1234"; + assert(utils.isUserNameValid(username), 'invalid username'); + }); + it('rejects empty string', function () { + var username = ''; + assert.ifError(utils.isUserNameValid(username), 'accepted as valid username'); + }); + }); + + describe('email validation', function () { + it('accepts sample address', function () { + var email = 'sample@example.com'; + assert(utils.isEmailValid(email), 'invalid email'); + }); + it('rejects empty address', function () { + var email = ''; + assert.ifError(utils.isEmailValid(email), 'accepted as valid email'); + }); + }); + + describe('UUID generation', function () { + it('return unique random value every time', function (){ + var uuid1 = utils.generateUUID(), + uuid2 = utils.generateUUID(); + assert.notEqual(uuid1, uuid2, 'matches'); + }); + }); + + describe('cleanUpTag', function () { + it('should cleanUp a tag', function (done) { + var cleanedTag = utils.cleanUpTag(',\/#!$%\^\*;TaG1:{}=_`<>\'"~()?\|'); + assert.equal(cleanedTag, 'tag1'); + done(); + }); + + it('should return empty string for invalid tags', function (done) { + assert.strictEqual(utils.cleanUpTag(undefined), ''); + assert.strictEqual(utils.cleanUpTag(null), ''); + assert.strictEqual(utils.cleanUpTag(false), ''); + assert.strictEqual(utils.cleanUpTag(1), ''); + assert.strictEqual(utils.cleanUpTag(0), ''); + done(); + }); + }); +}); diff --git a/tests/categories.js b/tests/categories.js deleted file mode 100644 index 7115b88597..0000000000 --- a/tests/categories.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; -/*global require, process, after*/ - -var winston = require('winston'); - -process.on('uncaughtException', function (err) { - winston.error('Encountered error while running test suite: ' + err.message); -}); - -var assert = require('assert'), - db = require('./mocks/databasemock'); - -var Categories = require('../src/categories'); - -describe('Categories', function() { - var categoryObj; - - describe('.create', function() { - it('should create a new category', function(done) { - - Categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - icon: 'fa-check', - blockclass: 'category-blue', - order: '5' - }, function(err, category) { - categoryObj = category; - done.apply(this, arguments); - }); - }); - }); - - describe('.getCategoryById', function() { - it('should retrieve a newly created category by its ID', function(done) { - Categories.getCategoryById({ - cid: categoryObj.cid, - set: 'cid:' + categoryObj.cid + ':tids', - reverse: true, - start: 0, - end: -1, - uid: 0 - }, function(err, categoryData) { - assert(categoryData); - assert.equal(categoryObj.name, categoryData.name); - assert.equal(categoryObj.description, categoryData.description); - - done(); - }); - }); - }); - - describe('.getCategoryTopics', function() { - it('should return a list of topics', function(done) { - Categories.getCategoryTopics({ - cid: categoryObj.cid, - set: 'cid:' + categoryObj.cid + ':tids', - reverse: true, - start: 0, - stop: 10, - uid: 0 - }, function(err, result) { - assert(Array.isArray(result.topics)); - assert(result.topics.every(function(topic) { - return topic instanceof Object; - })); - - done(); - }); - }); - - it('should return a list of topics by a specific user', function(done) { - Categories.getCategoryTopics({ - cid: categoryObj.cid, - set: 'cid:' + categoryObj.cid + ':uid:' + 1 + ':tids', - reverse: true, - start: 0, - stop: 10, - uid: 0, - targetUid: 1 - }, function(err, result) { - assert(Array.isArray(result.topics)); - assert(result.topics.every(function(topic) { - return topic instanceof Object && topic.uid === '1'; - })); - - done(); - }); - }); - }); - - after(function() { - db.flushdb(); - }); -}); diff --git a/tests/topics.js b/tests/topics.js deleted file mode 100644 index fe00db7646..0000000000 --- a/tests/topics.js +++ /dev/null @@ -1,194 +0,0 @@ -'use strict'; -/*global require, before, beforeEach, after*/ - -var assert = require('assert'); -var validator = require('validator'); -var db = require('./mocks/databasemock'); -var topics = require('../src/topics'); -var categories = require('../src/categories'); -var User = require('../src/user'); - -describe('Topic\'s', function() { - var topic, - categoryObj; - - before(function(done) { - var userData = { - username: 'John Smith', - password: 'swordfish', - email: 'john@example.com', - callback: undefined - }; - - User.create({username: userData.username, password: userData.password, email: userData.email}, function(err, uid) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - icon: 'fa-check', - blockclass: 'category-blue', - order: '5' - }, function(err, category) { - categoryObj = category; - - topic = { - userId: uid, - categoryId: categoryObj.cid, - title: 'Test Topic Title', - content: 'The content of test topic' - }; - done(); - }); - }); - - - }); - - describe('.post', function() { - - it('should create a new topic with proper parameters', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.equal(err, null, 'was created with error'); - assert.ok(result); - - done(); - }); - }); - - it('should fail to create new topic with invalid user id', function(done) { - topics.post({uid: null, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.equal(err.message, '[[error:no-privileges]]'); - done(); - }); - }); - - it('should fail to create new topic with empty title', function(done) { - topics.post({uid: topic.userId, title: '', content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.ok(err); - done(); - }); - }); - - it('should fail to create new topic with empty content', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: '', cid: topic.categoryId}, function(err, result) { - assert.ok(err); - done(); - }); - }); - - it('should fail to create new topic with non-existant category id', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: 99}, function(err, result) { - assert.equal(err.message, '[[error:no-category]]', 'received no error'); - done(); - }); - }); - }); - - describe('.reply', function() { - var newTopic; - var newPost; - - before(function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - newTopic = result.topicData; - newPost = result.postData; - done(); - }); - }); - - it('should create a new reply with proper parameters', function(done) { - topics.reply({uid: topic.userId, content: 'test post', tid: newTopic.tid}, function(err, result) { - assert.equal(err, null, 'was created with error'); - assert.ok(result); - - done(); - }); - }); - - it('should fail to create new reply with invalid user id', function(done) { - topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function(err, result) { - assert.equal(err.message, '[[error:no-privileges]]'); - done(); - }); - }); - - it('should fail to create new reply with empty content', function(done) { - topics.reply({uid: topic.userId, content: '', tid: newTopic.tid}, function(err, result) { - assert.ok(err); - done(); - }); - }); - - it('should fail to create new reply with invalid topic id', function(done) { - topics.reply({uid: null, content: 'test post', tid: 99}, function(err, result) { - assert.equal(err.message, '[[error:no-topic]]'); - done(); - }); - }); - }); - - describe('Get methods', function() { - var newTopic; - var newPost; - - beforeEach(function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - newTopic = result.topicData; - newPost = result.postData; - done(); - }); - }); - - describe('.getTopicData', function() { - it('should not receive errors', function(done) { - topics.getTopicData(newTopic.tid, done); - }); - }); - }); - - describe('Title escaping', function() { - - it('should properly escape topic title', function(done) { - var title = '" new topic test'; - var titleEscaped = validator.escape(title); - topics.post({uid: topic.userId, title: title, content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.ifError(err); - topics.getTopicData(result.topicData.tid, function(err, topicData) { - assert.ifError(err); - assert.strictEqual(topicData.titleRaw, title); - assert.strictEqual(topicData.title, titleEscaped); - done(); - }); - }); - }); - }); - - describe('.purge/.delete', function() { - var newTopic; - - before(function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - newTopic = result.topicData; - done(); - }); - }); - - it('should delete the topic', function(done) { - topics.delete(newTopic.tid, 1, function(err) { - assert.ifError(err); - done(); - }); - }); - - it('should purge the topic', function(done) { - topics.purge(newTopic.tid, 1, function(err) { - assert.ifError(err); - done(); - }); - }); - }); - - - after(function() { - db.flushdb(); - }); -}); diff --git a/tests/translator.js b/tests/translator.js deleted file mode 100644 index 7546ec1d90..0000000000 --- a/tests/translator.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; -/*global require*/ - -var assert = require('assert'); -var translator = require('../public/src/modules/translator.js'); - - -describe('Translator', function(){ - describe('.translate()', function(){ - it('should handle basic translations', function(done) { - translator.translate('[[global:home]]', function(translated) { - assert.strictEqual(translated, 'Home'); - done(); - }); - }); - - it('should handle language keys in regular text', function(done) { - translator.translate('Let\'s go [[global:home]]', function(translated) { - assert.strictEqual(translated, 'Let\'s go Home'); - done(); - }); - }); - - it('should accept a language parameter and adjust accordingly', function(done) { - translator.translate('[[global:home]]', 'de', function(translated) { - assert.strictEqual(translated, 'Übersicht'); - done(); - }); - }); - - it('should handle language keys in regular text with another language specified', function(done) { - translator.translate('[[global:home]] test', 'de', function(translated) { - assert.strictEqual(translated, 'Übersicht test'); - done(); - }); - }); - - it('should handle language keys with parameters', function(done) { - translator.translate('[[global:pagination.out_of, 1, 5]]', function(translated) { - assert.strictEqual(translated, '1 out of 5'); - done(); - }); - }); - - it('should handle language keys inside language keys', function(done) { - translator.translate('[[notifications:outgoing_link_message, [[global:guest]]]]', function(translated) { - assert.strictEqual(translated, 'You are now leaving Guest'); - done(); - }); - }); - - it('should handle language keys inside language keys with multiple parameters', function(done) { - translator.translate('[[notifications:user_posted_to, [[global:guest]], My Topic]]', function(translated) { - assert.strictEqual(translated, 'Guest has posted a reply to: My Topic'); - done(); - }); - }); - - it('should handle language keys inside language keys with all parameters as language keys', function(done) { - translator.translate('[[notifications:user_posted_to, [[global:guest]], [[global:guest]]]]', function(translated) { - assert.strictEqual(translated, 'Guest has posted a reply to: Guest'); - done(); - }); - }); - - it('should properly handle parameters that contain square brackets', function(done) { - translator.translate('[[global:pagination.out_of, [guest], [[global:home]]]]', function(translated) { - assert.strictEqual(translated, '[guest] out of Home'); - done(); - }); - }); - - it('should properly handle parameters that contain parentheses', function(done) { - translator.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]', function(translated) { - assert.strictEqual(translated, '(foobar) out of Home'); - done(); - }); - }); - - it('should not translate language key parameters with HTML in them', function(done) { - var key = '[[global:403.login, test]]'; - translator.translate(key, function(translated) { - assert.strictEqual(translated, 'Perhaps you should try logging in?'); - done(); - }); - }); - - it('should properly escape % and ,', function(done) { - var title = 'Test 1, 2, 3 % salmon'; - title = title.replace(/%/g, '%').replace(/,/g, ','); - var key = "[[topic:composer.replying_to, " + title + "]]"; - translator.translate(key, function(translated) { - assert.strictEqual(translated, 'Replying to Test 1, 2, 3 % salmon'); - done(); - }); - }); - - }); -}); diff --git a/tests/utils.js b/tests/utils.js deleted file mode 100644 index befb2f78d2..0000000000 --- a/tests/utils.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; -/*global require*/ - -var assert = require('assert'), - utils = require('./../public/src/utils.js'); - - -describe('Utility Methods', function(){ - describe('username validation', function(){ - it('accepts latin-1 characters', function(){ - var username = "John\"'-. Doeäâèéë1234"; - assert(utils.isUserNameValid(username), 'invalid username'); - }); - it('rejects empty string', function(){ - var username = ''; - assert.ifError(utils.isUserNameValid(username), 'accepted as valid username'); - }); - }); - - describe('email validation', function(){ - it('accepts sample address', function(){ - var email = 'sample@example.com'; - assert(utils.isEmailValid(email), 'invalid email'); - }); - it('rejects empty address', function(){ - var email = ''; - assert.ifError(utils.isEmailValid(email), 'accepted as valid email'); - }); - }); - - describe('UUID generation', function(){ - it('return unique random value every time', function(){ - var uuid1 = utils.generateUUID(), - uuid2 = utils.generateUUID(); - assert.notEqual(uuid1, uuid2, 'matches'); - }); - }); -});