You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
9.7 KiB

// 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 } = 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, options) {
options = Object.assign({ updateCache: true }, options);
this.log.info('deleting page', { pageId: page._id, options });
await Page.deleteOne({ _id: page._id });
if (options.updateCache) {
await this.cacheMainMenuPages();
}
}
async removeForAuthor (author) {
/*
* Execute the updates without page cache updates
*/
await Page
.find({ author: author._id })
.cursor()
.eachAsync(async (page) => {
try {
await this.deletePage(page, { updateCache: false });
} catch (error) {
this.log.error('failed to remove page for author', { error });
// fall through
}
});
/*
* and update the page cache once, instead.
*/
await this.cacheMainMenuPages();
}
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 ( ) {
let mainMenu = [];
try {
await Page
.find({ status: 'published' })
.select('slug menu')
.populate({path: 'menu.parent'})
.lean()
.cursor()
.eachAsync(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);
}
}
});
/*
* Sort the menu data
*/
mainMenu.sort((a, b) => a.order - b.order);
for (const menu of mainMenu) {
if (menu.children) {
menu.children.sort((a, b) => a.order - b.order);
}
}
/*
* Update the cache
*/
await this.dtp.services.cache.setObject("mainMenu", mainMenu);
} catch (error) {
this.dtp.log.error('failed to build page menu', { error });
}
}
async getMainMenuPages() {
return this.dtp.services.cache.getObject("mainMenu");
}
}
module.exports = {
logId: 'svc:page',
index: 'page',
className: 'PageService',
create: (dtp) => { return new PageService(dtp); },
};