diff --git a/customize.dist/about.html b/customize.dist/about.html index f3e87dd25..1cabfea02 100644 --- a/customize.dist/about.html +++ b/customize.dist/about.html @@ -8,7 +8,6 @@ - diff --git a/customize.dist/ckeditor-config.js b/customize.dist/ckeditor-config.js index 38e1165e6..7eb44b6d3 100644 --- a/customize.dist/ckeditor-config.js +++ b/customize.dist/ckeditor-config.js @@ -1,4 +1,5 @@ -CKEDITOR.editorConfig = function( config ) { // jshint ignore:line +/* global CKEDITOR */ +CKEDITOR.editorConfig = function( config ) { var fixThings = false; // https://dev.ckeditor.com/ticket/10907 config.needsBrFiller= fixThings; @@ -12,9 +13,29 @@ CKEDITOR.editorConfig = function( config ) { // jshint ignore:line config.toolbarGroups= [{"name":"clipboard","groups":["clipboard","undo"]},{"name":"editing","groups":["find","selection"]},{"name":"links"},{"name":"insert"},{"name":"forms"},{"name":"tools"},{"name":"document","groups":["mode","document","doctools"]},{"name":"others"},{"name":"basicstyles","groups":["basicstyles","cleanup"]},{"name":"paragraph","groups":["list","indent","blocks","align","bidi"]},{"name":"styles"},{"name":"colors"}]; config.font_defaultLabel = 'Arial'; - config.fontSize_defaultLabel = '16px'; + config.fontSize_defaultLabel = '16'; config.contentsCss = '/customize/ckeditor-contents.css'; + config.keystrokes = [ + [ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ], + [ CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ], + + [ CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' ], + + [ CKEDITOR.CTRL + 90 /*Z*/, 'undo' ], + [ CKEDITOR.CTRL + 89 /*Y*/, 'redo' ], + [ CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/, 'redo' ], + + [ CKEDITOR.CTRL + CKEDITOR.SHIFT + 76 /*L*/, 'link' ], + [ CKEDITOR.CTRL + 76 /*L*/, undefined ], + + [ CKEDITOR.CTRL + 66 /*B*/, 'bold' ], + [ CKEDITOR.CTRL + 73 /*I*/, 'italic' ], + [ CKEDITOR.CTRL + 85 /*U*/, 'underline' ], + + [ CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ] + ]; + //skin: 'moono-cryptpad,/pad/themes/moono-cryptpad/' //skin: 'flat,/pad/themes/flat/' //skin: 'moono-lisa,/pad/themes/moono-lisa/' diff --git a/customize.dist/contact.html b/customize.dist/contact.html index fc98c6eba..b653529c4 100644 --- a/customize.dist/contact.html +++ b/customize.dist/contact.html @@ -8,7 +8,6 @@ - diff --git a/customize.dist/index.html b/customize.dist/index.html index 02a10078d..e05858e37 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -8,7 +8,6 @@ - diff --git a/customize.dist/main.css b/customize.dist/main.css index 3f2f1e6bd..026649207 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -1074,59 +1074,6 @@ html.cp, .cp .panel { background-color: #cccccc; } -.cp table { - border-collapse: collapse; - border-spacing: 0; - margin: 20px; -} -.cp tbody { - border: 1px solid #555; -} -.cp tbody tr { - text-align: center; -} -.cp tbody tr:first-of-type th { - font-size: 20px; - border-top: 0px; - font-weight: bold; - padding: 10px; - text-decoration: underline; -} -.cp tbody tr:first-of-type th.table-refresh { - color: #46E981; - text-decoration: none; - cursor: pointer; -} -.cp tbody tr:nth-child(odd) { - background-color: #ffffff; -} -.cp tbody tr th:first-of-type { - border-left: 0px; -} -.cp tbody tr th { - box-sizing: border-box; - border: 1px solid #555; -} -.cp tbody tr th, -.cp tbody tr td { - color: #555; -} -.cp tbody tr th.remove, -.cp tbody tr td.remove { - cursor: pointer; -} -.cp tbody tr th:last-child { - border-right: 0px; -} -.cp tbody td { - border-right: 1px solid #555; - padding: 12px; - padding-top: 0px; - padding-bottom: 0px; -} -.cp tbody td:last-child { - border-right: none; -} .cp .bottom-left { border-bottom-left-radius: 5px; } @@ -1137,212 +1084,6 @@ html.cp, color: #FA5858; cursor: pointer !important; } -.cp form.realtime, -.cp div.realtime { - padding: 0px; - margin: 0px; -} -.cp form.realtime > textarea, -.cp div.realtime > textarea { - width: 50%; - height: 15vh; -} -.cp form.realtime table, -.cp div.realtime table { - border-collapse: collapse; - width: calc(100% - 1px); -} -.cp form.realtime table tr td:first-child, -.cp div.realtime table tr td:first-child { - position: absolute; - left: 29px; - top: auto; - width: calc(30% - 50px); -} -.cp form.realtime table tr td, -.cp div.realtime table tr td { - padding: 0px; - margin: 0px; -} -.cp form.realtime table tr td div.text-cell, -.cp div.realtime table tr td div.text-cell { - padding: 0px; - margin: 0px; - height: 100%; -} -.cp form.realtime table tr td div.text-cell input, -.cp div.realtime table tr td div.text-cell input { - width: 80%; - width: 90%; - height: 100%; - border: 0px; -} -.cp form.realtime table tr td div.text-cell input[disabled], -.cp div.realtime table tr td div.text-cell input[disabled] { - background-color: transparent; - color: #000; - font-weight: bold; -} -.cp form.realtime table tr td.checkbox-cell, -.cp div.realtime table tr td.checkbox-cell { - margin: 0px; - padding: 0px; - height: 100%; - min-width: 150px; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain { - display: inline-block; - height: 100%; - width: 100%; - position: relative; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain label, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain label { - background-color: transparent; - display: block; - position: absolute; - top: 0px; - left: 0px; - height: 100%; - width: 100%; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable), -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) { - display: none; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover { - font-weight: bold; - background-color: #FA5858; - color: #000; - display: block; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { - height: 100%; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { - content: "✖"; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes { - background-color: #46E981; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after { - content: "✔"; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted { - background: #ddd; -} -.cp form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine, -.cp div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine { - display: none; -} -.cp form.realtime table input[type="text"], -.cp div.realtime table input[type="text"] { - height: auto; - border: 1px solid #fff; - width: 80%; -} -.cp form.realtime table thead td, -.cp div.realtime table thead td { - padding: 0px 5px; - background: #aaa; - border-radius: 20px 20px 0 0; - text-align: center; -} -.cp form.realtime table thead td input[type="text"], -.cp div.realtime table thead td input[type="text"] { - width: 100%; - box-sizing: border-box; -} -.cp form.realtime table thead td input[type="text"][disabled], -.cp div.realtime table thead td input[type="text"][disabled] { - color: #000; - padding: 1px 5px; - border: none; -} -.cp form.realtime table tbody .text-cell, -.cp div.realtime table tbody .text-cell { - background: #aaa; -} -.cp form.realtime table tbody .text-cell input[type="text"], -.cp div.realtime table tbody .text-cell input[type="text"] { - width: calc(100% - 50px); -} -.cp form.realtime table tbody .text-cell .edit, -.cp div.realtime table tbody .text-cell .edit { - float: right; - margin: 0 10px 0 0; -} -.cp form.realtime table tbody .text-cell .remove, -.cp div.realtime table tbody .text-cell .remove { - float: left; - margin: 0 0 0 10px; -} -.cp form.realtime table tbody td label, -.cp div.realtime table tbody td label { - border: 0.5px solid #555; -} -.cp form.realtime table .edit, -.cp div.realtime table .edit { - color: #000; - cursor: pointer; - float: left; - margin-left: 10px; -} -.cp form.realtime table .remove, -.cp div.realtime table .remove { - float: right; - margin-right: 10px; -} -.cp form.realtime table thead tr th input[type="text"][disabled], -.cp div.realtime table thead tr th input[type="text"][disabled] { - background-color: transparent; - color: #555; - font-weight: bold; -} -.cp form.realtime table thead tr th .remove, -.cp div.realtime table thead tr th .remove { - cursor: pointer; - font-size: 20px; -} -.cp form.realtime table tfoot tr, -.cp div.realtime table tfoot tr { - border: none; -} -.cp form.realtime table tfoot tr td, -.cp div.realtime table tfoot tr td { - border: none; - text-align: center; -} -.cp form.realtime table tfoot tr td .save, -.cp div.realtime table tfoot tr td .save { - padding: 15px; - border-top-left-radius: 5px; - border-top-right-radius: 5px; -} -.cp form.realtime #adduser, -.cp div.realtime #adduser, -.cp form.realtime #addoption, -.cp div.realtime #addoption { - color: #46E981; - border: 1px solid #46E981; - padding: 15px; - cursor: pointer; -} -.cp form.realtime #adduser, -.cp div.realtime #adduser { - border-top-left-radius: 5px; -} -.cp form.realtime #addoption, -.cp div.realtime #addoption { - border-bottom-left-radius: 5px; -} #cors-store { display: none; } diff --git a/customize.dist/main.js b/customize.dist/main.js index f4720afca..a02433b05 100644 --- a/customize.dist/main.js +++ b/customize.dist/main.js @@ -1,9 +1,8 @@ define([ + 'jquery', '/customize/application_config.js', - '/common/cryptpad-common.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Cryptpad) { - var $ = window.$; + '/common/cryptpad-common.js' +], function ($, Config, Cryptpad) { var APP = window.APP = { Cryptpad: Cryptpad, @@ -191,4 +190,3 @@ define([ console.log("ready"); }); }); - diff --git a/customize.dist/messages.js b/customize.dist/messages.js index ba35f1d96..18d329d6d 100644 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -23,12 +23,10 @@ var getLanguage = function () { }; var language = getLanguage(); -var req = ['/customize/translations/messages.js']; +var req = ['jquery', '/customize/translations/messages.js']; if (language && map[language]) { req.push('/customize/translations/messages.' + language + '.js'); } -req.push('/bower_components/jquery/dist/jquery.min.js'); -define(req, function(Default, Language) { - var $ = window.jQuery; +define(req, function($, Default, Language) { var externalMap = JSON.parse(JSON.stringify(map)); diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index dd1a7a686..203bb1008 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -8,7 +8,6 @@ - diff --git a/customize.dist/share/frame.js b/customize.dist/share/frame.js index 9f604af23..a07ff05ce 100644 --- a/customize.dist/share/frame.js +++ b/customize.dist/share/frame.js @@ -163,12 +163,8 @@ if (typeof(module) !== 'undefined' && module.exports) { module.exports = Frame; - } - else if ((typeof(define) !== 'undefined' && define !== null) && - (define.amd !== null)) { - define([ - '/bower_components/jquery/dist/jquery.min.js', - ], function () { + } else if (typeof(define) === 'function' && define.amd) { + define(['jquery'], function ($) { return Frame; }); } else { diff --git a/customize.dist/share/test.js b/customize.dist/share/test.js index c54d7ac79..efc9d81b4 100644 --- a/customize.dist/share/test.js +++ b/customize.dist/share/test.js @@ -1,8 +1,7 @@ define([ - '/customize/share/frame.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Frame) { - var $ = window.jQuery; + 'jquery', + '/customize/share/frame.js' +], function ($, Frame) { var domain = 'https://beta.cryptpad.fr'; @@ -123,4 +122,3 @@ define([ }].forEach(runTest); }); }); - diff --git a/customize.dist/src/less/cryptpad.less b/customize.dist/src/less/cryptpad.less index cbee383bf..db8af27ce 100644 --- a/customize.dist/src/less/cryptpad.less +++ b/customize.dist/src/less/cryptpad.less @@ -520,60 +520,6 @@ noscript { /* Tables * Currently only used by /poll/ */ -table { - border-collapse: collapse; - border-spacing: 0; - margin: 20px; -} -tbody { - border: 1px solid @poll-border-color; - tr { - text-align: center; - &:first-of-type th{ - font-size: 20px; - border-top: 0px; - font-weight: bold; - padding: 10px; - text-decoration: underline; - &.table-refresh { - color: @cp-green; - text-decoration: none; - cursor: pointer; - } - - } - &:nth-child(odd) { - background-color: @light-base; - } - th:first-of-type { - border-left: 0px; - } - th { - box-sizing: border-box; - border: 1px solid @poll-border-color; - } - th, td { - color: @fore; - - &.remove { - cursor: pointer; - } - } - th:last-child { - border-right: 0px; - } - } - - td { - border-right: 1px solid @poll-border-color; - padding: 12px; - padding-top: 0px; - padding-bottom: 0px; - &:last-child { - border-right: none; - } - } -} // form things .bottom-left { @@ -588,226 +534,6 @@ tbody { color: @cp-red; cursor: pointer !important; } - -form.realtime, div.realtime { - > input { - &[type="text"] { - - } - } - > textarea { - width: 50%; - height: 15vh; - } - - padding: 0px; - margin: 0px; - - table { - border-collapse: collapse; - width: ~"calc(100% - 1px)"; - tr { - td:first-child { - position:absolute; - left: 29px; - top: auto; - width: ~"calc(30% - 50px)"; - } - td { - padding: 0px; - margin: 0px; - - div.text-cell { - padding: 0px; - margin: 0px; - height: 100%; - - input { - width: 80%; - width: 90%; - height: 100%; - border: 0px; - &[disabled] { - background-color: transparent; - color: @poll-fg; - font-weight: bold; - } - } - } - - &.checkbox-cell { - margin: 0px; - padding: 0px; - height: 100%; - min-width: 150px; - - div.checkbox-contain { - display: inline-block; - height: 100%; - width: 100%; - position: relative; - - label { - background-color: transparent; - display: block; - position: absolute; - top: 0px; - left: 0px; - height: 100%; - width: 100%; - } - - input { - &[type="checkbox"] { - &:not(.editable) { - display: none; - - ~ .cover { - display: block; - font-weight: bold; - - background-color: @cp-red; - color: @poll-cover-color; - - &:after { - height: 100%; - } - - &:after { content: "✖"; } - - display: block; - &.yes { - background-color: @cp-green; - &:after { content: "✔"; } - } - - &.uncommitted { - background: #ddd; - } - - - &.mine { - display: none; - } - } - } - } - } - } - } - } - } - - input { - &[type="text"] { - height: auto; - border: 1px solid @base; - width: 80%; - } - } - thead { - td { - padding: 0px 5px; - background: @poll-th-bg; - border-radius: 20px 20px 0 0; - text-align: center; - input { - &[type="text"] { - width: 100%; - box-sizing: border-box; - &[disabled] { - color: @poll-fg; - padding: 1px 5px; - border: none; - } - } - } - } - } - - tbody { - .text-cell { - background: @poll-td-bg; - //border-radius: 20px 0 0 20px; - input[type="text"] { - width: ~"calc(100% - 50px)"; - } - .edit { - float:right; - margin: 0 10px 0 0; - } - .remove { - float: left; - margin: 0 0 0 10px; - } - } - td { - label { - border: .5px solid @poll-border-color; - } - } - } - .edit { - color: @poll-cover-color; - cursor: pointer; - float: left; - margin-left: 10px; - } - - .remove { - float: right; - margin-right: 10px; - } - - thead { - tr { - th { - input[type="text"][disabled] { - background-color: transparent; - color: @fore; - font-weight: bold; - } - .remove { - cursor: pointer; - font-size: 20px; - } - } - } - } - tbody { - tr { - td { - - } - } - } - tfoot { - tr { - border: none; - td { - border: none; - text-align: center; - .save { - padding: 15px; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - } - } - } - } - } - - #adduser, - #addoption { - color: @cp-green; - border: 1px solid @cp-green; - padding: 15px; - cursor: pointer; - } - - #adduser { .top-left; } - #addoption { .bottom-left; } -} } // hack for our cross-origin iframe diff --git a/customize.dist/src/less/mixins.less b/customize.dist/src/less/mixins.less index 456adf9b2..b65af90d8 100644 --- a/customize.dist/src/less/mixins.less +++ b/customize.dist/src/less/mixins.less @@ -1,5 +1,5 @@ .fontface(@family, @src, @style: normal, @weight: 400, @fmt: 'truetype'){ - @font-face{ + @font-face { font-family: @family; src: url(@src) format(@fmt); font-weight: @weight; diff --git a/customize.dist/src/less/variables.less b/customize.dist/src/less/variables.less index 20a1d80e1..8a0b4222e 100644 --- a/customize.dist/src/less/variables.less +++ b/customize.dist/src/less/variables.less @@ -60,12 +60,6 @@ @slide-default-bg: #000; -@poll-th-bg: #aaa; -@poll-td-bg: #aaa; -@poll-border-color: #555; -@poll-cover-color: #000; -@poll-fg: #000; - @bg-loading: @old-base; @color-loading: @old-fore; diff --git a/customize.dist/src/template.html b/customize.dist/src/template.html index 069e23511..4615cce7f 100644 --- a/customize.dist/src/template.html +++ b/customize.dist/src/template.html @@ -8,7 +8,6 @@ - {{script}} diff --git a/customize.dist/terms.html b/customize.dist/terms.html index 28f0b62ac..391020c07 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -8,7 +8,6 @@ - diff --git a/customize.dist/translations/messages.es.js b/customize.dist/translations/messages.es.js index cfe682c48..16800568f 100644 --- a/customize.dist/translations/messages.es.js +++ b/customize.dist/translations/messages.es.js @@ -290,10 +290,23 @@ define(function () { out.fm_categoryError = "No se pudo abrir la categoría seleccionada, mostrando la raíz."; out.settings_userFeedbackHint1 = "CryptPad suministra informaciones muy básicas al servidor, para ayudarnos a mejorar vuestra experiencia."; out.settings_userFeedbackHint2 = "El contenido de tu pad nunca será compartido con el servidor."; - out.settings_userFeedback = "Activar feedback"; // "Disable user feedback" + out.settings_userFeedback = "Activar feedback"; out.settings_anonymous = "No has iniciado sesión. Tus ajustes se aplicarán solo a este navegador."; out.blog = "Blog"; - out.initialState = "

Esto es CryptPad, el editor collaborativo en tiempo real zero knowledge.
Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.
Incluso el servido no puede ver lo que escribes.

Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí

 

"; + + out.initialState = [ + '

', + 'Esto es CryptPad, el editor collaborativo en tiempo real Zero Knowledge. Todo está guardado cuando escribes.', + '
', + 'Comparte el enlace a este pad para editar con amigos o utiliza el botón  Compartir  para obtener un enlace solo lectura que permite leer pero no escribir.', + '

', + + '

', + 'Vamos, solo empezia a escribir...', + '

', + '

 

' + ].join(''); + out.codeInitialState = "/*\n Esto es CryptPad, el editor collaborativo en tiempo real zero knowledge.\n Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.\n Incluso el servidor no puede ver lo que escribes.\n Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí\n*/"; out.slideInitialState = "# CryptSlide\n* Esto es CryptPad, el editor collaborativo en tiempo real zero knowledge.\n* Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.\n* Incluso el servidor no puede ver lo que escribes.\n* Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí\n\n---\n# Como utilizarlo\n1. Escribe tu contenido en Markdown\n - Puedes aprender más sobre Markdown [aquí](http://www.markdowntutorial.com/)\n2. Separa tus slides con ---\n3. Haz clic en \"Presentar\" para ver el resultado - Tus slides se actualizan en tiempo real"; out.driveReadmeTitle = "¿Qué es CryptDrive?"; @@ -356,5 +369,29 @@ define(function () { out.register_warning = "Zero Knowledge significa que no podemos recuperar tus datos si pierdes tu contraseña."; out.register_alreadyRegistered = "Este usuario ya existe, ¿iniciar sesión?"; + // 1.4.0 - Easter Bunny + + out.button_newwhiteboard = "Nueva Pizarra"; + out.wrongApp = "No se pudo mostrar el contenido de la sessión en tiempo real en tu navigador. Por favor, actualiza la página."; + out.synced = "Todo está guardado."; + out.saveTemplateButton = "Guardar como plantilla"; + out.saveTemplatePrompt = "Élige un título para la plantilla"; + out.templateSaved = "¡Plantilla guardada!"; + out.selectTemplate = "Élige una plantilla o pulsa ESC"; + out.slideOptionsTitle = "Personaliza tus diapositivas"; + out.slideOptionsButton = "Guardar (enter)"; + out.canvas_clear = "Limpiar"; + out.canvas_delete = "Borrar selección"; + out.canvas_disable = "No permitir dibujos"; + out.canvas_enable = "Permitir dibujos"; + out.canvas_width = "Talla"; + out.canvas_opacity = "Opacidad"; + out.settings_publicSigningKey = "Clave de Firma Pública"; + out.settings_usage = "Utilización"; + out.settings_usageTitle = "Vee el uso total de tus pads en MB"; + out.settings_pinningNotAvailable = "Los pads pegados solo están disponibles para usuarios registrados."; + out.settings_pinningError = "Algo salió mal"; + out.settings_usageAmount = "Tus pads pegados utilizan {0}MB"; + return out; }); diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 6d8b3899a..8879d961e 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -378,12 +378,12 @@ define(function () { // Initial states out.initialState = [ - '

', + '

', 'Voici CryptPad, l\'éditeur collaboratif en temps-réel Zero Knowledge. Tout est sauvegardé dés que vous le tapez.', '
', 'Partagez le lien vers ce pad avec des amis ou utilisez le bouton  Partager  pour obtenir le lien de lecture-seule, qui permet la lecture mais non la modification.', '

', - '

', + '

', '', 'Lancez-vous, commencez à taper...', '

', diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 66f771033..8ab0e2084 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -300,6 +300,12 @@ define(function () { out.settings_anonymous = "You are not logged in. Settings here are specific to this browser."; out.settings_publicSigningKey = "Public Signing Key"; + out.settings_usage = "Usage"; + out.settings_usageTitle = "See the total size of your pinned pads in MB"; + out.settings_pinningNotAvailable = "Pinned pads are only available to registered users."; + out.settings_pinningError = "Something went wrong"; + out.settings_usageAmount = "Your pinned pads occupy {0}MB"; + // index.html @@ -386,7 +392,7 @@ define(function () { // Initial states out.initialState = [ - '

', + '

', 'This is CryptPad, the Zero Knowledge realtime collaborative editor. Everything is saved as you type.', '
', 'Share the link to this pad to edit with friends or use the  Share  button to share a read-only link which allows viewing but not editing.', diff --git a/package.json b/package.json index 882eeaa15..48aed5d0d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "scripts": { "lint": "jshint --config .jshintrc --exclude-path .jshintignore .", "test": "node TestSelenium.js", - "style": "lessc ./customize.dist/src/less/cryptpad.less > ./customize.dist/main.css && lessc ./customize.dist/src/less/toolbar.less > ./customize.dist/toolbar.css && lessc ./www/drive/file.less > ./www/drive/file.css && lessc ./www/settings/main.less > ./www/settings/main.css && lessc ./www/slide/slide.less > ./www/slide/slide.css && lessc ./www/whiteboard/whiteboard.less > ./www/whiteboard/whiteboard.css", + "style": "lessc ./customize.dist/src/less/cryptpad.less > ./customize.dist/main.css && lessc ./customize.dist/src/less/toolbar.less > ./customize.dist/toolbar.css && lessc ./www/drive/file.less > ./www/drive/file.css && lessc ./www/settings/main.less > ./www/settings/main.css && lessc ./www/slide/slide.less > ./www/slide/slide.css && lessc ./www/whiteboard/whiteboard.less > ./www/whiteboard/whiteboard.css && lessc ./www/poll/poll.less > ./www/poll/poll.css", "template": "cd customize.dist/src && node build.js" } } diff --git a/www/assert/main.js b/www/assert/main.js index b6a1fbd45..eb9bb9157 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -1,12 +1,10 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/bower_components/jquery/dist/jquery.min.js', + 'jquery', '/bower_components/hyperjson/hyperjson.js', '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', '/common/cryptpad-common.js', -], function (jQuery, Hyperjson, TextPatcher, Sortify, Cryptpad) { - var $ = window.jQuery; +], function ($, Hyperjson, TextPatcher, Sortify, Cryptpad) { window.Hyperjson = Hyperjson; window.TextPatcher = TextPatcher; window.Sortify = Sortify; diff --git a/www/assert/media/main.js b/www/assert/media/main.js index 31a496893..e60fa298b 100644 --- a/www/assert/media/main.js +++ b/www/assert/media/main.js @@ -1,8 +1,4 @@ -define([ - '/bower_components/jquery/dist/jquery.min.js', -], function () { - var $ = window.jQuery; - +define(['jquery'], function ($) { $('media').each(function () { window.alert("media tag selection works!"); }); diff --git a/www/assert/pretty/main.js b/www/assert/pretty/main.js index 1b8272884..825ad03ac 100644 --- a/www/assert/pretty/main.js +++ b/www/assert/pretty/main.js @@ -1,8 +1,7 @@ define([ - '/bower_components/hyperjson/hyperjson.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Hyperjson) { - var $ = window.jQuery; + 'jquery', + '/bower_components/hyperjson/hyperjson.js' +], function ($, Hyperjson) { var shjson = '["BODY",{"class":"cke_editable cke_editable_themed cke_contents_ltr cke_show_borders","spellcheck":"false"},[["P",{},["This is ",["STRONG",{},["CryptPad"]],", the zero knowledge realtime collaborative editor.",["BR",{},[]],"What you type here is encrypted so only people who have the link can access it.",["BR",{},[]],"Even the server cannot see what you type."]],["P",{},[["SMALL",{},[["I",{},["What you see here, what you hear here, when you leave here, let it stay here"]]]],["BR",{"type":"_moz"},[]]]]]]'; var hjson = JSON.parse(shjson); diff --git a/www/assert/translations/main.js b/www/assert/translations/main.js index e8825d422..9a5397c73 100644 --- a/www/assert/translations/main.js +++ b/www/assert/translations/main.js @@ -1,9 +1,8 @@ define([ - '/bower_components/jquery/dist/jquery.min.js', + 'jquery', '/common/cryptpad-common.js', '/customize/translations/messages.js', -], function (jQuery, Cryptpad, English) { - var $ = window.jQuery; +], function ($, Cryptpad, English) { var $body = $('body'); diff --git a/www/code/main.js b/www/code/main.js index 227c1e1fa..50bf06617 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -1,5 +1,5 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ + 'jquery', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/textpatcher/TextPatcher.js', @@ -12,10 +12,8 @@ define([ '/common/themes.js', '/common/visible.js', '/common/notify.js', - '/bower_components/file-saver/FileSaver.min.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify) { - var $ = window.jQuery; + '/bower_components/file-saver/FileSaver.min.js' +], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify) { var saveAs = window.saveAs; var Messages = Cryptpad.Messages; @@ -63,7 +61,7 @@ define([ styleActiveLine : true, search: true, highlightSelectionMatches: {showToken: /\w+/}, - extraKeys: {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }}, + extraKeys: {"Shift-Ctrl-R": undefined}, foldGutter: true, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], mode: "javascript", @@ -646,7 +644,7 @@ define([ return cursor; }; - var onRemote = config.onRemote = function (info) { + var onRemote = config.onRemote = function () { if (initializing) { return; } var scroll = editor.getScrollInfo(); @@ -733,6 +731,7 @@ define([ var second = function (CM) { Cryptpad.ready(function (err, env) { andThen(CM); + Cryptpad.reportAppUsage(); }); Cryptpad.onError(function (info) { if (info && info.type === "store") { diff --git a/www/common/boot2.js b/www/common/boot2.js index 8a38fad2f..599875a93 100644 --- a/www/common/boot2.js +++ b/www/common/boot2.js @@ -1,7 +1,14 @@ // This is stage 1, it can be changed but you must bump the version of the project. define([], function () { // fix up locations so that relative urls work. - require.config({ baseUrl: window.location.pathname }); + require.config({ + baseUrl: window.location.pathname, + paths: { + // jquery declares itself as literally "jquery" so it cannot be pulled by path :( + "jquery": "/bower_components/jquery/dist/jquery.min", + // json.sortify same + "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify" + } + }); require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); }); - diff --git a/www/common/clipboard.js b/www/common/clipboard.js index 5e62f5a37..557c1a809 100644 --- a/www/common/clipboard.js +++ b/www/common/clipboard.js @@ -1,7 +1,4 @@ -define([ - '/bower_components/jquery/dist/jquery.min.js', -], function () { - var $ = window.jQuery; +define(['jquery'], function ($) { var Clipboard = {}; // copy arbitrary text to the clipboard @@ -13,7 +10,7 @@ define([ $('body').append($ta); - if (!($ta.length && $ta[0].select)) { + if (!($ta.length && $ta[0].select)) { // console.log("oops"); return; } diff --git a/www/common/common-hash.js b/www/common/common-hash.js new file mode 100644 index 000000000..034ee5bbb --- /dev/null +++ b/www/common/common-hash.js @@ -0,0 +1,245 @@ +define([ + '/common/common-util.js', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/tweetnacl/nacl-fast.min.js' +], function (Util, Crypto) { + var Nacl = window.nacl; + + var Hash = {}; + + var uint8ArrayToHex = Util.uint8ArrayToHex; + var hexToBase64 = Util.hexToBase64; + var base64ToHex = Util.base64ToHex; + + // This implementation must match that on the server + // it's used for a checksum + Hash.hashChannelList = function (list) { + return Nacl.util.encodeBase64(Nacl.hash(Nacl.util + .decodeUTF8(JSON.stringify(list)))); + }; + + var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) { + if (typeof keys === 'string') { + return chanKey + keys; + } + if (!keys.editKeyStr) { return; } + return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr); + }; + var getViewHashFromKeys = Hash.getViewHashFromKeys = function (chanKey, keys) { + if (typeof keys === 'string') { + return; + } + return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr); + }; + + var parsePadUrl = Hash.parsePadUrl = function (href) { + var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i; + + var ret = {}; + + if (!href) { return ret; } + + if (!/^https*:\/\//.test(href)) { + var idx = href.indexOf('/#'); + ret.type = href.slice(1, idx); + ret.hash = href.slice(idx + 2); + return ret; + } + + var hash = href.replace(patt, function (a, domain, type, hash) { + ret.domain = domain; + ret.type = type; + return ''; + }); + ret.hash = hash.replace(/#/g, ''); + return ret; + }; + + var getRelativeHref = Hash.getRelativeHref = function (href) { + if (!href) { return; } + if (href.indexOf('#') === -1) { return; } + var parsed = parsePadUrl(href); + return '/' + parsed.type + '/#' + parsed.hash; + }; + + /* + * Returns all needed keys for a realtime channel + * - no argument: use the URL hash or create one if it doesn't exist + * - secretHash provided: use secretHash to find the keys + */ + var getSecrets = Hash.getSecrets = function (secretHash) { + var secret = {}; + var generate = function () { + secret.keys = Crypto.createEditCryptor(); + secret.key = Crypto.createEditCryptor().editKeyStr; + }; + if (!secretHash && !/#/.test(window.location.href)) { + generate(); + return secret; + } else { + var hash = secretHash || window.location.hash.slice(1); + if (hash.length === 0) { + generate(); + return secret; + } + // old hash system : #{hexChanKey}{cryptKey} + // new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey} + if (hash.slice(0,1) !== '/' && hash.length >= 56) { + // Old hash + secret.channel = hash.slice(0, 32); + secret.key = hash.slice(32); + } + else { + // New hash + var hashArray = hash.split('/'); + if (hashArray.length < 4) { + Hash.alert("Unable to parse the key"); + throw new Error("Unable to parse the key"); + } + var version = hashArray[1]; + if (version === "1") { + var mode = hashArray[2]; + if (mode === 'edit') { + secret.channel = base64ToHex(hashArray[3]); + var keys = Crypto.createEditCryptor(hashArray[4].replace(/-/g, '/')); + secret.keys = keys; + secret.key = keys.editKeyStr; + if (secret.channel.length !== 32 || secret.key.length !== 24) { + Hash.alert("The channel key and/or the encryption key is invalid"); + throw new Error("The channel key and/or the encryption key is invalid"); + } + } + else if (mode === 'view') { + secret.channel = base64ToHex(hashArray[3]); + secret.keys = Crypto.createViewCryptor(hashArray[4].replace(/-/g, '/')); + if (secret.channel.length !== 32) { + Hash.alert("The channel key is invalid"); + throw new Error("The channel key is invalid"); + } + } + } + } + } + return secret; + }; + + var getHashes = Hash.getHashes = function (channel, secret) { + var hashes = {}; + if (secret.keys.editKeyStr) { + hashes.editHash = getEditHashFromKeys(channel, secret.keys); + } + if (secret.keys.viewKeyStr) { + hashes.viewHash = getViewHashFromKeys(channel, secret.keys); + } + return hashes; + }; + + var createChannelId = Hash.createChannelId = function () { + var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16)); + if (id.length !== 32 || /[^a-f0-9]/.test(id)) { + throw new Error('channel ids must consist of 32 hex characters'); + } + return id; + }; + + var createRandomHash = Hash.createRandomHash = function () { + // 16 byte channel Id + var channelId = Util.hexToBase64(createChannelId()); + // 18 byte encryption key + var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); + return '/1/edit/' + [channelId, key].join('/'); + }; + + var parseHash = Hash.parseHash = function (hash) { + var parsed = {}; + if (hash.slice(0,1) !== '/' && hash.length >= 56) { + // Old hash + parsed.channel = hash.slice(0, 32); + parsed.key = hash.slice(32); + parsed.version = 0; + return parsed; + } + var hashArr = hash.split('/'); + if (hashArr[1] && hashArr[1] === '1') { + parsed.version = 1; + parsed.mode = hashArr[2]; + parsed.channel = hashArr[3]; + parsed.key = hashArr[4]; + parsed.present = hashArr[5] && hashArr[5] === 'present'; + return parsed; + } + return; + }; + + // STORAGE + var findWeaker = Hash.findWeaker = function (href, recents) { + var rHref = href || getRelativeHref(window.location.href); + var parsed = parsePadUrl(rHref); + if (!parsed.hash) { return false; } + var weaker; + recents.some(function (pad) { + var p = parsePadUrl(pad.href); + if (p.type !== parsed.type) { return; } // Not the same type + if (p.hash === parsed.hash) { return; } // Same hash, not stronger + var pHash = parseHash(p.hash); + var parsedHash = parseHash(parsed.hash); + if (!parsedHash || !pHash) { return; } + if (pHash.version !== parsedHash.version) { return; } + if (pHash.channel !== parsedHash.channel) { return; } + if (pHash.mode === 'view' && parsedHash.mode === 'edit') { + weaker = pad.href; + return true; + } + return; + }); + return weaker; + }; + var findStronger = Hash.findStronger = function (href, recents) { + var rHref = href || getRelativeHref(window.location.href); + var parsed = parsePadUrl(rHref); + if (!parsed.hash) { return false; } + var stronger; + recents.some(function (pad) { + var p = parsePadUrl(pad.href); + if (p.type !== parsed.type) { return; } // Not the same type + if (p.hash === parsed.hash) { return; } // Same hash, not stronger + var pHash = parseHash(p.hash); + var parsedHash = parseHash(parsed.hash); + if (!parsedHash || !pHash) { return; } + if (pHash.version !== parsedHash.version) { return; } + if (pHash.channel !== parsedHash.channel) { return; } + if (pHash.mode === 'edit' && parsedHash.mode === 'view') { + stronger = pad.href; + return true; + } + return; + }); + return stronger; + }; + var isNotStrongestStored = Hash.isNotStrongestStored = function (href, recents) { + return findStronger(href, recents); + }; + + var hrefToHexChannelId = Hash.hrefToHexChannelId = function (href) { + var parsed = Hash.parsePadUrl(href); + if (!parsed || !parsed.hash) { return; } + + parsed = Hash.parseHash(parsed.hash); + + if (parsed.version === 0) { + return parsed.channel; + } else if (parsed.version !== 1) { + console.error("parsed href had no version"); + console.error(parsed); + return; + } + + var channel = parsed.channel; + if (!channel) { return; } + + var hex = base64ToHex(channel); + return hex; + }; + + return Hash; +}); diff --git a/www/common/common-interface.js b/www/common/common-interface.js new file mode 100644 index 000000000..624bcfcf3 --- /dev/null +++ b/www/common/common-interface.js @@ -0,0 +1,220 @@ +define([ + 'jquery', + '/customize/messages.js', + '/common/common-util.js', + '/customize/application_config.js', + '/bower_components/alertifyjs/dist/js/alertify.js' +], function ($, Messages, Util, AppConfig, Alertify) { + + var UI = {}; + + /* + * Alertifyjs + */ + UI.Alertify = Alertify; + + // set notification timeout + Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000; + + var findCancelButton = UI.findCancelButton = function () { + return $('button.cancel'); + }; + + var findOKButton = UI.findOKButton = function () { + return $('button.ok'); + }; + + var listenForKeys = UI.listenForKeys = function (yes, no) { + var handler = function (e) { + switch (e.which) { + case 27: // cancel + if (typeof(no) === 'function') { no(e); } + no(); + break; + case 13: // enter + if (typeof(yes) === 'function') { yes(e); } + break; + } + }; + + $(window).keyup(handler); + return handler; + }; + + var stopListening = UI.stopListening = function (handler) { + $(window).off('keyup', handler); + }; + + UI.alert = function (msg, cb, force) { + cb = cb || function () {}; + if (force !== true) { msg = Util.fixHTML(msg); } + var close = function (e) { + findOKButton().click(); + }; + var keyHandler = listenForKeys(close, close); + Alertify.alert(msg, function (ev) { + cb(ev); + stopListening(keyHandler); + }); + window.setTimeout(function () { + findOKButton().focus(); + }); + }; + + UI.prompt = function (msg, def, cb, opt, force) { + opt = opt || {}; + cb = cb || function () {}; + if (force !== true) { msg = Util.fixHTML(msg); } + + var keyHandler = listenForKeys(function (e) { // yes + findOKButton().click(); + }, function (e) { // no + findCancelButton().click(); + }); + + Alertify + .defaultValue(def || '') + .okBtn(opt.ok || Messages.okButton || 'OK') + .cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel') + .prompt(msg, function (val, ev) { + cb(val, ev); + stopListening(keyHandler); + }, function (ev) { + cb(null, ev); + stopListening(keyHandler); + }); + }; + + UI.confirm = function (msg, cb, opt, force, styleCB) { + opt = opt || {}; + cb = cb || function () {}; + if (force !== true) { msg = Util.fixHTML(msg); } + + var keyHandler = listenForKeys(function (e) { + findOKButton().click(); + }, function (e) { + findCancelButton().click(); + }); + + Alertify + .okBtn(opt.ok || Messages.okButton || 'OK') + .cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel') + .confirm(msg, function () { + cb(true); + stopListening(keyHandler); + }, function () { + cb(false); + stopListening(keyHandler); + }); + + window.setTimeout(function () { + var $ok = findOKButton(); + var $cancel = findCancelButton(); + if (opt.okClass) { $ok.addClass(opt.okClass); } + if (opt.cancelClass) { $cancel.addClass(opt.cancelClass); } + if (opt.reverseOrder) { + $ok.insertBefore($ok.prev()); + } + if (typeof(styleCB) === 'function') { + styleCB($ok.closest('.dialog')); + } + }, 0); + }; + + UI.log = function (msg) { + Alertify.success(Util.fixHTML(msg)); + }; + + UI.warn = function (msg) { + Alertify.error(Util.fixHTML(msg)); + }; + + /* + * spinner + */ + UI.spinner = function (parent) { + var $target = $('', { + 'class': 'fa fa-spinner fa-pulse fa-4x fa-fw' + }).hide(); + + $(parent).append($target); + + return { + show: function () { + $target.show(); + return this; + }, + hide: function () { + $target.hide(); + return this; + }, + get: function () { + return $target; + }, + }; + }; + + var LOADING = 'loading'; + + var getRandomTip = function () { + if (!Messages.tips || !Object.keys(Messages.tips).length) { return ''; } + var keys = Object.keys(Messages.tips); + var rdm = Math.floor(Math.random() * keys.length); + return Messages.tips[keys[rdm]]; + }; + UI.addLoadingScreen = function (loadingText, hideTips) { + var $loading, $container; + if ($('#' + LOADING).length) { + $loading = $('#' + LOADING).show(); + if (loadingText) { + $('#' + LOADING).find('p').text(loadingText); + } + $container = $loading.find('.loadingContainer'); + } else { + $loading = $('

', {id: LOADING}); + $container = $('
', {'class': 'loadingContainer'}); + $container.append(''); + var $spinner = $('
', {'class': 'spinnerContainer'}); + UI.spinner($spinner).show(); + var $text = $('

').text(loadingText || Messages.loading); + $container.append($spinner).append($text); + $loading.append($container); + $('body').append($loading); + } + if (Messages.tips && !hideTips) { + var $loadingTip = $('

', {'id': 'loadingTip'}); + var $tip = $('', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip); + $loadingTip.css({ + 'top': $('body').height()/2 + $container.height()/2 + 20 + 'px' + }); + $('body').append($loadingTip); + } + }; + UI.removeLoadingScreen = function (cb) { + $('#' + LOADING).fadeOut(750, cb); + $('#loadingTip').css('top', ''); + window.setTimeout(function () { + $('#loadingTip').fadeOut(750); + }, 3000); + }; + UI.errorLoadingScreen = function (error, transparent) { + if (!$('#' + LOADING).is(':visible')) { UI.addLoadingScreen(undefined, true); } + $('.spinnerContainer').hide(); + if (transparent) { $('#' + LOADING).css('opacity', 0.8); } + $('#' + LOADING).find('p').html(error || Messages.error); + }; + + var importContent = UI.importContent = function (type, f) { + return function () { + var $files = $('').click(); + $files.on('change', function (e) { + var file = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function (e) { f(e.target.result, file); }; + reader.readAsText(file, type); + }); + }; + }; + + return UI; +}); diff --git a/www/common/common-util.js b/www/common/common-util.js new file mode 100644 index 000000000..29b25c4fb --- /dev/null +++ b/www/common/common-util.js @@ -0,0 +1,85 @@ +define([], function () { + var Util = {}; + + var find = Util.find = function (map, path) { + return (map && path.reduce(function (p, n) { + return typeof(p[n]) !== 'undefined' && p[n]; + }, map)); + }; + + var fixHTML = Util.fixHTML = function (str) { + if (!str) { return ''; } + return str.replace(/[<>&"']/g, function (x) { + return ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[x]; + }); + }; + + var hexToBase64 = Util.hexToBase64 = function (hex) { + var hexArray = hex + .replace(/\r|\n/g, "") + .replace(/([\da-fA-F]{2}) ?/g, "0x$1 ") + .replace(/ +$/, "") + .split(" "); + var byteString = String.fromCharCode.apply(null, hexArray); + return window.btoa(byteString).replace(/\//g, '-').slice(0,-2); + }; + + var base64ToHex = Util.base64ToHex = function (b64String) { + var hexArray = []; + atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){ + var h = e.charCodeAt(0).toString(16); + if (h.length === 1) { h = "0"+h; } + hexArray.push(h); + }); + return hexArray.join(""); + }; + + var uint8ArrayToHex = Util.uint8ArrayToHex = function (a) { + // call slice so Uint8Arrays work as expected + return Array.prototype.slice.call(a).map(function (e, i) { + var n = Number(e & 0xff).toString(16); + if (n === 'NaN') { + throw new Error('invalid input resulted in NaN'); + } + + switch (n.length) { + case 0: return '00'; // just being careful, shouldn't happen + case 1: return '0' + n; + case 2: return n; + default: throw new Error('unexpected value'); + } + }).join(''); + }; + + var deduplicateString = Util.deduplicateString = function (array) { + var a = array.slice(); + for(var i=0; i&"']/g, function (x) { - return ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[x]; - }); - }; - - - var truncate = common.truncate = function (text, len) { - if (typeof(text) === 'string' && text.length > len) { - return text.slice(0, len) + '…'; - } - return text; - }; - - var hexToBase64 = common.hexToBase64 = function (hex) { - var hexArray = hex - .replace(/\r|\n/g, "") - .replace(/([\da-fA-F]{2}) ?/g, "0x$1 ") - .replace(/ +$/, "") - .split(" "); - var byteString = String.fromCharCode.apply(null, hexArray); - return window.btoa(byteString).replace(/\//g, '-').slice(0,-2); - }; - - var base64ToHex = common.base64ToHex = function (b64String) { - var hexArray = []; - atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){ - var h = e.charCodeAt(0).toString(16); - if (h.length === 1) { h = "0"+h; } - hexArray.push(h); - }); - return hexArray.join(""); - }; - - var deduplicateString = common.deduplicateString = function (array) { - var a = array.slice(); - for(var i=0; i= 56) { - // Old hash - parsed.channel = hash.slice(0, 32); - parsed.key = hash.slice(32); - parsed.version = 0; - return parsed; - } - var hashArr = hash.split('/'); - if (hashArr[1] && hashArr[1] === '1') { - parsed.version = 1; - parsed.mode = hashArr[2]; - parsed.channel = hashArr[3]; - parsed.key = hashArr[4]; - parsed.present = hashArr[5] && hashArr[5] === 'present'; - return parsed; - } - return; - }; - var getEditHashFromKeys = common.getEditHashFromKeys = function (chanKey, keys) { - if (typeof keys === 'string') { - return chanKey + keys; - } - if (!keys.editKeyStr) { return; } - return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr); - }; - var getViewHashFromKeys = common.getViewHashFromKeys = function (chanKey, keys) { - if (typeof keys === 'string') { - return; - } - return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr); - }; - var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys; - - var specialHashes = common.specialHashes = ['iframe']; - - /* - * Returns all needed keys for a realtime channel - * - no argument: use the URL hash or create one if it doesn't exist - * - secretHash provided: use secretHash to find the keys - */ - var getSecrets = common.getSecrets = function (secretHash) { - var secret = {}; - var generate = function () { - secret.keys = Crypto.createEditCryptor(); - secret.key = Crypto.createEditCryptor().editKeyStr; - }; - if (!secretHash && !/#/.test(window.location.href)) { - generate(); - return secret; - } else { - var hash = secretHash || window.location.hash.slice(1); - if (hash.length === 0 || specialHashes.indexOf(hash) !== -1) { - generate(); - return secret; - } - // old hash system : #{hexChanKey}{cryptKey} - // new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey} - if (hash.slice(0,1) !== '/' && hash.length >= 56) { - // Old hash - secret.channel = hash.slice(0, 32); - secret.key = hash.slice(32); - } - else { - // New hash - var hashArray = hash.split('/'); - if (hashArray.length < 4) { - common.alert("Unable to parse the key"); - throw new Error("Unable to parse the key"); - } - var version = hashArray[1]; - if (version === "1") { - var mode = hashArray[2]; - if (mode === 'edit') { - secret.channel = base64ToHex(hashArray[3]); - var keys = Crypto.createEditCryptor(hashArray[4].replace(/-/g, '/')); - secret.keys = keys; - secret.key = keys.editKeyStr; - if (secret.channel.length !== 32 || secret.key.length !== 24) { - common.alert("The channel key and/or the encryption key is invalid"); - throw new Error("The channel key and/or the encryption key is invalid"); - } - } - else if (mode === 'view') { - secret.channel = base64ToHex(hashArray[3]); - secret.keys = Crypto.createViewCryptor(hashArray[4].replace(/-/g, '/')); - if (secret.channel.length !== 32) { - common.alert("The channel key is invalid"); - throw new Error("The channel key is invalid"); - } - } - } - } - } - return secret; - }; - - var getHashes = common.getHashes = function (channel, secret) { - var hashes = {}; - if (secret.keys.editKeyStr) { - hashes.editHash = getEditHashFromKeys(channel, secret.keys); - } - if (secret.keys.viewKeyStr) { - hashes.viewHash = getViewHashFromKeys(channel, secret.keys); - } - return hashes; - }; - - - var uint8ArrayToHex = common.uint8ArrayToHex = function (a) { - // call slice so Uint8Arrays work as expected - return Array.prototype.slice.call(a).map(function (e, i) { - var n = Number(e & 0xff).toString(16); - if (n === 'NaN') { - throw new Error('invalid input resulted in NaN'); - } - - switch (n.length) { - case 0: return '00'; // just being careful, shouldn't happen - case 1: return '0' + n; - case 2: return n; - default: throw new Error('unexpected value'); - } - }).join(''); - }; - - var createChannelId = common.createChannelId = function () { - var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16)); - if (id.length !== 32 || /[^a-f0-9]/.test(id)) { - throw new Error('channel ids must consist of 32 hex characters'); - } - return id; - }; - - var createRandomHash = common.createRandomHash = function () { - // 16 byte channel Id - var channelId = hexToBase64(createChannelId()); - // 18 byte encryption key - var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); - return '/1/edit/' + [channelId, key].join('/'); - }; - - var replaceHash = common.replaceHash = function (hash) { - if (window.history && window.history.replaceState) { - if (!/^#/.test(hash)) { hash = '#' + hash; } - return void window.history.replaceState({}, window.document.title, hash); - } - window.location.hash = hash; - }; - - var storageKey = common.storageKey = 'CryptPad_RECENTPADS'; + common.isArray = $.isArray; /* * localStorage formatting @@ -389,10 +234,23 @@ load pinpad dynamically only after you know that it will be needed */ * title * ??? // what else can we put in here? */ + var checkObjectData = function (pad) { + if (!pad.ctime) { pad.ctime = pad.atime; } + if (/^https*:\/\//.test(pad.href)) { + pad.href = common.getRelativeHref(pad.href); + } + var parsed = common.parsePadUrl(pad.href); + if (!parsed || !parsed.hash) { return; } + if (!pad.title) { + pad.title = common.getDefaultname(parsed); + } + return parsed.hash; + }; + // Migrate from legacy store (localStorage) var migrateRecentPads = common.migrateRecentPads = function (pads) { return pads.map(function (pad) { var hash; - if (isArray(pad)) { + if (Array.isArray(pad)) { // TODO DEPRECATE_F var href = pad[0]; href.replace(/\#(.*)$/, function (a, h) { hash = h; @@ -405,16 +263,7 @@ load pinpad dynamically only after you know that it will be needed */ ctime: pad[1], }; } else if (pad && typeof(pad) === 'object') { - if (!pad.ctime) { pad.ctime = pad.atime; } - if (!pad.title) { - pad.href.replace(/#(.*)$/, function (x, hash) { - pad.title = hash.slice(0,8); - }); - } - if (/^https*:\/\//.test(pad.href)) { - pad.href = common.getRelativeHref(pad.href); - } - hash = pad.href.slice(pad.href.indexOf('#')+1); + hash = checkObjectData(pad); if (!hash || !common.parseHash(hash)) { return; } return pad; } else { @@ -424,15 +273,27 @@ load pinpad dynamically only after you know that it will be needed */ } }).filter(function (x) { return x; }); }; + // Remove everything from RecentPads that is not an object and check the objects + var checkRecentPads = common.checkRecentPads = function (pads) { + pads.forEach(function (pad, i) { + if (pad && typeof(pad) === 'object') { + var hash = checkObjectData(pad); + if (!hash || !common.parseHash(hash)) { return; } + return pad; + } + console.error("[Cryptpad.migrateRecentPads] pad had unexpected value"); + getStore().removeData(i); + }); + }; // Get the pads from localStorage to migrate them to the object store var getLegacyPads = common.getLegacyPads = function (cb) { - require(['/customize/store.js'], function(Legacy) { + require(['/customize/store.js'], function(Legacy) { // TODO DEPRECATE_F Legacy.ready(function (err, legacy) { if (err) { cb(err, null); return; } legacy.get(storageKey, function (err2, recentPads) { if (err2) { cb(err2, null); return; } - if (isArray(recentPads)) { + if (Array.isArray(recentPads)) { cb(void 0, migrateRecentPads(recentPads)); return; } @@ -442,58 +303,15 @@ load pinpad dynamically only after you know that it will be needed */ }); }; - var getHash = common.getHash = function () { - return window.location.hash.slice(1); - }; - - var getRelativeHref = common.getRelativeHref = function (href) { - if (!href) { return; } - if (href.indexOf('#') === -1) { return; } - var parsed = common.parsePadUrl(href); - return '/' + parsed.type + '/#' + parsed.hash; - }; - - var parsePadUrl = common.parsePadUrl = function (href) { - var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i; - - var ret = {}; - - if (!href) { return ret; } - - if (!/^https*:\/\//.test(href)) { - var idx = href.indexOf('/#'); - ret.type = href.slice(1, idx); - ret.hash = href.slice(idx + 2); - return ret; - } - - var hash = href.replace(patt, function (a, domain, type, hash) { - ret.domain = domain; - ret.type = type; - return ''; - }); - ret.hash = hash.replace(/#/g, ''); - return ret; - }; - - var isNameAvailable = function (title, parsed, pads) { - return !pads.some(function (pad) { - // another pad is already using that title - if (pad.title === title) { - return true; - } - }); - }; - // Create untitled documents when no name is given - var getDefaultName = common.getDefaultName = function (parsed, recentPads) { + var getDefaultName = common.getDefaultName = function (parsed) { var type = parsed.type; var untitledIndex = 1; var name = (Messages.type)[type] + ' - ' + new Date().toString().split(' ').slice(0,4).join(' '); return name; }; var isDefaultName = common.isDefaultName = function (parsed, title) { - var name = getDefaultName(parsed, []); + var name = getDefaultName(parsed); return title === name; }; @@ -553,8 +371,9 @@ load pinpad dynamically only after you know that it will be needed */ }); return templates; }; - var addTemplate = common.addTemplate = function (href) { - getStore().addTemplate(href); + var addTemplate = common.addTemplate = function (data) { + getStore().pushData(data); + getStore().addPad(data.href, ['template']); }; var isTemplate = common.isTemplate = function (href) { @@ -594,29 +413,18 @@ load pinpad dynamically only after you know that it will be needed */ }; // STORAGE - /* fetch and migrate your pad history from localStorage */ + /* fetch and migrate your pad history from the store */ var getRecentPads = common.getRecentPads = function (cb) { getStore().getDrive(storageKey, function (err, recentPads) { - if (isArray(recentPads)) { - cb(void 0, migrateRecentPads(recentPads)); + if (Array.isArray(recentPads)) { + checkRecentPads(recentPads); + cb(void 0, recentPads); return; } cb(void 0, []); }); }; - // STORAGE - /* commit a list of pads to localStorage */ - // TODO integrate pinning if enabled - var setRecentPads = common.setRecentPads = function (pads, cb) { - getStore().setDrive(storageKey, pads, function (err, data) { - if (PINNING_ENABLED && isLoggedIn()) { - console.log("TODO check pin hash"); - } - cb(err, data); - }); - }; - // STORAGE: Display Name var getLastName = common.getLastName = function (cb) { common.getAttribute('username', function (err, userName) { @@ -636,7 +444,6 @@ load pinpad dynamically only after you know that it will be needed */ }; // STORAGE - // TODO integrate pinning if enabled var forgetPad = common.forgetPad = function (href, cb) { var parsed = parsePadUrl(href); @@ -670,60 +477,12 @@ load pinpad dynamically only after you know that it will be needed */ } }; - // STORAGE - var findWeaker = common.findWeaker = function (href, recents) { - var rHref = href || getRelativeHref(window.location.href); - var parsed = parsePadUrl(rHref); - if (!parsed.hash) { return false; } - var weaker; - recents.some(function (pad) { - var p = parsePadUrl(pad.href); - if (p.type !== parsed.type) { return; } // Not the same type - if (p.hash === parsed.hash) { return; } // Same hash, not stronger - var pHash = parseHash(p.hash); - var parsedHash = parseHash(parsed.hash); - if (!parsedHash || !pHash) { return; } - if (pHash.version !== parsedHash.version) { return; } - if (pHash.channel !== parsedHash.channel) { return; } - if (pHash.mode === 'view' && parsedHash.mode === 'edit') { - weaker = pad.href; - return true; - } - return; - }); - return weaker; - }; - var findStronger = common.findStronger = function (href, recents) { - var rHref = href || getRelativeHref(window.location.href); - var parsed = parsePadUrl(rHref); - if (!parsed.hash) { return false; } - var stronger; - recents.some(function (pad) { - var p = parsePadUrl(pad.href); - if (p.type !== parsed.type) { return; } // Not the same type - if (p.hash === parsed.hash) { return; } // Same hash, not stronger - var pHash = parseHash(p.hash); - var parsedHash = parseHash(parsed.hash); - if (!parsedHash || !pHash) { return; } - if (pHash.version !== parsedHash.version) { return; } - if (pHash.channel !== parsedHash.channel) { return; } - if (pHash.mode === 'edit' && parsedHash.mode === 'view') { - stronger = pad.href; - return true; - } - return; - }); - return stronger; - }; - var isNotStrongestStored = common.isNotStrongestStored = function (href, recents) { - return findStronger(href, recents); - }; - - // TODO integrate pinning var setPadTitle = common.setPadTitle = function (name, cb) { var href = window.location.href; var parsed = parsePadUrl(href); href = getRelativeHref(href); + // getRecentPads return the array from the drive, not a copy + // We don't have to call "set..." at the end, everything is stored with listmap getRecentPads(function (err, recent) { if (err) { cb(err); @@ -779,175 +538,18 @@ load pinpad dynamically only after you know that it will be needed */ if (!contains) { var data = makePad(href, name); - renamed.push(data); - if (typeof(getStore().addPad) === "function") { - getStore().addPad(href, common.initialPath, common.initialName || name); - } + getStore().pushData(data); + getStore().addPad(href, common.initialPath, common.initialName || name); } - - setRecentPads(renamed, function (err, data) { - if (updateWeaker.length > 0) { - updateWeaker.forEach(function (obj) { - getStore().replaceHref(obj.o, obj.n); - }); - } - cb(err, data); - }); + if (updateWeaker.length > 0) { + updateWeaker.forEach(function (obj) { + getStore().replaceHref(obj.o, obj.n); + }); + } + cb(err, recent); }); }; - // STORAGE - var getPadTitle = common.getPadTitle = function (cb) { - var href = window.location.href; - var parsed = parsePadUrl(window.location.href); - var hashSlice = window.location.hash.slice(1,9); - var title = ''; - - getRecentPads(function (err, pads) { - if (err) { - cb(err); - return; - } - pads.some(function (pad) { - var p = parsePadUrl(pad.href); - if (p.hash === parsed.hash && p.type === parsed.type) { - title = pad.title || hashSlice; - return true; - } - }); - - if (title === '') { title = getDefaultName(parsed, pads); } - - cb(void 0, title); - }); - }; - - // STORAGE - var causesNamingConflict = common.causesNamingConflict = function (title, cb) { - var href = window.location.href; - - var parsed = parsePadUrl(href); - getRecentPads(function (err, pads) { - if (err) { - cb(err); - return; - } - var conflicts = pads.some(function (pad) { - // another pad is already using that title - if (pad.title === title) { - var p = parsePadUrl(pad.href); - - if (p.type === parsed.type && p.hash === parsed.hash) { - // the duplicate pad has the same type and hash - // allow renames - } else { - // it's an entirely different pad... it conflicts - return true; - } - } - }); - cb(void 0, conflicts); - }); - }; - - var newPadNameKey = common.newPadNameKey = "newPadName"; - var newPadPathKey = common.newPadPathKey = "newPadPath"; - - // local name? - common.ready = function (f) { - var block = 0; - var env = {}; - - var cb = function () { - block--; - if (!block) { - f(void 0, env); - } - }; - - if (sessionStorage[newPadNameKey]) { - common.initialName = sessionStorage[newPadNameKey]; - delete sessionStorage[newPadNameKey]; - } - if (sessionStorage[newPadPathKey]) { - common.initialPath = sessionStorage[newPadPathKey]; - delete sessionStorage[newPadPathKey]; - } - - Store.ready(function (err, storeObj) { - store = common.store = env.store = storeObj; - - var proxy = getProxy(); - var network = getNetwork(); - - $(function() { - // Race condition : if document.body is undefined when alertify.js is loaded, Alertify - // won't work. We have to reset it now to make sure it uses a correct "body" - Alertify.reset(); - - // Load the new pad when the hash has changed - var oldHash = document.location.hash.slice(1); - window.onhashchange = function () { - var newHash = document.location.hash.slice(1); - var parsedOld = parseHash(oldHash); - var parsedNew = parseHash(newHash); - if (parsedOld && parsedNew && ( - parsedOld.channel !== parsedNew.channel - || parsedOld.mode !== parsedNew.mode - || parsedOld.key !== parsedNew.key)) { - document.location.reload(); - return; - } - if (parsedNew) { - oldHash = newHash; - } - }; - - if (PINNING_ENABLED && isLoggedIn()) { - console.log("logged in. pads will be pinned"); - block++; - - // TODO setTimeout in case rpc doesn't - // activate in reasonable time? - Pinpad.create(network, proxy, function (e, call) { - if (e) { - console.error(e); - return cb(); - } - - console.log('RPC handshake complete'); - rpc = common.rpc = env.rpc = call; - - // TODO check if pin list is up to date - // if not, reset - cb(); - }); - } else if (PINNING_ENABLED) { - console.log('not logged in. pads will not be pinned'); - } else { - console.log('pinning disabled'); - } - - // Everything's ready, continue... - if($('#pad-iframe').length) { - block++; - var $iframe = $('#pad-iframe'); - var iframe = $iframe[0]; - var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - if (iframeDoc.readyState === 'complete') { - cb(); - return; - } - $iframe.load(cb); - return; - } - - block++; - cb(); - }); - }, common); - }; - var errorHandlers = []; common.onError = function (h) { if (typeof h !== "function") { return; } @@ -961,75 +563,6 @@ load pinpad dynamically only after you know that it will be needed */ }); }; - var LOADING = 'loading'; - var getRandomTip = function () { - if (!Messages.tips || !Object.keys(Messages.tips).length) { return ''; } - var keys = Object.keys(Messages.tips); - var rdm = Math.floor(Math.random() * keys.length); - return Messages.tips[keys[rdm]]; - }; - common.addLoadingScreen = function (loadingText, hideTips) { - var $loading, $container; - if ($('#' + LOADING).length) { - $loading = $('#' + LOADING).show(); - if (loadingText) { - $('#' + LOADING).find('p').text(loadingText); - } - $container = $loading.find('.loadingContainer'); - } else { - $loading = $('
', {id: LOADING}); - $container = $('
', {'class': 'loadingContainer'}); - $container.append(''); - var $spinner = $('
', {'class': 'spinnerContainer'}); - common.spinner($spinner).show(); - var $text = $('

').text(loadingText || Messages.loading); - $container.append($spinner).append($text); - $loading.append($container); - $('body').append($loading); - } - if (Messages.tips && !hideTips) { - var $loadingTip = $('

', {'id': 'loadingTip'}); - var $tip = $('', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip); - $loadingTip.css({ - 'top': $('body').height()/2 + $container.height()/2 + 20 + 'px' - }); - $('body').append($loadingTip); - } - }; - common.removeLoadingScreen = function (cb) { - $('#' + LOADING).fadeOut(750, cb); - $('#loadingTip').css('top', ''); - window.setTimeout(function () { - $('#loadingTip').fadeOut(750); - }, 3000); - }; - common.errorLoadingScreen = function (error, transparent) { - if (!$('#' + LOADING).is(':visible')) { common.addLoadingScreen(undefined, true); } - $('.spinnerContainer').hide(); - if (transparent) { $('#' + LOADING).css('opacity', 0.8); } - $('#' + LOADING).find('p').html(error || Messages.error); - }; - - /* - * Saving files - */ - var fixFileName = common.fixFileName = function (filename) { - return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_') - .replace(/_+/g, '_'); - }; - - var importContent = common.importContent = function (type, f) { - return function () { - var $files = $('').click(); - $files.on('change', function (e) { - var file = e.target.files[0]; - var reader = new FileReader(); - reader.onload = function (e) { f(e.target.result, file); }; - reader.readAsText(file, type); - }); - }; - }; - /* * Buttons */ @@ -1051,27 +584,6 @@ load pinpad dynamically only after you know that it will be needed */ }); }; - var hrefToHexChannelId = common.hrefToHexChannelId = function (href) { - var parsed = common.parsePadUrl(href); - if (!parsed || !parsed.hash) { return; } - - parsed = common.parseHash(parsed.hash); - - if (parsed.version === 0) { - return parsed.channel; - } else if (parsed.version !== 1) { - console.error("parsed href had no version"); - console.error(parsed); - return; - } - - var channel = parsed.channel; - if (!channel) { return; } - - var hex = common.base64ToHex(channel); - return hex; - }; - var getUserChannelList = common.getUserChannelList = function () { var store = common.getStore(); var proxy = store.getProxy(); @@ -1084,7 +596,7 @@ load pinpad dynamically only after you know that it will be needed */ var userChannel = common.parseHash(userHash).channel; if (!userChannel) { return null; } - var list = fo.getFilesDataFiles().map(hrefToHexChannelId) + var list = fo.getFiles([fo.FILES_DATA]).map(hrefToHexChannelId) .filter(function (x) { return x; }); list.push(common.base64ToHex(userChannel)); @@ -1098,6 +610,9 @@ load pinpad dynamically only after you know that it will be needed */ }; var pinsReady = common.pinsReady = function () { + if (!isLoggedIn()) { + return false; + } if (!PINNING_ENABLED) { console.error('[PINNING_DISABLED]'); return false; @@ -1113,7 +628,7 @@ load pinpad dynamically only after you know that it will be needed */ if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } var list = getCanonicalChannelList(); - var local = rpc.hashChannelList(list); + var local = Hash.hashChannelList(list); rpc.getServerHash(function (e, hash) { if (e) { return void cb(e); } cb(void 0, hash === local); @@ -1121,8 +636,7 @@ load pinpad dynamically only after you know that it will be needed */ }; var resetPins = common.resetPins = function (cb) { - if (!PINNING_ENABLED) { return void console.error('[PINNING_DISABLED]'); } - if (!rpc) { return void console.error('[RPC_NOT_READY]'); } + if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } var list = getCanonicalChannelList(); rpc.reset(list, function (e, hash) { @@ -1131,6 +645,29 @@ load pinpad dynamically only after you know that it will be needed */ }); }; + var pinPads = common.pinPads = function (pads, cb) { + if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } + + rpc.pin(pads, function (e, hash) { + if (e) { return void cb(e); } + cb(void 0, hash); + }); + }; + + var unpinPads = common.unpinPads = function (pads, cb) { + if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } + + rpc.unpin(pads, function (e, hash) { + if (e) { return void cb(e); } + cb(void 0, hash); + }); + }; + + var getPinnedUsage = common.getPinnedUsage = function (cb) { + if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } + rpc.getFileListSize(cb); + }; + var createButton = common.createButton = function (type, rightside, data, callback) { var button; var size = "17px"; @@ -1148,7 +685,7 @@ load pinpad dynamically only after you know that it will be needed */ title: Messages.importButtonTitle, }).append($('', {'class':'fa fa-upload', style: 'font:'+size+' FontAwesome'})); if (callback) { - button.click(common.importContent('text/plain', function (content, file) { + button.click(UI.importContent('text/plain', function (content, file) { callback(content, file); })); } @@ -1279,7 +816,7 @@ load pinpad dynamically only after you know that it will be needed */ // // allowed options tags: ['a', 'hr', 'p'] var createDropdown = common.createDropdown = function (config) { - if (typeof config !== "object" || !isArray(config.options)) { return; } + if (typeof config !== "object" || !Array.isArray(config.options)) { return; } var allowedTags = ['a', 'p', 'hr']; var isValidOption = function (o) { @@ -1571,155 +1108,111 @@ load pinpad dynamically only after you know that it will be needed */ return $userAdmin; }; - /* - * Alertifyjs - */ + common.ready = function (f) { + var block = 0; + var env = {}; - var styleAlerts = common.styleAlerts = function () {}; - - var findCancelButton = common.findCancelButton = function () { - return $('button.cancel'); - }; - - var findOKButton = common.findOKButton = function () { - return $('button.ok'); - }; - - var listenForKeys = common.listenForKeys = function (yes, no) { - var handler = function (e) { - switch (e.which) { - case 27: // cancel - if (typeof(no) === 'function') { no(e); } - no(); - break; - case 13: // enter - if (typeof(yes) === 'function') { yes(e); } - break; + var cb = function () { + block--; + if (!block) { + f(void 0, env); } }; - $(window).keyup(handler); - return handler; - }; + if (sessionStorage[newPadNameKey]) { + common.initialName = sessionStorage[newPadNameKey]; + delete sessionStorage[newPadNameKey]; + } + if (sessionStorage[newPadPathKey]) { + common.initialPath = sessionStorage[newPadPathKey]; + delete sessionStorage[newPadPathKey]; + } - var stopListening = common.stopListening = function (handler) { - $(window).off('keyup', handler); - }; + Store.ready(function (err, storeObj) { + store = common.store = env.store = storeObj; - common.alert = function (msg, cb, force) { - cb = cb || function () {}; - if (force !== true) { msg = fixHTML(msg); } - var close = function (e) { - findOKButton().click(); - }; - var keyHandler = listenForKeys(close, close); - Alertify.alert(msg, function (ev) { - cb(ev); - stopListening(keyHandler); - }); - window.setTimeout(function () { - findOKButton().focus(); - }); - }; + var proxy = getProxy(); - common.prompt = function (msg, def, cb, opt, force) { - opt = opt || {}; - cb = cb || function () {}; - if (force !== true) { msg = fixHTML(msg); } + /* TODO log users out if they are logged in, but don't have + signing keys stored in their object. + */ - var keyHandler = listenForKeys(function (e) { // yes - findOKButton().click(); - }, function (e) { // no - findCancelButton().click(); - }); + var network = getNetwork(); - Alertify - .defaultValue(def || '') - .okBtn(opt.ok || Messages.okButton || 'OK') - .cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel') - .prompt(msg, function (val, ev) { - cb(val, ev); - stopListening(keyHandler); - }, function (ev) { - cb(null, ev); - stopListening(keyHandler); + $(function() { + // Race condition : if document.body is undefined when alertify.js is loaded, Alertify + // won't work. We have to reset it now to make sure it uses a correct "body" + UI.Alertify.reset(); + + // Load the new pad when the hash has changed + var oldHash = document.location.hash.slice(1); + window.onhashchange = function () { + var newHash = document.location.hash.slice(1); + var parsedOld = parseHash(oldHash); + var parsedNew = parseHash(newHash); + if (parsedOld && parsedNew && ( + parsedOld.channel !== parsedNew.channel + || parsedOld.mode !== parsedNew.mode + || parsedOld.key !== parsedNew.key)) { + document.location.reload(); + return; + } + if (parsedNew) { + oldHash = newHash; + } + }; + + if (PINNING_ENABLED && isLoggedIn()) { + console.log("logged in. pads will be pinned"); + block++; + + Pinpad.create(network, proxy, function (e, call) { + if (e) { + console.error(e); + return cb(); + } + + console.log('RPC handshake complete'); + rpc = common.rpc = env.rpc = call; + + common.arePinsSynced(function (err, yes) { + if (!yes) { + common.resetPins(function (err, hash) { + console.log('RESET DONE'); + }); + } + }); + cb(); + }); + } else if (PINNING_ENABLED) { + console.log('not logged in. pads will not be pinned'); + } else { + console.log('pinning disabled'); + } + + // Everything's ready, continue... + if($('#pad-iframe').length) { + block++; + var $iframe = $('#pad-iframe'); + var iframe = $iframe[0]; + var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (iframeDoc.readyState === 'complete') { + cb(); + return; + } + $iframe.load(cb); + return; + } + + block++; + cb(); }); - }; - - common.confirm = function (msg, cb, opt, force, styleCB) { - opt = opt || {}; - cb = cb || function () {}; - if (force !== true) { msg = fixHTML(msg); } - - var keyHandler = listenForKeys(function (e) { - findOKButton().click(); - }, function (e) { - findCancelButton().click(); - }); - - Alertify - .okBtn(opt.ok || Messages.okButton || 'OK') - .cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel') - .confirm(msg, function () { - cb(true); - stopListening(keyHandler); - }, function () { - cb(false); - stopListening(keyHandler); - }); - - window.setTimeout(function () { - var $ok = findOKButton(); - var $cancel = findCancelButton(); - if (opt.okClass) { $ok.addClass(opt.okClass); } - if (opt.cancelClass) { $cancel.addClass(opt.cancelClass); } - if (opt.reverseOrder) { - $ok.insertBefore($ok.prev()); - } - if (typeof(styleCB) === 'function') { - styleCB($ok.closest('.dialog')); - } - }, 0); - }; - - common.log = function (msg) { - Alertify.success(fixHTML(msg)); - }; - - common.warn = function (msg) { - Alertify.error(fixHTML(msg)); - }; - - /* - * spinner - */ - common.spinner = function (parent) { - var $target = $('', { - 'class': 'fa fa-spinner fa-pulse fa-4x fa-fw' - }).hide(); - - $(parent).append($target); - - return { - show: function () { - $target.show(); - return this; - }, - hide: function () { - $target.hide(); - return this; - }, - get: function () { - return $target; - }, - }; + }, common); }; $(function () { Messages._applyTranslation(); }); - Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000; - return common; }); diff --git a/www/common/cursor.js b/www/common/cursor.js index 07e705244..6da99f1bd 100644 --- a/www/common/cursor.js +++ b/www/common/cursor.js @@ -2,10 +2,6 @@ define([ '/common/treesome.js', '/bower_components/rangy/rangy-core.min.js' ], function (Tree, Rangy, saveRestore) { - //window.Rangy = Rangy; - //window.Tree = Tree; - // do some function for the start and end of the cursor - var log = function (x) { console.log(x); }; var error = function (x) { console.log(x); }; var verbose = function (x) { if (window.verboseMode) { console.log(x); } }; @@ -27,108 +23,6 @@ define([ } }; - // TODO deprecate - // assumes a negative index - var seekLeft /* = cursor.seekLeft*/ = function (el, delta, current) { - var textLength; - var previous; - - // normalize - - if (-delta >= current) { - delta += current; - current = 0; - } else { - current += delta; - delta = 0; - } - - while (delta) { - previous = el; - el = Tree.previousNode(el, inner); - if (el) { - textLength = el.textContent.length; - if (-delta > textLength) { - delta -= textLength; - } else { - current = textLength + delta; - delta = 0; - } - } else { - return { - el: previous, - offset: 0, - error: "out of bounds" - }; - } - } - return { - el: el, - offset: current - }; - }; - - // TODO deprecate - // seekRight assumes a positive delta - var seekRight = /* cursor.seekRight = */ function (el, delta, current) { - var textLength; - var previous; - - // normalize - delta += current; - current = 0; - - while (delta) { - if (el) { - textLength = el.textContent.length; - if (delta >= textLength) { - delta -= textLength; - previous = el; - el = Tree.nextNode(el, inner); - } else { - current = delta; - delta = 0; - } - } else { - // don't ever return a negative index - if (previous.textContent.length) { - textLength = previous.textContent.length - 1; - } else { - textLength = 0; - } - return { - el: previous, - offset: textLength, - error: "out of bounds" - }; - } - } - return { - el: el, - offset: current - }; - }; - - // TODO deprecate - var seekToDelta = /* cursor.seekToDelta = */ function (el, delta, current) { - var result = null; - if (el) { - if (delta < 0) { - return seekLeft(el, delta, current); - } else if (delta > 0) { - return seekRight(el, delta, current); - } else { - result = { - el: el, - offset: current - }; - } - } else { - error("[cursor.seekToDelta] el is undefined"); - } - return result; - }; - /* cursor.update takes notes about wherever the cursor was last seen in the event of a cursor loss, the information produced by side effects of this function should be used to recover the cursor @@ -262,109 +156,6 @@ define([ }; }; - /* getLength assumes that both nodes exist inside of the active editor. */ - // unused currently - var getLength = cursor.getLength = function () { - if (Range.start.el === Range.end.el) { - if (Range.start.offset === Range.end.offset) { return 0; } - if (Range.start.offset < Range.end.offset) { - return Range.end.offset - Range.start.offset; - } else { - return Range.start.offset - Range.end.offset; - } - } else { - var order = Tree.orderOfNodes(Range.start.el, Range.end.el, inner); - var L; - var cur; - - /* we know that the cursor elements are different, and that we - must traverse to find the total length. We also know the - order of the nodes (probably 1 or -1) */ - if (order === 1) { - L = (Range.start.el.textContent.length - Range.start.offset); - cur = Tree.nextNode(Range.start.el, inner); - while (cur && cur !== Range.end.el) { - L += cur.textContent.length; - cur = Tree.nextNode(cur, inner); - } - L += Range.end.offset; - return L; - } else if (order === -1) { - L = (Range.end.el.textContent - Range.end.offset); - cur = Tree.nextNode(Range.end.el, inner); - while (cur && cur !== Range.start.el) { - L += cur.textContent.length; - cur = Tree.nextNode(cur, inner); - } - L += Range.start.offset; - return -L; - } else { - console.error("unexpected ordering of nodes..."); - return null; - } - } - }; - - // previously used for testing - // TODO deprecate - var delta = /* cursor.delta = */ function (delta1, delta2) { - var sel = Rangy.getSelection(inner); - delta2 = (typeof delta2 !== 'undefined') ? delta2 : delta1; - - // update returns errors if there are problems - // and updates the persistent Range object - var err = cursor.update(sel, inner); - if (err) { return err; } - - // create a range to modify - var range = Rangy.createRange(); - - /* - The assumption below is that Range.(start|end).el - actually exists. This might not be the case. - TODO check if start and end elements are defined - */ - - // using infromation about wherever you were last... - // move both parts by some delta - var start = seekToDelta(Range.start.el, delta1, Range.start.offset); - var end = seekToDelta(Range.end.el, delta2, Range.end.offset); - - /* if range is backwards, cursor.delta fails - so check if they're in the expected order - before setting the new range */ - - var order = Tree.orderOfNodes(start.el, end.el, inner); - var backward; - - // this could all be one line but nobody would be able to read it - if (order === -1) { - // definitely backward - backward = true; - } else if (order === 0) { - // might be backward, check offsets to know for sure - backward = (start.offset > end.offset); - } else { - // definitely not backward - backward = false; - } - - if (backward) { - range.setStart(end.el, end.offset); - range.setEnd(start.el, start.offset); - } else { - range.setStart(start.el, start.offset); - range.setEnd(end.el, end.offset); - } - - // actually set the cursor to the new range - sel.setSingleRange(range); - return { - startError: start.error, - endError: end.error - }; - }; - cursor.brFix = function () { cursor.update(); var start = Range.start; diff --git a/www/common/fileObject.js b/www/common/fileObject.js index 537ada142..6f947dbe3 100644 --- a/www/common/fileObject.js +++ b/www/common/fileObject.js @@ -1,7 +1,6 @@ define([ - '/bower_components/jquery/dist/jquery.min.js', -], function () { - var $ = window.jQuery; + 'jquery', +], function ($) { var module = {}; var Messages = {}; @@ -45,6 +44,23 @@ define([ return a; }; + var pushFileData = exp.pushData = function (data) { + Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { + console.log(hash); + }); + files[FILES_DATA].push(data); + }; + var spliceFileData = exp.removeData = function (idx) { + var data = files[FILES_DATA][idx]; + if (typeof data === "object") { + Cryptpad.unpinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { + console.log(hash); + }); + } + files[FILES_DATA].splice(idx, 1); + }; + + var comparePath = exp.comparePath = function (a, b) { if (!a || !b || !$.isArray(a) || !$.isArray(b)) { return false; } if (a.length !== b.length) { return false; } @@ -72,12 +88,15 @@ define([ var isPathInTrash = exp.isPathInTrash = function (path) { return path[0] && path[0] === TRASH; }; + var isInTrashRoot = exp.isInTrashRoot = function (path) { + return path[0] === TRASH && path.length === 4; + }; var isPathInFilesData = exp.isPathInFilesData = function (path) { return path[0] && path[0] === FILES_DATA; }; - var isFile = exp.isFile = function (element) { + var isFile = exp.isFile = function (element) { return typeof(element) === "string"; }; @@ -422,10 +441,6 @@ define([ return paths; }; - var isInTrashRoot = exp.isInTrashRoot = function (path) { - return path[0] === TRASH && path.length === 4; - }; - var removePadAttribute = function (f) { Object.keys(files).forEach(function (key) { var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null; @@ -459,7 +474,7 @@ define([ var idx = files[FILES_DATA].indexOf(f); if (idx !== -1) { debug("Removing", f, "from filesData"); - files[FILES_DATA].splice(idx, 1); + spliceFileData(idx); removePadAttribute(f.href); } }); @@ -469,9 +484,9 @@ define([ var parentPath = path.slice(); var key = parentPath.pop(); var parentEl = exp.findElement(files, parentPath); - if (path.length === 4 && path[0] === TRASH) { + if (isInTrashRoot(path)) { files[TRASH][path[1]].splice(path[2], 1); - } else if (path[0] === UNSORTED || path[0] === TEMPLATE) { + } else if (isPathInHrefArray(path)) { parentEl.splice(key, 1); } else { parentEl[key] = undefined; @@ -745,7 +760,7 @@ define([ }; var pushNewFileData = function (href, title) { - files[FILES_DATA].push({ + pushFileData({ href: href, title: title, atime: +new Date(), @@ -865,7 +880,7 @@ define([ var idx = files[FILES_DATA].indexOf(f); if (idx !== -1) { debug("Removing", f, "from filesData"); - files[FILES_DATA].splice(idx, 1); + spliceFileData(idx); // Remove the "padAttributes" stored in the realtime object for that pad removePadAttribute(f.href); } @@ -1009,7 +1024,7 @@ define([ return o.href === href; }); if (!test) { - files[FILES_DATA].push(fileData); + pushFileData(fileData); } if (files[TEMPLATE].indexOf(href) === -1) { files[TEMPLATE].push(href); @@ -1141,7 +1156,7 @@ define([ toClean.forEach(function (el) { var idx = fd.indexOf(el); if (idx !== -1) { - fd.splice(idx, 1); + spliceFileData(idx); } }); }; diff --git a/www/common/fsStore.js b/www/common/fsStore.js index 794a39ecd..b02cccf16 100644 --- a/www/common/fsStore.js +++ b/www/common/fsStore.js @@ -1,10 +1,10 @@ define([ + 'jquery', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js?v=0.1.5', '/bower_components/textpatcher/TextPatcher.amd.js', - '/common/fileObject.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Listmap, Crypto, TextPatcher, FO) { + '/common/userObject.js', +], function ($, Listmap, Crypto, TextPatcher, FO) { /* This module uses localStorage, which is synchronous, but exposes an asyncronous API. This is so that we can substitute other storage @@ -13,7 +13,6 @@ define([ To override these methods, create another file at: /customize/storage.js */ - var $ = window.jQuery; var Store = {}; var store; @@ -86,21 +85,26 @@ define([ cb(void 0, Object.keys(storeObj)); }; + ret.removeData = filesOp.removeData; + ret.pushData = filesOp.pushData; + ret.addPad = function (href, path, name) { - filesOp.addPad(href, path, name); + filesOp.add(href, path, name); }; ret.forgetPad = function (href, cb) { - filesOp.forgetPad(href); + filesOp.forget(href); cb(); }; - ret.addTemplate = function (href) { - filesOp.addTemplate(href); - }; - ret.listTemplates = function () { - return filesOp.listTemplates(); + var templateFiles = filesOp.getFiles(['template']); + var res = []; + templateFiles.forEach(function (f) { + var data = filesOp.getFileData(f); + res.push(JSON.parse(JSON.stringify(data))); + }); + return res; }; ret.getProxy = function () { @@ -120,7 +124,7 @@ define([ }; ret.replaceHref = function (o, n) { - return filesOp.replaceHref(o, n); + return filesOp.replace(o, n); }; var changeHandlers = ret.changeHandlers = []; diff --git a/www/common/login.js b/www/common/login.js index 8e9067bc7..dff4a3be3 100644 --- a/www/common/login.js +++ b/www/common/login.js @@ -1,12 +1,12 @@ define([ + 'jquery', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', '/common/cryptpad-common.js', '/common/credential.js', '/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/scrypt-async/scrypt-async.min.js', // better load speed - '/bower_components/jquery/dist/jquery.min.js', -], function (Listmap, Crypto, Cryptpad, Cred) { +], function ($, Listmap, Crypto, Cryptpad, Cred) { var Exports = { Cred: Cred, }; diff --git a/www/common/mergeDrive.js b/www/common/mergeDrive.js index 688f1cedc..dfd891291 100644 --- a/www/common/mergeDrive.js +++ b/www/common/mergeDrive.js @@ -1,8 +1,7 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/common/cryptpad-common.js', '/common/cryptget.js', - '/common/fileObject.js', + '/common/userObject.js', 'json.sortify' ], function (Cryptpad, Crypt, FO, Sortify) { var exp = {}; @@ -76,8 +75,8 @@ define([ console.error(msg || "Unable to find that path", path); }; - if (path[0] === FO.TRASH && path.length === 4) { - href = oldFo.getTrashElementData(path); + if (oldFo.isInTrashRoot(path)) { + href = oldFo.find(path.slice(0,3)); path.pop(); } @@ -156,13 +155,13 @@ define([ var newData = Cryptpad.getStore().getProxy(); var newFo = newData.fo; var newRecentPads = proxy.drive[Cryptpad.storageKey]; - var newFiles = newFo.getFilesDataFiles(); - var oldFiles = oldFo.getFilesDataFiles(); + var newFiles = newFo.getFiles([newFo.FILES_DATA]); + var oldFiles = oldFo.getFiles([newFo.FILES_DATA]); oldFiles.forEach(function (href) { // Do not migrate a pad if we already have it, it would create a duplicate in the drive if (newFiles.indexOf(href) !== -1) { return; } // If we have a stronger version, do not add the current href - if (Cryptpad.findStronger(href, newRecentPads)) { return; } + if (Cryptpad.findStronger(href, newRecentPads)) { console.log(href); return; } // If we have a weaker version, replace the href by the new one // NOTE: if that weaker version is in the trash, the strong one will be put in unsorted var weaker = Cryptpad.findWeaker(href, newRecentPads); @@ -176,7 +175,7 @@ define([ return; }); // Update the file in the drive - newFo.replaceHref(weaker, href); + newFo.replace(weaker, href); return; } // Here it means we have a new href, so we should add it to the drive at its old location diff --git a/www/common/otaml.js b/www/common/otaml.js deleted file mode 100644 index 3c5482847..000000000 --- a/www/common/otaml.js +++ /dev/null @@ -1,1004 +0,0 @@ -(function(){ -var r=function(){var e="function"==typeof require&&require,r=function(i,o,u){o||(o=0);var n=r.resolve(i,o),t=r.m[o][n];if(!t&&e){if(t=e(n))return t}else if(t&&t.c&&(o=t.c,n=t.m,t=r.m[o][t.m],!t))throw new Error('failed to require "'+n+'" from '+o);if(!t)throw new Error('failed to require "'+i+'" from '+u);return t.exports||(t.exports={},t.call(t.exports,t,t.exports,r.relative(n,o))),t.exports};return r.resolve=function(e,n){var i=e,t=e+".js",o=e+"/index.js";return r.m[n][t]&&t?t:r.m[n][o]&&o?o:i},r.relative=function(e,t){return function(n){if("."!=n.charAt(0))return r(n,t,e);var o=e.split("/"),f=n.split("/");o.pop();for(var i=0;i. - */ - -var Common = require('./Common'); -var HtmlParse = require('./HtmlParse'); -var Operation = require('./Operation'); -var Sha = require('./SHA256'); - -var makeTextOperation = module.exports.makeTextOperation = function(oldval, newval) -{ - if (oldval === newval) { return; } - - var begin = 0; - for (; oldval[begin] === newval[begin]; begin++) ; - - var end = 0; - for (var oldI = oldval.length, newI = newval.length; - oldval[--oldI] === newval[--newI]; - end++) ; - - if (end >= oldval.length - begin) { end = oldval.length - begin; } - if (end >= newval.length - begin) { end = newval.length - begin; } - - return { - offset: begin, - toRemove: oldval.length - begin - end, - toInsert: newval.slice(begin, newval.length - end), - }; -}; - -var VOID_TAG_REGEX = new RegExp('^(' + [ - 'area', - 'base', - 'br', - 'col', - 'hr', - 'img', - 'input', - 'link', - 'meta', - 'param', - 'command', - 'keygen', - 'source', -].join('|') + ')$'); - -// Get the offset of the previous open/close/void tag. -// returns the offset of the opening angle bracket. -var getPreviousTagIdx = function (data, idx) -{ - if (idx === 0) { return -1; } - idx = data.lastIndexOf('>', idx); - // The html tag from hell: - // < abc def="g" k='lm"nopw>"qrstu" - for (;;) { - var mch = data.substring(0,idx).match(/[<"'][^<'"]*$/); - if (!mch) { return -1; } - if (mch[0][0] === '<') { return mch.index; } - idx = data.lastIndexOf(mch[0][0], mch.index-1); - } -}; - -/** - * Get the name of an HTML tag with leading / if the tag is an end tag. - * - * @param data the html text - * @param offset the index of the < bracket. - * @return the tag name with possible leading slash. - */ -var getTagName = function (data, offset) -{ - if (data[offset] !== '<') { throw new Error(); } - // Match ugly tags like < / xxx> - // or < xxx y="z" > - var m = data.substring(offset).match(/^(<[\s\/]*)([a-zA-Z0-9_-]+)/); - if (!m) { throw new Error("could not get tag name"); } - if (m[1].indexOf('/') !== -1) { return '/'+m[2]; } - return m[2]; -}; - -/** - * Get the previous non-void opening tag. - * - * @param data the document html - * @param ctx an empty map for the first call, the same element thereafter. - * @return an array containing the offset of the open bracket for the begin tag and the - * the offset of the open bracket for the matching end tag. - */ -var getPreviousNonVoidTag = function (data, ctx) -{ - for (;;) { - if (typeof(ctx.offsets) === 'undefined') { - // ' ' is an invalid html element name so it will never match anything. - ctx.offsets = [ { idx: data.length, name: ' ' } ]; - ctx.idx = data.length; - } - - var prev = ctx.idx = getPreviousTagIdx(data, ctx.idx); - if (prev === -1) { - if (ctx.offsets.length > 1) { throw new Error(); } - return [ 0, data.length ]; - } - var prevTagName = getTagName(data, prev); - - if (prevTagName[0] === '/') { - ctx.offsets.push({ idx: prev, name: prevTagName.substring(1) }); - } else if (prevTagName === ctx.offsets[ctx.offsets.length-1].name) { - var os = ctx.offsets.pop(); - return [ prev, os.idx ]; - } else if (!VOID_TAG_REGEX.test(prevTagName)) { - throw new Error(); - } - } -}; - -var indexOfSkipQuoted = function (haystack, needle) -{ - var os = 0; - for (;;) { - var dqi = haystack.indexOf('"'); - var sqi = haystack.indexOf("'"); - var needlei = haystack.indexOf(needle); - if (needlei === -1) { return -1; } - if (dqi > -1 && dqi < sqi && dqi < needlei) { - dqi = haystack.indexOf('"', dqi+1); - if (dqi === -1) { throw new Error(); } - haystack = haystack.substring(dqi+1); - os += dqi+1; - } else if (sqi > -1 && sqi < needlei) { - sqi = haystack.indexOf('"', sqi+1); - if (sqi === -1) { throw new Error(); } - haystack = haystack.substring(sqi+1); - os += sqi+1; - } else { - return needlei + os; - } - } -}; - -var tagWidth = module.exports.tagWidth = function (nodeOuterHTML) -{ - if (nodeOuterHTML.length < 2 || nodeOuterHTML[1] === '!' || nodeOuterHTML[0] !== '<') { - return 0; - } - return indexOfSkipQuoted(nodeOuterHTML, '>') + 1; -}; - -var makeHTMLOperation = module.exports.makeHTMLOperation = function (oldval, newval) -{ - var op = makeTextOperation(oldval, newval); - if (!op) { return; } - - var end = op.offset + op.toRemove; - var lastTag; - var tag; - var ctx = {}; - do { - lastTag = tag; - tag = getPreviousNonVoidTag(oldval, ctx); - } while (tag[0] > op.offset || tag[1] < end); - - if (lastTag - && end < lastTag[0] - && op.offset > tag[0] + tagWidth(oldval.substring(tag[0]))) - { - // plain old text operation. - if (op.toRemove && oldval.substr(op.offset, op.toRemove).indexOf('<') !== -1) { - throw new Error(); - } - return op; - } - - op.offset = tag[0]; - op.toRemove = tag[1] - tag[0]; - op.toInsert = newval.slice(tag[0], newval.length - (oldval.length - tag[1])); - - return op; -}; - -/** - * Expand an operation to cover enough HTML that any naive transformation - * will result in correct HTML. - */ -var expandOp = module.exports.expandOp = function (html, op) { -return op; - if (Common.PARANOIA && typeof(html) !== 'string') { throw new Error(); } - var ctx = {}; - for (;;) { - var elem = HtmlParse.getPreviousElement(html, ctx); - // reached the end, this should not happen... - if (!elem) { throw new Error(JSON.stringify(op)); } - if (elem.openTagIndex <= op.offset) { - var endIndex = html.indexOf('>', elem.closeTagIndex) + 1; - if (!endIndex) { throw new Error(); } - if (endIndex >= op.offset + op.toRemove) { - var newHtml = Operation.apply(op, html); - var newEndIndex = endIndex - op.toRemove + op.toInsert.length; - var out = Operation.create(elem.openTagIndex, - endIndex - elem.openTagIndex, - newHtml.substring(elem.openTagIndex, newEndIndex)); - if (Common.PARANOIA) { - var test = Operation.apply(out, html); - if (test !== newHtml) { - throw new Error(test + '\n\n\n' + newHtml + '\n\n' + elem.openTagIndex + '\n\n' + newEndIndex); - } - if (out.toInsert[0] !== '<') { throw new Error(); } - if (out.toInsert[out.toInsert.length - 1] !== '>') { throw new Error(); } - } - return out; - } - } - //console.log(elem); - } -}; - -var transformB = function (html, toTransform, transformBy) { - - var transformByEndOffset = transformBy.offset + transformBy.toRemove; - if (toTransform.offset > transformByEndOffset) { - // simple rebase - toTransform.offset -= transformBy.toRemove; - toTransform.offset += transformBy.toInsert.length; - return toTransform; - } - - var toTransformEndOffset = toTransform.offset + toTransform.toRemove; - - if (transformBy.offset > toTransformEndOffset) { - // we're before them, no transformation needed. - return toTransform; - } - - // so we overlap, we're just going to revert one and apply the other. - // The one which affects more content should probably be applied. - var toRevert = toTransform; - var toApply = transformBy; - var swap = function () { - var x = toRevert; - toRevert = toApply; - toApply = x; - }; - - if (toTransform.toInsert.length > transformBy.toInsert.length) { - swap(); - } else if (toTransform.toInsert.length < transformBy.toInsert.length) { - // fall through - } else if (toTransform.toRemove > transformBy.toRemove) { - swap(); - } else if (toTransform.toRemove < transformBy.toRemove) { - // fall through - } else { - if (Operation.equals(toTransform, transformBy)) { return null; } - // tie-breaker: we just strcmp the JSON. - if (Common.strcmp(JSON.stringify(toTransform), JSON.stringify(transformBy)) < 0) { swap(); } - } - - var inverse = Operation.invert(toRevert, html); - if (Common.PARANOIA) { - var afterToRevert = Operation.apply(toRevert, html); - - } - if (Common.PARANOIA && !Operation.shouldMerge(inverse, toApply)) { throw new Error(); } - var out = Operation.merge(inverse, toApply); -}; - -// FIXME looks like the old transform is deprecated? figure out why -var transform = module.exports.transform = function (html, toTransform, transformBy) { - - return transformB(html, toTransform, transformBy); -/* - toTransform = Operation.clone(toTransform); - toTransform = expandOp(html, toTransform); - - transformBy = Operation.clone(transformBy); - transformBy = expandOp(html, transformBy); - - if (toTransform.offset >= transformBy.offset) { - if (toTransform.offset >= transformBy.offset + transformBy.toRemove) { - // simple rebase - toTransform.offset -= transformBy.toRemove; - toTransform.offset += transformBy.toInsert.length; - return toTransform; - } - - // They deleted our begin offset... - - var toTransformEndOffset = toTransform.offset + toTransform.toRemove; - var transformByEndOffset = transformBy.offset + transformBy.toRemove; - if (transformByEndOffset >= toTransformEndOffset) { - // They also deleted our end offset, lets forget we wrote anything because - // whatever it was, they deleted it's context. - return null; - } - - // goto the end, anything you deleted that they also deleted should be skipped. - var newOffset = transformBy.offset + transformBy.toInsert.length; - toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); - if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } - toTransform.offset = newOffset; - if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { - return null; - } - return toTransform; - } - if (toTransform.offset + toTransform.toRemove < transformBy.offset) { - return toTransform; - } - toTransform.toRemove = transformBy.offset - toTransform.offset; - if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { - return null; - } - return toTransform; -*/ -}; - -}, -"SHA256.js": function(module, exports, require){ -/* A JavaScript implementation of the Secure Hash Algorithm, SHA-256 - * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ - * Distributed under the BSD License - * Some bits taken from Paul Johnston's SHA-1 implementation - */ -(function () { - var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ - function safe_add (x, y) { - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - } - function S (X, n) {return ( X >>> n ) | (X << (32 - n));} - function R (X, n) {return ( X >>> n );} - function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} - function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} - function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} - function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} - function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} - function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} - function newArray (n) { - var a = []; - for (;n>0;n--) { - a.push(undefined); - } - return a; - } - function core_sha256 (m, l) { - var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2]; - var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; - var W = newArray(64); - var a, b, c, d, e, f, g, h, i, j; - var T1, T2; - /* append padding */ - m[l >> 5] |= 0x80 << (24 - l % 32); - m[((l + 64 >> 9) << 4) + 15] = l; - for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); - return bin; - } - function binb2hex (binarray) { - var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var str = ""; - for (var i = 0; i < binarray.length * 4; i++) { - str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + - hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); - } - return str; - } - function hex_sha256(s){ - return binb2hex(core_sha256(str2binb(s),s.length * chrsz)); - } - module.exports.hex_sha256 = hex_sha256; -}()); - -}, -"Common.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -var PARANOIA = module.exports.PARANOIA = false; - -/* throw errors over non-compliant messages which would otherwise be treated as invalid */ -var TESTING = module.exports.TESTING = true; - -var assert = module.exports.assert = function (expr) { - if (!expr) { throw new Error("Failed assertion"); } -}; - -var isUint = module.exports.isUint = function (integer) { - return (typeof(integer) === 'number') && - (Math.floor(integer) === integer) && - (integer >= 0); -}; - -var randomASCII = module.exports.randomASCII = function (length) { - var content = []; - for (var i = 0; i < length; i++) { - content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 ); - } - return content.join(''); -}; - -var strcmp = module.exports.strcmp = function (a, b) { - if (PARANOIA && typeof(a) !== 'string') { throw new Error(); } - if (PARANOIA && typeof(b) !== 'string') { throw new Error(); } - return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) ); -} - -}, -"Operation.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var Common = require('./Common'); - -var Operation = module.exports; - -var check = Operation.check = function (op, docLength_opt) { - Common.assert(op.type === 'Operation'); - Common.assert(Common.isUint(op.offset)); - Common.assert(Common.isUint(op.toRemove)); - Common.assert(typeof(op.toInsert) === 'string'); - Common.assert(op.toRemove > 0 || op.toInsert.length > 0); - Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt); -}; - -var create = Operation.create = function (offset, toRemove, toInsert) { - var out = { - type: 'Operation', - offset: offset || 0, - toRemove: toRemove || 0, - toInsert: toInsert || '', - }; - if (Common.PARANOIA) { check(out); } - return out; -}; - -var toObj = Operation.toObj = function (op) { - if (Common.PARANOIA) { check(op); } - return [op.offset,op.toRemove,op.toInsert]; -}; - -var fromObj = Operation.fromObj = function (obj) { - Common.assert(Array.isArray(obj) && obj.length === 3); - return create(obj[0], obj[1], obj[2]); -}; - -var clone = Operation.clone = function (op) { - return create(op.offset, op.toRemove, op.toInsert); -}; - -/** - * @param op the operation to apply. - * @param doc the content to apply the operation on - */ -var apply = Operation.apply = function (op, doc) -{ - if (Common.PARANOIA) { - check(op); - Common.assert(typeof(doc) === 'string'); - Common.assert(op.offset + op.toRemove <= doc.length); - } - return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove); -}; - -var invert = Operation.invert = function (op, doc) { - if (Common.PARANOIA) { - check(op); - Common.assert(typeof(doc) === 'string'); - Common.assert(op.offset + op.toRemove <= doc.length); - } - var rop = clone(op); - rop.toInsert = doc.substring(op.offset, op.offset + op.toRemove); - rop.toRemove = op.toInsert.length; - return rop; -}; - -var simplify = Operation.simplify = function (op, doc) { - if (Common.PARANOIA) { - check(op); - Common.assert(typeof(doc) === 'string'); - Common.assert(op.offset + op.toRemove <= doc.length); - } - var rop = invert(op, doc); - op = clone(op); - - var minLen = Math.min(op.toInsert.length, rop.toInsert.length); - var i; - for (i = 0; i < minLen && rop.toInsert[i] === op.toInsert[i]; i++) ; - op.offset += i; - op.toRemove -= i; - op.toInsert = op.toInsert.substring(i); - rop.toInsert = rop.toInsert.substring(i); - - if (rop.toInsert.length === op.toInsert.length) { - for (i = rop.toInsert.length-1; i >= 0 && rop.toInsert[i] === op.toInsert[i]; i--) ; - op.toInsert = op.toInsert.substring(0, i+1); - op.toRemove = i+1; - } - - if (op.toRemove === 0 && op.toInsert.length === 0) { return null; } - return op; -}; - -var equals = Operation.equals = function (opA, opB) { - return (opA.toRemove === opB.toRemove - && opA.toInsert === opB.toInsert - && opA.offset === opB.offset); -}; - -var lengthChange = Operation.lengthChange = function (op) -{ - if (Common.PARANOIA) { check(op); } - return op.toInsert.length - op.toRemove; -}; - -/* - * @return the merged operation OR null if the result of the merger is a noop. - */ -var merge = Operation.merge = function (oldOpOrig, newOpOrig) { - if (Common.PARANOIA) { - check(newOpOrig); - check(oldOpOrig); - } - - var newOp = clone(newOpOrig); - var oldOp = clone(oldOpOrig); - var offsetDiff = newOp.offset - oldOp.offset; - - if (newOp.toRemove > 0) { - var origOldInsert = oldOp.toInsert; - oldOp.toInsert = ( - oldOp.toInsert.substring(0,offsetDiff) - + oldOp.toInsert.substring(offsetDiff + newOp.toRemove) - ); - newOp.toRemove -= (origOldInsert.length - oldOp.toInsert.length); - if (newOp.toRemove < 0) { newOp.toRemove = 0; } - - oldOp.toRemove += newOp.toRemove; - newOp.toRemove = 0; - } - - if (offsetDiff < 0) { - oldOp.offset += offsetDiff; - oldOp.toInsert = newOp.toInsert + oldOp.toInsert; - - } else if (oldOp.toInsert.length === offsetDiff) { - oldOp.toInsert = oldOp.toInsert + newOp.toInsert; - - } else if (oldOp.toInsert.length > offsetDiff) { - oldOp.toInsert = ( - oldOp.toInsert.substring(0,offsetDiff) - + newOp.toInsert - + oldOp.toInsert.substring(offsetDiff) - ); - } else { - throw new Error("should never happen\n" + - JSON.stringify([oldOpOrig,newOpOrig], null, ' ')); - } - - if (oldOp.toInsert === '' && oldOp.toRemove === 0) { - return null; - } - if (Common.PARANOIA) { check(oldOp); } - - return oldOp; -}; - -/** - * If the new operation deletes what the old op inserted or inserts content in the middle of - * the old op's content or if they abbut one another, they should be merged. - */ -var shouldMerge = Operation.shouldMerge = function (oldOp, newOp) { - if (Common.PARANOIA) { - check(oldOp); - check(newOp); - } - if (newOp.offset < oldOp.offset) { - return (oldOp.offset <= (newOp.offset + newOp.toRemove)); - } else { - return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length)); - } -}; - -/** - * Rebase newOp against oldOp. - * - * @param oldOp the eariler operation to have happened. - * @param newOp the later operation to have happened (in time). - * @return either the untouched newOp if it need not be rebased, - * the rebased clone of newOp if it needs rebasing, or - * null if newOp and oldOp must be merged. - */ -var rebase = Operation.rebase = function (oldOp, newOp) { - if (Common.PARANOIA) { - check(oldOp); - check(newOp); - } - if (newOp.offset < oldOp.offset) { return newOp; } - newOp = clone(newOp); - newOp.offset += oldOp.toRemove; - newOp.offset -= oldOp.toInsert.length; - return newOp; -}; - -/** - * this is a lossy and dirty algorithm, everything else is nice but transformation - * has to be lossy because both operations have the same base and they diverge. - * This could be made nicer and/or tailored to a specific data type. - * - * @param toTransform the operation which is converted *MUTATED*. - * @param transformBy an existing operation which also has the same base. - * @return toTransform *or* null if the result is a no-op. - */ -var transform0 = Operation.transform0 = function (text, toTransform, transformBy) { - if (toTransform.offset > transformBy.offset) { - if (toTransform.offset > transformBy.offset + transformBy.toRemove) { - // simple rebase - toTransform.offset -= transformBy.toRemove; - toTransform.offset += transformBy.toInsert.length; - return toTransform; - } - // goto the end, anything you deleted that they also deleted should be skipped. - var newOffset = transformBy.offset + transformBy.toInsert.length; - toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); - if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } - toTransform.offset = newOffset; - if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { - return null; - } - return toTransform; - } - if (toTransform.offset + toTransform.toRemove < transformBy.offset) { - return toTransform; - } - toTransform.toRemove = transformBy.offset - toTransform.offset; - if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { - return null; - } - return toTransform; -}; - -/** - * @param toTransform the operation which is converted - * @param transformBy an existing operation which also has the same base. - * @return a modified clone of toTransform *or* toTransform itself if no change was made. - */ -var transform = Operation.transform = function (text, toTransform, transformBy, transformFunction) { - if (Common.PARANOIA) { - check(toTransform); - check(transformBy); - } - transformFunction = transformFunction || transform0; - toTransform = clone(toTransform); - var result = transformFunction(text, toTransform, transformBy); - if (Common.PARANOIA && result) { check(result); } - return result; -}; - -/** Used for testing. */ -var random = Operation.random = function (docLength) { - Common.assert(Common.isUint(docLength)); - var offset = Math.floor(Math.random() * 100000000 % docLength) || 0; - var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0; - var toInsert = ''; - do { - var toInsert = Common.randomASCII(Math.floor(Math.random() * 20)); - } while (toRemove === 0 && toInsert === ''); - return create(offset, toRemove, toInsert); -}; - -}, -"HtmlParse.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var VOID_TAG_REGEX = module.exports.VOID_TAG_REGEX = new RegExp('^(' + [ - 'area', - 'base', - 'br', - 'col', - 'hr', - 'img', - 'input', - 'link', - 'meta', - 'param', - 'command', - 'keygen', - 'source', -].join('|') + ')$'); - -/** - * Get the offset of the previous open/close/void tag. - * returns the offset of the opening angle bracket. - */ -var getPreviousTagIdx = module.exports.getPreviousTagIdx = function (data, idx) { - if (idx === 0) { return -1; } - idx = data.lastIndexOf('>', idx); - // The html tag from hell: - // < abc def="g" k='lm"nopw>"qrstu" - for (;;) { - var mch = data.substring(0,idx).match(/[<"'][^<'"]*$/); - if (!mch) { return -1; } - if (mch[0][0] === '<') { return mch.index; } - idx = data.lastIndexOf(mch[0][0], mch.index-1); - } -}; - -/** - * Get the name of an HTML tag with leading / if the tag is an end tag. - * - * @param data the html text - * @param offset the index of the < bracket. - * @return the tag name with possible leading slash. - */ -var getTagName = module.exports.getTagName = function (data, offset) { - if (data[offset] !== '<') { throw new Error(); } - // Match ugly tags like < / xxx> - // or < xxx y="z" > - var m = data.substring(offset).match(/^(<[\s\/]*)([a-zA-Z0-9_-]+)/); - if (!m) { throw new Error("could not get tag name"); } - if (m[1].indexOf('/') !== -1) { return '/'+m[2]; } - return m[2]; -}; - -/** - * Get the previous void or opening tag. - * - * @param data the document html - * @param ctx an empty map for the first call, the same element thereafter. - * @return an object containing openTagIndex: the offset of the < bracket for the begin tag, - * closeTagIndex: the the offset of the < bracket for the matching end tag, and - * nodeName: the element name. - * If the element is a void element, the second value in the array will be -1. - */ -var getPreviousElement = module.exports.getPreviousElement = function (data, ctx) { - for (;;) { - if (typeof(ctx.offsets) === 'undefined') { - // ' ' is an invalid html element name so it will never match anything. - ctx.offsets = [ { idx: data.length, name: ' ' } ]; - ctx.idx = data.length; - } - - var prev = ctx.idx = getPreviousTagIdx(data, ctx.idx); - if (prev === -1) { - if (ctx.offsets.length > 1) { throw new Error(); } - return null; - } - var prevTagName = getTagName(data, prev); - - if (prevTagName[0] === '/') { - ctx.offsets.push({ idx: prev, name: prevTagName.substring(1) }); - } else if (prevTagName === ctx.offsets[ctx.offsets.length-1].name) { - var os = ctx.offsets.pop(); - return { openTagIndex: prev, closeTagIndex: os.idx, nodeName: prevTagName }; - } else if (!VOID_TAG_REGEX.test(prevTagName)) { - throw new Error("unmatched tag [" + prevTagName + "] which is not a void tag"); - } else { - return { openTagIndex: prev, closeTagIndex: -1, nodeName: prevTagName }; - } - } -}; - -/** - * Given a piece of HTML text which begins at the < of a non-close tag, - * give the index within that content which contains the matching > - * character skipping > characters contained within attributes. - */ -var getEndOfTag = module.exports.getEndOfTag = function (html) { - var arr = html.match(/['">][^"'>]*/g); - var q = null; - var idx = html.indexOf(arr[0]); - for (var i = 0; i < arr.length; i++) { - if (!q) { - q = arr[i][0]; - if (q === '>') { return idx; } - } else if (q === arr[i][0]) { - q = null; - } - idx += arr[i].length; - } - throw new Error("Could not find end of tag"); -}; - - -var ParseTagState = { - OUTSIDE: 0, - NAME: 1, - VALUE: 2, - SQUOTE: 3, - DQUOTE: 4, -}; - -var parseTag = module.exports.parseTag = function (html) { - if (html[0] !== '<') { throw new Error("Must be the beginning of a tag"); } - - var out = { - nodeName: null, - attributes: [], - endIndex: -1, - trailingSlash: false - }; - - if (html.indexOf('>') < html.indexOf(' ') || html.indexOf(' ') === -1) { - out.endIndex = html.indexOf('>'); - out.nodeName = html.substring(1, out.endIndex); - return out; - } - - out.nodeName = html.substring(1, html.indexOf(' ')); - - if (html.indexOf('<' + out.nodeName + ' ') !== 0) { - throw new Error("Nonstandard beginning of tag [" + - html.substring(0, 30) + '] for nodeName [' + out.nodeName + ']'); - } - var i = 1 + out.nodeName.length + 1; - - var state = ParseTagState.OUTSIDE; - var name = []; - var value = []; - var pushAttribute = function () { - out.attributes.push([name.join(''), value.join('')]); - name = []; - value = []; - }; - for (; i < html.length; i++) { - var chr = html[i]; - switch (state) { - case ParseTagState.OUTSIDE: { - if (chr === '/') { - out.trailingSlash = true; - } else if (chr.match(/[a-zA-Z0-9_-]/)) { - state = ParseTagState.NAME; - if (name.length > 0) { throw new Error(); } - name.push(chr); - } else if (chr === '>') { - out.endIndex = i; - return out; - } else if (chr === ' ') { - // fall through - } else { - throw new Error(); - } - continue; - } - case ParseTagState.NAME: { - if (chr.match(/[a-zA-Z0-9_-]/)) { - name.push(chr); - } else if (chr === '=') { - state = ParseTagState.VALUE; - } else if (chr === '/' || chr === ' ') { - if (chr === '/') { - out.trailingSlash = true; - } - out.attributes.push([name.join(''), null]); - name = []; - state = ParseTagState.OUTSIDE; - } else if (chr === '>') { - out.attributes.push([name.join(''), null]); - name = []; - out.endIndex = i; - return out; - } else { - throw new Error("bad character [" + chr + "] in name [" + name.join('') + "]"); - } - continue; - } - case ParseTagState.VALUE: { - value.push(chr); - if (chr === '"') { - state = ParseTagState.DQUOTE; - } else if (chr === "'") { - state = ParseTagState.SQUOTE; - } else { - throw new Error(); - } - continue; - } - case ParseTagState.SQUOTE: { - value.push(chr); - if (chr === "'") { - pushAttribute(); - state = ParseTagState.OUTSIDE; - } - continue; - } - case ParseTagState.DQUOTE: { - value.push(chr); - if (chr === '"') { - pushAttribute(); - state = ParseTagState.OUTSIDE; - } - continue; - } - } - } - - throw new Error("reached end of file while parsing"); -}; - -var serializeTag = module.exports.serializeTag = function (tag) { - var out = ['<', tag.nodeName]; - for (var i = 0; i < tag.attributes.length; i++) { - var att = tag.attributes[i]; - if (att[1] === null) { - out.push(' ', att[0]); - } else { - out.push(' ', att[0], '=', att[1]); - } - } - if (tag.trailingSlash) { - out.push(' /'); - } - out.push('>'); - return out.join(''); -}; - -} -}; -Otaml = r("Otaml.js");}()); diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 78ae607db..a9467cff9 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -1,17 +1,29 @@ define([ '/common/rpc.js', - '/bower_components/tweetnacl/nacl-fast.min.js' ], function (Rpc) { - var Nacl = window.nacl; - var create = function (network, proxy, cb) { - if (!network) { return void cb('INVALID_NETWORK'); } - if (!proxy) { return void cb('INVALID_PROXY'); } + if (!network) { + window.setTimeout(function () { + cb('INVALID_NETWORK'); + }); + return; + } + if (!proxy) { + window.setTimeout(function () { + cb('INVALID_PROXY'); + }); + return; + } var edPrivate = proxy.edPrivate; var edPublic = proxy.edPublic; - if (!(edPrivate && edPublic)) { return void cb('INVALID_KEYS'); } + if (!(edPrivate && edPublic)) { + window.setTimeout(function () { + cb('INVALID_KEYS'); + }); + return; + } Rpc.create(network, edPrivate, edPublic, function (e, rpc) { if (e) { return void cb(e); } @@ -26,21 +38,26 @@ define([ // you can ask the server to pin a particular channel for you exp.pin = function (channels, cb) { + if (!Array.isArray(channels)) { + window.setTimeout(function () { + cb('[TypeError] pin expects an array'); + }); + return; + } rpc.send('PIN', channels, cb); }; // you can also ask to unpin a particular channel exp.unpin = function (channels, cb) { + if (!Array.isArray(channels)) { + window.setTimeout(function () { + cb('[TypeError] pin expects an array'); + }); + return; + } rpc.send('UNPIN', channels, cb); }; - // This implementation must match that on the server - // it's used for a checksum - exp.hashChannelList = function (list) { - return Nacl.util.encodeBase64(Nacl.hash(Nacl.util - .decodeUTF8(JSON.stringify(list)))); - }; - // ask the server what it thinks your hash is exp.getServerHash = function (cb) { rpc.send('GET_HASH', edPublic, function (e, hash) { @@ -52,8 +69,14 @@ define([ }; // if local and remote hashes don't match, send a reset - exp.reset = function (list, cb) { - rpc.send('RESET', list, function (e, response) { + exp.reset = function (channels, cb) { + if (!Array.isArray(channels)) { + window.setTimeout(function () { + cb('[TypeError] pin expects an array'); + }); + return; + } + rpc.send('RESET', channels, function (e, response) { cb(e, response[0]); }); }; @@ -66,7 +89,12 @@ define([ // get the combined size of all channels (in bytes) for all the // channels which the server has pinned for your publicKey exp.getFileListSize = function (cb) { - rpc.send('GET_TOTAL_SIZE', undefined, cb); + rpc.send('GET_TOTAL_SIZE', undefined, function (e, response) { + if (e) { return void cb(e); } + if (response && response.length) { + cb(void 0, response[0]); + } + }); }; cb(e, exp); diff --git a/www/common/rainbow.js b/www/common/rainbow.js deleted file mode 100644 index 6aa732c4e..000000000 --- a/www/common/rainbow.js +++ /dev/null @@ -1,29 +0,0 @@ -define([], function () { - return function (n) { - n = n || 24; // default is 24 colours - var r = 0.6, - i = 0, - t = [], - rgb = [0,2,4]; - - while(i= 0) { - // check from back to front - - // check the node's children (depth first) - // if the predicate tests true, return true - if (tree.some(root.children[last], predicate)) { - return true; - } // otherwise none of the nodes inside it matched. - - // check the node itself - if (predicate(root.children[last], last)) { - return true; - } - last--; - } - return false; - }; - - // FIXME this isn't being used - var someText = tree.someIncludingText = function (root, predicate) { - // take the index of the last element in the current root - var last = root.childNodes.length - 1; - - // it might be a leaf node - if (last < 0) { return false; } - - // otherwise it has children - while (last >= 0) { - // check from back to front - - // check the node's children (depth first) - // if the predicate tests true, return true - if (tree.someIncludingText(root.childNodes[last], predicate)) { - return true; - } // otherwise none of the nodes inside it matched. - - // check the node itself - if (predicate(root.childNodes[last], last)) { - return true; - } - last--; - } - return false; - }; - - // FIXME not being used - tree.findSameHierarchy = function (list, ancestor) { - var i = 0; - var success = true; - var last = list.length - 1; - var el; - - tree.someIncludingText(ancestor, function (e) { - // don't out of bounds - if (i > last) { - // unsuccessful - success = false; - return true; - } - - if (list[i] === (e.tagName||e.nodeName)) { - - if (i === last) { - el = e; - return true; - } - i++; - } else { - // hierarchy has changed, what should we do? - success = false; - return true; // terminate - } - }); - return success? el: false; - }; - var indexOfNode = tree.indexOfNode = function (el) { if (!(el && el.parentNode)) { console.log("No parentNode found!"); @@ -107,13 +22,6 @@ define([], function () { return el.childNodes.length; }; - var parentsOf = tree.parentsOf = function (el, root) { - var P = []; - var p = el; - while (p !== root) { P.push((p = p.parentNode)); } - return P; - }; - /* rightmost and leftmost return the deepest right and left leaf nodes of a tree */ diff --git a/www/common/userObject.js b/www/common/userObject.js new file mode 100644 index 000000000..6a70a3622 --- /dev/null +++ b/www/common/userObject.js @@ -0,0 +1,898 @@ +define([ + 'jquery', +], function ($) { + var module = {}; + + var ROOT = module.ROOT = "root"; + var UNSORTED = module.UNSORTED = "unsorted"; + var TRASH = module.TRASH = "trash"; + var TEMPLATE = module.TEMPLATE = "template"; + + var init = module.init = function (files, config) { + var exp = {}; + var Cryptpad = config.Cryptpad; + var Messages = Cryptpad.Messages; + + var FILES_DATA = module.FILES_DATA = exp.FILES_DATA = Cryptpad.storageKey; + var NEW_FOLDER_NAME = Messages.fm_newFolder; + var NEW_FILE_NAME = Messages.fm_newFile; + + // Logging + var DEBUG = config.DEBUG || false; + var logging = function () { + console.log.apply(console, arguments); + }; + var log = config.log || logging; + var logError = config.logError || logging; + var debug = config.debug || logging; + var error = exp.error = function() { + exp.fixFiles(); + console.error.apply(console, arguments); + }; + + // TODO: workgroup + var workgroup = config.workgroup; + + + /* + * UTILS + */ + + var getStructure = exp.getStructure = function () { + var a = {}; + a[ROOT] = {}; + a[UNSORTED] = []; + a[TRASH] = {}; + a[FILES_DATA] = []; + a[TEMPLATE] = []; + return a; + }; + var getHrefArray = function () { + return [UNSORTED, TEMPLATE]; + }; + + + var compareFiles = function (fileA, fileB) { return fileA === fileB; }; + + var isFile = exp.isFile = function (element) { + return typeof(element) === "string"; + }; + + var isReadOnlyFile = exp.isReadOnlyFile = function (element) { + if (!isFile(element)) { return false; } + var parsed = Cryptpad.parsePadUrl(element); + if (!parsed) { return false; } + var hash = parsed.hash; + var pHash = Cryptpad.parseHash(hash); + if (pHash && !pHash.mode) { return; } + return pHash && pHash.mode === 'view'; + }; + + var isFolder = exp.isFolder = function (element) { + return typeof(element) !== "string"; + }; + var isFolderEmpty = exp.isFolderEmpty = function (element) { + if (typeof(element) !== "object") { return false; } + return Object.keys(element).length === 0; + }; + + var hasSubfolder = exp.hasSubfolder = function (element, trashRoot) { + if (typeof(element) !== "object") { return false; } + var subfolder = 0; + var addSubfolder = function (el, idx) { + subfolder += isFolder(el.element) ? 1 : 0; + }; + for (var f in element) { + if (trashRoot) { + if ($.isArray(element[f])) { + element[f].forEach(addSubfolder); + } + } else { + subfolder += isFolder(element[f]) ? 1 : 0; + } + } + return subfolder; + }; + + var hasFile = exp.hasFile = function (element, trashRoot) { + if (typeof(element) !== "object") { return false; } + var file = 0; + var addFile = function (el, idx) { + file += isFile(el.element) ? 1 : 0; + }; + for (var f in element) { + if (trashRoot) { + if ($.isArray(element[f])) { + element[f].forEach(addFile); + } + } else { + file += isFile(element[f]) ? 1 : 0; + } + } + return file; + }; + + // Get data from AllFiles (Cryptpad_RECENTPADS) + var getFileData = exp.getFileData = function (file) { + if (!file) { return; } + var res; + files[FILES_DATA].some(function(arr) { + var href = arr.href; + if (href === file) { + res = arr; + return true; + } + return false; + }); + return res; + }; + + // Data from filesData + var getTitle = exp.getTitle = function (href) { + if (workgroup) { debug("No titles in workgroups"); return; } + var data = getFileData(href); + if (!href || !data) { + error("getTitle called with a non-existing href: ", href); + return; + } + return data.title; + }; + + + // PATHS + + var comparePath = exp.comparePath = function (a, b) { + if (!a || !b || !$.isArray(a) || !$.isArray(b)) { return false; } + if (a.length !== b.length) { return false; } + var result = true; + var i = a.length - 1; + while (result && i >= 0) { + result = a[i] === b[i]; + i--; + } + return result; + }; + + var isSubpath = exp.isSubpath = function (path, parentPath) { + var pathA = parentPath.slice(); + var pathB = path.slice(0, pathA.length); + return comparePath(pathA, pathB); + }; + + var isPathIn = exp.isPathIn = function (path, categories) { + if (!categories) { return; } + var idx = categories.indexOf('hrefArray'); + if (idx !== -1) { + categories.splice(idx, 1); + categories = categories.concat(getHrefArray()); + } + return categories.some(function (c) { + return Array.isArray(path) && path[0] === c; + }); + }; + + var isInTrashRoot = exp.isInTrashRoot = function (path) { + return path[0] === TRASH && path.length === 4; + }; + + + // FIND + + var findElement = function (root, pathInput) { + if (!pathInput) { + error("Invalid path:\n", pathInput, "\nin root\n", root); + return; + } + if (pathInput.length === 0) { return root; } + var path = pathInput.slice(); + var key = path.shift(); + if (typeof root[key] === "undefined") { + debug("Unable to find the key '" + key + "' in the root object provided:", root); + return; + } + return findElement(root[key], path); + }; + + var find = exp.find = function (path) { + return findElement(files, path); + }; + + + // GET FILES + + var getFilesRecursively = function (root, arr) { + for (var e in root) { + if (isFile(root[e])) { + if(arr.indexOf(root[e]) === -1) { arr.push(root[e]); } + } else { + getFilesRecursively(root[e], arr); + } + } + }; + var _getFiles = {}; + _getFiles['array'] = function (cat) { + if (!files[cat]) { files[cat] = []; } + return files[cat].slice(); + }; + getHrefArray().forEach(function (c) { + _getFiles[c] = function () { return _getFiles['array'](c); }; + }); + _getFiles['hrefArray'] = function () { + var ret = []; + getHrefArray().forEach(function (c) { + ret = ret.concat(_getFiles[c]()); + }); + return Cryptpad.deduplicateString(ret); + }; + _getFiles[ROOT] = function () { + var ret = []; + getFilesRecursively(files[ROOT], ret); + return ret; + }; + _getFiles[TRASH] = function () { + var root = files[TRASH]; + var ret = []; + var addFiles = function (el, idx) { + if (isFile(el.element)) { + if(ret.indexOf(el.element) === -1) { ret.push(el.element); } + } else { + getFilesRecursively(el.element, ret); + } + }; + for (var e in root) { + if (!$.isArray(root[e])) { + error("Trash contains a non-array element"); + return; + } + root[e].forEach(addFiles); + } + return ret; + }; + _getFiles[FILES_DATA] = function () { + var ret = []; + files[FILES_DATA].forEach(function (el) { + if (el.href && ret.indexOf(el.href) === -1) { + ret.push(el.href); + } + }); + return ret; + }; + var getFiles = exp.getFiles = function (categories) { + var ret = []; + if (!categories || !categories.length) { + categories = [ROOT, 'hrefArray', TRASH, FILES_DATA]; + } + categories.forEach(function (c) { + if (typeof _getFiles[c] === "function") { + ret = ret.concat(_getFiles[c]()); + } + }); + return Cryptpad.deduplicateString(ret); + }; + + // SEARCH + var _findFileInRoot = function (path, href) { + if (!isPathIn(path, [ROOT, TRASH])) { return []; } + var paths = []; + var root = find(path); + var addPaths = function (p) { + if (paths.indexOf(p) === -1) { + paths.push(p); + } + }; + + if (isFile(root)) { + if (compareFiles(href, root)) { + if (paths.indexOf(path) === -1) { + paths.push(path); + } + } + return paths; + } + for (var e in root) { + var nPath = path.slice(); + nPath.push(e); + _findFileInRoot(nPath, href).forEach(addPaths); + } + + return paths; + }; + var _findFileInHrefArray = function (rootName, href) { + var unsorted = files[rootName].slice(); + var ret = []; + var i = -1; + while ((i = unsorted.indexOf(href, i+1)) !== -1){ + ret.push([rootName, i]); + } + return ret; + }; + var _findFileInTrash = function (path, href) { + var root = find(path); + var paths = []; + var addPaths = function (p) { + if (paths.indexOf(p) === -1) { + paths.push(p); + } + }; + if (path.length === 1 && typeof(root) === 'object') { + Object.keys(root).forEach(function (key) { + var arr = root[key]; + if (!Array.isArray(arr)) { return; } + var nPath = path.slice(); + nPath.push(key); + _findFileInTrash(nPath, href).forEach(addPaths); + }); + } + if (path.length === 2) { + if (!Array.isArray(root)) { return []; } + root.forEach(function (el, i) { + var nPath = path.slice(); + nPath.push(i); + nPath.push('element'); + if (isFile(el.element)) { + if (compareFiles(href, el.element)) { + addPaths(nPath); + } + return; + } + _findFileInTrash(nPath, href).forEach(addPaths); + }); + } + if (path.length >= 4) { + _findFileInRoot(path, href).forEach(addPaths); + } + return paths; + }; + var findFile = exp.findFile = function (href) { + var rootpaths = _findFileInRoot([ROOT], href); + var unsortedpaths = _findFileInHrefArray(UNSORTED, href); + var templatepaths = _findFileInHrefArray(TEMPLATE, href); + var trashpaths = _findFileInTrash([TRASH], href); + return rootpaths.concat(unsortedpaths, templatepaths, trashpaths); + }; + var search = exp.search = function (value) { + if (typeof(value) !== "string") { return []; } + var res = []; + // Search in ROOT + var findIn = function (root) { + Object.keys(root).forEach(function (k) { + if (isFile(root[k])) { + if (k.toLowerCase().indexOf(value.toLowerCase()) !== -1) { + res.push(root[k]); + } + return; + } + findIn(root[k]); + }); + }; + findIn(files[ROOT]); + // Search in TRASH + var trash = files[TRASH]; + Object.keys(trash).forEach(function (k) { + if (k.toLowerCase().indexOf(value.toLowerCase()) !== -1) { + trash[k].forEach(function (el) { + if (isFile(el.element)) { + res.push(el.element); + } + }); + } + trash[k].forEach(function (el) { + if (isFolder(el.element)) { + findIn(el.element); + } + }); + }); + + // Search title + var allFilesList = files[FILES_DATA].slice(); + allFilesList.forEach(function (t) { + if (t.title && t.title.toLowerCase().indexOf(value.toLowerCase()) !== -1) { + res.push(t.href); + } + }); + + // Search Href + var href = Cryptpad.getRelativeHref(value); + if (href) { + res.push(href); + } + + res = Cryptpad.deduplicateString(res); + + var ret = []; + res.forEach(function (l) { + var paths = findFile(l); + ret.push({ + paths: findFile(l), + data: exp.getFileData(l) + }); + }); + return ret; + }; + + /** + * OPERATIONS + */ + + var getAvailableName = function (parentEl, name) { + if (typeof(parentEl[name]) === "undefined") { return name; } + var newName = name; + var i = 1; + while (typeof(parentEl[newName]) !== "undefined") { + newName = name + "_" + i; + i++; + } + return newName; + }; + + // FILES DATA + var pushFileData = exp.pushData = function (data) { + Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { + if (e) { console.log(e); return; } + console.log(hash); + }); + files[FILES_DATA].push(data); + }; + var spliceFileData = exp.removeData = function (idx) { + var data = files[FILES_DATA][idx]; + if (typeof data === "object") { + Cryptpad.unpinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { + if (e) { console.log(e); return; } + console.log(hash); + }); + } + files[FILES_DATA].splice(idx, 1); + }; + + // MOVE + var pushToTrash = function (name, element, path) { + var trash = files[TRASH]; + if (typeof(trash[name]) === "undefined") { trash[name] = []; } + var trashArray = trash[name]; + var trashElement = { + element: element, + path: path + }; + trashArray.push(trashElement); + }; + var copyElement = function (elementPath, newParentPath) { + if (comparePath(elementPath, newParentPath)) { return; } // Nothing to do... + var element = find(elementPath); + var newParent = find(newParentPath); + + // Never move a folder in one of its children + if (isSubpath(newParentPath, elementPath)) { + log(Messages.fo_moveFolderToChildError); + return; + } + + // Move to Trash + if (isPathIn(newParentPath, [TRASH])) { + if (!elementPath || elementPath.length < 2 || elementPath[0] === TRASH) { + debug("Can't move an element from the trash to the trash: ", elementPath); + return; + } + var key = elementPath[elementPath.length - 1]; + var elName = isPathIn(elementPath, ['hrefArray']) ? getTitle(element) : key; + var parentPath = elementPath.slice(); + parentPath.pop(); + pushToTrash(elName, element, parentPath); + return true; + } + // Move to hrefArray + if (isPathIn(newParentPath, ['hrefArray'])) { + if (isFolder(element)) { + log(Messages.fo_moveUnsortedError); + return; + } else { + if (elementPath[0] === newParentPath[0]) { return; } + var fileRoot = newParentPath[0]; + if (files[fileRoot].indexOf(element) === -1) { + files[fileRoot].push(element); + } + return true; + } + } + // Move to root + var name; + if (isPathIn(elementPath, ['hrefArray'])) { + name = getTitle(element); + } else if (isInTrashRoot(elementPath)) { + // Element from the trash root: elementPath = [TRASH, "{dirName}", 0, 'element'] + name = elementPath[1]; + } else { + name = elementPath[elementPath.length-1]; + } + var newName = !isPathIn(elementPath, [ROOT]) ? getAvailableName(newParent, name) : name; + + if (typeof(newParent[newName]) !== "undefined") { + log(Messages.fo_unavailableName); + return; + } + newParent[newName] = element; + return true; + }; + var move = exp.move = function (paths, newPath, cb) { + // Copy the elements to their new location + var toRemove = []; + paths.forEach(function (p) { + var parentPath = p.slice(); + parentPath.pop(); + if (comparePath(parentPath, newPath)) { return; } + copyElement(p, newPath); + toRemove.push(p); + }); + exp.delete(toRemove, cb); + }; + var restore = exp.restore = function (path, cb) { + if (!isInTrashRoot(path)) { return; } + var parentPath = path.slice(); + parentPath.pop(); + var oldPath = find(parentPath).path; + move([path], oldPath, cb); + }; + + + // ADD + var add = exp.add = function (href, path, name, cb) { + if (!href) { return; } + var newPath = path, parentEl; + if (path && !Array.isArray(path)) { + newPath = decodeURIComponent(path).split(','); + } + // Add to href array + if (path && isPathIn(newPath, ['hrefArray'])) { + parentEl = find(newPath); + parentEl.push(href); + return; + } + // Add to root + if (path && isPathIn(newPath, [ROOT]) && name) { + parentEl = find(newPath); + if (parentEl) { + var newName = getAvailableName(parentEl, name); + parentEl[newName] = href; + return; + } + } + // No path: push to unsorted + var filesList = getFiles([ROOT, TRASH, 'hrefArray']); + if (filesList.indexOf(href) === -1) { files[UNSORTED].push(href); } + + if (typeof cb === "function") { cb(); } + }; + var addFile = exp.addFile = function (filePath, name, type, cb) { + var parentEl = findElement(files, filePath); + var fileName = getAvailableName(parentEl, name || NEW_FILE_NAME); + var href = '/' + type + '/#' + Cryptpad.createRandomHash(); + parentEl[fileName] = href; + + pushFileData({ + href: href, + title: fileName, + atime: +new Date(), + ctime: +new Date() + }); + + var newPath = filePath.slice(); + newPath.push(fileName); + cb({ + newPath: newPath + }); + }; + var addFolder = exp.addFolder = function (folderPath, name, cb) { + var parentEl = find(folderPath); + var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME); + parentEl[folderName] = {}; + var newPath = folderPath.slice(); + newPath.push(folderName); + cb({ + newPath: newPath + }); + }; + + // FORGET (move with href not path) + var forget = exp.forget = function (href) { + var paths = findFile(href); + move(paths, [TRASH]); + }; + + // DELETE + // Permanently delete multiple files at once using a list of paths + // NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template) + var removePadAttribute = function (f) { + Object.keys(files).forEach(function (key) { + var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null; + if (hash && key.indexOf(hash) === 0) { + debug("Deleting pad attribute in the realtime object"); + files[key] = undefined; + delete files[key]; + } + }); + }; + var checkDeletedFiles = function () { + // Nothing in FILES_DATA for workgroups + if (workgroup) { return; } + + var filesList = getFiles([ROOT, 'hrefArray', TRASH]); + var toRemove = []; + files[FILES_DATA].forEach(function (arr) { + var f = arr.href; + if (filesList.indexOf(f) === -1) { + toRemove.push(arr); + } + }); + toRemove.forEach(function (f) { + var idx = files[FILES_DATA].indexOf(f); + if (idx !== -1) { + debug("Removing", f, "from filesData"); + spliceFileData(idx); + removePadAttribute(f.href); + } + }); + }; + var deleteHrefs = function (hrefs) { + hrefs.forEach(function (obj) { + var idx = files[obj.root].indexOf(obj.href); + files[obj.root].splice(idx, 1); + }); + }; + var deleteMultipleTrashRoot = function (roots) { + roots.forEach(function (obj) { + var idx = files[TRASH][obj.name].indexOf(obj.el); + files[TRASH][obj.name].splice(idx, 1); + }); + }; + var deleteMultiplePermanently = function (paths, nocheck) { + var hrefPaths = paths.filter(function(x) { return isPathIn(x, ['hrefArray']); }); + var rootPaths = paths.filter(function(x) { return isPathIn(x, [ROOT]); }); + var trashPaths = paths.filter(function(x) { return isPathIn(x, [TRASH]); }); + + var hrefs = []; + hrefPaths.forEach(function (path) { + var href = find(path); + hrefs.push({ + root: path[0], + href: href + }); + }); + deleteHrefs(hrefs); + + rootPaths.forEach(function (path) { + var parentPath = path.slice(); + var key = parentPath.pop(); + var parentEl = find(parentPath); + parentEl[key] = undefined; + delete parentEl[key]; + }); + + var trashRoot = []; + trashPaths.forEach(function (path) { + var parentPath = path.slice(); + var key = parentPath.pop(); + var parentEl = find(parentPath); + // Trash root: we have array here, we can't just splice with the path otherwise we might break the path + // of another element in the loop + if (path.length === 4) { + trashRoot.push({ + name: path[1], + el: parentEl + }); + return; + } + // Trash but not root: it's just a tree so remove the key + parentEl[key] = undefined; + delete parentEl[key]; + }); + deleteMultipleTrashRoot(trashRoot); + + // In some cases, we want to remove pads from a location without removing them from + // FILES_DATA (replaceHref) + if (!nocheck) { checkDeletedFiles(); } + }; + var deletePath = exp.delete = function (paths, cb, nocheck) { + deleteMultiplePermanently(paths, nocheck); + if (typeof cb === "function") { cb(); } + }; + var emptyTrash = exp.emptyTrash = function (cb) { + files[TRASH] = {}; + checkDeletedFiles(); + if(cb) { cb(); } + }; + + // RENAME + var rename = exp.rename = function (path, newName, cb) { + if (path.length <= 1) { + logError('Renaming `root` is forbidden'); + return; + } + if (!newName || newName.trim() === "") { return; } + // Copy the element path and remove the last value to have the parent path and the old name + var element = find(path); + var parentPath = path.slice(); + var oldName = parentPath.pop(); + if (oldName === newName) { + return; + } + var parentEl = find(parentPath); + if (typeof(parentEl[newName]) !== "undefined") { + log(Messages.fo_existingNameError); + return; + } + parentEl[newName] = element; + parentEl[oldName] = undefined; + delete parentEl[oldName]; + cb(); + }; + + // REPLACE + var replaceFile = function (path, o, n) { + var root = find(path); + + if (isFile(root)) { return; } + for (var e in root) { + if (isFile(root[e])) { + if (compareFiles(o, root[e])) { + root[e] = n; + } + } else { + var nPath = path.slice(); + nPath.push(e); + replaceFile(nPath, o, n); + } + } + }; + // Replace a href by a stronger one everywhere in the drive (except FILES_DATA) + var replaceHref = exp.replace = function (o, n) { + if (!isFile(o) || !isFile(n)) { return; } + var paths = findFile(o); + + // Remove all the occurences in the trash + // Replace all the occurences not in the trash + // If all the occurences are in the trash or no occurence, add the pad to unsorted + var allInTrash = true; + paths.forEach(function (p) { + if (p[0] === TRASH) { + exp.delete(p, null, true); // 3rd parameter means skip "checkDeletedFiles" + return; + } else { + allInTrash = false; + var parentPath = p.slice(); + var key = parentPath.pop(); + var parentEl = find(parentPath); + parentEl[key] = n; + } + }); + if (allInTrash) { + add(n); + } + }; + + /** + * INTEGRITY CHECK + */ + + var fixFiles = exp.fixFiles = function () { + // Explore the tree and check that everything is correct: + // * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects + // * ROOT: Folders are objects, files are href + // * TRASH: Trash root contains only arrays, each element of the array is an object {element:.., path:..} + // * FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate. + // - Dates (adate, cdate) can be parsed/formatted + // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' + // * UNSORTED: Contains only files (href), and does not contains files that are in ROOT + debug("Cleaning file system..."); + + var before = JSON.stringify(files); + + var fixRoot = function (elem) { + if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; } + var element = elem || files[ROOT]; + for (var el in element) { + if (!isFile(element[el]) && !isFolder(element[el])) { + debug("An element in ROOT was not a folder nor a file. ", element[el]); + element[el] = undefined; + delete element[el]; + } else if (isFolder(element[el])) { + fixRoot(element[el]); + } + } + }; + var fixTrashRoot = function () { + if (typeof(files[TRASH]) !== "object") { debug("TRASH was not an object"); files[TRASH] = {}; } + var tr = files[TRASH]; + var toClean; + var addToClean = function (obj, idx) { + if (typeof(obj) !== "object") { toClean.push(idx); return; } + if (!isFile(obj.element) && !isFolder(obj.element)) { toClean.push(idx); return; } + if (!$.isArray(obj.path)) { toClean.push(idx); return; } + }; + for (var el in tr) { + if (!$.isArray(tr[el])) { + debug("An element in TRASH root is not an array. ", tr[el]); + tr[el] = undefined; + delete tr[el]; + } else { + toClean = []; + tr[el].forEach(addToClean); + for (var i = toClean.length-1; i>=0; i--) { + tr[el].splice(toClean[i], 1); + } + } + } + }; + var fixUnsorted = function () { + if (!Array.isArray(files[UNSORTED])) { debug("UNSORTED was not an array"); files[UNSORTED] = []; } + files[UNSORTED] = Cryptpad.deduplicateString(files[UNSORTED].slice()); + var us = files[UNSORTED]; + var rootFiles = getFiles([ROOT, TEMPLATE]).slice(); + var toClean = []; + us.forEach(function (el, idx) { + if (!isFile(el) || rootFiles.indexOf(el) !== -1) { + toClean.push(idx); + } + }); + toClean.forEach(function (idx) { + us.splice(idx, 1); + }); + }; + var fixTemplate = function () { + if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; } + files[TEMPLATE] = Cryptpad.deduplicateString(files[TEMPLATE].slice()); + var us = files[TEMPLATE]; + var rootFiles = getFiles([ROOT, UNSORTED]).slice(); + var toClean = []; + us.forEach(function (el, idx) { + if (!isFile(el) || rootFiles.indexOf(el) !== -1) { + toClean.push(idx); + } + }); + toClean.forEach(function (idx) { + us.splice(idx, 1); + }); + }; + var fixFilesData = function () { + if (!$.isArray(files[FILES_DATA])) { debug("FILES_DATA was not an array"); files[FILES_DATA] = []; } + var fd = files[FILES_DATA]; + var rootFiles = getFiles([ROOT, TRASH, 'hrefArray']); + var toClean = []; + fd.forEach(function (el, idx) { + if (!el || typeof(el) !== "object") { + debug("An element in filesData was not an object.", el); + toClean.push(el); + return; + } + if (rootFiles.indexOf(el.href) === -1) { + debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el); + files[UNSORTED].push(el.href); + return; + } + }); + toClean.forEach(function (el) { + var idx = fd.indexOf(el); + if (idx !== -1) { + spliceFileData(idx); + } + }); + }; + + fixRoot(); + fixTrashRoot(); + if (!workgroup) { + fixUnsorted(); + fixTemplate(); + fixFilesData(); + } + + if (JSON.stringify(files) !== before) { + debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); + return; + } + debug("File system was clean"); + }; + + return exp; + }; + + return module; +}); diff --git a/www/drive/main.js b/www/drive/main.js index 474ea4b06..f6e807031 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -1,20 +1,19 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ + 'jquery', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', '/common/cryptpad-common.js', - '/common/fileObject.js', + '/common/userObject.js', '/common/toolbar.js', '/customize/application_config.js', '/common/cryptget.js', '/common/mergeDrive.js' -], function (Listmap, Crypto, TextPatcher, JSONSortify, Cryptpad, FO, Toolbar, AppConfig, Get, Merge) { +], function ($, Listmap, Crypto, TextPatcher, JSONSortify, Cryptpad, FO, Toolbar, AppConfig, Get, Merge) { var module = window.MODULE = {}; var Messages = Cryptpad.Messages; - var $ = window.jQuery; var saveAs = window.saveAs; // Use `$(function () {});` to make sure the html is loaded before doing anything else @@ -264,9 +263,7 @@ define([ var removeInput = function (cancel) { if (!cancel && $iframe.find('.element-row > input').length === 1) { var $input = $iframe.find('.element-row > input'); - filesOp.renameElement($input.data('path'), $input.val(), function () { - APP.refresh(); - }); + filesOp.rename($input.data('path'), $input.val(), APP.refresh); } $iframe.find('.element-row > input').remove(); $iframe.find('.element-row > span:hidden').removeAttr('style'); @@ -332,9 +329,7 @@ define([ $input.on('keyup', function (e) { if (e.which === 13) { removeInput(true); - filesOp.renameElement(path, $input.val(), function () { - refresh(); - }); + filesOp.rename(path, $input.val(), refresh); return; } if (e.which === 27) { @@ -371,6 +366,7 @@ define([ var filterContextMenu = function ($menu, paths) { //var path = $element.data('path'); + if (!paths || paths.length === 0) { console.error('no paths'); } var hide = []; var hasFolder = false; @@ -652,16 +648,12 @@ define([ var getElementName = function (path) { // Trash root - if (filesOp.isInTrashRoot(path)) { - return path[0]; - } + if (filesOp.isInTrashRoot(path)) { return path[0]; } // Root or trash - if (filesOp.isPathInRoot(path) || filesOp.isPathInTrash(path)) { - return path[path.length - 1]; - } + if (filesOp.isPathIn(path, [ROOT, TRASH])) { return path[path.length - 1]; } // Unsorted or template - if (filesOp.isPathInUnsorted(path) || filesOp.isPathInTemplate(path)) { - var file = filesOp.findElement(files, path); + if (filesOp.isPathIn(path, ['hrefArray'])) { + var file = filesOp.find(path); if (filesOp.isFile(file) && filesOp.getTitle(file)) { return filesOp.getTitle(file); } @@ -674,10 +666,10 @@ define([ var moveElements = function (paths, newPath, force, cb) { if (!APP.editable) { return; } var andThen = function () { - filesOp.moveElements(paths, newPath, cb); + filesOp.move(paths, newPath, cb); }; // Cancel drag&drop from TRASH to TRASH - if (filesOp.comparePath(newPath, [TRASH]) && paths.length >= 1 && paths[0][0] === TRASH) { + if (filesOp.isPathIn(newPath, [TRASH]) && paths.length && paths[0][0] === TRASH) { return; } // "force" is currently unused but may be configurable by user @@ -688,7 +680,7 @@ define([ var msg = Messages._getKey('fm_removeSeveralDialog', [paths.length]); if (paths.length === 1) { var path = paths[0]; - var name = path[0] === UNSORTED ? filesOp.getTitle(filesOp.findElement(files, path)) : path[path.length - 1]; + var name = path[0] === UNSORTED ? filesOp.getTitle(filesOp.find(path)) : path[path.length - 1]; msg = Messages._getKey('fm_removeDialog', [name]); } Cryptpad.confirm(msg, function (res) { @@ -707,7 +699,7 @@ define([ $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { - var val = filesOp.findElement(files, ePath); + var val = filesOp.find(ePath); if (!val) { return; } // Error? A ".selected" element in not in the object paths.push({ path: ePath, @@ -721,7 +713,7 @@ define([ } else { removeSelected(); $element.addClass('selected'); - var val = filesOp.findElement(files, path); + var val = filesOp.find(path); if (!val) { return; } // The element in not in the object paths = [{ path: path, @@ -749,7 +741,7 @@ define([ var movedPaths = []; var importedElements = []; oldPaths.forEach(function (p) { - var el = filesOp.findElement(files, p.path); + var el = filesOp.find(p.path); if (el && (stringify(el) === stringify(p.value.el) || !p.value || !p.value.el)) { movedPaths.push(p.path); } else { @@ -764,7 +756,8 @@ define([ moveElements(movedPaths, newPath, null, refresh); } if (importedElements && importedElements.length) { - filesOp.importElements(importedElements, newPath, refresh); + // TODO workgroup + //filesOp.importElements(importedElements, newPath, refresh); } }; @@ -882,7 +875,7 @@ define([ newPath.push(key); } - var element = filesOp.findElement(files, newPath); + var element = filesOp.find(newPath); var $icon = !isFolder ? getFileIcon(element) : undefined; var ro = filesOp.isReadOnlyFile(element); // ro undefined mens it's an old hash which doesn't support read-only @@ -967,7 +960,7 @@ define([ // Create the title block with the "parent folder" button var createTitle = function (path, noStyle) { if (!path || path.length === 0) { return; } - var isTrash = filesOp.isPathInTrash(path); + var isTrash = filesOp.isPathIn(path, [TRASH]); var $title = $('', {'class': 'path unselectable'}); if (APP.mobile()) { return $title; @@ -1119,17 +1112,17 @@ define([ refresh(); }; $block.find('a.newFolder').click(function () { - filesOp.createNewFolder(currentPath, null, onCreated); + filesOp.addFolder(currentPath, null, onCreated); }); $block.find('a.newdoc').click(function (e) { var type = $(this).attr('data-type') || 'pad'; var name = Cryptpad.getDefaultName({type: type}); - filesOp.createNewFile(currentPath, name, type, onCreated); + filesOp.addFile(currentPath, name, type, onCreated); }); } else { $block.find('a.newdoc').click(function (e) { var type = $(this).attr('data-type') || 'pad'; - sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathInTrash(currentPath) ? '' : currentPath; + sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath; window.open('/' + type + '/'); }); } @@ -1251,11 +1244,11 @@ define([ }; var allFilesSorted = function () { - return filesOp.getUnsortedFiles().length === 0; + return filesOp.getFiles([UNSORTED]).length === 0; }; var sortElements = function (folder, path, oldkeys, prop, asc, useHref, useData) { - var root = filesOp.findElement(files, path); + var root = filesOp.find(path); var test = folder ? filesOp.isFolder : filesOp.isFile; var keys; if (!useData) { @@ -1478,7 +1471,7 @@ define([ var $atime = $('', {'class': 'col2'}).text(new Date(r.data.atime).toLocaleString()); var $ctimeName = $('', {'class': 'label2'}).text(Messages.fm_creation); var $ctime = $('', {'class': 'col2'}).text(new Date(r.data.ctime).toLocaleString()); - if (filesOp.isPathInHrefArray(path)) { + if (filesOp.isPathIn(path, ['hrefArray'])) { path.pop(); path.push(r.data.title); } @@ -1522,14 +1515,14 @@ define([ if (!path || path.length === 0) { path = [ROOT]; } - var isInRoot = filesOp.isPathInRoot(path); + var isInRoot = filesOp.isPathIn(path, [ROOT]); var isTrashRoot = filesOp.comparePath(path, [TRASH]); var isUnsorted = filesOp.comparePath(path, [UNSORTED]); var isTemplate = filesOp.comparePath(path, [TEMPLATE]); var isAllFiles = filesOp.comparePath(path, [FILES_DATA]); var isSearch = path[0] === SEARCH; - var root = isSearch ? undefined : filesOp.findElement(files, path); + var root = isSearch ? undefined : filesOp.find(path); if (!isSearch && typeof(root) === "undefined") { log(Messages.fm_unknownFolderError); debug("Unable to locate the selected directory: ", path); @@ -1642,11 +1635,11 @@ define([ var $el = $(e); if ($el.data('path')) { var path = $el.data('path'); - var element = filesOp.findElement(files, path); + var element = filesOp.find(path); if (!filesOp.isFile(element)) { return; } var data = filesOp.getFileData(element); if (!data) { return; } - if (filesOp.isPathInHrefArray(path)) { $el.find('.name').attr('title', data.title).text(data.title); } + if (filesOp.isPathIn(path, ['hrefArray'])) { $el.find('.name').attr('title', data.title).text(data.title); } $el.find('.title').attr('title', data.title).text(data.title); $el.find('.atime').attr('title', getDate(data.atime)).text(getDate(data.atime)); $el.find('.ctime').attr('title', getDate(data.ctime)).text(getDate(data.ctime)); @@ -1700,7 +1693,7 @@ define([ }; var createTree = function ($container, path) { - var root = filesOp.findElement(files, path); + var root = filesOp.find(path); // don't try to display what doesn't exist if (!root) { return; } @@ -1930,7 +1923,7 @@ define([ } else if ($(this).hasClass('open_ro')) { paths.forEach(function (p) { - var el = filesOp.findElement(files, p.path); + var el = filesOp.find(p.path); if (filesOp.isFolder(el)) { return; } var roUrl = getReadOnlyUrl(el); openFile(roUrl, false); @@ -1942,11 +1935,11 @@ define([ module.newFolder = info.newPath; module.displayDirectory(paths[0].path); }; - filesOp.createNewFolder(paths[0].path, null, onCreated); + filesOp.addFolder(paths[0].path, null, onCreated); } else if ($(this).hasClass("properties")) { if (paths.length !== 1) { return; } - var el = filesOp.findElement(files, paths[0].path); + var el = filesOp.find(paths[0].path); var prop = getProperties(el); Cryptpad.alert('', undefined, true); $('.alertify .msg').html(prop); @@ -1972,8 +1965,8 @@ define([ } else if ($(this).hasClass('open_ro')) { paths.forEach(function (p) { - var el = filesOp.findElement(files, p.path); - if (filesOp.isPathInFilesData(p.path)) { el = el.href; } + var el = filesOp.find(p.path); + if (filesOp.isPathIn(p.path, [FILES_DATA])) { el = el.href; } if (!el || filesOp.isFolder(el)) { return; } var roUrl = getReadOnlyUrl(el); openFile(roUrl, false); @@ -1986,7 +1979,7 @@ define([ } else if ($(this).hasClass("properties")) { if (paths.length !== 1) { return; } - var el = filesOp.findElement(files, paths[0].path); + var el = filesOp.find(paths[0].path); var prop = getProperties(el); Cryptpad.alert('', undefined, true); $('.alertify .msg').html(prop); @@ -2004,12 +1997,12 @@ define([ refresh(); }; if ($(this).hasClass("newfolder")) { - filesOp.createNewFolder(path, null, onCreated); + filesOp.addFolder(path, null, onCreated); } else if ($(this).hasClass("newdoc")) { var type = $(this).data('type') || 'pad'; var name = Cryptpad.getDefaultName({type: type}); - filesOp.createNewFile(path, name, type, onCreated); + filesOp.addFile(path, name, type, onCreated); } module.hideMenu(); }); @@ -2046,7 +2039,7 @@ define([ if (path.length === 4) { name = path[1]; } Cryptpad.confirm(Messages._getKey("fm_removePermanentlyDialog", [name]), function(res) { if (!res) { return; } - filesOp.removeFromTrash(path, refresh); + filesOp.delete([path], refresh); }); return; } @@ -2055,8 +2048,7 @@ define([ var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]); Cryptpad.confirm(msg, function(res) { if (!res) { return; } - filesOp.deletePathsPermanently(pathsList); - refresh(); + filesOp.delete(pathsList, refresh); }); } else if ($(this).hasClass("restore")) { @@ -2064,13 +2056,12 @@ define([ if (path.length === 4) { name = path[1]; } Cryptpad.confirm(Messages._getKey("fm_restoreDialog", [name]), function(res) { if (!res) { return; } - filesOp.restoreTrash(path, refresh); + filesOp.restore(path, refresh); }); } else if ($(this).hasClass("properties")) { - if (paths.length !== 1) { return; } - if (path.length !== 4) { return; } - var element = filesOp.getTrashElementData(path); + if (paths.length !== 1 || path.length !== 4) { return; } + var element = filesOp.find(path.slice(0,3)); // element containing the oldpath var sPath = stringifyPath(element.path); Cryptpad.alert('' + Messages.fm_originalPath + ":
" + sPath, undefined, true); } @@ -2094,20 +2085,17 @@ define([ $appContainer.on('keydown', function (e) { // "Del" if (e.which === 46) { - if (filesOp.isPathInFilesData(currentPath)) { return; } // We can't remove elements directly from filesData + if (filesOp.isPathIn(currentPath, [FILES_DATA])) { return; } // We can't remove elements directly from filesData var $selected = $iframe.find('.selected'); if (!$selected.length) { return; } var paths = []; - var isTrash = filesOp.isPathInTrash(currentPath); + var isTrash = filesOp.isPathIn(currentPath, [TRASH]); $selected.each(function (idx, elmt) { if (!$(elmt).data('path')) { return; } paths.push($(elmt).data('path')); }); // If we are in the trash or anon pad or if we are holding the "shift" key, delete permanently, if (isTrash || e.shiftKey) { - //var cb = filesOp.removeFromTrash; // We're in the trash - //if (!isTrash) { cb = filesOp.deletePathPermanently; } // We're in root - var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]); if (paths.length === 1) { msg = Messages.fm_removePermanentlyDialog; @@ -2116,8 +2104,7 @@ define([ Cryptpad.confirm(msg, function(res) { $(ifrw).focus(); if (!res) { return; } - filesOp.deletePathsPermanently(paths); - refresh(); + filesOp.delete(paths, refresh); }); return; } @@ -2143,10 +2130,8 @@ define([ if (path[0] !== 'drive') { return false; } path = path.slice(1); var cPath = currentPath.slice(); - if ((filesOp.isPathInUnsorted(cPath) && filesOp.isPathInUnsorted(path)) || - (filesOp.isPathInTemplate(cPath) && filesOp.isPathInTemplate(path)) || - (path.length >= cPath.length && filesOp.isSubpath(path, cPath)) || - (filesOp.isPathInTrash(cPath) && filesOp.isPathInTrash(path))) { + if ((filesOp.isPathIn(cPath, ['hrefArray', TRASH]) && cPath[0] === path[0]) || + (path.length >= cPath.length && filesOp.isSubpath(path, cPath))) { // Reload after a few ms to make sure all the change events have been received onRefresh.refresh(); } else if (path.length && path[0] === FILES_DATA) { @@ -2159,10 +2144,8 @@ define([ if (path[0] !== 'drive') { return false; } path = path.slice(1); var cPath = currentPath.slice(); - if ((filesOp.isPathInUnsorted(cPath) && filesOp.isPathInUnsorted(path)) || - (filesOp.isPathInTemplate(cPath) && filesOp.isPathInTemplate(path)) || - (path.length >= cPath.length && filesOp.isSubpath(path, cPath)) || - (filesOp.isPathInTrash(cPath) && filesOp.isPathInTrash(path))) { + if ((filesOp.isPathIn(cPath, ['hrefArray', TRASH]) && cPath[0] === path[0]) || + (path.length >= cPath.length && filesOp.isSubpath(path, cPath))) { // Reload after a few to make sure all the change events have been received onRefresh.to = window.setTimeout(refresh, 500); } @@ -2243,6 +2226,7 @@ define([ // don't initialize until the store is ready. Cryptpad.ready(function () { + Cryptpad.reportAppUsage(); APP.$bar = $iframe.find('#toolbar'); var storeObj = Cryptpad.getStore().getProxy && Cryptpad.getStore().getProxy().proxy ? Cryptpad.getStore().getProxy() : undefined; diff --git a/www/examples/board/board.js b/www/examples/board/board.js index 08f0b0dcd..26d695173 100644 --- a/www/examples/board/board.js +++ b/www/examples/board/board.js @@ -1,7 +1,6 @@ define([ - '/bower_components/jquery/dist/jquery.min.js', -],function () { - var $ = window.jQuery; + 'jquery' +],function ($) { var Board = {}; var proxy; diff --git a/www/examples/board/main.js b/www/examples/board/main.js index ca690bdc4..c5c664c49 100644 --- a/www/examples/board/main.js +++ b/www/examples/board/main.js @@ -1,5 +1,6 @@ define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), + 'jquery', + '/api/config', '/customize/messages.js', 'board.js', '/bower_components/textpatcher/TextPatcher.js', @@ -8,10 +9,9 @@ define([ '/common/cryptpad-common.js', '/common/visible.js', '/common/notify.js', - '/bower_components/file-saver/FileSaver.min.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad, Visible, Notify) { - var $ = window.jQuery; + '/bower_components/file-saver/FileSaver.min.js' +], function ($, Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad, Visible, Notify) { + var saveAs = window.saveAs; Cryptpad.styleAlerts(); diff --git a/www/examples/form/main.js b/www/examples/form/main.js index bdb10065a..04a65e2c4 100644 --- a/www/examples/form/main.js +++ b/www/examples/form/main.js @@ -1,16 +1,14 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), + 'jquery', + '/api/config', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', 'ula.js', '/bower_components/chainpad-json-validator/json-ot.js', - '/common/cryptpad-common.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT, Cryptpad) { - var $ = window.jQuery; + '/common/cryptpad-common.js' +], function ($, Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT, Cryptpad) { var secret = Cryptpad.getSecrets(); diff --git a/www/examples/hack/main.js b/www/examples/hack/main.js index 82a2b0f6e..c59460a60 100644 --- a/www/examples/hack/main.js +++ b/www/examples/hack/main.js @@ -1,12 +1,11 @@ define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), + 'jquery', + '/api/config', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', - '/common/cryptpad-common.js', - '/bower_components/jquery/dist/jquery.min.js' -], function (Config, Realtime, Crypto, TextPatcher, Cryptpad) { - var $ = window.jQuery; + '/common/cryptpad-common.js' +], function ($, Config, Realtime, Crypto, TextPatcher, Cryptpad) { var secret = Cryptpad.getSecrets(); diff --git a/www/examples/json/main.js b/www/examples/json/main.js index 8b018ebd7..90654afa6 100644 --- a/www/examples/json/main.js +++ b/www/examples/json/main.js @@ -1,11 +1,10 @@ define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), + 'jquery', + '/api/config', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', - '/common/cryptpad-common.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Config, RtListMap, Crypto, Common) { - var $ = window.jQuery; + '/common/cryptpad-common.js' +], function ($, Config, RtListMap, Crypto, Common) { var secret = Common.getSecrets(); diff --git a/www/examples/pin/main.js b/www/examples/pin/main.js index 1853b9e4c..ad6905d7d 100644 --- a/www/examples/pin/main.js +++ b/www/examples/pin/main.js @@ -1,10 +1,8 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ + 'jquery', '/common/cryptpad-common.js', - '/common/pinpad.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Cryptpad, Pinpad) { - var $ = window.jQuery; + '/common/pinpad.js' +], function ($, Cryptpad, Pinpad) { var APP = window.APP = { Cryptpad: Cryptpad, }; diff --git a/www/examples/read/main.js b/www/examples/read/main.js index 9bce1ebe7..5692f9b6b 100644 --- a/www/examples/read/main.js +++ b/www/examples/read/main.js @@ -1,8 +1,7 @@ define([ - '/common/cryptget.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Crypt) { - var $ = window.jQuery; + 'jquery', + '/common/cryptget.js' +], function ($, Crypt) { var $target = $('#target'); var $dest = $('#dest'); diff --git a/www/examples/render/index.html b/www/examples/render/index.html index 24e7e41b5..56112aab3 100644 --- a/www/examples/render/index.html +++ b/www/examples/render/index.html @@ -3,7 +3,7 @@ - + diff --git a/www/poll/main.js b/www/poll/main.js index 7225bee7f..5aa3e4ee2 100644 --- a/www/poll/main.js +++ b/www/poll/main.js @@ -1,4 +1,5 @@ define([ + 'jquery', '/bower_components/textpatcher/TextPatcher.js', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', @@ -9,10 +10,8 @@ define([ '/common/toolbar.js', '/common/visible.js', '/common/notify.js', - '/bower_components/file-saver/FileSaver.min.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (TextPatcher, Listmap, Crypto, Cryptpad, Cryptget, Hyperjson, Renderer, Toolbar, Visible, Notify) { - var $ = window.jQuery; + '/bower_components/file-saver/FileSaver.min.js' +], function ($, TextPatcher, Listmap, Crypto, Cryptpad, Cryptget, Hyperjson, Renderer, Toolbar, Visible, Notify) { var Messages = Cryptpad.Messages; @@ -25,6 +24,7 @@ define([ var secret = Cryptpad.getSecrets(); var readOnly = secret.keys && !secret.keys.editKeyStr; + // DEPRECATE_F if (!secret.keys) { secret.keys = secret.key; } @@ -210,7 +210,7 @@ define([ }; /* Any time the realtime object changes, call this function */ - var change = function (o, n, path, throttle) { + var change = function (o, n, path, throttle, cb) { if (path && !Cryptpad.isArray(path)) { return; } @@ -259,8 +259,14 @@ define([ var displayedObj2 = mergeUncommitted(APP.proxy, APP.uncommitted); var f = getFocus(); Render.updateTable(table, displayedObj2, conf); + APP.proxy.table.rowsOrder.forEach(function (rowId) { + $('input[data-rt-id="' + rowId +'"]').val(APP.proxy.table.rows[rowId] || ''); + }); updateDisplayedTable(); setFocus(f); + if (typeof(cb) === "function") { + cb(); + } }; if (throttle) { @@ -280,7 +286,7 @@ define([ }; /* Called whenever an event is fired on an input element */ - var handleInput = function (input) { + var handleInput = function (input, isKeyup) { var type = input.type.toLowerCase(); var id = getRealtimeId(input); @@ -331,7 +337,9 @@ define([ }); } else if (isEdit) { unlockRow(id, function () { - change(); + change(null, null, null, null, function() { + $('input[data-rt-id="' + id + '"]').focus(); + }); }); } } else if (type === 'col') { @@ -344,7 +352,9 @@ define([ }); } else if (isEdit) { unlockColumn(id, function () { - change(); + change(null, null, null, null, function() { + $('input[data-rt-id="' + id + '"]').focus(); + }); }); } } else if (type === 'cell') { @@ -354,8 +364,8 @@ define([ } }; - var hideInputs = function (e) { - if ($(e.target).is('[type="text"]')) { + var hideInputs = function (e, isKeyup) { + if (!isKeyup && $(e.target).is('[type="text"]')) { return; } $('.lock[data-rt-id!="' + APP.userid + '"]').html(lockHTML); @@ -388,6 +398,10 @@ define([ switch (nodeName) { case 'INPUT': + if (isKeyup && (e.keyCode === 13 || e.keyCode === 27)) { + hideInputs(e, isKeyup); + return; + } handleInput(target); break; case 'SPAN': @@ -546,20 +560,18 @@ define([ var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly)); var $createRow = APP.$createRow = $('#create-option').click(function () { //console.error("BUTTON CLICKED! LOL"); - Render.createRow(proxy, function () { - change(); - var order = APP.proxy.table.rowsOrder; - - var last = order[order.length - 1]; - var $newest = $('[data-rt-id="' + last + '"]'); - $newest.val(''); - window.setTimeout(change); + Render.createRow(proxy, function (empty, id) { + change(null, null, null, null, function() { + $('.edit[data-rt-id="' + id + '"]').click(); + }); }); }); var $createCol = APP.$createCol = $('#create-user').click(function () { - Render.createColumn(proxy, function () { - change(); + Render.createColumn(proxy, function (empty, id) { + change(null, null, null, null, function() { + $('.edit[data-rt-id="' + id + '"]').click(); + }); }); }); @@ -768,19 +780,11 @@ define([ } Cryptpad.onDisplayNameChanged(setName); - - Cryptpad.getPadTitle(function (err, title) { - if (err) { - error(err); - debug("Couldn't get pad title"); - return; - } - updateTitle(title || defaultName); - }); }; // don't initialize until the store is ready. Cryptpad.ready(function () { + Cryptpad.reportAppUsage(); var config = { websocketURL: Cryptpad.getWebsocketURL(), channel: secret.channel, @@ -838,4 +842,3 @@ define([ }); }); - diff --git a/www/poll/poll.css b/www/poll/poll.css new file mode 100644 index 000000000..321121b59 --- /dev/null +++ b/www/poll/poll.css @@ -0,0 +1,357 @@ +html, +body { + width: 100%; + height: 100%; + margin: 0px; + padding: 0px; + border: 0px; +} +.cryptpad-toolbar h2 { + font: normal normal normal 12px Arial, Helvetica, Tahoma, Verdana, Sans-Serif; + color: #000; + line-height: auto; +} +.cryptpad-toolbar { + display: inline-block; +} +.realtime { + display: block; + max-height: 100%; + max-width: 100%; +} +.realtime input[type="text"] { + height: 1em; + margin: 0px; +} +.text-cell input[type="text"] { + width: 400px; +} +input[type="text"][disabled], +textarea[disabled] { + background-color: transparent; + font: white; + border: 0px; +} +table#table { + margin: 0px; +} +#tableContainer { + position: relative; + padding: 29px; + padding-right: 79px; +} +#tableContainer button { + height: 2rem; + display: none; +} +#publish { + display: none; +} +#publish, +#admin { + margin-top: 15px; + margin-bottom: 15px; +} +#create-user { + position: absolute; + display: inline-block; + /*left: 0px;*/ + top: 55px; + width: 50px; + overflow: hidden; +} +#create-option { + width: 50px; +} +#tableScroll { + overflow-y: hidden; + overflow-x: auto; + margin-left: calc(30% - 50px + 29px); + max-width: 70%; + width: auto; + display: inline-block; +} +#description { + padding: 15px; + margin: auto; + min-width: 80%; + width: 80%; + min-height: 5em; + font-size: 20px; + font-weight: bold; +} +#description[disabled] { + resize: none; + color: #000; + border: 1px solid #444; +} +#commit { + width: 100%; +} +#howItWorks { + width: 80%; + margin: auto; +} +div.upper { + width: 80%; + margin: auto; +} +table { + border-collapse: collapse; + border-spacing: 0; + margin: 20px; +} +tbody { + border: 1px solid #555; +} +tbody tr { + text-align: center; +} +tbody tr:first-of-type th { + font-size: 20px; + border-top: 0px; + font-weight: bold; + padding: 10px; + text-decoration: underline; +} +tbody tr:first-of-type th.table-refresh { + color: #46E981; + text-decoration: none; + cursor: pointer; +} +tbody tr:nth-child(odd) { + background-color: #ffffff; +} +tbody tr th:first-of-type { + border-left: 0px; +} +tbody tr th { + box-sizing: border-box; + border: 1px solid #555; +} +tbody tr th, +tbody tr td { + color: #555; +} +tbody tr th.remove, +tbody tr td.remove { + cursor: pointer; +} +tbody tr th:last-child { + border-right: 0px; +} +tbody td { + border-right: 1px solid #555; + padding: 12px; + padding-top: 0px; + padding-bottom: 0px; +} +tbody td:last-child { + border-right: none; +} +form.realtime, +div.realtime { + padding: 0px; + margin: 0px; +} +form.realtime > textarea, +div.realtime > textarea { + width: 50%; + height: 15vh; +} +form.realtime table, +div.realtime table { + border-collapse: collapse; + width: calc(100% - 1px); +} +form.realtime table tr td:first-child, +div.realtime table tr td:first-child { + position: absolute; + left: 29px; + top: auto; + width: calc(30% - 50px); +} +form.realtime table tr td, +div.realtime table tr td { + padding: 0px; + margin: 0px; +} +form.realtime table tr td div.text-cell, +div.realtime table tr td div.text-cell { + padding: 0px; + margin: 0px; + height: 100%; +} +form.realtime table tr td div.text-cell input, +div.realtime table tr td div.text-cell input { + width: 80%; + width: 90%; + height: 100%; + border: 0px; +} +form.realtime table tr td div.text-cell input[disabled], +div.realtime table tr td div.text-cell input[disabled] { + background-color: transparent; + color: #000; + font-weight: bold; +} +form.realtime table tr td.checkbox-cell, +div.realtime table tr td.checkbox-cell { + margin: 0px; + padding: 0px; + height: 100%; + min-width: 150px; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain, +div.realtime table tr td.checkbox-cell div.checkbox-contain { + display: inline-block; + height: 100%; + width: 100%; + position: relative; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain label, +div.realtime table tr td.checkbox-cell div.checkbox-contain label { + background-color: transparent; + display: block; + position: absolute; + top: 0px; + left: 0px; + height: 100%; + width: 100%; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable), +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) { + display: none; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover { + font-weight: bold; + background-color: #FA5858; + color: #000; + display: block; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { + height: 100%; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { + content: "✖"; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes { + background-color: #46E981; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after { + content: "✔"; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted { + background: #ddd; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine { + display: none; +} +form.realtime table input[type="text"], +div.realtime table input[type="text"] { + height: auto; + border: 1px solid #fff; + width: 80%; +} +form.realtime table thead td, +div.realtime table thead td { + padding: 0px 5px; + background: #aaa; + border-radius: 20px 20px 0 0; + text-align: center; +} +form.realtime table thead td input[type="text"], +div.realtime table thead td input[type="text"] { + width: 100%; + box-sizing: border-box; +} +form.realtime table thead td input[type="text"][disabled], +div.realtime table thead td input[type="text"][disabled] { + color: #000; + padding: 1px 5px; + border: none; +} +form.realtime table tbody .text-cell, +div.realtime table tbody .text-cell { + background: #aaa; +} +form.realtime table tbody .text-cell input[type="text"], +div.realtime table tbody .text-cell input[type="text"] { + width: calc(100% - 50px); +} +form.realtime table tbody .text-cell .edit, +div.realtime table tbody .text-cell .edit { + float: right; + margin: 0 10px 0 0; +} +form.realtime table tbody .text-cell .remove, +div.realtime table tbody .text-cell .remove { + float: left; + margin: 0 0 0 10px; +} +form.realtime table tbody td label, +div.realtime table tbody td label { + border: 0.5px solid #555; +} +form.realtime table .edit, +div.realtime table .edit { + color: #000; + cursor: pointer; + float: left; + margin-left: 10px; +} +form.realtime table .remove, +div.realtime table .remove { + float: right; + margin-right: 10px; +} +form.realtime table thead tr th input[type="text"][disabled], +div.realtime table thead tr th input[type="text"][disabled] { + background-color: transparent; + color: #555; + font-weight: bold; +} +form.realtime table thead tr th .remove, +div.realtime table thead tr th .remove { + cursor: pointer; + font-size: 20px; +} +form.realtime table tfoot tr, +div.realtime table tfoot tr { + border: none; +} +form.realtime table tfoot tr td, +div.realtime table tfoot tr td { + border: none; + text-align: center; +} +form.realtime table tfoot tr td .save, +div.realtime table tfoot tr td .save { + padding: 15px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} +form.realtime #adduser, +div.realtime #adduser, +form.realtime #addoption, +div.realtime #addoption { + color: #46E981; + border: 1px solid #46E981; + padding: 15px; + cursor: pointer; +} +form.realtime #adduser, +div.realtime #adduser { + border-top-left-radius: 5px; +} +form.realtime #addoption, +div.realtime #addoption { + border-bottom-left-radius: 5px; +} diff --git a/www/poll/poll.less b/www/poll/poll.less new file mode 100644 index 000000000..7292b0e15 --- /dev/null +++ b/www/poll/poll.less @@ -0,0 +1,387 @@ +@import "../../customize.dist/src/less/variables.less"; +@import "../../customize.dist/src/less/mixins.less"; + +@poll-th-bg: #aaa; +@poll-td-bg: #aaa; +@poll-border-color: #555; +@poll-cover-color: #000; +@poll-fg: #000; + +html, body { + width: 100%; + height: 100%; + margin: 0px; + padding: 0px; + border: 0px; +} + +.cryptpad-toolbar h2 { + font: normal normal normal 12px Arial, Helvetica, Tahoma, Verdana, Sans-Serif; + color: #000; + line-height: auto; +} +.cryptpad-toolbar { + display: inline-block; +} +.realtime { + display: block; + max-height: 100%; + max-width: 100%; +} + +.realtime input[type="text"] { + height: 1em; + margin: 0px; +} +.text-cell input[type="text"] { + width: 400px; +} + +input[type="text"][disabled], textarea[disabled] { + background-color: transparent; + font: white; + border: 0px; +} +table#table { + margin: 0px; +} +#tableContainer { + position: relative; + padding: 29px; + padding-right: 79px; +} +#tableContainer button { + height: 2rem; + display: none; +} +#publish { + display: none; +} +#publish, #admin { + margin-top: 15px; + margin-bottom: 15px; +} +#create-user { + position: absolute; + display: inline-block; + /*left: 0px;*/ + top: 55px; + width: 50px; + overflow: hidden; +} +#create-option { + width: 50px; +} +#tableScroll { + overflow-y: hidden; + overflow-x: auto; + margin-left: calc(~"30% - 50px + 29px"); + max-width: 70%; + width: auto; + display: inline-block; +} +#description { + padding: 15px; + margin: auto; + + min-width: 80%; + width: 80%; + min-height: 5em; + font-size: 20px; + font-weight: bold; + +} +#description[disabled] { + resize: none; + color: #000; + border: 1px solid #444; +} + +#commit { + width: 100%; +} +#howItWorks { + width: 80%; + margin: auto; +} +div.upper { + width: 80%; + margin: auto; +} + +// from cryptpad.less + +table { + border-collapse: collapse; + border-spacing: 0; + margin: 20px; +} +tbody { + border: 1px solid @poll-border-color; + tr { + text-align: center; + &:first-of-type th{ + font-size: 20px; + border-top: 0px; + font-weight: bold; + padding: 10px; + text-decoration: underline; + &.table-refresh { + color: @cp-green; + text-decoration: none; + cursor: pointer; + } + + } + &:nth-child(odd) { + background-color: @light-base; + } + th:first-of-type { + border-left: 0px; + } + th { + box-sizing: border-box; + border: 1px solid @poll-border-color; + } + th, td { + color: @fore; + + &.remove { + cursor: pointer; + } + } + th:last-child { + border-right: 0px; + } + } + + td { + border-right: 1px solid @poll-border-color; + padding: 12px; + padding-top: 0px; + padding-bottom: 0px; + &:last-child { + border-right: none; + } + } +} + +form.realtime, div.realtime { + > input { + &[type="text"] { + + } + } + > textarea { + width: 50%; + height: 15vh; + } + + padding: 0px; + margin: 0px; + + table { + border-collapse: collapse; + width: ~"calc(100% - 1px)"; + tr { + td:first-child { + position:absolute; + left: 29px; + top: auto; + width: ~"calc(30% - 50px)"; + } + td { + padding: 0px; + margin: 0px; + + div.text-cell { + padding: 0px; + margin: 0px; + height: 100%; + + input { + width: 80%; + width: 90%; + height: 100%; + border: 0px; + &[disabled] { + background-color: transparent; + color: @poll-fg; + font-weight: bold; + } + } + } + + &.checkbox-cell { + margin: 0px; + padding: 0px; + height: 100%; + min-width: 150px; + + div.checkbox-contain { + display: inline-block; + height: 100%; + width: 100%; + position: relative; + + label { + background-color: transparent; + display: block; + position: absolute; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + } + + input { + &[type="checkbox"] { + &:not(.editable) { + display: none; + + ~ .cover { + display: block; + font-weight: bold; + + background-color: @cp-red; + color: @poll-cover-color; + + &:after { + height: 100%; + } + + &:after { content: "✖"; } + + display: block; + &.yes { + background-color: @cp-green; + &:after { content: "✔"; } + } + + &.uncommitted { + background: #ddd; + } + + + &.mine { + display: none; + } + } + } + } + } + } + } + } + } + + input { + &[type="text"] { + height: auto; + border: 1px solid @base; + width: 80%; + } + } + thead { + td { + padding: 0px 5px; + background: @poll-th-bg; + border-radius: 20px 20px 0 0; + text-align: center; + input { + &[type="text"] { + width: 100%; + box-sizing: border-box; + &[disabled] { + color: @poll-fg; + padding: 1px 5px; + border: none; + } + } + } + } + } + + tbody { + .text-cell { + background: @poll-td-bg; + //border-radius: 20px 0 0 20px; + input[type="text"] { + width: ~"calc(100% - 50px)"; + } + .edit { + float:right; + margin: 0 10px 0 0; + } + .remove { + float: left; + margin: 0 0 0 10px; + } + } + td { + label { + border: .5px solid @poll-border-color; + } + } + } + .edit { + color: @poll-cover-color; + cursor: pointer; + float: left; + margin-left: 10px; + } + + .remove { + float: right; + margin-right: 10px; + } + + thead { + tr { + th { + input[type="text"][disabled] { + background-color: transparent; + color: @fore; + font-weight: bold; + } + .remove { + cursor: pointer; + font-size: 20px; + } + } + } + } + tbody { + tr { + td { + + } + } + } + tfoot { + tr { + border: none; + td { + border: none; + text-align: center; + .save { + padding: 15px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + } + } + } + } + + #adduser, + #addoption { + color: @cp-green; + border: 1px solid @cp-green; + padding: 15px; + cursor: pointer; + } + + #adduser { .top-left; } + #addoption { .bottom-left; } +} diff --git a/www/register/main.js b/www/register/main.js index 251a69f24..374118919 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -1,11 +1,10 @@ define([ + 'jquery', '/common/login.js', '/common/cryptpad-common.js', '/common/cryptget.js', - '/common/credential.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Login, Cryptpad, Crypt) { - var $ = window.jQuery; + '/common/credential.js' +], function ($, Login, Cryptpad, Crypt) { var APP = window.APP = { Login: Login, @@ -68,6 +67,12 @@ define([ proxy.edPublic = result.edPublic; proxy.edPrivate = result.edPrivate; + // feedback API won't work because proxy wasn't loaded + $.ajax({ + type: 'HEAD', + url: '/common/feedback.html?REGISTRATION=' + (+new Date()), + }); + Cryptpad.whenRealtimeSyncs(result.realtime, function () { Cryptpad.login(result.userHash, result.userName, function () { if (sessionStorage.redirectTo) { diff --git a/www/settings/index.html b/www/settings/index.html index 795ab825a..56d77905e 100644 --- a/www/settings/index.html +++ b/www/settings/index.html @@ -8,7 +8,6 @@ - diff --git a/www/settings/main.js b/www/settings/main.js index 64bf15f7b..8600dba4d 100644 --- a/www/settings/main.js +++ b/www/settings/main.js @@ -1,11 +1,10 @@ define([ + 'jquery', '/common/cryptpad-common.js', '/common/cryptget.js', '/common/mergeDrive.js', - '/bower_components/file-saver/FileSaver.min.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Cryptpad, Crypt, Merge) { - var $ = window.jQuery; + '/bower_components/file-saver/FileSaver.min.js' +], function ($, Cryptpad, Crypt, Merge) { var saveAs = window.saveAs; var USERNAME_KEY = 'cryptpad.username'; @@ -227,6 +226,40 @@ define([ return $div; }; + var bytesToMegabytes = function (bytes) { + return Math.floor((bytes / (1024 * 1024) * 100)) / 100; + }; + + var createUsageButton = function (obj) { + var proxy = obj.proxy; + + var $div = $('
', { 'class': 'pinned-usage' }) + .text(Messages.settings_usageTitle) + .append('
'); + + $('