Explorar o código

[frontend] fix issues related to file path encoding in file browser

- Single quotes supported in file name when viewing file

- Ampersand supported in file and folder names

- Current path correctly displayed when editing

- Destination folder path correctly displayed when uploading files

- hash character in file name supported when viewing file

- plus character supported in file name
Björn Alm %!s(int64=3) %!d(string=hai) anos
pai
achega
9d774afeaa

+ 8 - 1
apps/filebrowser/src/filebrowser/templates/display.mako

@@ -242,7 +242,14 @@ ${ fb_components.menubar() }
       mode: viewModel.mode()
       mode: viewModel.mode()
     };
     };
 
 
-    $.getJSON(_baseUrl, params, function (data) {
+    // Singel quotes (') encoded as Unicode Hex Character
+    // will cause the $.getJSON call to produce an incorrect url, 
+    // so we remove the encoding and use plain single quotes.
+    let contentUrl = _baseUrl.replaceAll(''', "'");
+    // Entity encoded ampersand doesn't work in file or folder names and
+    // needs to be replaced with '&'
+    contentUrl = contentUrl.replaceAll('&', "&");
+    $.getJSON(contentUrl, params, function (data) {
       var _html = "";
       var _html = "";
 
 
       viewModel.file(ko.mapping.fromJS(data, { 'ignore': ['view.contents', 'view.xxd'] }));
       viewModel.file(ko.mapping.fromJS(data, { 'ignore': ['view.contents', 'view.xxd'] }));

+ 4 - 3
apps/filebrowser/src/filebrowser/templates/fb_components.mako

@@ -68,7 +68,7 @@ else:
               <a data-bind="text: label, click: show, attr: {'href': '${url('filebrowser:filebrowser.views.view', path=urlencode(''))}' + url}"></a><span class="divider">/</span>
               <a data-bind="text: label, click: show, attr: {'href': '${url('filebrowser:filebrowser.views.view', path=urlencode(''))}' + url}"></a><span class="divider">/</span>
             </li
             </li
           ></ul>
           ></ul>
-          <input id="hueBreadcrumbText" type="text" style="display:none" data-bind="value: currentPath" autocomplete="off" class="input-xxlarge" />
+          <input id="hueBreadcrumbText" type="text" style="display:none" data-bind="value: currentDecodedPath" autocomplete="off" class="input-xxlarge" />          
         </li>
         </li>
         % if is_trash_enabled:
         % if is_trash_enabled:
         <li class="pull-right">
         <li class="pull-right">
@@ -86,9 +86,10 @@ else:
           % for breadcrumb_item in breadcrumbs:
           % for breadcrumb_item in breadcrumbs:
             <% label, f_url = breadcrumb_item['label'], breadcrumb_item['url'] %>
             <% label, f_url = breadcrumb_item['label'], breadcrumb_item['url'] %>
             %if label[-1] == '/':
             %if label[-1] == '/':
-            <li><a data-bind="hueLink: '${'/filebrowser/view=' + f_url}'"><span class="divider">${label}</span></a></li>
+            ## Backticks are used as quotes to handle single quote in f_url
+            <li><a data-bind="hueLink: `${'/filebrowser/view=' + f_url}`"><span class="divider">${label}</span></a></li>
             %else:
             %else:
-            <li><a data-bind="hueLink: '${'/filebrowser/view=' + f_url}'">${label}</a><span class="divider">/</span></li>
+            <li><a data-bind="hueLink: `${'/filebrowser/view=' + f_url}`">${label}</a><span class="divider">/</span></li>
             %endif
             %endif
           % endfor
           % endfor
           </ul>
           </ul>

+ 17 - 3
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -443,7 +443,7 @@ else:
   <div id="uploadFileModal" class="modal hide fade">
   <div id="uploadFileModal" class="modal hide fade">
     <div class="modal-header">
     <div class="modal-header">
       <button type="button" class="close" data-dismiss="modal" aria-label="${ _('Close') }"><span aria-hidden="true">&times;</span></button>
       <button type="button" class="close" data-dismiss="modal" aria-label="${ _('Close') }"><span aria-hidden="true">&times;</span></button>
-      <h2 class="modal-title">${_('Upload to')} <span data-bind="text: currentPath"></span></h2>
+      <h2 class="modal-title">${_('Upload to')} <span data-bind="text: currentDecodedPath"></span></h2>
     </div>
     </div>
     <div class="modal-body form-inline">
     <div class="modal-body form-inline">
       <div id="fileUploader" class="uploader">
       <div id="fileUploader" class="uploader">
@@ -1055,7 +1055,16 @@ else:
       };
       };
 
 
       self.currentPath = ko.observable(currentDirPath);
       self.currentPath = ko.observable(currentDirPath);
+      self.currentDecodedPath = ko.observable(decodeURI(currentDirPath));      
+
       self.currentPath.subscribe(function (path) {
       self.currentPath.subscribe(function (path) {
+        // currentEditablePath can be edited by the user in #hueBreadcrumbText.
+        // It is using a decoded currentPath in order to correctly display non ASCII characters        
+        const decodedPath = decodeURI(path);
+        if (decodedPath !== self.currentDecodedPath()) {          
+          self.currentDecodedPath(decodedPath);
+        } 
+        
         $(document).trigger('currPathLoaded', { path: path });
         $(document).trigger('currPathLoaded', { path: path });
       });
       });
 
 
@@ -1367,10 +1376,15 @@ else:
           self.targetPath(file.url);
           self.targetPath(file.url);
           updateHash(stripHashes(file.path));
           updateHash(stripHashes(file.path));
         } else {
         } else {
+          
+          // Fix to encode potential hash characters in a file name when viewing a file
+          const url = file.name.indexOf('#') >= 0 && file.url.indexOf('#') >= 0 
+            ? file.url.replaceAll('#', encodeURIComponent('#')) : file.url;
+
           %if is_embeddable:
           %if is_embeddable:
-          huePubSub.publish('open.link', file.url);
+          huePubSub.publish('open.link', url);
           %else:
           %else:
-          window.location.href = file.url;
+          window.location.href = url;
           %endif
           %endif
         }
         }
       };
       };

+ 4 - 1
desktop/core/src/desktop/js/ext/fileuploader.custom.js

@@ -1260,7 +1260,10 @@ qq.extend(qq.UploadHandlerXhr.prototype, {
         formData.append(params.fileFieldLabel, file);
         formData.append(params.fileFieldLabel, file);
         formData.append('dest', params.dest);
         formData.append('dest', params.dest);
 
 
-        var action = this._options.action + "?dest=" + params.dest;
+        // Encoding is needed to support folder names with some special 
+        // non alfanumeric characters like plus (+) and ampersand (&)
+        var destination = encodeURIComponent(params.dest);
+        var action = this._options.action + "?dest=" + destination;
         xhr.open("POST", action, true);
         xhr.open("POST", action, true);
         xhr.send(formData);
         xhr.send(formData);
     },
     },

+ 18 - 1
desktop/core/src/desktop/js/onePageViewModel.js

@@ -831,13 +831,30 @@ class OnePageViewModel {
       });
       });
     });
     });
 
 
+    // The page library encodes the plus character (+) as a space in the returning
+    // params object. That causes the path used by the file viewer to break and this
+    // is a defensive fix to handle that while still having access to the other ctx params
+    // needed to "revert" the incorrect encoding.
+    const fixPlusCharTurnedIntoSpace = (ctx, mapping) => {
+      let correctCtxParams = ctx.params;
+      if (mapping.app === 'filebrowser' && ctx.path.indexOf('+') >= 0) {
+        if (ctx.params[0] && ctx.params[0].indexOf(' ') >= 0) {
+          const pathWithoutParams = mapping.url.slice(0, -1);
+          const modifiedParam = decodeURIComponent(ctx.path.replace(pathWithoutParams, ''));
+          correctCtxParams = { 0: modifiedParam };
+        }
+      }
+      return correctCtxParams;
+    };
+
     pageMapping.forEach(mapping => {
     pageMapping.forEach(mapping => {
       page(
       page(
         mapping.url,
         mapping.url,
         _.isFunction(mapping.app)
         _.isFunction(mapping.app)
           ? mapping.app
           ? mapping.app
           : ctx => {
           : ctx => {
-              self.currentContextParams(ctx.params);
+              const ctxParams = fixPlusCharTurnedIntoSpace(ctx, mapping);
+              self.currentContextParams(ctxParams);
               self.currentQueryString(ctx.querystring);
               self.currentQueryString(ctx.querystring);
               self.loadApp(mapping.app);
               self.loadApp(mapping.app);
             }
             }