From 2be5db95409defe75259d7dcd1bc8cfe3d8bb044 Mon Sep 17 00:00:00 2001
From: yflory <yann.flory@xwiki.com>
Date: Tue, 10 Jan 2017 15:04:02 +0100
Subject: [PATCH] Ability to open files in readonly mode

Fix CSS issues
---
 customize.dist/src/less/toolbar.less       |   7 +-
 customize.dist/toolbar.css                 |   9 +-
 customize.dist/translations/messages.fr.js |   1 +
 customize.dist/translations/messages.js    |   1 +
 www/common/toolbar.js                      |  11 +-
 www/drive/file.css                         |   2 +
 www/drive/inner.html                       |   9 +-
 www/drive/main.js                          | 141 ++++++++++++++-------
 8 files changed, 128 insertions(+), 53 deletions(-)

diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less
index fa9bf2f60..2b5798594 100644
--- a/customize.dist/src/less/toolbar.less
+++ b/customize.dist/src/less/toolbar.less
@@ -91,8 +91,7 @@
     }
 
     .cryptpad-state {
-        line-height: 30px; /* equivalent to 26px + 2*2px margin used for buttons */
-        float: left;
+        line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */
     }
 
     .rightside-button {
@@ -324,6 +323,10 @@
     text-transform: uppercase;
 }
 .cryptpad-toolbar-username {
+    line-height: 32px;
+    button {
+        line-height: initial;
+    }
 }
 .lag {
     height: 15px !important;
diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css
index f3e44fb29..a6efcadb0 100644
--- a/customize.dist/toolbar.css
+++ b/customize.dist/toolbar.css
@@ -96,9 +96,8 @@
   }
 }
 .cryptpad-toolbar .cryptpad-state {
-  line-height: 30px;
+  line-height: 32px;
   /* equivalent to 26px + 2*2px margin used for buttons */
-  float: left;
 }
 .cryptpad-toolbar .rightside-button {
   float: right;
@@ -332,6 +331,12 @@
   font-weight: bold;
   text-transform: uppercase;
 }
