diff --git a/LICENSE b/LICENSE index a35c25535..8ff473088 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -187,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2012 THE ETHERPAD FOUNDATION + Copyright 2012-2013 THE ETHERPAD FOUNDATION Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app.js b/app.js new file mode 100755 index 000000000..8c8bf5753 --- /dev/null +++ b/app.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +var server = require("ep_etherpad-lite/node/server.js"); diff --git a/package.json b/package.json new file mode 100644 index 000000000..398ba8b19 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "etherpad-lite", + "version": "0.0.1", + "engines": { + "node" : ">=0.6.0", + "npm" : ">=1.0" + } +} diff --git a/src/ep.json b/src/ep.json index 89c8964aa..3a1246232 100644 --- a/src/ep.json +++ b/src/ep.json @@ -23,6 +23,9 @@ { "name": "adminsettings", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer", "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" } + }, + { "name": "teampad", "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/teampad:expressCreateServer" } } ] } diff --git a/src/node/db/API.js b/src/node/db/API.js index f99a43afd..07141fec2 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -325,17 +325,17 @@ exports.getChatHistory = function(padID, start, end, callback) if(!start || !end) { start = 0; - end = pad.chatHead - 1; + end = pad.chatHead; } - if(start >= chatHead) + if(start >= chatHead && chatHead > 0) { callback(new customError("start is higher or equal to the current chatHead","apierror")); return; } - if(end >= chatHead) + if(end > chatHead) { - callback(new customError("end is higher or equal to the current chatHead","apierror")); + callback(new customError("end is higher than the current chatHead","apierror")); return; } diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 4701e82a3..f89efaf75 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -38,6 +38,7 @@ var Pad = function Pad(id) { this.head = -1; this.chatHead = -1; this.publicStatus = false; + this.teamStatus = false; this.passwordHash = null; this.id = id; this.savedRevisions = []; @@ -551,6 +552,15 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() { return this.savedRevisions; }; +Pad.prototype.getTeamStatus = function getTeamStatus() { + return this.teamStatus; +}; + +Pad.prototype.setTeamStatus = function setTeamStatus(teamStatus) { + this.teamStatus = teamStatus; + this.saveToDatabase(); +}; + /* Crypto helper methods */ function hash(password, salt) diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 5e0af4643..2bd3d99ee 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -236,3 +236,23 @@ exports.unloadPad = function(padId) if(globalPads.get(padId)) globalPads.remove(padId); } + +//checks if a pad is a "team pad" +exports.isTeamPad = function(padId) +{ + var isTeamPad = false; + db.get("pad:"+padId, function(err, value) + { + if(ERR(err)) return; + + if(value != null && value.atext && value.teamStatus){ + isTeamPad = true; + } + else + { + isTeamPad = false; + } + }); + + return isTeamPad; +} diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 4289e39ca..c96f9ac30 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -49,6 +49,27 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) callback(null, {accessStatus: "deny"}); return; } + } + else if(padManager.isTeamPad(padID)) + { + authorManager.getAuthor4Token(token, function(err, author) + { + if(ERR(err, callback)) return; + + // TODO check session + console.log('sessionCookie: ' + sessionCookie); + sessionManager.getSessionInfo(sessionCookie, function(err, result) { + if (err) { + statusObject = {accessStatus: "denyTeamPad", authorID: author}; + callback(null, statusObject); + } else { + // TODO figure out how to force authorID to match account name... + statusObject = {accessStatus: "grant", authorID: author}; + callback(null, statusObject); + } + }); + }); + } // a session is not required, so we'll check if it's a public pad else diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 5ce4f7487..b8eb0de4c 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -360,6 +360,66 @@ function listSessionsWithDBKey (dbkey, callback) }); } +/** + * Creates a new session based on an externally-verified account (e.g. persona) + */ +exports.createVerifiedSession = function(sessionID, account, validUntil, callback) +{ + async.series([ + + //check validUntil and create the session db entry + function(callback) + { + //check if rev is a number + if(typeof validUntil != "number") + { + //try to parse the number + if(!isNaN(parseInt(validUntil))) + { + validUntil = parseInt(validUntil); + } + else + { + callback(new customError("validUntil is not a number","apierror")); + return; + } + } + + //ensure this is not a negativ number + if(validUntil < 0) + { + callback(new customError("validUntil is a negativ number","apierror")); + return; + } + + //ensure this is not a float value + if(!is_int(validUntil)) + { + callback(new customError("validUntil is a float value","apierror")); + return; + } + + //check if validUntil is in the future + if(Math.floor(new Date().getTime()/1000) > validUntil) + { + callback(new customError("validUntil is in the past","apierror")); + return; + } + + //set the session into the database + db.set("session:" + sessionID, {"account": account, "validUntil": validUntil}); + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + + //return error and sessionID + callback(null, {sessionID: sessionID}); + }) +} + //checks if a number is an int function is_int(value) { diff --git a/src/node/db/TeamManager.js b/src/node/db/TeamManager.js new file mode 100644 index 000000000..47798739b --- /dev/null +++ b/src/node/db/TeamManager.js @@ -0,0 +1,211 @@ +/** + * The Team Manager provides functions to manage teams in the database + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var ERR = require("async-stacktrace"); +var customError = require("../utils/customError"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var db = require("./DB").db; +var async = require("async"); +var padManager = require("./PadManager"); +var sessionManager = require("./SessionManager"); + +exports.listAllTeams = function(callback) { + db.get("teams", function (err, teams) { + if(ERR(err, callback)) return; + + // there are no teams + if(teams == null) { + callback(null, {teamIDs: []}); + return; + } + + var teamIDs = []; + for ( var teamID in teams) { + teamIDs.push(teamID); + } + callback(null, {teamIDs: teamIDs}); + }); +} + +exports.doesTeamExist = function(teamID, callback) +{ + //try to get the team entry + db.get("team:" + teamID, function (err, team) + { + if(ERR(err, callback)) return; + callback(null, team != null); + }); +} + +exports.createTeam = function(teamName, pads, accounts, admins, callback) +{ + //search for non existing teamID + var teamID = "t." + randomString(16); + + //create the team + db.set("team:" + teamID, {name: teamName, pads: pads, accounts: accounts, + admins: admins}); + + //list the team + exports.listAllTeams(function(err, teams) { + if(ERR(err, callback)) return; + teams = teams? teams.teamIDs : []; + + teams.push(teamID); + + // regenerate team list + var newTeams = {}; + async.forEach(teams, function(team, cb) { + newTeams[team] = 1; + cb(); + },function() { + db.set("teams", newTeams); + callback(null, {teamID: teamID}); + }); + }); +} + +exports.createTeamPad = function(teamName, teamID, padName, text, callback) +{ + //create the padID + var padID = teamName + "+" + padName; + + async.series([ + //ensure team exists + function (callback) + { + exports.doesTeamExist(teamID, function(err, exists) + { + if(ERR(err, callback)) return; + + //team does not exist + if(exists == false) + { + callback(new customError("teamID does not exist","apierror")); + } + //team exists, everything is fine + else + { + callback(); + } + }); + }, + //ensure pad does not exists + function (callback) + { + padManager.doesPadExists(padID, function(err, exists) + { + if(ERR(err, callback)) return; + + //pad exists already + if(exists == true) + { + callback(new customError("padName does already exist","apierror")); + } + //pad does not exist, everything is fine + else + { + callback(); + } + }); + }, + //create the pad + function (callback) + { + padManager.getPad(padID, text, function(err, pad) + { + if(ERR(err, callback)) return; + + pad.setTeamStatus(true); + + callback(); + }); + }, + //add to DB + function (callback) + { + db.get("team:" + teamID, function(err, result) + { + if(ERR(err, callback)) return; + + result.pads.push(padID); + db.set('team:' + teamID, result); + }); + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, {padID: padID}); + }); +} + +exports.listInfo = function(teamID, callback) +{ + exports.doesTeamExist(teamID, function(err, exists) + { + if(ERR(err, callback)) return; + + //team does not exist + if(exists == false) + { + callback(new customError("teamID does not exist","apierror")); + } + //team exists, let's get the info + else + { + db.get("team:" + teamID, function(err, result) + { + if(ERR(err, callback)) return; + + callback(null, result); + }); + } + }); +} + +exports.addAccountToTeam = function(teamID, account, callback) +{ + exports.doesTeamExist(teamID, function(err, exists) + { + if(ERR(err, callback)) return; + + //team does not exist + if(exists == false) + { + console.log('debug1: ' + teamID); + callback(new customError("teamID does not exist","apierror")); + } + //team exists, let's get the info + else + { + db.get("team:" + teamID, function(err, result) + { + if(ERR(err, callback)) return; + + result.accounts.push(account); + console.log('setting team to: ' + result); + db.set("team:" + teamID, result); + callback(null, result); + }); + } + }); +} diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 6781cd884..25e439585 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -238,7 +238,14 @@ exports.handleMessage = function(client, message) // our "sessions" "connections". // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess(message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(message.padID)) { + sessionID = message.sessionid; + } else { + sessionID = client.handshake.sessionID; + } + securityManager.checkAccess(message.padId, sessionID, message.token, message.password, function(err, statusObject) { if(ERR(err, callback)) return; @@ -874,7 +881,14 @@ function handleClientReady(client, message) // our "sessions" "connections". // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess (padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(message.padID)) { + sessionID = message.sessionid; + } else { + sessionID = client.handshake.sessionID; + } + securityManager.checkAccess (padIds.padId, sessionID, message.token, message.password, function(err, statusObject) { if(ERR(err, callback)) return; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index f3b82b8c7..159e8cee1 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -23,6 +23,7 @@ var ERR = require("async-stacktrace"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var securityManager = require("../db/SecurityManager"); +var padManager = require("../db/PadManager"); /** * Saves all components @@ -108,7 +109,14 @@ exports.setSocketIO = function(_socket) //this message has everything to try an authorization if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { - securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(message.padID)) { + sessionID = message.sessionid; + } else { + sessionID = client.handshake.sessionID; + } + securityManager.checkAccess (message.padId, sessionID, message.token, message.password, function(err, statusObject) { ERR(err); diff --git a/src/node/hooks/express/teampad.js b/src/node/hooks/express/teampad.js new file mode 100644 index 000000000..9598d1fed --- /dev/null +++ b/src/node/hooks/express/teampad.js @@ -0,0 +1,276 @@ +var express = require('express'), + async = require('async'), + eejs = require('ep_etherpad-lite/node/eejs'), + teamManager = require('ep_etherpad-lite/node/db/TeamManager'), + sessionManager = require('ep_etherpad-lite/node/db/SessionManager'), + padManager = require('ep_etherpad-lite/node/db/PadManager'), + https = require('https'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.use(express.bodyParser()); + + // TODO use more generic, pluggable auth, hardcoded to persona for now + args.app.post('/teampad/verify', function(req, res) { + console.log('sign in attempt'); + var body = JSON.stringify({ + assertion: req.param('assertion', null), + audience: 'http://' + req.headers.host + }); + + var vreq = https.request({ + host: 'persona.org', + path: '/verify', + method: 'POST', + headers: { + 'Content-Length': body.length, + 'Content-Type': 'application/json' + } + }, function(vres) { + var body = ''; + vres.on('data', function(chunk) { body += chunk; }); + vres.on('end', function() { + try { + account = JSON.parse(body).email; + validUntil = JSON.parse(body).expires; + console.log(body); + var sessionID = req.signedCookies.express_sid; + sessionManager.createVerifiedSession( + sessionID, account, validUntil, function(err, result) { + if (err) { + console.log(err); + return; + } + }); + console.log(account + ' logged in'); + } catch(e) { + console.log(e); + } + }); + res.redirect('/teampad'); + }); + vreq.write(body); + vreq.end(); + }); + + args.app.post('/teampad/createteam', function(req, res) { + var sessionID = req.signedCookies.express_sid, + currentUser = null, + signedIn = false, + teamName = null, + rawTeamName = req.param('teamname', null); + + async.waterfall([ + function(callback) { + sessionManager.getSessionInfo(sessionID, callback); + }, + function(result, callback) { + currentUser = result.account; + signedIn = true; + callback(); + }, + function(callback) { + console.log('about to sanitize ' + rawTeamName); + padManager.sanitizePadId(rawTeamName, function(teamName) { + callback(null, teamName); + }) + }, + function(result, callback) { + teamName = result; + console.log('sanitized ' + teamName); + teamManager.createTeam(teamName, [], [currentUser], [currentUser], + callback); + }, + function(teamID, callback) { + console.log(teamID + ' created for ' + teamName); + res.redirect('/teampad'); + } + ], function(err) { + console.log('error: ' + err); + res.redirect('/teampad'); + }); + }); + + args.app.post('/teampad/createpad', function(req, res) { + var sessionID = req.signedCookies.express_sid; + + var teamName = null, + padName = null, + currentUser = null, + signedIn = false, + teamID = req.param('teamID', null), + rawTeamName = req.param('teamname', null), + rawPadName = req.param('padname', null); + + async.waterfall([ + function(callback) { + sessionManager.getSessionInfo(sessionID, callback); + }, + function(result, callback) { + currentUser = result.account; + signedIn = true; + padManager.sanitizePadId(rawTeamName, function(teamName) { + callback(null, teamName); + }); + }, + function(result, callback) { + teamName = result; + padManager.sanitizePadId(rawPadName, function(padName) { + callback(null, padName); + }); + }, + function(result, callback) { + padName = result; + teamManager.createTeamPad(teamName, teamID, padName, 'super sekrit!', + callback); + }, + function(callback) { + console.log(padName + ' created for ' + teamName); + res.redirect('/teampad/' + teamName); + } + ], function(err) { + console.log(err); + res.redirect('/teampad'); + }); + }); + + args.app.post('/teampad/addaccount', function(req, res) { + var sessionID = req.signedCookies.express_sid, + currentUser = null, + signedIn = false, + teamName = null, + teamID = req.param('teamID', null), + rawTeamName = req.param('teamname', null), + account = req.param('accountname', null); + + async.waterfall([ + function(callback) { + sessionManager.getSessionInfo(sessionID, callback); + }, + function(result, callback) { + currentUser = result.account; + padManager.sanitizePadId(rawTeamName, function(teamName) { + callback(null, teamName); + }); + }, + function(result, callback) { + teamName = result; + console.log('teamID: ' + teamID); + teamManager.addAccountToTeam(teamID, account, callback); + }, + function(result, callback) { + teamID = result; + console.log(account+ ' added to ' + teamID); + res.redirect('/teampad/' + teamName); + }, + ], function(err) { + console.log(err); + }); + }); + + args.app.get('/teampad', function(req, res) { + var sessionID = req.signedCookies.express_sid; + var currentUser = null; + var signedIn = false; + + sessionManager.getSessionInfo(sessionID, function(err, result) { + if (err) { + console.log(err); + } else { + currentUser = result.account; + signedIn = true; + } + }); + + var teamsInfo = []; + + // TODO an index for finding teams by account would make this + // *way* faster and easier... + teamManager.listAllTeams(function(err, teams) { + for (var team in teams.teamIDs) { + teamID = teams.teamIDs[team]; + teamManager.listInfo(teamID, function(err, info) { + if (info.accounts) { + if (info.accounts.indexOf(currentUser) != -1) { + teamsInfo.push(info); + } + } + }); + } + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); + + res.send(eejs.require('ep_etherpad-lite/templates/teampad/index.html', + { teamsInfo: teamsInfo, + signedIn: signedIn, + currentUser: currentUser})); + }); + }); + + args.app.get('/teampad/:teamName', function(req, res) { + var sessionID = req.signedCookies.express_sid; + var currentUser = null; + var signedIn = false; + + sessionManager.getSessionInfo(sessionID, function(err, result) { + if (err) { + console.log(err); + res.redirect('/teampad'); + } else { + currentUser = result.account; + signedIn = true; + + var teamName = req.path.split('/')[2]; + var teamInfo = { + pads: [], + accounts: [], + name: [], + teamID: [] + }; + + // TODO an index for finding pads/accounts by team would make this + // *way* faster and easier... + teamManager.listAllTeams(function(err, teams) { + for (var team in teams.teamIDs) { + teamID = teams.teamIDs[team]; + teamManager.listInfo(teamID, function(err, info) { + if (info.name) { + if (teamName === info.name) { + teamInfo = info; + teamInfo.teamID = teamID; + } + } + }); + } + + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); + + res.send(eejs.require('ep_etherpad-lite/templates/teampad/team.html', + {teamInfo: teamInfo, + signedIn: false})); + }); + } + }); + }); + + // TODO implement, for now we are linking to normal pads via templates + args.app.get('/teampad/:teamName/:padName', function(req, res) { + var sessionID = req.signedCookies.express_sid; + var currentUser = null; + var signedIn = false; + + sessionManager.getSessionInfo(sessionID, function(err, result) { + if (err) { + console.log(err); + res.redirect('/teampad'); + } else { + currentUser = result.account; + signedIn = true; + } + }); + + var padName = req.path.split('/')[3]; + + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); + + res.send(eejs.require('ep_etherpad-lite/templates/teampad/pad.html')); + }); +} diff --git a/src/node/padaccess.js b/src/node/padaccess.js index d87809149..cc2dc3729 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,5 +1,6 @@ var ERR = require("async-stacktrace"); var securityManager = require('./db/SecurityManager'); +var padManager = require("./db/PadManager"); //checks for padAccess module.exports = function (req, res, callback) { @@ -7,7 +8,14 @@ module.exports = function (req, res, callback) { // FIXME: Why is this ever undefined?? if (req.cookies === undefined) req.cookies = {}; - securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(req.params.pad)) { + sessionID = req.cookies.sessionid; + } else { + sessionID = req.cookies.express_sid; + } + securityManager.checkAccess(req.params.pad, sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { if(ERR(err, callback)) return; //there is access, continue diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 8435ab2c2..d9692f9b9 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -45,7 +45,7 @@ exports.faviconTimeslider = "../../" + exports.favicon; /** * The IP ep-lite should listen to */ -exports.ip = "0.0.0.0"; +exports.ip = process.env.VCAP_APP_HOST || "0.0.0.0"; /** * The Port ep-lite should listen to diff --git a/src/static/css/admin.css b/src/static/css/admin.css index 3a7291516..9ea02744f 100644 --- a/src/static/css/admin.css +++ b/src/static/css/admin.css @@ -42,6 +42,7 @@ div.innerwrapper { border-radius: 0 0 7px 7px; margin-left:250px; min-width:400px; + width:100%; } #wrapper { diff --git a/src/static/css/teampad.css b/src/static/css/teampad.css new file mode 100644 index 000000000..c52bd277f --- /dev/null +++ b/src/static/css/teampad.css @@ -0,0 +1,101 @@ +body { + margin: 0; + color: #333; + font: 14px helvetica, sans-serif; + background: #ddd; + background: -webkit-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -moz-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -ms-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -o-radial-gradient(circle,#aaa,#eee 60%) center fixed; + border-top: 8px solid rgba(51,51,51,.8); +} +#wrapper { + margin-top: 160px; + padding: 15px; + background: #fff; + opacity: .9; + box-shadow: 0px 1px 8px rgba(0,0,0,0.3); + max-width: 700px; + margin: auto; + border-radius: 0 0 7px 7px; +} +h1 { + font-size: 29px; +} +h2 { + font-size: 24px; +} +.separator { + margin: 10px 0; + height: 1px; + background: #aaa; + background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); +} +form { + margin-bottom: 0; +} +#inner, +#footer { + margin:0 auto; + width: 300px; +} +#footer { + margin-top: 160px; + padding: 15px; + text-align: center; +} +#teampads { + color: inherit; + font-size: 24px; + text-decoration: none; + letter-spacing: -2px; +} +#teampads::after { + content: ' »'; +} +input { + font-weight: bold; + font-size: 15px; +} +input[type="button"] { + padding: 4px 6px; + margin: 0; +} +input[type="button"].do-install, input[type="button"].do-uninstall { + float: right; + width: 100px; +} +input[type="button"]#do-search { + display: block; +} +input[type="text"] { + border-radius: 3px; + box-sizing: border-box; + -moz-box-sizing: border-box; + padding: 10px; + *padding: 0; /* IE7 hack */ + width: 100%; + outline: none; + border: 1px solid #ddd; + margin: 0 0 5px 0; + max-width: 500px; +} +table { + border: 1px solid #ddd; + border-radius: 3px; + border-spacing: 0; + width: 100%; + margin: 20px 0; +} +table thead tr { + background: #eee; +} +td, th { + padding: 5px; +} +.template { + display: none; +} diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js index 8a7d16ee3..fb7ce4feb 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -33,19 +33,6 @@ function object(o) f.prototype = o; return new f(); } -var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase(); - -// Figure out what browser is being used (stolen from jquery 1.2.1) -var browser = { - version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1], - safari: /webkit/.test(userAgent), - opera: /opera/.test(userAgent), - msie: /msie/.test(userAgent) && !/opera/.test(userAgent), - mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent), - windows: /windows/.test(userAgent), - mobile: /mobile/.test(userAgent) || /android/.test(userAgent) -}; - function getAssoc(obj, name) { @@ -97,7 +84,6 @@ var noop = function(){}; exports.isNodeText = isNodeText; exports.object = object; -exports.browser = browser; exports.getAssoc = getAssoc; exports.setAssoc = setAssoc; exports.binarySearch = binarySearch; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index da8dea85d..d62b9f685 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -28,7 +28,7 @@ $ = jQuery = require('./rjquery').$; _ = require("./underscore"); var isNodeText = Ace2Common.isNodeText, - browser = Ace2Common.browser, + browser = $.browser, getAssoc = Ace2Common.getAssoc, setAssoc = Ace2Common.setAssoc, isTextNode = Ace2Common.isTextNode, @@ -154,7 +154,12 @@ function Ace2Inner(){ var dmesg = noop; window.dmesg = noop; - var scheduler = parent; + // Ugly hack for Firefox 18 + // get the timeout and interval methods from the parent iframe + setTimeout = parent.setTimeout; + clearTimeout = parent.clearTimeout; + setInterval = parent.setInterval; + clearInterval = parent.clearInterval; var textFace = 'monospace'; var textSize = 12; @@ -174,7 +179,7 @@ function Ace2Inner(){ parentDynamicCSS = makeCSSManager("dynamicsyntax", true); } - var changesetTracker = makeChangesetTracker(scheduler, rep.apool, { + var changesetTracker = makeChangesetTracker(rep.apool, { withCallbacks: function(operationName, f) { inCallStackIfNecessary(operationName, function() @@ -594,7 +599,7 @@ function Ace2Inner(){ doesWrap = newVal; var dwClass = "doesWrap"; setClassPresence(root, "doesWrap", doesWrap); - scheduler.setTimeout(function() + setTimeout(function() { inCallStackIfNecessary("setWraps", function() { @@ -634,7 +639,7 @@ function Ace2Inner(){ textFace = face; root.style.fontFamily = textFace; lineMetricsDiv.style.fontFamily = textFace; - scheduler.setTimeout(function() + setTimeout(function() { setUpTrackingCSS(); }, 0); @@ -647,7 +652,7 @@ function Ace2Inner(){ root.style.lineHeight = textLineHeight() + "px"; sideDiv.style.lineHeight = textLineHeight() + "px"; lineMetricsDiv.style.fontSize = textSize + "px"; - scheduler.setTimeout(function() + setTimeout(function() { setUpTrackingCSS(); }, 0); @@ -1085,7 +1090,7 @@ function Ace2Inner(){ { if (scheduledTimeout) { - scheduler.clearTimeout(scheduledTimeout); + clearTimeout(scheduledTimeout); scheduledTimeout = null; } } @@ -1096,7 +1101,7 @@ function Ace2Inner(){ scheduledTime = time; var delay = time - now(); if (delay < 0) delay = 0; - scheduledTimeout = scheduler.setTimeout(callback, delay); + scheduledTimeout = setTimeout(callback, delay); } function callback() @@ -2817,7 +2822,6 @@ function Ace2Inner(){ rep.selStart = selectStart; rep.selEnd = selectEnd; rep.selFocusAtStart = newSelFocusAtStart; - if (mozillaFakeArrows) mozillaFakeArrows.notifySelectionChanged(); currentCallStack.repChanged = true; return true; @@ -3614,7 +3618,7 @@ function Ace2Inner(){ evt.preventDefault(); doReturnKey(); //scrollSelectionIntoView(); - scheduler.setTimeout(function() + setTimeout(function() { outerWin.scrollBy(-100, 0); }, 0); @@ -3690,11 +3694,41 @@ function Ace2Inner(){ doDeleteKey(); specialHandled = true; } + if((evt.which == 33 || evt.which == 34) && type == 'keydown'){ + var oldVisibleLineRange = getVisibleLineRange(); + var topOffset = rep.selStart[0] - oldVisibleLineRange[0]; + if(topOffset < 0 ){ + topOffset = 0; + } - if (mozillaFakeArrows && mozillaFakeArrows.handleKeyEvent(evt)) - { - evt.preventDefault(); - specialHandled = true; + var isPageDown = evt.which === 34; + var isPageUp = evt.which === 33; + + setTimeout(function(){ + var newVisibleLineRange = getVisibleLineRange(); + var linesCount = rep.lines.length(); + + var newCaretRow = rep.selStart[0]; + if(isPageUp){ + newCaretRow = oldVisibleLineRange[0]; + } + + if(isPageDown){ + newCaretRow = newVisibleLineRange[0] + topOffset; + } + + //ensure min and max + if(newCaretRow < 0){ + newCaretRow = 0; + } + if(newCaretRow >= linesCount){ + newCaretRow = linesCount-1; + } + + rep.selStart[0] = newCaretRow; + rep.selEnd[0] = newCaretRow; + updateBrowserSelectionFromRep(); + }, 200); } } @@ -4119,6 +4153,11 @@ function Ace2Inner(){ selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); + + if(selection.startPoint.node.ownerDocument !== window.document){ + return null; + } + return selection; } else return null; @@ -4722,7 +4761,7 @@ function Ace2Inner(){ }); - scheduler.setTimeout(function() + setTimeout(function() { parent.readyFunc(); // defined in code that sets up the inner iframe }, 0); @@ -5032,331 +5071,6 @@ function Ace2Inner(){ } editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList; editorInfo.ace_doInsertOrderedList = doInsertOrderedList; - - var mozillaFakeArrows = (browser.mozilla && (function() - { - // In Firefox 2, arrow keys are unstable while DOM-manipulating - // operations are going on. Specifically, if an operation - // (computation that ties up the event queue) is going on (in the - // call-stack of some event, like a timeout) that at some point - // mutates nodes involved in the selection, then the arrow - // keypress may (randomly) move the caret to the beginning or end - // of the document. If the operation also mutates the selection - // range, the old selection or the new selection may be used, or - // neither. - // As long as the arrow is pressed during the busy operation, it - // doesn't seem to matter that the keydown and keypress events - // aren't generated until afterwards, or that the arrow movement - // can still be stopped (meaning it hasn't been performed yet); - // Firefox must be preserving some old information about the - // selection or the DOM from when the key was initially pressed. - // However, it also doesn't seem to matter when the key was - // actually pressed relative to the time of the mutation within - // the prolonged operation. Also, even in very controlled tests - // (like a mutation followed by a long period of busyWaiting), the - // problem shows up often but not every time, with no discernable - // pattern. Who knows, it could have something to do with the - // caret-blinking timer, or DOM changes not being applied - // immediately. - // This problem, mercifully, does not show up at all in IE or - // Safari. My solution is to have my own, full-featured arrow-key - // implementation for Firefox. - // Note that the problem addressed here is potentially very subtle, - // especially if the operation is quick and is timed to usually happen - // when the user is idle. - // features: - // - 'up' and 'down' arrows preserve column when passing through shorter lines - // - shift-arrows extend the "focus" point, which may be start or end of range - // - the focus point is kept horizontally and vertically scrolled into view - // - arrows without shift cause caret to move to beginning or end of selection (left,right) - // or move focus point up or down a line (up,down) - // - command-(left,right,up,down) on Mac acts like (line-start, line-end, doc-start, doc-end) - // - takes wrapping into account when doesWrap is true, i.e. up-arrow and down-arrow move - // between the virtual lines within a wrapped line; this was difficult, and unfortunately - // requires mutating the DOM to get the necessary information - var savedFocusColumn = 0; // a value of 0 has no effect - var updatingSelectionNow = false; - - function getVirtualLineView(lineNum) - { - var lineNode = rep.lines.atIndex(lineNum).lineNode; - while (lineNode.firstChild && isBlockElement(lineNode.firstChild)) - { - lineNode = lineNode.firstChild; - } - return makeVirtualLineView(lineNode); - } - - function markerlessLineAndChar(line, chr) - { - return [line, chr - rep.lines.atIndex(line).lineMarker]; - } - - function markerfulLineAndChar(line, chr) - { - return [line, chr + rep.lines.atIndex(line).lineMarker]; - } - - return { - notifySelectionChanged: function() - { - if (!updatingSelectionNow) - { - savedFocusColumn = 0; - } - }, - handleKeyEvent: function(evt) - { - // returns "true" if handled - if (evt.type != "keypress") return false; - var keyCode = evt.keyCode; - if (keyCode < 37 || keyCode > 40) return false; - incorporateUserChanges(); - - if (!(rep.selStart && rep.selEnd)) return true; - - // {byWord,toEnd,normal} - var moveMode = (evt.altKey ? "byWord" : (evt.ctrlKey ? "byWord" : (evt.metaKey ? "toEnd" : "normal"))); - - var anchorCaret = markerlessLineAndChar(rep.selStart[0], rep.selStart[1]); - var focusCaret = markerlessLineAndChar(rep.selEnd[0], rep.selEnd[1]); - var wasCaret = isCaret(); - if (rep.selFocusAtStart) - { - var tmp = anchorCaret; - anchorCaret = focusCaret; - focusCaret = tmp; - } - var K_UP = 38, - K_DOWN = 40, - K_LEFT = 37, - K_RIGHT = 39; - var dontMove = false; - if (wasCaret && !evt.shiftKey) - { - // collapse, will mutate both together - anchorCaret = focusCaret; - } - else if ((!wasCaret) && (!evt.shiftKey)) - { - if (keyCode == K_LEFT) - { - // place caret at beginning - if (rep.selFocusAtStart) anchorCaret = focusCaret; - else focusCaret = anchorCaret; - if (moveMode == "normal") dontMove = true; - } - else if (keyCode == K_RIGHT) - { - // place caret at end - if (rep.selFocusAtStart) focusCaret = anchorCaret; - else anchorCaret = focusCaret; - if (moveMode == "normal") dontMove = true; - } - else - { - // collapse, will mutate both together - anchorCaret = focusCaret; - } - } - if (!dontMove) - { - function lineLength(i) - { - var entry = rep.lines.atIndex(i); - return entry.text.length - entry.lineMarker; - } - - function lineText(i) - { - var entry = rep.lines.atIndex(i); - return entry.text.substring(entry.lineMarker); - } - - if (keyCode == K_UP || keyCode == K_DOWN) - { - var up = (keyCode == K_UP); - var canChangeLines = ((up && focusCaret[0]) || ((!up) && focusCaret[0] < rep.lines.length() - 1)); - var virtualLineView, virtualLineSpot, canChangeVirtualLines = false; - if (doesWrap) - { - virtualLineView = getVirtualLineView(focusCaret[0]); - virtualLineSpot = virtualLineView.getVLineAndOffsetForChar(focusCaret[1]); - canChangeVirtualLines = ((up && virtualLineSpot.vline > 0) || ((!up) && virtualLineSpot.vline < ( - virtualLineView.getNumVirtualLines() - 1))); - } - var newColByVirtualLineChange; - if (moveMode == "toEnd") - { - if (up) - { - focusCaret[0] = 0; - focusCaret[1] = 0; - } - else - { - focusCaret[0] = rep.lines.length() - 1; - focusCaret[1] = lineLength(focusCaret[0]); - } - } - else if (moveMode == "byWord") - { - // move by "paragraph", a feature that Firefox lacks but IE and Safari both have - if (up) - { - if (focusCaret[1] === 0 && canChangeLines) - { - focusCaret[0]--; - focusCaret[1] = 0; - } - else focusCaret[1] = 0; - } - else - { - var lineLen = lineLength(focusCaret[0]); - if (browser.windows) - { - if (canChangeLines) - { - focusCaret[0]++; - focusCaret[1] = 0; - } - else - { - focusCaret[1] = lineLen; - } - } - else - { - if (focusCaret[1] == lineLen && canChangeLines) - { - focusCaret[0]++; - focusCaret[1] = lineLength(focusCaret[0]); - } - else - { - focusCaret[1] = lineLen; - } - } - } - savedFocusColumn = 0; - } - else if (canChangeVirtualLines) - { - var vline = virtualLineSpot.vline; - var offset = virtualLineSpot.offset; - if (up) vline--; - else vline++; - if (savedFocusColumn > offset) offset = savedFocusColumn; - else - { - savedFocusColumn = offset; - } - var newSpot = virtualLineView.getCharForVLineAndOffset(vline, offset); - focusCaret[1] = newSpot.lineChar; - } - else if (canChangeLines) - { - if (up) focusCaret[0]--; - else focusCaret[0]++; - var offset = focusCaret[1]; - if (doesWrap) - { - offset = virtualLineSpot.offset; - } - if (savedFocusColumn > offset) offset = savedFocusColumn; - else - { - savedFocusColumn = offset; - } - if (doesWrap) - { - var newLineView = getVirtualLineView(focusCaret[0]); - var vline = (up ? newLineView.getNumVirtualLines() - 1 : 0); - var newSpot = newLineView.getCharForVLineAndOffset(vline, offset); - focusCaret[1] = newSpot.lineChar; - } - else - { - var lineLen = lineLength(focusCaret[0]); - if (offset > lineLen) offset = lineLen; - focusCaret[1] = offset; - } - } - else - { - if (up) focusCaret[1] = 0; - else focusCaret[1] = lineLength(focusCaret[0]); - savedFocusColumn = 0; - } - } - else if (keyCode == K_LEFT || keyCode == K_RIGHT) - { - var left = (keyCode == K_LEFT); - if (left) - { - if (moveMode == "toEnd") focusCaret[1] = 0; - else if (focusCaret[1] > 0) - { - if (moveMode == "byWord") - { - focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false); - } - else - { - focusCaret[1]--; - } - } - else if (focusCaret[0] > 0) - { - focusCaret[0]--; - focusCaret[1] = lineLength(focusCaret[0]); - if (moveMode == "byWord") - { - focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false); - } - } - } - else - { - var lineLen = lineLength(focusCaret[0]); - if (moveMode == "toEnd") focusCaret[1] = lineLen; - else if (focusCaret[1] < lineLen) - { - if (moveMode == "byWord") - { - focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true); - } - else - { - focusCaret[1]++; - } - } - else if (focusCaret[0] < rep.lines.length() - 1) - { - focusCaret[0]++; - focusCaret[1] = 0; - if (moveMode == "byWord") - { - focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true); - } - } - } - savedFocusColumn = 0; - } - } - - var newSelFocusAtStart = ((focusCaret[0] < anchorCaret[0]) || (focusCaret[0] == anchorCaret[0] && focusCaret[1] < anchorCaret[1])); - var newSelStart = (newSelFocusAtStart ? focusCaret : anchorCaret); - var newSelEnd = (newSelFocusAtStart ? anchorCaret : focusCaret); - updatingSelectionNow = true; - performSelectionChange(markerfulLineAndChar(newSelStart[0], newSelStart[1]), markerfulLineAndChar(newSelEnd[0], newSelEnd[1]), newSelFocusAtStart); - updatingSelectionNow = false; - currentCallStack.userChangedSelection = true; - return true; - } - }; - })()); var lineNumbersShown; var sideDivInner; @@ -5495,7 +5209,7 @@ function Ace2Inner(){ documentAttributeManager: documentAttributeManager }); - scheduler.setTimeout(function() + setTimeout(function() { parent.readyFunc(); // defined in code that sets up the inner iframe }, 0); diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js index 58ef21cb5..91e854d65 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -23,7 +23,7 @@ var AttributePool = require('./AttributePool'); var Changeset = require('./Changeset'); -function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) +function makeChangesetTracker(apool, aceCallbacksProvider) { // latest official text from server @@ -51,7 +51,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) // and if there isn't a timeout already scheduled. if (changeCallback && changeCallbackTimeout === null) { - changeCallbackTimeout = scheduler.setTimeout(function() + changeCallbackTimeout = setTimeout(function() { try { diff --git a/src/static/js/domline.js b/src/static/js/domline.js index 1be0f4eee..43b5f21a3 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -30,8 +30,7 @@ var Security = require('./security'); var hooks = require('./pluginfw/hooks'); var _ = require('./underscore'); var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; -var Ace2Common = require('./ace2_common'); -var noop = Ace2Common.noop; +var noop = function(){}; var domline = {}; diff --git a/src/static/js/html10n.js b/src/static/js/html10n.js index d0d14814b..e1c025c43 100644 --- a/src/static/js/html10n.js +++ b/src/static/js/html10n.js @@ -23,27 +23,27 @@ window.html10n = (function(window, document, undefined) { // fix console - var console = window.console + var console = window.console; function interceptConsole(method){ - if (!console) return function() {} + if (!console) return function() {}; - var original = console[method] + var original = console[method]; // do sneaky stuff if (original.bind){ // Do this for normal browsers - return original.bind(console) + return original.bind(console); }else{ return function() { // Do this for IE - var message = Array.prototype.slice.apply(arguments).join(' ') - original(message) + var message = Array.prototype.slice.apply(arguments).join(' '); + original(message); } } } var consoleLog = interceptConsole('log') , consoleWarn = interceptConsole('warn') - , consoleError = interceptConsole('warn') + , consoleError = interceptConsole('warn'); // fix Array.prototype.instanceOf in, guess what, IE! <3 @@ -84,14 +84,14 @@ window.html10n = (function(window, document, undefined) { * MicroEvent - to make any js object an event emitter (server or browser) */ - var MicroEvent = function(){} + var MicroEvent = function(){} MicroEvent.prototype = { - bind : function(event, fct){ + bind : function(event, fct){ this._events = this._events || {}; this._events[event] = this._events[event] || []; this._events[event].push(fct); }, - unbind : function(event, fct){ + unbind : function(event, fct){ this._events = this._events || {}; if( event in this._events === false ) return; this._events[event].splice(this._events[event].indexOf(fct), 1); @@ -100,7 +100,7 @@ window.html10n = (function(window, document, undefined) { this._events = this._events || {}; if( event in this._events === false ) return; for(var i = 0; i < this._events[event].length; i++){ - this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)) + this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); } } }; @@ -122,50 +122,50 @@ window.html10n = (function(window, document, undefined) { * and caching all necessary resources */ function Loader(resources) { - this.resources = resources - this.cache = {} // file => contents - this.langs = {} // lang => strings + this.resources = resources; + this.cache = {}; // file => contents + this.langs = {}; // lang => strings } Loader.prototype.load = function(lang, cb) { - if(this.langs[lang]) return cb() + if(this.langs[lang]) return cb(); if (this.resources.length > 0) { var reqs = 0; for (var i=0, n=this.resources.length; i < n; i++) { this.fetch(this.resources[i], lang, function(e) { reqs++; - if(e) return setTimeout(function(){ throw e }, 0) + if(e) return setTimeout(function(){ throw e }, 0); if (reqs < n) return;// Call back once all reqs are completed - cb && cb() + cb && cb(); }) } } } Loader.prototype.fetch = function(href, lang, cb) { - var that = this + var that = this; if (this.cache[href]) { this.parse(lang, href, this.cache[href], cb) return; } - var xhr = new XMLHttpRequest() - xhr.open('GET', href, /*async: */true) + var xhr = new XMLHttpRequest(); + xhr.open('GET', href, /*async: */true); if (xhr.overrideMimeType) { xhr.overrideMimeType('application/json; charset=utf-8'); } xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status === 0) { - var data = JSON.parse(xhr.responseText) - that.cache[href] = data + var data = JSON.parse(xhr.responseText); + that.cache[href] = data; // Pass on the contents for parsing - that.parse(lang, href, data, cb) + that.parse(lang, href, data, cb); } else { - cb(new Error('Failed to load '+href)) + cb(new Error('Failed to load '+href)); } } }; @@ -174,39 +174,39 @@ window.html10n = (function(window, document, undefined) { Loader.prototype.parse = function(lang, currHref, data, cb) { if ('object' != typeof data) { - cb(new Error('A file couldn\'t be parsed as json.')) - return + cb(new Error('A file couldn\'t be parsed as json.')); + return; } - if (!data[lang]) lang = lang.substr(0, lang.indexOf('-') == -1? lang.length : lang.indexOf('-')) + if (!data[lang]) lang = lang.substr(0, lang.indexOf('-') == -1? lang.length : lang.indexOf('-')); if (!data[lang]) { - cb(new Error('Couldn\'t find translations for '+lang)) - return + cb(new Error('Couldn\'t find translations for '+lang)); + return; } if ('string' == typeof data[lang]) { // Import rule // absolute path - var importUrl = data[lang] + var importUrl = data[lang]; // relative path if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) { - importUrl = currHref+"/../"+data[lang] + importUrl = currHref+"/../"+data[lang]; } - this.fetch(importUrl, lang, cb) - return + this.fetch(importUrl, lang, cb); + return; } if ('object' != typeof data[lang]) { - cb(new Error('Translations should be specified as JSON objects!')) - return + cb(new Error('Translations should be specified as JSON objects!')); + return; } - this.langs[lang] = data[lang] + this.langs[lang] = data[lang]; // TODO: Also store accompanying langs - cb() + cb(); } @@ -216,11 +216,11 @@ window.html10n = (function(window, document, undefined) { var html10n = { language : null } - MicroEvent.mixin(html10n) + MicroEvent.mixin(html10n); - html10n.macros = {} + html10n.macros = {}; - html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"] + html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]; /** * Get rules for plural forms (shared with JetPack), see: @@ -664,14 +664,14 @@ window.html10n = (function(window, document, undefined) { * @param langs An array of lang codes defining fallbacks */ html10n.localize = function(langs) { - var that = this + var that = this; // if only one string => create an array - if ('string' == typeof langs) langs = [langs] + if ('string' == typeof langs) langs = [langs]; this.build(langs, function(er, translations) { - html10n.translations = translations - html10n.translateElement(translations) - that.trigger('localized') + html10n.translations = translations; + html10n.translateElement(translations); + that.trigger('localized'); }) } @@ -682,78 +682,78 @@ window.html10n = (function(window, document, undefined) { * @param element A DOM element, if omitted, the document element will be used */ html10n.translateElement = function(translations, element) { - element = element || document.documentElement + element = element || document.documentElement; var children = element? getTranslatableChildren(element) : document.childNodes; for (var i=0, n=children.length; i < n; i++) { - this.translateNode(translations, children[i]) + this.translateNode(translations, children[i]); } // translate element itself if necessary - this.translateNode(translations, element) + this.translateNode(translations, element); } function asyncForEach(list, iterator, cb) { var i = 0 - , n = list.length + , n = list.length; iterator(list[i], i, function each(err) { - if(err) consoleLog(err) - i++ + if(err) consoleLog(err); + i++; if (i < n) return iterator(list[i],i, each); - cb() + cb(); }) } function getTranslatableChildren(element) { if(!document.querySelectorAll) { - if (!element) return [] + if (!element) return []; var nodes = element.getElementsByTagName('*') - , l10nElements = [] + , l10nElements = []; for (var i=0, n=nodes.length; i < n; i++) { if (nodes[i].getAttribute('data-l10n-id')) l10nElements.push(nodes[i]); } - return l10nElements + return l10nElements; } - return element.querySelectorAll('*[data-l10n-id]') + return element.querySelectorAll('*[data-l10n-id]'); } html10n.get = function(id, args) { - var translations = html10n.translations - if(!translations) return consoleWarn('No translations available (yet)') - if(!translations[id]) return consoleWarn('Could not find string '+id) + var translations = html10n.translations; + if(!translations) return consoleWarn('No translations available (yet)'); + if(!translations[id]) return consoleWarn('Could not find string '+id); // apply args - var str = substArguments(translations[id], args) + var str = substArguments(translations[id], args); // apply macros - return substMacros(id, str, args) + return substMacros(id, str, args); // replace {{arguments}} with their values or the // associated translation string (based on its key) function substArguments(str, args) { var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/ - , match + , match; while (match = reArgs.exec(str)) { if (!match || match.length < 2) - return str // argument key not found + return str; // argument key not found var arg = match[1] - , sub = '' + , sub = ''; if (arg in args) { - sub = args[arg] + sub = args[arg]; } else if (arg in translations) { - sub = translations[arg] + sub = translations[arg]; } else { - consoleWarn('Could not find argument {{' + arg + '}}') - return str + consoleWarn('Could not find argument {{' + arg + '}}'); + return str; } - str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length) + str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length); } - return str + return str; } // replace {[macros]} with their values @@ -766,21 +766,21 @@ window.html10n = (function(window, document, undefined) { // a macro has been found // Note: at the moment, only one parameter is supported var macroName = reMatch[1] - , paramName = reMatch[2] + , paramName = reMatch[2]; - if (!(macroName in gMacros)) return str + if (!(macroName in gMacros)) return str; - var param + var param; if (args && paramName in args) { - param = args[paramName] + param = args[paramName]; } else if (paramName in translations) { - param = translations[paramName] + param = translations[paramName]; } // there's no macro parser yet: it has to be defined in gMacros - var macro = html10n.macros[macroName] - str = macro(translations, key, str, param) - return str + var macro = html10n.macros[macroName]; + str = macro(translations, key, str, param); + return str; } } @@ -788,26 +788,26 @@ window.html10n = (function(window, document, undefined) { * Applies translations to a DOM node (recursive) */ html10n.translateNode = function(translations, node) { - var str = {} + var str = {}; // get id - str.id = node.getAttribute('data-l10n-id') - if (!str.id) return + str.id = node.getAttribute('data-l10n-id'); + if (!str.id) return; - if(!translations[str.id]) return consoleWarn('Couldn\'t find translation key '+str.id) + if(!translations[str.id]) return consoleWarn('Couldn\'t find translation key '+str.id); // get args if(window.JSON) { - str.args = JSON.parse(node.getAttribute('data-l10n-args')) + str.args = JSON.parse(node.getAttribute('data-l10n-args')); }else{ try{ - str.args = eval(node.getAttribute('data-l10n-args')) + str.args = eval(node.getAttribute('data-l10n-args')); }catch(e) { - consoleWarn('Couldn\'t parse args for '+str.id) + consoleWarn('Couldn\'t parse args for '+str.id); } } - str.str = html10n.get(str.id, str.args) + str.str = html10n.get(str.id, str.args); // get attribute name to apply str to var prop @@ -817,31 +817,31 @@ window.html10n = (function(window, document, undefined) { , "innerHTML": 1 , "alt": 1 , "textContent": 1 - } + }; if (index > 0 && str.id.substr(index + 1) in attrList) { // an attribute has been specified - prop = str.id.substr(index + 1) + prop = str.id.substr(index + 1); } else { // no attribute: assuming text content by default - prop = document.body.textContent ? 'textContent' : 'innerText' + prop = document.body.textContent ? 'textContent' : 'innerText'; } // Apply translation if (node.children.length === 0 || prop != 'textContent') { - node[prop] = str.str + node[prop] = str.str; } else { var children = node.childNodes, - found = false + found = false; for (var i=0, n=children.length; i < n; i++) { if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) { if (!found) { - children[i].nodeValue = str.str - found = true + children[i].nodeValue = str.str; + found = true; } else { - children[i].nodeValue = '' + children[i].nodeValue = ''; } } } if (!found) { - consoleWarn('Unexpected error: could not translate element content for key '+str.id, node) + consoleWarn('Unexpected error: could not translate element content for key '+str.id, node); } } } @@ -852,32 +852,32 @@ window.html10n = (function(window, document, undefined) { */ html10n.build = function(langs, cb) { var that = this - , build = {} + , build = {}; asyncForEach(langs, function (lang, i, next) { if(!lang) return next(); - that.loader.load(lang, next) + that.loader.load(lang, next); }, function() { - var lang - langs.reverse() + var lang; + langs.reverse(); // loop through priority array... for (var i=0, n=langs.length; i < n; i++) { - lang = langs[i] + lang = langs[i]; if(!lang || !(lang in that.loader.langs)) continue; // ... and apply all strings of the current lang in the list // to our build object for (var string in that.loader.langs[lang]) { - build[string] = that.loader.langs[lang][string] + build[string] = that.loader.langs[lang][string]; } // the last applied lang will be exposed as the // lang the page was translated to - that.language = lang + that.language = lang; } - cb(null, build) + cb(null, build); }) } @@ -893,8 +893,8 @@ window.html10n = (function(window, document, undefined) { * Returns the direction of the language returned be html10n#getLanguage */ html10n.getDirection = function() { - var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-')) - return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl' + var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-')); + return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl'; } /** @@ -903,28 +903,28 @@ window.html10n = (function(window, document, undefined) { html10n.index = function () { // Find all s var links = document.getElementsByTagName('link') - , resources = [] + , resources = []; for (var i=0, n=links.length; i < n; i++) { if (links[i].type != 'application/l10n+json') continue; - resources.push(links[i].href) + resources.push(links[i].href); } - this.loader = new Loader(resources) - this.trigger('indexed') + this.loader = new Loader(resources); + this.trigger('indexed'); } if (document.addEventListener) // modern browsers and IE9+ document.addEventListener('DOMContentLoaded', function() { - html10n.index() - }, false) + html10n.index(); + }, false); else if (window.attachEvent) window.attachEvent('onload', function() { - html10n.index() - }, false) + html10n.index(); + }, false); // gettext-like shortcut if (window._ === undefined) window._ = html10n.get; - return html10n -})(window, document) \ No newline at end of file + return html10n; +})(window, document); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 27dd3b737..a99e3e148 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -275,6 +275,11 @@ function handshake() $('#passwordRequired').show(); $("#passwordinput").focus(); } + else if(obj.accessStatus == "denyTeamPad") + { + $("#editorloadingbox").html("This is a Team pad
" + + "Manage teampads"); + } } //if we haven't recieved the clientVars yet, then this message should it be diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index eb10f8afd..15d879409 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -94,11 +94,12 @@ exports.search = function(query, cache, cb) { 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(query.pattern) != -1 + && key.indexOf(pattern) != -1 || key.indexOf(plugins.prefix) == 0 - && data[key].description.indexOf(query.pattern) != -1 + && 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 diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index 16ea84279..0e5c16edd 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -9,12 +9,15 @@
-
diff --git a/src/templates/admin/plugins-info.html b/src/templates/admin/plugins-info.html index 605b23d38..648e7d6d0 100644 --- a/src/templates/admin/plugins-info.html +++ b/src/templates/admin/plugins-info.html @@ -12,9 +12,13 @@
diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index a85db557a..bfe535e7e 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -19,11 +19,14 @@ <% } %> diff --git a/src/templates/admin/settings.html b/src/templates/admin/settings.html index be262f243..0fb1c9ae1 100644 --- a/src/templates/admin/settings.html +++ b/src/templates/admin/settings.html @@ -24,9 +24,13 @@
diff --git a/src/templates/index.html b/src/templates/index.html index c3c13db32..880fc3fc8 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -63,9 +63,24 @@ background: -o-linear-gradient(#fff,#ccc); box-shadow: 0px 1px 8px rgba(0,0,0,0.3); } - #inner { - width: 300px; - margin: 0 auto; + #inner, + #footer { + margin:0 auto; + width: 300px; + } + #footer { + margin-top: 160px; + padding: 15px; + text-align: center; + } + #teampads { + color: inherit; + font-size: 24px; + text-decoration: none; + letter-spacing: -2px; + } + #teampads::after { + content: ' »'; } #button { margin: 0 auto; @@ -146,6 +161,9 @@ text-align: center; } } + #teampads { + font: 32px verdana,arial,sans-serif; + } @@ -162,6 +180,10 @@ <% e.end_block(); %>
+ + + + + +
+

Etherpad Lite

+ <% if (signedIn) { %> +
+ +

Teams you are a member of

+ <% if (teamsInfo.length == 0) { %> + None yet! + <% } else { %> + + + + + + + + + <% for (var i=0; i < teamsInfo.length; i++) { %> + + + + + <% } %> + +
Team name
+ <%= teamsInfo[i].name %> + + <% if (teamsInfo[i].admins.indexOf(currentUser) != undefined) { %> + [admin] + <% } %> +
+ <% } %> +
+
+

Create new team:

+ + + + + <% } else { %> + + Sign in to create, manage and see your team pads → + +
+ +
+ + + + + <% } %> +

+ diff --git a/src/templates/teampad/team.html b/src/templates/teampad/team.html new file mode 100644 index 000000000..7881ba52a --- /dev/null +++ b/src/templates/teampad/team.html @@ -0,0 +1,69 @@ + + + + <%= teamInfo.name %> + + +
+

Etherpad Lite

+
+

Pads belonging to this team:

+ <% if (teamInfo.pads.length == 0) { %> + None yet! + <% } else { %> + + + + + + + + + <% for (var i=0; i < teamInfo.pads.length; i++) { %> + + + + + <% } %> + +
Pad name
+ <% var teamName = teamInfo.pads[i].split('+')[1]; %> + <%= teamName %> +
+ <% } %> +
+

Create pad:

+ + + + +
+
+

Authors belonging to this team:

+ + + + + + + + + <% for (var i=0; i < teamInfo.accounts.length; i++) { %> + + + + + <% } %> + +
Account name
+ <%= teamInfo.accounts[i] %> +
+
+

Add account to team:

+ + + + +
+
+ diff --git a/stackato.yml b/stackato.yml new file mode 100644 index 000000000..fa11020b6 --- /dev/null +++ b/stackato.yml @@ -0,0 +1,5 @@ +name: etherpad-lite +instances: 1 +framework: + type: node +mem: 128