From 1acceb941d84775cdeb0019bdfbdd63e9e125982 Mon Sep 17 00:00:00 2001 From: rr- Date: Fri, 20 Jan 2017 21:51:04 +0100 Subject: [PATCH] client: refactor linking and routing Print all links through new uri.js component Refactor the router to use more predictable parsing Fix linking to entities with weird names (that contain slashes, + etc.) --- client/html/comment.tpl | 4 +- client/html/comments_page.tpl | 2 +- client/html/help.tpl | 10 +-- client/html/help_search.tpl | 8 +-- client/html/home.tpl | 2 +- client/html/home_footer.tpl | 2 +- client/html/login.tpl | 2 +- client/html/not_found.tpl | 2 +- client/html/post_detail.tpl | 4 +- client/html/post_readonly_sidebar.tpl | 4 +- client/html/posts_header.tpl | 2 +- client/html/posts_page.tpl | 2 +- client/html/settings.tpl | 2 +- client/html/tag.tpl | 8 +-- client/html/tag_category_row.tpl | 2 +- client/html/tag_delete.tpl | 2 +- client/html/tag_summary.tpl | 2 +- client/html/tags_header.tpl | 4 +- client/html/tags_page.tpl | 20 +++--- client/html/user.tpl | 6 +- client/html/user_registration.tpl | 2 +- client/html/user_summary.tpl | 10 +-- client/html/users_header.tpl | 2 +- client/html/users_page.tpl | 4 +- client/js/api.js | 1 + client/js/controllers/auth_controller.js | 9 +-- client/js/controllers/comments_controller.js | 7 +- client/js/controllers/help_controller.js | 6 +- client/js/controllers/home_controller.js | 2 +- client/js/controllers/not_found_controller.js | 2 +- .../controllers/password_reset_controller.js | 17 ++--- .../js/controllers/post_detail_controller.js | 10 ++- client/js/controllers/post_list_controller.js | 9 ++- client/js/controllers/post_main_controller.js | 15 ++-- .../js/controllers/post_upload_controller.js | 5 +- client/js/controllers/settings_controller.js | 2 +- client/js/controllers/snapshots_controller.js | 7 +- .../controllers/tag_categories_controller.js | 2 +- client/js/controllers/tag_controller.js | 17 +++-- client/js/controllers/tag_list_controller.js | 9 ++- client/js/controllers/user_controller.js | 14 ++-- client/js/controllers/user_list_controller.js | 9 ++- .../user_registration_controller.js | 5 +- client/js/controls/tag_input_control.js | 9 ++- client/js/main.js | 4 +- client/js/models/comment.js | 13 ++-- client/js/models/info.js | 3 +- client/js/models/post.js | 31 ++++++--- client/js/models/post_list.js | 33 +++++---- client/js/models/snapshot_list.js | 19 +++--- client/js/models/tag.js | 13 ++-- client/js/models/tag_category.js | 7 +- client/js/models/tag_category_list.js | 19 ++++-- client/js/models/tag_list.js | 26 ++++--- client/js/models/user.js | 11 ++- client/js/models/user_list.js | 19 +++--- client/js/router.js | 68 +++++++++++++------ client/js/util/misc.js | 38 +---------- client/js/util/uri.js | 62 +++++++++++++++++ client/js/util/views.js | 28 ++++---- client/js/views/home_view.js | 4 +- client/js/views/manual_page_view.js | 1 - client/js/views/post_main_view.js | 9 +-- client/js/views/users_header_view.js | 1 - client/package.json | 1 - 65 files changed, 380 insertions(+), 295 deletions(-) create mode 100644 client/js/util/uri.js diff --git a/client/html/comment.tpl b/client/html/comment.tpl index 04469c9a..6bea7045 100644 --- a/client/html/comment.tpl +++ b/client/html/comment.tpl @@ -1,7 +1,7 @@
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %> - + '> <% } %> <%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %> @@ -23,7 +23,7 @@ diff --git a/client/html/help_search.tpl b/client/html/help_search.tpl index 8c22d73e..70737893 100644 --- a/client/html/help_search.tpl +++ b/client/html/help_search.tpl @@ -1,9 +1,9 @@ diff --git a/client/html/home.tpl b/client/html/home.tpl index f1d9b1c1..1e51b12b 100644 --- a/client/html/home.tpl +++ b/client/html/home.tpl @@ -8,7 +8,7 @@ <%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %> or - browse all posts + '>browse all posts <% } %> diff --git a/client/html/home_footer.tpl b/client/html/home_footer.tpl index 9ab9cd0c..8f9cb10a 100644 --- a/client/html/home_footer.tpl +++ b/client/html/home_footer.tpl @@ -2,6 +2,6 @@
  • <%- ctx.postCount %> posts
  • <%= ctx.makeFileSize(ctx.diskUsage) %>
  • Build <%- ctx.version %> from <%= ctx.makeRelativeTime(ctx.buildDate) %>
  • - <% if (ctx.canListSnapshots) { %>
  • History
  • + <% if (ctx.canListSnapshots) { %>
  • '>History
  • <% } %> diff --git a/client/html/login.tpl b/client/html/login.tpl index cc2a6805..8ccc439d 100644 --- a/client/html/login.tpl +++ b/client/html/login.tpl @@ -31,7 +31,7 @@
    <% if (ctx.canSendMails) { %> - Forgot the password? + '>Forgot the password? <% } %>
    diff --git a/client/html/not_found.tpl b/client/html/not_found.tpl index 15a7dfed..53bcdcd7 100644 --- a/client/html/not_found.tpl +++ b/client/html/not_found.tpl @@ -1,5 +1,5 @@

    Not found

    <%- ctx.path %> is not a valid URL.

    -

    Back to main page

    +

    Back to main page

    diff --git a/client/html/post_detail.tpl b/client/html/post_detail.tpl index 5e04ab22..65ff7868 100644 --- a/client/html/post_detail.tpl +++ b/client/html/post_detail.tpl @@ -2,9 +2,9 @@

    Post #<%- ctx.post.id %>

    diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl index 32dafd6c..f29c8b65 100644 --- a/client/html/post_readonly_sidebar.tpl +++ b/client/html/post_readonly_sidebar.tpl @@ -67,14 +67,14 @@ --><% for (let tag of ctx.post.tags) { %>
  • <% if (ctx.canViewTags) { %>'>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><% } %><% if (ctx.canViewTags) { %><% } %><% if (ctx.canListPosts) { %>'>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><% } %><%- tag %> <% if (ctx.canListPosts) { %> diff --git a/client/html/tag_category_row.tpl b/client/html/tag_category_row.tpl index 3ec199c1..dcc8c16a 100644 --- a/client/html/tag_category_row.tpl +++ b/client/html/tag_category_row.tpl @@ -19,7 +19,7 @@ <% if (ctx.tagCategory.name) { %> - + '> <%- ctx.tagCategory.tagCount %> <% } else { %> diff --git a/client/html/tag_delete.tpl b/client/html/tag_delete.tpl index 7dbfc1e6..e7be8cf2 100644 --- a/client/html/tag_delete.tpl +++ b/client/html/tag_delete.tpl @@ -1,6 +1,6 @@
    -

    This tag has <%- ctx.tag.postCount %> usage(s).

    +

    This tag has '><%- ctx.tag.postCount %> usage(s).

    • diff --git a/client/html/tag_summary.tpl b/client/html/tag_summary.tpl index 6938e344..0513d643 100644 --- a/client/html/tag_summary.tpl +++ b/client/html/tag_summary.tpl @@ -36,6 +36,6 @@

      <%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %> -

      This tag has <%- ctx.tag.postCount %> usage(s).

      +

      This tag has '><%- ctx.tag.postCount %> usage(s).

    diff --git a/client/html/tags_header.tpl b/client/html/tags_header.tpl index ed641279..59c5bcbc 100644 --- a/client/html/tags_header.tpl +++ b/client/html/tags_header.tpl @@ -8,9 +8,9 @@
    - Syntax help + '>Syntax help <% if (ctx.canEditTagCategories) { %> - Tag categories + '>Tag categories <% } %>
    diff --git a/client/html/tags_page.tpl b/client/html/tags_page.tpl index d66b342e..f27f49e3 100644 --- a/client/html/tags_page.tpl +++ b/client/html/tags_page.tpl @@ -4,37 +4,37 @@ <% if (ctx.query == 'sort:name' || !ctx.query) { %> - Tag name(s) + '>Tag name(s) <% } else { %> - Tag name(s) + '>Tag name(s) <% } %> <% if (ctx.query == 'sort:implication-count') { %> - Implications + '>Implications <% } else { %> - Implications + '>Implications <% } %> <% if (ctx.query == 'sort:suggestion-count') { %> - Suggestions + '>Suggestions <% } else { %> - Suggestions + '>Suggestions <% } %> <% if (ctx.query == 'sort:usages') { %> - Usages + '>Usages <% } else { %> - Usages + '>Usages <% } %> <% if (ctx.query == 'sort:creation-time') { %> - Created on + '>Created on <% } else { %> - Created on + '>Created on <% } %> diff --git a/client/html/user.tpl b/client/html/user.tpl index 4e7d00ae..28e34e67 100644 --- a/client/html/user.tpl +++ b/client/html/user.tpl @@ -2,12 +2,12 @@

    <%- ctx.user.name %>

    diff --git a/client/html/user_registration.tpl b/client/html/user_registration.tpl index e0e0f81c..a6d291f4 100644 --- a/client/html/user_registration.tpl +++ b/client/html/user_registration.tpl @@ -51,6 +51,6 @@
  • vote up/down on posts and comments

  • -

    By creating an account, you are agreeing to the Terms of Service.

    +

    By creating an account, you are agreeing to the '>Terms of Service.

    diff --git a/client/html/user_summary.tpl b/client/html/user_summary.tpl index b2d40ace..1a33a57f 100644 --- a/client/html/user_summary.tpl +++ b/client/html/user_summary.tpl @@ -10,9 +10,9 @@ @@ -20,8 +20,8 @@ <% } %> diff --git a/client/html/users_header.tpl b/client/html/users_header.tpl index 6cefe556..7faab8ea 100644 --- a/client/html/users_header.tpl +++ b/client/html/users_header.tpl @@ -8,7 +8,7 @@ diff --git a/client/html/users_page.tpl b/client/html/users_page.tpl index 61b07f84..6cb04d3a 100644 --- a/client/html/users_page.tpl +++ b/client/html/users_page.tpl @@ -4,7 +4,7 @@ -->
  • <% if (ctx.canViewUsers) { %> - + '> <% } %> <%= ctx.makeThumbnail(user.avatarUrl) %> <% if (ctx.canViewUsers) { %> @@ -12,7 +12,7 @@ <% } %>
    <% if (ctx.canViewUsers) { %> - + '> <% } %> <%- user.name %> <% if (ctx.canViewUsers) { %> diff --git a/client/js/api.js b/client/js/api.js index 0b0ed541..abfeb6f0 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -5,6 +5,7 @@ const request = require('superagent'); const config = require('./config.js'); const events = require('./events.js'); const progress = require('./util/progress.js'); +const uri = require('./util/uri.js'); let fileTokens = {}; diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js index 42e6c0a2..2838e531 100644 --- a/client/js/controllers/auth_controller.js +++ b/client/js/controllers/auth_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const uri = require('../util/uri.js'); const topNavigation = require('../models/top_navigation.js'); const LoginView = require('../views/login_view.js'); @@ -21,7 +22,7 @@ class LoginController { api.forget(); api.login(e.detail.name, e.detail.password, e.detail.remember) .then(() => { - const ctx = router.show('/'); + const ctx = router.show(uri.formatClientLink()); ctx.controller.showSuccess('Logged in'); }, error => { this._loginView.showError(error.message); @@ -34,16 +35,16 @@ class LogoutController { constructor() { api.forget(); api.logout(); - const ctx = router.show('/'); + const ctx = router.show(uri.formatClientLink()); ctx.controller.showSuccess('Logged out'); } } module.exports = router => { - router.enter('/login', (ctx, next) => { + router.enter(['login'], (ctx, next) => { ctx.controller = new LoginController(); }); - router.enter('/logout', (ctx, next) => { + router.enter(['logout'], (ctx, next) => { ctx.controller = new LogoutController(); }); }; diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js index a7cb76ea..ce886323 100644 --- a/client/js/controllers/comments_controller.js +++ b/client/js/controllers/comments_controller.js @@ -1,7 +1,7 @@ 'use strict'; const api = require('../api.js'); -const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const PostList = require('../models/post_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); @@ -28,7 +28,7 @@ class CommentsController { getClientUrlForPage: page => { const parameters = Object.assign( {}, ctx.parameters, {page: page}); - return '/comments/' + misc.formatUrlParameters(parameters); + return uri.formatClientLink('comments', parameters); }, requestPage: page => { return PostList.search( @@ -69,7 +69,6 @@ class CommentsController { }; module.exports = router => { - router.enter('/comments/:parameters?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + router.enter(['comments'], (ctx, next) => { new CommentsController(ctx); }); }; diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js index 2010176d..8e65346b 100644 --- a/client/js/controllers/help_controller.js +++ b/client/js/controllers/help_controller.js @@ -12,13 +12,13 @@ class HelpController { } module.exports = router => { - router.enter('/help', (ctx, next) => { + router.enter(['help'], (ctx, next) => { new HelpController(); }); - router.enter('/help/:section', (ctx, next) => { + router.enter(['help', ':section'], (ctx, next) => { new HelpController(ctx.parameters.section); }); - router.enter('/help/:section/:subsection', (ctx, next) => { + router.enter(['help', ':section', ':subsection'], (ctx, next) => { new HelpController(ctx.parameters.section, ctx.parameters.subsection); }); }; diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js index 8619c1ad..b7590a00 100644 --- a/client/js/controllers/home_controller.js +++ b/client/js/controllers/home_controller.js @@ -44,7 +44,7 @@ class HomeController { }; module.exports = router => { - router.enter('/', (ctx, next) => { + router.enter([], (ctx, next) => { ctx.controller = new HomeController(); }); }; diff --git a/client/js/controllers/not_found_controller.js b/client/js/controllers/not_found_controller.js index 1d54b219..66f52e92 100644 --- a/client/js/controllers/not_found_controller.js +++ b/client/js/controllers/not_found_controller.js @@ -12,7 +12,7 @@ class NotFoundController { }; module.exports = router => { - router.enter('*', (ctx, next) => { + router.enter(null, (ctx, next) => { ctx.controller = new NotFoundController(ctx.canonicalPath); }); }; diff --git a/client/js/controllers/password_reset_controller.js b/client/js/controllers/password_reset_controller.js index 3bd77fd5..e0a9801f 100644 --- a/client/js/controllers/password_reset_controller.js +++ b/client/js/controllers/password_reset_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const uri = require('../util/uri.js'); const topNavigation = require('../models/top_navigation.js'); const PasswordResetView = require('../views/password_reset_view.js'); @@ -20,7 +21,7 @@ class PasswordResetController { this._passwordResetView.disableForm(); api.forget(); api.logout(); - api.get('/password-reset/' + e.detail.userNameOrEmail) + api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail)) .then(() => { this._passwordResetView.showSuccess( 'E-mail has been sent. To finish the procedure, ' + @@ -37,26 +38,26 @@ class PasswordResetFinishController { api.forget(); api.logout(); let password = null; - api.post('/password-reset/' + name, {token: token}) + api.post(uri.formatApiLink('password-reset', name), {token: token}) .then(response => { password = response.password; return api.login(name, password, false); }).then(() => { - const ctx = router.show('/'); + const ctx = router.show(uri.formatClientLink()); ctx.controller.showSuccess('New password: ' + password); }, error => { - const ctx = router.show('/'); + const ctx = router.show(uri.formatClientLink()); ctx.controller.showError(error.message); }); } } module.exports = router => { - router.enter('/password-reset', (ctx, next) => { + router.enter(['password-reset'], (ctx, next) => { ctx.controller = new PasswordResetController(); }); - router.enter(/\/password-reset\/([^:]+):([^:]+)$/, (ctx, next) => { - ctx.controller = new PasswordResetFinishController( - ctx.parameters[0], ctx.parameters[1]); + router.enter(['password-reset', ':descriptor'], (ctx, next) => { + const [name, token] = ctx.parameters.descriptor.split(':', 2); + ctx.controller = new PasswordResetFinishController(name, token); }); }; diff --git a/client/js/controllers/post_detail_controller.js b/client/js/controllers/post_detail_controller.js index 3930b5e2..47d5430c 100644 --- a/client/js/controllers/post_detail_controller.js +++ b/client/js/controllers/post_detail_controller.js @@ -3,6 +3,7 @@ const router = require('../router.js'); const api = require('../api.js'); const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const settings = require('../models/settings.js'); const Post = require('../models/post.js'); const PostList = require('../models/post_list.js'); @@ -55,7 +56,8 @@ class PostDetailController extends BasePostController { misc.disableExitConfirmation(); if (this._id !== e.detail.post.id) { router.replace( - '/post/' + e.detail.post.id + '/' + section, null, false); + uri.formatClientLink('post', e.detail.post.id, section), + null, false); } } @@ -67,7 +69,9 @@ class PostDetailController extends BasePostController { this._installView(e.detail.post, 'merge'); this._view.showSuccess('Post merged.'); router.replace( - '/post/' + e.detail.targetPost.id + '/merge', null, false); + uri.formatClientLink( + 'post', e.detail.targetPost.id, 'merge'), + null, false); }, error => { this._view.showError(error.message); this._view.enableForm(); @@ -77,7 +81,7 @@ class PostDetailController extends BasePostController { module.exports = router => { router.enter( - '/post/:id/merge', + ['post', ':id', 'merge'], (ctx, next) => { ctx.controller = new PostDetailController(ctx, 'merge'); }); diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js index f7928b07..1e6dd7c8 100644 --- a/client/js/controllers/post_list_controller.js +++ b/client/js/controllers/post_list_controller.js @@ -2,7 +2,7 @@ const api = require('../api.js'); const settings = require('../models/settings.js'); -const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const PostList = require('../models/post_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); @@ -52,7 +52,7 @@ class PostListController { history.pushState( null, window.title, - '/posts/' + misc.formatUrlParameters(e.detail.parameters)); + uri.formatClientLink('posts', e.detail.parameters)); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } @@ -89,7 +89,7 @@ class PostListController { this._pageController.run({ parameters: this._ctx.parameters, getClientUrlForPage: page => { - return '/posts/' + misc.formatUrlParameters( + return uri.formatClientLink('posts', Object.assign({}, this._ctx.parameters, {page: page})); }, requestPage: page => { @@ -114,7 +114,6 @@ class PostListController { module.exports = router => { router.enter( - '/posts/:parameters(.*)?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + ['posts'], (ctx, next) => { ctx.controller = new PostListController(ctx); }); }; diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index 920ce758..ba5f6886 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const uri = require('../util/uri.js'); const misc = require('../util/misc.js'); const settings = require('../models/settings.js'); const Comment = require('../models/comment.js'); @@ -29,8 +30,8 @@ class PostMainController extends BasePostController { if (parameters.query) { ctx.state.parameters = parameters; const url = editMode ? - '/post/' + ctx.parameters.id + '/edit' : - '/post/' + ctx.parameters.id; + uri.formatClientLink('post', ctx.parameters.id, 'edit') : + uri.formatClientLink('post', ctx.parameters.id); router.replace(url, ctx.state, false); } @@ -124,7 +125,7 @@ class PostMainController extends BasePostController { } _evtMergePost(e) { - router.show('/post/' + e.detail.post.id + '/merge'); + router.show(uri.formatClientLink('post', e.detail.post.id, 'merge')); } _evtDeletePost(e) { @@ -133,7 +134,7 @@ class PostMainController extends BasePostController { e.detail.post.delete() .then(() => { misc.disableExitConfirmation(); - const ctx = router.show('/posts'); + const ctx = router.show(uri.formatClientLink('posts')); ctx.controller.showSuccess('Post deleted.'); }, error => { this._view.sidebarControl.showError(error.message); @@ -244,8 +245,7 @@ class PostMainController extends BasePostController { } module.exports = router => { - router.enter('/post/:id/edit/:parameters(.*)?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + router.enter(['post', ':id', 'edit'], (ctx, next) => { // restore parameters from history state if (ctx.state.parameters) { @@ -254,8 +254,7 @@ module.exports = router => { ctx.controller = new PostMainController(ctx, true); }); router.enter( - '/post/:id/:parameters(.*)?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + ['post', ':id'], (ctx, next) => { // restore parameters from history state if (ctx.state.parameters) { diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js index 45023413..977ac2be 100644 --- a/client/js/controllers/post_upload_controller.js +++ b/client/js/controllers/post_upload_controller.js @@ -2,6 +2,7 @@ const api = require('../api.js'); const router = require('../router.js'); +const uri = require('../util/uri.js'); const misc = require('../util/misc.js'); const progress = require('../util/progress.js'); const topNavigation = require('../models/top_navigation.js'); @@ -61,7 +62,7 @@ class PostUploadController { .then(() => { this._view.clearMessages(); misc.disableExitConfirmation(); - const ctx = router.show('/posts'); + const ctx = router.show(uri.formatClientLink('posts')); ctx.controller.showSuccess('Posts uploaded.'); }, error => { if (error.uploadable) { @@ -149,7 +150,7 @@ class PostUploadController { } module.exports = router => { - router.enter('/upload', (ctx, next) => { + router.enter(['upload'], (ctx, next) => { ctx.controller = new PostUploadController(); }); }; diff --git a/client/js/controllers/settings_controller.js b/client/js/controllers/settings_controller.js index 2b087ebf..224b2059 100644 --- a/client/js/controllers/settings_controller.js +++ b/client/js/controllers/settings_controller.js @@ -22,7 +22,7 @@ class SettingsController { }; module.exports = router => { - router.enter('/settings', (ctx, next) => { + router.enter(['settings'], (ctx, next) => { ctx.controller = new SettingsController(); }); }; diff --git a/client/js/controllers/snapshots_controller.js b/client/js/controllers/snapshots_controller.js index 3aa3f89e..034bf498 100644 --- a/client/js/controllers/snapshots_controller.js +++ b/client/js/controllers/snapshots_controller.js @@ -1,7 +1,7 @@ 'use strict'; const api = require('../api.js'); -const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const SnapshotList = require('../models/snapshot_list.js'); const PageController = require('../controllers/page_controller.js'); const topNavigation = require('../models/top_navigation.js'); @@ -25,7 +25,7 @@ class SnapshotsController { getClientUrlForPage: page => { const parameters = Object.assign( {}, ctx.parameters, {page: page}); - return '/history/' + misc.formatUrlParameters(parameters); + return uri.formatClientLink('history', parameters); }, requestPage: page => { return SnapshotList.search('', page, 25); @@ -43,7 +43,6 @@ class SnapshotsController { } module.exports = router => { - router.enter('/history/:parameters?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + router.enter(['history'], (ctx, next) => { ctx.controller = new SnapshotsController(ctx); }); }; diff --git a/client/js/controllers/tag_categories_controller.js b/client/js/controllers/tag_categories_controller.js index 0dd7a77b..dadf8e4a 100644 --- a/client/js/controllers/tag_categories_controller.js +++ b/client/js/controllers/tag_categories_controller.js @@ -51,7 +51,7 @@ class TagCategoriesController { } module.exports = router => { - router.enter('/tag-categories', (ctx, next) => { + router.enter(['tag-categories'], (ctx, next) => { ctx.controller = new TagCategoriesController(ctx, next); }); }; diff --git a/client/js/controllers/tag_controller.js b/client/js/controllers/tag_controller.js index 939b8f04..c1de3a36 100644 --- a/client/js/controllers/tag_controller.js +++ b/client/js/controllers/tag_controller.js @@ -3,6 +3,7 @@ const router = require('../router.js'); const api = require('../api.js'); const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const tags = require('../tags.js'); const Tag = require('../models/tag.js'); const topNavigation = require('../models/top_navigation.js'); @@ -61,7 +62,8 @@ class TagController { misc.disableExitConfirmation(); if (this._name !== e.detail.tag.names[0]) { router.replace( - '/tag/' + e.detail.tag.names[0] + '/' + section, null, false); + uri.formatClientLink('tag', e.detail.tag.names[0], section), + null, false); } } @@ -99,7 +101,8 @@ class TagController { this._view.showSuccess('Tag merged.'); this._view.enableForm(); router.replace( - '/tag/' + e.detail.targetTagName + '/merge', null, false); + uri.formatClientLink('tag', e.detail.targetTagName, 'merge'), + null, false); }, error => { this._view.showError(error.message); this._view.enableForm(); @@ -111,7 +114,7 @@ class TagController { this._view.disableForm(); e.detail.tag.delete() .then(() => { - const ctx = router.show('/tags/'); + const ctx = router.show(uri.formatClientLink('tags')); ctx.controller.showSuccess('Tag deleted.'); }, error => { this._view.showError(error.message); @@ -121,16 +124,16 @@ class TagController { } module.exports = router => { - router.enter('/tag/:name(.+?)/edit', (ctx, next) => { + router.enter(['tag', ':name', 'edit'], (ctx, next) => { ctx.controller = new TagController(ctx, 'edit'); }); - router.enter('/tag/:name(.+?)/merge', (ctx, next) => { + router.enter(['tag', ':name', 'merge'], (ctx, next) => { ctx.controller = new TagController(ctx, 'merge'); }); - router.enter('/tag/:name(.+?)/delete', (ctx, next) => { + router.enter(['tag', ':name', 'delete'], (ctx, next) => { ctx.controller = new TagController(ctx, 'delete'); }); - router.enter('/tag/:name(.+)', (ctx, next) => { + router.enter(['tag', ':name'], (ctx, next) => { ctx.controller = new TagController(ctx, 'summary'); }); }; diff --git a/client/js/controllers/tag_list_controller.js b/client/js/controllers/tag_list_controller.js index d61d8f71..2b485d2e 100644 --- a/client/js/controllers/tag_list_controller.js +++ b/client/js/controllers/tag_list_controller.js @@ -1,7 +1,7 @@ 'use strict'; const api = require('../api.js'); -const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const TagList = require('../models/tag_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); @@ -49,7 +49,7 @@ class TagListController { history.pushState( null, window.title, - '/tags/' + misc.formatUrlParameters(e.detail.parameters)); + uri.formatClientLink('tags', e.detail.parameters)); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } @@ -60,7 +60,7 @@ class TagListController { getClientUrlForPage: page => { const parameters = Object.assign( {}, this._ctx.parameters, {page: page}); - return '/tags/' + misc.formatUrlParameters(parameters); + return uri.formatClientLink('tags', parameters); }, requestPage: page => { return TagList.search( @@ -75,7 +75,6 @@ class TagListController { module.exports = router => { router.enter( - '/tags/:parameters(.*)?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + ['tags'], (ctx, next) => { ctx.controller = new TagListController(ctx); }); }; diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js index e9c1fb39..46020f37 100644 --- a/client/js/controllers/user_controller.js +++ b/client/js/controllers/user_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const uri = require('../util/uri.js'); const misc = require('../util/misc.js'); const config = require('../config.js'); const views = require('../util/views.js'); @@ -77,7 +78,8 @@ class UserController { misc.disableExitConfirmation(); if (this._name !== e.detail.user.name) { router.replace( - '/user/' + e.detail.user.name + '/' + section, null, false); + uri.formatClientLink('user', e.detail.user.name, section), + null, false); } } @@ -135,10 +137,10 @@ class UserController { api.logout(); } if (api.hasPrivilege('users:list')) { - const ctx = router.show('/users'); + const ctx = router.show(uri.formatClientLink('users')); ctx.controller.showSuccess('Account deleted.'); } else { - const ctx = router.show('/'); + const ctx = router.show(uri.formatClientLink()); ctx.controller.showSuccess('Account deleted.'); } }, error => { @@ -149,13 +151,13 @@ class UserController { } module.exports = router => { - router.enter('/user/:name', (ctx, next) => { + router.enter(['user', ':name'], (ctx, next) => { ctx.controller = new UserController(ctx, 'summary'); }); - router.enter('/user/:name/edit', (ctx, next) => { + router.enter(['user', ':name', 'edit'], (ctx, next) => { ctx.controller = new UserController(ctx, 'edit'); }); - router.enter('/user/:name/delete', (ctx, next) => { + router.enter(['user', ':name', 'delete'], (ctx, next) => { ctx.controller = new UserController(ctx, 'delete'); }); }; diff --git a/client/js/controllers/user_list_controller.js b/client/js/controllers/user_list_controller.js index 98e70031..13aec009 100644 --- a/client/js/controllers/user_list_controller.js +++ b/client/js/controllers/user_list_controller.js @@ -1,7 +1,7 @@ 'use strict'; const api = require('../api.js'); -const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const UserList = require('../models/user_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); @@ -41,7 +41,7 @@ class UserListController { history.pushState( null, window.title, - '/users/' + misc.formatUrlParameters(e.detail.parameters)); + uri.formatClientLink('users', e.detail.parameters)); Object.assign(this._ctx.parameters, e.detail.parameters); this._syncPageController(); } @@ -52,7 +52,7 @@ class UserListController { getClientUrlForPage: page => { const parameters = Object.assign( {}, this._ctx.parameters, {page: page}); - return '/users/' + misc.formatUrlParameters(parameters); + return uri.formatClientLink('users', parameters); }, requestPage: page => { return UserList.search(this._ctx.parameters.query, page); @@ -69,7 +69,6 @@ class UserListController { module.exports = router => { router.enter( - '/users/:parameters(.*)?', - (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); }, + ['users'], (ctx, next) => { ctx.controller = new UserListController(ctx); }); }; diff --git a/client/js/controllers/user_registration_controller.js b/client/js/controllers/user_registration_controller.js index 47165d60..7d822380 100644 --- a/client/js/controllers/user_registration_controller.js +++ b/client/js/controllers/user_registration_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const uri = require('../util/uri.js'); const User = require('../models/user.js'); const topNavigation = require('../models/top_navigation.js'); const RegistrationView = require('../views/registration_view.js'); @@ -32,7 +33,7 @@ class UserRegistrationController { api.forget(); return api.login(e.detail.name, e.detail.password, false); }).then(() => { - const ctx = router.show('/'); + const ctx = router.show(uri.formatClientLink()); ctx.controller.showSuccess('Welcome aboard!'); }, error => { this._view.showError(error.message); @@ -42,7 +43,7 @@ class UserRegistrationController { } module.exports = router => { - router.enter('/register', (ctx, next) => { + router.enter(['register'], (ctx, next) => { new UserRegistrationController(); }); }; diff --git a/client/js/controls/tag_input_control.js b/client/js/controls/tag_input_control.js index 2912b0fc..ee864281 100644 --- a/client/js/controls/tag_input_control.js +++ b/client/js/controls/tag_input_control.js @@ -3,6 +3,7 @@ const api = require('../api.js'); const tags = require('../tags.js'); const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const settings = require('../models/settings.js'); const events = require('../events.js'); const views = require('../util/views.js'); @@ -308,7 +309,7 @@ class TagInputControl extends events.EventTarget { tagLinkNode.classList.add(className); } tagLinkNode.setAttribute( - 'href', '/tag/' + encodeURIComponent(tagName)); + 'href', uri.formatClientLink('tag', tagName)); const tagIconNode = document.createElement('i'); tagIconNode.classList.add('fa'); tagIconNode.classList.add('fa-tag'); @@ -319,7 +320,7 @@ class TagInputControl extends events.EventTarget { searchLinkNode.classList.add(className); } searchLinkNode.setAttribute( - 'href', '/posts/query=' + encodeURIComponent(tagName)); + 'href', uri.formatClientLink('posts', {query: tagName})); searchLinkNode.textContent = tagName + ' '; searchLinkNode.addEventListener('click', e => { e.preventDefault(); @@ -360,7 +361,9 @@ class TagInputControl extends events.EventTarget { if (!browsingSettings.tagSuggestions) { return; } - api.get('/tag-siblings/' + tag.names[0], {noProgress: true}) + api.get( + uri.formatApiLink('tag-siblings', tag.names[0]), + {noProgress: true}) .then(response => { return Promise.resolve(response.results); }, response => { diff --git a/client/js/main.js b/client/js/main.js index 27c14e88..2308ee90 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -8,7 +8,7 @@ const router = require('./router.js'); history.scrollRestoration = 'manual'; router.exit( - /.*/, + null, (ctx, next) => { ctx.state.scrollX = window.scrollX; ctx.state.scrollY = window.scrollY; @@ -20,7 +20,7 @@ router.exit( const mousetrap = require('mousetrap'); router.enter( - /.*/, + null, (ctx, next) => { mousetrap.reset(); next(); diff --git a/client/js/models/comment.js b/client/js/models/comment.js index 623fd7e3..e10e83cb 100644 --- a/client/js/models/comment.js +++ b/client/js/models/comment.js @@ -1,6 +1,7 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const events = require('../events.js'); class Comment extends events.EventTarget { @@ -38,9 +39,9 @@ class Comment extends events.EventTarget { text: this._text, }; let promise = this._id ? - api.put('/comment/' + this._id, detail) : - api.post( - '/comments', Object.assign({postId: this._postId}, detail)); + api.put(uri.formatApiLink('comment', this.id), detail) : + api.post(uri.formatApiLink('comments'), + Object.assign({postId: this._postId}, detail)); return promise.then(response => { this._updateFromResponse(response); @@ -55,7 +56,7 @@ class Comment extends events.EventTarget { delete() { return api.delete( - '/comment/' + this._id, + uri.formatApiLink('comment', this.id), {version: this._version}) .then(response => { this.dispatchEvent(new CustomEvent('delete', { @@ -68,7 +69,9 @@ class Comment extends events.EventTarget { } setScore(score) { - return api.put('/comment/' + this._id + '/score', {score: score}) + return api.put( + uri.formatApiLink('comment', this.id, 'score'), + {score: score}) .then(response => { this._updateFromResponse(response); this.dispatchEvent(new CustomEvent('changeScore', { diff --git a/client/js/models/info.js b/client/js/models/info.js index 3b196dd4..35ba867a 100644 --- a/client/js/models/info.js +++ b/client/js/models/info.js @@ -1,11 +1,12 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const Post = require('./post.js'); class Info { static get() { - return api.get('/info') + return api.get(uri.formatApiLink('info')) .then(response => { return Promise.resolve(Object.assign( {}, diff --git a/client/js/models/post.js b/client/js/models/post.js index 8b767a2e..71f3eb98 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -1,6 +1,7 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const tags = require('../tags.js'); const events = require('../events.js'); const NoteList = require('./note_list.js'); @@ -69,7 +70,9 @@ class Post extends events.EventTarget { static reverseSearch(content) { let apiPromise = api.post( - '/posts/reverse-search', {}, {content: content}); + uri.formatApiLink('posts', 'reverse-search'), + {}, + {content: content}); let returnedPromise = apiPromise .then(response => { if (response.exactPost) { @@ -85,7 +88,7 @@ class Post extends events.EventTarget { } static get(id) { - return api.get('/post/' + id) + return api.get(uri.formatApiLink('post', id)) .then(response => { return Promise.resolve(Post.fromResponse(response)); }); @@ -149,8 +152,8 @@ class Post extends events.EventTarget { } let apiPromise = this._id ? - api.put('/post/' + this._id, detail, files) : - api.post('/posts', detail, files); + api.put(uri.formatApiLink('post', this.id), detail, files) : + api.post(uri.formatApiLink('posts'), detail, files); return apiPromise.then(response => { this._updateFromResponse(response); @@ -176,14 +179,18 @@ class Post extends events.EventTarget { } feature() { - return api.post('/featured-post', {id: this._id}) + return api.post( + uri.formatApiLink('featured-post'), + {id: this._id}) .then(response => { return Promise.resolve(); }); } delete() { - return api.delete('/post/' + this._id, {version: this._version}) + return api.delete( + uri.formatApiLink('post', this.id), + {version: this._version}) .then(response => { this.dispatchEvent(new CustomEvent('delete', { detail: { @@ -195,9 +202,9 @@ class Post extends events.EventTarget { } merge(targetId, useOldContent) { - return api.get('/post/' + encodeURIComponent(targetId)) + return api.get(uri.formatApiLink('post', targetId)) .then(response => { - return api.post('/post-merge/', { + return api.post(uri.formatApiLink('post-merge'), { removeVersion: this._version, remove: this._id, mergeToVersion: response.version, @@ -216,7 +223,9 @@ class Post extends events.EventTarget { } setScore(score) { - return api.put('/post/' + this._id + '/score', {score: score}) + return api.put( + uri.formatApiLink('post', this.id, 'score'), + {score: score}) .then(response => { const prevFavorite = this._ownFavorite; this._updateFromResponse(response); @@ -237,7 +246,7 @@ class Post extends events.EventTarget { } addToFavorites() { - return api.post('/post/' + this.id + '/favorite') + return api.post(uri.formatApiLink('post', this.id, 'favorite')) .then(response => { const prevScore = this._ownScore; this._updateFromResponse(response); @@ -258,7 +267,7 @@ class Post extends events.EventTarget { } removeFromFavorites() { - return api.delete('/post/' + this.id + '/favorite') + return api.delete(uri.formatApiLink('post', this.id, 'favorite')) .then(response => { const prevScore = this._ownScore; this._updateFromResponse(response); diff --git a/client/js/models/post_list.js b/client/js/models/post_list.js index 312afc86..2af40f58 100644 --- a/client/js/models/post_list.js +++ b/client/js/models/post_list.js @@ -1,29 +1,32 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const AbstractList = require('./abstract_list.js'); const Post = require('./post.js'); class PostList extends AbstractList { static getAround(id, searchQuery) { - const url = - `/post/${id}/around?fields=id` + - `&query=${encodeURIComponent(searchQuery)}`; - return api.get(url); + return api.get( + uri.formatApiLink( + 'post', id, 'around', {query: searchQuery, fields: 'id'})); } static search(text, page, pageSize, fields) { - const url = - `/posts/?query=${encodeURIComponent(text)}` + - `&page=${page}` + - `&pageSize=${pageSize}` + - `&fields=${fields.join(',')}`; - return api.get(url).then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: PostList.fromResponse(response.results)})); - }); + return api.get( + uri.formatApiLink( + 'posts', { + query: text, + page: page, + pageSize: pageSize, + fields: fields.join(','), + })) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: PostList.fromResponse(response.results)})); + }); } } diff --git a/client/js/models/snapshot_list.js b/client/js/models/snapshot_list.js index 22348f4a..a23850a3 100644 --- a/client/js/models/snapshot_list.js +++ b/client/js/models/snapshot_list.js @@ -1,21 +1,20 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const AbstractList = require('./abstract_list.js'); const Snapshot = require('./snapshot.js'); class SnapshotList extends AbstractList { static search(text, page, pageSize) { - const url = - `/snapshots/?query=${encodeURIComponent(text)}` + - `&page=${page}` + - `&pageSize=${pageSize}`; - return api.get(url).then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: SnapshotList.fromResponse(response.results)})); - }); + return api.get(uri.formatApiLink( + 'snapshots', {query: text, page: page, pageSize: pageSize})) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: SnapshotList.fromResponse(response.results)})); + }); } } diff --git a/client/js/models/tag.js b/client/js/models/tag.js index 07bbf7ff..1a156936 100644 --- a/client/js/models/tag.js +++ b/client/js/models/tag.js @@ -1,6 +1,7 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const events = require('../events.js'); const misc = require('../util/misc.js'); @@ -33,7 +34,7 @@ class Tag extends events.EventTarget { } static get(name) { - return api.get('/tag/' + encodeURIComponent(name)) + return api.get(uri.formatApiLink('tag', name)) .then(response => { return Promise.resolve(Tag.fromResponse(response)); }); @@ -60,8 +61,8 @@ class Tag extends events.EventTarget { } let promise = this._origName ? - api.put('/tag/' + encodeURIComponent(this._origName), detail) : - api.post('/tags', detail); + api.put(uri.formatApiLink('tag', this._origName), detail) : + api.post(uri.formatApiLink('tags'), detail); return promise .then(response => { this._updateFromResponse(response); @@ -75,9 +76,9 @@ class Tag extends events.EventTarget { } merge(targetName) { - return api.get('/tag/' + encodeURIComponent(targetName)) + return api.get(uri.formatApiLink('tag', targetName)) .then(response => { - return api.post('/tag-merge/', { + return api.post(uri.formatApiLink('tag-merge'), { removeVersion: this._version, remove: this._origName, mergeToVersion: response.version, @@ -96,7 +97,7 @@ class Tag extends events.EventTarget { delete() { return api.delete( - '/tag/' + encodeURIComponent(this._origName), + uri.formatApiLink('tag', this._origName), {version: this._version}) .then(response => { this.dispatchEvent(new CustomEvent('delete', { diff --git a/client/js/models/tag_category.js b/client/js/models/tag_category.js index cc08d345..04bd8fe6 100644 --- a/client/js/models/tag_category.js +++ b/client/js/models/tag_category.js @@ -1,6 +1,7 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const events = require('../events.js'); class TagCategory extends events.EventTarget { @@ -45,9 +46,9 @@ class TagCategory extends events.EventTarget { let promise = this._origName ? api.put( - '/tag-category/' + encodeURIComponent(this._origName), + uri.formatApiLink('tag-category', this._origName), detail) : - api.post('/tag-categories', detail); + api.post(uri.formatApiLink('tag-categories'), detail); return promise .then(response => { @@ -63,7 +64,7 @@ class TagCategory extends events.EventTarget { delete() { return api.delete( - '/tag-category/' + encodeURIComponent(this._origName), + uri.formatApiLink('tag-category', this._origName), {version: this._version}) .then(response => { this.dispatchEvent(new CustomEvent('delete', { diff --git a/client/js/models/tag_category_list.js b/client/js/models/tag_category_list.js index 812d6959..6c1182fb 100644 --- a/client/js/models/tag_category_list.js +++ b/client/js/models/tag_category_list.js @@ -1,6 +1,7 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const AbstractList = require('./abstract_list.js'); const TagCategory = require('./tag_category.js'); @@ -26,12 +27,13 @@ class TagCategoryList extends AbstractList { } static get() { - return api.get('/tag-categories/').then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: TagCategoryList.fromResponse(response.results)})); - }); + return api.get(uri.formatApiLink('tag-categories')) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: TagCategoryList.fromResponse(response.results)})); + }); } get defaultCategory() { @@ -54,7 +56,10 @@ class TagCategoryList extends AbstractList { if (this._defaultCategory !== this._origDefaultCategory) { promises.push( api.put( - `/tag-category/${this._defaultCategory.name}/default`)); + uri.formatApiLink( + 'tag-category', + this._defaultCategory.name, + 'default'))); } return Promise.all(promises) diff --git a/client/js/models/tag_list.js b/client/js/models/tag_list.js index 268bcd80..d480745c 100644 --- a/client/js/models/tag_list.js +++ b/client/js/models/tag_list.js @@ -1,22 +1,26 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const AbstractList = require('./abstract_list.js'); const Tag = require('./tag.js'); class TagList extends AbstractList { static search(text, page, pageSize, fields) { - const url = - `/tags/?query=${encodeURIComponent(text)}` + - `&page=${page}` + - `&pageSize=${pageSize}` + - `&fields=${fields.join(',')}`; - return api.get(url).then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: TagList.fromResponse(response.results)})); - }); + return api.get( + uri.formatApiLink( + 'tags', { + query: text, + page: page, + pageSize: pageSize, + fields: fields.join(','), + })) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: TagList.fromResponse(response.results)})); + }); } } diff --git a/client/js/models/user.js b/client/js/models/user.js index ab19bef3..abb5f57f 100644 --- a/client/js/models/user.js +++ b/client/js/models/user.js @@ -1,6 +1,7 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const events = require('../events.js'); class User extends events.EventTarget { @@ -40,7 +41,7 @@ class User extends events.EventTarget { } static get(name) { - return api.get('/user/' + encodeURIComponent(name)) + return api.get(uri.formatApiLink('user', name)) .then(response => { return Promise.resolve(User.fromResponse(response)); }); @@ -73,10 +74,8 @@ class User extends events.EventTarget { let promise = this._orig._name ? api.put( - '/user/' + encodeURIComponent(this._orig._name), - detail, - files) : - api.post('/users', detail, files); + uri.formatApiLink('user', this._orig._name), detail, files) : + api.post(uri.formatApiLink('users'), detail, files); return promise .then(response => { @@ -92,7 +91,7 @@ class User extends events.EventTarget { delete() { return api.delete( - '/user/' + encodeURIComponent(this._orig._name), + uri.formatApiLink('user', this._orig._name), {version: this._version}) .then(response => { this.dispatchEvent(new CustomEvent('delete', { diff --git a/client/js/models/user_list.js b/client/js/models/user_list.js index 4ca430d5..ff3e27cd 100644 --- a/client/js/models/user_list.js +++ b/client/js/models/user_list.js @@ -1,20 +1,21 @@ 'use strict'; const api = require('../api.js'); +const uri = require('../util/uri.js'); const AbstractList = require('./abstract_list.js'); const User = require('./user.js'); class UserList extends AbstractList { static search(text, page) { - const url = - `/users/?query=${encodeURIComponent(text)}` + - `&page=${page}&pageSize=30`; - return api.get(url).then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: UserList.fromResponse(response.results)})); - }); + return api.get( + uri.formatApiLink( + 'users', {query: text, page: page, pageSize: 30})) + .then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: UserList.fromResponse(response.results)})); + }); } } diff --git a/client/js/router.js b/client/js/router.js index 570fb3ef..9bff064f 100644 --- a/client/js/router.js +++ b/client/js/router.js @@ -1,6 +1,7 @@ 'use strict'; // modified page.js by visionmedia +// - changed regexes to components // - removed unused crap // - refactored to classes // - simplified method chains @@ -9,19 +10,12 @@ // - rename .save() to .replaceState() // - offer .url -const pathToRegexp = require('path-to-regexp'); const clickEvent = document.ontouchstart ? 'touchstart' : 'click'; +const uri = require('./util/uri.js'); let location = window.history.location || window.location; const base = ''; -function _decodeURLEncodedURIComponent(val) { - if (typeof val !== 'string') { - return val; - } - return decodeURIComponent(val.replace(/\+/g, ' ')); -} - function _isSameOrigin(href) { let origin = location.protocol + '//' + location.hostname; if (location.port) { @@ -55,11 +49,28 @@ class Context { }; class Route { - constructor(path, options) { - options = options || {}; - this.path = (path === '*') ? '(.*)' : path; + constructor(path) { this.method = 'GET'; - this.regexp = pathToRegexp(this.path, this.keys = [], options); + this.path = path; + + this.parameterNames = []; + if (this.path === null) { + this.regex = /.*/; + } else { + let parts = []; + for (let component of this.path) { + if (component[0] === ':') { + parts.push('([^/]+)'); + this.parameterNames.push(component.substr(1)); + } else { // assert [a-z]+ + parts.push(component); + } + } + let regexString = '^/' + parts.join('/'); + regexString += '(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$'; + this.parameterNames.push('variable'); + this.regex = new RegExp(regexString); + } } middleware(fn) { @@ -72,24 +83,39 @@ class Route { } match(path, parameters) { - const keys = this.keys; const qsIndex = path.indexOf('?'); const pathname = ~qsIndex ? path.slice(0, qsIndex) : path; - const m = this.regexp.exec(pathname); + const match = this.regex.exec(pathname); - if (!m) { + if (!match) { return false; } - for (let i = 1, len = m.length; i < len; ++i) { - const key = keys[i - 1]; - const val = _decodeURLEncodedURIComponent(m[i]); - if (val !== undefined || - !(hasOwnProperty.call(parameters, key.name))) { - parameters[key.name] = val; + try { + for (let i = 1; i < match.length; i++) { + const name = this.parameterNames[i - 1]; + const value = match[i]; + if (value === undefined) { + continue; + } + + if (name === 'variable') { + for (let word of (value || '').split(/;/)) { + const [key, subvalue] = word.split(/=/, 2); + parameters[key] = uri.unescapeParam(subvalue); + } + } else { + parameters[name] = uri.unescapeParam(value); + } } + } catch (e) { + return false; } + // XXX: it is very unfitting place for this + parameters.query = parameters.query || ''; + parameters.page = parseInt(parameters.page || '1'); + return true; } }; diff --git a/client/js/util/misc.js b/client/js/util/misc.js index 3037b103..d825d996 100644 --- a/client/js/util/misc.js +++ b/client/js/util/misc.js @@ -1,6 +1,7 @@ 'use strict'; const markdown = require('./markdown.js'); +const uri = require('./uri.js'); function decamelize(str, sep) { sep = sep === undefined ? '-' : sep; @@ -99,44 +100,10 @@ function formatInlineMarkdown(text) { return markdown.formatInlineMarkdown(text); } -function formatUrlParameters(dict) { - let result = []; - for (let key of Object.keys(dict)) { - const value = dict[key]; - if (key === 'parameters') { - continue; - } - if (value) { - result.push(`${key}=${encodeURIComponent(value)}`); - } - } - return result.join(';'); -} - function splitByWhitespace(str) { return str.split(/\s+/).filter(s => s); } -function parseUrlParameters(query) { - let result = {}; - for (let word of (query || '').split(/;/)) { - const [key, value] = word.split(/=/, 2); - result[key] = value; - } - result.query = result.query || ''; - result.page = parseInt(result.page || '1'); - return result; -} - -function parseUrlParametersRoute(ctx, next) { - // ctx.parameters = {"user":...,"action":...} from /users/:user/:action - // ctx.parameters.parameters = value of :parameters as per /url/:parameters - Object.assign( - ctx.parameters, - parseUrlParameters(ctx.parameters.parameters)); - next(); -} - function unindent(callSite, ...args) { function format(str) { let size = -1; @@ -232,9 +199,6 @@ function dataURItoBlob(dataURI) { module.exports = { range: range, - formatUrlParameters: formatUrlParameters, - parseUrlParameters: parseUrlParameters, - parseUrlParametersRoute: parseUrlParametersRoute, formatRelativeTime: formatRelativeTime, formatFileSize: formatFileSize, formatMarkdown: formatMarkdown, diff --git a/client/js/util/uri.js b/client/js/util/uri.js new file mode 100644 index 00000000..52e90a06 --- /dev/null +++ b/client/js/util/uri.js @@ -0,0 +1,62 @@ +'use strict'; + +function formatApiLink(...values) { + let parts = []; + for (let value of values) { + if (value.constructor === Object) { + // assert this is the last piece + let variableParts = []; + for (let key of Object.keys(value)) { + if (value[key]) { + variableParts.push( + key + '=' + encodeURIComponent(value[key].toString())); + } + } + if (variableParts.length) { + parts.push('?' + variableParts.join('&')); + } + break; + } else { + parts.push(encodeURIComponent(value.toString())); + } + } + return '/' + parts.join('/'); +} + +function escapeParam(text) { + return encodeURIComponent(text).replace(/%/g, '$'); +} + +function unescapeParam(text) { + return decodeURIComponent(text.replace(/\$/g, '%')); +} + +function formatClientLink(...values) { + let parts = []; + for (let value of values) { + if (value.constructor === Object) { + // assert this is the last piece + let variableParts = []; + for (let key of Object.keys(value)) { + if (value[key]) { + variableParts.push( + key + '=' + escapeParam(value[key].toString())); + } + } + if (variableParts.length) { + parts.push(variableParts.join(';')); + } + break; + } else { + parts.push(escapeParam(value.toString())); + } + } + return '/' + parts.join('/'); +} + +module.exports = { + formatClientLink: formatClientLink, + formatApiLink: formatApiLink, + escapeParam: escapeParam, + unescapeParam: unescapeParam, +}; diff --git a/client/js/util/views.js b/client/js/util/views.js index fbf00a3b..7f062599 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -6,6 +6,7 @@ const templates = require('../templates.js'); const tags = require('../tags.js'); const domParser = new DOMParser(); const misc = require('./misc.js'); +const uri = require('./uri.js'); function _imbueId(options) { if (!options.id) { @@ -152,19 +153,15 @@ function makeNumericInput(options) { } function getPostUrl(id, parameters) { - let url = '/post/' + encodeURIComponent(id); - if (parameters && parameters.query) { - url += '/query=' + encodeURIComponent(parameters.query); - } - return url; + return uri.formatClientLink( + 'post', id, + parameters ? {query: parameters.query} : {}); } function getPostEditUrl(id, parameters) { - let url = '/post/' + encodeURIComponent(id) + '/edit'; - if (parameters && parameters.query) { - url += '/query=' + encodeURIComponent(parameters.query); - } - return url; + return uri.formatClientLink( + 'post', id, 'edit', + parameters ? {query: parameters.query} : {}); } function makePostLink(id, includeHash) { @@ -175,7 +172,7 @@ function makePostLink(id, includeHash) { return api.hasPrivilege('posts:view') ? makeElement( 'a', - {'href': '/post/' + encodeURIComponent(id)}, + {href: uri.formatClientLink('post', id)}, misc.escapeHtml(text)) : misc.escapeHtml(text); } @@ -191,13 +188,13 @@ function makeTagLink(name, includeHash) { makeElement( 'a', { - 'href': '/tag/' + encodeURIComponent(name), - 'class': misc.makeCssName(category, 'tag'), + href: uri.formatClientLink('tag', name), + class: misc.makeCssName(category, 'tag'), }, misc.escapeHtml(text)) : makeElement( 'span', - {'class': misc.makeCssName(category, 'tag')}, + {class: misc.makeCssName(category, 'tag')}, misc.escapeHtml(text)); } @@ -206,7 +203,7 @@ function makeUserLink(user) { text += user && user.name ? misc.escapeHtml(user.name) : 'Anonymous'; const link = user && api.hasPrivilege('users:view') ? makeElement( - 'a', {'href': '/user/' + encodeURIComponent(user.name)}, text) : + 'a', {href: uri.formatClientLink('user', user.name)}, text) : text; return makeElement('span', {class: 'user'}, link); } @@ -385,6 +382,7 @@ function getTemplate(templatePath) { makeElement: makeElement, makeCssName: misc.makeCssName, makeNumericInput: makeNumericInput, + formatClientLink: uri.formatClientLink }); return htmlToDom(templateFactory(ctx)); }; diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js index af3d2ddc..00ffc7d1 100644 --- a/client/js/views/home_view.js +++ b/client/js/views/home_view.js @@ -1,7 +1,7 @@ 'use strict'; const router = require('../router.js'); -const misc = require('../util/misc.js'); +const uri = require('../util/uri.js'); const views = require('../util/views.js'); const PostContentControl = require('../controls/post_content_control.js'); const PostNotesOverlayControl @@ -88,7 +88,7 @@ class HomeView { _evtFormSubmit(e) { e.preventDefault(); this._searchInputNode.blur(); - router.show('/posts/' + misc.formatUrlParameters({ + router.show(uri.formatClientLink('posts', { query: this._searchInputNode.value})); } } diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js index 94fc70b9..e3767c0d 100644 --- a/client/js/views/manual_page_view.js +++ b/client/js/views/manual_page_view.js @@ -2,7 +2,6 @@ const router = require('../router.js'); const keyboard = require('../util/keyboard.js'); -const misc = require('../util/misc.js'); const views = require('../util/views.js'); const holderTemplate = views.getTemplate('manual-pager'); diff --git a/client/js/views/post_main_view.js b/client/js/views/post_main_view.js index 7c115dbd..452d63f9 100644 --- a/client/js/views/post_main_view.js +++ b/client/js/views/post_main_view.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const views = require('../util/views.js'); +const uri = require('../util/uri.js'); const keyboard = require('../util/keyboard.js'); const PostContentControl = require('../controls/post_content_control.js'); const PostNotesOverlayControl = @@ -61,19 +62,19 @@ class PostMainView { keyboard.bind('e', () => { if (ctx.editMode) { - router.show('/post/' + ctx.post.id); + router.show(uri.formatClientLink('post', ctx.post.id)); } else { - router.show('/post/' + ctx.post.id + '/edit'); + router.show(uri.formatClientLink('post', ctx.post.id, 'edit')); } }); keyboard.bind(['a', 'left'], () => { if (ctx.prevPostId) { - router.show('/post/' + ctx.prevPostId); + router.show(uri.formatClientLink('post', ctx.prevPostId)); } }); keyboard.bind(['d', 'right'], () => { if (ctx.nextPostId) { - router.show('/post/' + ctx.nextPostId); + router.show(uri.formatClientLink('post', ctx.nextPostId)); } }); } diff --git a/client/js/views/users_header_view.js b/client/js/views/users_header_view.js index e4f1c149..08b7620c 100644 --- a/client/js/views/users_header_view.js +++ b/client/js/views/users_header_view.js @@ -1,7 +1,6 @@ 'use strict'; const events = require('../events.js'); -const misc = require('../util/misc.js'); const search = require('../util/search.js'); const views = require('../util/views.js'); diff --git a/client/package.json b/client/package.json index add6de7f..3999e32b 100644 --- a/client/package.json +++ b/client/package.json @@ -22,7 +22,6 @@ "merge": "^1.2.0", "mousetrap": "^1.5.3", "nprogress": "^0.2.0", - "path-to-regexp": "^1.5.1", "stylus": "^0.54.2", "superagent": "^1.8.3", "uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony",