From 95072be97e27945e071e62c9658b1d81520ef204 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 17 May 2018 15:59:49 -0600 Subject: [PATCH 01/11] switched ejs for nunjucks --- app.js | 23 +++- package.json | 1 + .../server/router.js | 2 +- .../views/unsubscribe-notifications.ejs | 64 --------- .../views/unsubscribe-notifications.njk | 61 +++++++++ .../talk-plugin-profile-data/server/router.js | 2 +- .../server/views/download.ejs | 56 -------- .../server/views/download.njk | 56 ++++++++ routes/account/index.js | 4 +- routes/admin/index.js | 2 +- routes/api/v1/graph.js | 2 +- routes/dev/assets.js | 6 +- routes/dev/index.js | 2 +- routes/embed/index.js | 2 +- routes/login/index.js | 2 +- services/passport.js | 6 +- views/account/email/confirm.ejs | 63 --------- views/account/email/confirm.njk | 63 +++++++++ views/account/password/reset.ejs | 85 ------------ views/account/password/reset.njk | 85 ++++++++++++ views/admin.ejs | 17 --- views/admin.njk | 14 ++ views/api/graphiql.ejs | 125 ----------------- views/api/graphiql.njk | 126 ++++++++++++++++++ views/auth-callback.ejs | 10 -- views/auth-callback.njk | 10 ++ views/dev/article.ejs | 52 -------- views/dev/article.njk | 55 ++++++++ views/dev/articles.ejs | 40 ------ views/dev/articles.njk | 37 +++++ views/embed/stream.ejs | 14 -- views/embed/stream.njk | 16 +++ views/login.ejs | 16 --- views/login.njk | 16 +++ views/partials/account.ejs | 4 - views/partials/custom-css.ejs | 1 - views/partials/custom-css.njk | 3 + views/partials/data.ejs | 3 - views/partials/data.njk | 3 + views/partials/dev-nav.ejs | 8 -- views/partials/favicon.njk | 14 ++ views/partials/head.ejs | 23 ---- views/partials/nav.njk | 8 ++ views/templates/account.njk | 6 + views/templates/base.njk | 39 ++++++ .../dev.ejs => templates/development.njk} | 16 ++- yarn.lock | 6 + 47 files changed, 670 insertions(+), 599 deletions(-) delete mode 100644 plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs create mode 100644 plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk delete mode 100644 plugins/talk-plugin-profile-data/server/views/download.ejs create mode 100644 plugins/talk-plugin-profile-data/server/views/download.njk delete mode 100644 views/account/email/confirm.ejs create mode 100644 views/account/email/confirm.njk delete mode 100644 views/account/password/reset.ejs create mode 100644 views/account/password/reset.njk delete mode 100644 views/admin.ejs create mode 100644 views/admin.njk delete mode 100644 views/api/graphiql.ejs create mode 100644 views/api/graphiql.njk delete mode 100644 views/auth-callback.ejs create mode 100644 views/auth-callback.njk delete mode 100644 views/dev/article.ejs create mode 100644 views/dev/article.njk delete mode 100644 views/dev/articles.ejs create mode 100644 views/dev/articles.njk delete mode 100644 views/embed/stream.ejs create mode 100644 views/embed/stream.njk delete mode 100644 views/login.ejs create mode 100644 views/login.njk delete mode 100644 views/partials/account.ejs delete mode 100644 views/partials/custom-css.ejs create mode 100644 views/partials/custom-css.njk delete mode 100644 views/partials/data.ejs create mode 100644 views/partials/data.njk delete mode 100644 views/partials/dev-nav.ejs create mode 100644 views/partials/favicon.njk delete mode 100644 views/partials/head.ejs create mode 100644 views/partials/nav.njk create mode 100644 views/templates/account.njk create mode 100644 views/templates/base.njk rename views/{partials/dev.ejs => templates/development.njk} (54%) diff --git a/app.js b/app.js index c5507da2e..b3400b939 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,6 @@ const express = require('express'); +const nunjucks = require('nunjucks'); +const cons = require('consolidate'); const trace = require('./middleware/trace'); const logging = require('./middleware/logging'); const path = require('path'); @@ -72,7 +74,26 @@ app.use( // VIEW CONFIGURATION //============================================================================== -app.set('views', path.join(__dirname, 'views')); +// configure the default views directory. +const views = path.join(__dirname, 'views'); +app.set('views', views); + +// reconfigure nunjucks. +cons.requires.nunjucks = nunjucks.configure(views, { + autoescape: true, + trimBlocks: true, + lstripBlocks: true, + watch: process.env.NODE_ENV === 'development', +}); + +// assign the nunjucks engine to .njk files. +app.engine('njk', cons.nunjucks); + +// assign the ejs engine to .ejs and .html files. +app.engine('ejs', cons.ejs); +app.engine('html', cons.ejs); + +// set .ejs as the default extension. app.set('view engine', 'ejs'); //============================================================================== diff --git a/package.json b/package.json index 97f4e373d..e8ff0182d 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "common-tags": "^1.4.0", "compression": "1.7.1", "compression-webpack-plugin": "^1.0.0", + "consolidate": "0.14.0", "cookie-parser": "^1.4.3", "copy-webpack-plugin": "^4.0.0", "cross-spawn": "^5.1.0", diff --git a/plugins/talk-plugin-notifications/server/router.js b/plugins/talk-plugin-notifications/server/router.js index 68919a936..9185c5aba 100644 --- a/plugins/talk-plugin-notifications/server/router.js +++ b/plugins/talk-plugin-notifications/server/router.js @@ -4,7 +4,7 @@ const { get, isEmpty, reduce } = require('lodash'); module.exports = router => { router.get('/account/unsubscribe-notifications', (req, res) => { - res.render(path.join(__dirname, 'views/unsubscribe-notifications')); + res.render(path.join(__dirname, 'views/unsubscribe-notifications.njk')); }); /** diff --git a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs deleted file mode 100644 index 129c3115c..000000000 --- a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs +++ /dev/null @@ -1,64 +0,0 @@ - - - - - <%= t('talk-plugin-notifications.unsubscribe_page.unsubscribe') %> - <%- include(root + '/partials/head') %> - - - - -
-
<%= t('talk-plugin-notifications.unsubscribe_page.token_invalid') %>
- -
- <%= t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') %> - -
-
- - - - \ No newline at end of file diff --git a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk new file mode 100644 index 000000000..f8191d525 --- /dev/null +++ b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk @@ -0,0 +1,61 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %} + +{% block html %} +
+
{{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}
+ +
+ {{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }} + +
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/plugins/talk-plugin-profile-data/server/router.js b/plugins/talk-plugin-profile-data/server/router.js index 6d8f47d03..6bd165e64 100644 --- a/plugins/talk-plugin-profile-data/server/router.js +++ b/plugins/talk-plugin-profile-data/server/router.js @@ -102,7 +102,7 @@ async function loadComments(ctx, userID, archive, latestContentDate) { module.exports = router => { // /account/download will render the download page. router.get('/account/download', (req, res) => { - res.render(path.join(__dirname, 'views', 'download')); + res.render(path.join(__dirname, 'views', 'download.njk')); }); // /api/v1/account/download will send back a zipped archive of the users diff --git a/plugins/talk-plugin-profile-data/server/views/download.ejs b/plugins/talk-plugin-profile-data/server/views/download.ejs deleted file mode 100644 index 260badf3a..000000000 --- a/plugins/talk-plugin-profile-data/server/views/download.ejs +++ /dev/null @@ -1,56 +0,0 @@ - - - - <%= t('download_landing.download_your_account') %> - <%- include(root + '/partials/account') %> - - -
-
-

<%= t('download_landing.download_your_account') %>

-

<%= t('download_landing.download_details') %>

-

<%= t('download_landing.all_information_included') %>

-
    -
  • <%= t('download_landing.information_included.date') %>
  • -
  • <%= t('download_landing.information_included.url') %>
  • -
  • <%= t('download_landing.information_included.body') %>
  • -
  • <%= t('download_landing.information_included.asset_url') %>
  • -
-
-
- -
-
-
- - - - diff --git a/plugins/talk-plugin-profile-data/server/views/download.njk b/plugins/talk-plugin-profile-data/server/views/download.njk new file mode 100644 index 000000000..5527473bc --- /dev/null +++ b/plugins/talk-plugin-profile-data/server/views/download.njk @@ -0,0 +1,56 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('download_landing.download_your_account') }}{% endblock %} + +{% block html %} +
+
+

{{ t('download_landing.download_your_account') }}

+

{{ t('download_landing.download_details') }}

+

{{ t('download_landing.all_information_included') }}

+
    +
  • {{ t('download_landing.information_included.date') }}
  • +
  • {{ t('download_landing.information_included.url') }}
  • +
  • {{ t('download_landing.information_included.body') }}
  • +
  • {{ t('download_landing.information_included.asset_url') }}
  • +
+
+
+ +
+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/routes/account/index.js b/routes/account/index.js index 70e62accc..37b26e344 100644 --- a/routes/account/index.js +++ b/routes/account/index.js @@ -2,11 +2,11 @@ const express = require('express'); const router = express.Router(); router.get('/email/confirm', (req, res) => { - res.render('account/email/confirm'); + res.render('account/email/confirm.njk'); }); router.get('/password/reset', (req, res) => { - res.render('account/password/reset'); + res.render('account/password/reset.njk'); }); module.exports = router; diff --git a/routes/admin/index.js b/routes/admin/index.js index d00ef8642..1a9993cfd 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.get('*', (req, res) => { - res.render('admin'); + res.render('admin.njk'); }); module.exports = router; diff --git a/routes/api/v1/graph.js b/routes/api/v1/graph.js index 40e87415b..0660755ea 100644 --- a/routes/api/v1/graph.js +++ b/routes/api/v1/graph.js @@ -10,7 +10,7 @@ router.use('/ql', apollo.graphqlExpress(createGraphOptions)); if (process.env.NODE_ENV !== 'production') { // Interactive graphiql interface. router.use('/iql', staticTemplate, (req, res) => { - res.render('api/graphiql', { + res.render('api/graphiql.njk', { endpointURL: 'api/v1/graph/ql', }); }); diff --git a/routes/dev/assets.js b/routes/dev/assets.js index aea0628e3..df59f21a0 100644 --- a/routes/dev/assets.js +++ b/routes/dev/assets.js @@ -11,7 +11,7 @@ router.get('/id/:asset_id', async (req, res, next) => { throw new ErrNotFound(); } - res.render('dev/article', { + res.render('dev/article.njk', { title: asset.title, asset_id: asset.id, asset_url: asset.url, @@ -28,7 +28,7 @@ router.get('/random', (req, res) => { }); router.get('/title/:asset_title', (req, res) => { - res.render('dev/article', { + res.render('dev/article.njk', { title: req.params.asset_title.split('-').join(' '), asset_url: '', asset_id: null, @@ -48,7 +48,7 @@ router.get('/', async (req, res, next) => { Asset.count(), ]); - res.render('dev/articles', { + res.render('dev/articles.njk', { skip, limit, count, diff --git a/routes/dev/index.js b/routes/dev/index.js index 58daca583..580e702fd 100644 --- a/routes/dev/index.js +++ b/routes/dev/index.js @@ -12,7 +12,7 @@ router.get('/', staticTemplate, async (req, res) => { await SetupService.isAvailable(); return res.redirect(url.resolve(MOUNT_PATH, 'admin/install')); } catch (e) { - return res.render('dev/article', { + return res.render('dev/article.njk', { title: 'Coral Talk', asset_url: '', asset_id: '', diff --git a/routes/embed/index.js b/routes/embed/index.js index 695ffe609..7456a5b71 100644 --- a/routes/embed/index.js +++ b/routes/embed/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.use('/stream', (req, res) => { - res.render('embed/stream'); + res.render('embed/stream.njk'); }); module.exports = router; diff --git a/routes/login/index.js b/routes/login/index.js index b69cb7f5c..e03690dd0 100644 --- a/routes/login/index.js +++ b/routes/login/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.get('*', (req, res) => { - res.render('login'); + res.render('login.njk'); }); module.exports = router; diff --git a/services/passport.js b/services/passport.js index e4813e419..4b96d2352 100644 --- a/services/passport.js +++ b/services/passport.js @@ -115,13 +115,13 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { res.locals.encodeJSONForHTML = encodeJSONForHTML; if (err) { - return res.render('auth-callback', { + return res.render('auth-callback.njk', { auth: { err, data: null }, }); } if (!user) { - return res.render('auth-callback', { + return res.render('auth-callback.njk', { auth: { err: new ErrNotAuthorized(), data: null }, }); } @@ -132,7 +132,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { SetTokenForSafari(req, res, token); // We logged in the user! Let's send back the user data. - res.render('auth-callback', { + res.render('auth-callback.njk', { auth: { err: null, data: { user, token } }, }); }; diff --git a/views/account/email/confirm.ejs b/views/account/email/confirm.ejs deleted file mode 100644 index 47bc7773b..000000000 --- a/views/account/email/confirm.ejs +++ /dev/null @@ -1,63 +0,0 @@ - - - - <%= t('confirm_email.email_confirmation') %> - <%- include ../../partials/account %> - - -
-
-

<%= t('confirm_email.email_confirmation') %>

-

<%= t('confirm_email.click_to_confirm') %>

-
-
- -
-
-
- - - - diff --git a/views/account/email/confirm.njk b/views/account/email/confirm.njk new file mode 100644 index 000000000..6111cb21e --- /dev/null +++ b/views/account/email/confirm.njk @@ -0,0 +1,63 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('confirm_email.email_confirmation') }}{% endblock %} + +{% block html %} +
+
+

