diff options
-rw-r--r-- | CHANGELOG.md | 12 | ||||
-rw-r--r-- | bin/importSqlFile.js | 110 | ||||
-rw-r--r-- | src/ep.json | 3 | ||||
-rw-r--r-- | src/locales/fa.json | 8 | ||||
-rw-r--r-- | src/locales/ia.json | 32 | ||||
-rw-r--r-- | src/locales/te.json | 1 | ||||
-rw-r--r-- | src/node/handler/APIHandler.js | 5 | ||||
-rw-r--r-- | src/node/handler/PadMessageHandler.js | 18 | ||||
-rw-r--r-- | src/node/hooks/express/admin.js | 1 | ||||
-rw-r--r-- | src/node/hooks/express/swagger.js | 431 | ||||
-rw-r--r-- | src/package.json | 5 | ||||
-rw-r--r-- | src/static/js/ace2_inner.js | 4 | ||||
-rw-r--r-- | src/static/js/chat.js | 2 | ||||
-rw-r--r-- | src/static/js/pad.js | 12 | ||||
-rw-r--r-- | src/static/js/pad_userlist.js | 12 | ||||
-rw-r--r-- | tests/frontend/specs/alphabet.js (renamed from tests/frontend/specs/keystroke_alphabet.js) | 0 | ||||
-rw-r--r-- | tests/frontend/specs/bold.js | 71 | ||||
-rw-r--r-- | tests/frontend/specs/button_bold.js | 36 | ||||
-rw-r--r-- | tests/frontend/specs/button_italic.js | 36 | ||||
-rw-r--r-- | tests/frontend/specs/button_redo.js | 37 | ||||
-rw-r--r-- | tests/frontend/specs/button_undo.js | 33 | ||||
-rw-r--r-- | tests/frontend/specs/chat.js (renamed from tests/frontend/specs/keystroke_chat.js) | 35 | ||||
-rw-r--r-- | tests/frontend/specs/chat_always_on_screen.js | 40 | ||||
-rw-r--r-- | tests/frontend/specs/clear_authorship_colors.js (renamed from tests/frontend/specs/button_clear_authorship_colors.js) | 0 | ||||
-rw-r--r-- | tests/frontend/specs/delete.js (renamed from tests/frontend/specs/keystroke_delete.js) | 0 | ||||
-rw-r--r-- | tests/frontend/specs/enter.js (renamed from tests/frontend/specs/keystroke_enter.js) | 2 | ||||
-rw-r--r-- | tests/frontend/specs/indentation.js (renamed from tests/frontend/specs/button_indentation.js) | 28 | ||||
-rw-r--r-- | tests/frontend/specs/italic.js | 73 | ||||
-rw-r--r-- | tests/frontend/specs/language.js | 3 | ||||
-rw-r--r-- | tests/frontend/specs/ordered_list.js (renamed from tests/frontend/specs/button_ordered_list.js) | 0 | ||||
-rw-r--r-- | tests/frontend/specs/redo.js | 76 | ||||
-rw-r--r-- | tests/frontend/specs/strikethrough.js (renamed from tests/frontend/specs/button_strikethrough.js) | 0 | ||||
-rw-r--r-- | tests/frontend/specs/timeslider.js (renamed from tests/frontend/specs/button_timeslider.js) | 0 | ||||
-rw-r--r-- | tests/frontend/specs/undo.js | 69 | ||||
-rw-r--r-- | tests/frontend/specs/urls_become_clickable.js (renamed from tests/frontend/specs/keystroke_urls_become_clickable.js) | 0 |
35 files changed, 980 insertions, 215 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fac58f2..642846a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 1.2.9 + * Fix: MAJOR Security issue, where a hacker could submit content as another user + * Fix: security issue due to unescaped user input + * Fix: Admin page at /admin redirects to /admin/ now to prevent breaking relative links + * Fix: indentation in chrome on linux + * 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: Localisation updates + + # 1.2.81 * Fix: CtrlZ-Y for Undo Redo * Fix: RTL functionality on contents & fix RTL/LTR tests and RTL in Safari diff --git a/bin/importSqlFile.js b/bin/importSqlFile.js new file mode 100644 index 00000000..6491cbea --- /dev/null +++ b/bin/importSqlFile.js @@ -0,0 +1,110 @@ +var startTime = new Date().getTime(); + +require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) { + + var fs = require("fs"); + + var ueberDB = require("ep_etherpad-lite/node_modules/ueberDB"); + var settings = require("ep_etherpad-lite/node/utils/Settings"); + var log4js = require('ep_etherpad-lite/node_modules/log4js'); + + var dbWrapperSettings = { + cache: 0, + writeInterval: 100, + json: false // data is already json encoded + }; + var db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger("ueberDB")); + + var sqlFile = process.argv[2]; + + //stop if the settings file is not set + if(!sqlFile) + { + console.error("Use: node importSqlFile.js $SQLFILE"); + process.exit(1); + } + + log("initializing db"); + db.init(function(err) + { + //there was an error while initializing the database, output it and stop + if(err) + { + console.error("ERROR: Problem while initalizing the database"); + console.error(err.stack ? err.stack : err); + process.exit(1); + } + else + { + log("done"); + + log("open output file..."); + var lines = fs.readFileSync(sqlFile, 'utf8').split("\n"); + + var count = lines.length; + var keyNo = 0; + + process.stdout.write("Start importing " + count + " keys...\n"); + lines.forEach(function(l) { + if (l.substr(0, 27) == "REPLACE INTO store VALUES (") { + var pos = l.indexOf("', '"); + var key = l.substr(28, pos - 28); + var value = l.substr(pos + 3); + value = value.substr(0, value.length - 2); + console.log("key: " + key + " val: " + value); + console.log("unval: " + unescape(value)); + db.set(key, unescape(value), null); + keyNo++; + if (keyNo % 1000 == 0) { + process.stdout.write(" " + keyNo + "/" + count + "\n"); + } + } + }); + process.stdout.write("\n"); + process.stdout.write("done. waiting for db to finish transaction. depended on dbms this may take some time...\n"); + + db.doShutdown(function() { + log("finished, imported " + keyNo + " keys."); + process.exit(0); + }); + } + }); +}); + +function log(str) +{ + console.log((new Date().getTime() - startTime)/1000 + "\t" + str); +} + +unescape = function(val) { + // value is a string + if (val.substr(0, 1) == "'") { + val = val.substr(0, val.length - 1).substr(1); + + return val.replace(/\\[0nrbtZ\\'"]/g, function(s) { + switch(s) { + case "\\0": return "\0"; + case "\\n": return "\n"; + case "\\r": return "\r"; + case "\\b": return "\b"; + case "\\t": return "\t"; + case "\\Z": return "\x1a"; + default: return s.substr(1); + } + }); + } + + // value is a boolean or NULL + if (val == 'NULL') { + return null; + } + if (val == 'true') { + return true; + } + if (val == 'false') { + return false; + } + + // value is a number + return val; +}; diff --git a/src/ep.json b/src/ep.json index 89c8964a..eeb5c640 100644 --- a/src/ep.json +++ b/src/ep.json @@ -23,6 +23,7 @@ { "name": "adminsettings", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer", "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" } - } + }, + { "name": "swagger", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/swagger:expressCreateServer" } } ] } diff --git a/src/locales/fa.json b/src/locales/fa.json index 437b8da0..bccc353c 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -2,7 +2,8 @@ "@metadata": { "authors": { "0": "BMRG14", - "2": "ZxxZxxZ" + "1": "Dalba", + "3": "ZxxZxxZ" } }, "index.newPad": "\u062f\u0641\u062a\u0631\u0686\u0647 \u06cc\u0627\u062f\u062f\u0627\u0634\u062a \u062a\u0627\u0632\u0647", @@ -20,7 +21,7 @@ "pad.toolbar.clearAuthorship.title": "\u067e\u0627\u06a9 \u06a9\u0631\u062f\u0646 \u0631\u0646\u06af\u200c\u0647\u0627\u06cc \u0646\u0648\u06cc\u0633\u0646\u062f\u06af\u06cc", "pad.toolbar.import_export.title": "\u062f\u0631\u0648\u0646\u200c\u0631\u06cc\u0632\u06cc\/\u0628\u0631\u0648\u0646\u200c\u0631\u06cc\u0632\u06cc \u0627\u0632\/\u0628\u0647 \u0642\u0627\u0644\u0628\u200c\u0647\u0627\u06cc \u0645\u062e\u062a\u0644\u0641", "pad.toolbar.timeslider.title": "\u0627\u0633\u0644\u0627\u06cc\u062f\u0631 \u0632\u0645\u0627\u0646", - "pad.toolbar.savedRevision.title": "\u0628\u0627\u0632\u0646\u0648\u06cc\u0633\u06cc\u200c\u0647\u0627\u06cc \u0630\u062e\u06cc\u0631\u0647 \u0634\u062f\u0647", + "pad.toolbar.savedRevision.title": "\u0630\u062e\u06cc\u0631\u0647\u200c\u0633\u0627\u0632\u06cc \u0646\u0633\u062e\u0647", "pad.toolbar.settings.title": "\u062a\u0646\u0638\u06cc\u0645\u0627\u062a", "pad.toolbar.embed.title": "\u062c\u0627\u0633\u0627\u0632\u06cc \u0627\u06cc\u0646 \u062f\u0641\u062a\u0631\u0686\u0647 \u06cc\u0627\u062f\u062f\u0627\u0634\u062a", "pad.toolbar.showusers.title": "\u0646\u0645\u0627\u06cc\u0634 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646 \u062f\u0631 \u0627\u06cc\u0646 \u062f\u0641\u062a\u0631\u0686\u0647 \u06cc\u0627\u062f\u062f\u0627\u0634\u062a", @@ -79,6 +80,7 @@ "pad.share.emebdcode": "\u062c\u0627\u0633\u0627\u0632\u06cc \u0646\u0634\u0627\u0646\u06cc", "pad.chat": "\u06af\u0641\u062a\u06af\u0648", "pad.chat.title": "\u0628\u0627\u0632\u06a9\u0631\u062f\u0646 \u06af\u0641\u062a\u06af\u0648 \u0628\u0631\u0627\u06cc \u0627\u06cc\u0646 \u062f\u0641\u062a\u0631\u0686\u0647 \u06cc\u0627\u062f\u062f\u0627\u0634\u062a", + "pad.chat.loadmessages": "\u0628\u0627\u0631\u06af\u06cc\u0631\u06cc \u067e\u06cc\u0627\u0645\u200c\u0647\u0627\u06cc \u0628\u06cc\u0634\u062a\u0631", "timeslider.pageTitle": "\u0627\u0633\u0644\u0627\u06cc\u062f\u0631 \u0632\u0645\u0627\u0646 {{appTitle}}", "timeslider.toolbar.returnbutton": "\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u062f\u0641\u062a\u0631\u0686\u0647 \u06cc\u0627\u062f\u062f\u0627\u0634\u062a", "timeslider.toolbar.authors": "\u0646\u0648\u06cc\u0633\u0646\u062f\u06af\u0627\u0646:", @@ -100,6 +102,8 @@ "timeslider.month.october": "\u0627\u06a9\u062a\u0628\u0631", "timeslider.month.november": "\u0646\u0648\u0627\u0645\u0628\u0631", "timeslider.month.december": "\u062f\u0633\u0627\u0645\u0628\u0631", + "timeslider.unnamedauthor": "{{num}} \u0646\u0648\u06cc\u0633\u0646\u062f\u0647\u0654 \u0628\u06cc\u200c\u0646\u0627\u0645", + "timeslider.unnamedauthors": "{{num}} \u0646\u0648\u06cc\u0633\u0646\u062f\u0647\u0654 \u0628\u06cc\u200c\u0646\u0627\u0645", "pad.savedrevs.marked": "\u0627\u06cc\u0646 \u0628\u0627\u0632\u0646\u0648\u06cc\u0633\u06cc \u0647\u0645 \u0627\u06a9\u0646\u0648\u0646 \u0628\u0647 \u0639\u0646\u0648\u0627\u0646 \u0630\u062e\u06cc\u0631\u0647 \u0634\u062f\u0647 \u0639\u0644\u0627\u0645\u062a\u200c\u06af\u0630\u0627\u0631\u06cc \u0634\u062f", "pad.userlist.entername": "\u0646\u0627\u0645 \u062e\u0648\u062f \u0631\u0627 \u0628\u0646\u0648\u06cc\u0633\u06cc\u062f", "pad.userlist.unnamed": "\u0628\u062f\u0648\u0646 \u0646\u0627\u0645", diff --git a/src/locales/ia.json b/src/locales/ia.json index 21b1d291..e6c5dde1 100644 --- a/src/locales/ia.json +++ b/src/locales/ia.json @@ -19,13 +19,16 @@ "pad.toolbar.clearAuthorship.title": "Rader colores de autor", "pad.toolbar.import_export.title": "Importar\/exportar in differente formatos de file", "pad.toolbar.timeslider.title": "Glissa-tempore", - "pad.toolbar.savedRevision.title": "Versiones salveguardate", + "pad.toolbar.savedRevision.title": "Version salveguardate", "pad.toolbar.settings.title": "Configuration", "pad.toolbar.embed.title": "Incorporar iste pad", "pad.toolbar.showusers.title": "Monstrar le usatores de iste pad", "pad.colorpicker.save": "Salveguardar", "pad.colorpicker.cancel": "Cancellar", "pad.loading": "Cargamento\u2026", + "pad.passwordRequired": "Un contrasigno es necessari pro acceder a iste pad", + "pad.permissionDenied": "Tu non ha le permission de acceder a iste pad", + "pad.wrongPassword": "Le contrasigno es incorrecte", "pad.settings.padSettings": "Configuration del pad", "pad.settings.myView": "Mi vista", "pad.settings.stickychat": "Chat sempre visibile", @@ -38,6 +41,7 @@ "pad.settings.language": "Lingua:", "pad.importExport.import_export": "Importar\/Exportar", "pad.importExport.import": "Incargar qualcunque file de texto o documento", + "pad.importExport.importSuccessful": "Succedite!", "pad.importExport.export": "Exportar le pad actual como:", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "Texto simple", @@ -45,9 +49,11 @@ "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportdokuwiki": "DokuWiki", + "pad.importExport.abiword.innerHTML": "Tu pote solmente importar files in formato de texto simple o HTML. Pro functionalitate de importation plus extense, <a href=\"https:\/\/github.com\/ether\/etherpad-lite\/wiki\/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">installa AbiWord<\/a>.", "pad.modals.connected": "Connectite.", "pad.modals.reconnecting": "Reconnecte a tu pad\u2026", "pad.modals.forcereconnect": "Fortiar reconnexion", + "pad.modals.userdup": "Aperte in un altere fenestra", "pad.modals.userdup.explanation": "Iste pad pare esser aperte in plus de un fenestra de navigator in iste computator.", "pad.modals.userdup.advice": "Reconnecte pro usar iste fenestra.", "pad.modals.unauth": "Non autorisate", @@ -72,11 +78,16 @@ "pad.share.emebdcode": "Codice de incorporation", "pad.chat": "Chat", "pad.chat.title": "Aperir le chat pro iste pad.", + "pad.chat.loadmessages": "Cargar plus messages", "timeslider.pageTitle": "Glissa-tempore pro {{appTitle}}", "timeslider.toolbar.returnbutton": "Retornar al pad", "timeslider.toolbar.authors": "Autores:", "timeslider.toolbar.authorsList": "Nulle autor", + "timeslider.toolbar.exportlink.title": "Exportar", "timeslider.exportCurrent": "Exportar le version actual como:", + "timeslider.version": "Version {{version}}", + "timeslider.saved": "Salveguardate le {{day}} de {{month}} {{year}}", + "timeslider.dateformat": "{{year}}-{{month}}-{{day}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "januario", "timeslider.month.february": "februario", "timeslider.month.march": "martio", @@ -88,5 +99,22 @@ "timeslider.month.september": "septembre", "timeslider.month.october": "octobre", "timeslider.month.november": "novembre", - "timeslider.month.december": "decembre" + "timeslider.month.december": "decembre", + "timeslider.unnamedauthor": "{{num}} autor sin nomine", + "timeslider.unnamedauthors": "{{num}} autores sin nomine", + "pad.savedrevs.marked": "Iste version es ora marcate como version salveguardate", + "pad.userlist.entername": "Entra tu nomine", + "pad.userlist.unnamed": "sin nomine", + "pad.userlist.guest": "Invitato", + "pad.userlist.deny": "Refusar", + "pad.userlist.approve": "Approbar", + "pad.editbar.clearcolors": "Rader le colores de autor in tote le documento?", + "pad.impexp.importbutton": "Importar ora", + "pad.impexp.importing": "Importation in curso\u2026", + "pad.impexp.confirmimport": "Le importation de un file superscribera le texto actual del pad. Es tu secur de voler continuar?", + "pad.impexp.convertFailed": "Nos non ha potite importar iste file. Per favor usa un altere formato de documento o copia e colla le texto manualmente.", + "pad.impexp.uploadFailed": "Le incargamento ha fallite. Per favor reproba.", + "pad.impexp.importfailed": "Importation fallite", + "pad.impexp.copypaste": "Per favor copia e colla", + "pad.impexp.exportdisabled": "Le exportation in formato {{type}} es disactivate. Per favor contacta le administrator del systema pro detalios." }
\ No newline at end of file diff --git a/src/locales/te.json b/src/locales/te.json index 666a40aa..955b263a 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -26,6 +26,7 @@ "pad.colorpicker.save": "\u0c2d\u0c26\u0c4d\u0c30\u0c2a\u0c30\u0c1a\u0c41", "pad.colorpicker.cancel": "\u0c30\u0c26\u0c4d\u0c26\u0c41\u0c1a\u0c47\u0c2f\u0c3f", "pad.loading": "\u0c32\u0c4b\u0c21\u0c35\u0c41\u0c24\u0c4b\u0c02\u0c26\u0c3f...", + "pad.wrongPassword": "\u0c2e\u0c40 \u0c30\u0c39\u0c38\u0c4d\u0c2f\u0c2a\u0c26\u0c02 \u0c24\u0c2a\u0c41", "pad.settings.padSettings": "\u0c2a\u0c32\u0c15 \u0c05\u0c2e\u0c30\u0c3f\u0c15\u0c32\u0c41", "pad.settings.myView": "\u0c28\u0c3e \u0c09\u0c26\u0c4d\u0c26\u0c47\u0c36\u0c4d\u0c2f\u0c2e\u0c41", "pad.settings.stickychat": "\u0c24\u0c46\u0c30\u0c2a\u0c48\u0c28\u0c47 \u0c2e\u0c3e\u0c1f\u0c3e\u0c2e\u0c02\u0c24\u0c3f\u0c28\u0c3f \u0c0e\u0c32\u0c4d\u0c32\u0c2a\u0c41\u0c21\u0c41 \u0c1a\u0c47\u0c2f\u0c41\u0c2e\u0c41", diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 8be5b5fe..4b7dd951 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -219,6 +219,9 @@ var version = // set the latest available API version here exports.latestApiVersion = '1.2.7'; +// exports the versions so it can be used by the new Swagger endpoint +exports.version = version; + /** * Handles a HTTP API call * @param functionName the name of the called function @@ -266,6 +269,8 @@ exports.handle = function(apiVersion, functionName, fields, req, res) } //check the api key! + fields["apikey"] = fields["apikey"] || fields["api_key"]; + if(fields["apikey"] != apikey.trim()) { res.send({code: 4, message: "no or wrong API Key", data: null}); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 15a9b8ea..4b98292e 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -550,11 +550,25 @@ function handleUserChanges(client, message) throw "Attribute pool is missing attribute "+n+" for changeset "+changeset; } }); + + // Validate all added 'author' attribs to be the same value as the current user + var iterator = Changeset.opIterator(Changeset.unpack(changeset).ops) + , op + while(iterator.hasNext()) { + op = iterator.next() + if(op.opcode != '+') continue; + op.attribs.split('*').forEach(function(attr) { + if(!attr) return + attr = wireApool.getAttrib(attr) + if(!attr) return + if('author' == attr[0] && attr[1] != thisSession.author) throw "Trying to submit changes as another author" + }) + } } catch(e) { // There is an error in this changeset, so just refuse it - console.warn("Can't apply USER_CHANGES "+changeset+", because it failed checkRep"); + console.warn("Can't apply USER_CHANGES "+changeset+", because: "+e); client.json.send({disconnect:"badChangeset"}); return; } @@ -1448,7 +1462,7 @@ exports.padUsersCount = function (padID, callback) { exports.padUsers = function (padID, callback) { var result = []; - async.forEach(socketio.sockets.clients(padId), function(roomClient, callback) { + async.forEach(socketio.sockets.clients(padID), function(roomClient, callback) { var s = sessioninfos[roomClient.id]; if(s) { authorManager.getAuthor(s.author, function(err, author) { diff --git a/src/node/hooks/express/admin.js b/src/node/hooks/express/admin.js index 766370fc..70539f0c 100644 --- a/src/node/hooks/express/admin.js +++ b/src/node/hooks/express/admin.js @@ -2,6 +2,7 @@ var eejs = require('ep_etherpad-lite/node/eejs'); exports.expressCreateServer = function (hook_name, args, cb) { args.app.get('/admin', function(req, res) { + if('/' != req.path[req.path.length-1]) return res.redirect('/admin/'); res.send( eejs.require("ep_etherpad-lite/templates/admin/index.html", {}) ); }); } diff --git a/src/node/hooks/express/swagger.js b/src/node/hooks/express/swagger.js new file mode 100644 index 00000000..f4fc5cff --- /dev/null +++ b/src/node/hooks/express/swagger.js @@ -0,0 +1,431 @@ +var log4js = require('log4js'); +var express = require('express'); +var apiHandler = require('../../handler/APIHandler'); +var apiCaller = require('./apicalls').apiCaller; +var settings = require("../../utils/Settings"); + +var swaggerModels = { + 'models': { + 'SessionInfo' : { + "id": 'SessionInfo', + "properties": { + "id": { + "type": "string" + }, + "authorID": { + "type": "string" + }, + "groupID":{ + "type":"string" + }, + "validUntil":{ + "type":"long" + } + } + }, + 'UserInfo' : { + "id": 'UserInfo', + "properties": { + "id": { + "type": "string" + }, + "colorId": { + "type": "string" + }, + "name":{ + "type":"string" + }, + "timestamp":{ + "type":"long" + } + } + }, + 'Message' : { + "id": 'Message', + "properties": { + "text": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "userName":{ + "type":"string" + }, + "time":{ + "type":"long" + } + } + } + } +}; + +function sessionListResponseProcessor(res) { + if (res.data) { + var sessions = []; + for (var sessionId in res.data) { + var sessionInfo = res.data[sessionId]; + sessionId["id"] = sessionId; + sessions.push(sessionInfo); + } + res.data = sessions; + } + + return res; +} + +// We'll add some more info to the API methods +var API = { + + // Group + "group": { + "create" : { + "func" : "createGroup", + "description": "creates a new group", + "response": {"groupID":{"type":"string"}} + }, + "createIfNotExistsFor" : { + "func": "createGroupIfNotExistsFor", + "description": "this functions helps you to map your application group ids to etherpad lite group ids", + "response": {"groupID":{"type":"string"}} + }, + "delete" : { + "func": "deleteGroup", + "description": "deletes a group" + }, + "listPads" : { + "func": "listPads", + "description": "returns all pads of this group", + "response": {"padIDs":{"type":"List", "items":{"type":"string"}}} + }, + "createPad" : { + "func": "createGroupPad", + "description": "creates a new pad in this group" + }, + "listSessions": { + "func": "listSessionsOfGroup", + "responseProcessor": sessionListResponseProcessor, + "description": "", + "response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}} + }, + "list": { + "func": "listAllGroups", + "description": "", + "response": {"groupIDs":{"type":"List", "items":{"type":"string"}}} + }, + }, + + // Author + "author": { + "create" : { + "func" : "createAuthor", + "description": "creates a new author", + "response": {"authorID":{"type":"string"}} + }, + "createIfNotExistsFor": { + "func": "createAuthorIfNotExistsFor", + "description": "this functions helps you to map your application author ids to etherpad lite author ids", + "response": {"authorID":{"type":"string"}} + }, + "listPads": { + "func": "listPadsOfAuthor", + "description": "returns an array of all pads this author contributed to", + "response": {"padIDs":{"type":"List", "items":{"type":"string"}}} + }, + "listSessions": { + "func": "listSessionsOfAuthor", + "responseProcessor": sessionListResponseProcessor, + "description": "returns all sessions of an author", + "response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}} + }, + // We need an operation that return a UserInfo so it can be picked up by the codegen :( + "getName" : { + "func": "getAuthorName", + "description": "Returns the Author Name of the author", + "responseProcessor": function(response) { + if (response.data) { + response["info"] = {"name": response.data.authorName}; + delete response["data"]; + } + }, + "response": {"info":{"type":"UserInfo"}} + } + }, + "session": { + "create" : { + "func": "createSession", + "description": "creates a new session. validUntil is an unix timestamp in seconds", + "response": {"sessionID":{"type":"string"}} + }, + "delete" : { + "func": "deleteSession", + "description": "deletes a session" + }, + // We need an operation that returns a SessionInfo so it can be picked up by the codegen :( + "info": { + "func": "getSessionInfo", + "description": "returns informations about a session", + "responseProcessor": function(response) { + // move this to info + if (response.data) { + response["info"] = response.data; + delete response["data"]; + } + }, + "response": {"info":{"type":"SessionInfo"}} + } + }, + "pad": { + "listAll" : { + "func": "listAllPads", + "description": "list all the pads", + "response": {"padIDs":{"type":"List", "items": {"type" : "string"}}} + }, + "createDiffHTML" : { + "func" : "createDiffHTML", + "description": "", + "response": {} + }, + "create" : { + "func" : "createPad", + "description": "creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad" + }, + "getText" : { + "func" : "getText", + "description": "returns the text of a pad", + "response": {"text":{"type":"string"}} + }, + "setText" : { + "func" : "setText", + "description": "sets the text of a pad" + }, + "getHTML": { + "func" : "getHTML", + "description": "returns the text of a pad formatted as HTML", + "response": {"html":{"type":"string"}} + }, + "setHTML": { + "func" : "setHTML", + "description": "sets the text of a pad with HTML" + }, + "getRevisionsCount": { + "func" : "getRevisionsCount", + "description": "returns the number of revisions of this pad", + "response": {"revisions":{"type":"long"}} + }, + "getLastEdited": { + "func" : "getLastEdited", + "description": "returns the timestamp of the last revision of the pad", + "response": {"lastEdited":{"type":"long"}} + }, + "delete": { + "func" : "deletePad", + "description": "deletes a pad" + }, + "getReadOnlyID": { + "func" : "getReadOnlyID", + "description": "returns the read only link of a pad", + "response": {"readOnlyID":{"type":"string"}} + }, + "setPublicStatus": { + "func": "setPublicStatus", + "description": "sets a boolean for the public status of a pad" + }, + "getPublicStatus": { + "func": "getPublicStatus", + "description": "return true of false", + "response": {"publicStatus":{"type":"boolean"}} + }, + "setPassword": { + "func": "setPassword", + "description": "returns ok or a error message" + }, + "isPasswordProtected": { + "func": "isPasswordProtected", + "description": "returns true or false", + "response": {"passwordProtection":{"type":"boolean"}} + }, + "authors": { + "func": "listAuthorsOfPad", + "description": "returns an array of authors who contributed to this pad", + "response": {"authorIDs":{"type":"List", "items":{"type" : "string"}}} + }, + "usersCount": { + "func": "padUsersCount", + "description": "returns the number of user that are currently editing this pad", + "response": {"padUsersCount":{"type": "long"}} + }, + "users": { + "func": "padUsers", + "description": "returns the list of users that are currently editing this pad", + "response": {"padUsers":{"type":"List", "items":{"type": "UserInfo"}}} + }, + "sendClientsMessage": { + "func": "sendClientsMessage", + "description": "sends a custom message of type msg to the pad" + }, + "checkToken" : { + "func": "checkToken", + "description": "returns ok when the current api token is valid" + }, + "getChatHistory": { + "func": "getChatHistory", + "description": "returns the chat history", + "response": {"messages":{"type":"List", "items": {"type" : "Message"}}} + }, + // We need an operation that returns a Message so it can be picked up by the codegen :( + "getChatHead": { + "func": "getChatHead", + "description": "returns the chatHead (chat-message) of the pad", + "responseProcessor": function(response) { + // move this to info + if (response.data) { + response["chatHead"] = {"time": response.data["chatHead"]}; + delete response["data"]; + } + }, + "response": {"chatHead":{"type":"Message"}} + } + } +}; + +function capitalise(string){ + return string.charAt(0).toUpperCase() + string.slice(1); +} + +for (var resource in API) { + for (var func in API[resource]) { + + // The base response model + var responseModel = { + "properties": { + "code":{ + "type":"int" + }, + "message":{ + "type":"string" + } + } + }; + + var responseModelId = "Response"; + + // Add the data properties (if any) to the response + if (API[resource][func]["response"]) { + // This is a specific response so let's set a new id + responseModelId = capitalise(resource) + capitalise(func) + "Response"; + + for(var prop in API[resource][func]["response"]) { + var propType = API[resource][func]["response"][prop]; + responseModel["properties"][prop] = propType; + } + } + + // Add the id + responseModel["id"] = responseModelId; + + // Add this to the swagger models + swaggerModels['models'][responseModelId] = responseModel; + + // Store the response model id + API[resource][func]["responseClass"] = responseModelId; + + } +} + +function newSwagger() { + var swagger_module = require.resolve("swagger-node-express"); + if (require.cache[swagger_module]) { + // delete the child modules from cache + require.cache[swagger_module].children.forEach(function(m) {delete require.cache[m.id];}); + // delete the module from cache + delete require.cache[swagger_module]; + } + return require("swagger-node-express"); +} + +exports.expressCreateServer = function (hook_name, args, cb) { + + for (var version in apiHandler.version) { + + var swagger = newSwagger(); + var basePath = "/rest/" + version; + + // Let's put this under /rest for now + var subpath = express(); + + args.app.use(express.bodyParser()); + args.app.use(basePath, subpath); + + swagger.setAppHandler(subpath); + + swagger.addModels(swaggerModels); + + for (var resource in API) { + + for (var funcName in API[resource]) { + var func = API[resource][funcName]; + + // get the api function + var apiFunc = apiHandler.version[version][func["func"]]; + + // Skip this one if it does not exist in the version + if(!apiFunc) { + continue; + } + + var swaggerFunc = { + 'spec': { + "description" : func["description"], + "path" : "/" + resource + "/" + funcName, + "summary" : funcName, + "nickname" : funcName, + "method": "GET", + "params" : apiFunc.map( function(param) { + return swagger.queryParam(param, param, "string"); + }), + "responseClass" : func["responseClass"] + }, + 'action': (function(func, responseProcessor) { + return function (req,res) { + req.params.version = version; + req.params.func = func; // call the api function + + //wrap the send function so we can process the response + res.__swagger_send = res.send; + res.send = function (response) { + // ugly but we need to get this as json + response = JSON.parse(response); + // process the response if needed + if (responseProcessor) { + response = responseProcessor(response); + } + // Let's move everything out of "data" + if (response.data) { + for(var prop in response.data) { + response[prop] = response.data[prop]; + delete response.data; + } + } + response = JSON.stringify(response); + res.__swagger_send(response); + }; + + apiCaller(req, res, req.query); + }; + })(func["func"], func["responseProcessor"]) // must use a closure here + }; + + swagger.addGet(swaggerFunc); + } + } + + swagger.setHeaders = function setHeaders(res) { + res.header('Access-Control-Allow-Origin', "*"); + }; + + swagger.configureSwaggerPaths("", "/api" , ""); + + swagger.configure("http://" + settings.ip + ":" + settings.port + basePath, version); + } +}; diff --git a/src/package.json b/src/package.json index 13a4de4c..a7147cf2 100644 --- a/src/package.json +++ b/src/package.json @@ -36,7 +36,8 @@ "tinycon" : "0.0.1", "underscore" : "1.3.1", "unorm" : "1.0.0", - "languages4translatewiki" : "0.1.3" + "languages4translatewiki" : "0.1.3", + "swagger-node-express" : "1.2.3" }, "bin": { "etherpad-lite": "./node/server.js" }, "devDependencies": { @@ -45,5 +46,5 @@ "engines" : { "node" : ">=0.6.3", "npm" : ">=1.0" }, - "version" : "1.2.81" + "version" : "1.2.9" } diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index b8b59b84..2dc6408b 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3583,7 +3583,7 @@ function Ace2Inner(){ } var specialHandled = false; - var isTypeForSpecialKey = ((browser.msie || browser.safari) ? (type == "keydown") : (type == "keypress")); + var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress")); var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress")); var stopped = false; @@ -5125,7 +5125,7 @@ function Ace2Inner(){ function initLineNumbers() { lineNumbersShown = 1; - sideDiv.innerHTML = '<table border="0" cellpadding="0" cellspacing="0" align="right">' + '<tr><td id="sidedivinner"><div>1</div></td></tr></table>'; + sideDiv.innerHTML = '<table border="0" cellpadding="0" cellspacing="0" align="right"><tr><td id="sidedivinner"><div>1</div></td></tr></table>'; sideDivInner = outerWin.document.getElementById("sidedivinner"); } diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 2dff2edf..83a487de 100644 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -111,7 +111,7 @@ var chat = (function() var authorName = msg.userName == null ? _('pad.userlist.unnamed') : padutils.escapeHtml(msg.userName); - var html = "<p class='" + authorClass + "'><b>" + authorName + ":</b><span class='time " + authorClass + "'>" + timeStr + "</span> " + text + "</p>"; + var html = "<p data-authorId='" + msg.userId + "' class='" + authorClass + "'><b>" + authorName + ":</b><span class='time " + authorClass + "'>" + timeStr + "</span> " + text + "</p>"; if(isHistoryAdd) $(html).insertAfter('#chatloadmessagesbutton'); else diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 4b052620..60a43557 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -470,14 +470,6 @@ var pad = { userAgent: pad.getDisplayUserAgent() }; - if (clientVars.specialKey) - { - pad.myUserInfo.specialKey = clientVars.specialKey; - if (clientVars.specialKeyTranslation) - { - $("#specialkeyarea").html("mode: " + String(clientVars.specialKeyTranslation).toUpperCase()); - } - } padimpexp.init(this); padsavedrevs.init(this); @@ -663,8 +655,8 @@ var pad = { { alertBar.displayMessage(function(abar) { - abar.find("#servermsgdate").html(" (" + padutils.simpleDateTime(new Date) + ")"); - abar.find("#servermsgtext").html(m.text); + abar.find("#servermsgdate").text(" (" + padutils.simpleDateTime(new Date) + ")"); + abar.find("#servermsgtext").text(m.text); }); } if (m.js) diff --git a/src/static/js/pad_userlist.js b/src/static/js/pad_userlist.js index 962595d2..77ebb190 100644 --- a/src/static/js/pad_userlist.js +++ b/src/static/js/pad_userlist.js @@ -116,12 +116,12 @@ var paduserlist = (function() nameHtml = '<input data-l10n-id="pad.userlist.unnamed" type="text" class="editempty newinput" value="'+_('pad.userlist.unnamed')+'" ' + (isNameEditable(data) ? '' : 'disabled="disabled" ') + '/>'; } - return ['<td style="height:', height, 'px" class="usertdswatch"><div class="swatch" style="background:' + data.color + '"> </div></td>', '<td style="height:', height, 'px" class="usertdname">', nameHtml, '</td>', '<td style="height:', height, 'px" class="activity">', padutils.escapeHtml(data.activity), '</td>'].join(''); + return ['<td style="height:', height, 'px" class="usertdswatch"><div class="swatch" style="background:' + padutils.escapeHtml(data.color) + '"> </div></td>', '<td style="height:', height, 'px" class="usertdname">', nameHtml, '</td>', '<td style="height:', height, 'px" class="activity">', padutils.escapeHtml(data.activity), '</td>'].join(''); } - function getRowHtml(id, innerHtml) + function getRowHtml(id, innerHtml, authorId) { - return '<tr id="' + id + '">' + innerHtml + '</tr>'; + return '<tr data-authorId="'+authorId+'" id="' + id + '">' + innerHtml + '</tr>'; } function rowNode(row) @@ -191,18 +191,20 @@ var paduserlist = (function() domId: domId, animationPower: animationPower }; + var authorId = data.id; + handleRowData(row); rowsPresent.splice(position, 0, row); var tr; if (animationPower == 0) { - tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data))); + tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data), authorId)); row.animationStep = 0; } else { rowsFadingIn.push(row); - tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START)))); + tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START)), authorId)); } handleRowNode(tr, data); if (position == 0) diff --git a/tests/frontend/specs/keystroke_alphabet.js b/tests/frontend/specs/alphabet.js index 131a81c0..131a81c0 100644 --- a/tests/frontend/specs/keystroke_alphabet.js +++ b/tests/frontend/specs/alphabet.js diff --git a/tests/frontend/specs/bold.js b/tests/frontend/specs/bold.js new file mode 100644 index 00000000..95da7331 --- /dev/null +++ b/tests/frontend/specs/bold.js @@ -0,0 +1,71 @@ +describe("bold button", function(){ + //create a new pad before each test run + beforeEach(function(cb){ + helper.newPad(cb); + this.timeout(60000); + }); + + it("makes text bold on click", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //get the first text element out of the inner iframe + var $firstTextElement = inner$("div").first(); + + //select this text element + $firstTextElement.sendkeys('{selectall}'); + + //get the bold button and click it + var $boldButton = chrome$(".buttonicon-bold"); + $boldButton.click(); + + //ace creates a new dom element when you press a button, so just get the first text element again + var $newFirstTextElement = inner$("div").first(); + + // is there a <b> element now? + var isBold = $newFirstTextElement.find("b").length === 1; + + //expect it to be bold + expect(isBold).to.be(true); + + //make sure the text hasn't changed + expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); + + done(); + }); + + it("makes text bold on keypress", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //get the first text element out of the inner iframe + var $firstTextElement = inner$("div").first(); + + //select this text element + $firstTextElement.sendkeys('{selectall}'); + if(inner$.browser.mozilla){ // if it's a mozilla browser + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + + var e = inner$.Event(evtType); + e.ctrlKey = true; // Control key + e.which = 66; // b + inner$("#innerdocbody").trigger(e); + + //ace creates a new dom element when you press a button, so just get the first text element again + var $newFirstTextElement = inner$("div").first(); + + // is there a <b> element now? + var isBold = $newFirstTextElement.find("b").length === 1; + + //expect it to be bold + expect(isBold).to.be(true); + + //make sure the text hasn't changed + expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); + + done(); + }); +}); diff --git a/tests/frontend/specs/button_bold.js b/tests/frontend/specs/button_bold.js deleted file mode 100644 index 1feafe61..00000000 --- a/tests/frontend/specs/button_bold.js +++ /dev/null @@ -1,36 +0,0 @@ -describe("bold button", function(){ - //create a new pad before each test run - beforeEach(function(cb){ - helper.newPad(cb); - this.timeout(60000); - }); - - it("makes text bold", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); - - //select this text element - $firstTextElement.sendkeys('{selectall}'); - - //get the bold button and click it - var $boldButton = chrome$(".buttonicon-bold"); - $boldButton.click(); - - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); - - // is there a <b> element now? - var isBold = $newFirstTextElement.find("b").length === 1; - - //expect it to be bold - expect(isBold).to.be(true); - - //make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - - done(); - }); -});
\ No newline at end of file diff --git a/tests/frontend/specs/button_italic.js b/tests/frontend/specs/button_italic.js deleted file mode 100644 index fc2e15a7..00000000 --- a/tests/frontend/specs/button_italic.js +++ /dev/null @@ -1,36 +0,0 @@ -describe("italic button", function(){ - //create a new pad before each test run - beforeEach(function(cb){ - helper.newPad(cb); - this.timeout(60000); - }); - - it("makes text italic", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); - - //select this text element - $firstTextElement.sendkeys('{selectall}'); - - //get the bold button and click it - var $boldButton = chrome$(".buttonicon-italic"); - $boldButton.click(); - - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); - - // is there a <i> element now? - var isItalic = $newFirstTextElement.find("i").length === 1; - - //expect it to be bold - expect(isItalic).to.be(true); - - //make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - - done(); - }); -}); diff --git a/tests/frontend/specs/button_redo.js b/tests/frontend/specs/button_redo.js deleted file mode 100644 index 3ce69142..00000000 --- a/tests/frontend/specs/button_redo.js +++ /dev/null @@ -1,37 +0,0 @@ -describe("undo button then redo button", function(){ - beforeEach(function(cb){ - helper.newPad(cb); // creates a new pad - this.timeout(60000); - }); - - it("undo some typing", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value - var newString = "Foo"; - - $firstTextElement.sendkeys(newString); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - // get undo and redo buttons - var $undoButton = chrome$(".buttonicon-undo"); - var $redoButton = chrome$(".buttonicon-redo"); - // click the buttons - $undoButton.click(); // removes foo - $redoButton.click(); // resends foo - - helper.waitFor(function(){ - console.log(inner$("div span").first().text()); - return inner$("div span").first().text() === newString; - }).done(function(){ - var finalValue = inner$("div").first().text(); - expect(finalValue).to.be(modifiedValue); // expect the value to change - done(); - }); - }); -}); - diff --git a/tests/frontend/specs/button_undo.js b/tests/frontend/specs/button_undo.js deleted file mode 100644 index 412b786b..00000000 --- a/tests/frontend/specs/button_undo.js +++ /dev/null @@ -1,33 +0,0 @@ -describe("undo button", function(){ - beforeEach(function(cb){ - helper.newPad(cb); // creates a new pad - this.timeout(60000); - }); - - it("undo some typing", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value - - $firstTextElement.sendkeys("foo"); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value - expect(modifiedValue).not.to.be(originalValue); // expect the value to change - - // get clear authorship button as a variable - var $undoButton = chrome$(".buttonicon-undo"); - // click the button - $undoButton.click(); - - helper.waitFor(function(){ - return inner$("div span").first().text() === originalValue; - }).done(function(){ - var finalValue = inner$("div span").first().text(); - expect(finalValue).to.be(originalValue); // expect the value to change - done(); - }); - }); -}); - diff --git a/tests/frontend/specs/keystroke_chat.js b/tests/frontend/specs/chat.js index e4908728..a488193f 100644 --- a/tests/frontend/specs/keystroke_chat.js +++ b/tests/frontend/specs/chat.js @@ -1,4 +1,4 @@ -describe("send chat message", function(){ +describe("Chat messages and UI", function(){ //create a new pad before each test run beforeEach(function(cb){ helper.newPad(cb); @@ -64,5 +64,36 @@ describe("send chat message", function(){ }); }); -}); + it("makes chat stick to right side of the screen", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //click on the settings button to make settings visible + var $settingsButton = chrome$(".buttonicon-settings"); + $settingsButton.click(); + + //get the chat selector + var $stickychatCheckbox = chrome$("#options-stickychat"); + + //select chat always on screen and fire change event + $stickychatCheckbox.attr('selected','selected'); + $stickychatCheckbox.change(); + $stickychatCheckbox.click(); + //check if chat changed to get the stickychat Class + var $chatbox = chrome$("#chatbox"); + var hasStickyChatClass = $chatbox.hasClass("stickyChat"); + expect(hasStickyChatClass).to.be(true); + + //select chat always on screen and fire change event + $stickychatCheckbox.attr('selected','selected'); + $stickychatCheckbox.change(); + $stickychatCheckbox.click(); + + //check if chat changed to remove the stickychat Class + var hasStickyChatClass = $chatbox.hasClass("stickyChat"); + expect(hasStickyChatClass).to.be(false); + + done(); + }); +}); diff --git a/tests/frontend/specs/chat_always_on_screen.js b/tests/frontend/specs/chat_always_on_screen.js deleted file mode 100644 index 4873763f..00000000 --- a/tests/frontend/specs/chat_always_on_screen.js +++ /dev/null @@ -1,40 +0,0 @@ -describe("chat always ons creen select", function(){ - //create a new pad before each test run - beforeEach(function(cb){ - helper.newPad(cb); - this.timeout(60000); - }); - - it("makes chat stick to right side of the screen", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); - $settingsButton.click(); - - //get the chat selector - var $stickychatCheckbox = chrome$("#options-stickychat"); - - //select chat always on screen and fire change event - $stickychatCheckbox.attr('selected','selected'); - $stickychatCheckbox.change(); - $stickychatCheckbox.click(); - - //check if chat changed to get the stickychat Class - var $chatbox = chrome$("#chatbox"); - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(true); - - //select chat always on screen and fire change event - $stickychatCheckbox.attr('selected','selected'); - $stickychatCheckbox.change(); - $stickychatCheckbox.click(); - - //check if chat changed to remove the stickychat Class - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(false); - - done(); - }); -}); diff --git a/tests/frontend/specs/button_clear_authorship_colors.js b/tests/frontend/specs/clear_authorship_colors.js index 5db35612..5db35612 100644 --- a/tests/frontend/specs/button_clear_authorship_colors.js +++ b/tests/frontend/specs/clear_authorship_colors.js diff --git a/tests/frontend/specs/keystroke_delete.js b/tests/frontend/specs/delete.js index 86e76f56..86e76f56 100644 --- a/tests/frontend/specs/keystroke_delete.js +++ b/tests/frontend/specs/delete.js diff --git a/tests/frontend/specs/keystroke_enter.js b/tests/frontend/specs/enter.js index e46b1d2f..baafeded 100644 --- a/tests/frontend/specs/keystroke_enter.js +++ b/tests/frontend/specs/enter.js @@ -5,7 +5,7 @@ describe("enter keystroke", function(){ this.timeout(60000); }); - it("creates a enw line & puts cursor onto a new line", function(done) { + it("creates a new line & puts cursor onto a new line", function(done) { var inner$ = helper.padInner$; var chrome$ = helper.padChrome$; diff --git a/tests/frontend/specs/button_indentation.js b/tests/frontend/specs/indentation.js index 9c8e317e..9692120a 100644 --- a/tests/frontend/specs/button_indentation.js +++ b/tests/frontend/specs/indentation.js @@ -5,7 +5,32 @@ describe("indentation button", function(){ this.timeout(60000); }); - it("indent text", function(done){ + it("indent text with keypress", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //get the first text element out of the inner iframe + var $firstTextElement = inner$("div").first(); + + //select this text element + $firstTextElement.sendkeys('{selectall}'); + + if(inner$.browser.mozilla){ // if it's a mozilla browser + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + + var e = inner$.Event(evtType); + e.keyCode = 9; // tab :| + inner$("#innerdocbody").trigger(e); + + helper.waitFor(function(){ + return inner$("div").first().find("ul li").length === 1; + }).done(done); + }); + + it("indent text with button", function(done){ var inner$ = helper.padInner$; var chrome$ = helper.padChrome$; @@ -176,4 +201,5 @@ describe("indentation button", function(){ expect(isLI).to.be(true); },1000); });*/ + }); diff --git a/tests/frontend/specs/italic.js b/tests/frontend/specs/italic.js new file mode 100644 index 00000000..29dbae59 --- /dev/null +++ b/tests/frontend/specs/italic.js @@ -0,0 +1,73 @@ +describe("italic some text", function(){ + //create a new pad before each test run + beforeEach(function(cb){ + helper.newPad(cb); + this.timeout(60000); + }); + + it("makes text italic using button", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //get the first text element out of the inner iframe + var $firstTextElement = inner$("div").first(); + + //select this text element + $firstTextElement.sendkeys('{selectall}'); + + //get the bold button and click it + var $boldButton = chrome$(".buttonicon-italic"); + $boldButton.click(); + + //ace creates a new dom element when you press a button, so just get the first text element again + var $newFirstTextElement = inner$("div").first(); + + // is there a <i> element now? + var isItalic = $newFirstTextElement.find("i").length === 1; + + //expect it to be bold + expect(isItalic).to.be(true); + + //make sure the text hasn't changed + expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); + + done(); + }); + + it("makes text italic using keypress", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //get the first text element out of the inner iframe + var $firstTextElement = inner$("div").first(); + + //select this text element + $firstTextElement.sendkeys('{selectall}'); + + if(inner$.browser.mozilla){ // if it's a mozilla browser + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + + var e = inner$.Event(evtType); + e.ctrlKey = true; // Control key + e.which = 105; // i + inner$("#innerdocbody").trigger(e); + + //ace creates a new dom element when you press a button, so just get the first text element again + var $newFirstTextElement = inner$("div").first(); + + // is there a <i> element now? + var isItalic = $newFirstTextElement.find("i").length === 1; + + //expect it to be bold + expect(isItalic).to.be(true); + + //make sure the text hasn't changed + expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); + + done(); + }); + +}); diff --git a/tests/frontend/specs/language.js b/tests/frontend/specs/language.js index ab7f2b3d..d607ff98 100644 --- a/tests/frontend/specs/language.js +++ b/tests/frontend/specs/language.js @@ -13,7 +13,6 @@ describe("Language select and change", function(){ }); // Destroy language cookies - it("makes text german", function(done) { var inner$ = helper.padInner$; var chrome$ = helper.padChrome$; @@ -92,7 +91,7 @@ describe("Language select and change", function(){ var $languageoption = $language.find("[value=ar]"); //select arabic - $languageoption.attr('selected','selected'); + // $languageoption.attr('selected','selected'); // Breaks the test.. $language.val("ar"); $languageoption.change(); diff --git a/tests/frontend/specs/button_ordered_list.js b/tests/frontend/specs/ordered_list.js index ca7d755e..ca7d755e 100644 --- a/tests/frontend/specs/button_ordered_list.js +++ b/tests/frontend/specs/ordered_list.js diff --git a/tests/frontend/specs/redo.js b/tests/frontend/specs/redo.js new file mode 100644 index 00000000..c2f8a95a --- /dev/null +++ b/tests/frontend/specs/redo.js @@ -0,0 +1,76 @@ +describe("undo button then redo button", function(){ + beforeEach(function(cb){ + helper.newPad(cb); // creates a new pad + this.timeout(60000); + }); + + it("redo some typing with button", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + // get the first text element inside the editable space + var $firstTextElement = inner$("div span").first(); + var originalValue = $firstTextElement.text(); // get the original value + var newString = "Foo"; + + $firstTextElement.sendkeys(newString); // send line 1 to the pad + var modifiedValue = $firstTextElement.text(); // get the modified value + expect(modifiedValue).not.to.be(originalValue); // expect the value to change + + // get undo and redo buttons + var $undoButton = chrome$(".buttonicon-undo"); + var $redoButton = chrome$(".buttonicon-redo"); + // click the buttons + $undoButton.click(); // removes foo + $redoButton.click(); // resends foo + + helper.waitFor(function(){ + console.log(inner$("div span").first().text()); + return inner$("div span").first().text() === newString; + }).done(function(){ + var finalValue = inner$("div").first().text(); + expect(finalValue).to.be(modifiedValue); // expect the value to change + done(); + }); + }); + + it("redo some typing with keypress", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + // get the first text element inside the editable space + var $firstTextElement = inner$("div span").first(); + var originalValue = $firstTextElement.text(); // get the original value + var newString = "Foo"; + + $firstTextElement.sendkeys(newString); // send line 1 to the pad + var modifiedValue = $firstTextElement.text(); // get the modified value + expect(modifiedValue).not.to.be(originalValue); // expect the value to change + + if(inner$.browser.mozilla){ // if it's a mozilla browser + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + + var e = inner$.Event(evtType); + e.ctrlKey = true; // Control key + e.which = 90; // z + inner$("#innerdocbody").trigger(e); + + var e = inner$.Event(evtType); + e.ctrlKey = true; // Control key + e.which = 121; // y + inner$("#innerdocbody").trigger(e); + + helper.waitFor(function(){ + console.log(inner$("div span").first().text()); + return inner$("div span").first().text() === newString; + }).done(function(){ + var finalValue = inner$("div").first().text(); + expect(finalValue).to.be(modifiedValue); // expect the value to change + done(); + }); + }); +}); + diff --git a/tests/frontend/specs/button_strikethrough.js b/tests/frontend/specs/strikethrough.js index 9afcea0f..9afcea0f 100644 --- a/tests/frontend/specs/button_strikethrough.js +++ b/tests/frontend/specs/strikethrough.js diff --git a/tests/frontend/specs/button_timeslider.js b/tests/frontend/specs/timeslider.js index cb37bacb..cb37bacb 100644 --- a/tests/frontend/specs/button_timeslider.js +++ b/tests/frontend/specs/timeslider.js diff --git a/tests/frontend/specs/undo.js b/tests/frontend/specs/undo.js new file mode 100644 index 00000000..0c58c9b8 --- /dev/null +++ b/tests/frontend/specs/undo.js @@ -0,0 +1,69 @@ +describe("undo button", function(){ + beforeEach(function(cb){ + helper.newPad(cb); // creates a new pad + this.timeout(60000); + }); + +/* + it("undo some typing by clicking undo button", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + // get the first text element inside the editable space + var $firstTextElement = inner$("div span").first(); + var originalValue = $firstTextElement.text(); // get the original value + + $firstTextElement.sendkeys("foo"); // send line 1 to the pad + var modifiedValue = $firstTextElement.text(); // get the modified value + expect(modifiedValue).not.to.be(originalValue); // expect the value to change + + // get clear authorship button as a variable + var $undoButton = chrome$(".buttonicon-undo"); + // click the button + $undoButton.click(); + + helper.waitFor(function(){ + return inner$("div span").first().text() === originalValue; + }).done(function(){ + var finalValue = inner$("div span").first().text(); + expect(finalValue).to.be(originalValue); // expect the value to change + done(); + }); + }); +*/ + + it("undo some typing using a keypress", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + // get the first text element inside the editable space + var $firstTextElement = inner$("div span").first(); + var originalValue = $firstTextElement.text(); // get the original value + + $firstTextElement.sendkeys("foo"); // send line 1 to the pad + var modifiedValue = $firstTextElement.text(); // get the modified value + expect(modifiedValue).not.to.be(originalValue); // expect the value to change + + if(inner$.browser.mozilla){ // if it's a mozilla browser + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + + var e = inner$.Event(evtType); + e.ctrlKey = true; // Control key + e.which = 90; // z + inner$("#innerdocbody").trigger(e); + + helper.waitFor(function(){ + return inner$("div span").first().text() === originalValue; + }).done(function(){ + var finalValue = inner$("div span").first().text(); + expect(finalValue).to.be(originalValue); // expect the value to change + done(); + }); + }); + + +}); + diff --git a/tests/frontend/specs/keystroke_urls_become_clickable.js b/tests/frontend/specs/urls_become_clickable.js index 8c72d748..8c72d748 100644 --- a/tests/frontend/specs/keystroke_urls_become_clickable.js +++ b/tests/frontend/specs/urls_become_clickable.js |