浏览代码

HUE-8750 [editor] Add knox service definition.

jdesjean 6 年之前
父节点
当前提交
b621aa99de
共有 54 个文件被更改,包括 519 次插入264 次删除
  1. 1 1
      apps/about/src/about/templates/admin_wizard.mako
  2. 1 1
      apps/beeswax/src/beeswax/static/beeswax/js/beeswax.vm.js
  3. 3 3
      apps/beeswax/src/beeswax/templates/execute.mako
  4. 5 5
      apps/beeswax/src/beeswax/urls.py
  5. 4 2
      apps/filebrowser/src/filebrowser/templates/display.mako
  6. 7 5
      apps/filebrowser/src/filebrowser/templates/fb_components.mako
  7. 10 14
      apps/filebrowser/src/filebrowser/templates/listdir_components.mako
  8. 62 28
      apps/filebrowser/src/filebrowser/views.py
  9. 24 1
      apps/jobbrowser/src/jobbrowser/templates/job_browser.mako
  10. 6 6
      apps/jobbrowser/src/jobbrowser/urls.py
  11. 1 1
      apps/metastore/src/metastore/urls.py
  12. 3 1
      apps/oozie/src/oozie/templates/dashboard/list_oozie_bundle.mako
  13. 3 1
      apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinator.mako
  14. 54 54
      apps/oozie/src/oozie/urls.py
  15. 16 16
      apps/oozie/src/oozie/views/dashboard.py
  16. 6 9
      apps/oozie/src/oozie/views/editor2.py
  17. 8 8
      apps/pig/src/pig/urls.py
  18. 3 1
      apps/pig/src/pig/views.py
  19. 2 2
      apps/rdbms/src/rdbms/urls.py
  20. 1 1
      apps/search/src/search/urls.py
  21. 1 1
      apps/useradmin/src/useradmin/templates/edit_user.mako
  22. 3 3
      desktop/core/src/desktop/js/api/apiHelper.js
  23. 1 1
      desktop/core/src/desktop/js/apps/notebook/snippet.js
  24. 1 1
      desktop/core/src/desktop/js/apps/notebook2/snippet.js
  25. 1 1
      desktop/core/src/desktop/js/assist/assistStorageEntry.js
  26. 1 1
      desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js
  27. 1 1
      desktop/core/src/desktop/js/ko/bindings/ko.hueLink.js
  28. 24 15
      desktop/core/src/desktop/js/onePageViewModel.js
  29. 2 2
      desktop/core/src/desktop/js/sql/sqlWorkerHandler.js
  30. 2 1
      desktop/core/src/desktop/js/utils/hueUtils.js
  31. 2 0
      desktop/core/src/desktop/lib/paths.py
  32. 4 4
      desktop/core/src/desktop/models.py
  33. 2 2
      desktop/core/src/desktop/static/desktop/js/document/hueFileEntry.js
  34. 10 6
      desktop/core/src/desktop/templates/common_header.mako
  35. 15 1
      desktop/core/src/desktop/templates/common_header_footer_components.mako
  36. 2 2
      desktop/core/src/desktop/templates/common_header_m.mako
  37. 1 1
      desktop/core/src/desktop/templates/common_notebook_ko_components.mako
  38. 7 0
      desktop/core/src/desktop/templates/global_js_constants.mako
  39. 7 5
      desktop/core/src/desktop/templates/hue.mako
  40. 1 1
      desktop/core/src/desktop/templates/logs.mako
  41. 8 8
      desktop/core/src/desktop/urls.py
  42. 1 1
      desktop/libs/dashboard/src/dashboard/static/dashboard/js/collections.ko.js
  43. 1 2
      desktop/libs/dashboard/src/dashboard/templates/common_search.mako
  44. 1 1
      desktop/libs/dashboard/src/dashboard/urls.py
  45. 3 3
      desktop/libs/indexer/src/indexer/static/indexer/js/collections.js
  46. 4 4
      desktop/libs/indexer/src/indexer/static/indexer/js/indexes.ko.js
  47. 2 2
      desktop/libs/indexer/src/indexer/templates/importer.mako
  48. 27 27
      desktop/libs/indexer/src/indexer/urls.py
  49. 3 2
      desktop/libs/notebook/src/notebook/connectors/hiveserver2.py
  50. 2 1
      desktop/libs/notebook/src/notebook/models.py
  51. 1 1
      desktop/libs/notebook/src/notebook/templates/editor_m.mako
  52. 3 3
      desktop/libs/notebook/src/notebook/urls.py
  53. 118 0
      tools/knox/hue/1.0.0/rewrite.xml
  54. 37 0
      tools/knox/hue/1.0.0/service.xml

+ 1 - 1
apps/about/src/about/templates/admin_wizard.mako