{{ t('confirm_email.email_confirmation') }}

+

{{ t('confirm_email.click_to_confirm') }}

+
+
+ +
+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/account/password/reset.ejs b/views/account/password/reset.ejs deleted file mode 100644 index c8b106139..000000000 --- a/views/account/password/reset.ejs +++ /dev/null @@ -1,85 +0,0 @@ - - - - <%= t('password_reset.set_new_password') %> - <%- include ../../partials/account %> - - -
-
-

<%= t('password_reset.set_new_password') %>

-

<%= t('password_reset.change_password_help') %>

-
-
- - - -
-
-
- - - - diff --git a/views/account/password/reset.njk b/views/account/password/reset.njk new file mode 100644 index 000000000..b6f227dea --- /dev/null +++ b/views/account/password/reset.njk @@ -0,0 +1,85 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('password_reset.set_new_password') }}{% endblock %} + +{% block html %} +
+
+

{{ t('password_reset.set_new_password') }}

+

{{ t('password_reset.change_password_help') }}

+
+
+ + + +
+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/admin.ejs b/views/admin.ejs deleted file mode 100644 index 89cc150f7..000000000 --- a/views/admin.ejs +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Talk - Coral Admin - <%- include partials/head %> - - - - <%- include partials/custom-css %> - - -
- - - - diff --git a/views/admin.njk b/views/admin.njk new file mode 100644 index 000000000..4b5236424 --- /dev/null +++ b/views/admin.njk @@ -0,0 +1,14 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk Admin{% endblock %} + +{% block css %} + + + +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/api/graphiql.ejs b/views/api/graphiql.ejs deleted file mode 100644 index 8007569b3..000000000 --- a/views/api/graphiql.ejs +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - GraphiQL - - <%- include ../partials/dev %> - - - - - - - - - <%- include ../partials/dev-nav %> -
- - - diff --git a/views/api/graphiql.njk b/views/api/graphiql.njk new file mode 100644 index 000000000..b726918f3 --- /dev/null +++ b/views/api/graphiql.njk @@ -0,0 +1,126 @@ +{% extends "templates/development.njk" %} + +{% block title %}GraphiQL{% endblock %} + +{% block css %} +{# Include the base development pieces #} +{{ super() }} + + +{% endblock %} + +{% block js %} + + + + + +{% endblock %} + +{% block html %} +
+{% endblock %} diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs deleted file mode 100644 index e4d2387b7..000000000 --- a/views/auth-callback.ejs +++ /dev/null @@ -1,10 +0,0 @@ - - - - <%- include partials/data %> - - - - - - diff --git a/views/auth-callback.njk b/views/auth-callback.njk new file mode 100644 index 000000000..7119a6255 --- /dev/null +++ b/views/auth-callback.njk @@ -0,0 +1,10 @@ + + + + {% include "partials/data.njk" %} + + + + + + diff --git a/views/dev/article.ejs b/views/dev/article.ejs deleted file mode 100644 index 008d3989e..000000000 --- a/views/dev/article.ejs +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - <%= title %> - <%- include ../partials/dev %> - - - <%- include ../partials/dev-nav %> -
-

<%= title %>

-
- - -
- - diff --git a/views/dev/article.njk b/views/dev/article.njk new file mode 100644 index 000000000..2fb8b3724 --- /dev/null +++ b/views/dev/article.njk @@ -0,0 +1,55 @@ +{% extends "templates/development.njk" %} + +{% block title %}{{ title }}{% endblock %} + +{% block meta %} + + + + + + + + +{% endblock %} + +{% block html %} +
+

{{ title }}

+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/dev/articles.ejs b/views/dev/articles.ejs deleted file mode 100644 index 1b07ed116..000000000 --- a/views/dev/articles.ejs +++ /dev/null @@ -1,40 +0,0 @@ - - - All Assets - <%- include ../partials/dev %> - - - <%- include ../partials/dev-nav %> -
-
-

All Assets

- <%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets -
-
- <% if (skip === 0) { %> Create a random article<% } %> - <% assets.forEach(function (asset) { %> - -
-
<%= asset.title %>
- Created <%= asset.created_at.toLocaleString('en-US') %> -
- <%= asset.url %> -
- <% }) %> -
- <% if (count !== assets.length) { %> - - <% } %> -
- - diff --git a/views/dev/articles.njk b/views/dev/articles.njk new file mode 100644 index 000000000..e204c39fc --- /dev/null +++ b/views/dev/articles.njk @@ -0,0 +1,37 @@ +{% extends "templates/development.njk" %} + +{% block title %}All Assets{% endblock %} + +{% block html %} +
+
+

All Assets

+ {{ skip + 1 }} - {{ skip + assets.length }} of {{ count }} Assets +
+ + {% if count !== assets.length %} + + {% endif %} +
+{% endblock %} diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs deleted file mode 100644 index a1c708b55..000000000 --- a/views/embed/stream.ejs +++ /dev/null @@ -1,14 +0,0 @@ - - - - - <%- include ../partials/head %> - - - <%- include ../partials/custom-css %> - - -
- - - diff --git a/views/embed/stream.njk b/views/embed/stream.njk new file mode 100644 index 000000000..ddc620f8e --- /dev/null +++ b/views/embed/stream.njk @@ -0,0 +1,16 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk{% endblock %} + +{% block css %} + + +{% endblock %} + +{% block html %} +
+{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/views/login.ejs b/views/login.ejs deleted file mode 100644 index 671b25c83..000000000 --- a/views/login.ejs +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - <%- include partials/head %> - - <%- include partials/custom-css %> - - -
- - - - diff --git a/views/login.njk b/views/login.njk new file mode 100644 index 000000000..2288898c7 --- /dev/null +++ b/views/login.njk @@ -0,0 +1,16 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk - Login{% endblock %} + +{% block css %} + +{% endblock %} + +{% block html %} +
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/partials/account.ejs b/views/partials/account.ejs deleted file mode 100644 index ff2166816..000000000 --- a/views/partials/account.ejs +++ /dev/null @@ -1,4 +0,0 @@ -<%- include head %> - - -<%- include custom-css %> diff --git a/views/partials/custom-css.ejs b/views/partials/custom-css.ejs deleted file mode 100644 index d453c37e1..000000000 --- a/views/partials/custom-css.ejs +++ /dev/null @@ -1 +0,0 @@ -<% if (locals.customCssUrl) { %><% } %> diff --git a/views/partials/custom-css.njk b/views/partials/custom-css.njk new file mode 100644 index 000000000..6d725e31e --- /dev/null +++ b/views/partials/custom-css.njk @@ -0,0 +1,3 @@ +{% if customCssUrl %} + +{% endif %} diff --git a/views/partials/data.ejs b/views/partials/data.ejs deleted file mode 100644 index f0bbf5925..000000000 --- a/views/partials/data.ejs +++ /dev/null @@ -1,3 +0,0 @@ -<%_ if (data != null) { _%> - -<%_ } _%> \ No newline at end of file diff --git a/views/partials/data.njk b/views/partials/data.njk new file mode 100644 index 000000000..4edaf2e2e --- /dev/null +++ b/views/partials/data.njk @@ -0,0 +1,3 @@ +{% if data %} + +{% endif %} diff --git a/views/partials/dev-nav.ejs b/views/partials/dev-nav.ejs deleted file mode 100644 index 088c52c20..000000000 --- a/views/partials/dev-nav.ejs +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/views/partials/favicon.njk b/views/partials/favicon.njk new file mode 100644 index 000000000..5ec2aad8a --- /dev/null +++ b/views/partials/favicon.njk @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/views/partials/head.ejs b/views/partials/head.ejs deleted file mode 100644 index dc3df7103..000000000 --- a/views/partials/head.ejs +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -<%- include data %> - - diff --git a/views/partials/nav.njk b/views/partials/nav.njk new file mode 100644 index 000000000..ecea9018d --- /dev/null +++ b/views/partials/nav.njk @@ -0,0 +1,8 @@ + diff --git a/views/templates/account.njk b/views/templates/account.njk new file mode 100644 index 000000000..7e1b6e6c9 --- /dev/null +++ b/views/templates/account.njk @@ -0,0 +1,6 @@ +{% extends "templates/base.njk" %} + +{% block css %} + + +{% endblock %} diff --git a/views/templates/base.njk b/views/templates/base.njk new file mode 100644 index 000000000..23048151e --- /dev/null +++ b/views/templates/base.njk @@ -0,0 +1,39 @@ + + + + {# Meta tags #} + + + {% block meta %}{% endblock %} + + {# Favicon Configuration #} + {% include "partials/favicon.njk" %} + + {# CSP and security headers #} + {% block security %}{% endblock %} + + {# Title #} + {% block title %}{% endblock %} + + {# CSS #} + + + + {% include "partials/custom-css.njk" %} + {% block css %}{% endblock %} + + {# Static data injection #} + {% include "partials/data.njk" %} + + {# Configuration #} + + + + {% block body %} + {% block html %} +
+ {% endblock %} + {% endblock %} + {% block js %}{% endblock %} + + diff --git a/views/partials/dev.ejs b/views/templates/development.njk similarity index 54% rename from views/partials/dev.ejs rename to views/templates/development.njk index 691b548e5..f7fab6682 100644 --- a/views/partials/dev.ejs +++ b/views/templates/development.njk @@ -1,5 +1,17 @@ - +{% extends "templates/base.njk" %} + +{# Null out the security block, we don't want/need that in development #} +{% block security %}{% endblock %} + +{% block css %} - + +{% endblock %} + +{% block body %} + {% include "partials/nav.njk" %} + + {% block html %}{% endblock %} +{% endblock %} diff --git a/yarn.lock b/yarn.lock index f0da6d006..770dcc7ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2392,6 +2392,12 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +consolidate@0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.0.tgz#b03acd566a2565ca96e99f44fd1417486b4df88d" + dependencies: + bluebird "^3.1.1" + constantinople@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.0.tgz#7569caa8aa3f8d5935d62e1fa96f9f702cd81c79" From c84a9716a11ccdcc2942f92a0e00b66aa7c4747b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 17 May 2018 18:37:50 -0600 Subject: [PATCH 02/11] added support for csp --- middleware/contentSecurityPolicy.js | 49 +++++++++++++++++++ middleware/nonce.js | 9 ++++ .../views/unsubscribe-notifications.njk | 11 ++++- .../server/views/download.njk | 2 +- routes/api/v1/csp.js | 33 +++++++++++++ routes/api/v1/index.js | 1 + routes/index.js | 16 +++--- routes/plugins.js | 10 ++-- views/account/email/confirm.njk | 2 +- views/account/password/reset.njk | 2 +- views/admin.njk | 2 +- views/login.njk | 2 +- 12 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 middleware/contentSecurityPolicy.js create mode 100644 middleware/nonce.js create mode 100644 routes/api/v1/csp.js diff --git a/middleware/contentSecurityPolicy.js b/middleware/contentSecurityPolicy.js new file mode 100644 index 000000000..992e41e36 --- /dev/null +++ b/middleware/contentSecurityPolicy.js @@ -0,0 +1,49 @@ +const helmet = require('helmet'); +const { WEBSOCKET_LIVE_URI } = require('../config'); +const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url'); +const { URL } = require('url'); + +// websocketSrc represents the host where we can connect for websocket requests. +const websocketSrc = new URL(WEBSOCKET_LIVE_URI || BASE_URL).host; + +// staticSrc represents any static asset hosted on the static host. +const staticSrc = new URL(STATIC_URL).host; + +// nonceSrc represents the nonce source that is used to indicate a safe resource +// to load. +const nonceSrc = (req, res) => `'nonce-${res.locals.nonce}'`; + +module.exports = helmet.contentSecurityPolicy({ + directives: { + reportUri: `${BASE_PATH}api/v1/csp`, // report all policy violations to our reporting uri + defaultSrc: ["'none'"], // by default, do not allow anything at all + scriptSrc: [ + "'self'", + 'https://ajax.googleapis.com', // for jquery + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + styleSrc: [ + "'self'", + 'https://maxcdn.bootstrapcdn.com', // for bootstrap css + 'https://fonts.googleapis.com', // for google fonts + 'https://code.getmdl.io', // for mdl css + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + connectSrc: ["'self'", websocketSrc], + fontSrc: [ + "'self'", + 'https://maxcdn.bootstrapcdn.com', // for font-awesome + 'https://fonts.gstatic.com', // for google fonts + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + imgSrc: [ + "'self'", + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + }, + browserSniff: false, +}); diff --git a/middleware/nonce.js b/middleware/nonce.js new file mode 100644 index 000000000..1181fac85 --- /dev/null +++ b/middleware/nonce.js @@ -0,0 +1,9 @@ +const uuid = require('uuid/v4'); + +// nonce is designed to create a random value that can be used in conjunction +// with the csp middleware. +module.exports = (req, res, next) => { + res.locals.nonce = uuid(); + + next(); +}; diff --git a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk index f8191d525..5080277f0 100644 --- a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk +++ b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk @@ -2,10 +2,17 @@ {% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %} +{% block css %} +{{ super() }} + +{% endblock %} + {% block html %}
{{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}
- +
{{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}
{{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }} @@ -15,7 +22,7 @@ {% block js %} - - - - + {% endblock %} diff --git a/views/login.njk b/views/login.njk index 2288898c7..6a3c1112f 100644 --- a/views/login.njk +++ b/views/login.njk @@ -11,6 +11,6 @@ {% endblock %} {% block js %} - + {% endblock %} From f58e3df439fb696cf7456f25122e0fa77a0c362e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 May 2018 15:27:29 -0600 Subject: [PATCH 03/11] added config for report only mode --- config.js | 5 +++++ docs/source/02-02-advanced-configuration.md | 7 +++++++ middleware/contentSecurityPolicy.js | 4 +++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config.js b/config.js index 6a1f999a7..ec04a8c9d 100644 --- a/config.js +++ b/config.js @@ -48,6 +48,11 @@ const CONFIG = { // request all of the records. Otherwise, minimum limits of 0 are enforced. ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE', + // ONLY_REPORT_CSP_VIOLATIONS disables strict CSP enforcement, and only + // reports CSP violations instead of blocking them. + ONLY_REPORT_CSP_VIOLATIONS: + process.env.TALK_ONLY_REPORT_CSP_VIOLATIONS === 'TRUE', + // LOGGING_LEVEL specifies the logging level used by the bunyan logger. LOGGING_LEVEL: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes( process.env.TALK_LOGGING_LEVEL diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md index 5b638401d..d4ef5f2f7 100644 --- a/docs/source/02-02-advanced-configuration.md +++ b/docs/source/02-02-advanced-configuration.md @@ -497,6 +497,13 @@ tracing of GraphQL requests. **Note: Apollo Engine is a premium service, charges may apply.** +## TALK_ONLY_REPORT_CSP_VIOLATIONS + +Setting this to `TRUE` will enable the [report only](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#Testing_your_policy) +mode of the Content Security Policy. The policy is not enforced, but any +violations are reported to a provided URI. If you are encountering issues with +resources not loading, try experimenting with this parameter. (Default `FALSE`) + ## ALLOW_NO_LIMIT_QUERIES Setting this to `TRUE` will allow queries to execute without a limit (returns diff --git a/middleware/contentSecurityPolicy.js b/middleware/contentSecurityPolicy.js index 992e41e36..ca30f7c6e 100644 --- a/middleware/contentSecurityPolicy.js +++ b/middleware/contentSecurityPolicy.js @@ -1,5 +1,5 @@ const helmet = require('helmet'); -const { WEBSOCKET_LIVE_URI } = require('../config'); +const { WEBSOCKET_LIVE_URI, ONLY_REPORT_CSP_VIOLATIONS } = require('../config'); const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url'); const { URL } = require('url'); @@ -46,4 +46,6 @@ module.exports = helmet.contentSecurityPolicy({ ], }, browserSniff: false, + // Allow the configuration to disable strict enforcement of CSP. + reportOnly: ONLY_REPORT_CSP_VIOLATIONS, }); From 5403872bb70c147316dba2b944bf0647280c2430 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 May 2018 16:21:41 -0600 Subject: [PATCH 04/11] disable by default --- config.js | 7 +++---- docs/source/02-02-advanced-configuration.md | 11 ++++++----- middleware/contentSecurityPolicy.js | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config.js b/config.js index ec04a8c9d..77ed4ab2b 100644 --- a/config.js +++ b/config.js @@ -48,10 +48,9 @@ const CONFIG = { // request all of the records. Otherwise, minimum limits of 0 are enforced. ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE', - // ONLY_REPORT_CSP_VIOLATIONS disables strict CSP enforcement, and only - // reports CSP violations instead of blocking them. - ONLY_REPORT_CSP_VIOLATIONS: - process.env.TALK_ONLY_REPORT_CSP_VIOLATIONS === 'TRUE', + // ENABLE_STRICT_CSP enables strict CSP enforcement, and will enforce as well + // as report CSP violations. + ENABLE_STRICT_CSP: process.env.TALK_ENABLE_STRICT_CSP === 'TRUE', // LOGGING_LEVEL specifies the logging level used by the bunyan logger. LOGGING_LEVEL: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes( diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md index d4ef5f2f7..035d994b6 100644 --- a/docs/source/02-02-advanced-configuration.md +++ b/docs/source/02-02-advanced-configuration.md @@ -497,12 +497,13 @@ tracing of GraphQL requests. **Note: Apollo Engine is a premium service, charges may apply.** -## TALK_ONLY_REPORT_CSP_VIOLATIONS +## TALK_ENABLE_STRICT_CSP -Setting this to `TRUE` will enable the [report only](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#Testing_your_policy) -mode of the Content Security Policy. The policy is not enforced, but any -violations are reported to a provided URI. If you are encountering issues with -resources not loading, try experimenting with this parameter. (Default `FALSE`) +Setting this to `TRUE` will enforce the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) +(or CSP) policy. By default, this configuration is set to +[report only](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#Testing_your_policy) +where the policy is not enforced, but any violations are reported to a provided +URI. (Default `FALSE`) ## ALLOW_NO_LIMIT_QUERIES diff --git a/middleware/contentSecurityPolicy.js b/middleware/contentSecurityPolicy.js index ca30f7c6e..d6036ca76 100644 --- a/middleware/contentSecurityPolicy.js +++ b/middleware/contentSecurityPolicy.js @@ -1,5 +1,5 @@ const helmet = require('helmet'); -const { WEBSOCKET_LIVE_URI, ONLY_REPORT_CSP_VIOLATIONS } = require('../config'); +const { WEBSOCKET_LIVE_URI, ENABLE_STRICT_CSP } = require('../config'); const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url'); const { URL } = require('url'); @@ -47,5 +47,5 @@ module.exports = helmet.contentSecurityPolicy({ }, browserSniff: false, // Allow the configuration to disable strict enforcement of CSP. - reportOnly: ONLY_REPORT_CSP_VIOLATIONS, + reportOnly: !ENABLE_STRICT_CSP, }); From 868343a29b223022a46adcd5e97731d46508bfc7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 May 2018 16:42:23 -0600 Subject: [PATCH 05/11] fixes to csp policy --- middleware/contentSecurityPolicy.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/middleware/contentSecurityPolicy.js b/middleware/contentSecurityPolicy.js index d6036ca76..c47faa74e 100644 --- a/middleware/contentSecurityPolicy.js +++ b/middleware/contentSecurityPolicy.js @@ -3,11 +3,15 @@ const { WEBSOCKET_LIVE_URI, ENABLE_STRICT_CSP } = require('../config'); const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url'); const { URL } = require('url'); -// websocketSrc represents the host where we can connect for websocket requests. -const websocketSrc = new URL(WEBSOCKET_LIVE_URI || BASE_URL).host; +// websocketUri represents the host where we can connect for websocket requests. +const websocketUri = new URL(WEBSOCKET_LIVE_URI || BASE_URL); +websocketUri.protocol = websocketUri.protocol.startsWith('https') + ? 'wss' + : 'ws'; +const { origin: websocketSrc } = websocketUri; // staticSrc represents any static asset hosted on the static host. -const staticSrc = new URL(STATIC_URL).host; +const { host: staticSrc } = new URL(STATIC_URL); // nonceSrc represents the nonce source that is used to indicate a safe resource // to load. From dd0ad17038604e0608046a0fdf4f3c5c813930e6 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 May 2018 16:44:43 -0600 Subject: [PATCH 06/11] fix for custom css --- views/partials/custom-css.njk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/partials/custom-css.njk b/views/partials/custom-css.njk index 6d725e31e..4e5a494e6 100644 --- a/views/partials/custom-css.njk +++ b/views/partials/custom-css.njk @@ -1,3 +1,3 @@ {% if customCssUrl %} - + {% endif %} From 5aba578a986b06f41cf4a924cefcc31e689fc93d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 18 May 2018 16:47:58 -0600 Subject: [PATCH 07/11] doc patch --- docs/source/02-02-advanced-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md index 035d994b6..c4f86dbb8 100644 --- a/docs/source/02-02-advanced-configuration.md +++ b/docs/source/02-02-advanced-configuration.md @@ -500,7 +500,7 @@ tracing of GraphQL requests. ## TALK_ENABLE_STRICT_CSP Setting this to `TRUE` will enforce the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) -(or CSP) policy. By default, this configuration is set to +(or CSP). By default, this configuration is set to [report only](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#Testing_your_policy) where the policy is not enforced, but any violations are reported to a provided URI. (Default `FALSE`) From 10a786f84d9ba962fcbe762fc9ea076bafc07435 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 22 May 2018 14:21:32 -0600 Subject: [PATCH 08/11] simplified language negotiation --- client/coral-framework/services/i18n.js | 131 +++++++++++------------- locales/fi_FI.yml | 4 +- package.json | 2 +- services/i18n.js | 76 ++++++++------ yarn.lock | 6 +- 5 files changed, 110 insertions(+), 109 deletions(-) diff --git a/client/coral-framework/services/i18n.js b/client/coral-framework/services/i18n.js index 03a0b4712..02258a585 100644 --- a/client/coral-framework/services/i18n.js +++ b/client/coral-framework/services/i18n.js @@ -1,7 +1,10 @@ -import ta from 'timeago.js'; +import { negotiateLanguages } from 'fluent-langneg/compat'; + import has from 'lodash/has'; import get from 'lodash/get'; import merge from 'lodash/merge'; +import first from 'lodash/first'; +import isUndefined from 'lodash/isUndefined'; import moment from 'moment'; import 'moment/locale/ar'; @@ -12,8 +15,8 @@ import 'moment/locale/fr'; import 'moment/locale/nl'; import 'moment/locale/pt-br'; -import { createStorage } from 'coral-framework/services/storage'; - +// timeago +import ta from 'timeago.js'; import arTA from 'timeago.js/locales/ar'; import daTA from 'timeago.js/locales/da'; import deTA from 'timeago.js/locales/de'; @@ -24,6 +27,7 @@ import pt_BRTA from 'timeago.js/locales/pt_BR'; import zh_CNTA from 'timeago.js/locales/zh_CN'; import zh_TWTA from 'timeago.js/locales/zh_TW'; +// locales import ar from '../../../locales/ar.yml'; import en from '../../../locales/en.yml'; import da from '../../../locales/da.yml'; @@ -35,8 +39,9 @@ import pt_BR from '../../../locales/pt_BR.yml'; import zh_CN from '../../../locales/zh_CN.yml'; import zh_TW from '../../../locales/zh_TW.yml'; -const defaultLanguage = process.env.TALK_DEFAULT_LANG; -const translations = { +export const defaultLocale = process.env.TALK_DEFAULT_LANG.replace(/-/g, '_'); + +export const translations = { ...ar, ...en, ...da, @@ -49,84 +54,62 @@ const translations = { ...zh_TW, }; -let lang; -let timeagoInstance; +export const supportedLocales = Object.keys(translations); -function setLocale(storage, locale) { - storage.setItem('locale', locale); -} +let LOCALE; +let TIMEAGO_INSTANCE; // detectLanguage will try to get the locale from storage if available, // otherwise will try to get it from the navigator, otherwise, it will fallback // to the default language. -function detectLanguage(storage) { - try { - const lang = storage.getItem('locale') || navigator.language; - if (lang) { - return lang; - } - } catch (err) { - console.warn( - 'Error while trying to detect language, will fallback to', - err - ); - } - - console.warn('Could not detect language, will fallback to', defaultLanguage); - return defaultLanguage; -} - -// getLocale will get the users locale from the local detector and parse it to a -// format we can work with. -function getLocale(storage) { - // Get the language from the local detector. - const lang = detectLanguage(storage); - - // Some language strings come with additional subtags as defined in: - // - // https://www.ietf.org/rfc/bcp/bcp47.txt - // - // So we should strip that off if we find it. - return lang.split('-')[0]; -} +const detectLanguage = () => + first( + negotiateLanguages(navigator.languages, supportedLocales, { + defaultLocale, + strategy: 'lookup', + }) + ); export function setupTranslations() { - // Setup the translation framework with the storage. - const storage = createStorage('localStorage'); + // locale + LOCALE = detectLanguage(); - const locale = getLocale(storage); - setLocale(storage, locale); - - // Setting moment - moment.locale(locale); - - // Extract language key. - lang = locale.split('-')[0]; - - // Check if we have a translation in this language. - if (!(lang in translations)) { - lang = defaultLanguage; - } + // moment + moment.locale(LOCALE); + // timeago ta.register('ar', arTA); ta.register('es', esTA); ta.register('da', daTA); ta.register('de', deTA); ta.register('fr', frTA); - ta.register('nl_NL', nlTA); - ta.register('pt_BR', pt_BRTA); - ta.register('zh_CN', zh_CNTA); - ta.register('zh_TW', zh_TWTA); - - timeagoInstance = ta(); + ta.register('nl-NL', nlTA); + ta.register('pt-BR', pt_BRTA); + ta.register('zh-CN', zh_CNTA); + ta.register('zh-TW', zh_TWTA); + TIMEAGO_INSTANCE = ta(); } +/** + * loadTranslations will load the new language pack into the existing ones. + * + * @param {Object} newTranslations translation object to merge into the existing + * languages. + */ export function loadTranslations(newTranslations) { + // Merge the new translations into the existing translations. merge(translations, newTranslations); + + // Push new languages into the supportedLocales array. + Object.keys(newTranslations).forEach(language => { + if (!supportedLocales.includes(language)) { + supportedLocales.push(language); + } + }); } export function timeago(time) { - return timeagoInstance.format(new Date(time), lang); + return TIMEAGO_INSTANCE.format(new Date(time), LOCALE); } /** @@ -140,24 +123,24 @@ export function timeago(time) { */ export function t(key, ...replacements) { let translation; - if (has(translations[lang], key)) { - translation = get(translations[lang], key); + if (has(translations[LOCALE], key)) { + translation = get(translations[LOCALE], key); } else if (has(translations['en'], key)) { translation = get(translations['en'], key); - console.warn(`${lang}.${key} language key not set`); + console.warn(`${LOCALE}.${key} language key not set`); } - if (translation) { - // replace any {n} with the arguments passed to this method - replacements.forEach((str, i) => { - translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str); - }); - - return translation; - } else { - console.warn(`${lang}.${key} and en.${key} language key not set`); + if (!translation) { + console.warn(`${LOCALE}.${key} and en.${key} language key not set`); return key; } + + // Handle replacements in the translation string. + return translation.replace( + /{(\d+)}/g, + (match, number) => + !isUndefined(replacements[number]) ? replacements[number] : match + ); } export default t; diff --git a/locales/fi_FI.yml b/locales/fi_FI.yml index 71416b4ff..494a48cdb 100644 --- a/locales/fi_FI.yml +++ b/locales/fi_FI.yml @@ -413,7 +413,7 @@ fi_FI: title_reject: "Huomasimme sinun hylänneen käyttäjänimen" suspend_user: "Aseta väliaikainen käyttökielto" yes_suspend: "Kyllä, sulje väliaikaisesti" - email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää." + email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää." write_message: "Kirjoita viesti" send: Lähetä thank_you: "Arvostamme palautettasi. Moderaattorimme käy läpi tekemäsi ilmiannon." @@ -462,4 +462,4 @@ fi_FI: close: "Sulje asennusnäkymä" admin_sidebar: view_options: "Näytä asetukset" -sort_comments: "Järjestä kommentit" \ No newline at end of file + sort_comments: "Järjestä kommentit" diff --git a/package.json b/package.json index c387d6145..5dcb5c6ff 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "dependencies": { "@coralproject/gql-merge": "^0.1.0", "@coralproject/graphql-anywhere-optimized": "^0.1.0", - "accepts": "^1.3.4", "apollo-client": "^1.9.1", "apollo-engine": "^0.8.1", "apollo-server-express": "^1.2.0", @@ -108,6 +107,7 @@ "express-static-gzip": "^0.3.1", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^0.11.2", + "fluent-langneg": "^0.1.0", "form-data": "^2.3.1", "fs-extra": "^4.0.1", "graphql": "^0.10.1", diff --git a/services/i18n.js b/services/i18n.js index 666f15a59..52c34a4ed 100644 --- a/services/i18n.js +++ b/services/i18n.js @@ -1,8 +1,11 @@ const fs = require('fs'); const path = require('path'); const debug = require('debug')('talk:services:i18n'); -const accepts = require('accepts'); -const { get, has, merge } = require('lodash'); +const { + acceptedLanguages, + negotiateLanguages, +} = require('fluent-langneg/compat'); +const { first, get, has, merge, isUndefined } = require('lodash'); const yaml = require('yamljs'); const plugins = require('./plugins'); const { DEFAULT_LANG } = require('../config'); @@ -11,7 +14,7 @@ const resolve = (...paths) => path.resolve(path.join(__dirname, '..', 'locales', ...paths)); // Load all the translations. -let translations = fs +const translations = fs .readdirSync(resolve()) // Resolve all the filenames relative the the locales directory. @@ -23,26 +26,22 @@ let translations = fs // Load the translation files from disk. .map(filename => fs.readFileSync(filename, 'utf8')) - // Load the translation files. - .reduce((packs, contents) => { - const pack = yaml.parse(contents); - - return merge(packs, pack); - }, {}); + // Load the translation files and merge the yaml into the existing packs. + .reduce((packs, contents) => merge(packs, yaml.parse(contents)), {}); // Create a list of all supported translations. -const languages = Object.keys(translations); +const supportedLocales = Object.keys(translations); // Move the default language to the front. -if (languages.includes(DEFAULT_LANG)) { - const from = languages.indexOf(DEFAULT_LANG); - languages.splice(from, 1); - languages.splice(0, 0, DEFAULT_LANG); +if (supportedLocales.includes(DEFAULT_LANG)) { + const from = supportedLocales.indexOf(DEFAULT_LANG); + supportedLocales.splice(from, 1); + supportedLocales.splice(0, 0, DEFAULT_LANG); } -debug(`loaded language sets for ${languages}`); +debug(`loaded language sets for ${supportedLocales}`); let loadedPluginTranslations = false; -const loadPluginTranslations = () => { +const lazyLoadPluginTranslations = () => { if (loadedPluginTranslations) { return; } @@ -55,7 +54,15 @@ const loadPluginTranslations = () => { const pack = yaml.parse(fs.readFileSync(filename, 'utf8')); - translations = merge(translations, pack); + // Merge the translations into the system translations. + merge(translations, pack); + + // Push new languages into the supportedLocales array. + Object.keys(pack).forEach(language => { + if (!supportedLocales.includes(language)) { + supportedLocales.push(language); + } + }); }); loadedPluginTranslations = true; @@ -64,7 +71,7 @@ const loadPluginTranslations = () => { const t = lang => (key, ...replacements) => { // Loads the translations into the translations array from plugins. This is // done lazily to ensure that we don't have an import cycle. - loadPluginTranslations(); + lazyLoadPluginTranslations(); let translation; if (has(translations[lang], key)) { @@ -74,16 +81,17 @@ const t = lang => (key, ...replacements) => { console.warn(`${lang}.${key} language key not set`); } - if (translation) { - // replace any {n} with the arguments passed to this method - replacements.forEach((str, i) => { - translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str); - }); - return translation; - } else { + if (!translation) { console.warn(`${lang}.${key} and en.${key} language key not set`); return key; } + + // Handle replacements in the translation string. + return translation.replace( + /{(\d+)}/g, + (match, number) => + !isUndefined(replacements[number]) ? replacements[number] : match + ); }; /** @@ -92,13 +100,19 @@ const t = lang => (key, ...replacements) => { */ const i18n = { request(req) { - debug(`possible languages given request '${accepts(req).languages()}'`); - const lang = accepts(req).language(languages); - debug(`parsed request language as '${lang}'`); - const language = lang ? lang : DEFAULT_LANG; - debug(`decided language as '${language}'`); + const acceptsLanguages = acceptedLanguages(req.headers['accept-language']); + debug(`possible languages given request '${acceptsLanguages}'`); - return t(language); + // negotiate the language. + const lang = first( + negotiateLanguages(acceptsLanguages, supportedLocales, { + defaultLocale: DEFAULT_LANG, + strategy: 'lookup', + }) + ); + debug(`decided language as '${lang}'`); + + return t(lang); }, t: t(DEFAULT_LANG), }; diff --git a/yarn.lock b/yarn.lock index 13da4b2f7..dff8e4dab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -167,7 +167,7 @@ abbrev@1, abbrev@^1.0.7: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" -accepts@^1.3.4, accepts@~1.3.4: +accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: @@ -3975,6 +3975,10 @@ flexbuffer@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" +fluent-langneg@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fluent-langneg/-/fluent-langneg-0.1.0.tgz#aa12054fbfa4b728daec38331efc12f01faae93a" + flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" From 849fe37b9bb0dd8ff4c45a3f5b44a8caec668641 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 22 May 2018 23:25:12 +0200 Subject: [PATCH 09/11] Fix incorrect statement regarding GDPR GDPR's reach doesn't depend on the person or business's location, but on whether this person or this business include european citizen in its target (which can be easy to state for a business, but not that much for an individual). --- docs/source/03-08-gdpr.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/03-08-gdpr.md b/docs/source/03-08-gdpr.md index b6689be0e..b5e4ca404 100644 --- a/docs/source/03-08-gdpr.md +++ b/docs/source/03-08-gdpr.md @@ -11,9 +11,9 @@ can enable the following plugins: - [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association - [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion -Even if you don't reside in a location where GDPR will apply, it is recommended -to enable these features as a best practice to provide your users with control over their -own data. +Even if GDPR will not apply apply to you, it is recommended to enable these +features as a best practice to provide your users with control over their own +data. ## GPDR Feature Overview From 879865a4c11dac27d79330cfb86aa7ea5a2e85f6 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 23 May 2018 09:14:32 +0200 Subject: [PATCH 10/11] Fix broken link to tokenusernotfound Fix broken link to tokenusernotfound --- docs/source/integrating/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/integrating/authentication.md b/docs/source/integrating/authentication.md index b7340caf5..5b19768fe 100644 --- a/docs/source/integrating/authentication.md +++ b/docs/source/integrating/authentication.md @@ -81,5 +81,5 @@ example issuer and Talk must match: reference, the basic takeaway is that the secret used to sign the tokens issued by the issuer must be able to be verified by Talk. -For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound) +For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound) reference. From a95d55b6da8ce18285d6421f1ca071396de952ee Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 23 May 2018 10:05:21 +0200 Subject: [PATCH 11/11] Remuve dupe "apply" --- docs/source/03-08-gdpr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/03-08-gdpr.md b/docs/source/03-08-gdpr.md index b5e4ca404..e3fd8623f 100644 --- a/docs/source/03-08-gdpr.md +++ b/docs/source/03-08-gdpr.md @@ -11,7 +11,7 @@ can enable the following plugins: - [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association - [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion -Even if GDPR will not apply apply to you, it is recommended to enable these +Even if GDPR will not apply to you, it is recommended to enable these features as a best practice to provide your users with control over their own data.