/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc., 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.
*/
/* global $, window */
var socket;
// These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins.
require('/jquery');
require('/farbtastic');
require('/excanvas');
JSON = require('/json2');
require('/undo-xpopup');
require('/prefixfree');
var chat = require('/chat').chat,
getCollabClient = require('/collab_client').getCollabClient,
padconnectionstatus = require('/pad_connectionstatus').padconnectionstatus,
padcookie = require('/pad_cookie').padcookie,
paddocbar = require('/pad_docbar').paddocbar,
padeditbar = require('/pad_editbar').padeditbar,
padeditor = require('/pad_editor').padeditor,
padimpexp = require('/pad_impexp').padimpexp,
padmodals = require('/pad_modals').padmodals,
padsavedrevs = require('/pad_savedrevs').padsavedrevs,
paduserlist = require('/pad_userlist').paduserlist,
padutils = require('/pad_utils').padutils,
createCookie = require('/pad_utils').createCookie,
readCookie = require('/pad_utils').readCookie,
randomString = require('/pad_utils').randomString;
function getParams() {
var params = getUrlVars(),
showControls = params['showControls'],
showChat = params['showChat'],
userName = params['userName'],
showLineNumbers = params['showLineNumbers'],
useMonospaceFont = params['useMonospaceFont'],
IsnoColors = params['noColors'],
hideQRCode = params['hideQRCode'],
rtl = params['rtl'],
alwaysShowChat = params['alwaysShowChat'];
if (IsnoColors) {
if (IsnoColors == 'true') {
settings.noColors = true;
$('#buttonicon-clearauthorship').hide();
}
}
if (showControls) {
if (showControls == 'false') {
$('#editbar').hide();
$('#editorcontainer').css({top:0});
}
}
if (showChat) {
if (showChat == 'false') {
$('#chaticon').hide();
}
}
if (showLineNumbers) {
if (showLineNumbers == 'false')
settings.LineNumbersDisabled = true;
}
if (useMonospaceFont) {
if (useMonospaceFont == 'true')
settings.useMonospaceFontGlobal = true;
}
if (userName) {
// if the username is set as a parameter we should set a global value that we can call once we have initiated the pad
settings.globalUserName = unescape(userName);
}
if (hideQRCode) {
$('#qrcode').hide();
}
if (rtl) {
if (rtl == 'true')
settings.rtlIsTrue = true;
}
if (alwaysShowChat) {
if (alwaysShowChat == 'true')
chat.stickToScreen();
}
}
function getUrlVars() {
var vars = [], hash,
hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
for (var i=0, l=hashes.length; i < l; i++) {
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;
}
function savePassword() {
// set the password cookie
createCookie('password', $('#passwordinput').val(), null, document.location.pathname);
// reload
document.location = document.location;
}
function handshake() {
var loc = document.location,
port = loc.port == '' ? (loc.protocol == 'https:' ? 443 : 80) : loc.port, // get the correct port
url = loc.protocol + '//' + loc.hostname + ':' + port + '/', // create the url
resource = loc.pathname.substr(1, loc.pathname.indexOf('/p/')) + 'socket.io'; // determine current subfolder
// connect
socket = pad.socket = io.connect(url, {
resource: resource,
'max reconnection attempts': 3
});
function sendClientReady(isReconnect) {
var padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces
if (!isReconnect)
document.title = padId.replace(/_+/g, ' ') + " | " + document.title;
var token = readCookie('token');
if (token == null) {
token = 't.' + randomString();
createCookie('token', token, 60);
}
var sessionID = readCookie('sessionID'),
password = readCookie('password'),
msg = {
'component' : 'pad',
'type' : 'CLIENT_READY',
'padId' : padId,
'sessionID' : sessionID,
'password' : password,
'token' : token,
'protocolVersion' : 2
};
// this is a reconnect, lets tell the server our revision number
if (isReconnect == true) {
msg.client_rev=pad.collabClient.getCurrentRevisionNumber();
msg.reconnect=true;
}
socket.json.send(msg);
};
var disconnectTimeout;
socket.once('connect', function() {
sendClientReady(false);
});
socket.on('reconnect', function() {
// reconnect is before timeout, let´s stop the timeout
if (disconnectTimeout)
clearTimeout(disconnectTimeout);
pad.collabClient.setChannelState('CONNECTED');
sendClientReady(true);
});
socket.on('disconnect', function() {
function disconnectEvent() {
pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout');
}
pad.collabClient.setChannelState('RECONNECTING');
disconnectTimeout = setTimeout(disconnectEvent, 10000);
});
var receivedClientVars = false,
initalized = false;
socket.on('message', function(obj) {
// access was not granted, give the user a message
if (!receivedClientVars && obj.accessStatus) {
if (obj.accessStatus == 'deny')
$('#editorloadingbox').html("You do not have permission to access this pad");
else if (obj.accessStatus == "needPassword") {
$('#editorloadingbox').html("You need a password to access this pad
" +
""+
"");
} else if (obj.accessStatus == 'wrongPassword') {
$("#editorloadingbox").html("You're password was wrong
" +
""+
"");
}
} else if (!receivedClientVars) { // if we haven't received the clientVars yet, then this message should it be
// log the message
if (window.console)
console.log(obj);
receivedClientVars = true;
// set some client vars
clientVars = obj;
clientVars.userAgent = 'Anonymous';
clientVars.collab_client_vars.clientAgent = 'Anonymous';
// initalize the pad
pad._afterHandshake();
initalized = true;
// if LineNumbersDisabled is set to true, we need to hide the line numbers
if (settings.LineNumbersDisabled == true)
pad.changeViewOption('showLineNumbers', false);
// if noColors is set to true, we need to hide the backround colors on the ace spans
if (settings.noColors == true)
pad.changeViewOption('noColors', true);
if (settings.rtlIsTrue == true)
pad.changeViewOption('rtl', true);
// if Monospacefont is set to true, change it to monospace
if (settings.useMonospaceFontGlobal == true)
pad.changeViewOption('useMonospaceFont', true);
// if globalUserName is set, tell the server and the client about the new authorname
if (settings.globalUserName !== false) {
pad.notifyChangeName(settings.globalUserName); // notify the server
pad.myUserInfo.name = settings.globalUserName;
$('#myusernameedit').attr({"value":settings.globalUserName}); // update the current user´s UI
}
}
// handle every message after the clientVars
else {
// advice the client to disconnect
if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect();
return;
} else {
pad.collabClient.handleMessageFromServer(obj);
}
}
});
// bind the colorpicker
var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220});
}
var pad = {
// don't access these directly from outside this file, except for debugging
collabClient : null,
myUserInfo : null,
diagnosticInfo : {},
initTime : 0,
clientTimeOffset : null,
preloadedImages : false,
padOptions : {},
// these don't require init; clientVars should all go through here
getPadId : function() {
return clientVars.padId;
},
getClientIp : function() {
return clientVars.clientIp;
},
getIsProPad : function() {
return clientVars.isProPad;
},
getColorPalette : function() {
return clientVars.colorPalette;
},
getDisplayUserAgent: function() {
return padutils.uaDisplay(clientVars.userAgent);
},
getIsDebugEnabled : function() {
return clientVars.debugEnabled;
},
getPrivilege : function(name) {
return clientVars.accountPrivs[name];
},
getUserIsGuest : function() {
return clientVars.userIsGuest;
},
getUserId : function() {
return pad.myUserInfo.userId;
},
getUserName : function() {
return pad.myUserInfo.name;
},
sendClientMessage : function(msg) {
pad.collabClient.sendClientMessage(msg);
},
init : function() {
padutils.setupGlobalExceptionHandler();
$(document).ready(function() {
// start the custom js
if (typeof customStart == 'function')
customStart();
getParams();
handshake();
});
$(window).unload(function() {
pad.dispose();
});
},
_afterHandshake : function() {
pad.clientTimeOffset = new Date().getTime() - clientVars.serverTimestamp;
// initialize the chat
chat.init(this);
pad.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions;
if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0)))
document.domain = document.domain; // for comet
// for IE
if ($.browser.msie) {
try {
doc.execCommand('BackgroundImageCache', false, true);
}
catch (e)
{}
}
// order of inits is important here:
padcookie.init(clientVars.cookiePrefsToSet, this);
$('#widthprefcheck').click(pad.toggleWidthPref);
pad.myUserInfo = {
userId : clientVars.userId,
name : clientVars.userName,
ip : pad.getClientIp(),
colorId : clientVars.userColor,
userAgent : pad.getDisplayUserAgent()
};
if (clientVars.specialKey) {
pad.myUserInfo.specialKey = clientVars.specialKey;
if (clientVars.specialKeyTranslation)
$('#specialkeyarea').html('mode: ' + String(clientVars.specialKeyTranslation).toUpperCase());
}
paddocbar.init({
isTitleEditable : pad.getIsProPad(),
initialTitle : clientVars.initialTitle,
initialPassword : clientVars.initialPassword,
guestPolicy : pad.padOptions.guestPolicy
}, this);
padimpexp.init(this);
padsavedrevs.init(clientVars.initialRevisionList, this);
padeditor.init(postAceInit, pad.padOptions.view || {}, this);
paduserlist.init(pad.myUserInfo, this);
// padchat.init(clientVars.chatHistory, pad.myUserInfo);
padconnectionstatus.init();
padmodals.init(this);
pad.collabClient = getCollabClient(padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, {
colorPalette: pad.getColorPalette()
}, pad);
pad.collabClient.setOnUserJoin(pad.handleUserJoin);
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
pad.collabClient.setOnUserLeave(pad.handleUserLeave);
pad.collabClient.setOnClientMessage(pad.handleClientMessage);
pad.collabClient.setOnServerMessage(pad.handleServerMessage);
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
pad.collabClient.setOnInternalAction(pad.handleCollabAction);
function postAceInit() {
padeditbar.init();
setTimeout(function() {padeditor.ace.focus();}, 0);
}
},
dispose: function() {
padeditor.dispose();
},
notifyChangeName: function(newName) {
pad.myUserInfo.name = newName;
pad.collabClient.updateUserInfo(pad.myUserInfo);
//padchat.handleUserJoinOrUpdate(pad.myUserInfo);
},
notifyChangeColor: function(newColorId) {
pad.myUserInfo.colorId = newColorId;
pad.collabClient.updateUserInfo(pad.myUserInfo);
//padchat.handleUserJoinOrUpdate(pad.myUserInfo);
},
notifyChangeTitle: function(newTitle) {
pad.collabClient.sendClientMessage({
type : 'padtitle',
title : newTitle,
changedBy : pad.myUserInfo.name || 'unnamed'
});
},
notifyChangePassword: function(newPass) {
pad.collabClient.sendClientMessage({
type : 'padpassword',
password : newPass,
changedBy : pad.myUserInfo.name || 'unnamed'
});
},
changePadOption: function(key, value) {
var options = {};
options[key] = value;
pad.handleOptionsChange(options);
pad.collabClient.sendClientMessage({
type : 'padoptions',
options : options,
changedBy : pad.myUserInfo.name || 'unnamed'
});
},
changeViewOption: function(key, value) {
var options = {
view: {}
};
options.view[key] = value;
pad.handleOptionsChange(options);
},
handleOptionsChange: function(opts) {
// opts object is a full set of options or just some options to change
if (opts.view) {
if (!pad.padOptions.view)
pad.padOptions.view = {};
for (var k in opts.view) {
pad.padOptions.view[k] = opts.view[k];
}
padeditor.setViewOptions(pad.padOptions.view);
}
if (opts.guestPolicy) {
// order is important here:
pad.padOptions.guestPolicy = opts.guestPolicy;
paddocbar.setGuestPolicy(opts.guestPolicy);
}
},
getPadOptions: function() {
// caller shouldn't mutate the object
return pad.padOptions;
},
isPadPublic: function() {
return (!pad.getIsProPad()) || (pad.getPadOptions().guestPolicy == 'allow');
},
suggestUserName: function(userId, name) {
pad.collabClient.sendClientMessage({
type : 'suggestUserName',
unnamedId : userId,
newName : name
});
},
handleUserJoin: function(userInfo) {
paduserlist.userJoinOrUpdate(userInfo);
//padchat.handleUserJoinOrUpdate(userInfo);
},
handleUserUpdate: function(userInfo) {
paduserlist.userJoinOrUpdate(userInfo);
//padchat.handleUserJoinOrUpdate(userInfo);
},
handleUserLeave: function(userInfo) {
paduserlist.userLeave(userInfo);
//padchat.handleUserLeave(userInfo);
},
handleClientMessage: function(msg) {
switch (msg.type) {
case 'suggestUserName':
if (msg.unnamedId == pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) {
pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo);
}
break;
case 'chat':
// padchat.receiveChat(msg);
break;
case 'padtitle':
paddocbar.changeTitle(msg.title);
break;
case 'padpassword':
paddocbar.changePassword(msg.password);
break;
case 'newRevisionList':
padsavedrevs.newRevisionList(msg.revisionList);
break;
case 'revisionLabel':
padsavedrevs.newRevisionList(msg.revisionList);
break;
case 'padoptions':
var opts = msg.options;
pad.handleOptionsChange(opts);
break;
case 'guestanswer':
// someone answered a prompt, remove it
paduserlist.removeGuestPrompt(msg.guestId);
break;
}
},
editbarClick: function(cmd) {
if (padeditbar)
padeditbar.toolbarClick(cmd);
},
dmesg: function(m) {
if (pad.getIsDebugEnabled()) {
var djs = $('#djs').get(0),
wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height()) >= -20);
$('#djs').append('
' + m + '
'); if (wasAtBottom) djs.scrollTop = djs.scrollHeight; } }, handleServerMessage: function(m) { if (m.type == 'NOTICE') { if (m.text) { alertBar.displayMessage(function(abar) { abar.find('#servermsgdate').html(' (' + padutils.simpleDateTime(new Date) + ')'); abar.find('#servermsgtext').html(m.text); }); } if (m.js) window['ev' + 'al'](m.js); } else if (m.type == 'GUEST_PROMPT') { paduserlist.showGuestPrompt(m.userId, m.displayName); } }, handleChannelStateChange: function(newState, message) { var oldFullyConnected = !! padconnectionstatus.isFullyConnected(), wasConnecting = (padconnectionstatus.getStatus().what == 'connecting'); if (newState == 'CONNECTED') padconnectionstatus.connected(); else if (newState == 'RECONNECTING') padconnectionstatus.reconnecting(); else if (newState == 'DISCONNECTED') { pad.diagnosticInfo.disconnectedMessage = message; pad.diagnosticInfo.padId = pad.getPadId(); pad.diagnosticInfo.socket = {}; // we filter non objects from the socket object and put them in the diagnosticInfo // this ensures we have no cyclic data - this allows us to stringify the data for (var i in socket.socket) { var value = socket.socket[i], type = typeof value; if (type == 'string' || type == 'number') pad.diagnosticInfo.socket[i] = value; } pad.asyncSendDiagnosticInfo(); if (typeof window.ajlog == 'string') window.ajlog += ('Disconnected: ' + message + '\n'); padeditor.disable(); padeditbar.disable(); paddocbar.disable(); padimpexp.disable(); padconnectionstatus.disconnected(message); } var newFullyConnected = !! padconnectionstatus.isFullyConnected(); if (newFullyConnected != oldFullyConnected) pad.handleIsFullyConnected(newFullyConnected, wasConnecting); }, handleIsFullyConnected: function(isConnected, isInitialConnect) { // load all images referenced from CSS, one at a time, // starting one second after connection is first established. if (isConnected && !pad.preloadedImages) { window.setTimeout(function() { if (!pad.preloadedImages) { pad.preloadImages(); pad.preloadedImages = true; } }, 1000); } padsavedrevs.handleIsFullyConnected(isConnected); // pad.determineSidebarVisibility(isConnected && !isInitialConnect); pad.determineChatVisibility(isConnected && !isInitialConnect); }, /* determineSidebarVisibility: function(asNowConnectedFeedback) { if (pad.isFullyConnected()) { var setSidebarVisibility = padutils.getCancellableAction("set-sidebar-visibility", function() { // $("body").toggleClass('hidesidebar', !! padcookie.getPref('hideSidebar')); }); window.setTimeout(setSidebarVisibility, asNowConnectedFeedback ? 3000 : 0); } else { padutils.cancelActions("set-sidebar-visibility"); $("body").removeClass('hidesidebar'); } }, */ determineChatVisibility: function(asNowConnectedFeedback) { var chatVisCookie = padcookie.getPref('chatAlwaysVisible'); if (chatVisCookie) { // if the cookie is set for chat always visible chat.stickToScreen(true); // stick chat to the screen $('#options-stickychat').prop('checked', true); // set the checkbox to on } else { $('#options-stickychat').prop('checked', false); // set the checkbox for off } }, handleCollabAction: function(action) { if (action == 'commitPerformed') padeditbar.setSyncStatus('syncing'); else if (action == 'newlyIdle') padeditbar.setSyncStatus('done'); }, hideServerMessage: function() { alertBar.hideMessage(); }, asyncSendDiagnosticInfo: function() { window.setTimeout(function() { $.post({ url : '/ep/pad/connection-diagnostic-info', data: {diagnosticInfo: JSON.stringify(pad.diagnosticInfo)} }); }, 0); }, forceReconnect: function() { $('#reconnectform .padId').val(pad.getPadId()); pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo(); $('#reconnectform .diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo)); $('#reconnectform .missedChanges').val(JSON.stringify(pad.collabClient.getMissedChanges())); $('#reconnectform').submit(); }, toggleWidthPref: function() { var newValue = !padcookie.getPref('fullWidth'); padcookie.setPref('fullWidth', newValue); $('#widthprefcheck').toggleClass('widthprefchecked', !! newValue).toggleClass('widthprefunchecked', !newValue); pad.handleWidthChange(); }, /* toggleSidebar: function() { var newValue = !padcookie.getPref('hideSidebar'); padcookie.setPref('hideSidebar', newValue); $("#sidebarcheck").toggleClass('sidebarchecked', !newValue).toggleClass('sidebarunchecked', !! newValue); pad.determineSidebarVisibility(); }, */ handleWidthChange: function() { var isFullWidth = padcookie.getPref('fullWidth'); if (isFullWidth) $('BODY').addClass('fullwidth').removeClass('limwidth squish1width squish2width'); else { $('BODY').addClass('limwidth').removeClass('fullwidth'); var pageWidth = $(window).width(); $('BODY').toggleClass('squish1width', (pageWidth < 912 && pageWidth > 812)).toggleClass('squish2width', (pageWidth <= 812)); } }, // this is called from code put into a frame from the server: handleImportExportFrameCall: function(callName, varargs) { padimpexp.handleFrameCall.call(padimpexp, callName, Array.prototype.slice.call(arguments, 1)); }, callWhenNotCommitting: function(f) { pad.collabClient.callWhenNotCommitting(f); }, getCollabRevisionNumber: function() { return pad.collabClient.getCurrentRevisionNumber(); }, isFullyConnected: function() { return padconnectionstatus.isFullyConnected(); }, addHistoricalAuthors: function(data) { if (!pad.collabClient) { window.setTimeout(function() { pad.addHistoricalAuthors(data); }, 1000); } else { pad.collabClient.addHistoricalAuthors(data); } }, preloadImages: function() { var images = ["../static/img/connectingbar.gif"]; function loadNextImage() { if (images.length == 0) return; var img = new Image(); img.src = images.shift(); if (img.complete) scheduleLoadNextImage(); else $(img).bind('error load onreadystatechange', scheduleLoadNextImage); } function scheduleLoadNextImage() { window.setTimeout(loadNextImage, 0); } scheduleLoadNextImage(); } }; var alertBar = (function() { var animator = padutils.makeShowHideAnimator(arriveAtAnimationState, false, 25, 400); function arriveAtAnimationState(state) { if (state == -1) $('#alertbar').css('opacity', 0).css('display', 'block'); else if (state == 0) $('#alertbar').css('opacity', 1); else if (state == 1) $('#alertbar').css('opacity', 0).css('display', 'none'); else if (state < 0) $('#alertbar').css('opacity', state + 1); else if (state > 0) $('#alertbar').css('opacity', 1 - state); } var self = { displayMessage: function(setupFunc) { animator.show(); setupFunc($('#alertbar')); }, hideMessage: function() { animator.hide(); } }; return self; }()); function init() { return pad.init(); } var settings = { LineNumbersDisabled : false, noColors : false, useMonospaceFontGlobal: false, globalUserName : false, hideQRCode : false, rtlIsTrue : false }; pad.settings = settings; exports.settings = settings; exports.createCookie = createCookie; exports.readCookie = readCookie; exports.randomString = randomString; exports.getParams = getParams; exports.getUrlVars = getUrlVars; exports.savePassword = savePassword; exports.handshake = handshake; exports.pad = pad; exports.init = init; exports.alertBar = alertBar;