diff --git a/CHANGELOG.md b/CHANGELOG.md index 642846a6e..ca5b078fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 1.2.91 + * NEW: Authors can now send custom object messages to other Authors making 3 way conversations possible. This introduces WebRTC plugin support. + * NEW: Hook for Chat Messages Allows for Desktop Notification support + * NEW: FreeBSD installation docs + * Fix: Cookies inside of plugins + * Fix: Refactor Caret navigation with Arrow and Pageup/down keys stops cursor being lost + * Fix: Long lines in Firefox now wrap properly + * Fix: Log HTTP on DEBUG log level + * Fix: Server wont crash on import fails on 0 file import. + * Fix: Import no longer fails consistantly + * Fix: Language support for non existing languages + * Fix: Mobile support for chat notifications are now usable + * Fix: Re-Enable Editbar buttons on reconnect + * Fix: Clearing authorship colors no longer disconnects all clients + # 1.2.9 * Fix: MAJOR Security issue, where a hacker could submit content as another user * Fix: security issue due to unescaped user input @@ -6,7 +21,7 @@ * Fix: PadUsers API endpoint * NEW: A script to import data to all dbms * NEW: Add authorId to chat and userlist as a data attribute - * NEW Refactor and fix our frontend tests + * NEW: Refactor and fix our frontend tests * NEW: Localisation updates diff --git a/doc/api/http_api.md b/doc/api/http_api.md index 7e05ff86d..f71e0a5d0 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -458,4 +458,4 @@ returns ok when the current api token is valid lists all pads on this epl instance *Example returns:* - * `{code: 0, message:"ok", data: ["testPad", "thePadsOfTheOthers"]}` + * `{code: 0, message:"ok", data: {padIDs: ["testPad", "thePadsOfTheOthers"]}}` diff --git a/src/locales/en.json b/src/locales/en.json index 920a2b003..d08ebe657 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -16,7 +16,7 @@ "pad.toolbar.timeslider.title": "Timeslider", "pad.toolbar.savedRevision.title": "Save Revision", "pad.toolbar.settings.title": "Settings", - "pad.toolbar.embed.title": "Embed this pad", + "pad.toolbar.embed.title": "Share and Embed this pad", "pad.toolbar.showusers.title": "Show the users on this pad", "pad.colorpicker.save": "Save", "pad.colorpicker.cancel": "Cancel", @@ -113,4 +113,4 @@ "pad.impexp.importfailed": "Import failed", "pad.impexp.copypaste": "Please copy paste", "pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details." -} \ No newline at end of file +} diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 7e221cf1e..d8f19bba9 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -27,49 +27,84 @@ exports.socketio = function (hook_name, args, cb) { io.on('connection', function (socket) { if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return; - socket.on("load", function (query) { + socket.on("getInstalled", function (query) { // send currently installed plugins - socket.emit("installed-results", {results: plugins.plugins}); - socket.emit("progress", {progress:1}); + var installed = Object.keys(plugins.plugins).map(function(plugin) { + return plugins.plugins[plugin].package + }) + socket.emit("results:installed", {installed: installed}); }); socket.on("checkUpdates", function() { - socket.emit("progress", {progress:0, message:'Checking for plugin updates...'}); // Check plugins for updates - installer.search({offset: 0, pattern: '', limit: 500}, /*useCache:*/true, function(data) { // hacky - if (!data.results) return; + installer.getAvailablePlugins(/*maxCacheAge:*/60*10, function(er, results) { + if(er) { + console.warn(er); + socket.emit("results:updatable", {updatable: {}}); + return; + } var updatable = _(plugins.plugins).keys().filter(function(plugin) { - if(!data.results[plugin]) return false; - var latestVersion = data.results[plugin]['dist-tags'].latest + if(!results[plugin]) return false; + var latestVersion = results[plugin].version var currentVersion = plugins.plugins[plugin].package.version return semver.gt(latestVersion, currentVersion) }); - socket.emit("updatable", {updatable: updatable}); - socket.emit("progress", {progress:1}); + socket.emit("results:updatable", {updatable: updatable}); }); }) + + socket.on("getAvailable", function (query) { + installer.getAvailablePlugins(/*maxCacheAge:*/false, function (er, results) { + if(er) { + console.error(er) + results = {} + } + socket.emit("results:available", results); + }); + }); socket.on("search", function (query) { - socket.emit("progress", {progress:0, message:'Fetching results...'}); - installer.search(query, true, function (progress) { - if (progress.results) - socket.emit("search-result", progress); - socket.emit("progress", progress); + installer.search(query.searchTerm, /*maxCacheAge:*/60*10, function (er, results) { + if(er) { + console.error(er) + results = {} + } + var res = Object.keys(results) + .map(function(pluginName) { + return results[pluginName] + }) + .filter(function(plugin) { + return !plugins.plugins[plugin.name] + }); + res = sortPluginList(res, query.sortBy, query.sortDir) + .slice(query.offset, query.offset+query.limit); + socket.emit("results:search", {results: res, query: query}); }); }); socket.on("install", function (plugin_name) { - socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."}); - installer.install(plugin_name, function (progress) { - socket.emit("progress", progress); + installer.install(plugin_name, function (er) { + if(er) console.warn(er) + socket.emit("finished:install", {plugin: plugin_name, error: er? er.message : null}); }); }); socket.on("uninstall", function (plugin_name) { - socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."}); - installer.uninstall(plugin_name, function (progress) { - socket.emit("progress", progress); + installer.uninstall(plugin_name, function (er) { + if(er) console.warn(er) + socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null}); }); }); }); } + +function sortPluginList(plugins, property, /*ASC?*/dir) { + return plugins.sort(function(a, b) { + if (a[property] < b[property]) + return dir? -1 : 1; + if (a[property] > b[property]) + return dir? 1 : -1; + // a must be equal to b + return 0; + }) +} \ No newline at end of file diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 3c5956835..825c5f3b4 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -28,6 +28,7 @@ exports.gracefulShutdown = function(err) { }, 3000); } +process.on('uncaughtException', exports.gracefulShutdown); exports.expressCreateServer = function (hook_name, args, cb) { exports.app = args.app; @@ -47,6 +48,4 @@ exports.expressCreateServer = function (hook_name, args, cb) { //https://github.com/joyent/node/issues/1553 process.on('SIGINT', exports.gracefulShutdown); } - - process.on('uncaughtException', exports.gracefulShutdown); -} +} \ No newline at end of file diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js index 27138e648..925203433 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js @@ -63,7 +63,7 @@ if(os.type().indexOf("Windows") > -1) callback(); }); - } + }; exports.convertFile = function(srcFile, destFile, type, callback) { @@ -121,7 +121,7 @@ else firstPrompt = false; } }); - } + }; spawnAbiword(); doConvertTask = function(task, callback) @@ -135,7 +135,7 @@ else console.log("queue continue"); task.callback(err); }; - } + }; //Queue with the converts we have to do var queue = async.queue(doConvertTask, 1); diff --git a/src/node/utils/ExportDokuWiki.js b/src/node/utils/ExportDokuWiki.js index d2f71236b..f5d2d177f 100644 --- a/src/node/utils/ExportDokuWiki.js +++ b/src/node/utils/ExportDokuWiki.js @@ -316,7 +316,7 @@ exports.getPadDokuWikiDocument = function (padId, revNum, callback) getPadDokuWiki(pad, revNum, callback); }); -} +}; function _escapeDokuWiki(s) { diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index a939a8b6e..136896f06 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -45,7 +45,7 @@ exports.getPadPlainText = function(pad, revNum){ } return pieces.join(''); -} +}; exports._analyzeLine = function(text, aline, apool){ @@ -77,11 +77,11 @@ exports._analyzeLine = function(text, aline, apool){ line.aline = aline; } return line; -} +}; exports._encodeWhitespace = function(s){ return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c){ - return "" +c.charCodeAt(0) + ";" + return "" +c.charCodeAt(0) + ";"; }); -} +}; diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 585694d4b..7b94310a3 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -21,7 +21,7 @@ var padManager = require("../db/PadManager"); var ERR = require("async-stacktrace"); var Security = require('ep_etherpad-lite/static/js/security'); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var getPadPlainText = require('./ExportHelper').getPadPlainText +var getPadPlainText = require('./ExportHelper').getPadPlainText; var _analyzeLine = require('./ExportHelper')._analyzeLine; var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; @@ -515,7 +515,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback) callback(null, head + html + foot); }); }); -} +}; // copied from ACE @@ -595,4 +595,3 @@ function _processSpaces(s){ } return parts.join(''); } - diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index c57424f1d..f0b62743a 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -289,5 +289,4 @@ exports.getPadTXTDocument = function (padId, revNum, noDocType, callback) callback(null, html); }); }); -} - +}; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 5fc8accbb..58d08b30e 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -125,11 +125,11 @@ function requestURIs(locations, method, headers, callback) { } function completed() { - var statuss = responses.map(function (x) {return x[0]}); - var headerss = responses.map(function (x) {return x[1]}); - var contentss = responses.map(function (x) {return x[2]}); + var statuss = responses.map(function (x) {return x[0];}); + var headerss = responses.map(function (x) {return x[1];}); + var contentss = responses.map(function (x) {return x[2];}); callback(statuss, headerss, contentss); - }; + } } /** @@ -263,7 +263,7 @@ function getAceFile(callback) { var filename = item.match(/"([^"]*)"/)[1]; var request = require('request'); - var baseURI = 'http://localhost:' + settings.port + var baseURI = 'http://localhost:' + settings.port; var resourceURI = baseURI + path.normalize(path.join('/static/', filename)); resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 45f81aa5f..a6c71a85e 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -137,7 +137,7 @@ exports.abiwordAvailable = function() { return "no"; } -} +}; exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives @@ -157,7 +157,7 @@ exports.reloadSettings = function reloadSettings() { try { if(settingsStr) { settings = vm.runInContext('exports = '+settingsStr, vm.createContext(), "settings.json"); - settings = JSON.parse(JSON.stringify(settings)) // fix objects having constructors of other vm.context + settings = JSON.parse(JSON.stringify(settings)); // fix objects having constructors of other vm.context } }catch(e){ console.error('There was an error processing your settings.json file: '+e.message); @@ -196,9 +196,9 @@ exports.reloadSettings = function reloadSettings() { } if(exports.dbType === "dirty"){ - console.warn("DirtyDB is used. This is fine for testing but not recommended for production.") + console.warn("DirtyDB is used. This is fine for testing but not recommended for production."); } -} +}; // initially load settings exports.reloadSettings(); diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index c6b237139..1d103ffd6 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -23,7 +23,7 @@ var util = require('util'); var settings = require('./Settings'); var semver = require('semver'); -var existsSync = (semver.satisfies(process.version, '>=0.8.0')) ? fs.existsSync : path.existsSync +var existsSync = (semver.satisfies(process.version, '>=0.8.0')) ? fs.existsSync : path.existsSync; var CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; @@ -133,7 +133,7 @@ CachingMiddleware.prototype = new function () { old_res.write = res.write; old_res.end = res.end; res.write = function(data, encoding) {}; - res.end = function(data, encoding) { respond() }; + res.end = function(data, encoding) { respond(); }; } else { res.writeHead(status, headers); } diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 1b3cf58f5..c53540417 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -68,7 +68,7 @@ PadDiff.prototype._isClearAuthorship = function(changeset){ return false; return true; -} +}; PadDiff.prototype._createClearAuthorship = function(rev, callback){ var self = this; @@ -84,7 +84,7 @@ PadDiff.prototype._createClearAuthorship = function(rev, callback){ callback(null, changeset); }); -} +}; PadDiff.prototype._createClearStartAtext = function(rev, callback){ var self = this; @@ -107,7 +107,7 @@ PadDiff.prototype._createClearStartAtext = function(rev, callback){ callback(null, newAText); }); }); -} +}; PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { var self = this; @@ -124,7 +124,7 @@ PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { async.forEach(revisions, function(rev, callback){ self._pad.getRevision(rev, function(err, revision){ if(err){ - return callback(err) + return callback(err); } var arrayNum = rev-startRev; @@ -137,7 +137,7 @@ PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { }, function(err){ callback(err, changesets, authors); }); -} +}; PadDiff.prototype._addAuthors = function(authors) { var self = this; @@ -147,7 +147,7 @@ PadDiff.prototype._addAuthors = function(authors) { self._authors.push(author); } }); -} +}; PadDiff.prototype._createDiffAtext = function(callback) { var self = this; @@ -219,7 +219,7 @@ PadDiff.prototype._createDiffAtext = function(callback) { } ); }); -} +}; PadDiff.prototype.getHtml = function(callback){ //cache the html @@ -279,7 +279,7 @@ PadDiff.prototype.getAuthors = function(callback){ } else { callback(null, self._authors); } -} +}; PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) { //unpack @@ -312,7 +312,7 @@ PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool //return the modified changeset return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank); -} +}; //this method is 80% like Changeset.inverse. I just changed so instead of reverting, it adds deletions and attribute changes to to the atext. PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { @@ -463,7 +463,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { // If the text this operator applies to is only a star, than this is a false positive and should be ignored if (csOp.attribs && textBank != "*") { var deletedAttrib = apool.putAttrib(["removed", true]); - var authorAttrib = apool.putAttrib(["author", ""]);; + var authorAttrib = apool.putAttrib(["author", ""]); attribKeys.length = 0; attribValues.length = 0; @@ -473,7 +473,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { if(apool.getAttribKey(n) === "author"){ authorAttrib = n; - }; + } }); var undoBackToAttribs = cachedStrFunc(function (attribs) { diff --git a/src/package.json b/src/package.json index 822676a38..24f15ca9b 100644 --- a/src/package.json +++ b/src/package.json @@ -27,8 +27,7 @@ "nodemailer" : "0.3.x", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1.x", - "npm-registry-client" : "0.2.10", + "npm" : "1.2.x", "ejs" : "0.6.1", "graceful-fs" : "1.1.5", "slide" : "1.1.3", diff --git a/src/static/css/admin.css b/src/static/css/admin.css index b68238425..2260e27df 100644 --- a/src/static/css/admin.css +++ b/src/static/css/admin.css @@ -43,7 +43,7 @@ div.innerwrapper { box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2); margin: auto; max-width: 1150px; - min-height: 100%; + min-height: 101%;/*always display a scrollbar*/ } h1 { @@ -102,12 +102,26 @@ input[type="text"] { max-width: 500px; } +.sort { + cursor: pointer; +} +.sort:after { + content: '▲▼' +} +.sort.up:after { + content:'▲' +} +.sort.down:after { + content:'▼' +} + table { border: 1px solid #ddd; border-radius: 3px; border-spacing: 0; width: 100%; margin: 20px 0; + position:relative; /* Allows us to position the loading indicator relative to the table */ } table thead tr { @@ -122,13 +136,40 @@ td, th { display: none; } -#progress { - position: absolute; - bottom: 50px; +#installed-plugins td>div { + position: relative;/* Allows us to position the loading indicator relative to this row */ + display: inline-block; /*make this fill the whole cell*/ + width:100%; } -#progress img { - vertical-align: top; +.messages td>* { + display: none; + text-align: center; +} + +.messages .fetching { + display: block; +} + +.progress { + position: absolute; + top: 0; left: 0; bottom:0; right:0; + padding: auto; + + background: rgb(255,255,255); + display: none; +} + +#search-progress.progress { + padding-top: 20%; + background: rgba(255,255,255,0.7); +} + +.progress * { + display: block; + margin: 0 auto; + text-align: center; + color: #666; } .settings { @@ -147,7 +188,25 @@ a:link, a:visited, a:hover, a:focus { } a:focus, a:hover { - border-bottom: #333333 1px solid; + text-decoration: underline; +} + +.installed-results a:link, +.search-results a:link, +.installed-results a:visited, +.search-results a:visited, +.installed-results a:hover, +.search-results a:hover, +.installed-results a:focus, +.search-results a:focus { + text-decoration: underline; +} + +.installed-results a:focus, +.search-results a:focus, +.installed-results a:hover, +.search-results a:hover { + text-decoration: none; } pre { diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 320a47202..dafb77ef4 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -559,6 +559,15 @@ table#otheruserstable { margin: 4px 0 0 4px; position: absolute; } +#titlesticky{ + font-size: 10px; + padding-top:2px; + float: right; + text-align: right; + text-decoration: none; + cursor: pointer; + color: #555; +} #titlecross { font-size: 25px; float: right; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 2dc6408b1..fc69d5921 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1013,6 +1013,11 @@ function Ace2Inner(){ return caughtErrors.slice(); }; + editorInfo.ace_getDocument = function() + { + return doc; + }; + editorInfo.ace_getDebugProperty = function(prop) { if (prop == "debugger") diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index a973875ce..41affa745 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -12,176 +12,248 @@ $(document).ready(function () { //connect socket = io.connect(url, {resource : resource}).of("/pluginfw/installer"); - $('.search-results').data('query', { - pattern: '', - offset: 0, - limit: 12, - }); - - var doUpdate = false; - - var search = function () { - socket.emit("search", $('.search-results').data('query')); - tasks++; + function search(searchTerm, limit) { + if(search.searchTerm != searchTerm) { + search.offset = 0 + search.results = [] + search.end = false + } + limit = limit? limit : search.limit + search.searchTerm = searchTerm; + socket.emit("search", {searchTerm: searchTerm, offset:search.offset, limit: limit, sortBy: search.sortBy, sortDir: search.sortDir}); + search.offset += limit; + $('#search-progress').show() + } + search.offset = 0; + search.limit = 12; + search.results = []; + search.sortBy = 'name'; + search.sortDir = /*DESC?*/true; + search.end = true;// have we received all results already? + search.messages = { + show: function(msg) { + $('.search-results .messages').show() + $('.search-results .messages .'+msg+'').show() + }, + hide: function(msg) { + $('.search-results .messages').hide() + $('.search-results .messages .'+msg+'').hide() + } } - function updateHandlers() { - $("form").submit(function(){ - var query = $('.search-results').data('query'); - query.pattern = $("#search-query").val(); - query.offset = 0; - search(); - return false; - }); - - $("#search-query").unbind('keyup').keyup(function () { - var query = $('.search-results').data('query'); - query.pattern = $("#search-query").val(); - query.offset = 0; - search(); - }); - - $(".do-install, .do-update").unbind('click').click(function (e) { - var row = $(e.target).closest("tr"); - doUpdate = true; - socket.emit("install", row.find(".name").text()); - tasks++; - }); - - $(".do-uninstall").unbind('click').click(function (e) { - var row = $(e.target).closest("tr"); - doUpdate = true; - socket.emit("uninstall", row.find(".name").text()); - tasks++; - }); - - $(".do-prev-page").unbind('click').click(function (e) { - var query = $('.search-results').data('query'); - query.offset -= query.limit; - if (query.offset < 0) { - query.offset = 0; + var installed = { + progress: { + show: function(plugin, msg) { + $('.installed-results .'+plugin+' .progress').show() + $('.installed-results .'+plugin+' .progress .message').text(msg) + if($(window).scrollTop() > $('.'+plugin).offset().top)$(window).scrollTop($('.'+plugin).offset().top-100) + }, + hide: function(plugin) { + $('.installed-results .'+plugin+' .progress').hide() + $('.installed-results .'+plugin+' .progress .message').text('') } - search(); - }); - $(".do-next-page").unbind('click').click(function (e) { - var query = $('.search-results').data('query'); - var total = $('.search-results').data('total'); - if (query.offset + query.limit < total) { - query.offset += query.limit; + }, + messages: { + show: function(msg) { + $('.installed-results .messages').show() + $('.installed-results .messages .'+msg+'').show() + }, + hide: function(msg) { + $('.installed-results .messages').hide() + $('.installed-results .messages .'+msg+'').hide() } - search(); - }); + }, + list: [] } - updateHandlers(); - - var tasks = 0; - socket.on('progress', function (data) { - $("#progress").show(); - $('#progress').data('progress', data.progress); - - var message = "Unknown status"; - if (data.message) { - message = data.message.toString(); - } - if (data.error) { - data.progress = 1; - } - - $("#progress .message").html(message); - - if (data.progress >= 1) { - tasks--; - if (tasks <= 0) { - // Hide the activity indicator once all tasks are done - $("#progress").hide(); - tasks = 0; - } - - if (data.error) { - alert('An error occurred: '+data.error+' -- the server log might know more...'); - }else { - if (doUpdate) { - doUpdate = false; - socket.emit("load"); - tasks++; - } - } - } - }); - - socket.on('search-result', function (data) { - var widget=$(".search-results"); - - widget.data('query', data.query); - widget.data('total', data.total); - - widget.find('.offset').html(data.query.offset); - if (data.query.offset + data.query.limit > data.total){ - widget.find('.limit').html(data.total); - }else{ - widget.find('.limit').html(data.query.offset + data.query.limit); - } - widget.find('.total').html(data.total); - - widget.find(".results *").remove(); - for (plugin_name in data.results) { - var plugin = data.results[plugin_name]; - var row = widget.find(".template tr").clone(); + function displayPluginList(plugins, container, template) { + plugins.forEach(function(plugin) { + var row = template.clone(); for (attr in plugin) { if(attr == "name"){ // Hack to rewrite URLS into name - row.find(".name").html(""+plugin[attr]+""); + row.find(".name").html(""+plugin['name'].substr(3)+""); // remove 'ep_' }else{ row.find("." + attr).html(plugin[attr]); } } - row.find(".version").html( data.results[plugin_name]['dist-tags'].latest ); - - widget.find(".results").append(row); - } - + row.find(".version").html( plugin.version ); + row.addClass(plugin.name) + row.data('plugin', plugin.name) + container.append(row); + }) updateHandlers(); + } + + function sortPluginList(plugins, property, /*ASC?*/dir) { + return plugins.sort(function(a, b) { + if (a[property] < b[property]) + return dir? -1 : 1; + if (a[property] > b[property]) + return dir? 1 : -1; + // a must be equal to b + return 0; + }) + } + + // Infinite scroll + $(window).scroll(checkInfiniteScroll) + function checkInfiniteScroll() { + if(search.end) return;// don't keep requesting if there are no more results + try{ + var top = $('.search-results .results > tr:last').offset().top + if($(window).scrollTop()+$(window).height() > top) search(search.searchTerm) + }catch(e){} + } + + function updateHandlers() { + // Search + $("#search-query").unbind('keyup').keyup(function () { + search($("#search-query").val()); + }); + + // update & install + $(".do-install, .do-update").unbind('click').click(function (e) { + var $row = $(e.target).closest("tr") + , plugin = $row.data('plugin'); + if($(this).hasClass('do-install')) { + $row.remove().appendTo('#installed-plugins') + installed.progress.show(plugin, 'Installing') + }else{ + installed.progress.show(plugin, 'Updating') + } + socket.emit("install", plugin); + installed.messages.hide("nothing-installed") + }); + + // uninstall + $(".do-uninstall").unbind('click').click(function (e) { + var $row = $(e.target).closest("tr") + , pluginName = $row.data('plugin'); + socket.emit("uninstall", pluginName); + installed.progress.show(pluginName, 'Uninstalling') + installed.list = installed.list.filter(function(plugin) { + return plugin.name != pluginName + }) + }); + + // Sort + $('.sort.up').unbind('click').click(function() { + search.sortBy = $(this).text().toLowerCase(); + search.sortDir = false; + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + $('.sort.down, .sort.none').unbind('click').click(function() { + search.sortBy = $(this).text().toLowerCase(); + search.sortDir = true; + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + } + + socket.on('results:search', function (data) { + if(!data.results.length) search.end = true; + search.messages.hide('nothing-found') + search.messages.hide('fetching') + $("#search-query").removeAttr('disabled') + + console.log('got search results', data) + + // add to results + search.results = search.results.concat(data.results); + + // Update sorting head + $('.sort') + .removeClass('up down') + .addClass('none'); + $('.search-results thead th[data-label='+data.query.sortBy+']') + .removeClass('none') + .addClass(data.query.sortDir? 'up' : 'down'); + + // re-render search results + var searchWidget = $(".search-results"); + searchWidget.find(".results *").remove(); + if(search.results.length > 0) { + displayPluginList(search.results, searchWidget.find(".results"), searchWidget.find(".template tr")) + }else { + search.messages.show('nothing-found') + } + $('#search-progress').hide() + checkInfiniteScroll() }); - socket.on('installed-results', function (data) { - $("#installed-plugins *").remove(); + socket.on('results:installed', function (data) { + installed.messages.hide("fetching") + installed.messages.hide("nothing-installed") - for (plugin_name in data.results) { - if (plugin_name == "ep_etherpad-lite") continue; // Hack... - var plugin = data.results[plugin_name]; - var row = $("#installed-plugin-template").clone(); + installed.list = data.installed + sortPluginList(installed.list, 'name', /*ASC?*/true); - for (attr in plugin.package) { - if(attr == "name"){ // Hack to rewrite URLS into name - row.find(".name").html(""+plugin.package[attr]+""); - }else{ - row.find("." + attr).html(plugin.package[attr]); - } - } - $("#installed-plugins").append(row); + // filter out epl + installed.list = installed.list.filter(function(plugin) { + return plugin.name != 'ep_etherpad-lite' + }) + + // remove all installed plugins (leave plugins that are still being installed) + installed.list.forEach(function(plugin) { + $('#installed-plugins .'+plugin.name).remove() + }) + + if(installed.list.length > 0) { + displayPluginList(installed.list, $("#installed-plugins"), $("#installed-plugin-template")); + socket.emit('checkUpdates'); + }else { + installed.messages.show("nothing-installed") } - updateHandlers(); - - socket.emit('checkUpdates'); - tasks++; }); - socket.on('updatable', function(data) { - $('#installed-plugins>tr').each(function(i,tr) { - var pluginName = $(tr).find('.name').text() - - if (data.updatable.indexOf(pluginName) >= 0) { - var actions = $(tr).find('.actions') - actions.append('') - actions.css('width', 200) - } + socket.on('results:updatable', function(data) { + data.updatable.forEach(function(pluginName) { + var $row = $('#installed-plugins > tr.'+pluginName) + , actions = $row.find('.actions') + actions.append('') }) updateHandlers(); }) - socket.emit("load"); - tasks++; - - search(); + socket.on('finished:install', function(data) { + if(data.error) { + alert('An error occured while installing '+data.plugin+' \n'+data.error) + $('#installed-plugins .'+data.plugin).remove() + } + + socket.emit("getInstalled"); + + // update search results + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + + socket.on('finished:uninstall', function(data) { + if(data.error) alert('An error occured while uninstalling the '+data.plugin+' \n'+data.error) + + // remove plugin from installed list + $('#installed-plugins .'+data.plugin).remove() + + socket.emit("getInstalled"); + + // update search results + search.offset = 0; + search(search.searchTerm, search.results.length); + search.results = []; + }) + + // init + updateHandlers(); + socket.emit("getInstalled"); + search(''); + + // check for updates every 5mins + setInterval(function() { + socket.emit('checkUpdates'); + }, 1000*60*5) }); diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 15d879409..377be35e9 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -1,118 +1,77 @@ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm"); -var RegClient = require("npm-registry-client") -var registry = new RegClient( -{ registry: "http://registry.npmjs.org" -, cache: npm.cache } -); - -var withNpm = function (npmfn, final, cb) { +var npmIsLoaded = false; +var withNpm = function (npmfn) { + if(npmIsLoaded) return npmfn(); npm.load({}, function (er) { - if (er) return cb({progress:1, error:er}); + if (er) return npmfn(er); + npmIsLoaded = true; npm.on("log", function (message) { - cb({progress: 0.5, message:message.msg + ": " + message.pref}); - }); - npmfn(function (er, data) { - if (er) { - console.error(er); - return cb({progress:1, error: er.message}); - } - if (!data) data = {}; - data.progress = 1; - data.message = "Done."; - cb(data); - final(); + console.log('npm: ',message) }); + npmfn(); }); } -// All these functions call their callback multiple times with -// {progress:[0,1], message:STRING, error:object}. They will call it -// with progress = 1 at least once, and at all times will either -// message or error be present, not both. It can be called multiple -// times for all values of propgress except for 1. - exports.uninstall = function(plugin_name, cb) { - withNpm( - function (cb) { - npm.commands.uninstall([plugin_name], function (er) { + withNpm(function (er) { + if (er) return cb && cb(er); + npm.commands.uninstall([plugin_name], function (er) { + if (er) return cb && cb(er); + hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { if (er) return cb(er); - hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { - if (er) return cb(er); - plugins.update(cb); - }); + plugins.update(cb); + hooks.aCallAll("restartServer", {}, function () {}); }); - }, - function () { - hooks.aCallAll("restartServer", {}, function () {}); - }, - cb - ); + }); + }); }; exports.install = function(plugin_name, cb) { - withNpm( - function (cb) { - npm.commands.install([plugin_name], function (er) { + withNpm(function (er) { + if (er) return cb && cb(er); + npm.commands.install([plugin_name], function (er) { + if (er) return cb && cb(er); + hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { if (er) return cb(er); - hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { - if (er) return cb(er); - plugins.update(cb); - }); + plugins.update(cb); + hooks.aCallAll("restartServer", {}, function () {}); }); - }, - function () { - hooks.aCallAll("restartServer", {}, function () {}); - }, - cb - ); + }); + }); }; -exports.searchCache = null; +exports.availablePlugins = null; +var cacheTimestamp = 0; -exports.search = function(query, cache, cb) { - withNpm( - function (cb) { - var getData = function (cb) { - if (cache && exports.searchCache) { - cb(null, exports.searchCache); - } else { - registry.get( - "/-/all", 600, false, true, - function (er, data) { - if (er) return cb(er); - exports.searchCache = data; - cb(er, data); - } - ); - } - } - getData( - function (er, data) { - if (er) return cb(er); - var res = {}; - var i = 0; - var pattern = query.pattern.toLowerCase(); - for (key in data) { // for every plugin in the data from npm - if ( key.indexOf(plugins.prefix) == 0 - && key.indexOf(pattern) != -1 - || key.indexOf(plugins.prefix) == 0 - && data[key].description.indexOf(pattern) != -1 - ) { // If the name contains ep_ and the search string is in the name or description - i++; - if (i > query.offset - && i <= query.offset + query.limit) { - res[key] = data[key]; - } - } - } - cb(null, {results:res, query: query, total:i}); - } - ); - }, - function () { }, - cb - ); +exports.getAvailablePlugins = function(maxCacheAge, cb) { + withNpm(function (er) { + if (er) return cb && cb(er); + if(exports.availablePlugins && maxCacheAge && Math.round(+new Date/1000)-cacheTimestamp <= maxCacheAge) { + return cb && cb(null, exports.availablePlugins) + } + npm.commands.search(['ep_'], /*silent?*/true, function(er, results) { + if(er) return cb && cb(er); + exports.availablePlugins = results; + cacheTimestamp = Math.round(+new Date/1000); + cb && cb(null, results) + }) + }); +}; + + +exports.search = function(searchTerm, maxCacheAge, cb) { + exports.getAvailablePlugins(maxCacheAge, function(er, results) { + if(er) return cb && cb(er); + var res = {}; + searchTerm = searchTerm.toLowerCase(); + for (var pluginName in results) { // for every available plugin + if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! + if(pluginName.indexOf(searchTerm) < 0 && results[pluginName].description.indexOf(searchTerm) < 0) continue; + res[pluginName] = results[pluginName]; + } + cb && cb(null, res) + }) }; diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 7c2a7abf2..44e6f7a51 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -28,43 +28,11 @@
Name | -Description | -Version | -- |
---|---|---|---|
- | - | - | - - | -
Name | @@ -74,23 +42,70 @@||||
---|---|---|---|---|
- | - - | +
+
+
+
+ |
||
+ You haven't installed any plugins yet. +
|
Name | +Description | +Version | ++ |
---|---|---|---|
+ | + | + |
+
+
+
+ |
+
+ ![]() No plugins found. +
|