From 13a995133bbfa93f29dc01294694b2e5a404a8b8 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 1 Mar 2020 18:22:26 -0500 Subject: [PATCH] feat: branch off / create from template --- client/components/editor.vue | 2 +- client/components/editor/editor-markdown.vue | 2 +- client/components/history.vue | 19 +++++- server/controllers/common.js | 62 +++++++++++++++++++- server/graph/resolvers/page.js | 49 ++++++++++++---- server/helpers/error.js | 8 +++ server/models/pageHistory.js | 21 +++++-- 7 files changed, 142 insertions(+), 21 deletions(-) diff --git a/client/components/editor.vue b/client/components/editor.vue index 837f5b5c..3e9497e3 100644 --- a/client/components/editor.vue +++ b/client/components/editor.vue @@ -171,7 +171,7 @@ export default { this.initContentParsed = this.initContent ? Base64.decode(this.initContent) : '' this.$store.set('editor/content', this.initContentParsed) - if (this.mode === 'create') { + if (this.mode === 'create' && !this.initEditor) { _.delay(() => { this.dialogEditorSelector = true }, 500) diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index 965f618d..edda7793 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -474,7 +474,7 @@ export default { mounted() { this.$store.set('editor/editorKey', 'markdown') - if (this.mode === 'create') { + if (this.mode === 'create' && !this.$store.get('editor/content')) { this.$store.set('editor/content', '# Header\nYour content here') } diff --git a/client/components/history.vue b/client/components/history.vue index b22055f1..bdc5dbf9 100644 --- a/client/components/history.vue +++ b/client/components/history.vue @@ -122,6 +122,8 @@ v-btn(text, @click='isRestoreConfirmDialogShown = false', :disabled='restoreLoading') {{$t('common:actions.cancel')}} v-btn(color='orange darken-2', dark, @click='restoreConfirm', :loading='restoreLoading') {{$t('history:restore.confirmButton')}} + page-selector(mode='create', v-model='branchOffOpts.modal', :open-handler='branchOffHandle', :path='branchOffOpts.path', :locale='branchOffOpts.locale') + nav-footer notify search-results @@ -211,6 +213,12 @@ export default { versionId: 0, versionDate: '' }, + branchOffOpts: { + versionId: 0, + locale: 'en', + path: 'new-page', + modal: false + }, isRestoreConfirmDialogShown: false, restoreLoading: false } @@ -408,7 +416,16 @@ export default { this.restoreLoading = false }, branchOff (versionId) { - + const pathParts = this.path.split('/') + this.branchOffOpts = { + versionId: versionId, + locale: this.locale, + path: (pathParts.length > 1) ? _.initial(pathParts).join('/') + `/new-page` : `new-page`, + modal: true + } + }, + branchOffHandle ({ locale, path }) { + window.location.assign(`/e/${locale}/${path}?from=${this.pageId},${this.branchOffOpts.versionId}`) }, toggleViewMode () { this.viewMode = (this.viewMode === 'line-by-line') ? 'side-by-side' : 'line-by-line' diff --git a/server/controllers/common.js b/server/controllers/common.js index 4f822a75..94805ed8 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -5,6 +5,8 @@ const _ = require('lodash') /* global WIKI */ +const tmplCreateRegex = /^[0-9]+(,[0-9]+)?$/ + /** * Robots.txt */ @@ -89,13 +91,16 @@ router.get(['/e', '/e/*'], async (req, res, next) => { return res.redirect(`/e/${pageArgs.locale}/${pageArgs.path}`) } + // -> Set Editor Lang _.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') + // -> Check for reserved path if (pageHelper.isReservedPath(pageArgs.path)) { return next(new Error('Cannot create this page because it starts with a system reserved path.')) } + // -> Get page data from DB let page = await WIKI.models.pages.getPageFromDb({ path: pageArgs.path, locale: pageArgs.locale, @@ -112,11 +117,13 @@ router.get(['/e', '/e/*'], async (req, res, next) => { } if (page) { + // -> EDIT MODE if (!WIKI.auth.checkAccess(req.user, ['write:pages', 'manage:pages'], pageArgs)) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.render('unauthorized', { action: 'edit' }) } + // -> Get page tags await page.$relatedQuery('tags') page.tags = _.map(page.tags, 'tag') @@ -126,6 +133,7 @@ router.get(['/e', '/e/*'], async (req, res, next) => { page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false' page.content = Buffer.from(page.content).toString('base64') } else { + // -> CREATE MODE if (!WIKI.auth.checkAccess(req.user, ['write:pages'], pageArgs)) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.render('unauthorized', { action: 'create' }) @@ -137,7 +145,54 @@ router.get(['/e', '/e/*'], async (req, res, next) => { localeCode: pageArgs.locale, editorKey: null, mode: 'create', - content: null + content: null, + title: null, + description: null + } + + // -> From Template + if (req.query.from && tmplCreateRegex.test(req.query.from)) { + let tmplPageId = 0 + let tmplVersionId = 0 + if (req.query.from.indexOf(',')) { + const q = req.query.from.split(',') + tmplPageId = _.toSafeInteger(q[0]) + tmplVersionId = _.toSafeInteger(q[1]) + } else { + tmplPageId = _.toSafeInteger(req.query.from) + } + + if (tmplVersionId > 0) { + // -> From Page Version + const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: tmplPageId, versionId: tmplVersionId }) + if (!pageVersion) { + _.set(res.locals, 'pageMeta.title', 'Page Not Found') + return res.status(404).render('notfound', { action: 'template' }) + } + if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) { + _.set(res.locals, 'pageMeta.title', 'Unauthorized') + return res.render('unauthorized', { action: 'sourceVersion' }) + } + page.content = Buffer.from(pageVersion.content).toString('base64') + page.editorKey = pageVersion.editor + page.title = pageVersion.title + page.description = pageVersion.description + } else { + // -> From Page Live + const pageOriginal = await WIKI.models.pages.query().findById(tmplPageId) + if (!pageOriginal) { + _.set(res.locals, 'pageMeta.title', 'Page Not Found') + return res.status(404).render('notfound', { action: 'template' }) + } + if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) { + _.set(res.locals, 'pageMeta.title', 'Unauthorized') + return res.render('unauthorized', { action: 'source' }) + } + page.content = Buffer.from(pageOriginal.content).toString('base64') + page.editorKey = pageOriginal.editorKey + page.title = pageOriginal.title + page.description = pageOriginal.description + } } } res.render('editor', { page, injectCode }) @@ -163,6 +218,11 @@ router.get(['/h', '/h/*'], async (req, res, next) => { isPrivate: false }) + if (!page) { + _.set(res.locals, 'pageMeta.title', 'Page Not Found') + return res.status(404).render('notfound', { action: 'history' }) + } + pageArgs.tags = _.get(page, 'tags', []) if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) { diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index 7052d76d..9be541ed 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -15,20 +15,36 @@ module.exports = { * PAGE HISTORY */ async history(obj, args, context, info) { - return WIKI.models.pageHistory.getHistory({ - pageId: args.id, - offsetPage: args.offsetPage || 0, - offsetSize: args.offsetSize || 100 - }) + const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.id) + if (WIKI.auth.checkAccess(context.req.user, ['read:history'], { + path: page.path, + locale: page.localeCode + })) { + return WIKI.models.pageHistory.getHistory({ + pageId: args.id, + offsetPage: args.offsetPage || 0, + offsetSize: args.offsetSize || 100 + }) + } else { + throw new WIKI.Error.PageHistoryForbidden() + } }, /** * PAGE VERSION */ async version(obj, args, context, info) { - return WIKI.models.pageHistory.getVersion({ - pageId: args.pageId, - versionId: args.versionId - }) + const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId) + if (WIKI.auth.checkAccess(context.req.user, ['read:history'], { + path: page.path, + locale: page.localeCode + })) { + return WIKI.models.pageHistory.getVersion({ + pageId: args.pageId, + versionId: args.versionId + }) + } else { + throw new WIKI.Error.PageHistoryForbidden() + } }, /** * SEARCH PAGES @@ -123,10 +139,17 @@ module.exports = { async single (obj, args, context, info) { let page = await WIKI.models.pages.getPageFromDb(args.id) if (page) { - return { - ...page, - locale: page.localeCode, - editor: page.editorKey + if (WIKI.auth.checkAccess(context.req.user, ['read:history'], { + path: page.path, + locale: page.localeCode + })) { + return { + ...page, + locale: page.localeCode, + editor: page.editorKey + } + } else { + throw new WIKI.Error.PageViewForbidden() } } else { throw new WIKI.Error.PageNotFound() diff --git a/server/helpers/error.js b/server/helpers/error.js index c7da98fa..c5a466ff 100644 --- a/server/helpers/error.js +++ b/server/helpers/error.js @@ -137,6 +137,10 @@ module.exports = { message: 'Page content cannot be empty.', code: 6004 }), + PageHistoryForbidden: CustomError('PageHistoryForbidden', { + message: 'You are not authorized to view the history of this page.', + code: 6012 + }), PageIllegalPath: CustomError('PageIllegalPath', { message: 'Page path cannot contains illegal characters.', code: 6005 @@ -161,6 +165,10 @@ module.exports = { message: 'You are not authorized to update this page.', code: 6009 }), + PageViewForbidden: CustomError('PageViewForbidden', { + message: 'You are not authorized to view this page.', + code: 6013 + }), SearchActivationFailed: CustomError('SearchActivationFailed', { message: 'Search Engine activation failed.', code: 4002 diff --git a/server/models/pageHistory.js b/server/models/pageHistory.js index c9d2f3ae..1e7e9654 100644 --- a/server/models/pageHistory.js +++ b/server/models/pageHistory.js @@ -84,6 +84,9 @@ module.exports = class PageHistory extends Model { this.createdAt = new Date().toISOString() } + /** + * Create Page Version + */ static async addVersion(opts) { await WIKI.models.pageHistory.query().insert({ pageId: opts.id, @@ -105,6 +108,9 @@ module.exports = class PageHistory extends Model { }) } + /** + * Get Page Version + */ static async getVersion({ pageId, versionId }) { const version = await WIKI.models.pageHistory.query() .column([ @@ -134,13 +140,20 @@ module.exports = class PageHistory extends Model { 'pageHistory.id': versionId, 'pageHistory.pageId': pageId }).first() - return { - ...version, - updatedAt: version.createdAt, - tags: [] + if (version) { + return { + ...version, + updatedAt: version.createdAt || null, + tags: [] + } + } else { + return null } } + /** + * Get History Trail of a Page + */ static async getHistory({ pageId, offsetPage = 0, offsetSize = 100 }) { const history = await WIKI.models.pageHistory.query() .column([