+.cryptpad-toolbar-username {
+  line-height: 32px;
+}
+.cryptpad-toolbar-username button {
+  line-height: initial;
+}
 .lag {
   height: 15px !important;
   width: 15px !important;
diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js
index 6d415e2e6..f7f5a0294 100644
--- a/customize.dist/translations/messages.fr.js
+++ b/customize.dist/translations/messages.fr.js
@@ -210,6 +210,7 @@ define(function () {
     out.fc_newfolder = "Nouveau dossier";
     out.fc_rename = "Renommer";
     out.fc_open = "Ouvrir";
+    out.fc_open_ro = "Ouvrir (lecture seule)";
     out.fc_delete = "Supprimer";
     out.fc_restore = "Restaurer";
     out.fc_remove = "Supprimer définitivement";
diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js
index 7e1ba420c..9cf23c8ef 100644
--- a/customize.dist/translations/messages.js
+++ b/customize.dist/translations/messages.js
@@ -212,6 +212,7 @@ define(function () {
     out.fc_newfolder = "New folder";
     out.fc_rename = "Rename";
     out.fc_open = "Open";
+    out.fc_open_ro = "Open (read-only)";
     out.fc_delete = "Delete";
     out.fc_restore = "Restore";
     out.fc_remove = "Delete permanently";
diff --git a/www/common/toolbar.js b/www/common/toolbar.js
index 260e7d22d..45dfa6c1f 100644
--- a/www/common/toolbar.js
+++ b/www/common/toolbar.js
@@ -288,7 +288,8 @@ define([
             'class': LAG_ELEM_CLS,
             id: uid(),
         });
-        $container.prepend($lag);
+        // Now added in createUserAdmin
+        //$container.prepend($lag);
         return $lag[0];
     };
 
@@ -356,7 +357,9 @@ define([
         $linkContainer.append($aTagSmall).append($aTagBig);
     };
 
-    var createUserAdmin = function ($topContainer) {
+    var createUserAdmin = function ($topContainer, lagElement) {
+        var $lag = $(lagElement);
+
         var $userContainer = $('<span>', {
             'class': USER_CLS
         }).appendTo($topContainer);
@@ -365,6 +368,8 @@ define([
             'class': STATE_CLS
         }).text(Messages.synchronizing).appendTo($userContainer);
 
+        $userContainer.append($lag);
+
         var $span = $('<span>' , {
             'class': 'cryptpad-language'
         });
@@ -478,8 +483,8 @@ define([
         var userListElement = config.userData ? createUserList(toolbar.find('.' + LEFTSIDE_CLS), readOnly) : undefined;
         var $titleElement = createTitle(toolbar.find('.' + TOP_CLS), readOnly, config.title, Cryptpad);
         var $linkElement = createLinkToMain(toolbar.find('.' + TOP_CLS));
-        var $userAdminElement = createUserAdmin(toolbar.find('.' + TOP_CLS));
         var lagElement = createLagElement($userAdminElement);
+        var $userAdminElement = createUserAdmin(toolbar.find('.' + TOP_CLS), lagElement);
         var spinner = createSpinner($userAdminElement);
         var userData = config.userData;
         // readOnly = 1 (readOnly enabled), 0 (disabled), -1 (old pad without readOnly mode)
diff --git a/www/drive/file.css b/www/drive/file.css
index 96b02c9b0..7dd19d9ca 100644
--- a/www/drive/file.css
+++ b/www/drive/file.css
@@ -402,6 +402,8 @@ button.newElement:hover {
 }
 .dropdown-bar-content hr {
     margin: 5px 0px;
+    height: 1px;
+    background: #bbb;
 }
 
 /* Change color of dropdown links on hover */
diff --git a/www/drive/inner.html b/www/drive/inner.html
index 85a5f2340..f1df46b3c 100644
--- a/www/drive/inner.html
+++ b/www/drive/inner.html
@@ -15,9 +15,10 @@
         </div>
         <div id="content">
         </div>
-        <div id="contextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
+        <div id="treeContextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
             <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
                 <li><a tabindex="-1" href="#" class="open" data-localization="fc_open">Open</a></li>
+                <li><a tabindex="-1" href="#" class="open_ro" data-localization="fc_open_ro">Open (read-only)</a></li>
                 <li><a tabindex="-1" href="#" class="rename editable" data-localization="fc_rename">Rename</a></li>
                 <li><a tabindex="-1" href="#" class="delete editable" data-localization="fc_delete">Delete</a></li>
                 <li><a tabindex="-1" href="#" class="newfolder editable" data-localization="fc_newfolder">New folder</a></li>
@@ -32,6 +33,12 @@
                 <li><a tabindex="-1" href="#" class="newdoc own editable" data-type="poll" data-localization="fc_newpoll" target="_blank">New poll</a></li>
             </ul>
         </div>
+        <div id="defaultContextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
+            <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
+                <li><a tabindex="-1" href="#" class="open" data-localization="fc_open">Open</a></li>
+                <li><a tabindex="-1" href="#" class="open_ro" data-localization="fc_open_ro">Open (read-only)</a></li>
+            </ul>
+        </div>
         <div id="trashTreeContextMenu" class="contextMenu dropdown clearfix" oncontextmenu="return false;">
             <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
                 <li><a tabindex="-1" href="#" class="empty editable" data-localization="fc_empty">Empty the trash</a></li>
diff --git a/www/drive/main.js b/www/drive/main.js
index a0fd2750a..fff4a74ff 100644
--- a/www/drive/main.js
+++ b/www/drive/main.js
@@ -130,20 +130,6 @@ define([
         else { $iframe.find('[draggable="false"]').attr('draggable', true); }
     };
 
-    var keyPressed = [];
-    var pressKey = function (key, state) {
-        if (state) {
-            if (keyPressed.indexOf(key) === -1) {
-                keyPressed.push(key);
-            }
-            return;
-        }
-        var idx = keyPressed.indexOf(key);
-        if (idx !== -1) {
-            keyPressed.splice(idx, 1);
-        }
-    };
-
     var init = function (files) {
         var isOwnDrive = function () {
             return Cryptpad.getUserHash() === APP.hash || localStorage.FS_hash === APP.hash;
@@ -190,13 +176,17 @@ define([
         // Store the object sent for the "change username" button so that we can update the field value correctly
         var userNameButtonObject = APP.userName = {};
         /* add a "change username" button */
-        getLastName(function (err, lastName) {
-            userNameButtonObject.lastName = lastName;
-            var $username = APP.$userNameButton = Cryptpad.createButton('username', false, userNameButtonObject, setName).hide();
-            $userBlock.append($username);
-            $username.append(lastName);
-            $username.show();
-        });
+        if (!APP.readOnly) {
+            getLastName(function (err, lastName) {
+                userNameButtonObject.lastName = lastName;
+                var $username = APP.$userNameButton = Cryptpad.createButton('username', false, userNameButtonObject, setName).hide();
+                $userBlock.append($username);
+                $username.append(lastName);
+                $username.show();
+            });
+        } else {
+            $userBlock.html('<span class="' + Toolbar.constants.readonly + '">' + Messages.readonly + '</span>');
+        }
 
         // FILE MANAGER
         // _WORKGROUP_ and other people drive : display Documents as main page
@@ -206,8 +196,9 @@ define([
 
         var $tree = $iframe.find("#tree");
         var $content = $iframe.find("#content");
-        var $contextMenu = $iframe.find("#contextMenu");
+        var $contextMenu = $iframe.find("#treeContextMenu");
         var $contentContextMenu = $iframe.find("#contentContextMenu");
+        var $defaultContextMenu = $iframe.find("#defaultContextMenu");
         var $trashTreeContextMenu = $iframe.find("#trashTreeContextMenu");
         var $trashContextMenu = $iframe.find("#trashContextMenu");
 
@@ -292,13 +283,13 @@ define([
             window.open(fileEl);
         };
 
-        var refresh = function () {
+        var refresh = APP.refresh = function () {
             module.displayDirectory(currentPath);
         };
 
         // Replace a file/folder name by an input to change its value
         var displayRenameInput = function ($element, path) {
-            if (!APP.editable) { debug("Read-only mode"); return; }
+            if (!APP.editable) { return; }
             if (!path || path.length < 2) {
                 logError("Renaming a top level element (root, trash or filesData) is forbidden.");
                 return;
@@ -345,9 +336,9 @@ define([
         };
 
         // Add the "selected" class to the "li" corresponding to the clicked element
-        var onElementClick = function ($element, path) {
+        var onElementClick = function (e, $element, path) {
             // If "Ctrl" is pressed, do not remove the current selection
-            if (keyPressed.indexOf(17) === -1) {
+            if (!e || !e.ctrlKey) {
                 removeSelected();
             }
             if (!$element.is('li')) {
@@ -371,7 +362,7 @@ define([
             e.stopPropagation();
 
             var path = $(e.target).closest('li').data('path');
-            if (!path) { return; }
+            if (!path) { return false; }
 
             if (!APP.editable) {
                 $menu.find('a.editable').parent('li').hide();
@@ -394,12 +385,12 @@ define([
 
             // $element should be the <span class="element">, find it if it's not the case
             var $element = $(e.target).closest('li').children('span.element');
-            onElementClick($element);
+            onElementClick(undefined, $element);
             if (!$element.length) {
                 logError("Unable to locate the .element tag", e.target);
                 $menu.hide();
                 log(Messages.fm_contextMenuError);
-                return;
+                return false;
             }
             $menu.find('a').data('path', path);
             $menu.find('a').data('element', $element);
@@ -411,11 +402,25 @@ define([
             $contextMenu.find('li').show();
             if ($element.find('.file-element').length) {
                 $contextMenu.find('a.newfolder').parent('li').hide();
+            } else {
+                $contextMenu.find('a.open_ro').parent('li').hide();
             }
             openContextMenu(e, $contextMenu);
             return false;
         };
 
+        var openDefaultContextMenu = function (e) {
+            var $element = $(e.target).closest('li');
+            $defaultContextMenu.find('li').show();
+            if ($element.find('.file-element').length) {
+                $defaultContextMenu.find('a.newfolder').parent('li').hide();
+            } else {
+                $defaultContextMenu.find('a.open_ro').parent('li').hide();
+            }
+            openContextMenu(e, $defaultContextMenu);
+            return false;
+        };
+
         var openTrashTreeContextMenu = function (e) {
             openContextMenu(e, $trashTreeContextMenu);
             return false;
@@ -486,7 +491,7 @@ define([
         // filesOp.moveElements is able to move several paths to a new location, including
         // the Trash or the "Unsorted files" folder
         var moveElements = function (paths, newPath, force, cb) {
-            if (!APP.editable) { debug("Read-only mode"); return; }
+            if (!APP.editable) { return; }
             var andThen = function () {
                 filesOp.moveElements(paths, newPath, cb);
             };
@@ -577,7 +582,7 @@ define([
         };
 
         var addDragAndDropHandlers = function ($element, path, isFolder, droppable) {
-            if (!APP.editable) { debug("Read-only mode"); return; }
+            if (!APP.editable) { return; }
             // "dragenter" is fired for an element and all its children
             // "dragleave" may be fired when entering a child
             // --> we use pointer-events: none in CSS, but we still need a counter to avoid some issues
@@ -701,7 +706,7 @@ define([
             addDragAndDropHandlers($element, newPath, isFolder, !isTrash);
             $element.click(function(e) {
                 e.stopPropagation();
-                onElementClick($element, newPath);
+                onElementClick(e, $element, newPath);
             });
             if (!isTrash) {
                 $element.contextmenu(openDirectoryContextMenu);
@@ -859,6 +864,7 @@ define([
 
         var createNewButton = function (isInRoot) {
             var $block = $('<div>', {'class': 'dropdown-bar'});
+            if (!APP.editable) { return $block; }
 
             var $button = $('<button>', {
                 'class': 'newElement'
@@ -866,6 +872,11 @@ define([
 
             var $innerblock = $('<div>', {'class': 'dropdown-bar-content'});
 
+            if (isInRoot) {
+                $innerblock.append(createNewFolderButton());
+                $innerblock.append('<hr>');
+            }
+
             AppConfig.availablePadTypes.forEach(function (type) {
                 var $button = $('<a>', {
                     'class': 'newElement newdoc',
@@ -877,11 +888,6 @@ define([
                 $innerblock.append($button);
             });
 
-            if (isInRoot) {
-                $innerblock.append('<hr>');
-                $innerblock.append(createNewFolderButton());
-            }
-
             $block.append($button).append($innerblock);
 
             $button.click(function (e) {
@@ -1052,6 +1058,13 @@ define([
                 if (prop && !folder) {
                     var element = el.element;
                     var e = filesOp.getFileData(element);
+                    if (!e) {
+                        e = {
+                            title : Messages.fm_noname,
+                            atime : 0,
+                            ctime : 0
+                        }
+                    }
                     if (prop === 'atime' || prop === 'ctime') {
                         return new Date(e[prop]);
                     }
@@ -1106,8 +1119,9 @@ define([
                 $element.data('path', path);
                 $element.click(function(e) {
                     e.stopPropagation();
-                    onElementClick($element, path);
+                    onElementClick(e, $element, path);
                 });
+                $element.contextmenu(openDefaultContextMenu);
                 addDragAndDropHandlers($element, path, false, false);
                 $container.append($element);
             });
@@ -1129,8 +1143,9 @@ define([
                 });
                 $element.click(function(e) {
                     e.stopPropagation();
-                    onElementClick($element);
+                    onElementClick(e, $element);
                 });
+                $element.contextmenu(openDefaultContextMenu);
                 $container.append($element);
             });
         };
@@ -1178,6 +1193,7 @@ define([
         // NOTE: Elements in the trash are not using the same storage structure as the others
         // _WORKGROUP_ : do not change the lastOpenedFolder value in localStorage
         var displayDirectory = module.displayDirectory = function (path, force) {
+            if (!APP.editable) { debug("Read-only mode"); }
             if (!appStatus.isReady && !force) { return; }
             // Only Trash and Root are available in not-owned files manager
             if (isWorkgroup() && !filesOp.isPathInTrash(path) && !filesOp.isPathInRoot(path)) {
@@ -1424,6 +1440,7 @@ define([
             $trashTreeContextMenu.hide();
             $trashContextMenu.hide();
             $contentContextMenu.hide();
+            $defaultContextMenu.hide();
         };
 
         var stringifyPath = function (path) {
@@ -1460,6 +1477,16 @@ define([
             return $div.html();
         };
 
+        var getReadOnlyUrl = APP.getRO = function (href) {
+            if (!filesOp.isFile(href)) { return; }
+            var i = href.indexOf('#') + 1;
+            var hash = href.slice(i);
+            var base = href.slice(0, i);
+            var hrefsecret = Cryptpad.getSecrets(hash);
+            var viewHash = Cryptpad.getViewHashFromKeys(hrefsecret.channel, hrefsecret.keys);
+            return base + viewHash;
+        };
+
         $contextMenu.on("click", "a", function(e) {
             e.stopPropagation();
             var path = $(this).data('path');
@@ -1478,6 +1505,12 @@ define([
             else if ($(this).hasClass('open')) {
                 $element.dblclick();
             }
+            else if ($(this).hasClass('open_ro')) {
+                var el = filesOp.findElement(files, path);
+                if (filesOp.isFolder(el)) { return; }
+                var roUrl = getReadOnlyUrl(el);
+                openFile(roUrl);
+            }
             else if ($(this).hasClass('newfolder')) {
                 var onCreated = function (info) {
                     module.newFolder = info.newPath;
@@ -1488,6 +1521,27 @@ define([
             module.hideMenu();
         });
 
+        $defaultContextMenu.on("click", "a", function(e) {
+            e.stopPropagation();
+            var path = $(this).data('path');
+            var $element = $(this).data('element');
+            if (!$element || !path || path.length < 2) {
+                log(Messages.fm_forbidden);
+                debug("Directory context menu on a forbidden or unexisting element. ", $element, path);
+                return;
+            }
+            if ($(this).hasClass('open')) {
+                $element.dblclick();
+            }
+            else if ($(this).hasClass('open_ro')) {
+                var el = filesOp.findElement(files, path);
+                if (filesOp.isFolder(el)) { return; }
+                var roUrl = getReadOnlyUrl(el);
+                openFile(roUrl);
+            }
+            module.hideMenu();
+        });
+
         $contentContextMenu.on('click', 'a', function (e) {
             e.stopPropagation();
             var path = $(this).data('path');
@@ -1570,12 +1624,6 @@ define([
         $(ifrw).on('mouseup drop', function (e) {
             $iframe.find('.droppable').removeClass('droppable');
         });
-        $(ifrw).on('keydown', function (e) {
-            pressKey(e.which, true);
-        });
-        $(ifrw).on('keyup', function (e) {
-            pressKey(e.which, false);
-        });
         $(ifrw).on('keydown', function (e) {
             // "Del"
             if (e.which === 46) {
@@ -1761,10 +1809,13 @@ define([
         };
         var onDisconnect = function (info) {
             setEditable(false);
+            if (APP.refresh) { APP.refresh(); }
+            APP.toolbar.failed();
             Cryptpad.alert(Messages.common_connectionLost);
         };
         var onReconnect = function (info) {
             setEditable(true);
+            if (APP.refresh) { APP.refresh(); }
             APP.toolbar.reconnecting(info.myId);
             Cryptpad.findOKButton().click();
         };