// page.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const striptags = require('striptags'); const slug = require('slug'); const { SiteService, SiteError, SiteAsync } = require('../../lib/site-lib'); const mongoose = require('mongoose'); const ObjectId = mongoose.Types.ObjectId; const Page = mongoose.model('Page'); class PageService extends SiteService { constructor (dtp) { super(dtp, module.exports); } async menuMiddleware (req, res, next) { try { let mainMenu = await this.getMainMenuPages(); if (!mainMenu) { await this.cacheMainMenuPages(); mainMenu = await this.getMainMenuPages(); } res.locals.mainMenu = mainMenu; return next(); } catch (error) { this.log.error('failed to build page menu', { error }); return next(); } } async createPlaceholder (author) { const NOW = new Date(); if (!author.flags.isAdmin) { throw new SiteError(403, 'You are not permitted to author pages'); } let page = new Page(); page.created = NOW; page.authorType = author.type; page.author = author._id; page.title = "New Draft page"; page.slug = `draft-page-${page._id}`; await page.save(); page = page.toObject(); page.author = author; // self-populate instead of calling db return page; } async create (author, pageDefinition) { if (!author.permissions.canAuthorPages) { throw new SiteError(403, 'You are not permitted to author pages'); } const page = new Page(); page.title = striptags(pageDefinition.title.trim()); page.slug = this.createPageSlug(page._id, page.title); page.content = pageDefinition.content.trim(); page.status = pageDefinition.status || 'draft'; page.menu = { icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()), label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))), order: parseInt(pageDefinition.menuOrder || '0', 10), }; if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) { page.menu.parent = pageDefinition.parentPageId; } await page.save(); await this.cacheMainMenuPages(); return page.toObject(); } async update (user, page, pageDefinition) { const NOW = new Date(); const updateOp = { $set: { updated: NOW, }, }; // if (!user.permissions.canAuthorPages) { // throw new SiteError(403, 'You are not permitted to author or change pages.'); // } if (pageDefinition.title) { updateOp.$set.title = striptags(pageDefinition.title.trim()); } if (pageDefinition.slug) { let pageSlug = striptags(slug(pageDefinition.slug.trim())).split('-'); while (ObjectId.isValid(pageSlug[pageSlug.length - 1])) { pageSlug.pop(); } pageSlug = pageSlug.splice(0, 4); pageSlug.push(page._id.toString()); updateOp.$set.slug = `${pageSlug.join('-')}`; } if (pageDefinition.summary) { updateOp.$set.summary = striptags(pageDefinition.summary.trim()); } if (pageDefinition.content) { updateOp.$set.content = pageDefinition.content.trim(); } updateOp.$set.menu = { icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()), label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))), order: parseInt(pageDefinition.menuOrder || '0', 10), }; if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) { updateOp.$set.menu.parent = pageDefinition.parentPageId; } // if old status is not published and new status is published, we have to // verify that the calling user has Publisher privileges. if ((page.status !== 'published') && (pageDefinition.status === 'published')) { if (!user.permissions.canPublishPages) { throw new SiteError(403, 'You are not permitted to publish pages'); } } updateOp.$set.status = striptags(pageDefinition.status.trim()); await Page.updateOne( { _id: page._id }, updateOp, { upsert: true }, ); await this.cacheMainMenuPages(); } async getPages (pagination, status = ['published']) { if (!Array.isArray(status)) { status = [status]; } const pages = await Page .find({ status: { $in: status } }) .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .lean(); return pages; } async getById (pageId, populateParent) { if (populateParent) { const page = await Page .findById(pageId) .select('+content +menu') .populate('menu.parent') .lean(); return page; } else { const page = await Page .findById(pageId) .select('+content') .lean(); return page; } } async getBySlug (pageSlug) { const slugParts = pageSlug.split('-'); const pageId = slugParts[slugParts.length - 1]; return this.getById(pageId); } async isParentPage (page) { if (page) { page = [page]; } const parentPage = await Page.distinct( 'menu.parent', { "menu.parent" : { $in : page } } ); return parentPage.length > 0; } async getAvailablePages (excludedPageIds) { let search = { }; if (excludedPageIds) { search._id = { $nin: excludedPageIds }; } const pages = (await Page.find(search).lean()).filter((page) => { if (page.menu && !page.menu.parent ) { return page; } }); return pages; } async deletePage (page) { this.log.info('deleting page', { pageId: page._id }); await Page.deleteOne({ _id: page._id }); } createPageSlug (pageId, pageTitle) { if ((typeof pageTitle !== 'string') || (pageTitle.length < 1)) { throw new Error('Invalid input for making a page slug'); } const pageSlug = slug(pageTitle.trim().toLowerCase()).split('-').slice(0, 4).join('-'); return `${pageSlug}-${pageId}`; } async cacheMainMenuPages () { try { const pages = await Page .find({ status: 'published' }) .select('slug menu') .populate({path: 'menu.parent'}) .lean(); let mainMenu = []; await SiteAsync.each(pages, async (page) => { if (page.menu.parent) { let parent = page.menu.parent; if (parent.status === 'published') { let parentPage = mainMenu.find(item => item.slug === parent.slug); if (parentPage) { let childPage = { url: `/page/${page.slug}`, slug: page.slug, icon: page.menu.icon, label: page.menu.label, order: page.menu.order, }; parentPage.children.splice(childPage.order, 0, childPage); } else { let parentPage = { url: `/page/${parent.slug}`, slug: parent.slug, icon: parent.menu.icon, label: parent.menu.label, order: parent.menu.order, children: [], }; let childPage = { url: `/page/${page.slug}`, slug: page.slug, icon: page.menu.icon, label: page.menu.label, order: page.menu.order, }; parentPage.children.splice(childPage.order, 0, childPage); mainMenu.splice(parentPage.order, 0, parentPage); } } else { let menuPage = { url: `/page/${page.slug}`, slug: page.slug, icon: page.menu.icon, label: page.menu.label, order: page.menu.order, children: [], }; mainMenu.splice(menuPage.order, 0, menuPage); } } else { let isPageInMenu = mainMenu.find(item => item.slug === page.slug); if (!isPageInMenu) { let menuPage = { url: `/page/${page.slug}`, slug: page.slug, icon: page.menu.icon, label: page.menu.label, order: page.menu.order, children: [], }; mainMenu.push(menuPage); } } }); mainMenu.sort((a, b) => a.order - b.order); await SiteAsync.each(mainMenu, async (menu) => { if (menu.children) { menu.children.sort((a, b) => a.order - b.order); } }); const deleteResponse = await this.dtp.services.cache.del("mainMenu"); this.dtp.log.info(deleteResponse); const storeResponse = await this.dtp.services.cache.setObject("mainMenu", mainMenu); this.dtp.log.info(storeResponse); // const getresp = await this.dtp.services.cache.getObject("mainMenu"); } catch (error) { this.dtp.log.error('failed to build page menu', { error }); } } async getMainMenuPages() { return this.dtp.services.cache.getObject("mainMenu"); } } module.exports = { slug: 'page', name: 'page', className: 'PageService', create: (dtp) => { return new PageService(dtp); }, };