/** * 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;