diff --git a/client/components/admin/admin-storage.vue b/client/components/admin/admin-storage.vue index af711e1f..236f29ea 100644 --- a/client/components/admin/admin-storage.vue +++ b/client/components/admin/admin-storage.vue @@ -66,8 +66,9 @@ v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}} v-list-item-action v-menu - v-btn(slot='activator', icon) - v-icon(color='red') mdi-information + template(v-slot:activator='{ on }') + v-btn(icon, v-on='on') + v-icon(color='red') mdi-information v-card(width='450') v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}} v-card-text {{tgt.message}} diff --git a/client/components/common/nav-header.vue b/client/components/common/nav-header.vue index e7cacf11..df3ef503 100644 --- a/client/components/common/nav-header.vue +++ b/client/components/common/nav-header.vue @@ -107,7 +107,7 @@ //- PAGE ACTIONS - template(v-if='isAuthenticated && path') + template(v-if='isAuthenticated && path && mode !== `edit`') v-menu(offset-y, bottom, nudge-bottom='30', transition='slide-y-transition') template(v-slot:activator='{ on: menu }') v-tooltip(bottom) diff --git a/server/models/pages.js b/server/models/pages.js index 27cd8bd6..929589e7 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -114,6 +114,9 @@ module.exports = class Page extends Model { this.updatedAt = new Date().toISOString() } + /** + * Cache Schema + */ static get cacheSchema() { return new JSBinType({ id: 'uint', @@ -142,6 +145,8 @@ module.exports = class Page extends Model { /** * Inject page metadata into contents + * + * @returns {string} Page Contents with Injected Metadata */ injectMetadata () { return pageHelper.injectPageMetadata(this) @@ -149,6 +154,8 @@ module.exports = class Page extends Model { /** * Get the page's file extension based on content type + * + * @returns {string} File Extension */ getFileExtension() { switch (this.contentType) { @@ -166,6 +173,7 @@ module.exports = class Page extends Model { * * @param {String} raw Raw file contents * @param {String} contentType Content Type + * @returns {Object} Parsed Page Metadata with Raw Content */ static parseMetadata (raw, contentType) { let result @@ -204,6 +212,12 @@ module.exports = class Page extends Model { } } + /** + * Create a New Page + * + * @param {Object} opts Page Properties + * @returns {Promise} Promise of the Page Model Instance + */ static async createPage(opts) { const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first() if (dupCheck) { @@ -259,9 +273,22 @@ module.exports = class Page extends Model { }) } + // -> Reconnect Links + await WIKI.models.pages.reconnectLinks({ + locale: page.localeCode, + path: page.path, + mode: 'create' + }) + return page } + /** + * Update an Existing Page + * + * @param {Object} opts Page Properties + * @returns {Promise} Promise of the Page Model Instance + */ static async updatePage(opts) { const ogPage = await WIKI.models.pages.query().findById(opts.id) if (!ogPage) { @@ -314,6 +341,12 @@ module.exports = class Page extends Model { return page } + /** + * Delete an Existing Page + * + * @param {Object} opts Page Properties + * @returns {Promise} Promise with no value + */ static async deletePage(opts) { let page if (_.has(opts, 'id')) { @@ -344,8 +377,98 @@ module.exports = class Page extends Model { page }) } + + // -> Reconnect Links + await WIKI.models.pages.reconnectLinks({ + locale: page.localeCode, + path: page.path, + mode: 'delete' + }) } + /** + * Reconnect links to new/updated/deleted page + * + * @param {Object} opts - Page parameters + * @param {string} opts.path - Page Path + * @param {string} opts.locale - Page Locale Code + * @param {string} [opts.sourcePath] - Previous Page Path (move only) + * @param {string} [opts.sourceLocale] - Previous Page Locale Code (move only) + * @param {string} opts.mode - Page Update mode (new, move, delete) + * @returns {Promise} Promise with no value + */ + static async reconnectLinks (opts) { + const pageHref = `/${opts.locale}/${opts.path}` + let replaceArgs = { + from: '', + to: '' + } + switch (opts.mode) { + case 'create': + replaceArgs.from = `` + replaceArgs.to = `` + break + case 'move': + const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}` + replaceArgs.from = `` + replaceArgs.to = `` + break + case 'delete': + replaceArgs.from = `` + replaceArgs.to = `` + break + default: + return false + } + + let affectedHashes = [] + // -> Perform replace and return affected page hashes (POSTGRES, MSSQL only) + if (WIKI.config.db.type === 'postgres' || WIKI.config.db.type === 'mssql') { + affectedHashes = await WIKI.models.pages.query() + .returning('hash') + .patch({ + render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to]) + }) + .whereIn('pages.id', function () { + this.select('pageLinks.pageId').from('pageLinks').where({ + 'pageLinks.path': opts.path, + 'pageLinks.localeCode': opts.locale + }) + }) + .pluck('hash') + } else { + // -> Perform replace, then query affected page hashes (MYSQL, MARIADB, SQLITE only) + await WIKI.models.pages.query() + .patch({ + render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to]) + }) + .whereIn('pages.id', function () { + this.select('pageLinks.pageId').from('pageLinks').where({ + 'pageLinks.path': opts.path, + 'pageLinks.localeCode': opts.locale + }) + }) + affectedHashes = await WIKI.models.pages.query() + .column('hash') + .whereIn('pages.id', function () { + this.select('pageLinks.pageId').from('pageLinks').where({ + 'pageLinks.path': opts.path, + 'pageLinks.localeCode': opts.locale + }) + }) + .pluck('hash') + } + for (const hash of affectedHashes) { + await WIKI.models.pages.deletePageFromCache({ hash }) + } + } + + /** + * Trigger the rendering of a page + * + * @param {Object} page Page Model Instance + * @returns {Promise} Promise with no value + */ static async renderPage(page) { const renderJob = await WIKI.scheduler.registerJob({ name: 'render-page', @@ -355,6 +478,12 @@ module.exports = class Page extends Model { return renderJob.finished } + /** + * Fetch an Existing Page from Cache if possible, from DB otherwise and save render to Cache + * + * @param {Object} opts Page Properties + * @returns {Promise} Promise of the Page Model Instance + */ static async getPage(opts) { // -> Get from cache first let page = await WIKI.models.pages.getPageFromCache(opts) @@ -375,6 +504,12 @@ module.exports = class Page extends Model { return page } + /** + * Fetch an Existing Page from the Database + * + * @param {Object} opts Page Properties + * @returns {Promise} Promise of the Page Model Instance + */ static async getPageFromDb(opts) { const queryModeID = _.isNumber(opts) try { @@ -426,6 +561,12 @@ module.exports = class Page extends Model { } } + /** + * Save a Page Model Instance to Cache + * + * @param {Object} page Page Model Instance + * @returns {Promise} Promise with no value + */ static async savePageToCache(page) { const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`) await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({ @@ -448,6 +589,12 @@ module.exports = class Page extends Model { })) } + /** + * Fetch an Existing Page from Cache + * + * @param {Object} opts Page Properties + * @returns {Promise} Promise of the Page Model Instance + */ static async getPageFromCache(opts) { const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }) const cachePath = path.join(process.cwd(), `data/cache/${pageHash}.bin`) @@ -470,14 +617,32 @@ module.exports = class Page extends Model { } } + /** + * Delete an Existing Page from Cache + * + * @param {Object} page Page Model Instance + * @param {string} page.hash Hash of the Page + * @returns {Promise} Promise with no value + */ static async deletePageFromCache(page) { return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`)) } + /** + * Flush the contents of the Cache + */ static async flushCache() { return fs.emptyDir(path.join(process.cwd(), `data/cache`)) } + /** + * Migrate all pages from a source locale to the target locale + * + * @param {Object} opts Migration properties + * @param {string} opts.sourceLocale Source Locale Code + * @param {string} opts.targetLocale Target Locale Code + * @returns {Promise} Promise with no value + */ static async migrateToLocale({ sourceLocale, targetLocale }) { return WIKI.models.pages.query() .patch({ @@ -491,6 +656,12 @@ module.exports = class Page extends Model { }) } + /** + * Clean raw HTML from content for use in search engines + * + * @param {string} rawHTML Raw HTML + * @returns {string} Cleaned Content Text + */ static cleanHTML(rawHTML = '') { return striptags(rawHTML || '') .replace(emojiRegex(), '')