VYPR
High severityNVD Advisory· Published Jul 26, 2025· Updated Jul 28, 2025

HAX CMS Backend Lacks Comprehensive Authorization Checks

CVE-2025-54378

Description

HAX CMS allows you to manage your microsite universe with PHP or NodeJs backends. In versions 11.0.13 and below of haxcms-nodejs and versions 11.0.8 and below of haxcms-php, API endpoints do not perform authorization checks when interacting with a resource. Both the JS and PHP versions of the CMS do not verify that a user has permission to interact with a resource before performing a given operation. The API endpoints within the HAX CMS application check if a user is authenticated, but don't check for authorization before performing an operation. This is fixed in versions 11.0.14 of haxcms-nodejs and 11.0.9 of haxcms-php.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

HAX CMS (Node.js ≤11.0.13, PHP ≤11.0.8) API endpoints lack authorization checks, allowing authenticated users to read or modify other users’ sites and configuration.

Root

Cause

HAX CMS, a microsite management system with PHP or Node.js backends, contains an authorization bypass vulnerability in its API endpoints. The official description and advisory confirm that while API endpoints verify user authentication, they fail to perform any authorization check before allowing operations on resources [1][4]. This means an authenticated user is not restricted to their own sites. The issue affects versions 11.0.13 and below of the Node.js backend (haxcms-nodejs) and versions 11.0.8 and below of the PHP backend (haxcms-php) [1].

Exploitation

The attack is straightforward for any authenticated user of HAX CMS. No special network position is required beyond normal access to the CMS instance. An attacker can simply make API requests directly to endpoints such as createNode, deleteNode, saveManifest, or getConfig, and the server will execute the operation without validating that the user owns or has rights to the target site [4]. The commit diffs show that the fix introduces site-specific tokens (e.g., site_token) that must match the active user and site name, and changes HTTP response codes from 500 to 403 for unauthorized attempts [2][3].

Impact

An authenticated attacker can enumerate, modify, and delete other users’ sites and nodes. Critically, the advisory notes that the getConfig endpoint can be used to retrieve the full application configuration, which may contain cleartext credentials [4]. This makes the vulnerability a vector for significant data exposure and site compromise.

Mitigation

The vulnerability is fixed in versions 11.0.14 of haxcms-nodejs and 11.0.9 of haxcms-php [1]. Users should update their installations immediately. No workaround is mentioned in the available sources; the patch introduces per-site authorization tokens in all relevant API paths [2][3].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@haxtheweb/haxcms-nodejsnpm
< 11.0.1411.0.14
elmsln/haxcmsPackagist
< 11.0.1411.0.14

Affected products

3

Patches

2
5826e9b7f3d8

https://github.com/haxtheweb/issues/security/advisories/GHSA-9jr9-8ff3-m894

