diff --git a/.gitignore b/.gitignore index 4ad15747d..6b7b187d9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,6 @@ dump.rdb test/e2e/reports coverage/ +plugins.json plugins/* !plugins/coral-plugin-facebook-auth \ No newline at end of file diff --git a/PLUGINS.md b/PLUGINS.md index f04695f66..0c5f727d9 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -3,11 +3,18 @@ Plugins for Talk can take various forms, currently we are only supporting server side plugins. -## Plugin Registration: `plugins.json` +## Plugin Registration -All plugins must be registered in the root file `plugins.json`. +The parsing order for the plugin registration is as follows: -The format for this file is thus: +- `TALK_PLUGINS_JSON` environment variable +- `plugins.json` file +- `plugins.default.json` file + +If you need to "disable all plugins", you can simply provide `{}` as the +contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`. + +The format for this is thus: ```json { @@ -18,7 +25,7 @@ The format for this file is thus: ``` Where we have a `server` key with an array of plugins that match the folder -name in the `plugins/` folder. For example, the above `plugins.json` would +name in the `plugins/` folder. For example, the above config would require a plugin from `plugins/people`, which must provide a `index.js` file that returns an object that matches the Plugin Specification. diff --git a/README.md b/README.md index b88138425..ef621b59c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ available in the format: `://` without the path. - `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint. - `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout. - `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout. +- `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for alternative methods of loading configuration during development. diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 3f3c395f8..016536708 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -41,9 +41,10 @@ class Embed extends Component { state = {activeTab: 0, showSignInDialog: false, activeReplyBox: ''}; changeTab = (tab) => { + const {isAdmin} = this.props.auth; // Everytime the comes from another tab, the Stream needs to be updated. - if (tab === 0) { + if (tab === 0 && isAdmin) { this.props.data.refetch(); } @@ -116,7 +117,7 @@ class Embed extends Component { render () { const {activeTab} = this.state; const {closedAt, countCache = {}} = this.props.asset; - const {loading, asset, refetch, comment} = this.props.data; + const {asset, refetch, comment} = this.props.data; const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth; // even though the permalinked comment is the highlighted one, we're displaying its parent + replies @@ -130,7 +131,7 @@ class Embed extends Component { minHeight: document.body.scrollHeight + 200 } : {}; - if (loading || !asset) { + if (!asset) { return ; } diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index e3350e0d3..21c176500 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -1,5 +1,7 @@ import pym from 'pym.js'; +import {stringify} from 'querystring'; + const snackbarStyles = { position: 'fixed', cursor: 'default', @@ -26,30 +28,21 @@ const Coral = {}; const Talk = Coral.Talk = {}; // build the URL to load in the pym iframe -function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) { - let iframeArray = [ +function buildStreamIframeUrl(talkBaseUrl, query) { + let url = [ talkBaseUrl, (talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/' - 'embed/stream?asset_url=', - encodeURIComponent(asset_url) - ]; + 'embed/stream?' + ].join(''); - if (comment) { - iframeArray.push('&comment_id='); - iframeArray.push(encodeURIComponent(comment)); - } + url += stringify(query); - if (asset_id) { - iframeArray.push('&asset_id='); - iframeArray.push(encodeURIComponent(asset_id)); - } - - return iframeArray.join(''); + return url; } // Set up postMessage listeners/handlers on the pymParent // e.g. to resize the iframe, and navigate the host page -function configurePymParent(pymParent, asset_url) { +function configurePymParent(pymParent) { let notificationOffset = 200; let ready = false; let cachedHeight; @@ -117,8 +110,8 @@ function configurePymParent(pymParent, asset_url) { if (ready) { window.clearInterval(interval); - // @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place? - pymParent.sendMessage('DOMContentLoaded', asset_url); + // TODO: It's weird to me that this is sent here + pymParent.sendMessage('DOMContentLoaded'); } }, 100); }); @@ -166,42 +159,37 @@ Talk.render = function (el, opts) { } opts = opts || {}; - // @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script) + // TODO: infer this URL without explicit user input (if possible, may have to be added at build/render time of this script) if (!opts.talk) { throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL'); } - // ensure el has an id, as pym can't directly accept the HTMLElement + // Ensure el has an id, as pym can't directly accept the HTMLElement. if (!el.id) { el.id = `_${Math.random()}`; } - let asset_url = opts.asset_url; - if (!asset_url) { + // Compose the query to send down to the Talk API so it knows what to load. + let query = {}; + + query.comment_id = window.location.hash.slice(1); + query.asset_id = opts.asset_id; + + query.asset_url = opts.asset_url; + if (!query.asset_url) { try { - asset_url = document.querySelector('link[rel="canonical"]').href; + query.asset_url = document.querySelector('link[rel="canonical"]').href; } catch (e) { console.warn('This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'); - asset_url = window.location.origin + window.location.pathname; + query.asset_url = window.location.origin + window.location.pathname; } } - let comment = window.location.hash.slice(1); - - let query = { + configurePymParent(new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), { title: opts.title, - asset_url: asset_url, id: `${el.id}_iframe`, name: `${el.id}_iframe` - }; - - if (opts.asset_id && opts.asset_id.length > 0) { - query.asset_id = opts.asset_id; - } - - let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset_url, comment), query); - - configurePymParent(pymParent, asset_url); + })); }; export default Coral; diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index c9f9d3d5b..102c656e5 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -1,6 +1,6 @@ #import "../fragments/commentView.graphql" -query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) { +query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!) { # the comment here is for loading one comment and it's children, probably after following a permalink # $has_comment is derived from the comment_id query param in the iframe url, # which is in turn pulled from the host page url diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 6f4151ff6..32e2e4263 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -8,11 +8,6 @@ const RootQuery = { }, asset(_, query, {loaders: {Assets}}) { if (query.id) { - - // TODO: we may not always have a comment stream here, therefore, when we - // load it, we may also need to create with the url. This may also have to - // move the logic over to the mutators function as an upsert operation - // possibly. return Assets.getByID.load(query.id); } diff --git a/package.json b/package.json index 723c6dece..b13d1dce4 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "graphql-tools": "^0.10.1", "helmet": "^3.5.0", "inquirer": "^3.0.6", + "joi": "^10.4.1", "jsonwebtoken": "^7.3.0", "kue": "^0.11.5", "linkify-it": "^2.0.3", diff --git a/plugins.json b/plugins.default.json similarity index 100% rename from plugins.json rename to plugins.default.json diff --git a/plugins.js b/plugins.js index d7c9f9426..949a5b9ab 100644 --- a/plugins.js +++ b/plugins.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const resolve = require('resolve'); const debug = require('debug')('talk:plugins'); +const Joi = require('joi'); // Add support for require rewriting. require('app-module-path').addPath(__dirname); @@ -12,15 +13,42 @@ let plugins = {}; // file isn't loaded, but continuing. Else, like a parsing error, throw it and // crash the program. try { - plugins = JSON.parse(fs.readFileSync(path.join(__dirname, 'plugins.json'), 'utf8')); + let defaultPlugins = path.join(__dirname, 'plugins.default.json'); + let customPlugins = path.join(__dirname, 'plugins.json'); + let envPluginJSON = process.env.TALK_PLUGINS_JSON; + + if (envPluginJSON && envPluginJSON.length > 0) { + debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); + plugins = JSON.parse(envPluginJSON); + } else if (fs.existsSync(customPlugins)) { + debug(`Now using ${customPlugins} for plugins`); + plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8')); + } else { + debug(`Now using ${defaultPlugins} for plugins`); + plugins = JSON.parse(fs.readFileSync(defaultPlugins, 'utf8')); + } } catch (err) { if (err.code === 'ENOENT') { - console.error('plugins.json not found, plugins will not be active'); + console.error('plugins.json and plugins.default.json not found, plugins will not be active'); } else { throw err; } } +const hookSchemas = { + passport: Joi.func().arity(1), + router: Joi.func().arity(1), + context: Joi.object().pattern(/\w/, Joi.func().maxArity(1)), + hooks: Joi.object({ + pre: Joi.func(), + post: Joi.func() + }), + loaders: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())), + mutators: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())), + resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())), + typeDefs: Joi.string() +}; + /** * isInternal checks to see if a given plugin is internal, and returns true * if it is. @@ -122,6 +150,15 @@ class PluginSection { hook(hook) { return this.plugins .filter(({module}) => hook in module) + .filter((plugin) => { + + // Validate the hook. + if (hook in hookSchemas) { + Joi.assert(plugin.module[hook], hookSchemas[hook], `Plugin '${plugin.name}' failed validation for the '${hook}' hook`); + } + + return true; + }) .map((plugin) => ({ plugin, [hook]: plugin.module[hook] diff --git a/routes/assets/index.js b/routes/assets/index.js index e8c508b52..54d4e72ed 100644 --- a/routes/assets/index.js +++ b/routes/assets/index.js @@ -14,6 +14,7 @@ router.get('/id/:asset_id', (req, res, next) => { } res.render('article', { title: asset.title, + asset_id: asset.id, asset_url: asset.url, body: '', basePath: '/client/embed/stream' @@ -26,6 +27,7 @@ router.get('/title/:asset_title', (req, res) => { return res.render('article', { title: req.params.asset_title.split('-').join(' '), asset_url: '', + asset_id: null, body: body, basePath: '/client/embed/stream' }); diff --git a/routes/index.js b/routes/index.js index 9e38097f9..448bd6964 100644 --- a/routes/index.js +++ b/routes/index.js @@ -18,6 +18,7 @@ if (process.env.NODE_ENV !== 'production') { return res.render('article', { title: 'Coral Talk', asset_url: '', + asset_id: '', body: '', basePath: '/client/embed/stream' }); diff --git a/views/article.ejs b/views/article.ejs index 5f90cdf4d..e20f91117 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -27,7 +27,8 @@ diff --git a/yarn.lock b/yarn.lock index 837cec14e..c7863eacd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3626,6 +3626,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.1.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.1.1.tgz#9cc573ffba2b7b408fb5e9c2a13796be94cddce9" + hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" @@ -4156,6 +4160,10 @@ isemail@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" +isemail@2.x.x: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6" + isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" @@ -4254,6 +4262,10 @@ istanbul@^1.1.0-alpha.1: which "^1.1.1" wordwrap "^1.0.0" +items@2.x.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" + iterall@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.2.tgz#41a2e96ce9eda5e61c767ee5dc312373bb046e91" @@ -4275,6 +4287,15 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" +joi@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-10.4.1.tgz#a2fca1f0d603d1b843f2c1e086b52461f6be1f36" + dependencies: + hoek "4.x.x" + isemail "2.x.x" + items "2.x.x" + topo "2.x.x" + joi@^6.10.1: version "6.10.1" resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" @@ -7731,6 +7752,12 @@ topo@1.x.x: dependencies: hoek "2.x.x" +topo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" + dependencies: + hoek "4.x.x" + touch@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de"