UNPKG

sheercms

Version:

Sheer Cliff CMS is a simple and powerful content management system (CMS) for Node JS.

1,074 lines (998 loc) 48.5 kB
var logger = require('../logger'); var models = require('./packages-models'); var api = require('../api'); var mongoose = require('mongoose'); var S = require('string'); var util = require('../utilities'); var async = require('async'); var NodeZip = new require('node-zip'); var fs = require('fs'); var path_mod = require('path'); var config = require("../config"); var cms = require('sheercms'); /* callback: (err, package) */ module.exports.createPackage = function(name, callback) { try { var p = new models.Package({ name: name, filename: util.formatSafeName(name), overwrite: 'default', created: new Date(), items: null }); p.save(function (err) { if (err) { logger.error(err); if (callback) callback(err); } else { logger.info("Package created: " + p._id); if (callback) callback(null, mapPackage(p)); } }); } catch(e) { logger.error(e); if (callback) callback(e); } }; /* callback: (err, package) */ module.exports.updatePackage = function(pkg, callback) { try { var id = util.toObjectId(pkg._id); models.Package.findById(id, function (err, result) { try { if (err) { logger.error(err); if (callback) callback(err); } else if (result === null) { var e = new Error("Package not found: " + pkg._id); logger.error(e); if (callback) callback(e); } else { //update the properties result.name = pkg.name; result.description = pkg.description; result.overwrite = pkg.overwrite; result.filename = pkg.filename; result.modified = new Date(); result.items = pkg.items; result.markModified('items'); result.markModified('settings'); result.save(function (err) { if (err) { logger.error(err); if (callback) callback(err); } else { logger.info("Package updated: " + result._id); if (callback) callback(null, mapPackage(result)); } }); } } catch(e) { logger.error(e); if (callback) callback(e); } }); } catch(e) { logger.error(e); if (callback) callback(e); } }; /* callback: (err) */ module.exports.deletePackage = function(pkgId, callback) { try { var id = util.toObjectId(pkgId); models.Package.findByIdAndRemove(id, function (err) { if (err) { logger.error(err); if (callback) callback(err); } else { if (callback) callback(null); } }); } catch(e) { logger.error(e); if (callback) callback(e); } }; /* callback: (err, package) */ module.exports.getPackage = function(pkgId, callback) { try { if (callback) { var id = util.toObjectId(pkgId); models.Package.findOne({_id: id}).lean().exec(function (err, result) { if (err) { logger.error(err); callback(err); } else callback(null, result); }); } } catch(e) { logger.error(e); if (callback) callback(e); } }; /* callback: (err, packages) */ module.exports.getPackages = function(callback) { try { if (callback) { models.Package.find().select("name created modified description").sort("name").lean().exec(function (err, result) { if (err) { logger.error(err); callback(err); } else callback(err, result); }); } } catch(e) { logger.error(e); if (callback) callback(e); } }; /* callback: (err, result) result: {valid, message, invalidItems} */ module.exports.validatePackage = function(pkgId, callback) { try { if (callback) { logger.info("Validating package: " + pkgId); module.exports.getPackage(pkgId, function (err, pkg) { if (err) { callback(err); } else { if (pkg && pkg.items && pkg.items.length > 0) { logger.debug("Checking each package item for existence."); //make sure that all of the items exist var missingItems = []; async.eachSeries(pkg.items, function (item, done) { try { logger.debug("Checking package item: " + item.itemId + " - " + item.name); if (item.itemType === 'content' || item.itemType === 'media') { api.content.getContent({contentId: item.itemId}, function (err, content) { if (err) done(err); else if (content) { logger.debug("Package item valid: " + item.itemId); done(); } else { //missing content item logger.warn("Package item missing: " + item.itemId + " - " + item.name); missingItems.push(item); done(); } }); } else if (item.itemType === 'group') { api.groups.getGroup(item.itemId, function(err, group) { if (err) done(err); else if (group) { logger.debug("Package item valid: " + item.itemId); done(); } else { //missing group logger.warn("Package item missing: " + item.itemId + " - " + item.name); missingItems.push(item); done(); } }); } else if (item.itemType === 'template') { api.templates.getTemplate(item.itemId, function (err, template) { if (err) done(err); else if (template) { logger.debug("Package item valid: " + item.itemId); done(); } else { //missing template logger.warn("Package item missing: " + item.itemId + " - " + item.name); missingItems.push(item); done(); } }); } else if (item.itemType === 'type') { api.contentTypes.getContentType(item.itemId, function(err, type) { if (err) done(err); else if (type) { logger.debug("Package item valid: " + item.itemId); done(); } else { //missing content type logger.warn("Package item missing: " + item.itemId + " - " + item.name); missingItems.push(item); done(); } }); } else { var e = new Error('Invalid item type: ' + item.itemType); logger.error(e); done(e); } } catch(e) { logger.error(e); done(e); } }, function(err) { if (err) { callback(err); } else { var result = { valid: missingItems.length == 0, message: missingItems.length == 0 ? 'This package is valid.' : 'This package is not valid.', invalidItems: missingItems }; callback(null, result); } }); } else if (pkg) { callback(null, {valid: true, message: 'This package is valid but does not contain any items.'}); } else { var msg = 'The package was not found: ' + pkgId; var e = new Error(msg); logger.error(e); callback(e); } } }); } } catch(e) { logger.error(e); if (callback) callback(e); } }; /* callback: (err, result) */ module.exports.buildPackage = function(pkgId, callback) { try { module.exports.getPackage(pkgId, function(err, pkg) { if (err) { if (callback) callback(err); } else { if (pkg && pkg.items && pkg.items.length > 0) { createPackageZip(pkg, function (err, result) { if (callback) callback(err, result); }); } else { var msg = pkg ? 'This package does not contain items.' : 'The package was not found.'; var e = new Error(msg); logger.error(e); if (callback) callback(e); } } }); } catch(e) { logger.error(e); if (callback) callback(e); } }; module.exports.getPackageSettings = function() { var options = config.settings.packages; if (options.path && options.path.indexOf("~/") == 0) { options.path = path_mod.join(cms.getRoot(), options.path.substr(2)); } return options; }; /* options: {name, path, username} onProgress: (value, total) callback: (err, package) statuses: 0 = none, 1 = success, 2 = skipped, 3 = error */ module.exports.installPackage = function(options, onProgress, callback) { try { logger.info("Installing package: " + options.name); //first copy the file to the packages folder var settings = module.exports.getPackageSettings(); var newPath = path_mod.join(settings.path, options.name); logger.debug("Moving uploaded file to packages folder: " + newPath); fs.rename(options.path, newPath, function (err) { if (err) { logger.error(err); if (callback) callback(err); } else { try { logger.debug("Reading the package zip file: " + newPath); //read the zip file fs.readFile(newPath, function (err, data) { if (err) { logger.error(err); if (callback) callback(err); } else { try { logger.debug("Unzipping the package: " + options.name); //unzip the package var zipOptions = { base64: false, //binary checkCRC32: false }; var zip = new NodeZip(data, zipOptions); //get the package file logger.debug("Reading the package.json file from: " + options.name); var pkgJson = zip.file('package.json').asText(); if (!pkgJson) throw new Error("The package.json file was not found in the package. Package=" + newPath); var pkg = JSON.parse(pkgJson); if (!pkg || !pkg.items) throw new Error("The package json is null or does not contain items. Package=" + newPath); if (onProgress) onProgress(new InstallProgress(pkg, null, 0, pkg.items.length, 0, 'Package opened')); var count = 0; //install the items in a certain order: templates, content types, groups, items var pkgItems = { templates: [], types: [], groups: [], items: [] }; for(var i = 0; i < pkg.items.length; i++) { var p = pkg.items[i]; if (p.itemType === 'content' || p.itemType === 'media') pkgItems.items.push(p); else if (p.itemType === 'group') pkgItems.groups.push(p); else if (p.itemType === 'template') pkgItems.templates.push(p); else if (p.itemType === 'type') pkgItems.types.push(p); else { var e = new Error("Invalid package item type. Package=" + pkg.name + "; ItemType=" + p.itemType + "; Item=" + p.itemId); logger.error(e); } } logger.debug("Begin processing package items from: " + options.name); async.series([ //TEMPLATES function(complete){ async.eachSeries(pkgItems.templates, function (item, done) { try { logger.info("Installing TEMPLATE=" + item.itemId); count++; var itemObj = getPackageZipEntry(zip, item); if (itemObj) { installTemplate(zip, pkg, options, item, itemObj, count, onProgress, done); } else { done(new Error('Error processing package entry: ' + item.itemId)); } } catch(e) { logger.error(e); done(e); } }, function(err) { //reload the templates api.templates.loadTemplates(function(err) { complete(err); }); }); }, //CONTENT TYPES function(complete){ async.eachSeries(pkgItems.types, function (item, done) { try { logger.info("Installing CONTENT TYPE=" + item.itemId); count++; var itemObj = getPackageZipEntry(zip, item); if (itemObj) { installContentType(zip, pkg, options, item, itemObj, count, onProgress, done); } else { done(new Error('Error processing package entry: ' + item.itemId)); } } catch(e) { logger.error(e); done(e); } }, function(err) { if (err) { complete(err); } else { //reload the content types api.contentTypes.loadContentTypes(function (err) { complete(err); }); } }); }, //ITEM GROUPS function(complete){ async.eachSeries(pkgItems.groups, function (item, done) { try { logger.info("Installing ITEM GROUP=" + item.itemId); count++; var itemObj = getPackageZipEntry(zip, item); if (itemObj) { installGroup(zip, pkg, options, item, itemObj, count, onProgress, done); } else { done(new Error('Error processing package entry: ' + item.itemId)); } } catch(e) { logger.error(e); done(e); } }, function(err) { complete(err); }); }, //CONTENT ITEMS function(complete){ async.eachSeries(pkgItems.items, function (item, done) { try { logger.info("Installing CONTENT ITEM=" + item.itemId); count++; var itemObj = getPackageZipEntry(zip, item); if (itemObj) { installContentItem(zip, pkg, options, item, itemObj, count, onProgress, done); } else { done(new Error('Error processing package entry: ' + item.itemId)); } } catch(e) { logger.error(e); done(e); } }, function(err) { complete(err); }); } ], //callback after all items have been processed function(err){ //clean up tasks async.series([ //RECALCULATE ALL GROUP COUNTS function(complete) { api.groups.recalculateAllGroups(function(err) { complete(err); }) }, //CLEAR CACHE function(complete) { api.cache.removeAll(null, function(err) { complete(err); }); } ], //end of clean up function(err) { callback(err); }); }); } catch(e) { logger.error(e); if (callback) callback(e); } } }); } catch(e) { logger.error(e); if (callback) callback(e); } } }); } catch(e) { logger.error(e); if (callback) callback(e); } }; //callback: err, itemObj function getPackageZipEntry(zip, item) { try { //get the json file for this item from the zip file var entryName = item.itemType + '/' + item.itemId + '.json'; logger.debug("Reading package entry: " + entryName); var itemJson = zip.file(entryName).asText(); if (!itemJson) throw new Error('Could not find item JSON in package at: ' + entryName); logger.debug("Parsing package entry to JSON object: " + entryName); var itemObj = JSON.parse(itemJson); if (!itemObj) throw new Error('Could not parse the item JSON in package at: ' + entryName); return itemObj; } catch(e) { logger.error(e); return null; } } function installContentType(zip, pkg, options, item, itemObj, count, onProgress, done) { //validate that the type id matches if (item.itemId != itemObj.id) throw new Error('The item JSON does not match the item settings in the package.'); //see if this content type already exists... logger.debug("Looking for existing content type with id: " + item.itemId); api.contentTypes.getContentType(item.itemId, function(err, type) { try { if (err) { done(err); } else if (!type || item.overwrite === 'yes' || (item.overwrite === 'default' && (pkg.overwrite === 'yes' || pkg.overwrite === 'default'))) { //import the item type api.contentTypes.importContentType(itemObj, options.username, false, function (err, savedType) { try { if (err) { done(err); } else { var msg = "Package item successfully installed: " + item.itemId + " - " + savedType.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } } catch(e) { logger.error(e); done(e); } }); } else { logger.info("Skipping package item because it already exists and overwrite is false: " + item.itemId + " - " + content.name); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 2, 'Item skipped')); //2 = skip done(); } } catch(e) { logger.error(e); done(e); } }); } function installTemplate(zip, pkg, options, item, itemObj, count, onProgress, done) { //validate that the template id matches if (item.itemId != itemObj.id) throw new Error('The item JSON does not match the item settings in the package.'); //see if this template already exists... logger.debug("Looking for existing template with id: " + item.itemId); api.templates.getTemplate(item.itemId, function(err, template) { try { if (err) { done(err); } else if (!template || item.overwrite === 'yes' || (item.overwrite === 'default' && (pkg.overwrite === 'yes' || pkg.overwrite === 'default'))) { //import the template api.templates.importTemplate(itemObj, options.username, false, function (err, savedTemplate) { try { if (err) { done(err); } else { var msg = "Package item successfully installed: " + item.itemId + " - " + savedTemplate.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } } catch(e) { logger.error(e); done(e); } }); } else { logger.info("Skipping package item because it already exists and overwrite is false: " + item.itemId + " - " + content.name); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 2, 'Item skipped')); //2 = skip done(); } } catch(e) { logger.error(e); done(e); } }); } function installGroup(zip, pkg, options, item, itemObj, count, onProgress, done) { //validate that the id matches if (item.itemId != itemObj._id) throw new Error('The item JSON does not match the item settings in the package.'); //see if this group already exists... logger.debug("Looking for existing group with id: " + item.itemId); api.groups.getGroup(item.itemId, function(err, group) { try { if (err) { done(err); } else if (!group || item.overwrite === 'yes' || (item.overwrite === 'default' && (pkg.overwrite === 'yes' || pkg.overwrite === 'default'))) { //import the group api.groups.importGroup(itemObj, function (err, savedGroup) { try { if (err) { done(err); } else { var msg = "Package item successfully installed: " + item.itemId + " - " + savedGroup.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } } catch(e) { logger.error(e); done(e); } }); } else { logger.info("Skipping package item because it already exists and overwrite is false: " + item.itemId + " - " + content.name); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 2, 'Item skipped')); //2 = skip done(); } } catch(e) { logger.error(e); done(e); } }); } function installContentItem(zip, pkg, options, item, itemObj, count, onProgress, done) { logger.debug("Content or media item found. Validating the content id..."); //validate that the contentId matches if (item.itemId != itemObj.contentId) throw new Error('The item JSON does not match the item settings in the package.'); //first see if this item exists already logger.debug("Looking for existing content item with contentId: " + item.itemId); api.content.getContent({contentId: item.itemId}, function (err, content) { try { if (err) done(err); else { if (!content || item.overwrite === 'yes' || (item.overwrite === 'default' && pkg.overwrite === 'yes')) { api.content.importContent(itemObj, function (err, savedContent) { try { if (err) { logger.error("Error installing package item: " + item.itemId); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 3, err.message)); //3 = error done(err); } else { if (item.itemType === 'media' && item.fileEntry && item.fileEntry.length > 0) { //extract the file entry and write to the temp directory var fileBuffer = zip.file(item.fileEntry).asNodeBuffer(); var filename = path_mod.join(util.getTempDir(), path_mod.basename(item.fileEntry)); fs.writeFile(filename, fileBuffer, function (err) { try { if (err) { logger.error(err); done(err); } else { //get the media size fs.stat(filename, function (err, stats) { if (err) { logger.error(err); done(err); } else { api.content.updateMedia({contentId: item.itemId, path: filename, size: stats.size, username: options.username}, function (err) { try { if (err) { done(err); } else { if (item.publish === true) { logger.debug('Publishing package item: ' + item.itemId); api.content.publishContent(item.itemId, options.username, function(err) { if (err) { logger.error("Error publishing package item: " + item.itemId); done(err); } else { //installed the non-media item var msg = "Package item successfully installed: " + item.itemId + " - " + savedContent.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } }); } else { //installed the non-media item var msg = "Package item successfully installed: " + item.itemId + " - " + savedContent.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } } } catch(e) { logger.error(e); done(e); } }); } }); } } catch(e) { logger.error(e); done(e); } }); } else { if (item.publish === true) { logger.debug('Publishing package item: ' + item.itemId); api.content.publishContent(item.itemId, options.username, function(err) { if (err) { logger.error("Error publishing package item: " + item.itemId); done(err); } else { //installed the non-media item var msg = "Package item successfully installed: " + item.itemId + " - " + savedContent.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } }); } else { //installed the non-media item var msg = "Package item successfully installed: " + item.itemId + " - " + savedContent.name; logger.info(msg); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 1, msg)); //1 = success done(); } } } } catch (e) { logger.error(e); done(e); } }); } else { logger.info("Skipping package item because it already exists and overwrite is false: " + item.itemId + " - " + content.name); if (onProgress) onProgress(new InstallProgress(pkg, item, count, pkg.items.length, 2, 'Item skipped')); //2 = skip done(); } } } catch(e) { logger.error(e); done(e); } }); } function mapPackage(pkg) { var p = { id: pkg._id.toString(), _id: pkg._id.toString(), name: pkg.name, description: pkg.description, filename: pkg.filename, created: pkg.created, modified: pkg.modified, items: pkg.items, overwrite: pkg.overwrite }; return p; } function createPackageZip(pkg, callback) { try { var zip = new NodeZip(); var settings = module.exports.getPackageSettings(); //start processing each item async.eachSeries(pkg.items, function (key, done) { logger.info("Adding item to package zip: Package=" + pkg.name + "; Type=" + key.itemType + "; Item=" + key.itemId); if (key.itemType === 'content' || key.itemType === 'media') { api.content.getContent({contentId: key.itemId}, function(err, content) { try { if (err) { var e = new Error('Error adding the item to the package zip. Package=' + pkg.name + '; Item=' + key.itemId + '; Error=' + err); logger.error(e); done(e); } else if (content) { var entryName = key.itemType + '/' + content.contentId + '.json'; logger.info('Adding package zip entry: ' + entryName); var json = JSON.stringify(content); zip.file(entryName, json); if (key.itemType === 'media') { //need to zip the file too api.content.readMedia(content, function (err, data) { if (err) done(err); else { //get the file extension var ext = path_mod.extname(content.filePath); ext = ext ? ext.replace('.', '') : ''; entryName = 'media/files/' + content.contentId + '.' + ext; logger.info('Adding package zip entry: ' + entryName); zip.file(entryName, data); //tell the package item that it has an associated file key.fileEntry = entryName; done(); } }); } else done(); } else { //content item doesn't exist anymore var msg = 'Content item not found: ' + key.name + ' (' + key.itemId + ')'; logger.warn(msg); var e = new Error(msg); done(e); } } catch(e) { logger.error(e); done(e); } }); } else if (key.itemType === 'group') { api.groups.getGroup(key.itemId, function(err, group) { if (err) { var e = new Error('Error adding the group to the package zip. Package=' + pkg.name + '; Group=' + key.itemId + '; Error=' + err); logger.error(e); done(e); } else if (group) { var entryName = key.itemType + '/' + group._id + '.json'; logger.info('Adding package zip entry: ' + entryName); var json = JSON.stringify(group); zip.file(entryName, json); done(); } else { //missing group var msg = 'Group not found: ' + key.name + ' (' + key.itemId + ')'; var e = new Error(msg); logger.warn(msg); done(e); } }); } else if (key.itemType === 'template') { api.templates.getTemplate(key.itemId, function(err, template) { if (err) { var e = new Error('Error adding the template to the package zip. Package=' + pkg.name + '; Template=' + key.itemId + '; Error=' + err); logger.error(e); done(e); } else if (template) { var entryName = key.itemType + '/' + template.id + '.json'; logger.info('Adding package zip entry: ' + entryName); var json = JSON.stringify(template); zip.file(entryName, json); done(); } else { var msg = 'Template not found: ' + key.name + ' (' + key.itemId + ')'; var e = new Error(msg); logger.warn(msg); done(e); } }); } else if (key.itemType === 'type') { api.contentTypes.getContentType(key.itemId, function(err, type) { if (err) done(err); else if (type) { var entryName = key.itemType + '/' + type.id + '.json'; logger.info('Adding package zip entry: ' + entryName); var json = JSON.stringify(type); zip.file(entryName, json); done(); } else { var msg = 'Content Type not found: ' + key.name + ' (' + key.itemId + ')'; var e = new Error(msg); logger.warn(msg); done(e); } }); } else { var e = new Error("Item type not supported in package zip: " + key.itemType); logger.error(e); done(e); } }, function (err) { if (err) { callback(err); } else { //add the package JSON to the zip logger.info('Adding package JSON to zip file: ' + pkg.name); zip.file('package.json', JSON.stringify(pkg)); //now save the file to the packages folder var filename = (pkg.filename && pkg.filename.length > 0) ? pkg.filename : util.formatSafeName(pkg.name); var path = path_mod.join(settings.path, filename.replace('.zip', '') + '.zip'); var options = { type: 'nodebuffer', compression: 'DEFLATE', comment: pkg.description }; var buffer = zip.generate(options); fs.writeFile(path, buffer, function(err) { if (err) { logger.error(err); callback(err); } else { logger.info('Package zip created successfully: ' + pkg.name); var result = { success: true, path: path, package: pkg }; callback(null, result); } }); } }); } catch(e) { logger.error(e); callback(e); } } function InstallProgress(pkg, item, value, total, status, msg) { var self = this; self.package = pkg; self.item = item; self.value = value; self.total = total; self.status = status; self.message = msg; }