@@ -376,7 +376,7 @@ $(document).ready(function(){
   });
 
   $("#doneBtn").click(function () {
-    location.href = "${ is_embeddable and '/' or url('desktop_views_home2') }";
+    huePubSub.publish('open.link', "${ is_embeddable and '/' or url('desktop_views_home2') }");
   });
 
   $(".updatePreferences").click(function () {

+ 1 - 1
apps/beeswax/src/beeswax/static/beeswax/js/beeswax.vm.js

@@ -931,7 +931,7 @@ function BeeswaxViewModel(server, apiHelper) {
             self.design.results.save.errors(null);
 
             var redirect_fn = function() {
-              window.location.href = data.success_url + (data.success_url.indexOf("?") > -1 ? "&" : "?") + "refresh=true";
+              huePubSub.publish('open.link', data.success_url + (data.success_url.indexOf("?") > -1 ? "&" : "?") + "refresh=true");
               self.design.isRunning(false);
             };
             if (data.id) {

+ 3 - 3
apps/beeswax/src/beeswax/templates/execute.mako

@@ -2398,7 +2398,7 @@ $(document).on('server.unmanageable_error', function (e, responseText) {
 $(document).on('saved.design', function (e, id) {
   $('#saveAs').modal('hide');
   $(document).trigger('info', "${_('Query saved.')}");
-  window.location.href = "/${ app_name }/execute/design/" + id;
+  huePubSub.publish('open.link', "/${ app_name }/execute/design/" + id);
 });
 $(document).on('error_save.design', function (e, message) {
   var _message = "${_('Could not save design')}";
@@ -2867,12 +2867,12 @@ viewModel.design.fileResources.values.subscribe(function() {
     var successUrl = "${request.GET['on_success_url']}";
     if (viewModel.design.watch.errors().length != 0) {
       window.setTimeout(function(){
-        window.location.href = successUrl + (successUrl.indexOf("?") > -1 ? "&" : "?") + "error=" + encodeURIComponent(viewModel.design.watch.errors().join("\n"));
+        huePubSub.publish('open.link', successUrl + (successUrl.indexOf("?") > -1 ? "&" : "?") + "error=" + encodeURIComponent(viewModel.design.watch.errors().join("\n")));
       }, 200);
     }
     else if (viewModel.design.results.errors().length == 0) {
       window.setTimeout(function(){
-        window.location.href = successUrl + (successUrl.indexOf("?") > -1 ? "&" : "?") + "refresh=true";
+        huePubSub.publish('open.link', successUrl + (successUrl.indexOf("?") > -1 ? "&" : "?") + "refresh=true");
       }, 1000);
     }
   });

+ 5 - 5
apps/beeswax/src/beeswax/urls.py

@@ -67,12 +67,12 @@ urlpatterns += [
   url(r'^api/autocomplete/(?P<database>\w+)/(?P<table>\w+)/(?P<column>\w+)/?$', beeswax_api.autocomplete, name='api_autocomplete_column'),
   url(r'^api/autocomplete/(?P<database>\w+)/(?P<table>\w+)/(?P<column>\w+)/(?P<nested>.+)/?$', beeswax_api.autocomplete, name='api_autocomplete_nested'),
 
-  url(r'^api/design/(?P<design_id>\d+)?$', beeswax_api.save_query_design, name='api_save_design'),
+  url(r'^api/design(?:/(?P<design_id>\d+))?/?$', beeswax_api.save_query_design, name='api_save_design'),
   url(r'^api/design/(?P<design_id>\d+)/get$', beeswax_api.fetch_saved_design, name='api_fetch_saved_design'),
 
   url(r'^api/query/(?P<query_history_id>\d+)/get$', beeswax_api.fetch_query_history, name='api_fetch_query_history'),
   url(r'^api/query/parameters$', beeswax_api.parameters, name='api_parameters'),
-  url(r'^api/query/execute/(?P<design_id>\d+)?$', beeswax_api.execute, name='api_execute'),
+  url(r'^api/query/execute(?:/(?P<design_id>\d+))?/?$', beeswax_api.execute, name='api_execute'),
   url(r'^api/query/(?P<query_history_id>\d+)/cancel/?$', beeswax_api.cancel_query, name='api_cancel_query'),
   url(r'^api/query/(?P<query_history_id>\d+)/close/?$', beeswax_api.close_operation, name='api_close_operation'),
   url(r'^api/query/(?P<query_history_id>\d+)/results/save/hive/table/?$', beeswax_api.save_results_hive_table, name='api_save_results_hive_table'),
@@ -86,8 +86,8 @@ urlpatterns += [
   url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/indexes/?$', beeswax_api.get_indexes, name='get_indexes'),
   url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/sample/?$', beeswax_api.get_sample_data, name='get_sample_data'),
   url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/(?P<column>\w+)/sample/?$', beeswax_api.get_sample_data, name='get_sample_data_column'),
-  url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/stats/(?P<column>\w+)?$', beeswax_api.get_table_stats, name='get_table_stats'),
-  url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/terms/(?P<column>\w+)/(?P<prefix>\w+)?$', beeswax_api.get_top_terms, name='get_top_terms'),
+  url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/stats(?:/(?P<column>\w+))?/?$', beeswax_api.get_table_stats, name='get_table_stats'),
+  url(r'^api/table/(?P<database>\w+)/(?P<table>\w+)/terms/(?P<column>\w+)(?:/(?P<prefix>\w+))?/?$', beeswax_api.get_top_terms, name='get_top_terms'),
 
-  url(r'^api/analyze/(?P<database>\w+)/(?P<table>\w+)/(?P<columns>\w+)?$', beeswax_api.analyze_table, name='analyze_table'),
+  url(r'^api/analyze/(?P<database>\w+)/(?P<table>\w+)(?:/(?P<columns>\w+))?/?$', beeswax_api.analyze_table, name='analyze_table'),
 ]

+ 4 - 2
apps/filebrowser/src/filebrowser/templates/display.mako

@@ -16,13 +16,15 @@
 ## limitations under the License.
 <%!
   import datetime
+  import urllib
   from django.template.defaultfilters import urlencode, stringformat, date, filesizeformat, time
   from filebrowser.views import truncate
+  from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
 %>
 <%
-  path_enc = path
+  path_enc = urllib.quote(path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)
   dirname_enc = urlencode(view['dirname'])
   base_url = url('filebrowser.views.view', path=path_enc)
   edit_url = url('filebrowser_views_edit', path=path_enc)
@@ -372,7 +374,7 @@ ${ fb_components.menubar() }
     }
 
     self.downloadFile = function () {
-      location.href = "${url('filebrowser_views_download', path=path_enc)}";
+      huePubSub.publish('open.link', "${url('filebrowser_views_download', path=path_enc)}");
     };
 
     self.pageChanged = function () {

+ 7 - 5
apps/filebrowser/src/filebrowser/templates/fb_components.mako

@@ -15,6 +15,8 @@
 ## limitations under the License.
 <%!
 import datetime
+import urllib
+from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
 
 from django.template.defaultfilters import urlencode, stringformat, date, filesizeformat, time
 from django.utils.translation import ugettext as _
@@ -38,7 +40,7 @@ from aws import get_client
             </span>
           </li>
         %else:
-          <li><a class="pointer breadcrumb-link homeLink" data-bind="click: $root.openHome, attr:{'href': '${url('filebrowser.views.view', path=urlencode(path))}?default_to_home'}">
+          <li><a class="pointer breadcrumb-link homeLink" data-bind="click: $root.openHome, attr:{'href': window.HUE_BASE_URL + '/filebrowser/view=${ urlencode(path) }?default_to_home'}">
             <i class="fa fa-home"></i> ${_('Home')}</a>
           </li>
         %endif
@@ -55,7 +57,7 @@ from aws import get_client
         </li>
         % if is_trash_enabled:
         <li class="pull-right">
-          <a class="pointer breadcrumb-link trashLink" data-bind="click: $root.openTrash, attr:{'href': '${url('filebrowser.views.view', path=urlencode(path))}?default_to_trash'}" title="${_('View trash')}">
+          <a class="pointer breadcrumb-link trashLink" data-bind="click: $root.openTrash, attr:{'href': window.HUE_BASE_URL + '/filebrowser/view=${ urlencode(path) }?default_to_trash'}" title="${_('View trash')}">
             <i class="fa fa-trash-o"></i> ${_('Trash')}
           </a>
         </li>
@@ -63,15 +65,15 @@ from aws import get_client
       </ul>
     % else:
       <ul class="nav nav-pills hue-breadcrumbs-bar">
-        <li><a href="${url('filebrowser.views.view', path=urlencode(path))}?default_to_home" class="breadcrumb-link homeLink"><i class="fa fa-home"></i> ${_('Home')}</a></li>
+        <li><a data-bind="attr: {href: window.HUE_BASE_URL + '/filebrowser/view=${ urlencode(path) }?default_to_home'}" class="breadcrumb-link homeLink"><i class="fa fa-home"></i> ${_('Home')}</a></li>
         <li>
           <ul class="hue-breadcrumbs" style="padding-right:40px; padding-top: 12px">
           % for breadcrumb_item in breadcrumbs:
             <% label, f_url = breadcrumb_item['label'], breadcrumb_item['url'] %>
             %if label[-1] == '/':
-            <li><a href="${url('filebrowser.views.view', path=f_url)}"><span class="divider">${label}</span></a></li>
+            <li><a data-bind="hueLink: '${'/filebrowser/view=' + f_url}'"><span class="divider">${label}</span></a></li>
             %else:
-            <li><a href="${url('filebrowser.views.view', path=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
           % endfor
           </ul>

+ 10 - 14
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -787,7 +787,7 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
     }
 
     var updateHash = function (hash) {
-      hash = encodeURI(hash);
+      hash = encodeURI(decodeURIComponent(hash));
       %if not is_embeddable:
       window.location.hash = hash;
       %else:
@@ -826,7 +826,7 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
       return {
         name: file.name,
         path: file.path,
-        url: encodeURI(file.url),
+        url: file.url,
         type: file.type,
         permissions: file.rwx,
         mode: file.mode,
@@ -963,7 +963,6 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
           return _ret;
         }
       }
-
       self.files = ko.observableArray(ko.utils.arrayMap(files, function (file) {
         return new File(file);
       }));
@@ -1166,7 +1165,7 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
           }
 
           if (data.type != null && data.type == "file") {
-            window.location.href = data.url;
+            huePubSub.publish('open.link', data.url);
             return false;
           }
 
@@ -1325,7 +1324,7 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
           self.targetPageNum(1);
           self.enableFilterAfterSearch = false;
           self.searchQuery("");
-          self.targetPath("${url('filebrowser.views.view', path='')}" + stripHashes(file.path));
+          self.targetPath(file.url);
           updateHash(stripHashes(file.path));
         } else {
           %if is_embeddable:
@@ -1337,11 +1336,11 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
       };
 
       self.editFile = function () {
-        window.location.href = "${url('filebrowser_views_edit', path='')}" + encodeURI(self.selectedFile().path);
+        huePubSub.publish('open.link', self.selectedFile().url.replace("${url('filebrowser.views.view', path='')}", "${url('filebrowser_views_edit', path='')}"));
       };
 
       self.downloadFile = function () {
-        window.location.href = "${url('filebrowser_views_download', path='')}" + encodeURI(self.selectedFile().path);
+        huePubSub.publish('open.link', self.selectedFile().url.replace("${url('filebrowser.views.view', path='')}", "${url('filebrowser_views_download', path='')}"));
       };
 
       self.renameFile = function () {
@@ -2398,17 +2397,14 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
       var hashchange = function () {
         if (window.location.pathname.indexOf('/filebrowser') > -1) {
           var targetPath = "";
-          var hash = window.location.hash.substring(1);
-          if (hash.search(/(<([^>]+)>)/ig) > -1) {
-            hash = encodeURI(hash);
-          }
+          var hash = decodeURI(window.location.hash.substring(1));
           if (hash != null && hash != "" && hash.indexOf('/') > -1) {
             targetPath = "${url('filebrowser.views.view', path='')}";
             if (hash.indexOf("!!") != 0) {
-              targetPath += stripHashes(hash);
+              targetPath += stripHashes(encodeURIComponent(hash));
             }
             else {
-              targetPath = viewModel.targetPath() + hash;
+              targetPath = viewModel.targetPath() + encodeURI(hash);
             }
             viewModel.targetPageNum(1)
           }
@@ -2452,7 +2448,7 @@ from filebrowser.conf import ENABLE_EXTRACT_UPLOADED_ARCHIVE
               %if is_embeddable:
               huePubSub.publish('open.link', data.url);
               %else:
-              window.location.href = data.url;
+              huePubSub.publish('open.link', data.url);
               %endif
               return false;
             } else {

+ 62 - 28
apps/filebrowser/src/filebrowser/views.py

@@ -56,6 +56,7 @@ from desktop.lib.export_csvxls import file_reader
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.fs import splitpath
 from desktop.lib.i18n import smart_str
+from desktop.lib.paths import SAFE_CHARACTERS_URI, SAFE_CHARACTERS_URI_COMPONENTS
 from desktop.lib.tasks.compress_files.compress_utils import compress_files_in_hdfs
 from desktop.lib.tasks.extract_archive.extract_utils import extract_archive_in_hdfs
 from desktop.views import serve_403_error
@@ -125,6 +126,9 @@ def download(request, path):
     This is inspired by django.views.static.serve.
     ?disposition={attachment, inline}
     """
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     if not SHOW_DOWNLOAD_BUTTON.get():
         return serve_403_error(request)
     if not request.fs.exists(path):
@@ -158,7 +162,7 @@ def download(request, path):
       response = StreamingHttpResponse(file_reader(fh), content_type=content_type)
       response["Last-Modified"] = http_date(stats['mtime'])
       response["Content-Length"] = stats['size']
-      response['Content-Disposition'] = request.GET.get('disposition', 'attachment') if _can_inline_display(path) else 'attachment'
+      response['Content-Disposition'] = request.GET.get('disposition', 'attachment; filename="' + stats['name'] + '"') if _can_inline_display(path) else 'attachment'
 
     request.audit = {
         'operation': 'DOWNLOAD',
@@ -171,24 +175,29 @@ def download(request, path):
 
 def view(request, path):
     """Dispatches viewing of a path to either index() or fileview(), depending on type."""
-
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
+    if request.GET.get('format') == 'json':
+      prefix = '' 
+    else:
+      prefix = '/hue'
     # default_to_home is set in bootstrap.js
     if 'default_to_home' in request.GET:
         home_dir_path = request.user.get_home_directory()
         if request.fs.isdir(home_dir_path):
-            return format_preserving_redirect(request, reverse(view, kwargs=dict(path=home_dir_path)))
+            return format_preserving_redirect(request, prefix + '/filebrowser/view=' + urllib.quote(home_dir_path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS))
 
     # default_to_home is set in bootstrap.js
     if 'default_to_trash' in request.GET:
-        if request.fs.isdir(_home_trash_path(request.fs, request.user, path)):
-            return format_preserving_redirect(request, reverse(view, kwargs=dict(path=_home_trash_path(request.fs, request.user, path))))
-        if request.fs.isdir(request.fs.trash_path(path)):
-            return format_preserving_redirect(request, reverse(view, kwargs=dict(path=request.fs.trash_path(path))))
+        home_trash_path = _home_trash_path(request.fs, request.user, path)
+        if request.fs.isdir(home_trash_path):
+            return format_preserving_redirect(request, prefix + '/filebrowser/view=' + urllib.quote(home_trash_path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS))
+        trash_path = request.fs.trash_path(path)
+        if request.fs.isdir(trash_path):
+            return format_preserving_redirect(request, prefix + '/filebrowser/view=' + urllib.quote(trash_path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS))
 
     try:
-        decoded_path = urllib.unquote(path)
-        if path != decoded_path:
-          path = decoded_path
         stats = request.fs.stats(path)
         if stats.isDir:
             return listdir_paged(request, path)
@@ -227,6 +236,9 @@ def _home_trash_path(fs, user, path):
 
 
 def home_relative_view(request, path):
+  decoded_path = urllib.unquote(path)
+  if path != decoded_path:
+    path = decoded_path
   home_dir_path = request.user.get_home_directory()
   if request.fs.exists(home_dir_path):
     path = '%s%s' % (home_dir_path, path)
@@ -236,7 +248,9 @@ def home_relative_view(request, path):
 
 def edit(request, path, form=None):
     """Shows an edit form for the given path. Path does not necessarily have to exist."""
-
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     try:
         stats = request.fs.stats(path)
     except IOError, ioe:
@@ -292,6 +306,9 @@ def save_file(request):
     form = EditorForm(request.POST)
     is_valid = form.is_valid()
     path = form.cleaned_data.get('path')
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
 
     if request.POST.get('save') == "Save As":
         if not is_valid:
@@ -328,7 +345,7 @@ def parse_breadcrumbs(path):
       if url and not url.endswith('/'):
         url += '/'
       url += part
-      breadcrumbs.append({'url': urllib.quote(url.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\''), 'label': part})
+      breadcrumbs.append({'url': urllib.quote(url.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS), 'label': part})
     return breadcrumbs
 
 
@@ -338,6 +355,9 @@ def listdir(request, path):
 
     Intended to be called via view().
     """
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     if not request.fs.isdir(path):
         raise PopupException(_("Not a directory: %(path)s") % {'path': path})
 
@@ -353,8 +373,8 @@ def listdir(request, path):
         'path': path,
         'file_filter': file_filter,
         'breadcrumbs': breadcrumbs,
-        'current_dir_path': urllib.quote(path.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\''),
-        'current_request_path': urllib.quote(request.path.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\''),
+        'current_dir_path': urllib.quote(path.encode('utf-8'), safe=SAFE_CHARACTERS_URI),
+        'current_request_path': '/filebrowser/view=' + urllib.quote(path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS),
         'home_directory': home_dir_path if home_dir_path and request.fs.isdir(home_dir_path) else None,
         'cwd_set': True,
         'is_superuser': request.user.username == request.fs.superuser,
@@ -417,6 +437,9 @@ def listdir_paged(request, path):
       filter=?          - Specify a substring filter to search for in
                           the filename field.
     """
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     if not request.fs.isdir(path):
         raise PopupException("Not a directory: %s" % (path,))
 
@@ -497,7 +520,7 @@ def listdir_paged(request, path):
     data = {
         'path': path,
         'breadcrumbs': breadcrumbs,
-        'current_request_path': urllib.quote(request.path.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\''),
+        'current_request_path': '/filebrowser/view=' + urllib.quote(path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS),
         'is_trash_enabled': is_trash_enabled,
         'files': page.object_list if page else [],
         'page': _massage_page(page, paginator) if page else {},
@@ -507,7 +530,7 @@ def listdir_paged(request, path):
         # The following should probably be deprecated
         'cwd_set': True,
         'file_filter': 'any',
-        'current_dir_path': urllib.quote(path.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\''),
+        'current_dir_path': urllib.quote(path.encode('utf-8'), safe=SAFE_CHARACTERS_URI),
         'is_fs_superuser': is_fs_superuser,
         'groups': is_fs_superuser and [str(x) for x in Group.objects.values_list('name', flat=True)] or [],
         'users': is_fs_superuser and [str(x) for x in User.objects.values_list('username', flat=True)] or [],
@@ -540,7 +563,7 @@ def _massage_stats(request, stats):
     path = stats['path']
     normalized = request.fs.normpath(path)
     return {
-        'path': normalized,
+        'path': normalized, # Normally this should be quoted, but we only use this in POST request so we're ok. Changing this to quoted causes many issues.
         'name': stats['name'],
         'stats': stats.to_json_dict(),
         'mtime': datetime.fromtimestamp(stats['mtime']).strftime('%B %d, %Y %I:%M %p') if stats['mtime'] else '',
@@ -548,7 +571,7 @@ def _massage_stats(request, stats):
         'type': filetype(stats['mode']),
         'rwx': rwx(stats['mode'], stats['aclBit']),
         'mode': stringformat(stats['mode'], "o"),
-        'url': reverse('filebrowser.views.view', kwargs=dict(path=normalized)),
+        'url': '/filebrowser/view=' + urllib.quote(normalized.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS),
         'is_sentry_managed': request.fs.is_sentry_managed(path)
     }
 
@@ -560,6 +583,9 @@ def stat(request, path):
     Intended for use via AJAX (and hence doesn't provide
     an HTML view).
     """
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     if not request.fs.exists(path):
         raise Http404(_("File not found: %(path)s") % {'path': escape(path)})
     stats = request.fs.stats(path)
@@ -567,6 +593,9 @@ def stat(request, path):
 
 
 def content_summary(request, path):
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     if not request.fs.exists(path):
         raise Http404(_("File not found: %(path)s") % {'path': escape(path)})
     response = {'status': -1, 'message': '', 'summary': None}
@@ -596,6 +625,9 @@ def display(request, path):
     sequence files, decompress gzipped text files, etc.).
     There exists a python-magic package to interface with libmagic.
     """
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     if not request.fs.isfile(path):
         raise PopupException(_("Not a file: '%(path)s'") % {'path': path})
 
@@ -715,7 +747,9 @@ def read_contents(codec_type, path, fs, offset, length):
     """
     contents = ''
     fhandle = None
-
+    decoded_path = urllib.unquote(path)
+    if path != decoded_path:
+      path = decoded_path
     try:
         fhandle = fs.open(path)
         stats = fs.stats(path)
@@ -1102,7 +1136,7 @@ def rename(request):
           raise PopupException(_("Could not rename folder \"%s\" to \"%s\": Hashes are not allowed in filenames." % (src_path, dest_path)))
         if "/" not in dest_path:
             src_dir = os.path.dirname(src_path)
-            dest_path = request.fs.join(src_dir, dest_path)
+            dest_path = request.fs.join(urllib.unquote(src_dir), urllib.unquote(dest_path))
         if request.fs.exists(dest_path):
           raise PopupException(_('The destination path "%s" already exists.') % dest_path)
         request.fs.rename(src_path, dest_path)
@@ -1111,7 +1145,7 @@ def rename(request):
 
 def set_replication(request):
     def smart_set_replication(src_path, replication_factor):
-        result = request.fs.set_replication(src_path, replication_factor)
+        result = request.fs.set_replication(urllib.unquote(src_path), replication_factor)
         if not result:
             raise PopupException(_("Setting of replication factor failed"))
 
@@ -1124,7 +1158,7 @@ def mkdir(request):
         # No absolute directory specification allowed.
         if posixpath.sep in name or "#" in name:
             raise PopupException(_("Could not name folder \"%s\": Slashes or hashes are not allowed in filenames." % name))
-        request.fs.mkdir(request.fs.join(path, name))
+        request.fs.mkdir(request.fs.join(urllib.unquote(path), urllib.unquote(name)))
 
     return generic_op(MkDirForm, request, smart_mkdir, ["path", "name"], "path")
 
@@ -1144,7 +1178,7 @@ def rmtree(request):
     params = ["path"]
     def bulk_rmtree(*args, **kwargs):
         for arg in args:
-            request.fs.do_as_user(request.user, request.fs.rmtree, arg['path'], 'skip_trash' in request.GET)
+            request.fs.do_as_user(request.user, request.fs.rmtree, urllib.unquote(arg['path']), 'skip_trash' in request.GET)
     return generic_op(RmTreeFormSet, request, bulk_rmtree, ["path"], None,
                       data_extractor=formset_data_extractor(recurring, params),
                       arg_extractor=formset_arg_extractor,
@@ -1174,7 +1208,7 @@ def copy(request):
         for arg in args:
             if arg['src_path'] == arg['dest_path']:
                 raise PopupException(_('Source path and destination path cannot be same'))
-            request.fs.copy(arg['src_path'], arg['dest_path'], recursive=True, owner=request.user)
+            request.fs.copy(urllib.unquote(arg['src_path']), urllib.unquote(arg['dest_path']), recursive=True, owner=request.user)
     return generic_op(CopyFormSet, request, bulk_copy, ["src_path", "dest_path"], None,
                       data_extractor=formset_data_extractor(recurring, params),
                       arg_extractor=formset_arg_extractor,
@@ -1188,7 +1222,7 @@ def chmod(request):
     def bulk_chmod(*args, **kwargs):
         op = curry(request.fs.chmod, recursive=request.POST.get('recursive', False))
         for arg in args:
-            op(arg['path'], arg['mode'])
+            op(urllib.unquote(arg['path']), arg['mode'])
     # mode here is abused: on input, it's a string, but when retrieved,
     # it's an int.
     return generic_op(ChmodFormSet, request, bulk_chmod, ['path', 'mode'], "path",
@@ -1213,7 +1247,7 @@ def chown(request):
     def bulk_chown(*args, **kwargs):
         op = curry(request.fs.chown, recursive=request.POST.get('recursive', False))
         for arg in args:
-            varg = [arg[param] for param in param_names]
+            varg = [urllib.unquote(arg[param]) for param in param_names]
             op(*varg)
 
     return generic_op(ChownFormSet, request, bulk_chown, param_names, "path",
@@ -1228,7 +1262,7 @@ def trash_restore(request):
     params = ["path"]
     def bulk_restore(*args, **kwargs):
         for arg in args:
-            request.fs.do_as_user(request.user, request.fs.restore, arg['path'])
+            request.fs.do_as_user(request.user, request.fs.restore, urllib.unquote(arg['path']))
     return generic_op(RestoreFormSet, request, bulk_restore, ["path"], None,
                       data_extractor=formset_data_extractor(recurring, params),
                       arg_extractor=formset_arg_extractor,
@@ -1277,7 +1311,7 @@ def _upload_file(request):
 
     if form.is_valid():
         uploaded_file = request.FILES['hdfs_file']
-        dest = scheme_absolute_path(request.GET['dest'], form.cleaned_data['dest'])
+        dest = scheme_absolute_path(urllib.unquote(request.GET['dest']), urllib.unquote(form.cleaned_data['dest']))
         filepath = request.fs.join(dest, uploaded_file.name)
 
         if request.fs.isdir(dest) and posixpath.sep in uploaded_file.name:

+ 24 - 1
apps/jobbrowser/src/jobbrowser/templates/job_browser.mako

@@ -1428,7 +1428,7 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
           <!-- ko if: doc_url -->
           <li class="nav-header">${ _('Id') }</li>
           <li>
-            <a data-bind="attr: {href: doc_url}" target="_blank" title="${ _('Open in impalad') }">
+            <a data-bind="attr: { href: doc_url_modified }" target="_blank" title="${ _('Open in impalad') }">
               <span data-bind="text: id"></span>
             </a>
             <!-- ko if: $root.isMini() -->
@@ -2333,6 +2333,29 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
       });
       %endif
       self.doc_url = ko.observableDefault(job.doc_url);
+      self.doc_url_modified = ko.computed(function () {
+        var url = self.doc_url();
+        if (window.KNOX_BASE_URL.length && window.URL && url) { // KNOX
+          try {
+            var parsedDocUrl = new URL(url);
+            var parsedKnoxUrl = new URL(window.KNOX_BASE_URL);
+            parsedDocUrl.hostname = parsedKnoxUrl.hostname;
+            parsedDocUrl.protocol = parsedKnoxUrl.protocol;
+            parsedDocUrl.port = parsedKnoxUrl.port;
+            var service = url.indexOf('livy') >= 0 ? '/livy' : '/impala';
+            parsedDocUrl.pathname = parsedKnoxUrl.pathname + service + parsedDocUrl.pathname;
+            return parsedDocUrl.toString();
+          } catch (e) {
+            return url;
+          }
+        } else if (window.KNOX_BASE_PATH.length && window.URL) { // DWX
+          var parsedDocUrl = new URL(url);
+          var service = url.indexOf('livy') >= 0 ? '/livy' : '/impala';
+          parsedDocUrl.pathname = parsedKnoxUrl.pathname + service + window.KNOX_BASE_PATH;
+        } else {
+          return url;
+        }
+      });
       self.name = ko.observableDefault(job.name || job.id);
       self.type = ko.observableDefault(job.type);
       self.applicationType = ko.observableDefault(job.applicationType || '');

+ 6 - 6
apps/jobbrowser/src/jobbrowser/urls.py

@@ -23,7 +23,7 @@ from jobbrowser import api2 as jobbrowser_api2
 urlpatterns = [
   # "Default"
   url(r'^$', jobbrowser_views.jobs),
-  url(r'^jobs/$', jobbrowser_views.jobs, name='jobs'),
+  url(r'^jobs/?$', jobbrowser_views.jobs, name='jobs'),
   url(r'^jobs/(?P<job>\w+)$', jobbrowser_views.single_job, name='jobbrowser.views.single_job'),
   url(r'^jobs/(?P<job>\w+)/counters$', jobbrowser_views.job_counters, name='job_counters'),
   url(r'^jobs/(?P<job>\w+)/kill$', jobbrowser_views.kill_job, name='kill_job'),
@@ -39,7 +39,7 @@ urlpatterns = [
 
   # MR2 specific
   url(r'^jobs/(?P<job>\w+)/job_attempt_logs/(?P<attempt_index>\d+)$', jobbrowser_views.job_attempt_logs, name='job_attempt_logs'),
-  url(r'^jobs/(?P<job>\w+)/job_attempt_logs_json/(?P<attempt_index>\d+)/(?P<name>\w+)?/(?P<offset>[\d-]+)?$', jobbrowser_views.job_attempt_logs_json, name='job_attempt_logs_json'),
+  url(r'^jobs/(?P<job>\w+)/job_attempt_logs_json/(?P<attempt_index>\d+)(?:/(?P<name>\w+))?(?:/(?P<offset>[\d-]+))?/?$', jobbrowser_views.job_attempt_logs_json, name='job_attempt_logs_json'),
   url(r'^jobs/(?P<jobid>\w+)/job_not_assigned/(?P<path>.+)$', jobbrowser_views.job_not_assigned, name='job_not_assigned'),
 
   # Unused
@@ -48,7 +48,7 @@ urlpatterns = [
   url(r'^clusterstatus$', jobbrowser_views.clusterstatus, name='clusterstatus'),
   url(r'^queues$', jobbrowser_views.queues, name='queues'),
   url(r'^jobbrowser$', jobbrowser_views.jobbrowser, name='jobbrowser'),
-  url(r'^dock_jobs/$', jobbrowser_views.dock_jobs, name='dock_jobs'),
+  url(r'^dock_jobs/?$', jobbrowser_views.dock_jobs, name='dock_jobs'),
 ]
 
 # V2
@@ -57,9 +57,9 @@ urlpatterns += [
 ]
 
 urlpatterns += [
-  url(r'api/jobs/?(?P<interface>.+)?', jobbrowser_api2.jobs, name='jobs'),
+  url(r'api/jobs(?:/(?P<interface>.+))?/?', jobbrowser_api2.jobs, name='jobs'),
   url(r'api/job/logs', jobbrowser_api2.logs, name='logs'),
   url(r'api/job/profile', jobbrowser_api2.profile, name='profile'),
-  url(r'api/job/action/?(?P<interface>.+)?/?(?P<action>.+)?', jobbrowser_api2.action, name='action'),
-  url(r'api/job/?(?P<interface>.+)?', jobbrowser_api2.job, name='job'),
+  url(r'api/job/action(?:/(?P<interface>.+))?(?:/(?P<action>.+))?/?', jobbrowser_api2.action, name='action'),
+  url(r'api/job(?:/(?P<interface>.+))?/?', jobbrowser_api2.job, name='job'),
 ]

+ 1 - 1
apps/metastore/src/metastore/urls.py

@@ -26,7 +26,7 @@ urlpatterns = [
   url(r'^databases/(?P<database>\w+)/alter$', metastore_views.alter_database, name='alter_database'),
   url(r'^databases/(?P<database>\w+)/metadata$', metastore_views.get_database_metadata, name='get_database_metadata'),
 
-  url(r'^tables/(?P<database>\w+)?/?$', metastore_views.show_tables, name='show_tables'),
+  url(r'^tables(?:/(?P<database>\w+))?/?$', metastore_views.show_tables, name='show_tables'),
   url(r'^tables/drop/(?P<database>\w+)$', metastore_views.drop_table, name='drop_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/?$', metastore_views.describe_table, name='describe_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/alter$', metastore_views.alter_table, name='alter_table'),

+ 3 - 1
apps/oozie/src/oozie/templates/dashboard/list_oozie_bundle.mako

@@ -16,6 +16,8 @@
 ## limitations under the License.
 
 <%!
+  import urllib
+  from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
 
@@ -108,7 +110,7 @@ ${ layout.menubar(section='bundles', dashboard=True) }
                % endif
             "
               id="rerun-btn"
-              data-rerun-url="${ url('oozie:rerun_oozie_bundle', job_id=oozie_bundle.id, app_path=oozie_bundle.bundleJobPath) }"
+              data-rerun-url="${ url('oozie:rerun_oozie_bundle', job_id=oozie_bundle.id, app_path=urllib.quote(oozie_bundle.bundleJobPath.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)) }"
             style="margin-bottom: 5px">
               ${ _('Rerun') }
             </button>

+ 3 - 1
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinator.mako

@@ -16,6 +16,8 @@
 ## limitations under the License.
 
 <%!
+  import urllib
+  from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
   from desktop.views import commonheader, commonfooter, _ko
   from django.utils.translation import ugettext as _
   
@@ -197,7 +199,7 @@ ${ layout.menubar(section='coordinators', dashboard=True) }
                          % if oozie_coordinator.is_running() or oozie_coordinator.status in ('KILLED', 'FAILED'):
                            disabled="disabled"
                          % endif
-                        data-rerun-url="${ url('oozie:rerun_oozie_coord', job_id=oozie_coordinator.id, app_path=oozie_coordinator.coordJobPath) }">
+                        data-rerun-url="${ url('oozie:rerun_oozie_coord', job_id=oozie_coordinator.id, app_path=urllib.quote(oozie_coordinator.coordJobPath.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)) }">
                         <i class="fa fa-refresh"></i> ${ _('Rerun') }
                       </button>
                       <button id="trash-btn-caret" class="btn toolbarBtn dropdown-toggle" data-toggle="dropdown"

+ 54 - 54
apps/oozie/src/oozie/urls.py

@@ -27,22 +27,22 @@ IS_URL_NAMESPACED = True
 
 urlpatterns = [
 
-  url(r'^list_workflows/$', oozie_views_editor.list_workflows, name='list_workflows'),
-  url(r'^list_trashed_workflows/$', oozie_views_editor.list_trashed_workflows, name='list_trashed_workflows'),
-  url(r'^create_workflow/$', oozie_views_editor.create_workflow, name='create_workflow'),
-  url(r'^edit_workflow/(?P<workflow>\d+)$', oozie_views_editor.edit_workflow, name='edit_workflow'),
+  url(r'^list_workflows/?$', oozie_views_editor.list_workflows, name='list_workflows'),
+  url(r'^list_trashed_workflows/?$', oozie_views_editor.list_trashed_workflows, name='list_trashed_workflows'),
+  url(r'^create_workflow/?$', oozie_views_editor.create_workflow, name='create_workflow'),
+  url(r'^edit_workflow/(?P<workflow>\d+)/?$', oozie_views_editor.edit_workflow, name='edit_workflow'),
   url(r'^delete_workflow$', oozie_views_editor.delete_workflow, name='delete_workflow'),
-  url(r'^restore_workflow/$', oozie_views_editor.restore_workflow, name='restore_workflow'),
+  url(r'^restore_workflow/?$', oozie_views_editor.restore_workflow, name='restore_workflow'),
   url(r'^clone_workflow/(?P<workflow>\d+)$', oozie_views_editor.clone_workflow, name='clone_workflow'),
   url(r'^submit_workflow/(?P<workflow>\d+)$', oozie_views_editor.submit_workflow, name='submit_workflow'),
   url(r'^schedule_workflow/(?P<workflow>\d+)$', oozie_views_editor.schedule_workflow, name='schedule_workflow'),
-  url(r'^import_workflow/$', oozie_views_editor.import_workflow, name='import_workflow'),
-  url(r'^import_coordinator/$', oozie_views_editor.import_coordinator, name='import_coordinator'),
+  url(r'^import_workflow/?$', oozie_views_editor.import_workflow, name='import_workflow'),
+  url(r'^import_coordinator/?$', oozie_views_editor.import_coordinator, name='import_coordinator'),
   url(r'^export_workflow/(?P<workflow>\d+)$', oozie_views_editor.export_workflow, name='export_workflow'),
 
-  url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', oozie_views_editor.list_coordinators, name='list_coordinators'),
-  url(r'^list_trashed_coordinators/$', oozie_views_editor.list_trashed_coordinators, name='list_trashed_coordinators'),
-  url(r'^create_coordinator/(?P<workflow>[-\w]+)?$', oozie_views_editor.create_coordinator, name='create_coordinator'),
+  url(r'^list_coordinators(?:/(?P<workflow_id>[-\w]+))?/?$', oozie_views_editor.list_coordinators, name='list_coordinators'),
+  url(r'^list_trashed_coordinators/?$', oozie_views_editor.list_trashed_coordinators, name='list_trashed_coordinators'),
+  url(r'^create_coordinator(?:/(?P<workflow>[-\w]+))?/?$', oozie_views_editor.create_coordinator, name='create_coordinator'),
   url(r'^edit_coordinator/(?P<coordinator>[-\w]+)$', oozie_views_editor.edit_coordinator, name='edit_coordinator'),
   url(r'^delete_coordinator$', oozie_views_editor.delete_coordinator, name='delete_coordinator'),
   url(r'^restore_coordinator$', oozie_views_editor.restore_coordinator, name='restore_coordinator'),
@@ -65,56 +65,56 @@ urlpatterns = [
 
   url(r'^list_history$', oozie_views_editor.list_history, name='list_history'), # Unused
   url(r'^list_history/(?P<record_id>[-\w]+)$', oozie_views_editor.list_history_record, name='list_history_record'),
-  url(r'^install_examples/$', oozie_views_editor.install_examples, name='install_examples'),
+  url(r'^install_examples/?$', oozie_views_editor.install_examples, name='install_examples'),
 ]
 
 urlpatterns += [
 
-  url(r'^editor/workflow/list/$', oozie_views_editor2.list_editor_workflows, name='list_editor_workflows'),
-  url(r'^editor/workflow/edit/$', oozie_views_editor2.edit_workflow, name='edit_workflow'),
-  url(r'^editor/workflow/new/$', oozie_views_editor2.new_workflow, name='new_workflow'),
-  url(r'^editor/workflow/delete/$', oozie_views_editor2.delete_job, name='delete_editor_workflow'),
-  url(r'^editor/workflow/copy/$', oozie_views_editor2.copy_workflow, name='copy_workflow'),
-  url(r'^editor/workflow/save/$', oozie_views_editor2.save_workflow, name='save_workflow'),
+  url(r'^editor/workflow/list/?$', oozie_views_editor2.list_editor_workflows, name='list_editor_workflows'),
+  url(r'^editor/workflow/edit/?$', oozie_views_editor2.edit_workflow, name='edit_workflow'),
+  url(r'^editor/workflow/new/?$', oozie_views_editor2.new_workflow, name='new_workflow'),
+  url(r'^editor/workflow/delete/?$', oozie_views_editor2.delete_job, name='delete_editor_workflow'),
+  url(r'^editor/workflow/copy/?$', oozie_views_editor2.copy_workflow, name='copy_workflow'),
+  url(r'^editor/workflow/save/?$', oozie_views_editor2.save_workflow, name='save_workflow'),
   url(r'^editor/workflow/submit/(?P<doc_id>\d+)$', oozie_views_editor2.submit_workflow, name='editor_submit_workflow'),
   url(r'^editor/workflow/submit_single_action/(?P<doc_id>\d+)/(?P<node_id>.+)$', oozie_views_editor2.submit_single_action, name='submit_single_action'),
-  url(r'^editor/workflow/new_node/$', oozie_views_editor2.new_node, name='new_node'),
-  url(r'^editor/workflow/add_node/$', oozie_views_editor2.add_node, name='add_node'),
-  url(r'^editor/workflow/parameters/$', oozie_views_editor2.workflow_parameters, name='workflow_parameters'),
-  url(r'^editor/workflow/action/parameters/$', oozie_views_editor2.action_parameters, name='action_parameters'),
-  url(r'^editor/workflow/gen_xml/$', oozie_views_editor2.gen_xml_workflow, name='gen_xml_workflow'),
-  url(r'^editor/workflow/open_v1/$', oozie_views_editor2.open_old_workflow, name='open_old_workflow'),
-
-  url(r'^editor/coordinator/list/$', oozie_views_editor2.list_editor_coordinators, name='list_editor_coordinators'),
-  url(r'^editor/coordinator/edit/$', oozie_views_editor2.edit_coordinator, name='edit_coordinator'),
-  url(r'^editor/coordinator/new/$', oozie_views_editor2.new_coordinator, name='new_coordinator'),
-  url(r'^editor/coordinator/delete/$', oozie_views_editor2.delete_job, name='delete_editor_coordinator'),
-  url(r'^editor/coordinator/copy/$', oozie_views_editor2.copy_coordinator, name='copy_coordinator'),
-  url(r'^editor/coordinator/save/$', oozie_views_editor2.save_coordinator, name='save_coordinator'),
+  url(r'^editor/workflow/new_node/?$', oozie_views_editor2.new_node, name='new_node'),
+  url(r'^editor/workflow/add_node/?$', oozie_views_editor2.add_node, name='add_node'),
+  url(r'^editor/workflow/parameters/?$', oozie_views_editor2.workflow_parameters, name='workflow_parameters'),
+  url(r'^editor/workflow/action/parameters/?$', oozie_views_editor2.action_parameters, name='action_parameters'),
+  url(r'^editor/workflow/gen_xml/?$', oozie_views_editor2.gen_xml_workflow, name='gen_xml_workflow'),
+  url(r'^editor/workflow/open_v1/?$', oozie_views_editor2.open_old_workflow, name='open_old_workflow'),
+
+  url(r'^editor/coordinator/list/?$', oozie_views_editor2.list_editor_coordinators, name='list_editor_coordinators'),
+  url(r'^editor/coordinator/edit/?$', oozie_views_editor2.edit_coordinator, name='edit_coordinator'),
+  url(r'^editor/coordinator/new/?$', oozie_views_editor2.new_coordinator, name='new_coordinator'),
+  url(r'^editor/coordinator/delete/?$', oozie_views_editor2.delete_job, name='delete_editor_coordinator'),
+  url(r'^editor/coordinator/copy/?$', oozie_views_editor2.copy_coordinator, name='copy_coordinator'),
+  url(r'^editor/coordinator/save/?$', oozie_views_editor2.save_coordinator, name='save_coordinator'),
   url(r'^editor/coordinator/submit/(?P<doc_id>[-\w]+)$', oozie_views_editor2.submit_coordinator, name='editor_submit_coordinator'),
-  url(r'^editor/coordinator/gen_xml/$', oozie_views_editor2.gen_xml_coordinator, name='gen_xml_coordinator'),
-  url(r'^editor/coordinator/open_v1/$', oozie_views_editor2.open_old_coordinator, name='open_old_coordinator'),
-  url(r'^editor/coordinator/parameters/$', oozie_views_editor2.coordinator_parameters, name='coordinator_parameters'),
-
-  url(r'^editor/bundle/list/$', oozie_views_editor2.list_editor_bundles, name='list_editor_bundles'),
-  url(r'^editor/bundle/edit/$', oozie_views_editor2.edit_bundle, name='edit_bundle'),
-  url(r'^editor/bundle/new/$', oozie_views_editor2.new_bundle, name='new_bundle'),
-  url(r'^editor/bundle/delete/$', oozie_views_editor2.delete_job, name='delete_editor_bundle'),
-  url(r'^editor/bundle/copy/$', oozie_views_editor2.copy_bundle, name='copy_bundle'),
-  url(r'^editor/bundle/save/$', oozie_views_editor2.save_bundle, name='save_bundle'),
+  url(r'^editor/coordinator/gen_xml/?$', oozie_views_editor2.gen_xml_coordinator, name='gen_xml_coordinator'),
+  url(r'^editor/coordinator/open_v1/?$', oozie_views_editor2.open_old_coordinator, name='open_old_coordinator'),
+  url(r'^editor/coordinator/parameters/?$', oozie_views_editor2.coordinator_parameters, name='coordinator_parameters'),
+
+  url(r'^editor/bundle/list/?$', oozie_views_editor2.list_editor_bundles, name='list_editor_bundles'),
+  url(r'^editor/bundle/edit/?$', oozie_views_editor2.edit_bundle, name='edit_bundle'),
+  url(r'^editor/bundle/new/?$', oozie_views_editor2.new_bundle, name='new_bundle'),
+  url(r'^editor/bundle/delete/?$', oozie_views_editor2.delete_job, name='delete_editor_bundle'),
+  url(r'^editor/bundle/copy/?$', oozie_views_editor2.copy_bundle, name='copy_bundle'),
+  url(r'^editor/bundle/save/?$', oozie_views_editor2.save_bundle, name='save_bundle'),
   url(r'^editor/bundle/submit/(?P<doc_id>\d+)$', oozie_views_editor2.submit_bundle, name='editor_submit_bundle'),
-  url(r'^editor/bundle/open_v1/$', oozie_views_editor2.open_old_bundle, name='open_old_bundle'),
+  url(r'^editor/bundle/open_v1/?$', oozie_views_editor2.open_old_bundle, name='open_old_bundle'),
 ]
 
 
 urlpatterns += [
 
-  url(r'^workflows$', oozie_views_api.workflows, name='workflows'),
+  url(r'^workflows/?$', oozie_views_api.workflows, name='workflows'),
   url(r'^workflows/(?P<workflow>\d+)$', oozie_views_api.workflow, name='workflow'),
   url(r'^workflows/(?P<workflow>\d+)/save$', oozie_views_api.workflow_save, name='workflow_save'),
   url(r'^workflows/(?P<workflow>\d+)/actions$', oozie_views_api.workflow_actions, name='workflow_actions'),
   url(r'^workflows/(?P<workflow>\d+)/nodes/(?P<node_type>\w+)/validate$', oozie_views_api.workflow_validate_node, name='workflow_validate_node'),
-  url(r'^workflows/autocomplete_properties/$', oozie_views_api.autocomplete_properties, name='autocomplete_properties'),
+  url(r'^workflows/autocomplete_properties/?$', oozie_views_api.autocomplete_properties, name='autocomplete_properties'),
 ]
 
 
@@ -122,25 +122,25 @@ urlpatterns += [
 
   url(r'^$', oozie_views_dashboard.list_oozie_workflows, name='index'),
 
-  url(r'^list_oozie_workflows/$', oozie_views_dashboard.list_oozie_workflows, name='list_oozie_workflows'),
-  url(r'^list_oozie_coordinators/$', oozie_views_dashboard.list_oozie_coordinators, name='list_oozie_coordinators'),
-  url(r'^list_oozie_bundles/$', oozie_views_dashboard.list_oozie_bundles, name='list_oozie_bundles'),
-  url(r'^list_oozie_workflow/(?P<job_id>[-\w]+)/$', oozie_views_dashboard.list_oozie_workflow, name='list_oozie_workflow'),
-  url(r'^list_oozie_coordinator/(?P<job_id>[-\w]+)/$', oozie_views_dashboard.list_oozie_coordinator, name='list_oozie_coordinator'),
-  url(r'^list_oozie_workflow_action/(?P<action>[-\w@]+)/$', oozie_views_dashboard.list_oozie_workflow_action, name='list_oozie_workflow_action'),
+  url(r'^list_oozie_workflows/?$', oozie_views_dashboard.list_oozie_workflows, name='list_oozie_workflows'),
+  url(r'^list_oozie_coordinators/?$', oozie_views_dashboard.list_oozie_coordinators, name='list_oozie_coordinators'),
+  url(r'^list_oozie_bundles/?$', oozie_views_dashboard.list_oozie_bundles, name='list_oozie_bundles'),
+  url(r'^list_oozie_workflow/(?P<job_id>[-\w]+)/?$', oozie_views_dashboard.list_oozie_workflow, name='list_oozie_workflow'),
+  url(r'^list_oozie_coordinator/(?P<job_id>[-\w]+)/?$', oozie_views_dashboard.list_oozie_coordinator, name='list_oozie_coordinator'),
+  url(r'^list_oozie_workflow_action/(?P<action>[-\w@]+)/?$', oozie_views_dashboard.list_oozie_workflow_action, name='list_oozie_workflow_action'),
   url(r'^list_oozie_bundle/(?P<job_id>[-\w]+)$', oozie_views_dashboard.list_oozie_bundle, name='list_oozie_bundle'),
 
-  url(r'^rerun_oozie_job/(?P<job_id>[-\w]+)/(?P<app_path>.+?)?$', oozie_views_dashboard.rerun_oozie_job, name='rerun_oozie_job'),
-  url(r'^rerun_oozie_coord/(?P<job_id>[-\w]+)/(?P<app_path>.+?)?$', oozie_views_dashboard.rerun_oozie_coordinator, name='rerun_oozie_coord'),
+  url(r'^rerun_oozie_job/(?P<job_id>[-\w]+)(?:/(?P<app_path>.+?))?/?$', oozie_views_dashboard.rerun_oozie_job, name='rerun_oozie_job'),
+  url(r'^rerun_oozie_coord/(?P<job_id>[-\w]+)(?:/(?P<app_path>.+?))?/?$', oozie_views_dashboard.rerun_oozie_coordinator, name='rerun_oozie_coord'),
   url(r'^rerun_oozie_bundle/(?P<job_id>[-\w]+)/(?P<app_path>.+?)$', oozie_views_dashboard.rerun_oozie_bundle, name='rerun_oozie_bundle'),
   url(r'^sync_coord_workflow/(?P<job_id>[-\w]+)$', oozie_views_dashboard.sync_coord_workflow, name='sync_coord_workflow'),
   url(r'^manage_oozie_jobs/(?P<job_id>[-\w]+)/(?P<action>(start|suspend|resume|kill|rerun|change|ignore))$', oozie_views_dashboard.manage_oozie_jobs, name='manage_oozie_jobs'),
-  url(r'^bulk_manage_oozie_jobs/$', oozie_views_dashboard.bulk_manage_oozie_jobs, name='bulk_manage_oozie_jobs'),
+  url(r'^bulk_manage_oozie_jobs/?$', oozie_views_dashboard.bulk_manage_oozie_jobs, name='bulk_manage_oozie_jobs'),
 
   url(r'^submit_external_job/(?P<application_path>.+?)$', oozie_views_dashboard.submit_external_job, name='submit_external_job'),
   url(r'^get_oozie_job_log/(?P<job_id>[-\w]+)$', oozie_views_dashboard.get_oozie_job_log, name='get_oozie_job_log'),
 
-  url(r'^list_oozie_info/$', oozie_views_dashboard.list_oozie_info, name='list_oozie_info'),
+  url(r'^list_oozie_info/?$', oozie_views_dashboard.list_oozie_info, name='list_oozie_info'),
 
-  url(r'^list_oozie_sla/$', oozie_views_dashboard.list_oozie_sla, name='list_oozie_sla'),
+  url(r'^list_oozie_sla/?$', oozie_views_dashboard.list_oozie_sla, name='list_oozie_sla'),
 ]

+ 16 - 16
apps/oozie/src/oozie/views/dashboard.py

@@ -20,6 +20,7 @@ import logging
 import os
 import re
 import time
+import urllib
 
 from django.forms.formsets import formset_factory
 from django.http import HttpResponse
@@ -34,6 +35,7 @@ from desktop.lib.django_util import JsonResponse, render
 from desktop.lib.json_utils import JSONEncoderForHTML
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.i18n import smart_str, smart_unicode
+from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
 from desktop.lib.rest.http_client import RestException
 from desktop.lib.view_util import format_duration_in_millis
 from desktop.log.access import access_warn
@@ -759,6 +761,8 @@ def rerun_oozie_job(request, job_id, app_path=None):
   check_job_edition_permission(oozie_workflow, request.user)
   if app_path is None:
     app_path = oozie_workflow.appPath
+  else:
+    app_path = urllib.unquote(app_path)
   return_json = request.GET.get('format') == 'json'
 
   if request.method == 'POST':
@@ -790,15 +794,13 @@ def rerun_oozie_job(request, job_id, app_path=None):
     initial_params = ParameterForm.get_initial_params(oozie_workflow.conf_dict)
     params_form = ParametersFormSet(initial=initial_params)
 
-  popup = render('dashboard/rerun_workflow_popup.mako', request, {
+    return render('dashboard/rerun_workflow_popup.mako', request, {
                    'rerun_form': rerun_form,
                    'params_form': params_form,
-                   'action': reverse('oozie:rerun_oozie_job', kwargs={'job_id': job_id, 'app_path': app_path}),
+                   'action': reverse('oozie:rerun_oozie_job', kwargs={'job_id': job_id, 'app_path': urllib.quote(app_path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS) }), 
                    'return_json': return_json,
                    'is_mini': request.GET.get('is_mini', False),
-                 }, force_template=True).content
-
-  return JsonResponse(popup, safe=False)
+                 }, force_template=True)
 
 
 def _rerun_workflow(request, oozie_id, run_args, mapping):
@@ -820,6 +822,8 @@ def rerun_oozie_coordinator(request, job_id, app_path=None):
   ParametersFormSet = formset_factory(ParameterForm, extra=0)
   if app_path is None:
     app_path = oozie_coordinator.coordJobPath
+  else:
+    app_path = urllib.unquote(app_path)
   return_json = request.GET.get('format') == 'json'
 
   if request.method == 'POST':
@@ -854,15 +858,13 @@ def rerun_oozie_coordinator(request, job_id, app_path=None):
     initial_params = ParameterForm.get_initial_params(oozie_coordinator.conf_dict)
     params_form = ParametersFormSet(initial=initial_params)
 
-  popup = render('dashboard/rerun_coord_popup.mako', request, {
+    return render('dashboard/rerun_coord_popup.mako', request, {
                    'rerun_form': rerun_form,
                    'params_form': params_form,
-                   'action': reverse('oozie:rerun_oozie_coord', kwargs={'job_id': job_id, 'app_path': app_path}),
+                   'action': reverse('oozie:rerun_oozie_coord', kwargs={'job_id': job_id, 'app_path': urllib.quote(app_path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)}),
                    'return_json': return_json,
                    'is_mini': request.GET.get('is_mini', False),
-                 }, force_template=True).content
-
-  return JsonResponse(popup, safe=False)
+                 }, force_template=True)
 
 
 def _rerun_coordinator(request, oozie_id, args, params, properties):
@@ -882,7 +884,7 @@ def rerun_oozie_bundle(request, job_id, app_path):
   oozie_bundle = check_job_access_permission(request, job_id)
   check_job_edition_permission(oozie_bundle, request.user)
   ParametersFormSet = formset_factory(ParameterForm, extra=0)
-
+  app_path = urllib.unquote(app_path)
   if request.method == 'POST':
     params_form = ParametersFormSet(request.POST)
     rerun_form = RerunBundleForm(request.POST, oozie_bundle=oozie_bundle)
@@ -921,13 +923,11 @@ def rerun_oozie_bundle(request, job_id, app_path):
     initial_params = ParameterForm.get_initial_params(oozie_bundle.conf_dict)
     params_form = ParametersFormSet(initial=initial_params)
 
-  popup = render('dashboard/rerun_bundle_popup.mako', request, {
+    return render('dashboard/rerun_bundle_popup.mako', request, {
                    'rerun_form': rerun_form,
                    'params_form': params_form,
-                   'action': reverse('oozie:rerun_oozie_bundle', kwargs={'job_id': job_id, 'app_path': app_path}),
-                 }, force_template=True).content
-
-  return JsonResponse(popup, safe=False)
+                   'action': reverse('oozie:rerun_oozie_bundle', kwargs={'job_id': job_id, 'app_path': urllib.quote(app_path.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)}),
+                 }, force_template=True)
 
 
 def _rerun_bundle(request, oozie_id, args, params, properties):

+ 6 - 9
apps/oozie/src/oozie/views/editor2.py

@@ -427,7 +427,7 @@ def _submit_workflow_helper(request, workflow, submit_action):
     params_form = ParametersFormSet(initial=initial_params)
 
 
-    popup = render('editor2/submit_job_popup.mako', request, {
+    return render('editor2/submit_job_popup.mako', request, {
                      'params_form': params_form,
                      'name': workflow.name,
                      'action': submit_action,
@@ -436,8 +436,7 @@ def _submit_workflow_helper(request, workflow, submit_action):
                      'is_oozie_mail_enabled': _is_oozie_mail_enabled(request.user),
                      'return_json': request.GET.get('format') == 'json',
                      'cluster_json': cluster_json
-                   }, force_template=True).content
-    return JsonResponse(popup, safe=False)
+                   }, force_template=True)
 
 
 def _is_oozie_mail_enabled(user):
@@ -708,14 +707,13 @@ def submit_coordinator(request, doc_id):
     initial_params = ParameterForm.get_initial_params(dict([(param['name'], param['value']) for param in parameters]))
     params_form = ParametersFormSet(initial=initial_params)
 
-  popup = render('editor2/submit_job_popup.mako', request, {
+    return render('editor2/submit_job_popup.mako', request, {
                  'params_form': params_form,
                  'name': coordinator.name,
                  'action': reverse('oozie:editor_submit_coordinator',  kwargs={'doc_id': coordinator.id}),
                  'show_dryrun': True,
                  'return_json': request.GET.get('format') == 'json'
-                }, force_template=True).content
-  return JsonResponse(popup, safe=False)
+                }, force_template=True)
 
 
 def _submit_coordinator(request, coordinator, mapping):
@@ -938,14 +936,13 @@ def submit_bundle(request, doc_id):
     initial_params = ParameterForm.get_initial_params(dict([(param['name'], param['value']) for param in parameters]))
     params_form = ParametersFormSet(initial=initial_params)
 
-  popup = render('editor2/submit_job_popup.mako', request, {
+    return render('editor2/submit_job_popup.mako', request, {
                  'params_form': params_form,
                  'name': bundle.name,
                  'action': reverse('oozie:editor_submit_bundle',  kwargs={'doc_id': bundle.id}),
                  'return_json': request.GET.get('format') == 'json',
                  'show_dryrun': False
-                }, force_template=True).content
-  return JsonResponse(popup, safe=False)
+                }, force_template=True)
 
 
 def _submit_bundle(request, bundle, properties):

+ 8 - 8
apps/pig/src/pig/urls.py

@@ -21,16 +21,16 @@ from pig import views as pig_views
 urlpatterns = [
   url(r'^$', pig_views.app, name='index'),
 
-  url(r'^app/$', pig_views.app, name='app'),
+  url(r'^app/?$', pig_views.app, name='app'),
 
   # Ajax
-  url(r'^scripts/$', pig_views.scripts, name='scripts'),
-  url(r'^dashboard/$', pig_views.dashboard, name='dashboard'),
-  url(r'^save/$', pig_views.save, name='save'),
-  url(r'^run/$', pig_views.run, name='run'),
-  url(r'^copy/$', pig_views.copy, name='copy'),
-  url(r'^delete/$', pig_views.delete, name='delete'),
+  url(r'^scripts/?$', pig_views.scripts, name='scripts'),
+  url(r'^dashboard/?$', pig_views.dashboard, name='dashboard'),
+  url(r'^save/?$', pig_views.save, name='save'),
+  url(r'^run/?$', pig_views.run, name='run'),
+  url(r'^copy/?$', pig_views.copy, name='copy'),
+  url(r'^delete/?$', pig_views.delete, name='delete'),
   url(r'^watch/(?P<job_id>[-\w]+)$', pig_views.watch, name='watch'),
-  url(r'^stop/$', pig_views.stop, name='stop'),
+  url(r'^stop/?$', pig_views.stop, name='stop'),
   url(r'^install_examples$', pig_views.install_examples, name='install_examples'),
 ]

+ 3 - 1
apps/pig/src/pig/views.py

@@ -17,6 +17,7 @@
 
 import json
 import logging
+import urllib
 
 from django.urls import reverse
 from django.utils.translation import ugettext as _
@@ -25,6 +26,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
 from desktop.lib.django_util import JsonResponse, render
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.rest.http_client import RestException
+from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
 from desktop.models import Document
 
 from oozie.views.dashboard import show_oozie_error, check_job_access_permission,\
@@ -223,7 +225,7 @@ def watch(request, job_id):
     'progress': oozie_workflow.get_progress(),
     'isRunning': oozie_workflow.is_running(),
     'killUrl': reverse('oozie:manage_oozie_jobs', kwargs={'job_id': oozie_workflow.id, 'action': 'kill'}),
-    'rerunUrl': reverse('oozie:rerun_oozie_job', kwargs={'job_id': oozie_workflow.id, 'app_path': oozie_workflow.appPath}),
+    'rerunUrl': reverse('oozie:rerun_oozie_job', kwargs={'job_id': oozie_workflow.id, 'app_path': urllib.quote(oozie_workflow.appPath.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)}),
     'actions': workflow_actions
   }
 

+ 2 - 2
apps/rdbms/src/rdbms/urls.py

@@ -35,9 +35,9 @@ urlpatterns += [
   url(r'^api/servers/(?P<server>\w+)/databases/?$', rdbms_api.databases, name='api_databases'),
   url(r'^api/servers/(?P<server>\w+)/databases/(?P<database>.+?)/tables/?$', rdbms_api.tables, name='api_tables'),
   url(r'^api/servers/(?P<server>\w+)/databases/(?P<database>.+?)/tables/(?P<table>\w+)/columns/?$', rdbms_api.columns, name='api_columns'),
-  url(r'^api/query/((?P<design_id>\d+)/?)?$', rdbms_api.save_query, name='api_save_query'),
+  url(r'^api/query(?:/(?P<design_id>\d+))?/?$', rdbms_api.save_query, name='api_save_query'),
   url(r'^api/query/(?P<design_id>\d+)/get$', rdbms_api.fetch_saved_query, name='api_fetch_saved_query'),
-  url(r'^api/execute/(?P<design_id>\d+)?$', rdbms_api.execute_query, name='api_execute_query'),
+  url(r'^api/execute(?:/(?P<design_id>\d+))?/?$', rdbms_api.execute_query, name='api_execute_query'),
   url(r'^api/explain/?$', rdbms_api.explain_query, name='api_explain_query'),
   url(r'^api/results/(?P<id>\d+)/(?P<first_row>\d+)$', rdbms_api.fetch_results, name='api_fetch_results')
 ]

+ 1 - 1
apps/search/src/search/urls.py

@@ -45,7 +45,7 @@ urlpatterns += [
 
 urlpatterns += [
   url(r'^search$', dashboard_api.search, name='search'),
-  url(r'^suggest/$', dashboard_api.query_suggest, name='query_suggest'),
+  url(r'^suggest/?$', dashboard_api.query_suggest, name='query_suggest'),
   url(r'^index/fields/dynamic$', dashboard_api.index_fields_dynamic, name='index_fields_dynamic'),
   url(r'^index/fields/nested_documents', dashboard_api.nested_documents, name='nested_documents'),
   url(r'^template/new_facet$', dashboard_api.new_facet, name='new_facet'),

+ 1 - 1
apps/useradmin/src/useradmin/templates/edit_user.mako

@@ -135,7 +135,7 @@ $(document).ready(function(){
 
 
   % if is_embeddable:
-  $editUserComponents.find('#editForm').attr('action', window.location.pathname.substr(4).replace(/\/$/, ''));
+  $editUserComponents.find('#editForm').attr('action', window.location.pathname.substr((window.HUE_BASE_URL + '/hue').length).replace(/\/$/, ''));
   $editUserComponents.find('#editForm').ajaxForm({
     dataType:  'json',
     success: function(data) {

+ 3 - 3
desktop/core/src/desktop/js/api/apiHelper.js

@@ -29,10 +29,10 @@ const EXECUTE_API_PREFIX = '/notebook/api/execute/';
 const DOCUMENTS_API = '/desktop/api2/doc/';
 const DOCUMENTS_SEARCH_API = '/desktop/api2/docs/';
 const FETCH_CONFIG = '/desktop/api2/get_config/';
-const HDFS_API_PREFIX = '/filebrowser/view=/';
-const ADLS_API_PREFIX = '/filebrowser/view=adl:/';
+const HDFS_API_PREFIX = '/filebrowser/view=' + encodeURIComponent('/');
+const ADLS_API_PREFIX = '/filebrowser/view=' + encodeURIComponent('adl:/');
 const GIT_API_PREFIX = '/desktop/api/vcs/contents/';
-const S3_API_PREFIX = '/filebrowser/view=S3A://';
+const S3_API_PREFIX = '/filebrowser/view=' + encodeURIComponent('S3A://');
 const IMPALA_INVALIDATE_API = '/impala/api/invalidate';
 const CONFIG_SAVE_API = '/desktop/api/configurations/save/';
 const CONFIG_APPS_API = '/desktop/api/configurations';

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -2290,7 +2290,7 @@ class Snippet {
                     }
                   }
                   if (!self.result.handle().has_more_statements && vm.successUrl()) {
-                    window.location.href = vm.successUrl(); // Not used anymore in Hue 4
+                    huePubSub.publish('open.link', vm.successUrl()); // Not used anymore in Hue 4
                   }
                 }
               } else if (data.status === -3) {

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -1265,7 +1265,7 @@ class Snippet {
                   }
                 }
                 if (!self.result.handle().has_more_statements && self.parentVm.successUrl()) {
-                  window.location.href = self.parentVm.successUrl(); // Not used anymore in Hue 4
+                  huePubSub.publish('open.link', self.parentVm.successUrl()); // Not used anymore in Hue 4
                 }
               } else if (self.status() === STATUS.success) {
                 self.progress(99);

+ 1 - 1
desktop/core/src/desktop/js/assist/assistStorageEntry.js

@@ -146,7 +146,7 @@ class AssistStorageEntry {
         const filteredFiles = data.files.filter(file => file.name !== '.' && file.name !== '..');
         self.entries(
           filteredFiles.map(file => {
-            file.url = encodeURI(file.url);
+            file.url = encodeURIComponent(file.url);
             return new AssistStorageEntry({
               type: self.type,
               definition: file,

+ 1 - 1
desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js

@@ -275,7 +275,7 @@ Plugin.prototype.navigateTo = function(path) {
   if (path.indexOf('?') > -1) {
     pageSize = pageSize.replace(/\?/, '&');
   }
-  $.getJSON('/filebrowser/view=' + path + pageSize, data => {
+  $.getJSON('/filebrowser/view=' + encodeURIComponent(path) + pageSize, data => {
     $(_parent.element)
       .find('.filechooser-tree')
       .empty();

+ 1 - 1
desktop/core/src/desktop/js/ko/bindings/ko.hueLink.js

@@ -27,7 +27,7 @@ ko.bindingHandlers.hueLink = {
         return function(data, event) {
           const url = ko.unwrap(valueAccessor());
           if (url) {
-            const prefix = '/hue' + (url.indexOf('/') === 0 ? '' : '/');
+            const prefix = window.HUE_BASE_URL +  '/hue' + (url.indexOf('/') === 0 ? '' : '/');
             if ($(element).attr('target')) {
               window.open(prefix + url, $(element).attr('target'));
             } else if (event.ctrlKey || event.metaKey || event.which === 2) {

+ 24 - 15
desktop/core/src/desktop/js/onePageViewModel.js

@@ -232,7 +232,7 @@ class OnePageViewModel {
       $rawHtml.find('a[href]').each(function() {
         let link = $(this).attr('href');
         if (link.startsWith('/') && !link.startsWith('/hue')) {
-          link = '/hue' + link;
+          link =  window.HUE_BASE_URL + '/hue' + link;
         }
         $(this).attr('href', link);
       });
@@ -335,26 +335,26 @@ class OnePageViewModel {
                 if (typeof self.currentContextParams()[key.name] !== 'undefined') {
                   if (app === 'filebrowser') {
                     baseURL = baseURL
-                      .replace('*', self.currentContextParams()[key.name])
+                      .replace('*', encodeURIComponent(self.currentContextParams()[key.name]))
                       .replace(/#/g, '%23');
                   } else {
-                    baseURL = baseURL.replace('*', self.currentContextParams()[key.name]);
+                    baseURL = baseURL.replace('*', encodeURI(self.currentContextParams()[key.name])); // We have some really funky stuff in here, this should be encodeURIComponent
                   }
                 } else {
                   baseURL = baseURL.replace('*', '');
                 }
               } else {
-                baseURL = baseURL.replace(':' + key.name, self.currentContextParams()[key.name]);
+                baseURL = baseURL.replace(':' + key.name, encodeURI(self.currentContextParams()[key.name])); // We have some really funky stuff in here, this should be encodeURIComponent
               }
             });
           }
           self.currentContextParams(null);
         }
         if (self.currentQueryString() !== null) {
-          baseURL += (baseURL.indexOf('?') > -1 ? '&' : '?') + self.currentQueryString();
+          baseURL += (baseURL.indexOf('?') > -1 ? '&' : '?') + encodeURI(self.currentQueryString());
           self.currentQueryString(null);
         }
-        baseURL = encodeURI(baseURL);
+
         $.ajax({
           url:
             baseURL +
@@ -383,7 +383,7 @@ class OnePageViewModel {
                 }, 0);
               });
             } else {
-              window.location.href = baseURL;
+              window.location.href = window.HUE_BASE_URL + baseURL;
             }
           },
           error: function(xhr) {
@@ -445,7 +445,7 @@ class OnePageViewModel {
     $(window.IS_EMBEDDED ? '.hue-embedded-container a[href]' : 'a[href]').each(function() {
       let link = $(this).attr('href');
       if (link.startsWith('/') && !link.startsWith('/hue')) {
-        link = '/hue' + link;
+        link = window.HUE_BASE_URL + '/hue' + link;
       }
       $(this).attr('href', link);
     });
@@ -458,7 +458,7 @@ class OnePageViewModel {
       }
       page({ hashbang: true });
     } else {
-      page.base('/hue');
+      page.base(window.HUE_BASE_URL + '/hue');
     }
 
     const getUrlParameter = function(name) {
@@ -488,7 +488,7 @@ class OnePageViewModel {
       {
         url: '/accounts/logout',
         app: function() {
-          location.href = '/accounts/logout';
+          location.href = window.HUE_BASE_URL + '/accounts/logout';
         }
       },
       {
@@ -497,7 +497,12 @@ class OnePageViewModel {
           page('/home/?type=search-dashboard');
         }
       },
-      { url: '/dashboard/*', app: 'dashboard' },
+      { url: '/dashboard*', app: 'dashboard' },
+      { url: '/desktop/api/desktop/api2/doc/export*', app: function() {
+          var documents = getUrlParameter('documents');
+          location.href = window.HUE_BASE_URL + '/desktop/api2/doc/export?documents=' + documents;
+        }
+      },
       { url: '/desktop/dump_config', app: 'dump_config' },
       {
         url: '/desktop/debug/threads',
@@ -538,7 +543,7 @@ class OnePageViewModel {
       {
         url: '/desktop/download_logs',
         app: function() {
-          location.href = '/desktop/download_logs';
+          location.href = window.HUE_BASE_URL + '/desktop/download_logs';
         }
       },
       {
@@ -576,7 +581,10 @@ class OnePageViewModel {
         }
       },
       { url: '/filebrowser/view=*', app: 'filebrowser' },
-      { url: '/filebrowser/download=*', app: 'filebrowser' },
+      { url: '/filebrowser/download=*', app: function(ctx) {
+        location.href = window.HUE_BASE_URL + '/filebrowser/download=' + ctx.params[0];
+        }
+      },
       {
         url: '/filebrowser/*',
         app: function() {
@@ -766,7 +774,6 @@ class OnePageViewModel {
       { url: '/useradmin/users/add_ldap_groups', app: 'useradmin_addldapgroups' },
       { url: '/useradmin/users/edit/:user', app: 'useradmin_edituser' },
       { url: '/useradmin/users/new', app: 'useradmin_newuser' },
-      { url: '/useradmin/users/', app: 'useradmin_users' },
       { url: '/useradmin', app: 'useradmin_users' }
     ];
 
@@ -813,7 +820,9 @@ class OnePageViewModel {
       if (href) {
         const prefix = window.IS_EMBEDDED ? '' : '/hue';
         if (href.startsWith('/') && !href.startsWith(prefix)) {
-          page(prefix + href);
+          page(window.HUE_BASE_URL + prefix + href);
+        } else if (href.indexOf('#') == 0) { // Only place that seem to use this is hbase onclick row
+          window.location.hash = href;
         } else {
           page(href);
         }

+ 2 - 2
desktop/core/src/desktop/js/sql/sqlWorkerHandler.js

@@ -77,7 +77,7 @@ export default {
 
       // For syntax checking
       const aceSqlSyntaxWorker = new Worker(
-        '/desktop/workers/aceSqlSyntaxWorker.js?v=' + window.HUE_VERSION
+        window.HUE_BASE_URL + '/desktop/workers/aceSqlSyntaxWorker.js?v=' + window.HUE_VERSION
       );
       aceSqlSyntaxWorker.onmessage = function(e) {
         if (e.data.ping) {
@@ -93,7 +93,7 @@ export default {
 
       // For location marking
       const aceSqlLocationWorker = new Worker(
-        '/desktop/workers/aceSqlLocationWorker.js?v=' + window.HUE_VERSION
+          window.HUE_BASE_URL + '/desktop/workers/aceSqlLocationWorker.js?v=' + window.HUE_VERSION
       );
       aceSqlLocationWorker.onmessage = function(e) {
         if (e.data.ping) {

+ 2 - 1
desktop/core/src/desktop/js/utils/hueUtils.js

@@ -160,7 +160,8 @@ const changeURL = (newURL, params) => {
   }
 
   const hashSplit = newURL.split('#');
-  let url = hashSplit[0];
+  var base = hashSplit[0].length && hashSplit[0].indexOf(window.HUE_BASE_URL) !== 0 ? window.HUE_BASE_URL : '';
+  let url = base + hashSplit[0];
   if (extraSearch) {
     url += (url.indexOf('?') === -1 ? '?' : '&') + extraSearch;
   }

+ 2 - 0
desktop/core/src/desktop/lib/paths.py

@@ -35,6 +35,8 @@ The project directory structure:
             thirdparty/...
 """
 
+SAFE_CHARACTERS_URI = '~@#$&()*!+=:;,.?/\''
+SAFE_CHARACTERS_URI_COMPONENTS = '~@#$&()*!+=;,.\'' # Removing characters : / ? from safe list for KNOX
 
 def __get_root(*append):
   """

+ 4 - 4
desktop/core/src/desktop/models.py

@@ -49,7 +49,7 @@ from desktop import appmanager
 from desktop.conf import get_clusters, CLUSTER_ID, IS_MULTICLUSTER_ONLY, IS_EMBEDDED, IS_K8S_ONLY
 from desktop.lib.i18n import force_unicode
 from desktop.lib.exceptions_renderable import PopupException
-from desktop.lib.paths import get_run_root
+from desktop.lib.paths import get_run_root, SAFE_CHARACTERS_URI_COMPONENTS
 from desktop.redaction import global_redaction_engine
 from desktop.settings import DOCUMENT2_SEARCH_MAX_LENGTH
 from desktop.auth.backend import is_admin
@@ -1768,7 +1768,7 @@ class ClusterConfig():
         'displayName': _('Files'),
         'buttonName': _('Browse'),
         'tooltip': _('Files'),
-        'page': '/filebrowser/' + (not self.user.is_anonymous() and 'view=' + self.user.get_home_directory() or '')
+        'page': '/filebrowser/' + (not self.user.is_anonymous() and 'view=' + urllib.quote(self.user.get_home_directory().encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS) or '')
       })
 
     if is_s3_enabled() and 'filebrowser' in self.apps and has_s3_access(self.user) and not IS_EMBEDDED.get():
@@ -1777,7 +1777,7 @@ class ClusterConfig():
         'displayName': _('S3'),
         'buttonName': _('Browse'),
         'tooltip': _('S3'),
-        'page': '/filebrowser/view=S3A://'
+        'page': '/filebrowser/view=' + urllib.quote('S3A://'.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)
       })
 
     if is_adls_enabled() and 'filebrowser' in self.apps and has_adls_access(self.user) and ANALYTIC_DB not in self.cluster_type:
@@ -1786,7 +1786,7 @@ class ClusterConfig():
         'displayName': _('ADLS'),
         'buttonName': _('Browse'),
         'tooltip': _('ADLS'),
-        'page': '/filebrowser/view=adl:/'
+        'page': '/filebrowser/view=' + urllib.quote('adl:/'.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)
       })
 
     if 'metastore' in self.apps:

+ 2 - 2
desktop/core/src/desktop/static/desktop/js/document/hueFileEntry.js

@@ -790,7 +790,7 @@ var HueFileEntry = (function () {
 
   HueFileEntry.prototype.downloadThis = function () {
     var self = this;
-    window.location.href = '/desktop/api2/doc/export?documents=' + ko.mapping.toJSON([ self.definition().id ]);
+    window.location.href = window.HUE_BASE_URL + '/desktop/api2/doc/export?documents=' + ko.mapping.toJSON([ self.definition().id ]);
   };
 
   HueFileEntry.prototype.download = function () {
@@ -800,7 +800,7 @@ var HueFileEntry = (function () {
         var ids = self.selectedEntries().map(function (entry) {
           return entry.definition().id;
         });
-        window.location.href = '/desktop/api2/doc/export?documents=' + ko.mapping.toJSON(ids);
+        window.location.href = window.HUE_BASE_URL + '/desktop/api2/doc/export?documents=' + ko.mapping.toJSON([ self.definition().id ]);
       } else {
         self.downloadThis();
       }

+ 10 - 6
desktop/core/src/desktop/templates/common_header.mako

@@ -18,6 +18,7 @@ from django.utils.translation import ugettext as _
 
 from desktop import conf
 from desktop.conf import USE_NEW_EDITOR
+from desktop.models import hue_version
 from desktop.lib.i18n import smart_unicode
 
 from metadata.conf import has_optimizer, OPTIMIZER
@@ -122,7 +123,10 @@ if USE_NEW_EDITOR.get():
   ${ commonHeaderFooterComponents.header_i18n_redirection() }
 
   % if user.is_authenticated():
-  <script src="/desktop/globalJsConstants.js"></script>
+  <%
+    global_constants_url = '/desktop/globalJsConstants.js?v=' + hue_version()
+  %>
+  <script src="${global_constants_url}"></script>
   % endif
 
   % if not conf.DEV.get():
@@ -155,7 +159,7 @@ if USE_NEW_EDITOR.get():
   <script src="${ static('desktop/js/ace.extended.js') }"></script>
 
   <script>
-    ace.config.set("basePath", "/static/desktop/js/ace");
+    ace.config.set("basePath", "${ static('desktop/js/ace') }");
   </script>
 
   <script src="${ static('metastore/js/metastore.model.js') }"></script>
@@ -318,21 +322,21 @@ ${ hueIcons.symbols() }
       <li><a title="" data-rel="navigator-tooltip" href="#"><i class="fa fa-fw fa-user"></i>&nbsp;${user.username}</a></li>
     % endif
     % if 'help' in apps:
-    <li><a title="${_('Documentation')}" data-rel="navigator-tooltip" href="/help"><i class="fa fa-question-circle"></i></a></li>
+    <li><a title="${_('Documentation')}" data-rel="navigator-tooltip" data-bind="hueLink: '/help'" href="javascript: void(0);"><i class="fa fa-question-circle"></i></a></li>
     % endif
-    <li><a title="${_('Sign out')}" data-rel="navigator-tooltip" href="/accounts/logout/"><i class="fa fa-sign-out"></i></a></li>
+    <li><a title="${_('Sign out')}" data-rel="navigator-tooltip" data-bind="hueLink: '/accounts/logout/'" href="javascript: void(0);" ><i class="fa fa-sign-out"></i></a></li>
   </ul>
   % endif
 
   </div>
-    <a class="brand nav-tooltip pull-left" title="${_('About Hue')}" data-rel="navigator-tooltip" href="/about">
+    <a class="brand nav-tooltip pull-left" title="${_('About Hue')}" data-rel="navigator-tooltip" data-bind="hueLink: '/about'" href="javascript: void(0);">
       <svg style="margin-top: 2px; margin-left:8px;width: 60px;height: 16px;display: inline-block;">
         <use xlink:href="#hi-logo"></use>
       </svg>
     </a>
     % if user.is_authenticated() and section != 'login':
      <ul class="nav nav-pills pull-left">
-       <li><a title="${_('My documents')}" data-rel="navigator-tooltip" href="${ home_url }" style="padding-bottom:2px!important"><i class="fa fa-home" style="font-size: 19px"></i></a></li>
+       <li><a title="${_('My documents')}" data-rel="navigator-tooltip" data-bind="hueLink: '${ home_url }'" style="padding-bottom:2px!important"><i class="fa fa-home" style="font-size: 19px"></i></a></li>
        <%
          query_apps = count_apps(apps, ['beeswax', 'impala', 'rdbms', 'pig', 'jobsub', 'spark']);
        %>

+ 15 - 1
desktop/core/src/desktop/templates/common_header_footer_components.mako

@@ -47,7 +47,7 @@ from metadata.conf import has_optimizer, OPTIMIZER
     }
 
     // check for IE document modes
-    if (document.documentMode && document.documentMode < 9){
+    if (document.documentMode && document.documentMode < 9) {
       location.href = "${ url('desktop_views_unsupported') }";
     }
 
@@ -87,6 +87,20 @@ from metadata.conf import has_optimizer, OPTIMIZER
       }
     }
 
+
+    var xhrOpen = XMLHttpRequest.prototype.open;
+    XMLHttpRequest.prototype.open = function () {
+      if (arguments[1].indexOf(window.HUE_BASE_URL) < 0) {
+        var index = arguments[1].indexOf(window.location.host);
+        if (index >= 0 && window.HUE_BASE_URL.length) { //Host is present in the url when using an html form.
+          index += window.location.host.length;
+            arguments[1] = arguments[1].substring(0, index) + window.HUE_BASE_URL + arguments[1].substring(index);
+        } else {
+          arguments[1] = window.HUE_BASE_URL + arguments[1];
+        }
+      }
+      return xhrOpen.apply(this, arguments);
+    };
     var xhrSend = XMLHttpRequest.prototype.send;
     XMLHttpRequest.prototype.send = function (data) {
       // Add CSRF Token to all XHR Requests

+ 2 - 2
desktop/core/src/desktop/templates/common_header_m.mako

@@ -104,7 +104,7 @@ if USE_NEW_EDITOR.get():
     }
 
     // check for IE document modes
-    if (document.documentMode && document.documentMode < 9){
+    if (document.documentMode && document.documentMode < 9) {
       location.href = "${ url('desktop_views_unsupported') }";
     }
   </script>
@@ -123,7 +123,7 @@ if USE_NEW_EDITOR.get():
   <script src="${ static('desktop/js/ace/ext-language_tools.js') }"></script>
   <script src="${ static('desktop/js/ace.extended.js') }"></script>
   <script>
-    ace.config.set("basePath", "/static/desktop/js/ace");
+    ace.config.set("basePath", "${ static('desktop/js/ace') }");
   </script>
 
   <script type="text/javascript">

+ 1 - 1
desktop/core/src/desktop/templates/common_notebook_ko_components.mako

@@ -162,7 +162,7 @@ except ImportError, e:
 
 <%def name="downloadSnippetResults()">
   <script type="text/html" id="download-results-template">
-    <form method="POST" action="${ url('notebook:download') }" class="download-form" style="display: inline">
+    <form method="POST" class="download-form" style="display: inline" data-bind="attr: { action: window.HUE_BASE_URL + '${ url('notebook:download') }' }">
       ${ csrf_token(request) | n,unicode }
       <input type="hidden" name="notebook"/>
       <input type="hidden" name="snippet"/>

+ 7 - 0
desktop/core/src/desktop/templates/global_js_constants.mako

@@ -50,6 +50,13 @@
     window.CSRF_TOKEN = '';
   %endif
 
+  window.KNOX_BASE_PATH_HUE = '/KNOX_BASE_PATH_HUE';
+  window._KNOX_BASE_PATH = '/KNOX_BASE_PATH_KNOX';
+  window._KNOX_BASE_URL = '/KNOX_BASE_URL';
+  window.HUE_BASE_URL = window.KNOX_BASE_PATH_HUE.indexOf('KNOX_BASE_PATH_HUE') < 0 ? window.KNOX_BASE_PATH_HUE : '';
+  window.KNOX_BASE_PATH = window._KNOX_BASE_PATH.indexOf('KNOX_BASE_PATH_KNOX') < 0 ? window._KNOX_BASE_PATH_KNOX : '';
+  window.KNOX_BASE_URL = window._KNOX_BASE_URL.indexOf('KNOX_BASE_URL') < 0 ? window._KNOX_BASE_URL : '';
+
   window.HAS_MULTI_CLUSTER = '${ conf.has_multi_cluster() }' === 'True';
 
   window.HAS_SQL_DASHBOARD = '${ HAS_SQL_ENABLED.get() }' === 'True';

+ 7 - 5
desktop/core/src/desktop/templates/hue.mako

@@ -134,8 +134,10 @@
   </script>
 
   ${ commonHeaderFooterComponents.header_i18n_redirection() }
-
-  <script src="/desktop/globalJsConstants.js?v=${ hue_version() }"></script>
+  <%
+    global_constants_url = '/desktop/globalJsConstants.js?v=' + hue_version()
+  %>
+  <script src="${global_constants_url}"></script>
 
   % if not conf.DEV.get():
   <script src="${ static('desktop/js/hue.errorcatcher.js') }"></script>
@@ -273,12 +275,12 @@ ${ hueIcons.symbols() }
             <li data-bind="hueLink: '/useradmin/users/'"><a href="javascript: void(0);"><i class="fa fa-fw fa-group"></i> ${_('Manage Users')}</a></li>
             % endif
             % if is_admin(user):
-            <li><a href="/about/"><span class="dropdown-no-icon">${_('Administration')}</span></a></li>
+            <li><a data-bind="hueLink: '/about/'" href="javascript: void(0);"><span class="dropdown-no-icon">${_('Administration')}</span></a></li>
             % endif
             <li><a href="javascript:void(0)" onclick="huePubSub.publish('show.welcome.tour')"><span class="dropdown-no-icon">${_('Welcome Tour')}</span></a></li>
             <li><a href="http://gethue.com" target="_blank"><span class="dropdown-no-icon">${_('Help')}</span></a></li>
             <li class="divider"></li>
-            <li><a title="${_('Sign out')}" href="/accounts/logout/"><i class="fa fa-fw fa-sign-out"></i> ${ _('Sign out') }</a></li>
+            <li><a title="${_('Sign out')}" data-bind="hueLink: '/accounts/logout'" href="javascript: void(0);"><i class="fa fa-fw fa-sign-out"></i> ${ _('Sign out') }</a></li>
           </ul>
         </div>
         % endif
@@ -479,7 +481,7 @@ ${ render_bundle('hue') | n,unicode }
 <script src="${ static('desktop/js/ace/mode-hive.js') }"></script>
 <script src="${ static('desktop/js/ace/ext-language_tools.js') }"></script>
 <script src="${ static('desktop/js/ace.extended.js') }"></script>
-<script>ace.config.set("basePath", "/static/desktop/js/ace");</script>
+<script>ace.config.set("basePath", "${ static('desktop/js/ace') }");</script>
 
 <script src="${ static('desktop/js/share2.vm.js') }"></script>
 <script src="${ static('metastore/js/metastore.model.js') }"></script>

+ 1 - 1
desktop/core/src/desktop/templates/logs.mako

@@ -82,7 +82,7 @@ ${layout.menubar(section='log_view')}
               <input id="wrapLogs" id="wrap" type="checkbox" checked="checked">
               ${_('Wrap logs')}
             </label>
-            <a href="javascript:void(0)" onclick="window.location.href = '/desktop/download_logs'" class="btn"><i class="fa fa-download"></i> ${_('Download entire log as zip')}</a>
+            <a href="javascript:void(0)" onclick="huePubSub.publish('open.link', '/desktop/download_logs');" class="btn"><i class="fa fa-download"></i> ${_('Download entire log as zip')}</a>
         </form>
       </%def>
     </%actionbar:render>

+ 8 - 8
desktop/core/src/desktop/urls.py

@@ -60,8 +60,8 @@ admin.autodiscover()
 # Some django-wide URLs
 dynamic_patterns = [
   url(r'^hue/accounts/login', desktop_auth_views.dt_login, name='desktop_auth_views_dt_login'),
-  url(r'^accounts/login/$', desktop_auth_views.dt_login), # Deprecated
-  url(r'^accounts/logout/$', desktop_auth_views.dt_logout, {'next_page': '/'}),
+  url(r'^accounts/login/?$', desktop_auth_views.dt_login), # Deprecated
+  url(r'^accounts/logout/?$', desktop_auth_views.dt_logout, {'next_page': '/'}),
   url(r'^profile$', desktop_auth_views.profile),
   url(r'^login/oauth/?$', desktop_auth_views.oauth_login),
   url(r'^login/oauth_authenticated/?$', desktop_auth_views.oauth_authenticated),
@@ -103,7 +103,7 @@ dynamic_patterns += [
   # Mobile
   url(r'^assist_m', desktop_views.assist_m),
   # Hue 4
-  url(r'^hue.*/$', desktop_views.hue, name='desktop_views_hue'),
+  url(r'^hue.*/?$', desktop_views.hue, name='desktop_views_hue'),
   url(r'^403$', desktop_views.path_forbidden),
   url(r'^404$', desktop_views.not_found),
   url(r'^500$', desktop_views.server_error),
@@ -157,7 +157,7 @@ dynamic_patterns += [
   url(r'^desktop/api2/context/namespaces/(?P<interface>[\w\-]+)/?$', desktop_api2.get_context_namespaces),
   url(r'^desktop/api2/context/computes/(?P<interface>[\w\-]+)/?$', desktop_api2.get_context_computes),
   url(r'^desktop/api2/context/clusters/(?P<interface>[\w\-]+)/?$', desktop_api2.get_context_clusters),
-  url(r'^desktop/api2/user_preferences/(?P<key>\w+)?$', desktop_api2.user_preferences, name="desktop.api2.user_preferences"),
+  url(r'^desktop/api2/user_preferences(?:/(?P<key>\w+))?/?$', desktop_api2.user_preferences, name="desktop.api2.user_preferences"),
 
   url(r'^desktop/api2/doc/export/?$', desktop_api2.export_documents),
   url(r'^desktop/api2/doc/import/?$', desktop_api2.import_documents),
@@ -190,21 +190,21 @@ dynamic_patterns += [
 # Metrics specific
 if METRICS.ENABLE_WEB_METRICS.get():
   dynamic_patterns += [
-    url(r'^desktop/metrics/', include('desktop.lib.metrics.urls'))
+    url(r'^desktop/metrics/?', include('desktop.lib.metrics.urls'))
   ]
 
 if CONNECTORS.IS_ENABLED.get():
   dynamic_patterns += [
-    url(r'^desktop/connectors/', include('desktop.lib.connectors.urls'))
+    url(r'^desktop/connectors/?', include('desktop.lib.connectors.urls'))
   ]
 
 if ANALYTICS.IS_ENABLED.get():
   dynamic_patterns += [
-    url(r'^desktop/analytics/', include('desktop.lib.analytics.urls'))
+    url(r'^desktop/analytics/?', include('desktop.lib.analytics.urls'))
   ]
 
 dynamic_patterns += [
-  url(r'^admin/', include(admin.site.urls)),
+  url(r'^admin/?', include(admin.site.urls)),
 ]
 
 static_patterns = []

+ 1 - 1
desktop/libs/dashboard/src/dashboard/static/dashboard/js/collections.ko.js

@@ -125,7 +125,7 @@ var SearchCollectionsModel = function (props) {
 
   self.editCollection = function (collection) {
     cleanCollections();
-    location.href = '/hue' + collection.absoluteUrl();
+    huePubSub.publish('open.link', '/hue' + collection.absoluteUrl());
   };
 
   self.markManyForDeletion = function (collections) {

+ 1 - 2
desktop/libs/dashboard/src/dashboard/templates/common_search.mako

@@ -1469,7 +1469,7 @@ ${ dashboard.layout_skeleton(suffix='search') }
         <!-- /ko -->
 
         <!-- ko if: $root.collection.engine() == 'solr' -->
-        <form method="POST" action="${ url('dashboard:download') }" style="display:inline">
+        <form method="POST" style="display:inline" data-bind="attr: { action: window.HUE_BASE_URL + '${ url("dashboard:download") }' }">
           ${ csrf_token(request) | n,unicode }
           <input type="hidden" name="collection" data-bind="value: ko.mapping.toJSON($root.collection)"/>
           <input type="hidden" name="query" data-bind="value: ko.mapping.toJSON($root.query)"/>
@@ -3139,7 +3139,6 @@ var ALPHA_HIT_OPTIONS = [
 var HIT_OPTIONS = NUMERIC_HIT_OPTIONS
 ;
 
-
 function prepareShareModal() {
   shareViewModel.setDocUuid(this.collection.uuid());
   openShareModal();

+ 1 - 1
desktop/libs/dashboard/src/dashboard/urls.py

@@ -36,7 +36,7 @@ urlpatterns = [
 
 urlpatterns += [
   url(r'^search$', dashboard_api.search, name='search'),
-  url(r'^suggest/$', dashboard_api.query_suggest, name='query_suggest'),
+  url(r'^suggest/?$', dashboard_api.query_suggest, name='query_suggest'),
   url(r'^index/fields/dynamic$', dashboard_api.index_fields_dynamic, name='index_fields_dynamic'),
   url(r'^index/fields/nested_documents', dashboard_api.nested_documents, name='nested_documents'),
   url(r'^template/new_facet$', dashboard_api.new_facet, name='new_facet'),

+ 3 - 3
desktop/libs/indexer/src/indexer/static/indexer/js/collections.js

@@ -255,7 +255,7 @@ var CreateCollectionViewModel = function() {
           'quote': self.fieldQuoteCharacter()
         }).done(function(data) {
           if (data.status == 0) {
-            window.location.href = '/hue/indexer/'; // Can't open on collection name as no collections refresh yet
+            huePubSub.publish('open.link', '/hue/indexer');// Can't open on collection name as no collections refresh yet
           } else {
             self.isLoading(false);
             $(document).trigger("error", data.message);
@@ -274,7 +274,7 @@ var CreateCollectionViewModel = function() {
           'source': self.source()
         }).done(function(data) {
           if (data.status == 0) {
-            window.location.href = '/hue/indexer/'; // Can't open on collection name as no collections refresh yet
+            huePubSub.publish('open.link', '/hue/indexer');// Can't open on collection name as no collections refresh yet
           } else {
             $(document).trigger("error", data.message);
           }
@@ -383,7 +383,7 @@ var EditCollectionViewModel = function() {
         'quote': self.fieldQuoteCharacter()
       }).done(function(data) {
         if (data.status == 0) {
-          window.location.href = '/indexer';
+          huePubSub.publish('open.link', '/hue/indexer');
         } else {
           self.isLoading(false);
           $(document).trigger("error", data.message);

+ 4 - 4
desktop/libs/indexer/src/indexer/static/indexer/js/indexes.ko.js

@@ -255,7 +255,7 @@ var CreateCollectionViewModel = function() {
           'quote': self.fieldQuoteCharacter()
         }).done(function(data) {
           if (data.status == 0) {
-            window.location.href = '/indexer';
+            huePubSub.publish('open.link', '/indexer');
           } else {
             self.isLoading(false);
             $(document).trigger("error", data.message);
@@ -274,7 +274,7 @@ var CreateCollectionViewModel = function() {
           'source': self.source()
         }).done(function(data) {
           if (data.status == 0) {
-            window.location.href = '/indexer';
+            huePubSub.publish('open.link', '/indexer');
           } else {
             $(document).trigger("error", data.message);
           }
@@ -337,7 +337,7 @@ var EditCollectionViewModel = function() {
       'collections': ko.mapping.toJSON(data)
     }).done(function(data) {
       if (data.status == 0) {
-        window.location.href = '/indexer';
+        huePubSub.publish('open.link', '/indexer');
       } else {
         $(document).trigger("error", data.message);
       }
@@ -383,7 +383,7 @@ var EditCollectionViewModel = function() {
         'quote': self.fieldQuoteCharacter()
       }).done(function(data) {
         if (data.status == 0) {
-          window.location.href = '/indexer';
+          huePubSub.publish('open.link', '/indexer');
         } else {
           self.isLoading(false);
           $(document).trigger("error", data.message);

+ 2 - 2
desktop/libs/indexer/src/indexer/templates/importer.mako

@@ -2828,13 +2828,13 @@ ${ assist.assistPanel() }
                         var db = match[1];
                         dataCatalog.getEntry({ sourceType: snippet.type(), namespace: self.namespace(), compute: self.compute(), path: [ db ]}).done(function (dbEntry) {
                           dbEntry.clearCache({ invalidate: 'invalidate', silenceErrors: true }).done(function () {
-                            window.location.href = self.editorVM.selectedNotebook().onSuccessUrl();
+                            huePubSub.publish('open.link', self.editorVM.selectedNotebook().onSuccessUrl());
                           })
                         });
                       } else {
                         dataCatalog.getEntry({ sourceType: snippet.type(), namespace: self.namespace(), compute: self.compute(), path: []}).done(function (sourceEntry) {
                           sourceEntry.clearCache({ silenceErrors: true }).done(function () {
-                            window.location.href = self.editorVM.selectedNotebook().onSuccessUrl();
+                            huePubSub.publish('open.link', self.editorVM.selectedNotebook().onSuccessUrl());
                           })
                         });
                       }

+ 27 - 27
desktop/libs/indexer/src/indexer/urls.py

@@ -27,67 +27,67 @@ from indexer.conf import ENABLE_NEW_INDEXER
 urlpatterns = [
   url(r'^install_examples$', indexer_views.install_examples, name='install_examples'),
 
-  url(r'^importer/$', indexer_views.importer, name='importer'),
+  url(r'^importer/?$', indexer_views.importer, name='importer'),
   url(r'^importer/prefill/(?P<source_type>[^/]+)/(?P<target_type>[^/]+)/(?P<target_path>[^/]+)?$', indexer_views.importer_prefill, name='importer_prefill'),
 ]
 
 if ENABLE_NEW_INDEXER.get():
   urlpatterns += [
     url(r'^$', indexer_views.indexes, name='indexes'),
-    url(r'^indexes/$', indexer_views.indexes, name='indexes'),
+    url(r'^indexes/?$', indexer_views.indexes, name='indexes'),
     url(r'^indexes/(?P<index>[^/]+)/?$', indexer_views.indexes, name='indexes'),
     url(r'^collections$', indexer_views.collections, name='collections'), # Old page
   ]
 else:
   urlpatterns += [
     url(r'^$', indexer_views.collections, name='collections'),
-    url(r'^indexes/$', indexer_views.indexes, name='indexes'),
+    url(r'^indexes/?$', indexer_views.indexes, name='indexes'),
   ]
 
 urlpatterns += [
-    url(r'^topics/$', indexer_views.topics, name='topics'),
+    url(r'^topics/?$', indexer_views.topics, name='topics'),
     url(r'^topics/(?P<index>[^/]+)/?$', indexer_views.topics, name='topics'),
 ]
 
 
 urlpatterns += [
   # V2
-  url(r'^api/aliases/create/$', indexer_solr_api.create_alias, name='create_alias'),
-  url(r'^api/configs/list/$', indexer_solr_api.list_configs, name='list_configs'),
-  url(r'^api/index/list/$', indexer_solr_api.list_index, name='list_index'),
-  url(r'^api/indexes/list/$', indexer_solr_api.list_indexes, name='list_indexes'),
-  url(r'^api/indexes/create/$', indexer_solr_api.create_index, name='create_index'),
-  url(r'^api/indexes/index/$', indexer_solr_api.index, name='index'),
-  url(r'^api/indexes/sample/$', indexer_solr_api.sample_index, name='sample_index'),
-  url(r'^api/indexes/config/$', indexer_solr_api.config_index, name='config_index'),
-  url(r'^api/indexes/delete/$', indexer_solr_api.delete_indexes, name='delete_indexes'),
+  url(r'^api/aliases/create/?$', indexer_solr_api.create_alias, name='create_alias'),
+  url(r'^api/configs/list/?$', indexer_solr_api.list_configs, name='list_configs'),
+  url(r'^api/index/list/?$', indexer_solr_api.list_index, name='list_index'),
+  url(r'^api/indexes/list/?$', indexer_solr_api.list_indexes, name='list_indexes'),
+  url(r'^api/indexes/create/?$', indexer_solr_api.create_index, name='create_index'),
+  url(r'^api/indexes/index/?$', indexer_solr_api.index, name='index'),
+  url(r'^api/indexes/sample/?$', indexer_solr_api.sample_index, name='sample_index'),
+  url(r'^api/indexes/config/?$', indexer_solr_api.config_index, name='config_index'),
+  url(r'^api/indexes/delete/?$', indexer_solr_api.delete_indexes, name='delete_indexes'),
 ]
 
 urlpatterns += [
   # Importer
-  url(r'^api/indexer/guess_format/$', indexer_api3.guess_format, name='guess_format'),
-  url(r'^api/indexer/guess_field_types/$', indexer_api3.guess_field_types, name='guess_field_types'),
+  url(r'^api/indexer/guess_format/?$', indexer_api3.guess_format, name='guess_format'),
+  url(r'^api/indexer/guess_field_types/?$', indexer_api3.guess_field_types, name='guess_field_types'),
 
   url(r'^api/importer/submit', indexer_api3.importer_submit, name='importer_submit'),
   url(r'^api/importer/save/?$', indexer_api3.save_pipeline, name='save_pipeline'),
 ]
 
 urlpatterns += [
-  url(r'^api/indexer/indexers/get_db_component/$', indexer_indexers_rdbms.get_db_component, name='get_db_component'),
-  url(r'^api/indexer/indexers/get_drivers/$', indexer_indexers_rdbms.get_drivers, name='get_drivers'),
-  url(r'^api/indexer/indexers/jdbc_db_list/$', indexer_indexers_rdbms.jdbc_db_list, name='jdbc_db_list')
+  url(r'^api/indexer/indexers/get_db_component/?$', indexer_indexers_rdbms.get_db_component, name='get_db_component'),
+  url(r'^api/indexer/indexers/get_drivers/?$', indexer_indexers_rdbms.get_drivers, name='get_drivers'),
+  url(r'^api/indexer/indexers/jdbc_db_list/?$', indexer_indexers_rdbms.jdbc_db_list, name='jdbc_db_list')
 ]
 
 
 # Deprecated
 urlpatterns += [
-  url(r'^api/fields/parse/$', indexer_api.parse_fields, name='api_parse_fields'),
-  url(r'^api/autocomplete/$', indexer_api.autocomplete, name='api_autocomplete'),
-  url(r'^api/collections/$', indexer_api.collections, name='api_collections'),
-  url(r'^api/collections/create/$', indexer_api.collections_create, name='api_collections_create'),
-  url(r'^api/collections/import/$', indexer_api.collections_import, name='api_collections_import'),
-  url(r'^api/collections/remove/$', indexer_api.collections_remove, name='api_collections_remove'),
-  url(r'^api/collections/(?P<collection>[^/]+)/fields/$', indexer_api.collections_fields, name='api_collections_fields'),
-  url(r'^api/collections/(?P<collection>[^/]+)/update/$', indexer_api.collections_update, name='api_collections_update'),
-  url(r'^api/collections/(?P<collection>[^/]+)/data/$', indexer_api.collections_data, name='api_collections_data'),
+  url(r'^api/fields/parse/?$', indexer_api.parse_fields, name='api_parse_fields'),
+  url(r'^api/autocomplete/?$', indexer_api.autocomplete, name='api_autocomplete'),
+  url(r'^api/collections/?$', indexer_api.collections, name='api_collections'),
+  url(r'^api/collections/create/?$', indexer_api.collections_create, name='api_collections_create'),
+  url(r'^api/collections/import/?$', indexer_api.collections_import, name='api_collections_import'),
+  url(r'^api/collections/remove/?$', indexer_api.collections_remove, name='api_collections_remove'),
+  url(r'^api/collections/(?P<collection>[^/]+)/fields/?$', indexer_api.collections_fields, name='api_collections_fields'),
+  url(r'^api/collections/(?P<collection>[^/]+)/update/?$', indexer_api.collections_update, name='api_collections_update'),
+  url(r'^api/collections/(?P<collection>[^/]+)/data/?$', indexer_api.collections_data, name='api_collections_data'),
 ]

+ 3 - 2
desktop/libs/notebook/src/notebook/connectors/hiveserver2.py

@@ -31,6 +31,7 @@ from desktop.lib.conf import BoundConfig
 from desktop.lib.exceptions import StructuredException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.i18n import force_unicode
+from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
 from desktop.lib.rest.http_client import RestException
 from desktop.lib.thrift_util import unpack_guid, unpack_guid_base64
 from desktop.models import DefaultConfiguration, Document2
@@ -496,7 +497,7 @@ class HS2Api(Api):
 
     upload(target_file, handle, self.request.user, db, self.request.fs, max_rows=max_rows, max_bytes=max_bytes)
 
-    return '/filebrowser/view=%s' % urllib.quote(urllib.quote(target_file.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\'')) # Quote twice, because of issue in the routing on client
+    return '/filebrowser/view=%s' % urllib.quote(urllib.quote(target_file.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)) # Quote twice, because of issue in the routing on client
 
 
   def export_data_as_table(self, notebook, snippet, destination, is_temporary=False, location=None):
@@ -550,7 +551,7 @@ DROP TABLE IF EXISTS `%(table)s`;
       'location': self.request.fs.netnormpath(destination),
       'hql': query.hql_query
     }
-    success_url = '/filebrowser/view=%s' % urllib.quote(destination.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\'')
+    success_url = '/filebrowser/view=%s' % urllib.quote(destination.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS)
 
     return hql, success_url
 

+ 2 - 1
desktop/libs/notebook/src/notebook/models.py

@@ -29,6 +29,7 @@ from django.contrib.auth.models import User
 from django.contrib.sessions.models import Session
 from django.db.models import Count
 from django.db.models.functions import Trunc
+from desktop.lib.paths import SAFE_CHARACTERS_URI_COMPONENTS
 from django.utils.html import escape
 from django.utils.translation import ugettext as _
 
@@ -117,7 +118,7 @@ def make_notebook(name='Browse', description='', editor_type='hive', statement='
     'type': 'notebook' if is_notebook else 'query-%s' % editor_type,
     'showHistory': True,
     'isSaved': is_saved,
-    'onSuccessUrl': urllib.quote(on_success_url.encode('utf-8'), safe='~@#$&()*!+=:;,.?/\'') if on_success_url else None,
+    'onSuccessUrl': urllib.quote(on_success_url.encode('utf-8'), safe=SAFE_CHARACTERS_URI_COMPONENTS) if on_success_url else None,
     'pubSubUrl': pub_sub_url,
     'skipHistorify': skip_historify,
     'isManaged': is_task,

+ 1 - 1
desktop/libs/notebook/src/notebook/templates/editor_m.mako

@@ -167,7 +167,7 @@ ${ assist.assistJSModels() }
     ko.options.deferUpdates = true;
   }
 
-  ace.config.set("basePath", "/static/desktop/js/ace");
+  ace.config.set("basePath", "${ static('desktop/js/ace') }");
 
   var VIEW_MODEL_OPTIONS = $.extend(${ options_json | n,unicode }, {
     user: '${ user.username }',

+ 3 - 3
desktop/libs/notebook/src/notebook/urls.py

@@ -43,7 +43,7 @@ urlpatterns = [
 
   url(r'^editor/?$', notebook_views.editor, name='editor'),
   url(r'^editor_m/?$', notebook_views.editor_m, name='editor_m'),
-  url(r'^browse/(?P<database>\w+)/(?P<table>\w+)/(?P<partition_spec>.+?)?$', notebook_views.browse, name='browse'),
+  url(r'^browse/(?P<database>\w+)/(?P<table>\w+)(?:/(?P<partition_spec>.+?))?/?$', notebook_views.browse, name='browse'),
   url(r'^execute_and_watch/?$', notebook_views.execute_and_watch, name='execute_and_watch'),
 ]
 
@@ -52,7 +52,7 @@ urlpatterns += [
   url(r'^api/create_notebook/?$', notebook_api.create_notebook, name='create_notebook'),
   url(r'^api/create_session/?$', notebook_api.create_session, name='create_session'),
   url(r'^api/close_session/?$', notebook_api.close_session, name='close_session'),
-  url(r'^api/execute/?(?P<engine>.+)?$', notebook_api.execute, name='execute'),
+  url(r'^api/execute(?:/(?P<engine>.+))?/?$', notebook_api.execute, name='execute'),
   url(r'^api/check_status/?$', notebook_api.check_status, name='check_status'),
   url(r'^api/fetch_result_data/?$', notebook_api.fetch_result_data, name='fetch_result_data'),
   url(r'^api/fetch_result_metadata/?$', notebook_api.fetch_result_metadata, name='fetch_result_metadata'),
@@ -102,5 +102,5 @@ urlpatterns += [
 urlpatterns += [
   url(r'^api/describe/(?P<database>\w+)/?$', notebook_api.describe, name='api_describe_database'),
   url(r'^api/describe/(?P<database>\w+)/(?P<table>[\w_\-]+)/?$', notebook_api.describe, name='api_describe_table'),
-  url(r'^api/describe/(?P<database>\w+)/(?P<table>\w+)/stats/(?P<column>\w+)?$', notebook_api.describe, name='api_describe_column'),
+  url(r'^api/describe/(?P<database>\w+)/(?P<table>\w+)/stats(?:/(?P<column>\w+))?/?$', notebook_api.describe, name='api_describe_column'),
 ]

+ 118 - 0
tools/knox/hue/1.0.0/rewrite.xml

@@ -0,0 +1,118 @@
+<rules>
+  <rule dir="IN" name="HUE/hue/inbound/huerule" flow="OR">
+    <!-- We have exceptions for dashboard/hbase/notebook, because endpoint that ends at the app/lib level need to terminate with a slash and we remove them in generic case. -->
+    <!-- Other solution would be to accept trailing slash on all endpoints, but there's some issue with the filebrowser -->
+    <match pattern="*://*:*/**/hue/hue/dashboard?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/hue/dashboard/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/dashboard?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/dashboard/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/hue/hbase?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/hue/hbase/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/hbase?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/hbase/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/hue/jobbrowser?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/hue/jobbrowser/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/jobbrowser?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/jobbrowser/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/hue/notebook?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/hue/notebook/?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/notebook?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/notebook/?{**}"/>
+    </match>
+    <!-- We have exceptions for static because it can have hue or static further in the URL and KNOX matches the last one with **, but we want to match the first one. -->
+    <match pattern="*://*:*/**/hue/static/{**}?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/static/{**}?{**}"/>
+    </match>
+    <!-- The rest: Generic case -->
+    <match pattern="*://*:*/**/hue/hue/{**}?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/hue/{**}?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/{**}?{**}">
+      <rewrite template="{$serviceUrl[HUE]}/{**}?{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/{**}">
+      <rewrite template="{$serviceUrl[HUE]}/{**}"/>
+    </match>
+    <match pattern="*://*:*/**/hue/">
+      <rewrite template="{$serviceUrl[HUE]}/"/>
+    </match>
+ </rule>
+
+  <rule dir="OUT" name="HUE/hue/outbound/huerooturl/desktop" flow="OR">
+    <match pattern="/desktop/{**}?{**}" >
+       <rewrite template="{$frontend[path]}/hue/desktop/{**}?{**}"/>
+    </match>
+  </rule>
+  <rule dir="OUT" name="HUE/hue/outbound/huerooturl/static" flow="OR">
+    <match pattern="/static/{**}?{**}">
+       <rewrite template="{$frontend[path]}/hue/static/{**}?{**}"/>
+    </match>
+  </rule>
+  <rule dir="OUT" name="HUE/hue/outbound/huerooturl/css" pattern="/static/{*}/{**}?{**}">
+    <rewrite template="{$frontend[path]}/hue/static/{*}/{**}?{**}"/>
+  </rule>
+  <rule dir="OUT" name="HUE/hue/outbound/huerooturl/root" pattern="/boohoo">
+    <rewrite template="{$frontend[path]}/hue/boohoo"/>
+  </rule>
+  <filter name="HUE/hue/outbound/huerooturl">
+    <content type="*/html">
+        <apply path="/desktop/globalJsConstants.*" rule="HUE/hue/outbound/huerooturl/desktop"/>
+        <apply path="/boohoo" rule="HUE/hue/outbound/huerooturl/root"/>
+        <apply path="\/static\/.*\/.*\.(js|css|html|png)" rule="HUE/hue/outbound/huerooturl/static"/>
+        <apply path="\/static\/desktop\/js\/ace" rule="HUE/hue/outbound/huerooturl/static"/>
+    </content>
+    <content type="*/css">
+        <apply path="\/static\/.*\/.*\.(js|css|html|png)" rule="HUE/hue/outbound/huerooturl/static"/>
+    </content>
+  </filter>
+
+  <filter name="HUE/hue/outbound/workers">
+    <content type="*/javascript">
+        <apply path="\/static\/.*\/.*\.(js|css|html|png)" rule="HUE/hue/outbound/huerooturl/static"/>
+    </content>
+  </filter>
+
+  <rule dir="OUT" name="HUE/hue/outbound/global/base/hue" flow="OR">
+    <match pattern="/KNOX_BASE_PATH_HUE" >
+       <rewrite template="{$frontend[path]}/hue"/>
+    </match>
+  </rule>
+  <rule dir="OUT" name="HUE/hue/outbound/global/base/knox" flow="OR">
+    <match pattern="/KNOX_BASE_URL" >
+       <rewrite template="{$frontend[url]}"/>
+    </match>
+  </rule>
+  <filter name="HUE/hue/outbound/global">
+    <content type="*/javascript">
+        <apply path="/KNOX_BASE_PATH_HUE" rule="HUE/hue/outbound/global/base/hue"/>
+        <apply path="/KNOX_BASE_URL" rule="HUE/hue/outbound/global/base/knox"/>
+    </content>
+  </filter>
+
+  <rule dir="OUT" name="HUE/hue/outbound/headers/location" flow="OR">
+    <match pattern="/hue/{**}?{**}">
+      <rewrite template="{$frontend[path]}/hue/hue/{**}?{**}"/>
+    </match>
+    <match pattern="/hue">
+      <rewrite template="{$frontend[path]}/hue/hue"/>
+    </match>
+    <match pattern="/{**}?{**}">
+      <rewrite template="{$frontend[path]}/hue/{**}?{**}"/>
+    </match>
+    <match pattern="/">
+      <rewrite template="{$frontend[path]}/hue/"/>
+    </match>
+  </rule>
+  <filter name="HUE/hue/outbound/headers">
+    <content type="application/x-http-headers">
+       <apply path="Location" rule="HUE/hue/outbound/headers/location"/>
+    </content>
+  </filter>
+</rules>

+ 37 - 0
tools/knox/hue/1.0.0/service.xml

@@ -0,0 +1,37 @@
+<service role="HUE" name="hue" version="1.0.0">
+  <routes>
+    <!-- Since we don't have to rewrite URLs for all javascript, we single out these 2 exceptions: 1 for workers & the other for the base URL -->
+    <route path="/hue/desktop/workers/{**}?{**}">
+      <rewrite apply="HUE/hue/inbound/huerule" to="request.url"/>
+      <rewrite apply="HUE/hue/outbound/headers" to="response.headers"/>
+      <rewrite apply="HUE/hue/outbound/workers" to="response.body"/>
+    </route>
+    <route path="/hue/desktop/globalJsConstants.js?{**}">
+      <rewrite apply="HUE/hue/inbound/huerule" to="request.url"/>
+      <rewrite apply="HUE/hue/outbound/headers" to="response.headers"/>
+      <rewrite apply="HUE/hue/outbound/global" to="response.body"/>
+    </route>
+    <!-- All URLs except above exceptions -->
+    <route path="/hue/**">
+      <rewrite apply="HUE/hue/inbound/huerule" to="request.url"/>
+      <rewrite apply="HUE/hue/outbound/headers" to="response.headers"/>
+      <rewrite apply="HUE/hue/outbound/huerooturl" to="response.body"/>
+    </route>
+    <route path="/hue/">
+      <rewrite apply="HUE/hue/inbound/huerule" to="request.url"/>
+      <rewrite apply="HUE/hue/outbound/headers" to="response.headers"/>
+      <rewrite apply="HUE/hue/outbound/huerooturl" to="response.body"/>
+    </route>
+    <route path="/hue/**?**">
+      <rewrite apply="HUE/hue/inbound/huerule" to="request.url"/>
+      <rewrite apply="HUE/hue/outbound/headers" to="response.headers"/>
+      <rewrite apply="HUE/hue/outbound/huerooturl" to="response.body"/>
+    </route>
+    <route path="/hue/?**">
+      <rewrite apply="HUE/hue/inbound/huerule" to="request.url"/>
+      <rewrite apply="HUE/hue/outbound/headers" to="response.headers"/>
+      <rewrite apply="HUE/hue/outbound/huerooturl" to="response.body"/>
+    </route>
+  </routes>
+  <dispatch classname="org.apache.knox.gateway.dispatch.PassAllHeadersDispatch" />
+</service>