20 files changed · +995 912
  • package.json+1 0 modified
    @@ -10,6 +10,7 @@
       "scripts": {
         "dev:build": "npm run build && nodemon dist/app.js",
         "dev": "nodemon src/app.js",
    +    "dev:nologin": "nodemon src/local.js",
         "start": "open http://localhost:3000 && npm run dev",
         "build": "rm -rf dist && babel src --out-dir dist --copy-files --include-dotfiles && chmod 774 dist/local.js && chmod 774 dist/app.js && chmod 774 dist/cli.js",
         "release": "npm run build && commit-and-tag-version && git push --follow-tags origin main && npm publish",
    
  • src/app.js+1 1 modified
    @@ -22,7 +22,7 @@ var helmetPolicies = {
           scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "'wasm-unsafe-eval'", "www.youtube.com"],
           styleSrc: ["'self'", "'unsafe-inline'", "data:", "https:"],
           mediaSrc: ["'self'", "data:", "https:"],
    -      imgSrc: ["'self'", "data:", "https:"],
    +      imgSrc: ["'self'", "data:", "https:", "http:", "blob:"],
           connectSrc: ["'self'", "https:", "ws:"],
           defaultSrc: ["'self'", "data:", "https:"],
           objectSrc: ["'none'"],
    
  • src/lib/HAXCMS.js+28 16 modified
    @@ -1957,19 +1957,31 @@ class HAXCMSClass {
        */
       validateRequestToken(token = null, value = '', query = {})
         {
    -        if (this.isCLI() || this.HAXCMS_DISABLE_JWT_CHECKS) {
    -            return true;
    -        }
    -        // default token is POST
    -        if (token == null && query['token']) {
    -          token = query['token'];
    -        }
    -        if (token != null) {
    -          if (token == this.getRequestToken(value)) {
    -            return true;
    -          }
    +      if (this.isCLI() || this.HAXCMS_DISABLE_JWT_CHECKS) {
    +          return true;
    +      }
    +      // default token is POST
    +      if (token == null && query['token']) {
    +        token = query['token'];
    +      }
    +      if (token != null) {
    +        if (token == this.getRequestToken(value)) {
    +          return true;
             }
    -        return false;
    +      }
    +      return false;
    +    }
    +    /**
    +     * Get the active user name based on the session
    +     * or the super user if the session is not set
    +     */
    +    getActiveUserName() {
    +      if (this.user.name != null && this.user.name != '') {
    +        return this.user.name;
    +      }
    +      else if (this.superUser.name) {
    +        return this.superUser.name;
    +      }
         }
         getRequestToken(value = '')
         {
    @@ -1980,7 +1992,7 @@ class HAXCMSClass {
           var buf1 = crypto.createHmac("sha256", "0").update(data).digest();
           var buf2 = Buffer.from(key);
           // generate the hash
    -      return Buffer.concat([buf1, buf2]).toString('base64');
    +      return Buffer.concat([buf1, buf2]).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
         }
         /**
          * load form and spitting out HAXschema + values in our standard transmission method
    @@ -2462,7 +2474,7 @@ class HAXCMSClass {
         /**
          * Generate a valid HAX App store specification schema for connecting to this site via JSON.
          */
    -    siteConnectionJSON()
    +    siteConnectionJSON(siteToken = '')
         {
             return {
           "details": {
    @@ -2479,7 +2491,7 @@ class HAXCMSClass {
             "operations": {
               "browse": {
                 "method": "GET",
    -            "endPoint": this.systemRequestBase + "listFiles",
    +            "endPoint": this.systemRequestBase + "listFiles?site_token=" + siteToken,
                 "pagination": {
                   "style": "link",
                   "props": {
    @@ -2518,7 +2530,7 @@ class HAXCMSClass {
               },
               "add": {
                 "method": "POST",
    -            "endPoint": this.systemRequestBase + "saveFile",
    +            "endPoint": this.systemRequestBase + "saveFile?site_token=" + siteToken,
                 "acceptsGizmoTypes": [
                   "audio",
                   "image",
    
  • src/openapi/spec.json+1 1 modified
    @@ -242,7 +242,7 @@
                     "operationId": "Operations::generateAppStore",
                     "parameters": [
                         {
    -                        "name": "app-store-token",
    +                        "name": "appstore_token",
                             "in": "query",
                             "description": "security token for appstore",
                             "required": true,
    
  • src/openapi/spec.yaml+1 1 modified
    @@ -167,7 +167,7 @@ paths:
           operationId: 'Operations::generateAppStore'
           parameters:
             -
    -          name: app-store-token
    +          name: appstore_token
               in: query
               description: 'security token for appstore'
               required: true
    
  • src/routes/archiveSite.js+19 15 modified
    @@ -36,22 +36,26 @@ const { HAXCMS } = require('../lib/HAXCMS.js');
        * )
        */
       async function archiveSite(req, res) {
    -    let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    if (site.name) {
    -      // create archived directory in this tree if it doesn't exist already
    -      if (!fs.existsSync(HAXCMS.HAXCMS_ROOT + HAXCMS.archivedDirectory)) {
    -        fs.mkdirSync(HAXCMS.HAXCMS_ROOT + HAXCMS.archivedDirectory);
    +    if (req.query['user_token'] && HAXCMS.validateRequestToken(req.query['user_token'], HAXCMS.getActiveUserName())) {
    +      let site = await HAXCMS.loadSite(req.body['site']['name']);
    +      if (site.name) {
    +        // create archived directory in this tree if it doesn't exist already
    +        if (!fs.existsSync(HAXCMS.HAXCMS_ROOT + HAXCMS.archivedDirectory)) {
    +          fs.mkdirSync(HAXCMS.HAXCMS_ROOT + HAXCMS.archivedDirectory);
    +        }
    +        await fs.rename(
    +          HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + site.manifest.metadata.site.name,
    +          HAXCMS.HAXCMS_ROOT + HAXCMS.archivedDirectory + '/' + site.manifest.metadata.site.name);
    +        res.send({
    +          'name': site.name,
    +          'detail': 'Site archived',
    +        });
           }
    -      await fs.rename(
    -        HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + site.manifest.metadata.site.name,
    -        HAXCMS.HAXCMS_ROOT + HAXCMS.archivedDirectory + '/' + site.manifest.metadata.site.name);
    -      res.send({
    -        'name': site.name,
    -        'detail': 'Site archived',
    -      });
    -    }
    -    else {
    -      res.sendStatus(500);
    +      else {
    +        res.sendStatus(500);
    +      }
    +    } else {
    +      res.sendStatus(403);
         }
       }
       module.exports = archiveSite;
    \ No newline at end of file
    
  • src/routes/cloneSite.js+43 39 modified
    @@ -35,48 +35,52 @@ const { HAXCMS } = require('../lib/HAXCMS.js');
        * )
        */
       async function cloneSite(req, res) {
    -    let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    let originalPathForReplacement = HAXCMS.sitesDirectory + site.manifest.metadata.site.name + "/files/";
    +    if (req.query['user_token'] && HAXCMS.validateRequestToken(req.query['user_token'], HAXCMS.getActiveUserName())) {
    +      let site = await HAXCMS.loadSite(req.body['site']['name']);
    +      let originalPathForReplacement = HAXCMS.sitesDirectory + site.manifest.metadata.site.name + "/files/";
     
    -    let cloneName = HAXCMS.getUniqueName(site.name);
    -    // ensure the path to the new folder is valid
    -    await HAXCMS.recurseCopy(
    -        HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + site.name,
    -        HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + cloneName
    -    );
    -    // we need to then load and rewrite the site name var or it will conflict given the name change
    -    let newSite = await HAXCMS.loadSite(cloneName);
    -    newSite.manifest.metadata.site.name = cloneName;
    -    newSite.manifest.id =  HAXCMS.generateUUID();
    -    // loop through all items and rewrite the path to files as we cloned it
    -    for (var delta in newSite.manifest.items) {
    -      let item = newSite.manifest.items[delta];
    -      if (item.metadata.files) {
    -        for (var delta2 in item.metadata.files) {
    -          if (newSite.manifest.items[delta].metadata.files[delta2].path) {
    -            newSite.manifest.items[delta].metadata.files[delta2].path = newSite.manifest.items[delta].metadata.files[delta2].path.replace(
    -              originalPathForReplacement,
    -              '/sites/' + cloneName + '/files/',
    -            );
    -          }
    -          if (newSite.manifest.items[delta].metadata.files[delta2].fullUrl) {
    -            newSite.manifest.items[delta].metadata.files[delta2].fullUrl = newSite.manifest.items[delta].metadata.files[delta2].fullUrl.replace(
    -              originalPathForReplacement,
    -              '/sites/' + cloneName + '/files/',
    -            );
    +      let cloneName = HAXCMS.getUniqueName(site.name);
    +      // ensure the path to the new folder is valid
    +      await HAXCMS.recurseCopy(
    +          HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + site.name,
    +          HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + cloneName
    +      );
    +      // we need to then load and rewrite the site name var or it will conflict given the name change
    +      let newSite = await HAXCMS.loadSite(cloneName);
    +      newSite.manifest.metadata.site.name = cloneName;
    +      newSite.manifest.id =  HAXCMS.generateUUID();
    +      // loop through all items and rewrite the path to files as we cloned it
    +      for (var delta in newSite.manifest.items) {
    +        let item = newSite.manifest.items[delta];
    +        if (item.metadata.files) {
    +          for (var delta2 in item.metadata.files) {
    +            if (newSite.manifest.items[delta].metadata.files[delta2].path) {
    +              newSite.manifest.items[delta].metadata.files[delta2].path = newSite.manifest.items[delta].metadata.files[delta2].path.replace(
    +                originalPathForReplacement,
    +                '/sites/' + cloneName + '/files/',
    +              );
    +            }
    +            if (newSite.manifest.items[delta].metadata.files[delta2].fullUrl) {
    +              newSite.manifest.items[delta].metadata.files[delta2].fullUrl = newSite.manifest.items[delta].metadata.files[delta2].fullUrl.replace(
    +                originalPathForReplacement,
    +                '/sites/' + cloneName + '/files/',
    +              );
    +            }
               }
             }
           }
    -    }
     
    -    await newSite.save();
    -    res.send({
    -      'link':
    -        HAXCMS.basePath +
    -        HAXCMS.sitesDirectory +
    -        '/' +
    -        cloneName,
    -      'name': cloneName
    -    });
    +      await newSite.save();
    +      res.send({
    +        'link':
    +          HAXCMS.basePath +
    +          HAXCMS.sitesDirectory +
    +          '/' +
    +          cloneName,
    +        'name': cloneName
    +      });
    +    } else {
    +      res.sendStatus(403);
    +    }
       }
    -  module.exports = cloneSite;
    \ No newline at end of file
    +module.exports = cloneSite;
    \ No newline at end of file
    
  • src/routes/connectionSettings.js+34 20 modified
    @@ -22,48 +22,62 @@ async function connectionSettings(req, res) {
       if (req.headers && req.headers.referer && !req.headers.referer.includes('/sites/')) {
         baseAPIPath = HAXCMS.systemRequestBase;
       }
    +  var sitename = '';
       // express gives this up on requests but doesn't know it ahead of time
       if (req.headers && req.headers.referer) {
         let details = new url.URL(req.headers.referer);
         HAXCMS.protocol = details.protocol.replace(':', '');
         HAXCMS.domain = details.host;
         HAXCMS.request_url = details;
    +
    +    const sitepath = req.headers.referer.replace(`${HAXCMS.protocol}://${HAXCMS.domain}${HAXCMS.basePath}${HAXCMS.sitesDirectory}/`, '');
    +    const siteparts = sitepath.split('/');
    +    // should always be at the base here
    +    sitename = siteparts[0];
       }
    +  const siteToken = HAXCMS.getRequestToken(HAXCMS.getActiveUserName() + ':' + sitename);
    +  // user token is just the name of the logged in user
    +  const userToken = HAXCMS.getRequestToken(HAXCMS.getActiveUserName());
       const returnData = JSON.stringify({
         token: HAXCMS.getRequestToken(),
    +    login: `${baseAPIPath}login`,
    +    refreshUrl: `${baseAPIPath}refreshAccessToken`,
    +    logout: `${baseAPIPath}logout`,
    +    connectionSettings: `${baseAPIPath}connectionSettings`,
    +    // enables redirecting back to site root if JWT really is dead
    +    redirectUrl: HAXCMS.basePath,
    +    saveNodePath: `${baseAPIPath}saveNode?site_token=${siteToken}`,
    +    saveManifestPath: `${baseAPIPath}saveManifest?site_token=${siteToken}`,
    +    saveOutlinePath: `${baseAPIPath}saveOutline?site_token=${siteToken}`,
    +    getSiteFieldsPath: `${baseAPIPath}formLoad?haxcms_form_id=siteSettings`,
    +    // form token to validate form submissions as unique to the session
         getFormToken: HAXCMS.getRequestToken('form'),
    +    createNodePath: `${baseAPIPath}createNode?site_token=${siteToken}`,
    +    deleteNodePath: `${baseAPIPath}deleteNode?site_token=${siteToken}`,
    +
    +    getUserDataPath: `${baseAPIPath}getUserData?user_token=${userToken}`,
    +    createSite: `${baseAPIPath}createSite?user_token=${userToken}`,
    +    downloadSite: `${baseAPIPath}downloadSite?user_token=${userToken}`,
    +    archiveSite: `${baseAPIPath}archiveSite?user_token=${userToken}`,
    +    copySite: `${baseAPIPath}cloneSite?user_token=${userToken}`,
    +    getSitesList: `${baseAPIPath}listSites?user_token=${userToken}`,
         appStore: {
           url: `${baseAPIPath}generateAppStore`,
           params: {
    -        "app-store-token": HAXCMS.getRequestToken('appstore'),
    +        'appstore_token': HAXCMS.getRequestToken('appstore'),
    +        'site_token': siteToken,
    +        'siteName': sitename,
           }
         },
         themes: themes,
    -    connectionSettings: `${baseAPIPath}connectionSettings`,
    -    login: `${baseAPIPath}login`,
    -    refreshUrl: `${baseAPIPath}refreshAccessToken`,
    -    logout: `${baseAPIPath}logout`,
    -    redirectUrl: HAXCMS.basePath,
    -    saveNodePath: `${baseAPIPath}saveNode`,
    -    saveManifestPath: `${baseAPIPath}saveManifest`,
    -    saveOutlinePath: `${baseAPIPath}saveOutline`,
    -    getSiteFieldsPath: `${baseAPIPath}formLoad?haxcms_form_id=siteSettings`,
    -    createNodePath: `${baseAPIPath}createNode`,
    -    getUserDataPath: `${baseAPIPath}getUserData`,
    -    deleteNodePath: `${baseAPIPath}deleteNode`,
    -    createSite: `${baseAPIPath}createSite`,
    -    downloadSite: `${baseAPIPath}downloadSite`,
    -    archiveSite: `${baseAPIPath}archiveSite`,
    -    copySite: `${baseAPIPath}cloneSite`,
    -    getSitesList: `${baseAPIPath}listSites`,
       });
    -  let after;
    +  let after='';
       if (HAXCMS.HAXCMS_DISABLE_JWT_CHECKS) {
         after = `window.appSettings.jwt = "${HAXCMS.getJWT(HAXCMS.superUser.name)}"`;
       }
       res.send(`// force vercel calls to go from production
         window.MicroFrontendRegistryConfig = window.MicroFrontendRegistryConfig || {};
    -    window.MicroFrontendRegistryConfig.base = "https://haxapi.vercel.app";window.appSettings =${returnData};${after}`);
    +    window.MicroFrontendRegistryConfig.base = "https://open-apis.hax.cloud";window.appSettings =${returnData};${after}`);
     }
     
     module.exports = connectionSettings;
    \ No newline at end of file
    
  • src/routes/createNode.js+76 72 modified
    @@ -71,83 +71,87 @@ const JSONOutlineSchemaItem = require('../lib/JSONOutlineSchemaItem.js');
      */
     async function createNode(req, res) {
       let nodeParams = req.body;
    -  let item;
    -  let site = await HAXCMS.loadSite(req.body['site']['name'].toLowerCase());
    -  // implies we've been TOLD to create nodes
    -  // this is typically from a docx import
    -  if (nodeParams['items']) {
    -    // create pages
    -    for (i=0; i < nodeParams['items'].length; i++) {
    -      // outline-designer allows delete + confirmation but we don't have anything
    -      // so instead, just don't process the thing in question if asked to delete it
    -      if (nodeParams['items'][i]['delete'] && nodeParams['items'][i]['delete'] == true) {
    -        // do nothing
    -      }
    -      else {
    -        item = site.addPage(
    -        nodeParams['items'][i]['parent'], 
    -        nodeParams['items'][i]['title'], 
    -        'html', 
    -        nodeParams['items'][i]['slug'],
    -        nodeParams['items'][i]['id'],
    -        nodeParams['items'][i]['indent'],
    -        nodeParams['items'][i]['contents']
    -        );  
    +  if (req.query['site_token'] && HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + nodeParams['site']['name'])) {
    +    let item;
    +    let site = await HAXCMS.loadSite(req.body['site']['name'].toLowerCase());
    +    // implies we've been TOLD to create nodes
    +    // this is typically from a docx import
    +    if (nodeParams['items']) {
    +      // create pages
    +      for (i=0; i < nodeParams['items'].length; i++) {
    +        // outline-designer allows delete + confirmation but we don't have anything
    +        // so instead, just don't process the thing in question if asked to delete it
    +        if (nodeParams['items'][i]['delete'] && nodeParams['items'][i]['delete'] == true) {
    +          // do nothing
    +        }
    +        else {
    +          item = site.addPage(
    +          nodeParams['items'][i]['parent'], 
    +          nodeParams['items'][i]['title'], 
    +          'html', 
    +          nodeParams['items'][i]['slug'],
    +          nodeParams['items'][i]['id'],
    +          nodeParams['items'][i]['indent'],
    +          nodeParams['items'][i]['contents']
    +          );  
    +        }
           }
    +      await site.gitCommit(nodeParams['items'].length + ' pages added'); 
         }
    -    await site.gitCommit(nodeParams['items'].length + ' pages added'); 
    -  }
    -  else {
    -    // generate a new item based on the site
    -    item = site.itemFromParams(nodeParams);
    -    item.metadata.images = [];
    -    item.metadata.videos = [];
    -    // generate the boilerplate to fill this page
    -    HAXCMS.recurseCopy(
    -      HAXCMS.boilerplatePath + 'page/default',
    -      path.join(site.siteDirectory, item.location.replace('/index.html', ''))
    -    );
    -    // add the item back into the outline schema
    -    site.manifest.addItem(item);
    -    await site.manifest.save();
    -    // support for duplicating the content of another item
    -    if (nodeParams['node']['duplicate']) {
    -      // verify we can load this id
    -      let nodeToDuplicate;
    -      if (nodeToDuplicate = site.loadNode(nodeParams['node']['duplicate'])) {
    -          let content = await site.getPageContent(nodeToDuplicate);
    -          let page;
    -          // verify we actually have the id of an item that we just created
    -          if (page = site.loadNode(item.id)) {
    -          // write it to the file system
    -          // this all seems round about but it's more secure
    -          let bytes = await page.writeLocation(
    -              content,
    -              site.siteDirectory
    -          );
    -          }
    +    else {
    +      // generate a new item based on the site
    +      item = site.itemFromParams(nodeParams);
    +      item.metadata.images = [];
    +      item.metadata.videos = [];
    +      // generate the boilerplate to fill this page
    +      HAXCMS.recurseCopy(
    +        HAXCMS.boilerplatePath + 'page/default',
    +        path.join(site.siteDirectory, item.location.replace('/index.html', ''))
    +      );
    +      // add the item back into the outline schema
    +      site.manifest.addItem(item);
    +      await site.manifest.save();
    +      // support for duplicating the content of another item
    +      if (nodeParams['node']['duplicate']) {
    +        // verify we can load this id
    +        let nodeToDuplicate;
    +        if (nodeToDuplicate = site.loadNode(nodeParams['node']['duplicate'])) {
    +            let content = await site.getPageContent(nodeToDuplicate);
    +            let page;
    +            // verify we actually have the id of an item that we just created
    +            if (page = site.loadNode(item.id)) {
    +            // write it to the file system
    +            // this all seems round about but it's more secure
    +            let bytes = await page.writeLocation(
    +                content,
    +                site.siteDirectory
    +            );
    +            }
    +        }
           }
    -    }
    -    // implies front end was told to generate a page with set content
    -    // this is possible when importing and processing a file to generate
    -    // html which becomes the boilerplated content in effect
    -    else if (nodeParams['node']['contents']) {
    -      let page;
    -      if (page = site.loadNode(item.id)) {
    -          // write it to the file system
    -          let bytes = await page.writeLocation(
    -          nodeParams['node']['contents'],
    -          site.siteDirectory
    -          );
    +      // implies front end was told to generate a page with set content
    +      // this is possible when importing and processing a file to generate
    +      // html which becomes the boilerplated content in effect
    +      else if (nodeParams['node']['contents']) {
    +        let page;
    +        if (page = site.loadNode(item.id)) {
    +            // write it to the file system
    +            let bytes = await page.writeLocation(
    +            nodeParams['node']['contents'],
    +            site.siteDirectory
    +            );
    +        }
           }
    +      await site.gitCommit('Page added:' + item.title + ' (' + item.id + ')'); 
    +      // update the alternate formats as a new page exists
    +      await site.updateAlternateFormats();
         }
    -    await site.gitCommit('Page added:' + item.title + ' (' + item.id + ')'); 
    -    // update the alternate formats as a new page exists
    -    await site.updateAlternateFormats();
    +    res.send({
    +      status: 200,
    +      data: item
    +    });
    +  } else {
    +    res.sendStatus(403);
       }
    -  res.send({
    -    status: 200,
    -    data: item
    -  });    
     }
     module.exports = createNode;
    \ No newline at end of file
    
  • src/routes/createSite.js+2 3 modified
    @@ -52,7 +52,7 @@ const HAXCMSFile = require('../lib/HAXCMSFile.js');
        * )
        */
     async function createSite(req, res) {
    -  if (HAXCMS.validateRequestToken(req.body.token)) {
    +  if (HAXCMS.validateRequestToken(req.body.token) && req.query['user_token'] && HAXCMS.validateRequestToken(req.query['user_token'], HAXCMS.getActiveUserName())){
         let domain = null;
         // woohoo we can edit this thing!
         if (req.body['site']['domain'] && req.body['site']['domain'] != null && req.body['site']['domain'] != '') {
    @@ -209,14 +209,13 @@ async function createSite(req, res) {
           }
         }
         catch(e) {}
    -    
         res.send({
           "status": 200,
           "data": schema
         });
       }
       else {
    -      res.sendStatus(403);
    +    res.sendStatus(403);
       }
     }
     module.exports = createSite;
    \ No newline at end of file
    
  • src/routes/deleteNode.js+33 29 modified
    @@ -19,39 +19,43 @@ const { HAXCMS } = require('../lib/HAXCMS.js');
        */
       async function deleteNode(req, res) {
         let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    // update the page's content, using manifest to find it
    -    // this ensures that writing is always to what the file system
    -    // determines to be the correct page
    -    let page;
    -    if (page = site.loadNode(req.body['node']['id'])) {
    -        if (await site.deleteNode(page) === false) {
    -          res.sendStatus(500);
    -        } else {
    -          // now, we need to look for orphans if we deleted anything
    -          for (var key in site.manifest.items) {
    -            // just to be safe..
    -            let pageUpdate;
    -            if (pageUpdate = site.loadNode(site.manifest.items[key].id)) {
    -              // ensure that parent is valid to rescue orphan items
    -              let parentPage;
    -              if (pageUpdate.parent != null && !(parentPage = site.loadNode(pageUpdate.parent))) {
    -                pageUpdate.parent = null;
    -                // force to bottom of things while still being in old order if lots of things got axed
    -                pageUpdate.order = parseInt(pageUpdate.order) + site.manifest.items.length - 1;
    -                site.updateNode(pageUpdate);
    +    if (req.query['site_token'] && HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + req.body['site']['name'])) {
    +      // update the page's content, using manifest to find it
    +      // this ensures that writing is always to what the file system
    +      // determines to be the correct page
    +      let page;
    +      if (page = site.loadNode(req.body['node']['id'])) {
    +          if (await site.deleteNode(page) === false) {
    +            res.sendStatus(500);
    +          } else {
    +            // now, we need to look for orphans if we deleted anything
    +            for (var key in site.manifest.items) {
    +              // just to be safe..
    +              let pageUpdate;
    +              if (pageUpdate = site.loadNode(site.manifest.items[key].id)) {
    +                // ensure that parent is valid to rescue orphan items
    +                let parentPage;
    +                if (pageUpdate.parent != null && !(parentPage = site.loadNode(pageUpdate.parent))) {
    +                  pageUpdate.parent = null;
    +                  // force to bottom of things while still being in old order if lots of things got axed
    +                  pageUpdate.order = parseInt(pageUpdate.order) + site.manifest.items.length - 1;
    +                  site.updateNode(pageUpdate);
    +                }
                   }
                 }
    +            await site.gitCommit(
    +              'Page deleted: ' + page.title + ' (' + page.id + ')'
    +            );
    +            res.send({
    +              status: 200,
    +              data: page
    +            });
               }
    -          await site.gitCommit(
    -            'Page deleted: ' + page.title + ' (' + page.id + ')'
    -          );
    -          res.send({
    -            status: 200,
    -            data: page
    -          });
    -        }
    +      } else {
    +          res.sendStatus(500);
    +      }
         } else {
    -        res.sendStatus(500);
    +      res.sendStatus(403);
         }
       }
       module.exports = deleteNode;
    \ No newline at end of file
    
  • src/routes/downloadSite.js+33 29 modified
    @@ -36,36 +36,40 @@ const archiver = require('archiver');
        * )
        */
       async function downloadSite(req, res) {
    -    // load site
    -    let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    // create archived directory in this tree if it doesn't exist already
    -    if (!fs.existsSync(HAXCMS.HAXCMS_ROOT + HAXCMS.publishedDirectory)) {
    -      fs.mkdirSync(HAXCMS.HAXCMS_ROOT + HAXCMS.publishedDirectory);
    -    }
    -    // helpful boilerplate https://stackoverflow.com/questions/29873248/how-to-zip-a-whole-directory-and-download-using-php
    -    let dir = HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + site.name;
    -    // form a basic name
    -    let zip_file =
    -      HAXCMS.HAXCMS_ROOT +
    -      HAXCMS.publishedDirectory +
    -      '/' +
    -      site.name +
    -      '.zip';
    -    // Get real path for our folder
    -    let rootPath = await fs.realpath(dir);
    -    // Initialize archive object
    -    await zipDirectory(rootPath, zip_file);
    -    res.send({
    -      status: 200,
    -      data: {
    -      'link':
    -        HAXCMS.basePath +
    -        HAXCMS.publishedDirectory +
    -        '/' + site.name +
    -        '.zip',
    -      'name': site.name
    +    if (req.query['user_token'] && HAXCMS.validateRequestToken(req.query['user_token'], HAXCMS.getActiveUserName())) {
    +      // load site
    +      let site = await HAXCMS.loadSite(req.body['site']['name']);
    +      // create archived directory in this tree if it doesn't exist already
    +      if (!fs.existsSync(HAXCMS.HAXCMS_ROOT + HAXCMS.publishedDirectory)) {
    +        fs.mkdirSync(HAXCMS.HAXCMS_ROOT + HAXCMS.publishedDirectory);
           }
    -    });
    +      // helpful boilerplate https://stackoverflow.com/questions/29873248/how-to-zip-a-whole-directory-and-download-using-php
    +      let dir = HAXCMS.HAXCMS_ROOT + HAXCMS.sitesDirectory + '/' + site.name;
    +      // form a basic name
    +      let zip_file =
    +        HAXCMS.HAXCMS_ROOT +
    +        HAXCMS.publishedDirectory +
    +        '/' +
    +        site.name +
    +        '.zip';
    +      // Get real path for our folder
    +      let rootPath = await fs.realpath(dir);
    +      // Initialize archive object
    +      await zipDirectory(rootPath, zip_file);
    +      res.send({
    +        status: 200,
    +        data: {
    +        'link':
    +          HAXCMS.basePath +
    +          HAXCMS.publishedDirectory +
    +          '/' + site.name +
    +          '.zip',
    +        'name': site.name
    +        }
    +      });
    +    } else {
    +      res.sendStatus(403);
    +    }
       }
     
     
    
  • src/routes/generateAppStore.js+4 4 modified
    @@ -6,7 +6,7 @@ const AppStoreService = new HAXAppStoreService();
      *    path="/generateAppStore",
      *    tags={"hax","api"},
      *    @OA\Parameter(
    - *         name="app-store-token",
    + *         name="appstore_token",
      *         description="security token for appstore",
      *         in="query",
      *         required=true,
    @@ -22,8 +22,8 @@ function generateAppStore(req, res) {
       let returnData = {};
       // test if this is a valid user login with this specialty token that HAX looks for
       if (
    -    req.query['app-store-token'] &&
    -    HAXCMS.validateRequestToken(req.query['app-store-token'], 'appstore', req.query)
    +    req.query['appstore_token'] &&
    +    HAXCMS.validateRequestToken(req.query['appstore_token'], 'appstore', req.query)
       ) {
         let apikeys = {};
         let baseApps = AppStoreService.baseSupportedApps();
    @@ -37,7 +37,7 @@ function generateAppStore(req, res) {
         }
         let appStore = AppStoreService.loadBaseAppStore(apikeys);
         // pull in the core one we supply, though only upload works currently
    -    let tmp = HAXCMS.siteConnectionJSON();
    +    let tmp = HAXCMS.siteConnectionJSON(req.query['site_token']);
         appStore.push(tmp);
         let staxList,bloxList,autoloaderList;
         if (HAXCMS.config.appStore && HAXCMS.config.appStore.stax) {
    
  • src/routes/getUserData.js+9 5 modified
    @@ -17,11 +17,15 @@ const { HAXCMS } = require('../lib/HAXCMS.js');
      * )
      */
       function getUserData(req, res) {
    -    const returnData = {
    -      status: 200,
    -      data: HAXCMS.userData
    -    };
    -    res.send(returnData);
    +    if (req.query['user_token'] && HAXCMS.validateRequestToken(req.query['user_token'], HAXCMS.getActiveUserName())) {
    +      const returnData = {
    +        status: 200,
    +        data: HAXCMS.userData
    +      };
    +      res.send(returnData);
    +    } else {
    +      res.sendStatus(403);
    +    }
       }
     
     module.exports = getUserData;
    \ No newline at end of file
    
  • src/routes/listFiles.js+39 37 modified
    @@ -21,48 +21,50 @@ const mime = require('mime');
        */
       async function listFiles(req, res) {
         let files = [];
    -    // verify that we have params expected from frontend
    -    if (req.query && req.query['siteName']) {
    -      let site = await HAXCMS.loadSite(req.query['siteName']);
    -      if (site && site.siteDirectory) {
    -        let search = (typeof req.query['filename'] !== 'undefined') ? req.query['filename'] : '';
    -        // build files directory path
    -        let siteFilePath = path.join(site.siteDirectory, 'files');
    -        let handle;
    -        if (handle = fs.readdirSync(siteFilePath)) {
    -          handle.forEach(file => {
    -            if (
    -                file != "." &&
    -                file != ".." &&
    -                file != '.gitkeep' &&
    -                file != '.DS_Store'
    -            ) {
    -              // ensure this is a file
    +    if (req.query['site_token'] && HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + req.query['siteName'])) {
    +      // verify that we have params expected from frontend
    +      if (req.query && req.query['siteName']) {
    +        let site = await HAXCMS.loadSite(req.query['siteName']);
    +        if (site && site.siteDirectory) {
    +          let search = (typeof req.query['filename'] !== 'undefined') ? req.query['filename'] : '';
    +          // build files directory path
    +          let siteFilePath = path.join(site.siteDirectory, 'files');
    +          let handle;
    +          if (handle = fs.readdirSync(siteFilePath)) {
    +            handle.forEach(file => {
                   if (
    -                fs.lstatSync(siteFilePath + '/' + file).isFile()
    +                  file != "." &&
    +                  file != ".." &&
    +                  file != '.gitkeep' &&
    +                  file != '.DS_Store'
                   ) {
    -                // ensure this is a file and if we are searching for results then return only exact ones
    -                if (!search || search == "" || file.indexOf(search) !== -1) {
    -                  let fullUrl = '/files/' + file;
    -                  // multiple sites then append the base url to site management area
    -                  if (HAXCMS.operatingContext == 'multisite') {
    -                    fullUrl = HAXCMS.basePath +
    -                    HAXCMS.sitesDirectory + '/' +
    -                    site.manifest.metadata.site.name + '/files/' + file
    +                // ensure this is a file
    +                if (
    +                  fs.lstatSync(siteFilePath + '/' + file).isFile()
    +                ) {
    +                  // ensure this is a file and if we are searching for results then return only exact ones
    +                  if (!search || search == "" || file.indexOf(search) !== -1) {
    +                    let fullUrl = '/files/' + file;
    +                    // multiple sites then append the base url to site management area
    +                    if (HAXCMS.operatingContext == 'multisite') {
    +                      fullUrl = HAXCMS.basePath +
    +                      HAXCMS.sitesDirectory + '/' +
    +                      site.manifest.metadata.site.name + '/files/' + file
    +                    }
    +                    files.push({
    +                      'path' : 'files/' + file,
    +                      'fullUrl' : fullUrl,
    +                      'url' : 'files/' + file,
    +                      'mimetype' : mime.getType(siteFilePath + '/' + file),
    +                      'name' : file
    +                    });
                       }
    -                  files.push({
    -                    'path' : 'files/' + file,
    -                    'fullUrl' : fullUrl,
    -                    'url' : 'files/' + file,
    -                    'mimetype' : mime.getType(siteFilePath + '/' + file),
    -                    'name' : file
    -                  });
    +                } else {
    +                    // @todo maybe step into directories?
                     }
    -              } else {
    -                  // @todo maybe step into directories?
                   }
    -            }
    -          });
    +            });
    +          }
             }
           }
         }
    
  • src/routes/listSites.js+5 1 modified
    @@ -11,7 +11,8 @@ const { HAXCMS } = require('../lib/HAXCMS.js');
      *   )
      * )
      */
    -async function listSites (req, res) {
    +async function listSites(req, res) {
    +  if (req.query['user_token'] && HAXCMS.validateRequestToken(req.query['user_token'], HAXCMS.getActiveUserName())) {
         // top level fake JOS
         let returnData = {
           id: '123-123-123-123',
    @@ -51,5 +52,8 @@ async function listSites (req, res) {
           status: 200,
           data: returnData
         });
    +  } else {
    +    res.sendStatus(403);
    +  }
     }
     module.exports = listSites;
    \ No newline at end of file
    
  • src/routes/saveFile.js+11 0 modified
    @@ -51,7 +51,18 @@ const HAXCMSFile = require('../lib/HAXCMSFile.js');
        */
       async function saveFile(req, res, next) {
         let sendResult = 500;
    +    // resolve front-end parsing issue with saveFiles based on how that was structured
    +    // this is a bit of a hack but site token will have the ?siteName in it as opposed to stand alone params
    +    if (req.query['site_token'] && !req.query['site']) {
    +      let tmp = req.query['site_token'].split('?siteName=');
    +      if (tmp.length == 2) {
    +        req.query['site_token'] = tmp[0];
    +        req.query['siteName'] = tmp[1];
    +      }
    +    }
         if (
    +      req.query['site_token'] && 
    +      HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + req.query['siteName']) &&
           req.file &&
           req.file.fieldname == 'file-upload' && 
           req.query && 
    
  • src/routes/saveManifest.js+197 193 modified
    @@ -19,219 +19,223 @@ const fs = require('fs-extra');
        * )
        */
       async function saveManifest(req, res) {
    -    // load the site from name
    -    let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    // standard form submit
    -    // @todo 
    -    // make the form point to a form submission endpoint with appropriate name
    -    // add a hidden field to the output that always has the haxcms_form_id as well
    -    // as a dynamically generated Request token relative to the name of the
    -    // form
    -    // pull the form schema for the form itself internally
    -    // ensure ONLY the things that appear in that schema get set
    -    // if something DID NOT COME ACROSS, don't unset it, only set what shows up
    -    // if something DID COME ACROSS WE DIDN'T SET, kill the transaction (xss)
    +    if (req.query['site_token'] && HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + req.body['site']['name'])) {
    +      // load the site from name
    +      let site = await HAXCMS.loadSite(req.body['site']['name']);
    +      // standard form submit
    +      // @todo 
    +      // make the form point to a form submission endpoint with appropriate name
    +      // add a hidden field to the output that always has the haxcms_form_id as well
    +      // as a dynamically generated Request token relative to the name of the
    +      // form
    +      // pull the form schema for the form itself internally
    +      // ensure ONLY the things that appear in that schema get set
    +      // if something DID NOT COME ACROSS, don't unset it, only set what shows up
    +      // if something DID COME ACROSS WE DIDN'T SET, kill the transaction (xss)
     
    -    // - snag the form
    -    // @todo see if we can dynamically save the valus in the same format we loaded
    -    // the original form in. This would involve removing the vast majority of
    -    // what's below
    -    /*if (HAXCMS.validateRequestToken(null, 'form')) {
    -      let context = {
    -        'site' : [],
    -        'node' : [],
    -      };
    -      if ((req.body['site'])) {
    -        context['site'] = req.body['site'];
    -      }
    -      if ((req.body['node'])) {
    -        context['node'] = req.body['node'];
    -      }
    -      form = HAXCMS.loadForm(req.body['haxcms_form_id'], context);
    -    }*/
    -    if (HAXCMS.validateRequestToken(req.body['haxcms_form_token'], req.body['haxcms_form_id'])) {
    -      site.manifest.title = req.body['manifest']['site']['manifest-title'].replace(/<\/?[^>]+(>|$)/g, "");
    -      site.manifest.description = req.body['manifest']['site']['manifest-description'].replace(/<\/?[^>]+(>|$)/g, "");
    -      // store some version data here just so we can find it later
    -      site.manifest.metadata.site.version = await HAXCMS.getHAXCMSVersion();
    -      site.manifest.metadata.site.domain = filter_var(
    -          req.body['manifest']['site']['manifest-metadata-site-domain'],
    -          "FILTER_SANITIZE_STRING"
    -      );
    -      site.manifest.metadata.site.logo = filter_var(
    -          req.body['manifest']['site']['manifest-metadata-site-logo'],
    -          "FILTER_SANITIZE_STRING"
    -      );
    -      site.manifest.metadata.site.tags = filter_var(
    -        req.body['manifest']['site']['manifest-metadata-site-tags'],
    -        "FILTER_SANITIZE_STRING"
    -      );
    -      if (!(site.manifest.metadata.site.static)) {
    -        site.manifest.metadata.site.static = {};
    -      }
    -      if (!(site.manifest.metadata.site.settings)) {
    -        site.manifest.metadata.site.settings = {};
    -      }
    -      if (typeof req.body['manifest']['site']['manifest-domain'] !== 'undefined') {
    -        let domain = filter_var(
    -            req.body['manifest']['site']['manifest-domain'],
    -            "FILTER_SANITIZE_STRING"
    -        );
    -        // support updating the domain CNAME value
    -        if (site.manifest.metadata.site.domain != domain) {
    -          site.manifest.metadata.site.domain = domain;
    -          fs.writeFileSync(site.siteDirectory + '/CNAME', domain);
    +      // - snag the form
    +      // @todo see if we can dynamically save the valus in the same format we loaded
    +      // the original form in. This would involve removing the vast majority of
    +      // what's below
    +      /*if (HAXCMS.validateRequestToken(null, 'form')) {
    +        let context = {
    +          'site' : [],
    +          'node' : [],
    +        };
    +        if ((req.body['site'])) {
    +          context['site'] = req.body['site'];
             }
    -      }
    -      let hThemes = await HAXCMS.getThemes();
    -      // look for a match so we can set the correct data
    -      for (var key in hThemes) {
    -        let theme = hThemes[key];
    -        if (
    -            filter_var(req.body['manifest']['theme']['manifest-metadata-theme-element'], "FILTER_SANITIZE_STRING") ==
    -            key
    -        ) {
    -            site.manifest.metadata.theme = theme;
    +        if ((req.body['node'])) {
    +          context['node'] = req.body['node'];
             }
    -      }
    -      if (!(site.manifest.metadata.theme.variables)) {
    -        site.manifest.metadata.theme.variables = {};
    -      }
    -
    -      if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-image'] !== 'undefined') {
    -        site.manifest.metadata.theme.variables.image = filter_var(
    -          req.body['manifest']['theme']['manifest-metadata-theme-variables-image'],"FILTER_SANITIZE_STRING"
    +        form = HAXCMS.loadForm(req.body['haxcms_form_id'], context);
    +      }*/
    +      if (HAXCMS.validateRequestToken(req.body['haxcms_form_token'], req.body['haxcms_form_id'])) {
    +        site.manifest.title = req.body['manifest']['site']['manifest-title'].replace(/<\/?[^>]+(>|$)/g, "");
    +        site.manifest.description = req.body['manifest']['site']['manifest-description'].replace(/<\/?[^>]+(>|$)/g, "");
    +        // store some version data here just so we can find it later
    +        site.manifest.metadata.site.version = await HAXCMS.getHAXCMSVersion();
    +        site.manifest.metadata.site.domain = filter_var(
    +            req.body['manifest']['site']['manifest-metadata-site-domain'],
    +            "FILTER_SANITIZE_STRING"
             );
    -      }
    -      if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-imageAlt'] !== 'undefined') {
    -        site.manifest.metadata.theme.variables.imageAlt = filter_var(
    -          req.body['manifest']['theme']['manifest-metadata-theme-variables-imageAlt'], "FILTER_SANITIZE_STRING"
    +        site.manifest.metadata.site.logo = filter_var(
    +            req.body['manifest']['site']['manifest-metadata-site-logo'],
    +            "FILTER_SANITIZE_STRING"
             );
    -      }
    -      if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-imageLink'] !== 'undefined') {
    -        site.manifest.metadata.theme.variables.imageLink = filter_var(
    -          req.body['manifest']['theme']['manifest-metadata-theme-variables-imageLink'], "FILTER_SANITIZE_STRING"
    +        site.manifest.metadata.site.tags = filter_var(
    +          req.body['manifest']['site']['manifest-metadata-site-tags'],
    +          "FILTER_SANITIZE_STRING"
             );
    -      }
    -      // REGIONS SUPPORT
    -      if (!(site.manifest.metadata.theme.regions)) {
    -        site.manifest.metadata.theme.regions = {};
    -      }
    -      // look for a match so we can set the correct data
    -      let validRegions = [
    -        "header",
    -        "sidebarFirst",
    -        "sidebarSecond",
    -        "contentTop",
    -        "contentBottom",
    -        "footerPrimary",
    -        "footerSecondary"
    -      ];
    -      for (var i in validRegions) {
    -        let value = validRegions[i];
    -        if (req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value]) {
    -          for (var j in req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value]) {
    -            let id = req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value][j];
    -            req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value][j] = filter_var(id, "FILTER_SANITIZE_STRING");
    -          }
    -          site.manifest.metadata.theme.regions[value] = req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value];
    +        if (!(site.manifest.metadata.site.static)) {
    +          site.manifest.metadata.site.static = {};
             }
    -      }
    -      if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-hexCode'] !== 'undefined') {
    -        site.manifest.metadata.theme.variables.hexCode = filter_var(
    -          req.body['manifest']['theme']['manifest-metadata-theme-variables-hexCode'],"FILTER_SANITIZE_STRING"
    -        );
    -      }
    -      site.manifest.metadata.theme.variables.cssVariable = "--simple-colors-default-theme-" + filter_var(
    -        req.body['manifest']['theme']['manifest-metadata-theme-variables-cssVariable'], "FILTER_SANITIZE_STRING"
    -      ) + "-7";
    -      site.manifest.metadata.theme.variables.icon = filter_var(
    -        req.body['manifest']['theme']['manifest-metadata-theme-variables-icon'],"FILTER_SANITIZE_STRING"
    -      );
    -      if (typeof req.body['manifest']['author']['manifest-license'] !== 'undefined') {
    -          site.manifest.license = filter_var(
    -              req.body['manifest']['author']['manifest-license'],
    +        if (!(site.manifest.metadata.site.settings)) {
    +          site.manifest.metadata.site.settings = {};
    +        }
    +        if (typeof req.body['manifest']['site']['manifest-domain'] !== 'undefined') {
    +          let domain = filter_var(
    +              req.body['manifest']['site']['manifest-domain'],
                   "FILTER_SANITIZE_STRING"
               );
    -          if (!(site.manifest.metadata.author)) {
    -            site.manifest.metadata.author = {};
    +          // support updating the domain CNAME value
    +          if (site.manifest.metadata.site.domain != domain) {
    +            site.manifest.metadata.site.domain = domain;
    +            fs.writeFileSync(site.siteDirectory + '/CNAME', domain);
               }
    -          site.manifest.metadata.author.image = filter_var(
    -              req.body['manifest']['author']['manifest-metadata-author-image'],
    -              "FILTER_SANITIZE_STRING"
    +        }
    +        let hThemes = await HAXCMS.getThemes();
    +        // look for a match so we can set the correct data
    +        for (var key in hThemes) {
    +          let theme = hThemes[key];
    +          if (
    +              filter_var(req.body['manifest']['theme']['manifest-metadata-theme-element'], "FILTER_SANITIZE_STRING") ==
    +              key
    +          ) {
    +              site.manifest.metadata.theme = theme;
    +          }
    +        }
    +        if (!(site.manifest.metadata.theme.variables)) {
    +          site.manifest.metadata.theme.variables = {};
    +        }
    +
    +        if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-image'] !== 'undefined') {
    +          site.manifest.metadata.theme.variables.image = filter_var(
    +            req.body['manifest']['theme']['manifest-metadata-theme-variables-image'],"FILTER_SANITIZE_STRING"
               );
    -          site.manifest.metadata.author.name = filter_var(
    -              req.body['manifest']['author']['manifest-metadata-author-name'],
    -              "FILTER_SANITIZE_STRING"
    +        }
    +        if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-imageAlt'] !== 'undefined') {
    +          site.manifest.metadata.theme.variables.imageAlt = filter_var(
    +            req.body['manifest']['theme']['manifest-metadata-theme-variables-imageAlt'], "FILTER_SANITIZE_STRING"
               );
    -          site.manifest.metadata.author.email = filter_var(
    -              req.body['manifest']['author']['manifest-metadata-author-email'],
    -              "FILTER_SANITIZE_STRING"
    +        }
    +        if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-imageLink'] !== 'undefined') {
    +          site.manifest.metadata.theme.variables.imageLink = filter_var(
    +            req.body['manifest']['theme']['manifest-metadata-theme-variables-imageLink'], "FILTER_SANITIZE_STRING"
               );
    -          site.manifest.metadata.author.socialLink = filter_var(
    -              req.body['manifest']['author']['manifest-metadata-author-socialLink'],
    -              "FILTER_SANITIZE_STRING"
    +        }
    +        // REGIONS SUPPORT
    +        if (!(site.manifest.metadata.theme.regions)) {
    +          site.manifest.metadata.theme.regions = {};
    +        }
    +        // look for a match so we can set the correct data
    +        let validRegions = [
    +          "header",
    +          "sidebarFirst",
    +          "sidebarSecond",
    +          "contentTop",
    +          "contentBottom",
    +          "footerPrimary",
    +          "footerSecondary"
    +        ];
    +        for (var i in validRegions) {
    +          let value = validRegions[i];
    +          if (req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value]) {
    +            for (var j in req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value]) {
    +              let id = req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value][j];
    +              req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value][j] = filter_var(id, "FILTER_SANITIZE_STRING");
    +            }
    +            site.manifest.metadata.theme.regions[value] = req.body['manifest']['theme']['manifest-metadata-theme-regions-' + value];
    +          }
    +        }
    +        if (typeof req.body['manifest']['theme']['manifest-metadata-theme-variables-hexCode'] !== 'undefined') {
    +          site.manifest.metadata.theme.variables.hexCode = filter_var(
    +            req.body['manifest']['theme']['manifest-metadata-theme-variables-hexCode'],"FILTER_SANITIZE_STRING"
               );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-private'] !== 'undefined') {
    -          site.manifest.metadata.site.settings.private = filter_var(
    -          req.body['manifest']['seo']['manifest-metadata-site-settings-private'],
    +        }
    +        site.manifest.metadata.theme.variables.cssVariable = "--simple-colors-default-theme-" + filter_var(
    +          req.body['manifest']['theme']['manifest-metadata-theme-variables-cssVariable'], "FILTER_SANITIZE_STRING"
    +        ) + "-7";
    +        site.manifest.metadata.theme.variables.icon = filter_var(
    +          req.body['manifest']['theme']['manifest-metadata-theme-variables-icon'],"FILTER_SANITIZE_STRING"
    +        );
    +        if (typeof req.body['manifest']['author']['manifest-license'] !== 'undefined') {
    +            site.manifest.license = filter_var(
    +                req.body['manifest']['author']['manifest-license'],
    +                "FILTER_SANITIZE_STRING"
    +            );
    +            if (!(site.manifest.metadata.author)) {
    +              site.manifest.metadata.author = {};
    +            }
    +            site.manifest.metadata.author.image = filter_var(
    +                req.body['manifest']['author']['manifest-metadata-author-image'],
    +                "FILTER_SANITIZE_STRING"
    +            );
    +            site.manifest.metadata.author.name = filter_var(
    +                req.body['manifest']['author']['manifest-metadata-author-name'],
    +                "FILTER_SANITIZE_STRING"
    +            );
    +            site.manifest.metadata.author.email = filter_var(
    +                req.body['manifest']['author']['manifest-metadata-author-email'],
    +                "FILTER_SANITIZE_STRING"
    +            );
    +            site.manifest.metadata.author.socialLink = filter_var(
    +                req.body['manifest']['author']['manifest-metadata-author-socialLink'],
    +                "FILTER_SANITIZE_STRING"
    +            );
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-private'] !== 'undefined') {
    +            site.manifest.metadata.site.settings.private = filter_var(
    +            req.body['manifest']['seo']['manifest-metadata-site-settings-private'],
    +            "FILTER_VALIDATE_BOOLEAN"
    +            );
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-canonical'] !== 'undefined') {
    +            site.manifest.metadata.site.settings.canonical = filter_var(
    +            req.body['manifest']['seo']['manifest-metadata-site-settings-canonical'],
    +            "FILTER_VALIDATE_BOOLEAN"
    +            );
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-lang'] !== 'undefined') {
    +          site.manifest.metadata.site.settings.lang = filter_var(
    +          req.body['manifest']['seo']['manifest-metadata-site-settings-lang'],
    +          "FILTER_SANITIZE_STRING"
    +          );
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-pathauto'] !== 'undefined') {
    +            site.manifest.metadata.site.settings.pathauto = filter_var(
    +            req.body['manifest']['seo']['manifest-metadata-site-settings-pathauto'],
    +            "FILTER_VALIDATE_BOOLEAN"
    +            );
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-publishPagesOn'] !== 'undefined') {
    +          site.manifest.metadata.site.settings.publishPagesOn = filter_var(
    +          req.body['manifest']['seo']['manifest-metadata-site-settings-publishPagesOn'],
               "FILTER_VALIDATE_BOOLEAN"
               );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-canonical'] !== 'undefined') {
    -          site.manifest.metadata.site.settings.canonical = filter_var(
    -          req.body['manifest']['seo']['manifest-metadata-site-settings-canonical'],
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-sw'] !== 'undefined') {
    +          site.manifest.metadata.site.settings.sw = filter_var(
    +          req.body['manifest']['seo']['manifest-metadata-site-settings-sw'],
               "FILTER_VALIDATE_BOOLEAN"
               );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-lang'] !== 'undefined') {
    -        site.manifest.metadata.site.settings.lang = filter_var(
    -        req.body['manifest']['seo']['manifest-metadata-site-settings-lang'],
    -        "FILTER_SANITIZE_STRING"
    -        );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-pathauto'] !== 'undefined') {
    -          site.manifest.metadata.site.settings.pathauto = filter_var(
    -          req.body['manifest']['seo']['manifest-metadata-site-settings-pathauto'],
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-forceUpgrade'] !== 'undefined') {
    +          site.manifest.metadata.site.settings.forceUpgrade = filter_var(
    +          req.body['manifest']['seo']['manifest-metadata-site-settings-forceUpgrade'],
               "FILTER_VALIDATE_BOOLEAN"
               );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-publishPagesOn'] !== 'undefined') {
    -        site.manifest.metadata.site.settings.publishPagesOn = filter_var(
    -        req.body['manifest']['seo']['manifest-metadata-site-settings-publishPagesOn'],
    -        "FILTER_VALIDATE_BOOLEAN"
    -        );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-sw'] !== 'undefined') {
    -        site.manifest.metadata.site.settings.sw = filter_var(
    -        req.body['manifest']['seo']['manifest-metadata-site-settings-sw'],
    -        "FILTER_VALIDATE_BOOLEAN"
    -        );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-forceUpgrade'] !== 'undefined') {
    -        site.manifest.metadata.site.settings.forceUpgrade = filter_var(
    -        req.body['manifest']['seo']['manifest-metadata-site-settings-forceUpgrade'],
    -        "FILTER_VALIDATE_BOOLEAN"
    -        );
    -      }
    -      if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-gaID'] !== 'undefined') {
    -        site.manifest.metadata.site.settings.gaID = filter_var(
    -        req.body['manifest']['seo']['manifest-metadata-site-settings-gaID'],
    -        "FILTER_SANITIZE_STRING"
    -        );
    -      }
    -      site.manifest.metadata.site.updated = Math.floor(Date.now() / 1000);
    -      // don't reorganize the structure
    -      await site.manifest.save(false);
    -      await site.gitCommit('Manifest updated');
    -      // rebuild the files that twig processes
    -      await site.rebuildManagedFiles();
    -      site.updateAlternateFormats();
    -      await site.gitCommit('Managed files updated');
    -      res.send(site.manifest);
    -    }
    -    else {
    +        }
    +        if (typeof req.body['manifest']['seo']['manifest-metadata-site-settings-gaID'] !== 'undefined') {
    +          site.manifest.metadata.site.settings.gaID = filter_var(
    +          req.body['manifest']['seo']['manifest-metadata-site-settings-gaID'],
    +          "FILTER_SANITIZE_STRING"
    +          );
    +        }
    +        site.manifest.metadata.site.updated = Math.floor(Date.now() / 1000);
    +        // don't reorganize the structure
    +        await site.manifest.save(false);
    +        await site.gitCommit('Manifest updated');
    +        // rebuild the files that twig processes
    +        await site.rebuildManagedFiles();
    +        site.updateAlternateFormats();
    +        await site.gitCommit('Managed files updated');
    +        res.send(site.manifest);
    +      }
    +      else {
    +        res.sendStatus(403);
    +      }
    +    } else {
           res.sendStatus(403);
         }
       }
    
  • src/routes/saveNode.js+275 267 modified
    @@ -19,281 +19,289 @@ const strip_tags = require("locutus/php/strings/strip_tags");
        * )
        */
       async function saveNode(req, res) {
    -    let bodyParams = req.body;
    -    let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    let schema = [];
    -    let body;
    -    if ((bodyParams['node']['body'])) {
    -      body = bodyParams['node']['body'];
    -      // we ship the schema with the body
    -      if ((bodyParams['node']['schema'])) {
    -        schema = bodyParams['node']['schema'];
    -      }
    -    }
    -    let details = {};
    -    // if we have details object then merge configure and advanced
    -    if ((bodyParams['node']['details'])) {
    -      for (var key in bodyParams['node']['details']['node']['configure']) {
    -        details[key] = bodyParams['node']['details']['node']['configure'][key];
    +    if (req.query['site_token'] && HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + req.body['site']['name'])) {
    +      let bodyParams = req.body;
    +      let site = await HAXCMS.loadSite(req.body['site']['name']);
    +      let schema = [];
    +      let body;
    +      if (bodyParams['node']['body']) {
    +        body = bodyParams['node']['body'];
    +        // we ship the schema with the body
    +        if (bodyParams['node']['schema']) {
    +          schema = bodyParams['node']['schema'];
    +        }
           }
    -      for (var key in bodyParams['node']['details']['node']['advanced']) {
    -        details[key] = bodyParams['node']['details']['node']['advanced'][key];
    +      let details = {};
    +      // if we have details object then merge configure and advanced
    +      if (bodyParams['node']['details']) {
    +        for (var key in bodyParams['node']['details']['node']['configure']) {
    +          details[key] = bodyParams['node']['details']['node']['configure'][key];
    +        }
    +        for (var key in bodyParams['node']['details']['node']['advanced']) {
    +          details[key] = bodyParams['node']['details']['node']['advanced'][key];
    +        }
           }
    -    }
    -    // update the page's content, using manifest to find it
    -    // this ensures that writing is always to what the file system
    -    // determines to be the correct page
    -    // @todo review this step by step
    -    let page = site.loadNode(bodyParams['node']['id'])
    -    if (page) {
    -      // convert web location for loading into file location for writing
    -      if ((body)) {
    -        let bytes = 0;
    -        // see if we have multiple pages / this page has been told to split into multiple
    -        let pageData = HAXCMS.pageBreakParser(body);
    -        for (var i in pageData) {
    -          let data = pageData[i];
    -          // trap to ensure if front-end didnt send a UUID for id then we make it
    -          if (!(data["attributes"]["title"])) {
    -            data["attributes"]["title"] = 'New page';
    -          }
    -          // to avoid critical error in parsing, we defer to the POST's ID always
    -          // this also blocks multiple page breaks if it doesn't exist as we don't allow
    -          // the front end to dictate what gets created here
    -          if (!(data["attributes"]["item-id"])) {
    -            data["attributes"]["item-id"] = bodyParams['node']['id'];
    -          }
    -          if (!(data["attributes"]["path"]) || data["attributes"]["path"] == '#') {
    -            data["attributes"]["path"] = data["attributes"]["title"];
    -          }
    -          // verify this pages does not exist; this is only possible if we parse multiple page-break
    -          // a capability that is not supported currently beyond experiments
    -          page = site.loadNode(data["attributes"]["item-id"]);
    -          if (!page) {
    -            // generate a new item based on the site
    -            let nodeParams = {
    -              "node" : {
    -                "title" : data["attributes"]["title"],
    -                "id" : data["attributes"]["item-id"],
    -                "location" : data["attributes"]["path"],
    -              }
    -            };
    -            item = site.itemFromParams(nodeParams);
    -            // generate the boilerplate to fill this page
    -            HAXCMS.recurseCopy(
    -              HAXCMS.boilerplatePath + 'page/default',
    -              path.join(site.siteDirectory, item.location.replace('/index.html', ''))
    -            );
    -            // add the item back into the outline schema
    -            site.manifest.addItem(item);
    -            site.manifest.save();
    -            await site.gitCommit('Page added:' + item.title + ' (' + item.id + ')');
    -            // possible the item-id had to be made by back end
    -            data["attributes"]["item-id"] = item.id;
    -          }
    -          // now this should exist if it didn't a minute ago
    -          page = site.loadNode(data["attributes"]["item-id"]);
    -          // @todo make sure that we stripped off page-break
    -          // and now save WITHOUT the top level page-break
    -          // to avoid duplication issues
    -          bytes = await page.writeLocation(data['content'], site.siteDirectory);
    -          if (bytes === false) {
    -            return {
    -              '__failed' : {
    -                'status' : 500,
    -                'message' : 'failed to write',
    -              }
    -            };
    -          } else {
    -              // sanity check
    -              if (!(page.metadata)) {
    -                page.metadata = {};
    -              }
    -              // update attributes in the page
    -              if ((data["attributes"]["title"])) {
    -                page.title = data["attributes"]["title"];
    -              }
    -              if ((data["attributes"]["slug"])) {
    -                // account for x being the only front end reserved route
    -                if (data["attributes"]["slug"] == "x") {
    -                  data["attributes"]["slug"] = "x-x";
    -                }
    -                // same but trying to force a sub-route; paths cannot conflict with front end
    -                if (data["attributes"]["slug"].substring(0, 2) == "x/") {
    -                  data["attributes"]["slug"] = data["attributes"]["slug"].replace('x/', 'x-x/')
    -                }
    -                // machine name should more aggressively scrub the slug than clean title
    -                // @todo need to verify this doesn't already exist
    -                page.slug = HAXCMS.generateSlugName(data["attributes"]["slug"]);
    -              }
    -              if ((data["attributes"]["parent"])) {
    -                page.parent = data["attributes"]["parent"];
    -              }
    -              else {
    -                page.parent = null;
    -              }
    -              // allow setting theme via page break
    -              if ((data["attributes"]["developer-theme"]) && data["attributes"]["developer-theme"] != '') {
    -                let themes = HAXCMS.getThemes();
    -                let value = filter_var(data["attributes"]["developer-theme"], "FILTER_SANITIZE_STRING");
    -                // support for removing the custom theme or applying none
    -                if (value == '_none_' || value == '' || !value || !themes[value]) {
    -                  delete page.metadata.theme;
    +      // update the page's content, using manifest to find it
    +      // this ensures that writing is always to what the file system
    +      // determines to be the correct page
    +      // @todo review this step by step
    +      let page = site.loadNode(bodyParams['node']['id'])
    +      if (page) {
    +        // convert web location for loading into file location for writing
    +        if (body) {
    +          let bytes = 0;
    +          // see if we have multiple pages / this page has been told to split into multiple
    +          let pageData = HAXCMS.pageBreakParser(body);
    +          for (var i in pageData) {
    +            let data = pageData[i];
    +            // trap to ensure if front-end didnt send a UUID for id then we make it
    +            if (!(data["attributes"]["title"])) {
    +              data["attributes"]["title"] = 'New page';
    +            }
    +            // to avoid critical error in parsing, we defer to the POST's ID always
    +            // this also blocks multiple page breaks if it doesn't exist as we don't allow
    +            // the front end to dictate what gets created here
    +            if (!(data["attributes"]["item-id"])) {
    +              data["attributes"]["item-id"] = bodyParams['node']['id'];
    +            }
    +            if (!(data["attributes"]["path"]) || data["attributes"]["path"] == '#') {
    +              data["attributes"]["path"] = data["attributes"]["title"];
    +            }
    +            // verify this pages does not exist; this is only possible if we parse multiple page-break
    +            // a capability that is not supported currently beyond experiments
    +            page = site.loadNode(data["attributes"]["item-id"]);
    +            if (!page) {
    +              // generate a new item based on the site
    +              let nodeParams = {
    +                "node" : {
    +                  "title" : data["attributes"]["title"],
    +                  "id" : data["attributes"]["item-id"],
    +                  "location" : data["attributes"]["path"],
                     }
    -                // ensure it exists
    -                else if (themes[value]) {
    -                  page.metadata.theme = themes[value];
    -                  page.metadata.theme.key = value;
    -                }
    -              }
    -              else if ((page.metadata.theme)) {
    -                delete page.metadata.theme;
    -              }
    -              if ((data["attributes"]["depth"])) {
    -                page.indent = parseInt(data["attributes"]["depth"]);
    -              }
    -              if ((data["attributes"]["order"])) {
    -                page.order = parseInt(data["attributes"]["order"]);
    -              }
    -              // boolean so these are either there or not
    -              // historically we are published if this value is not set
    -              // and that will remain true however as we save / update pages
    -              // this will ensure that we set things to published
    -              if ((data["attributes"]["published"])) {
    -                page.metadata.published = true;
    -              }
    -              else {
    -                page.metadata.published = false;
    -              }
    -              // support for defining and updating page type
    -              if ((data["attributes"]["page-type"]) && data["attributes"]["page-type"] != '') {
    -                page.metadata.pageType = data["attributes"]["page-type"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.pageType)) {
    -                delete page.metadata.pageType;
    -              }
    -              // support for defining and updating hideInMenu which behaves strangely on DOM side for whatever reason
    -              if (typeof data["attributes"]["hide-in-menu"] !== 'undefined') {
    -                page.metadata.hideInMenu = true;
    -              }
    -              else {
    -                page.metadata.hideInMenu = false;
    -              }
    -              // support for defining and updating related-items
    -              if ((data["attributes"]["related-items"]) && data["attributes"]["related-items"] != '') {
    -                page.metadata.relatedItems = data["attributes"]["related-items"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.relatedItems)) {
    -                delete page.metadata.relatedItems;
    -              }
    -              // support for defining and updating image
    -              if ((data["attributes"]["image"]) && data["attributes"]["image"] != '') {
    -                page.metadata.image = data["attributes"]["image"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.image)) {
    -                delete page.metadata.image;
    -              }
    -              // support for defining and updating page type
    -              if ((data["attributes"]["tags"]) && data["attributes"]["tags"] != '') {
    -                page.metadata.tags = data["attributes"]["tags"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.tags)) {
    -                delete page.metadata.tags;
    -              }
    -              // support for defining and updating page accentColor
    -              if ((data["attributes"]["accent-color"]) && data["attributes"]["accent-color"] != '') {
    -                page.metadata.accentColor = data["attributes"]["accent-color"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.accentColor)) {
    -                delete page.metadata.accentColor;
    -              }
    -              // support for defining and updating page type
    -              if ((data["attributes"]["icon"]) && data["attributes"]["icon"] != '') {
    -                page.metadata.icon = data["attributes"]["icon"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.icon)) {
    -                delete page.metadata.icon;
    -              }
    -              // support for defining an image to represent the page
    -              if ((data["attributes"]["image"]) && data["attributes"]["image"] != '') {
    -                page.metadata.image = data["attributes"]["image"];
    -              }
    -              // they sent across nothing but we had something previously
    -              else if ((page.metadata.image)) {
    -                delete page.metadata.image;
    -              }
    -              if (!(data["attributes"]["locked"])) {
    -                page.metadata.locked = false;
    -              }
    -              else {
    -                page.metadata.locked = true;
    -              }
    -              // update the updated timestamp
    -              page.metadata.updated = Math.floor(Date.now() / 1000);
    -              let clean = strip_tags(body);
    -              // auto generate a text only description from first 200 chars
    -              // unless we were sent one to use
    -              if ((data["attributes"]["description"]) && data["attributes"]["description"] != '') {
    -                page.description = data["attributes"]["description"];
    -              }
    -              else {
    -                page.description = clean.substring(0, 200).replace("\n", '');
    -              }
    -              let readtime = Math.round(countWords(clean) / 200);
    -              // account for uber small body
    -              if (readtime == 0) {
    -                readtime = 1;
    -              }
    -              page.metadata.readtime = readtime;
    -              // reset bc we rebuild this each page save
    -              page.metadata.videos = {};
    -              page.metadata.images = {};
    -              // pull schema apart and seee if we have any images
    -              // that other things could use for metadata / theming purposes
    -              for (var element in schema) {
    -                switch(element['tag']) {
    -                  case 'img':
    -                    if ((element['properties']['src'])) {
    -                      page.metadata.images.push(element['properties']['src']);
    -                    }
    -                  break;
    -                  case 'a11y-gif-player':
    -                    if ((element['properties']['src'])) {
    -                      page.metadata.images.push(element['properties']['src']);
    -                    }
    -                  break;
    -                  case 'media-image':
    -                    if ((element['properties']['source'])) {
    -                      page.metadata.images.push(element['properties']['source']);
    -                    }
    -                  break;
    -                  case 'video-player':
    -                    if ((element['properties']['source'])) {
    -                      page.metadata.videos.push(element['properties']['source']);
    -                    }
    -                  break;
    -                }
    -              }
    -              await site.updateNode(page);
    -              site.manifest.metadata.site.updated = Math.floor(Date.now() / 1000);
    -              await site.manifest.save();
    -              await site.gitCommit(
    -                'Page details updated: ' + page.title + ' (' + page.id + ')'
    +              };
    +              item = site.itemFromParams(nodeParams);
    +              // generate the boilerplate to fill this page
    +              HAXCMS.recurseCopy(
    +                HAXCMS.boilerplatePath + 'page/default',
    +                path.join(site.siteDirectory, item.location.replace('/index.html', ''))
                   );
    +              // add the item back into the outline schema
    +              site.manifest.addItem(item);
    +              site.manifest.save();
    +              await site.gitCommit('Page added:' + item.title + ' (' + item.id + ')');
    +              // possible the item-id had to be made by back end
    +              data["attributes"]["item-id"] = item.id;
    +            }
    +            // now this should exist if it didn't a minute ago
    +            page = site.loadNode(data["attributes"]["item-id"]);
    +            // @todo make sure that we stripped off page-break
    +            // and now save WITHOUT the top level page-break
    +            // to avoid duplication issues
    +            bytes = await page.writeLocation(data['content'], site.siteDirectory);
    +            if (bytes === false) {
    +              return {
    +                '__failed' : {
    +                  'status' : 500,
    +                  'message' : 'failed to write',
    +                }
    +              };
    +            } else {
    +                // sanity check
    +                if (!(page.metadata)) {
    +                  page.metadata = {};
    +                }
    +                // update attributes in the page
    +                if (data["attributes"]["title"]) {
    +                  page.title = data["attributes"]["title"];
    +                }
    +                if (data["attributes"]["slug"]) {
    +                  // account for x being the only front end reserved route
    +                  if (data["attributes"]["slug"] == "x") {
    +                    data["attributes"]["slug"] = "x-x";
    +                  }
    +                  // same but trying to force a sub-route; paths cannot conflict with front end
    +                  if (data["attributes"]["slug"].substring(0, 2) == "x/") {
    +                    data["attributes"]["slug"] = data["attributes"]["slug"].replace('x/', 'x-x/')
    +                  }
    +                  // machine name should more aggressively scrub the slug than clean title
    +                  // @todo need to verify this doesn't already exist
    +                  page.slug = HAXCMS.generateSlugName(data["attributes"]["slug"]);
    +                }
    +                if ((data["attributes"]["parent"])) {
    +                  page.parent = data["attributes"]["parent"];
    +                }
    +                else {
    +                  page.parent = null;
    +                }
    +                // allow setting theme via page break
    +                if ((data["attributes"]["developer-theme"]) && data["attributes"]["developer-theme"] != '') {
    +                  let themes = HAXCMS.getThemes();
    +                  let value = filter_var(data["attributes"]["developer-theme"], "FILTER_SANITIZE_STRING");
    +                  // support for removing the custom theme or applying none
    +                  if (value == '_none_' || value == '' || !value || !themes[value]) {
    +                    delete page.metadata.theme;
    +                  }
    +                  // ensure it exists
    +                  else if (themes[value]) {
    +                    page.metadata.theme = themes[value];
    +                    page.metadata.theme.key = value;
    +                  }
    +                }
    +                else if ((page.metadata.theme)) {
    +                  delete page.metadata.theme;
    +                }
    +                if ((data["attributes"]["depth"])) {
    +                  page.indent = parseInt(data["attributes"]["depth"]);
    +                }
    +                if ((data["attributes"]["order"])) {
    +                  page.order = parseInt(data["attributes"]["order"]);
    +                }
    +                // boolean so these are either there or not
    +                // historically we are published if this value is not set
    +                // and that will remain true however as we save / update pages
    +                // this will ensure that we set things to published
    +                if ((data["attributes"]["published"])) {
    +                  page.metadata.published = true;
    +                }
    +                else {
    +                  page.metadata.published = false;
    +                }
    +                // support for defining and updating page type
    +                if ((data["attributes"]["page-type"]) && data["attributes"]["page-type"] != '') {
    +                  page.metadata.pageType = data["attributes"]["page-type"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.pageType)) {
    +                  delete page.metadata.pageType;
    +                }
    +                // support for defining and updating hideInMenu which behaves strangely on DOM side for whatever reason
    +                if (typeof data["attributes"]["hide-in-menu"] !== 'undefined') {
    +                  page.metadata.hideInMenu = true;
    +                }
    +                else {
    +                  page.metadata.hideInMenu = false;
    +                }
    +                // support for defining and updating related-items
    +                if ((data["attributes"]["related-items"]) && data["attributes"]["related-items"] != '') {
    +                  page.metadata.relatedItems = data["attributes"]["related-items"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.relatedItems)) {
    +                  delete page.metadata.relatedItems;
    +                }
    +                // support for defining and updating image
    +                if ((data["attributes"]["image"]) && data["attributes"]["image"] != '') {
    +                  page.metadata.image = data["attributes"]["image"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.image)) {
    +                  delete page.metadata.image;
    +                }
    +                // support for defining and updating page type
    +                if ((data["attributes"]["tags"]) && data["attributes"]["tags"] != '') {
    +                  page.metadata.tags = data["attributes"]["tags"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.tags)) {
    +                  delete page.metadata.tags;
    +                }
    +                // support for defining and updating page accentColor
    +                if ((data["attributes"]["accent-color"]) && data["attributes"]["accent-color"] != '') {
    +                  page.metadata.accentColor = data["attributes"]["accent-color"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.accentColor)) {
    +                  delete page.metadata.accentColor;
    +                }
    +                // support for defining and updating page type
    +                if ((data["attributes"]["icon"]) && data["attributes"]["icon"] != '') {
    +                  page.metadata.icon = data["attributes"]["icon"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.icon)) {
    +                  delete page.metadata.icon;
    +                }
    +                // support for defining an image to represent the page
    +                if ((data["attributes"]["image"]) && data["attributes"]["image"] != '') {
    +                  page.metadata.image = data["attributes"]["image"];
    +                }
    +                // they sent across nothing but we had something previously
    +                else if ((page.metadata.image)) {
    +                  delete page.metadata.image;
    +                }
    +                if (!(data["attributes"]["locked"])) {
    +                  page.metadata.locked = false;
    +                }
    +                else {
    +                  page.metadata.locked = true;
    +                }
    +                // update the updated timestamp
    +                page.metadata.updated = Math.floor(Date.now() / 1000);
    +                let clean = strip_tags(body);
    +                // auto generate a text only description from first 200 chars
    +                // unless we were sent one to use
    +                if ((data["attributes"]["description"]) && data["attributes"]["description"] != '') {
    +                  page.description = data["attributes"]["description"];
    +                }
    +                else {
    +                  page.description = clean.substring(0, 200).replace("\n", '');
    +                }
    +                let readtime = Math.round(countWords(clean) / 200);
    +                // account for uber small body
    +                if (readtime == 0) {
    +                  readtime = 1;
    +                }
    +                page.metadata.readtime = readtime;
    +                // reset bc we rebuild this each page save
    +                page.metadata.videos = {};
    +                page.metadata.images = {};
    +                // pull schema apart and seee if we have any images
    +                // that other things could use for metadata / theming purposes
    +                for (var element in schema) {
    +                  switch(element['tag']) {
    +                    case 'img':
    +                      if ((element['properties']['src'])) {
    +                        page.metadata.images.push(element['properties']['src']);
    +                      }
    +                    break;
    +                    case 'a11y-gif-player':
    +                      if ((element['properties']['src'])) {
    +                        page.metadata.images.push(element['properties']['src']);
    +                      }
    +                    break;
    +                    case 'media-image':
    +                      if ((element['properties']['source'])) {
    +                        page.metadata.images.push(element['properties']['source']);
    +                      }
    +                    break;
    +                    case 'video-player':
    +                      if ((element['properties']['source'])) {
    +                        page.metadata.videos.push(element['properties']['source']);
    +                      }
    +                    break;
    +                  }
    +                }
    +                await site.updateNode(page);
    +                site.manifest.metadata.site.updated = Math.floor(Date.now() / 1000);
    +                await site.manifest.save();
    +                await site.gitCommit(
    +                  'Page details updated: ' + page.title + ' (' + page.id + ')'
    +                );
    +            }
               }
    +          res.send({
    +            status: 200,
    +            data: page
    +          });
             }
    -        res.send({
    -          status: 200,
    -          data: page
    -        });
    +      }
    +      else {
    +        res.sendStatus(500);
           }
         }
    +    else {
    +      res.sendStatus(403);
    +    } 
       }
       function countWords(str) {
         return str.trim().split(/\s+/).length;
    
  • src/routes/saveOutline.js+183 179 modified
    @@ -19,198 +19,202 @@ const JSONOutlineSchemaItem = require('../lib/JSONOutlineSchemaItem.js');
        * )
        */
       async function saveOutline(req, res) {
    -    // items from the POST
    -    let site = await HAXCMS.loadSite(req.body['site']['name']);
    -    let original = [...site.manifest.items];
    -    let items = [...req.body['items']];
    -    let itemMap = {};
    -    var page, bytes, cleanTitle;
    -    // items from the POST
    -    for (var key in items) {
    -      let item = items[key];
    -      page = site.loadNode(item.id);
    -      // get a fake item of the existing
    -      if (!page) {
    -        page = HAXCMS.outlineSchema.newItem();
    -        // we don't trust the front end UUID if it wasn't existing already
    -        itemMap[item.id] = page.id;
    -      }
    -      // set a title if we have one
    -      if (item.title != '' && item.title) {
    -        page.title = item.title;
    -      }
    -      cleanTitle = HAXCMS.cleanTitle(page.title);
    -      if (item.parent == null) {
    -        page.parent = null;
    -        page.indent = 0;
    -      } else {
    -        // check the item map as backend dictates unique ID
    -        if (typeof itemMap[item.parent] !== 'undefined') {
    -          page.parent = itemMap[item.parent];
    +    if (req.query['site_token'] && HAXCMS.validateRequestToken(req.query['site_token'], HAXCMS.getActiveUserName() + ':' + req.body['site']['name'])) {
    +      // items from the POST
    +      let site = await HAXCMS.loadSite(req.body['site']['name']);
    +      let original = [...site.manifest.items];
    +      let items = [...req.body['items']];
    +      let itemMap = {};
    +      var page, bytes, cleanTitle;
    +      // items from the POST
    +      for (var key in items) {
    +        let item = items[key];
    +        page = site.loadNode(item.id);
    +        // get a fake item of the existing
    +        if (!page) {
    +          page = HAXCMS.outlineSchema.newItem();
    +          // we don't trust the front end UUID if it wasn't existing already
    +          itemMap[item.id] = page.id;
    +        }
    +        // set a title if we have one
    +        if (item.title != '' && item.title) {
    +          page.title = item.title;
    +        }
    +        cleanTitle = HAXCMS.cleanTitle(page.title);
    +        if (item.parent == null) {
    +          page.parent = null;
    +          page.indent = 0;
             } else {
    -          // set to the parent id
    -          page.parent = item.parent;
    +          // check the item map as backend dictates unique ID
    +          if (typeof itemMap[item.parent] !== 'undefined') {
    +            page.parent = itemMap[item.parent];
    +          } else {
    +            // set to the parent id
    +            page.parent = item.parent;
    +          }
    +          // move it one indentation below the parent; this can be changed later if desired
    +          page.indent = item.indent;
             }
    -        // move it one indentation below the parent; this can be changed later if desired
    -        page.indent = item.indent;
    -      }
    -      if (typeof item.order !== 'undefined') {
    -        page.order = parseInt(item.order);
    -      } else {
    -        page.order = parseInt(key);
    -      }
    -      // keep location if we get one already
    -      if (typeof item.location !== 'undefined' && item.location != '') {
    -        page.location = item.location;
    -      } else {
    -        // generate a logical page slug
    -        page.location = 'pages/' + page.id + '/index.html';
    -      }
    -      // keep location if we get one already
    -      if (typeof item.slug !== 'undefined' && item.slug != '') {
    -      } else {
    +        if (typeof item.order !== 'undefined') {
    +          page.order = parseInt(item.order);
    +        } else {
    +          page.order = parseInt(key);
    +        }
    +        // keep location if we get one already
    +        if (typeof item.location !== 'undefined' && item.location != '') {
    +          page.location = item.location;
    +        } else {
               // generate a logical page slug
    -          page.slug = site.getUniqueSlugName(cleanTitle, page, true);
    -      }
    -      // verify this exists, front end could have set what they wanted
    -      // or it could have just been renamed
    -      // if it doesn't exist currently make sure the name is unique
    -      let tmpLoad = site.loadNode(page.id);
    -      if (!tmpLoad) {
    -        await HAXCMS.recurseCopy(
    -            HAXCMS.boilerplatePath + 'page/default',
    -            site.siteDirectory + '/' + page.location.replace('/index.html', '')
    -        );
    -      }
    -      // this would imply existing item, lets see if it moved or needs moved
    -      else {
    -          let moved = false;
    -          for( var moveKey in original) {
    -            let tmpItem = original[moveKey];
    -              // see if this is something moving as opposed to brand new
    -              if (
    -                  tmpItem.id == page.id &&
    -                  tmpItem.slug != ''
    -              ) {
    -                  // core support for automatically managing paths to make them nice
    -                  if (typeof site.manifest.metadata.site.settings.pathauto !== 'undefined' && site.manifest.metadata.site.settings.pathauto) {
    -                      moved = true;
    -                      page.slug = site.getUniqueSlugName(HAXCMS.cleanTitle(page.title), page, true);
    -                  }
    -                  else if (tmpItem.slug != page.slug) {
    -                      moved = true;
    -                      page.slug = HAXCMS.generateSlugName(tmpItem.slug);
    -                  }
    -              }
    -          }
    -          // it wasn't moved and it doesn't exist... let's fix that
    -          // this is beyond an edge case
    -          if (
    -            !moved &&
    -            !fs.existsSync(site.siteDirectory + '/' + page.location)
    -        ) {
    -              pAuto = false;
    -              if (typeof site.manifest.metadata.site.settings.pathauto !== 'undefined' && site.manifest.metadata.site.settings.pathauto) {
    -                pAuto = true;
    -              }
    -              tmpTitle = site.getUniqueSlugName(cleanTitle, page, pAuto);
    -              page.location = 'pages/' + page.id + '/index.html';
    -              page.slug = tmpTitle;
    -              await HAXCMS.recurseCopy(
    -                  HAXCMS.boilerplatePath + 'page/default',
    -                  site.siteDirectory + '/' + page.location.replace('/index.html', '')
    -              );
    -          }
    -      }
    -      // check for any metadata keys that did come over
    -      for (let pageKey in item.metadata) {
    -          page.metadata[pageKey] = item.metadata[pageKey];
    -      }
    -      // safety check for new things
    -      if (typeof page.metadata.created === 'undefined') {
    -          page.metadata.created = Math.floor(Date.now() / 1000);
    -          page.metadata.images = [];
    -          page.metadata.videos = [];
    -      }
    -      // always update at this time
    -      page.metadata.updated = Math.floor(Date.now() / 1000);
    -      let tmp = site.loadNode(page.id);
    -      if (tmp) {
    -        await site.updateNode(page);
    -      } else {
    -        site.manifest.addItem(page);
    -        await site.manifest.save(false);
    -      }
    -    }
    -    // process any duplicate / contents requests we had now that structure is sane
    -    // including potentially duplication of material from something
    -    // we are about to act on and now that we have the map
    -    items = [...req.body['items']];
    -    for (let dupKey in items) {
    -      let item = items[dupKey];
    -      // load the item, or the item as built out of the itemMap
    -      // since we reset the UUID on creation
    -      page = site.loadNode(item.id);
    -      if (!page) {
    -        page = site.loadNode(itemMap[item.id]);
    -      }
    -      if (typeof item.duplicate !== 'undefined') {
    -        let nodeToDuplicate = site.loadNode(item.duplicate);
    -        // load the node we are duplicating with support for the same map needed for page loading
    -        if (!nodeToDuplicate) {
    -          nodeToDuplicate = site.loadNode(itemMap[item.duplicate]);
    -        }
    -        let content = await site.getPageContent(nodeToDuplicate);
    -        // write it to the file system
    -        bytes = await page.writeLocation(content, site.siteDirectory);
    -      }
    -      // contents that were shipped across, and not null, take priority over a dup request
    -      if (typeof item.contents !== 'undefined' && item.contents && item.contents != '') {
    -        // write it to the file system
    -        bytes = await page.writeLocation(item.contents, site.siteDirectory);
    +          page.location = 'pages/' + page.id + '/index.html';
    +        }
    +        // keep location if we get one already
    +        if (typeof item.slug !== 'undefined' && item.slug != '') {
    +        } else {
    +            // generate a logical page slug
    +            page.slug = site.getUniqueSlugName(cleanTitle, page, true);
    +        }
    +        // verify this exists, front end could have set what they wanted
    +        // or it could have just been renamed
    +        // if it doesn't exist currently make sure the name is unique
    +        let tmpLoad = site.loadNode(page.id);
    +        if (!tmpLoad) {
    +          await HAXCMS.recurseCopy(
    +              HAXCMS.boilerplatePath + 'page/default',
    +              site.siteDirectory + '/' + page.location.replace('/index.html', '')
    +          );
    +        }
    +        // this would imply existing item, lets see if it moved or needs moved
    +        else {
    +            let moved = false;
    +            for( var moveKey in original) {
    +              let tmpItem = original[moveKey];
    +                // see if this is something moving as opposed to brand new
    +                if (
    +                    tmpItem.id == page.id &&
    +                    tmpItem.slug != ''
    +                ) {
    +                    // core support for automatically managing paths to make them nice
    +                    if (typeof site.manifest.metadata.site.settings.pathauto !== 'undefined' && site.manifest.metadata.site.settings.pathauto) {
    +                        moved = true;
    +                        page.slug = site.getUniqueSlugName(HAXCMS.cleanTitle(page.title), page, true);
    +                    }
    +                    else if (tmpItem.slug != page.slug) {
    +                        moved = true;
    +                        page.slug = HAXCMS.generateSlugName(tmpItem.slug);
    +                    }
    +                }
    +            }
    +            // it wasn't moved and it doesn't exist... let's fix that
    +            // this is beyond an edge case
    +            if (
    +              !moved &&
    +              !fs.existsSync(site.siteDirectory + '/' + page.location)
    +          ) {
    +                pAuto = false;
    +                if (typeof site.manifest.metadata.site.settings.pathauto !== 'undefined' && site.manifest.metadata.site.settings.pathauto) {
    +                  pAuto = true;
    +                }
    +                tmpTitle = site.getUniqueSlugName(cleanTitle, page, pAuto);
    +                page.location = 'pages/' + page.id + '/index.html';
    +                page.slug = tmpTitle;
    +                await HAXCMS.recurseCopy(
    +                    HAXCMS.boilerplatePath + 'page/default',
    +                    site.siteDirectory + '/' + page.location.replace('/index.html', '')
    +                );
    +            }
    +        }
    +        // check for any metadata keys that did come over
    +        for (let pageKey in item.metadata) {
    +            page.metadata[pageKey] = item.metadata[pageKey];
    +        }
    +        // safety check for new things
    +        if (typeof page.metadata.created === 'undefined') {
    +            page.metadata.created = Math.floor(Date.now() / 1000);
    +            page.metadata.images = [];
    +            page.metadata.videos = [];
    +        }
    +        // always update at this time
    +        page.metadata.updated = Math.floor(Date.now() / 1000);
    +        let tmp = site.loadNode(page.id);
    +        if (tmp) {
    +          await site.updateNode(page);
    +        } else {
    +          site.manifest.addItem(page);
    +          await site.manifest.save(false);
    +        }
           }
    -    }
    -    items = [...req.body['items']];
    -    // now, we can finally delete as content operations have finished
    -    for (let delKey in items) {
    -      let item = items[delKey];
    -      // verify if we were told to delete this item via flag not in the real spec
    -      if (typeof item.delete !== 'undefined' && item.delete === true) {
    +      // process any duplicate / contents requests we had now that structure is sane
    +      // including potentially duplication of material from something
    +      // we are about to act on and now that we have the map
    +      items = [...req.body['items']];
    +      for (let dupKey in items) {
    +        let item = items[dupKey];
             // load the item, or the item as built out of the itemMap
             // since we reset the UUID on creation
             page = site.loadNode(item.id);
             if (!page) {
               page = site.loadNode(itemMap[item.id]);
             }
    -        await site.deleteNode(page);
    -        await site.gitCommit(
    -          'Page deleted: ' + page.title + ' (' + page.id + ')'
    -        );
    +        if (typeof item.duplicate !== 'undefined') {
    +          let nodeToDuplicate = site.loadNode(item.duplicate);
    +          // load the node we are duplicating with support for the same map needed for page loading
    +          if (!nodeToDuplicate) {
    +            nodeToDuplicate = site.loadNode(itemMap[item.duplicate]);
    +          }
    +          let content = await site.getPageContent(nodeToDuplicate);
    +          // write it to the file system
    +          bytes = await page.writeLocation(content, site.siteDirectory);
    +        }
    +        // contents that were shipped across, and not null, take priority over a dup request
    +        if (typeof item.contents !== 'undefined' && item.contents && item.contents != '') {
    +          // write it to the file system
    +          bytes = await page.writeLocation(item.contents, site.siteDirectory);
    +        }
           }
    -    }
    -    await site.manifest.save();
    -    // now, we need to look for orphans if we deleted anything
    -    let orphanCheck = [...site.manifest.items];
    -    for (let orKey in orphanCheck) {
    -      let item = orphanCheck[orKey];
    -      // just to be safe..
    -      page = site.loadNode(item.id)
    -      if (page && page.parent != null) {
    -        let parentPage = site.loadNode(page.parent);
    -        // ensure that parent is valid to rescue orphan items
    -        if (!parentPage) {
    -          page.parent = null;
    -          // force to bottom of things while still being in old order if lots of things got axed
    -          page.order = parseInt(page.order) + site.manifest.items.length - 1;
    -          await site.updateNode(page);
    +      items = [...req.body['items']];
    +      // now, we can finally delete as content operations have finished
    +      for (let delKey in items) {
    +        let item = items[delKey];
    +        // verify if we were told to delete this item via flag not in the real spec
    +        if (typeof item.delete !== 'undefined' && item.delete === true) {
    +          // load the item, or the item as built out of the itemMap
    +          // since we reset the UUID on creation
    +          page = site.loadNode(item.id);
    +          if (!page) {
    +            page = site.loadNode(itemMap[item.id]);
    +          }
    +          await site.deleteNode(page);
    +          await site.gitCommit(
    +            'Page deleted: ' + page.title + ' (' + page.id + ')'
    +          );
    +        }
    +      }
    +      await site.manifest.save();
    +      // now, we need to look for orphans if we deleted anything
    +      let orphanCheck = [...site.manifest.items];
    +      for (let orKey in orphanCheck) {
    +        let item = orphanCheck[orKey];
    +        // just to be safe..
    +        page = site.loadNode(item.id)
    +        if (page && page.parent != null) {
    +          let parentPage = site.loadNode(page.parent);
    +          // ensure that parent is valid to rescue orphan items
    +          if (!parentPage) {
    +            page.parent = null;
    +            // force to bottom of things while still being in old order if lots of things got axed
    +            page.order = parseInt(page.order) + site.manifest.items.length - 1;
    +            await site.updateNode(page);
    +          }
             }
           }
    +      site.manifest.metadata.site.updated = Math.floor(Date.now() / 1000);
    +      await site.manifest.save();
    +      // update alt formats like rss as we did massive changes
    +      await site.updateAlternateFormats();
    +      await site.gitCommit('Outline updated in bulk');
    +      res.send(site.manifest.items);
    +    } else {
    +      res.sendStatus(403);
         }
    -    site.manifest.metadata.site.updated = Math.floor(Date.now() / 1000);
    -    await site.manifest.save();
    -    // update alt formats like rss as we did massive changes
    -    await site.updateAlternateFormats();
    -    await site.gitCommit('Outline updated in bulk');
    -    res.send(site.manifest.items);
       }
       module.exports = saveOutline;
    \ No newline at end of file
    
24d30222481a

https://github.com/haxtheweb/issues/security/advisories/GHSA-9jr9-8ff3-m894 consistency w/ responses in node

https://github.com/haxtheweb/haxcms-phpbtoproJul 23, 2025via ghsa
2 files changed · +50 11
  • system/backend/php/lib/HAXCMS.php+2 2 modified
    @@ -852,7 +852,7 @@ public function pageBreakParser($body = '<page-break></page-break>') {
         /**
          * Generate a valid HAX App store specification schema for connecting to this site via JSON.
          */
    -    public function siteConnectionJSON($siteToken = '', $siteName = '')
    +    public function siteConnectionJSON($siteToken = '')
         {
             return '{
           "details": {
    @@ -869,7 +869,7 @@ public function siteConnectionJSON($siteToken = '', $siteName = '')
             "operations": {
               "browse": {
                 "method": "GET",
    -            "endPoint": "system/api/listFiles?site_token=' . $siteToken . '&siteName=' . $siteName . '",
    +            "endPoint": "system/api/listFiles?site_token=' . $siteToken . '",
                 "pagination": {
                   "style": "link",
                   "props": {
    
  • system/backend/php/lib/Operations.php+48 9 modified
    @@ -256,9 +256,8 @@ public function rebuildManagedFiles() {
        * )
        */
       public function saveManifest() {
    -    // load the site from name
         if (isset($this->params['site_token']) && $GLOBALS['HAXCMS']->validateRequestToken($this->params['site_token'], $GLOBALS['HAXCMS']->getActiveUserName() . ':' . $this->params['site']['name'])) {
    -
    +      // load the site from name
           $site = $GLOBALS['HAXCMS']->loadSite($this->params['site']['name']);
           // standard form submit
           // @todo 
    @@ -487,7 +486,7 @@ public function saveManifest() {
         else {
           return array(
             '__failed' => array(
    -          'status' => 500,
    +          'status' => 403,
               'message' => 'invalid site token',
             )
           );
    @@ -709,7 +708,7 @@ public function saveOutline() {
         } else {
           return array(
             '__failed' => array(
    -          'status' => 500,
    +          'status' => 403,
               'message' => 'invalid site token',
             )
           );
    @@ -884,8 +883,8 @@ public function createNode() {
         else {
           return array(
             '__failed' => array(
    -          'status' => 500,
    -          'message' => 'failed to create node',
    +          'status' => 403,
    +          'message' => 'invalid site token',
             )
           );
         }
    @@ -1278,8 +1277,8 @@ public function deleteNode() {
         else {
           return array(
             '__failed' => array(
    -          'status' => 500,
    -          'message' => 'failed to delete',
    +          'status' => 403,
    +          'message' => 'invalid site token',
             )
           );
         }
    @@ -1372,7 +1371,7 @@ public function generateAppStore() {
           }
           $appStore = $haxService->loadBaseAppStore($apikeys);
           // pull in the core one we supply, though only upload works currently
    -      $tmp = json_decode($GLOBALS['HAXCMS']->siteConnectionJSON($this->params['site_token'], $this->params['site']['name']));
    +      $tmp = json_decode($GLOBALS['HAXCMS']->siteConnectionJSON($this->params['site_token']));
           array_push($appStore, $tmp);
           if (isset($GLOBALS['HAXCMS']->config->appStore->stax)) {
               $staxList = $GLOBALS['HAXCMS']->config->appStore->stax;
    @@ -1452,6 +1451,14 @@ public function getUserData() {
             'data' => $GLOBALS['HAXCMS']->userData
           );
         }
    +    else {
    +      return array(
    +        '__failed' => array(
    +          'status' => 403,
    +          'message' => 'invalid request token',
    +        )
    +      );
    +    }
       }
       /**
        * @OA\Post(
    @@ -1904,6 +1911,14 @@ public function listSites() {
             "data" => $return
           );
         }
    +    else {
    +      return array(
    +        '__failed' => array(
    +          'status' => 403,
    +          'message' => 'invalid request token',
    +        )
    +      );
    +    }
       }
       /**
        * @OA\Post(
    @@ -2240,6 +2255,14 @@ public function cloneSite() {
             ),
           );
         }
    +    else {
    +      return array(
    +        '__failed' => array(
    +          'status' => 403,
    +          'message' => 'invalid request token',
    +        )
    +      );
    +    }
       }
       /**
        * @OA\Post(
    @@ -2324,6 +2347,14 @@ public function downloadSite() {
             )
           );
         }
    +    else {
    +      return array(
    +        '__failed' => array(
    +          'status' => 403,
    +          'message' => 'invalid request token',
    +        )
    +      );
    +    }
       }
       /**
        * @OA\Post(
    @@ -2383,5 +2414,13 @@ public function archiveSite() {
             );
           }
         }
    +    else {
    +      return array(
    +        '__failed' => array(
    +          'status' => 403,
    +          'message' => 'invalid request token',
    +        )
    +      );
    +    }
       }
     }
    \ No newline at end of file
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.