<commit_before>const getNodeVersion = () => {
  try {
    return parseFloat(process.version.replace(/v/, ""));
  } catch (err) {
    return null;
  }
};

const nodeVersion = getNodeVersion();

if (nodeVersion < 4.5) {
  console.log("prerendercloud requires node >= 4.5");
  process.exit(1);
}

require("./includes-polyfill");
if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === "[object Array]";
  };
}

const debug = require("debug")("prerendercloud");

const util = require("./lib/util");
const Url = require("./lib/Url");
const middlewareCacheSingleton = {};
const Options = require("./lib/Options");
const options = new Options(middlewareCacheSingleton);

const got = require("got-lite");
require("./lib/got-retries")(got, options, debug);

const vary = require("vary");

// preserve (and send to client) these headers from service.prerender.cloud which originally came from the origin server
const headerWhitelist = [
  "vary",
  "content-type",
  "cache-control",
  "strict-transport-security",
  "content-security-policy",
  "public-key-pins",
  "x-frame-options",
  "x-xss-protection",
  "x-content-type-options",
  "location"
];

const botsOnlyList = [
  "googlebot",
  "yahoo",
  "bingbot",
  "baiduspider",
  "facebookexternalhit",
  "twitterbot",
  "rogerbot",
  "linkedinbot",
  "embedly",
  "quora link preview",
  "showyoubot",
  "outbrain",
  "pinterest/0.",
  "pinterestbot",
  "developers.google.com/+/web/snippet",
  "slackbot",
  "vkShare",
  "W3C_Validator",
  "redditbot",
  "Applebot",
  "WhatsApp",
  "flipboard",
  "tumblr",
  "bitlybot",
  "Bitrix link preview",
  "Google Search Console"
].map(ua => ua.toLowerCase());

const userAgentIsBot = (headers, requestedPath = "") => {
  const reqUserAgent =
    (headers["user-agent"] && headers["user-agent"].toLowerCase()) || "";

  if (headers["x-bufferbot"]) return true;

  if (requestedPath.match(/[?&]_escaped_fragment_/)) return true;

  return botsOnlyList.some(enabledUserAgent =>
    reqUserAgent.includes(enabledUserAgent)
  );
};

const getServiceUrl = hardcoded =>
  (hardcoded && hardcoded.replace(/\/+$/, "")) ||
  process.env.PRERENDER_SERVICE_URL ||
  "https://service.prerender.cloud";
const getRenderUrl = (action, url) =>
  [getServiceUrl(), action, url].filter(p => p).join("/");

// from https://stackoverflow.com/a/41072596
// if there are multiple values with different case, it just takes the last
// (which is wrong, it should merge some of them according to the HTTP spec, but it's fine for now)
const objectKeysToLowerCase = function(origObj) {
  return Object.keys(origObj).reduce(function(newObj, key) {
    const val = origObj[key];
    const newVal = typeof val === "object" ? objectKeysToLowerCase(val) : val;
    newObj[key.toLowerCase()] = newVal;
    return newObj;
  }, {});
};

const is5xxError = statusCode => parseInt(statusCode / 100) === 5;
const is4xxError = statusCode => {
  const n = parseInt(statusCode);
  const i = parseInt(statusCode / 100);

  return i === 4 && n !== 429;
};
const isGotClientTimeout = err =>
  err.name === "RequestError" && err.code === "ETIMEDOUT";

const zlib = require("zlib");
function compression(req, res, data) {
  if (
    req.headers["accept-encoding"] &&
    req.headers["accept-encoding"].match(/gzip/i)
  ) {
    zlib.gzip(data.body, (err, gzipped) => {
      if (err) {
        console.error(err);
        return res.status(500).send("compression error");
      }

      res.writeHead(
        data.statusCode,
        Object.assign({}, data.headers, { "content-encoding": "gzip" })
      );
      res.end(gzipped);
    });
  } else {
    res.writeHead(data.statusCode, data.headers);
    res.end(data.body);
  }
}

const handleSkip = (msg, next) => {
  debug(msg);
  console.error("prerendercloud middleware SKIPPED:", msg);
  return next();
};

const concurrentRequestCache = {};

// response: { body, statusCode, headers }
function createResponse(requestedUrl, response) {
  const body = response.body;

  const lowerCasedHeaders = objectKeysToLowerCase(response.headers);

  const headers = {};
  headerWhitelist.forEach(h => {
    if (lowerCasedHeaders[h]) headers[h] = lowerCasedHeaders[h];
  });

  const data = { statusCode: response.statusCode, headers, body };

  if (
    options.options.enableMiddlewareCache &&
    `${response.statusCode}`.startsWith("2") &&
    body &&
    body.length
  )
    middlewareCacheSingleton.instance.set(requestedUrl, data);

  return data;
}

class Prerender {
  constructor(req) {
    this.req = req;
    this.url = Url.parse(req, options);
  }

  // promise cache wrapper around ._get to prevent concurrent requests to same URL
  get() {
    if (concurrentRequestCache[this.url.requestedUrl])
      return concurrentRequestCache[this.url.requestedUrl];

    const promise = this._get();

    const deleteCache = () => {
      concurrentRequestCache[this.url.requestedUrl] = undefined;
      delete concurrentRequestCache[this.url.requestedUrl];
    };

    return (concurrentRequestCache[this.url.requestedUrl] = promise)
      .then(res => {
        deleteCache();
        return res;
      })
      .catch(err => {
        deleteCache();
        return Promise.reject(err);
      });
  }

  // fulfills promise when service.prerender.cloud response is: 2xx, 4xx
  // rejects promise when request lib errors or service.prerender.cloud response is: 5xx
  _get() {
    const apiRequestUrl = this._createApiRequestUrl();
    const headers = this._createHeaders();

    let requestPromise;

    if (options.isThrottled(this.url.requestedUrl)) {
      requestPromise = Promise.reject(new Error("throttled"));
    } else {
      debug("prerendering:", apiRequestUrl, headers);
      requestPromise = got.get(apiRequestUrl, {
        headers,
        retries: options.options.retries,
        followRedirect: false,
        timeout: options.options.timeout || 20000
      });
    }

    return requestPromise
      .then(response => {
        return createResponse(this.url.requestedUrl, response);
      })
      .catch(err => {
        const shouldBubble = util.isFunction(options.options.bubbleUp5xxErrors)
          ? options.options.bubbleUp5xxErrors(err, this.req, err.response)
          : options.options.bubbleUp5xxErrors;

        options.recordFail(this.url.requestedUrl);

        if (shouldBubble) {
          if (err.response && is5xxError(err.response.statusCode))
            return createResponse(this.url.requestedUrl, err.response);

          if (err.message && err.message.match(/throttle/)) {
            return createResponse(this.url.requestedUrl, {
              body:
                "Error: prerender.cloud client throttled this prerender request due to a recent timeout",
              statusCode: 503,
              headers: { "content-type": "text/html" }
            });
          }

          if (isGotClientTimeout(err))
            return createResponse(this.url.requestedUrl, {
              body:
                "Error: prerender.cloud client timeout (as opposed to prerender.cloud server timeout)",
              statusCode: 500,
              headers: { "content-type": "text/html" }
            });

          return Promise.reject(err);
        } else if (err.response && is4xxError(err.response.statusCode)) {
          return createResponse(this.url.requestedUrl, err.response);
        }

        return Promise.reject(err);
      });
  }

  // data looks like { statusCode, headers, body }
  writeHttpResponse(req, res, next, data) {
    if (options.options.afterRender)
      process.nextTick(() => options.options.afterRender(null, req, data));

    try {
      if (data.statusCode === 400) {
        res.statusCode = 400;
        return res.end(
          `service.prerender.cloud can't prerender this page due to user error: ${data.body}`
        );
      } else if (data.statusCode === 429) {
        return handleSkip("rate limited due to free tier", next);
      } else {
        return compression(req, res, data);
      }
    } catch (error) {
      console.error(
        "unrecoverable prerendercloud middleware error:",
        error && error.message
      );
      console.error(
        "submit steps to reproduce here: https://github.com/sanfrancesco/prerendercloud-nodejs/issues"
      );
      throw error;
    }
  }

  static middleware(req, res, next) {
    const prerender = new Prerender(req);

    const objForReqRes = {
      url: { requestedPath: prerender.url.requestedPath }
    };
    // this is for beforeRender(req, done) func so there's visibility into what URL is being used
    req.prerender = objForReqRes;
    // this is for lambda@edge downstream: https://github.com/sanfrancesco/prerendercloud-lambda-edge
    res.prerender = objForReqRes;

    if (options.options.botsOnly) {
      vary(res, "User-Agent");
    }

    if (!prerender._shouldPrerender()) {
      debug(
        "NOT prerendering",
        req.originalUrl,
        req && req.headers && { "user-agent": req.headers["user-agent"] }
      );
      return next();
    }

    if (options.options.enableMiddlewareCache) {
      const cached = middlewareCacheSingleton.instance.get(
        prerender.url.requestedUrl
      );
      if (cached) {
        debug(
          "returning cache",
          req.originalUrl,
          req && req.headers && { "user-agent": req.headers["user-agent"] }
        );
        return prerender.writeHttpResponse(req, res, next, cached);
      }
    }

    const remotePrerender = function() {
      return prerender
        .get()
        .then(function(data) {
          return prerender.writeHttpResponse(req, res, next, data);
        })
        .catch(function(error) {
          return handleSkip(`server error: ${error && error.message}`, next);
        });
    };

    if (options.options.beforeRender) {
      const donePassedToUserBeforeRender = function(err, stringOrObject) {
        if (!stringOrObject) {
          return remotePrerender();
        } else if (typeof stringOrObject === "string") {
          return prerender.writeHttpResponse(req, res, next, {
            statusCode: 200,
            headers: {
              "content-type": "text/html; charset=utf-8"
            },
            body: stringOrObject
          });
        } else if (typeof stringOrObject === "object") {
          return prerender.writeHttpResponse(req, res, next, {
            statusCode: stringOrObject.status,
            headers: Object.assign(
              {
                "content-type": "text/html; charset=utf-8"
              },
              stringOrObject.headers
            ),
            body: stringOrObject.body
          });
        }
      };
      return options.options.beforeRender(req, donePassedToUserBeforeRender);
    } else {
      return remotePrerender();
    }
  }

  _shouldPrerender() {
    if (!(this.req && this.req.headers)) return false;

    if (this.req.method != "GET" && this.req.method != "HEAD") return false;

    if (this._alreadyPrerendered()) return false;

    if (!this._prerenderableExtension()) return false;

    if (this._isPrerenderCloudUserAgent()) return false;

    if (options.options.shouldPrerender) {
      return options.options.shouldPrerender(this.req);
    } else {
      return this._prerenderableUserAgent();
    }
  }

  _createHeaders() {
    let h = {
      "User-Agent": "prerender-cloud-nodejs-middleware",
      "accept-encoding": "gzip"
    };

    if (this.req.headers["user-agent"])
      Object.assign(h, {
        "X-Original-User-Agent": this.req.headers["user-agent"]
      });

    let token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;

    if (token) Object.assign(h, { "X-Prerender-Token": token });

    if (options.options.removeScriptTags)
      Object.assign(h, { "Prerender-Remove-Script-Tags": true });

    if (options.options.removeTrailingSlash)
      Object.assign(h, { "Prerender-Remove-Trailing-Slash": true });

    if (options.options.waitExtraLong)
      Object.assign(h, { "Prerender-Wait-Extra-Long": true });

    // disable prerender.cloud caching
    if (options.options.disableServerCache) Object.assign(h, { noCache: true });
    if (options.options.disableAjaxBypass)
      Object.assign(h, { "Prerender-Disable-Ajax-Bypass": true });
    if (options.options.disableAjaxPreload)
      Object.assign(h, { "Prerender-Disable-Ajax-Preload": true });

    if (this._hasOriginHeaderWhitelist()) {
      options.options.originHeaderWhitelist.forEach(_h => {
        if (this.req.headers[_h])
          Object.assign(h, { [_h]: this.req.headers[_h] });
      });

      Object.assign(h, {
        "Origin-Header-Whitelist": options.options.originHeaderWhitelist.join(
          " "
        )
      });
    }

    return h;
  }

  _hasOriginHeaderWhitelist() {
    return (
      options.options.originHeaderWhitelist &&
      Array.isArray(options.options.originHeaderWhitelist)
    );
  }

  _createApiRequestUrl() {
    return getRenderUrl(null, this.url.requestedUrl);
  }

  _alreadyPrerendered() {
    return !!this.req.headers["x-prerendered"];
  }

  _prerenderableExtension() {
    return this.url.hasHtmlPath;
  }

  _isPrerenderCloudUserAgent() {
    let reqUserAgent = this.req.headers["user-agent"];

    if (!reqUserAgent) return false;

    reqUserAgent = reqUserAgent.toLowerCase();

    return reqUserAgent.match(/prerendercloud/i);
  }

  _prerenderableUserAgent() {
    const reqUserAgent = this.req.headers["user-agent"];

    if (!reqUserAgent) return false;

    if (options.options.whitelistUserAgents)
      return options.options.whitelistUserAgents.some(enabledUserAgent =>
        reqUserAgent.includes(enabledUserAgent)
      );

    if (!options.options.botsOnly) return true;

    // bots only
    return userAgentIsBot(this.req.headers, this.url.original);
  }
}

Prerender.middleware.set = options.set.bind(options, Prerender.middleware);
// Prerender.middleware.cache =
Object.defineProperty(Prerender.middleware, "cache", {
  get: function() {
    return middlewareCacheSingleton.instance;
  }
});

const screenshotAndPdf = (action, url, params) => {
  const headers = {};

  const token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;

  if (token) Object.assign(headers, { "X-Prerender-Token": token });

  return got(getRenderUrl(action, url), {
    encoding: null,
    headers,
    retries: options.options.retries
  }).then(res => res.body);
};

Prerender.middleware.screenshot = screenshotAndPdf.bind(
  undefined,
  "screenshot"
);
Prerender.middleware.pdf = screenshotAndPdf.bind(undefined, "pdf");

Prerender.middleware.botsOnlyList = botsOnlyList;
Prerender.middleware.userAgentIsBot = userAgentIsBot;

Prerender.middleware.util = util;

// for testing only
Prerender.middleware.resetOptions = options.reset.bind(options);
Prerender.middleware.Options = Options;

module.exports = Prerender.middleware;
<commit_msg>Add XING-contenttabreceiver user-agent<commit_after>const getNodeVersion = () => {
  try {
    return parseFloat(process.version.replace(/v/, ""));
  } catch (err) {
    return null;
  }
};

const nodeVersion = getNodeVersion();

if (nodeVersion < 4.5) {
  console.log("prerendercloud requires node >= 4.5");
  process.exit(1);
}

require("./includes-polyfill");
if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === "[object Array]";
  };
}

const debug = require("debug")("prerendercloud");

const util = require("./lib/util");
const Url = require("./lib/Url");
const middlewareCacheSingleton = {};
const Options = require("./lib/Options");
const options = new Options(middlewareCacheSingleton);

const got = require("got-lite");
require("./lib/got-retries")(got, options, debug);

const vary = require("vary");

// preserve (and send to client) these headers from service.prerender.cloud which originally came from the origin server
const headerWhitelist = [
  "vary",
  "content-type",
  "cache-control",
  "strict-transport-security",
  "content-security-policy",
  "public-key-pins",
  "x-frame-options",
  "x-xss-protection",
  "x-content-type-options",
  "location"
];

const botsOnlyList = [
  "googlebot",
  "yahoo",
  "bingbot",
  "baiduspider",
  "facebookexternalhit",
  "twitterbot",
  "rogerbot",
  "linkedinbot",
  "embedly",
  "quora link preview",
  "showyoubot",
  "outbrain",
  "pinterest/0.",
  "pinterestbot",
  "developers.google.com/+/web/snippet",
  "slackbot",
  "vkShare",
  "W3C_Validator",
  "redditbot",
  "Applebot",
  "WhatsApp",
  "flipboard",
  "tumblr",
  "bitlybot",
  "Bitrix link preview",
  "XING-contenttabreceiver",
  "Google Search Console"
].map(ua => ua.toLowerCase());

const userAgentIsBot = (headers, requestedPath = "") => {
  const reqUserAgent =
    (headers["user-agent"] && headers["user-agent"].toLowerCase()) || "";

  if (headers["x-bufferbot"]) return true;

  if (requestedPath.match(/[?&]_escaped_fragment_/)) return true;

  return botsOnlyList.some(enabledUserAgent =>
    reqUserAgent.includes(enabledUserAgent)
  );
};

const getServiceUrl = hardcoded =>
  (hardcoded && hardcoded.replace(/\/+$/, "")) ||
  process.env.PRERENDER_SERVICE_URL ||
  "https://service.prerender.cloud";
const getRenderUrl = (action, url) =>
  [getServiceUrl(), action, url].filter(p => p).join("/");

// from https://stackoverflow.com/a/41072596
// if there are multiple values with different case, it just takes the last
// (which is wrong, it should merge some of them according to the HTTP spec, but it's fine for now)
const objectKeysToLowerCase = function(origObj) {
  return Object.keys(origObj).reduce(function(newObj, key) {
    const val = origObj[key];
    const newVal = typeof val === "object" ? objectKeysToLowerCase(val) : val;
    newObj[key.toLowerCase()] = newVal;
    return newObj;
  }, {});
};

const is5xxError = statusCode => parseInt(statusCode / 100) === 5;
const is4xxError = statusCode => {
  const n = parseInt(statusCode);
  const i = parseInt(statusCode / 100);

  return i === 4 && n !== 429;
};
const isGotClientTimeout = err =>
  err.name === "RequestError" && err.code === "ETIMEDOUT";

const zlib = require("zlib");
function compression(req, res, data) {
  if (
    req.headers["accept-encoding"] &&
    req.headers["accept-encoding"].match(/gzip/i)
  ) {
    zlib.gzip(data.body, (err, gzipped) => {
      if (err) {
        console.error(err);
        return res.status(500).send("compression error");
      }

      res.writeHead(
        data.statusCode,
        Object.assign({}, data.headers, { "content-encoding": "gzip" })
      );
      res.end(gzipped);
    });
  } else {
    res.writeHead(data.statusCode, data.headers);
    res.end(data.body);
  }
}

const handleSkip = (msg, next) => {
  debug(msg);
  console.error("prerendercloud middleware SKIPPED:", msg);
  return next();
};

const concurrentRequestCache = {};

// response: { body, statusCode, headers }
function createResponse(requestedUrl, response) {
  const body = response.body;

  const lowerCasedHeaders = objectKeysToLowerCase(response.headers);

  const headers = {};
  headerWhitelist.forEach(h => {
    if (lowerCasedHeaders[h]) headers[h] = lowerCasedHeaders[h];
  });

  const data = { statusCode: response.statusCode, headers, body };

  if (
    options.options.enableMiddlewareCache &&
    `${response.statusCode}`.startsWith("2") &&
    body &&
    body.length
  )
    middlewareCacheSingleton.instance.set(requestedUrl, data);

  return data;
}

class Prerender {
  constructor(req) {
    this.req = req;
    this.url = Url.parse(req, options);
  }

  // promise cache wrapper around ._get to prevent concurrent requests to same URL
  get() {
    if (concurrentRequestCache[this.url.requestedUrl])
      return concurrentRequestCache[this.url.requestedUrl];

    const promise = this._get();

    const deleteCache = () => {
      concurrentRequestCache[this.url.requestedUrl] = undefined;
      delete concurrentRequestCache[this.url.requestedUrl];
    };

    return (concurrentRequestCache[this.url.requestedUrl] = promise)
      .then(res => {
        deleteCache();
        return res;
      })
      .catch(err => {
        deleteCache();
        return Promise.reject(err);
      });
  }

  // fulfills promise when service.prerender.cloud response is: 2xx, 4xx
  // rejects promise when request lib errors or service.prerender.cloud response is: 5xx
  _get() {
    const apiRequestUrl = this._createApiRequestUrl();
    const headers = this._createHeaders();

    let requestPromise;

    if (options.isThrottled(this.url.requestedUrl)) {
      requestPromise = Promise.reject(new Error("throttled"));
    } else {
      debug("prerendering:", apiRequestUrl, headers);
      requestPromise = got.get(apiRequestUrl, {
        headers,
        retries: options.options.retries,
        followRedirect: false,
        timeout: options.options.timeout || 20000
      });
    }

    return requestPromise
      .then(response => {
        return createResponse(this.url.requestedUrl, response);
      })
      .catch(err => {
        const shouldBubble = util.isFunction(options.options.bubbleUp5xxErrors)
          ? options.options.bubbleUp5xxErrors(err, this.req, err.response)
          : options.options.bubbleUp5xxErrors;

        options.recordFail(this.url.requestedUrl);

        if (shouldBubble) {
          if (err.response && is5xxError(err.response.statusCode))
            return createResponse(this.url.requestedUrl, err.response);

          if (err.message && err.message.match(/throttle/)) {
            return createResponse(this.url.requestedUrl, {
              body:
                "Error: prerender.cloud client throttled this prerender request due to a recent timeout",
              statusCode: 503,
              headers: { "content-type": "text/html" }
            });
          }

          if (isGotClientTimeout(err))
            return createResponse(this.url.requestedUrl, {
              body:
                "Error: prerender.cloud client timeout (as opposed to prerender.cloud server timeout)",
              statusCode: 500,
              headers: { "content-type": "text/html" }
            });

          return Promise.reject(err);
        } else if (err.response && is4xxError(err.response.statusCode)) {
          return createResponse(this.url.requestedUrl, err.response);
        }

        return Promise.reject(err);
      });
  }

  // data looks like { statusCode, headers, body }
  writeHttpResponse(req, res, next, data) {
    if (options.options.afterRender)
      process.nextTick(() => options.options.afterRender(null, req, data));

    try {
      if (data.statusCode === 400) {
        res.statusCode = 400;
        return res.end(
          `service.prerender.cloud can't prerender this page due to user error: ${data.body}`
        );
      } else if (data.statusCode === 429) {
        return handleSkip("rate limited due to free tier", next);
      } else {
        return compression(req, res, data);
      }
    } catch (error) {
      console.error(
        "unrecoverable prerendercloud middleware error:",
        error && error.message
      );
      console.error(
        "submit steps to reproduce here: https://github.com/sanfrancesco/prerendercloud-nodejs/issues"
      );
      throw error;
    }
  }

  static middleware(req, res, next) {
    const prerender = new Prerender(req);

    const objForReqRes = {
      url: { requestedPath: prerender.url.requestedPath }
    };
    // this is for beforeRender(req, done) func so there's visibility into what URL is being used
    req.prerender = objForReqRes;
    // this is for lambda@edge downstream: https://github.com/sanfrancesco/prerendercloud-lambda-edge
    res.prerender = objForReqRes;

    if (options.options.botsOnly) {
      vary(res, "User-Agent");
    }

    if (!prerender._shouldPrerender()) {
      debug(
        "NOT prerendering",
        req.originalUrl,
        req && req.headers && { "user-agent": req.headers["user-agent"] }
      );
      return next();
    }

    if (options.options.enableMiddlewareCache) {
      const cached = middlewareCacheSingleton.instance.get(
        prerender.url.requestedUrl
      );
      if (cached) {
        debug(
          "returning cache",
          req.originalUrl,
          req && req.headers && { "user-agent": req.headers["user-agent"] }
        );
        return prerender.writeHttpResponse(req, res, next, cached);
      }
    }

    const remotePrerender = function() {
      return prerender
        .get()
        .then(function(data) {
          return prerender.writeHttpResponse(req, res, next, data);
        })
        .catch(function(error) {
          return handleSkip(`server error: ${error && error.message}`, next);
        });
    };

    if (options.options.beforeRender) {
      const donePassedToUserBeforeRender = function(err, stringOrObject) {
        if (!stringOrObject) {
          return remotePrerender();
        } else if (typeof stringOrObject === "string") {
          return prerender.writeHttpResponse(req, res, next, {
            statusCode: 200,
            headers: {
              "content-type": "text/html; charset=utf-8"
            },
            body: stringOrObject
          });
        } else if (typeof stringOrObject === "object") {
          return prerender.writeHttpResponse(req, res, next, {
            statusCode: stringOrObject.status,
            headers: Object.assign(
              {
                "content-type": "text/html; charset=utf-8"
              },
              stringOrObject.headers
            ),
            body: stringOrObject.body
          });
        }
      };
      return options.options.beforeRender(req, donePassedToUserBeforeRender);
    } else {
      return remotePrerender();
    }
  }

  _shouldPrerender() {
    if (!(this.req && this.req.headers)) return false;

    if (this.req.method != "GET" && this.req.method != "HEAD") return false;

    if (this._alreadyPrerendered()) return false;

    if (!this._prerenderableExtension()) return false;

    if (this._isPrerenderCloudUserAgent()) return false;

    if (options.options.shouldPrerender) {
      return options.options.shouldPrerender(this.req);
    } else {
      return this._prerenderableUserAgent();
    }
  }

  _createHeaders() {
    let h = {
      "User-Agent": "prerender-cloud-nodejs-middleware",
      "accept-encoding": "gzip"
    };

    if (this.req.headers["user-agent"])
      Object.assign(h, {
        "X-Original-User-Agent": this.req.headers["user-agent"]
      });

    let token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;

    if (token) Object.assign(h, { "X-Prerender-Token": token });

    if (options.options.removeScriptTags)
      Object.assign(h, { "Prerender-Remove-Script-Tags": true });

    if (options.options.removeTrailingSlash)
      Object.assign(h, { "Prerender-Remove-Trailing-Slash": true });

    if (options.options.waitExtraLong)
      Object.assign(h, { "Prerender-Wait-Extra-Long": true });

    // disable prerender.cloud caching
    if (options.options.disableServerCache) Object.assign(h, { noCache: true });
    if (options.options.disableAjaxBypass)
      Object.assign(h, { "Prerender-Disable-Ajax-Bypass": true });
    if (options.options.disableAjaxPreload)
      Object.assign(h, { "Prerender-Disable-Ajax-Preload": true });

    if (this._hasOriginHeaderWhitelist()) {
      options.options.originHeaderWhitelist.forEach(_h => {
        if (this.req.headers[_h])
          Object.assign(h, { [_h]: this.req.headers[_h] });
      });

      Object.assign(h, {
        "Origin-Header-Whitelist": options.options.originHeaderWhitelist.join(
          " "
        )
      });
    }

    return h;
  }

  _hasOriginHeaderWhitelist() {
    return (
      options.options.originHeaderWhitelist &&
      Array.isArray(options.options.originHeaderWhitelist)
    );
  }

  _createApiRequestUrl() {
    return getRenderUrl(null, this.url.requestedUrl);
  }

  _alreadyPrerendered() {
    return !!this.req.headers["x-prerendered"];
  }

  _prerenderableExtension() {
    return this.url.hasHtmlPath;
  }

  _isPrerenderCloudUserAgent() {
    let reqUserAgent = this.req.headers["user-agent"];

    if (!reqUserAgent) return false;

    reqUserAgent = reqUserAgent.toLowerCase();

    return reqUserAgent.match(/prerendercloud/i);
  }

  _prerenderableUserAgent() {
    const reqUserAgent = this.req.headers["user-agent"];

    if (!reqUserAgent) return false;

    if (options.options.whitelistUserAgents)
      return options.options.whitelistUserAgents.some(enabledUserAgent =>
        reqUserAgent.includes(enabledUserAgent)
      );

    if (!options.options.botsOnly) return true;

    // bots only
    return userAgentIsBot(this.req.headers, this.url.original);
  }
}

Prerender.middleware.set = options.set.bind(options, Prerender.middleware);
// Prerender.middleware.cache =
Object.defineProperty(Prerender.middleware, "cache", {
  get: function() {
    return middlewareCacheSingleton.instance;
  }
});

const screenshotAndPdf = (action, url, params) => {
  const headers = {};

  const token = options.options.prerenderToken || process.env.PRERENDER_TOKEN;

  if (token) Object.assign(headers, { "X-Prerender-Token": token });

  return got(getRenderUrl(action, url), {
    encoding: null,
    headers,
    retries: options.options.retries
  }).then(res => res.body);
};

Prerender.middleware.screenshot = screenshotAndPdf.bind(
  undefined,
  "screenshot"
);
Prerender.middleware.pdf = screenshotAndPdf.bind(undefined, "pdf");

Prerender.middleware.botsOnlyList = botsOnlyList;
Prerender.middleware.userAgentIsBot = userAgentIsBot;

Prerender.middleware.util = util;

// for testing only
Prerender.middleware.resetOptions = options.reset.bind(options);
Prerender.middleware.Options = Options;

module.exports = Prerender.middleware;
<|endoftext|>


<commit_before>function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for(var i = 0; i <ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) === ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

function get_monitored_entities(widgets)
{
    index = 0;
    entities = [];
    Object.keys(widgets).forEach(function (key) {
        var value = widgets[key];
        elen = value.monitored_entities.length;
        if ("resident_namespace" in value.parameters)
        {
            ns = value.parameters.resident_namespace
        }
        else
        {
            ns = value.parameters.namespace;
        }
        for (i=0;i < elen;i++)
        {
            entities[index++] = {entity: value.monitored_entities[i].entity, namespace: ns, widget: value}
        }
});
    return entities
}

var DashStream = function(transport, protocol, domain, port, title, widgets)
{
    var self = this;

    this.on_connect = function(data)
    {
        // Grab state

        self.stream.get_state('*', '*', self.populate_dash);

        // subscribe to all events

        self.stream.listen_event('*', 'ad_dashboard', self.update_dash);

        //deprecated. Will be removed.
        self.stream.listen_event('*', '__HADASHBOARD_EVENT', self.update_dash);

        // Subscribe to just the entities we care about for this dashboard

        entities = get_monitored_entities(widgets);
        elen = entities.length;
        for (i=0;i < elen;i++)
        {
            self.stream.listen_state(entities[i].namespace, entities[i].entity, self.update_dash)
        }

    };

    this.on_message = function(data)
    {
        console.log("Generic message", data)
    };

    this.on_disconnect = function(data)
    {
        console.log("Disconnect", data)
    };

    this.populate_dash = function(data) {
        {
            entities = get_monitored_entities(widgets);
            elen = entities.length;
            for (i = 0; i < elen; i++) {
                entity = entities[i].entity;
                ns = entities[i].namespace;
                widget = entities[i].widget;
                widget.set_state(widget, data.data[ns][entity]);
            }
        }
    };

    this.update_dash = function(msg)
    {
        data = msg.data;
        if ( (data.event_type === "__HADASHBOARD_EVENT" ||
              data.event_type === "ad_dashboard")  &&
             ((data.data.deviceid && data.data.deviceid === myDeviceID) ||
              (data.data.dashid && title.includes(data.data.dashid)) ||
              (!data.data.deviceid && !data.data.dashid)))
        {
            if (data.data.command === "navigate")
            {
                var timeout_params = "";
                if ("timeout" in data.data)
                {
                    var timeout = data.data.timeout;
                    if (location.search === "")
                    {
                        timeout_params = "?";
                    }
                    else
                    {
                        timeout_params = "&";
                    }
                    if ("return" in data.data)
                    {
                        ret = data.data.return
                    }
                    else
                    {
                        ret = location.pathname
                    }
                    if ("sticky")
                    {
                        sticky = data.data.sticky;
                    }
                    else
                    {
                        sticky = 0;
                    }

                    timeout_params += "timeout=" + timeout + "&return=" + ret + "&sticky=" + sticky;
                }
                window.location.href = data.data.target + location.search + timeout_params;
            }
        }
        Object.keys(widgets).forEach(function (key) {
            if ("on_ha_data" in widgets[key])
            {
                widgets[key].on_ha_data(data);
            }
        })
    };

    this.stream = new Stream(transport, protocol, domain, port, title, this.on_connect, this.on_message, this.on_disconnect);

};

var inheritsFrom = function (child, parent) {
    child.prototype = Object.create(parent.prototype);
};

var WidgetBase = function(widget_id, url, skin, parameters, monitored_entities, callbacks)
{
    child = this;
    child.monitored_entities = monitored_entities;
    child.url = url;

    // Function definitions

    this.set_field = function(self, field, value)
    {
        self.ViewModel[field](value)
    };

    this.format_number = function(self, value)
    {
        var precision = 0;
        if ("precision" in self.parameters)
        {
            precision = self.parameters.precision
        }
        value = parseFloat(value);
        value = value.toFixed(precision);

        if ("shorten" in self.parameters && self.parameters.shorten === 1)
        {
            if (value >= 1E9)
            {
                value = (value / 1E9).toFixed(1) + "B"
            }
            else if (value >= 1E6)
            {
                value = (value / 1E6).toFixed(1) + "M"
            }
            else if (value >= 1E3)
            {
                value = (value / 1E3).toFixed(1) + "K"
            }
        }
        if ("use_comma" in self.parameters && self.parameters.use_comma === 1)
        {
            value = value.toString().replace(".", ",")
        }
        return value
    };

    this.map_state = function(self, value)
    {
        if ("state_map" in self.parameters)
        {
            if (value in self.parameters.state_map)
            {
                state = self.parameters.state_map[value]
            }
            else
            {
                state = value
            }
        }
        else
        {
            state = value
        }
        return (state)
    };

    this.convert_icon = function(self, value)
    {
        bits = value.split("-");
        iprefix = bits[0];
        iname = "";
        for (var i = 1; i <  bits.length; i++)
        {
            if (i!==1)
            {
                iname += "-"
            }
            iname += bits[i]
        }
        if (iprefix === "mdi")
        {
            icon = "mdi" + ' ' + value
        }
        else
        {
           icon = iprefix + ' ' + 'fa-' + iname
        }

        return icon
    };

    this.set_icon = function(self, field, value)
    {
        self.ViewModel[field](self.convert_icon(self, value))
    };

    this.set_state = function(child, data)
    {
        if (data == null || data.state == null)
        {
            if ("title" in child.ViewModel)
            {
                child.ViewModel.title("entity not found: " + child.parameters.entity);
            }
            else
            {
                console.log("Entity not found: " + child.parameters.entity)
            }
        }
        else
        {
            if ("use_hass_icon" in child.parameters &&
                parameters.use_hass_icon === 1 &&
                "attributes" in data && "icon" in data.attributes && data.attributes.icon !== "False")
            {
                icon = data.attributes.icon.replace(":", "-");
                child.icons.icon_on = icon;
                child.icons.icon_off = icon
            }
            if ("title_is_friendly_name" in child.parameters
            && child.parameters.title_is_friendly_name === 1
            && "friendly_name" in data.attributes)
            {
                child.ViewModel.title(data.attributes.friendly_name)
            }
            if ("title2_is_friendly_name" in child.parameters
            && child.parameters.title2_is_friendly_name === 1
            && "friendly_name" in data.attributes)
            {
                child.ViewModel.title2(data.attributes.friendly_name)
            }
            if (typeof child.entity_state === 'undefined')
            {
                child.entity_state = {}
            }
            child.entity_state[child.entity] = data;
            var entity = data.entity_id;
            var elen = child.monitored_entities.length;
            for (j = 0; j < elen; j++)
            {
                if (child.monitored_entities[j].entity === entity)
                {
                    monitored_entities[j].initial(child, data)
                }
            }
        }
    };

    this.on_ha_data = function(data)
    {
        entity = data.data.entity_id;
        elen = monitored_entities.length;
        if (data.event_type === "state_changed" && data.namespace === parameters.namespace)
        {
            for (i = 0; i < elen; i++)
            {
                if (monitored_entities[i].entity === entity)
                {
                    state = data.data.new_state.state;
                    this.entity_state[entity] = data.data.new_state;
                    monitored_entities[i].update(this, data.data.new_state)
                }
            }
        }
    };

    this.call_service = function(child, args)
    {
        if ("resident_namespace" in child.parameters)
        {
            ns = child.parameters.resident_namespace
        }
        else
        {
            ns = child.parameters.namespace;
        }

        service = args["service"];

        window.dashstream.stream.call_service(service, ns, args)
    };

    // Initialization

    // Create and initialize bindings

    child.ViewModel = {};

    Object.keys(parameters.fields).forEach(function(key,index)
    {
        child.ViewModel[key] = ko.observable()
    });

    child.css = {};
    if ("css" in parameters)
    {
        Object.keys(parameters.css).forEach(function (key, index) {
            child.css[key] = parameters.css[key];
            child.ViewModel[key] = ko.observable()
        });
    }

    if ("static_css" in parameters)
    {
        Object.keys(parameters.static_css).forEach(function (key, index) {
            child.ViewModel[key] = ko.observable()
        });
    }

    child.icons = {};
    if ("icons" in parameters)
    {
        Object.keys(parameters.icons).forEach(function (key, index) {
            child.icons[key] = parameters.icons[key];
            child.ViewModel[key] = ko.observable()
        });
    }

    if ("static_icons" in parameters)
    {
        Object.keys(parameters.static_icons).forEach(function (key, index) {
            child.ViewModel[key] = ko.observable()
        });
    }

    ko.applyBindings(child.ViewModel, document.getElementById(widget_id));

    // Set any static values

    if ("fields" in parameters)
    {
        Object.keys(parameters.fields).forEach(function (key, index) {
            child.ViewModel[key](parameters.fields[key])
        });
    }

    if ("static_css" in parameters)
    {
        Object.keys(parameters.static_css).forEach(function (key, index) {
            child.ViewModel[key](parameters.static_css[key])
        });
    }

    if ("static_icons" in parameters)
    {
        Object.keys(parameters.static_icons).forEach(function (key, index) {
            child.ViewModel[key](self.convert_icon(self, parameters.static_icons[key]))
        });
    }

    // Setup callbacks

    clen = callbacks.length;
    for (i=0;i < clen;i++)
    {
        if ("selector" in callbacks[i])
        {
            $(callbacks[i].selector).on(callbacks[i].action, (
                function (callback, ch, params) {
                    return function () {
                        callback(ch, params)
                    };
                }(callbacks[i].callback, child, callbacks[i].parameters))
            );
        }
        else if ("observable" in callbacks[i])
        {
            this.ViewModel[callbacks[i].observable].subscribe(
                (function(callback, ch)
              {
                  return function(newValue) {
                      callback(ch, newValue);
                  }
              }(callbacks[i].callback, child)), null, callbacks[i].action);
        }
    }
};
<commit_msg>Pre-commit fixes<commit_after>function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for(var i = 0; i <ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) === ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

function get_monitored_entities(widgets)
{
    index = 0;
    entities = [];
    Object.keys(widgets).forEach(function (key) {
        var value = widgets[key];
        elen = value.monitored_entities.length;
        if ("resident_namespace" in value.parameters)
        {
            ns = value.parameters.resident_namespace
        }
        else
        {
            ns = value.parameters.namespace;
        }
        for (i=0;i < elen;i++)
        {
            entities[index++] = {entity: value.monitored_entities[i].entity, namespace: ns, widget: value}
        }
});
    return entities
}

var DashStream = function(transport, protocol, domain, port, title, widgets)
{
    var self = this;

    this.on_connect = function(data)
    {
        // Grab state

        self.stream.get_state('*', '*', self.populate_dash);

        // subscribe to all events

        self.stream.listen_event('*', 'ad_dashboard', self.update_dash);

        //deprecated. Will be removed.
        self.stream.listen_event('*', '__HADASHBOARD_EVENT', self.update_dash);

        // Subscribe to just the entities we care about for this dashboard

        entities = get_monitored_entities(widgets);
        elen = entities.length;
        for (i=0;i < elen;i++)
        {
            self.stream.listen_state(entities[i].namespace, entities[i].entity, self.update_dash)
        }

    };

    this.on_message = function(data)
    {
        console.log("Generic message", data)
    };

    this.on_disconnect = function(data)
    {
        console.log("Disconnect", data)
    };

    this.populate_dash = function(data) {
        {
            entities = get_monitored_entities(widgets);
            elen = entities.length;
            for (i = 0; i < elen; i++) {
                entity = entities[i].entity;
                ns = entities[i].namespace;
                widget = entities[i].widget;
                widget.set_state(widget, data.data[ns][entity]);
            }
        }
    };

    this.update_dash = function(msg)
    {
        data = msg.data;
        if ( (data.event_type === "__HADASHBOARD_EVENT" ||
              data.event_type === "ad_dashboard")  &&
             ((data.data.deviceid && data.data.deviceid === myDeviceID) ||
              (data.data.dashid && title.includes(data.data.dashid)) ||
              (!data.data.deviceid && !data.data.dashid)))
        {
            if (data.data.command === "navigate")
            {
                var timeout_params = "";
                if ("timeout" in data.data)
                {
                    var timeout = data.data.timeout;
                    if (location.search === "")
                    {
                        timeout_params = "?";
                    }
                    else
                    {
                        timeout_params = "&";
                    }
                    if ("return" in data.data)
                    {
                        ret = data.data.return
                    }
                    else
                    {
                        ret = location.pathname
                    }
                    if ("sticky")
                    {
                        sticky = data.data.sticky;
                    }
                    else
                    {
                        sticky = 0;
                    }

                    timeout_params += "timeout=" + timeout + "&return=" + ret + "&sticky=" + sticky;
                }
                window.location.href = data.data.target + location.search + timeout_params;
            }
        }
        Object.keys(widgets).forEach(function (key) {
            if ("on_ha_data" in widgets[key])
            {
                widgets[key].on_ha_data(data);
            }
        })
    };

    this.stream = new Stream(transport, protocol, domain, port, title, this.on_connect, this.on_message, this.on_disconnect);

};

var inheritsFrom = function (child, parent) {
    child.prototype = Object.create(parent.prototype);
};

var WidgetBase = function(widget_id, url, skin, parameters, monitored_entities, callbacks)
{
    child = this;
    child.monitored_entities = monitored_entities;
    child.url = url;

    // Function definitions

    this.set_field = function(self, field, value)
    {
        self.ViewModel[field](value)
    };

    this.format_number = function(self, value)
    {
        var precision = 0;
        if ("precision" in self.parameters)
        {
            precision = self.parameters.precision
        }
        value = parseFloat(value);
        value = value.toFixed(precision);

        if ("shorten" in self.parameters && self.parameters.shorten === 1)
        {
            if (value >= 1E9)
            {
                value = (value / 1E9).toFixed(1) + "B"
            }
            else if (value >= 1E6)
            {
                value = (value / 1E6).toFixed(1) + "M"
            }
            else if (value >= 1E3)
            {
                value = (value / 1E3).toFixed(1) + "K"
            }
        }
        if ("use_comma" in self.parameters && self.parameters.use_comma === 1)
        {
            value = value.toString().replace(".", ",")
        }
        return value
    };

    this.map_state = function(self, value)
    {
        if ("state_map" in self.parameters)
        {
            if (value in self.parameters.state_map)
            {
                state = self.parameters.state_map[value]
            }
            else
            {
                state = value
            }
        }
        else
        {
            state = value
        }
        return (state)
    };

    this.convert_icon = function(self, value)
    {
        bits = value.split("-");
        iprefix = bits[0];
        iname = "";
        for (var i = 1; i <  bits.length; i++)
        {
            if (i!==1)
            {
                iname += "-"
            }
            iname += bits[i]
        }
        if (iprefix === "mdi")
        {
            icon = "mdi" + ' ' + value
        }
        else
        {
           icon = iprefix + ' ' + 'fa-' + iname
        }

        return icon
    };

    this.set_icon = function(self, field, value)
    {
        self.ViewModel[field](self.convert_icon(self, value))
    };

    this.set_state = function(child, data)
    {
        if (data == null || data.state == null)
        {
            if ("title" in child.ViewModel)
            {
                child.ViewModel.title("entity not found: " + child.parameters.entity);
            }
            else
            {
                console.log("Entity not found: " + child.parameters.entity)
            }
        }
        else
        {
            if ("use_hass_icon" in child.parameters &&
                parameters.use_hass_icon === 1 &&
                "attributes" in data && "icon" in data.attributes && data.attributes.icon !== "False")
            {
                icon = data.attributes.icon.replace(":", "-");
                child.icons.icon_on = icon;
                child.icons.icon_off = icon
            }
            if ("title_is_friendly_name" in child.parameters
            && child.parameters.title_is_friendly_name === 1
            && "friendly_name" in data.attributes)
            {
                child.ViewModel.title(data.attributes.friendly_name)
            }
            if ("title2_is_friendly_name" in child.parameters
            && child.parameters.title2_is_friendly_name === 1
            && "friendly_name" in data.attributes)
            {
                child.ViewModel.title2(data.attributes.friendly_name)
            }
            if (typeof child.entity_state === 'undefined')
            {
                child.entity_state = {}
            }
            child.entity_state[child.entity] = data;
            var entity = data.entity_id;
            var elen = child.monitored_entities.length;
            for (j = 0; j < elen; j++)
            {
                if (child.monitored_entities[j].entity === entity)
                {
                    monitored_entities[j].initial(child, data)
                }
            }
        }
    };

    this.on_ha_data = function(data)
    {
        entity = data.data.entity_id;
        elen = monitored_entities.length;
        if (data.event_type === "state_changed" && data.namespace === parameters.namespace)
        {
            for (i = 0; i < elen; i++)
            {
                if (monitored_entities[i].entity === entity)
                {
                    state = data.data.new_state.state;
                    this.entity_state[entity] = data.data.new_state;
                    monitored_entities[i].update(this, data.data.new_state)
                }
            }
        }
    };

    this.call_service = function(child, args)
    {
        if ("resident_namespace" in child.parameters)
        {
            ns = child.parameters.resident_namespace
        }
        else
        {
            ns = child.parameters.namespace;
        }

        service = args["service"];

        window.dashstream.stream.call_service(service, ns, args)
    };

    // Initialization

    // Create and initialize bindings

    child.ViewModel = {};

    Object.keys(parameters.fields).forEach(function(key,index)
    {
        child.ViewModel[key] = ko.observable()
    });

    child.css = {};
    if ("css" in parameters)
    {
        Object.keys(parameters.css).forEach(function (key, index) {
            child.css[key] = parameters.css[key];
            child.ViewModel[key] = ko.observable()
        });
    }

    if ("static_css" in parameters)
    {
        Object.keys(parameters.static_css).forEach(function (key, index) {
            child.ViewModel[key] = ko.observable()
        });
    }

    child.icons = {};
    if ("icons" in parameters)
    {
        Object.keys(parameters.icons).forEach(function (key, index) {
            child.icons[key] = parameters.icons[key];
            child.ViewModel[key] = ko.observable()
        });
    }

    if ("static_icons" in parameters)
    {
        Object.keys(parameters.static_icons).forEach(function (key, index) {
            child.ViewModel[key] = ko.observable()
        });
    }

    ko.applyBindings(child.ViewModel, document.getElementById(widget_id));

    // Set any static values

    if ("fields" in parameters)
    {
        Object.keys(parameters.fields).forEach(function (key, index) {
            child.ViewModel[key](parameters.fields[key])
        });
    }

    if ("static_css" in parameters)
    {
        Object.keys(parameters.static_css).forEach(function (key, index) {
            child.ViewModel[key](parameters.static_css[key])
        });
    }

    if ("static_icons" in parameters)
    {
        Object.keys(parameters.static_icons).forEach(function (key, index) {
            child.ViewModel[key](self.convert_icon(self, parameters.static_icons[key]))
        });
    }

    // Setup callbacks

    clen = callbacks.length;
    for (i=0;i < clen;i++)
    {
        if ("selector" in callbacks[i])
        {
            $(callbacks[i].selector).on(callbacks[i].action, (
                function (callback, ch, params) {
                    return function () {
                        callback(ch, params)
                    };
                }(callbacks[i].callback, child, callbacks[i].parameters))
            );
        }
        else if ("observable" in callbacks[i])
        {
            this.ViewModel[callbacks[i].observable].subscribe(
                (function(callback, ch)
              {
                  return function(newValue) {
                      callback(ch, newValue);
                  }
              }(callbacks[i].callback, child)), null, callbacks[i].action);
        }
    }
};
<|endoftext|>


<commit_before>var persisteduls=new Object()
var ddtreemenu=new Object()

ddtreemenu.closefolder="closed.gif" //set image path to "closed" folder image
ddtreemenu.openfolder="open.gif" //set image path to "open" folder image

//////////No need to edit beyond here///////////////////////////

ddtreemenu.createTree=function(treeid, enablepersist, persistdays){
var ultags=document.getElementById(treeid).getElementsByTagName("ul")
if (typeof persisteduls[treeid]=="undefined")
persisteduls[treeid]=(enablepersist==true && ddtreemenu.getCookie(treeid)!="")? ddtreemenu.getCookie(treeid).split(",") : ""
for (var i=0; i<ultags.length; i++)
ddtreemenu.buildSubTree(treeid, ultags[i], i)
if (enablepersist==true){ //if enable persist feature
var durationdays=(typeof persistdays=="undefined")? 1 : parseInt(persistdays)
ddtreemenu.dotask(window, function(){ddtreemenu.rememberstate(treeid, durationdays)}, "unload") //save opened UL indexes on body unload
}
}

ddtreemenu.buildSubTree=function(treeid, ulelement, index){
ulelement.parentNode.className="submenu"
if (typeof persisteduls[treeid]=="object"){ //if cookie exists (persisteduls[treeid] is an array versus "" string)
if (ddtreemenu.searcharray(persisteduls[treeid], index)){
ulelement.setAttribute("rel", "open")
ulelement.style.display="block"
ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
}
else
ulelement.setAttribute("rel", "closed")
} //end cookie persist code
else if (ulelement.getAttribute("rel")==null || ulelement.getAttribute("rel")==false) //if no cookie and UL has NO rel attribute explicted added by user
ulelement.setAttribute("rel", "closed")
else if (ulelement.getAttribute("rel")=="open") //else if no cookie and this UL has an explicit rel value of "open"
ddtreemenu.expandSubTree(treeid, ulelement) //expand this UL plus all parent ULs (so the most inner UL is revealed!)
ulelement.parentNode.onclick=function(e){
var submenu=this.getElementsByTagName("ul")[0]
if (submenu.getAttribute("rel")=="closed"){
submenu.style.display="block"
submenu.setAttribute("rel", "open")
ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
}
else if (submenu.getAttribute("rel")=="open"){
submenu.style.display="none"
submenu.setAttribute("rel", "closed")
ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.closefolder+")"
}
ddtreemenu.preventpropagate(e)
}
ulelement.onclick=function(e){
ddtreemenu.preventpropagate(e)
}
}

ddtreemenu.expandSubTree=function(treeid, ulelement){ //expand a UL element and any of its parent ULs
var rootnode=document.getElementById(treeid)
var currentnode=ulelement
currentnode.style.display="block"
currentnode.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
while (currentnode!=rootnode){
if (currentnode.tagName=="UL"){ //if parent node is a UL, expand it too
currentnode.style.display="block"
currentnode.setAttribute("rel", "open") //indicate it's open
currentnode.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
}
currentnode=currentnode.parentNode
}
}

ddtreemenu.flatten=function(treeid, action){ //expand or contract all UL elements
var ultags=document.getElementById(treeid).getElementsByTagName("ul")
for (var i=0; i<ultags.length; i++){
ultags[i].style.display=(action=="expand")? "block" : "none"
var relvalue=(action=="expand")? "open" : "closed"
ultags[i].setAttribute("rel", relvalue)
ultags[i].parentNode.style.backgroundImage=(action=="expand")? "url("+ddtreemenu.openfolder+")" : "url("+ddtreemenu.closefolder+")"
}
}

ddtreemenu.rememberstate=function(treeid, durationdays){ //store index of opened ULs relative to other ULs in Tree into cookie
var ultags=document.getElementById(treeid).getElementsByTagName("ul")
var openuls=new Array()
for (var i=0; i<ultags.length; i++){
if (ultags[i].getAttribute("rel")=="open")
openuls[openuls.length]=i //save the index of the opened UL (relative to the entire list of ULs) as an array element
}
if (openuls.length==0) //if there are no opened ULs to save/persist
openuls[0]="none open" //set array value to string to simply indicate all ULs should persist with state being closed
ddtreemenu.setCookie(treeid, openuls.join(","), durationdays) //populate cookie with value treeid=1,2,3 etc (where 1,2... are the indexes of the opened ULs)
}

////A few utility functions below//////////////////////

ddtreemenu.getCookie=function(Name){ //get cookie value
var re=new RegExp(Name+"=[^;]+", "i"); //construct RE to search for target name/value pair
if (document.cookie.match(re)) //if cookie found
return document.cookie.match(re)[0].split("=")[1] //return its value
return ""
}

ddtreemenu.setCookie=function(name, value, days){ //set cookei value
var expireDate = new Date()
//set "expstring" to either future or past date, to set or delete cookie, respectively
var expstring=expireDate.setDate(expireDate.getDate()+parseInt(days))
document.cookie = name+"="+value+"; expires="+expireDate.toGMTString()+"; path=/";
}

ddtreemenu.searcharray=function(thearray, value){ //searches an array for the entered value. If found, delete value from array
var isfound=false
for (var i=0; i<thearray.length; i++){
if (thearray[i]==value){
isfound=true
thearray.shift() //delete this element from array for efficiency sake
break
}
}
return isfound
}

ddtreemenu.preventpropagate=function(e){ //prevent action from bubbling upwards
if (typeof e!="undefined")
e.stopPropagation()
else
event.cancelBubble=true
}

ddtreemenu.dotask=function(target, functionref, tasktype){ //assign a function to execute to an event handler (ie: onunload)
var tasktype=(window.addEventListener)? tasktype : "on"+tasktype
if (target.addEventListener)
target.addEventListener(tasktype, functionref, false)
else if (target.attachEvent)
target.attachEvent(tasktype, functionref)
}<commit_msg>Delete simpletreemenu.js<commit_after><|endoftext|>


<commit_before>const fs = require('fs');
const jsesc = require('jsesc');
const template = require('lodash.template');

function format(object) {
	return jsesc(object, {
		json: true,
		compact: false,
	}) + '\n';
}

function parse(source) {
	const indexByCodePoint = {};
	const indexByPointer = {};
	let decoded = '';
	const encoded = [];
	var lines = source.split('\n');
	for (const line of lines) {
		const data = line.trim().split('\t');
		if (data.length != 3) {
			continue;
		}
		const pointer = Number(data[0]);
		const codePoint = Number(data[1]);
		const symbol = String.fromCodePoint(codePoint);
		decoded += symbol;
		encoded.push(pointer + 0x80);
		indexByCodePoint[codePoint] = pointer;
		indexByPointer[pointer] = symbol;
	}
	return {
		decoded: decoded,
		encoded: encoded,
		indexByCodePoint: indexByCodePoint,
		indexByPointer: indexByPointer
	};
}

const source = fs.readFileSync('./data/index.txt', 'utf-8');
const result = parse(source);
fs.writeFileSync(
	'./data/index-by-code-point.json',
	format(result.indexByCodePoint)
);
fs.writeFileSync(
	'./data/index-by-pointer.json',
	format(result.indexByPointer)
);
fs.writeFileSync(
	'./data/decoded.json',
	format(result.decoded)
);
fs.writeFileSync(
	'./data/encoded.json',
	format(result.encoded)
);

// tests/tests.src.js → tests/tests.js
const TEST_TEMPLATE = fs.readFileSync('./tests/tests.src.mjs', 'utf8');
const createTest = template(TEST_TEMPLATE, {
	interpolate: /<\%=([\s\S]+?)%\>/g,
});
const testCode = createTest(require('./export-data.js'));
fs.writeFileSync('./tests/tests.mjs', testCode);

// src/windows-1251.src.mjs -> windows-1251.mjs
const LIB_TEMPLATE = fs.readFileSync('./src/windows-1251.src.mjs', 'utf8');
const createLib = template(LIB_TEMPLATE, {
	interpolate: /<\%=([\s\S]+?)%\>/g,
});
const libCode = createLib(require('./export-data.js'));
fs.writeFileSync('./windows-1251.mjs', libCode);
<commit_msg>Make string interpolation more robust<commit_after>const fs = require('fs');
const jsesc = require('jsesc');
const template = require('lodash.template');

function format(object) {
	return jsesc(object, {
		json: true,
		compact: false,
	}) + '\n';
}

function parse(source) {
	const indexByCodePoint = {};
	const indexByPointer = {};
	let decoded = '';
	const encoded = [];
	var lines = source.split('\n');
	for (const line of lines) {
		const data = line.trim().split('\t');
		if (data.length != 3) {
			continue;
		}
		const pointer = Number(data[0]);
		const codePoint = Number(data[1]);
		const symbol = String.fromCodePoint(codePoint);
		decoded += symbol;
		encoded.push(pointer + 0x80);
		indexByCodePoint[codePoint] = pointer;
		indexByPointer[pointer] = symbol;
	}
	return {
		decoded: decoded,
		encoded: encoded,
		indexByCodePoint: indexByCodePoint,
		indexByPointer: indexByPointer
	};
}

const source = fs.readFileSync('./data/index.txt', 'utf-8');
const result = parse(source);
fs.writeFileSync(
	'./data/index-by-code-point.json',
	format(result.indexByCodePoint)
);
fs.writeFileSync(
	'./data/index-by-pointer.json',
	format(result.indexByPointer)
);
fs.writeFileSync(
	'./data/decoded.json',
	format(result.decoded)
);
fs.writeFileSync(
	'./data/encoded.json',
	format(result.encoded)
);

// tests/tests.src.js → tests/tests.js
const TEST_TEMPLATE = fs.readFileSync('./tests/tests.src.mjs', 'utf8');
const createTest = template(TEST_TEMPLATE, {
	interpolate: /<%=([\s\S]+?)%>/g,
});
const testCode = createTest(require('./export-data.js'));
fs.writeFileSync('./tests/tests.mjs', testCode);

// src/windows-1251.src.mjs -> windows-1251.mjs
const LIB_TEMPLATE = fs.readFileSync('./src/windows-1251.src.mjs', 'utf8');
const createLib = template(LIB_TEMPLATE, {
	interpolate: /<%=([\s\S]+?)%>/g,
});
const libCode = createLib(require('./export-data.js'));
fs.writeFileSync('./windows-1251.mjs', libCode);
<|endoftext|>


<commit_before>'use strict';
const { readdir, readFile, writeFile } = require('fs').promises;
const NEW_VERSION = require('../package').version;
const PREV_VERSION = require('../packages/core-js/package').version;

const NEW_VERSION_MINOR = NEW_VERSION.replace(/^(\d+\.\d+)\..*/, '$1');
const PREV_VERSION_MINOR = PREV_VERSION.replace(/^(\d+\.\d+)\..*/, '$1');
const README = './README.md';
const SHARED = './packages/core-js/internals/shared.js';

(async function () {
  // eslint-disable-next-line no-console
  if (NEW_VERSION === PREV_VERSION) return console.log('\u001B[31mupdate is not required\u001B[0m');
  const readme = await readFile(README, 'utf8');
  await writeFile(README, readme
    .split(PREV_VERSION).join(NEW_VERSION)
    .split(PREV_VERSION_MINOR).join(NEW_VERSION_MINOR));
  const shared = await readFile(SHARED, 'utf8');
  await writeFile(SHARED, shared.split(PREV_VERSION).join(NEW_VERSION));
  const packages = await readdir('./packages');
  for (const NAME of packages) {
    const PATH = `./packages/${ NAME }/package.json`;
    const pkg = JSON.parse(await readFile(PATH, 'utf8'));
    pkg.version = NEW_VERSION;
    for (const field of ['dependencies', 'devDependencies']) {
      if (pkg[field]) for (const dependency of packages) {
        if (pkg[field][dependency]) pkg[field][dependency] = NEW_VERSION;
      }
    }
    await writeFile(PATH, `${ JSON.stringify(pkg, null, '  ') }\n`);
  }
  // eslint-disable-next-line no-console
  console.log('\u001B[32mthe version updated\u001B[0m');
})();
<commit_msg>update the year in the `update-version` script<commit_after>'use strict';
/* eslint-disable no-console */
const { readdir, readFile, writeFile } = require('fs').promises;
const NEW_VERSION = require('../package').version;
const PREV_VERSION = require('../packages/core-js/package').version;

const NEW_VERSION_MINOR = NEW_VERSION.replace(/^(\d+\.\d+)\..*/, '$1');
const PREV_VERSION_MINOR = PREV_VERSION.replace(/^(\d+\.\d+)\..*/, '$1');
const LICENSE = './LICENSE';
const README = './README.md';
const SHARED = './packages/core-js/internals/shared.js';
const CURRENT_YEAR = `${ new Date().getFullYear() }`;

(async function () {
  const license = await readFile(LICENSE, 'utf8');
  const OLD_YEAR = +license.match(/2014-(\d{4}) D/m)[1];
  if (NEW_VERSION === PREV_VERSION && CURRENT_YEAR === OLD_YEAR) {
    return console.log('\u001B[31mupdate is not required\u001B[0m');
  }
  await writeFile(LICENSE, license.split(OLD_YEAR).join(CURRENT_YEAR));
  const readme = await readFile(README, 'utf8');
  await writeFile(README, readme
    .split(PREV_VERSION).join(NEW_VERSION)
    .split(PREV_VERSION_MINOR).join(NEW_VERSION_MINOR));
  const shared = await readFile(SHARED, 'utf8');
  await writeFile(SHARED, shared
    .split(PREV_VERSION).join(NEW_VERSION)
    .split(OLD_YEAR).join(CURRENT_YEAR));
  const packages = await readdir('./packages');
  for (const NAME of packages) {
    const PATH = `./packages/${ NAME }/package.json`;
    const pkg = JSON.parse(await readFile(PATH, 'utf8'));
    pkg.version = NEW_VERSION;
    for (const field of ['dependencies', 'devDependencies']) {
      if (pkg[field]) for (const dependency of packages) {
        if (pkg[field][dependency]) pkg[field][dependency] = NEW_VERSION;
      }
    }
    await writeFile(PATH, `${ JSON.stringify(pkg, null, '  ') }\n`);
  }
  if (CURRENT_YEAR !== OLD_YEAR) console.log('\u001B[32mthe year updated\u001B[0m');
  if (NEW_VERSION !== PREV_VERSION) console.log('\u001B[32mthe version updated\u001B[0m');
})();
<|endoftext|>


<commit_before>// ==========================================================================
//                        DG.DocumentArchiver
//
//  Author:   Kirk Swenson
//
//  Copyright (c) 2014 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================

/** @class

  Coordinating controller for the document.

  @extends SC.Object
*/

/*global CSV: true */

DG.DocumentArchiver = SC.Object.extend(
/** @scope DG.DocumentArchiver.prototype */ {

    /**
     Tests the JSON text for validity as a possible document.

     The following assertions are tested:

     (1) The document is valid JSON. that is: it parses correctly
     (2) The document looks like a valid CODAP document. It has all mandatory
     top level elements and no unexpected top level elements.
     (3) all internal links can be resolved.

     @param    {String}    iDocument -- The JSON-formatted document text
     @returns  {[String]}   An array of error messages, zero length, if none.
     */
    isValidJsonDocument: function (iDocument) {
      function visit(key, value, fn) {
        var rtn = fn(key, value);
        if (rtn !== false) {
          if (Array.isArray(value)) {
            value.forEach(function (item) {
              visit(key, item, fn);
            });
          } else if (typeof value === 'object') {
            DG.ObjectMap.forEach(value, function (key, item) {
              visit(key, item, fn);
            });
          }
        }
      }

      function validateInternalRefs(iDocument) {
        var parts = subDocs.copy(false);
        var symbols = [];
        var references = [];
        parts.unshift(iDocument);
        parts.forEach(function (documentPart) {
          visit('doc', documentPart, function (key, value) {
            if (key === 'guid') {
              symbols.push(Number(value));
              return true;
            } else if (key === '_links_') {
              DG.ObjectMap.forEach(value, function(k, v) {
                references.push(Number(v.id));
              });
              return true;
            } else if (key === 'contextStorage') { // context storage is private to data interactive
              return false;
            }

          });
        });
        references.forEach(function (ref) {
          if (symbols.indexOf(Number(ref)) < 0) {
            errors.push('DG.AppController.validateDocument.unresolvedID'.loc(ref));
          }
        });
      }
      var expectedProperties = [
        'appBuildNum',
        'appName',
        'appVersion',
        'components',
        'contexts',
        'globalValues',
        'guid',
        'name',
        '_permissions'
      ];
      var requiredProperties = [
        'name'
      ];
      var errors = [];
      var doc;
      var subDocs = [];

      if (typeof iDocument === 'string') {
        try {
          doc = JSON.parse(iDocument);
        } catch (ex) {
          errors.push('DG.AppController.validateDocument.parseError'.loc(ex));
        }
      } else {
        doc = iDocument;
      }

      if (DG.ExternalDocumentCache) {
        subDocs = DG.ExternalDocumentCache.fetchAll().map(function (iFragment) {
          var parsedFragment = {};
          if (typeof iDocument === 'string') {
            try {
              parsedFragment = JSON.parse(iFragment);
            } catch (ex) {
              errors.push('DG.AppController.validateDocument.parseError'.loc(ex));
            }
          } else {
            parsedFragment = iFragment;
          }
          return parsedFragment;
        });
      }

      if (doc) {
        requiredProperties.forEach(function (prop) {
            if (!doc.hasOwnProperty(prop)) {
              errors.push('DG.AppController.validateDocument.missingRequiredProperty'.loc(prop));
            }
          }
        );
        DG.ObjectMap.keys(doc).forEach(function (prop) {
            if (expectedProperties.indexOf(prop) < 0) {
              errors.push('DG.AppController.validateDocument.unexpectedProperty'.loc(prop));
            }
          }
        );
        validateInternalRefs(doc);
      }
      DG.log('Document validation: ' + (errors.length? JSON.stringify(errors): 'No Errors'));
      return errors;
    },

  /**
    Open the specified document text as a new document, returning the newly-created document.
   */
  openDocument: function( iStore, iDocText) {
    var deferred = $.Deferred(),
    externalDocIds = DG.StringUtilities.scan(iDocText, /"externalDocumentId": ?"?(\d+)"?/g, function(m) { return m[1]; }),
        promises = DG.authorizationController.loadExternalDocuments(externalDocIds);

    Promise.all(promises).then(function() {
      try {
        var docArchive = SC.json.decode( iDocText),
          validationErrors;

        validationErrors = this.isValidJsonDocument(docArchive);
        if (validationErrors.length > 0) {
          deferred.reject(new Error('DG.AppController.validateDocument.invalidDocument'.loc(
            JSON.stringify(validationErrors))));
        }
        else {
          DG.store = DG.ModelStore.create();
          deferred.resolve(DG.Document.createDocument(docArchive));
        }
      } catch (ex) {
        deferred.reject(ex);
      }
      DG.ExternalDocumentCache.clear();
      DG.busyCursor.hide();
    }.bind(this));
    return deferred;
  },

  importCSV: function (iText, iFileName) {

    // trims empty columns from right side of
    function trimTrailingColumns(arr) {
      var newArr = [];
      arr.forEach(function (row) {
        var value;
        if (Array.isArray(row)) {
          do {
            value = row.pop();
          } while(value === '');
          if (!SC.empty(value)) row.push(value);
        }
        if (row.length) {
          newArr.push(row);
        }
      });
      return newArr;
    }
    function parseText() {
      var tValuesArray,
        tCollectionRow,
        tChildName = 'children',// Child Collection Name: should be first
                                // line of CSV
        tAttrNamesRow,// Column Header Names: should be second row
        tDoc = {
          name: 'DG.Document.defaultDocumentName'.loc(),
          components: [],
          contexts: [
            {
              "type": "DG.DataContext",
              "collections": [
                {
                  "attrs": [],
                  "cases": []
                }
              ],
              "contextStorage": {
                "gameUrl": iFileName
              }
            }
          ],
          appName: DG.APPNAME,
          appVersion: DG.VERSION,
          appBuildNum: DG.BUILD_NUM,
          globalValues: []
        },
        tAttrsArray = tDoc.contexts[0].collections[0].attrs,
        tCasesArray = tDoc.contexts[0].collections[0].cases;

      CSV.RELAXED = true;
      CSV.IGNORE_RECORD_LENGTH = true;
      tValuesArray = CSV.parse(iText);
      if (tValuesArray && tValuesArray.length >= 2) {
        tValuesArray = trimTrailingColumns(tValuesArray);
        tCollectionRow = tValuesArray.shift();
        tAttrNamesRow = tValuesArray[0];

        if (Array.isArray(tCollectionRow)) {
          // check if it looks like name row is missing
          if ((tAttrNamesRow.length === tCollectionRow.length) && (tAttrNamesRow.length > 1)) {
            tAttrNamesRow = tCollectionRow;
          } else {
            tChildName = tCollectionRow[0];
            tValuesArray.shift();
          }
        }
        else {
          tChildName = tCollectionRow;
          tValuesArray.shift();
        }
        tDoc.contexts[0].collections[0].name = tChildName;

        tAttrNamesRow.forEach(function (iName) {
          tAttrsArray.push( {
            name: iName
          });
        });

        tValuesArray.forEach( function( iValues) {
          var tCase = {
            values: {}
          };
          tAttrNamesRow.forEach( function( iName, iIndex) {
            tCase.values[ iName] = iValues[ iIndex];
          });
          tCasesArray.push( tCase);
        });

        return tDoc;
      }
    } // parseText

    var docArchive = parseText( );
    return docArchive;
  },
  /**
   *
   * @param iText - csv or tab-delimited
   * @returns {DG.Document}
   */
  importTextIntoDocument: function( iText, iName) {
    var docArchive = this.importCSV(iText, iName);

    return DG.Document.createDocument(docArchive);
  },

  /**
   *
   * @param iURL - URL of data interactive
   * @returns {DG.Document}
   */
  importURLIntoDocument: function (iURL) {
    var tDoc = {
      name: 'DG.Document.defaultDocumentName'.loc(),
      guid: 1,
      components: [
        {
          "type": "DG.GameView",
          "componentStorage": {
            "currentGameName": "",
            "currentGameUrl": iURL
          }
        }
      ],
      appName: DG.APPNAME,
      appVersion: DG.VERSION,
      appBuildNum: DG.BUILD_NUM,
      globalValues: []
    };

    return DG.Document.createDocument(tDoc);
  },

  /**
    Save the specified document in its JSON-text form.
    @param    {DG.Document}   iDocument   The document whose contents are to be archived

    signature of `callback`:
    @param  {Object} docArchive      An object suitable for JSON encoding
   */
  saveDocument: function( iDocument, callback, fullData) {

    // Prepare the context-specific storage for saving.
    // Start by saving the state of the current game in the appropriate context.
    // Callback below executes after the state has been saved
    DG.currDocumentController().saveCurrentGameState(function() {
      var docController = DG.currDocumentController();
      DG.DataContext.forEachContextInMap( iDocument.get('id'),
        function( iContextID, iContext) {
          iContext.willSaveContext();
        });
      if( docController) {
        // Prepare the component-specific storage for saving
        DG.ObjectMap.forEach( docController.componentControllersMap,
          function( iComponentID, iController) {
            iController.willSaveComponent();
          });
      }

      callback(iDocument.toArchive(fullData));
    });
  },

  saveDataContexts: function( iDocument, callback, saveAll) {
    var model,
        deferred = $.Deferred();
    DG.currDocumentController().saveCurrentGameState(function() {
      DG.DataContext.forEachContextInMap( iDocument.get('id'),
        function( iContextID, iContext) {
          iContext.willSaveContext();
          model = iContext.get('model');
          if ( saveAll || !SC.none(model.get('externalDocumentId'))) {
            callback(model, model.toArchive(true));
          }
        });
      deferred.resolve();
    });
    return deferred;
  }

});

<commit_msg>Fix validation, again.<commit_after>// ==========================================================================
//                        DG.DocumentArchiver
//
//  Author:   Kirk Swenson
//
//  Copyright (c) 2014 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================

/** @class

  Coordinating controller for the document.

  @extends SC.Object
*/

/*global CSV: true */

DG.DocumentArchiver = SC.Object.extend(
/** @scope DG.DocumentArchiver.prototype */ {

    /**
     Tests the JSON text for validity as a possible document.

     The following assertions are tested:

     (1) The document is valid JSON. that is: it parses correctly
     (2) The document looks like a valid CODAP document. It has all mandatory
     top level elements and no unexpected top level elements.
     (3) all internal links can be resolved.

     @param    {String}    iDocument -- The JSON-formatted document text
     @returns  {[String]}   An array of error messages, zero length, if none.
     */
    isValidJsonDocument: function (iDocument) {
      function visit(key, value, fn) {
        var rtn = fn(key, value);
        if (rtn !== false) {
          if (Array.isArray(value)) {
            value.forEach(function (item) {
              visit(key, item, fn);
            });
          } else if (typeof value === 'object') {
            DG.ObjectMap.forEach(value, function (key, item) {
              visit(key, item, fn);
            });
          }
        }
      }

      function validateInternalRefs(iDocument) {
        var parts = subDocs.copy(false);
        var symbols = [];
        var references = [];
        parts.unshift(iDocument);
        parts.forEach(function (documentPart) {
          visit('doc', documentPart, function (key, value) {
            if (key === 'guid') {
              symbols.push(Number(value));
              return true;
            } else if (key === '_links_') {
              visit(key, value, function (k, v) {
                if (k === 'id') {
                  references.push(Number(v));
                }
                return true;
              });
              return false;
            } else if (key === 'contextStorage') { // context storage is private to data interactive
              return false;
            }

          });
        });
        references.forEach(function (ref) {
          if (symbols.indexOf(Number(ref)) < 0) {
            errors.push('DG.AppController.validateDocument.unresolvedID'.loc(ref));
          }
        });
      }
      var expectedProperties = [
        'appBuildNum',
        'appName',
        'appVersion',
        'components',
        'contexts',
        'globalValues',
        'guid',
        'name',
        '_permissions'
      ];
      var requiredProperties = [
        'name'
      ];
      var errors = [];
      var doc;
      var subDocs = [];

      if (typeof iDocument === 'string') {
        try {
          doc = JSON.parse(iDocument);
        } catch (ex) {
          errors.push('DG.AppController.validateDocument.parseError'.loc(ex));
        }
      } else {
        doc = iDocument;
      }

      if (DG.ExternalDocumentCache) {
        subDocs = DG.ExternalDocumentCache.fetchAll().map(function (iFragment) {
          var parsedFragment = {};
          if (typeof iDocument === 'string') {
            try {
              parsedFragment = JSON.parse(iFragment);
            } catch (ex) {
              errors.push('DG.AppController.validateDocument.parseError'.loc(ex));
            }
          } else {
            parsedFragment = iFragment;
          }
          return parsedFragment;
        });
      }

      if (doc) {
        requiredProperties.forEach(function (prop) {
            if (!doc.hasOwnProperty(prop)) {
              errors.push('DG.AppController.validateDocument.missingRequiredProperty'.loc(prop));
            }
          }
        );
        DG.ObjectMap.keys(doc).forEach(function (prop) {
            if (expectedProperties.indexOf(prop) < 0) {
              errors.push('DG.AppController.validateDocument.unexpectedProperty'.loc(prop));
            }
          }
        );
        validateInternalRefs(doc);
      }
      DG.log('Document validation: ' + (errors.length? JSON.stringify(errors): 'No Errors'));
      return errors;
    },

  /**
    Open the specified document text as a new document, returning the newly-created document.
   */
  openDocument: function( iStore, iDocText) {
    var deferred = $.Deferred(),
    externalDocIds = DG.StringUtilities.scan(iDocText, /"externalDocumentId": ?"?(\d+)"?/g, function(m) { return m[1]; }),
        promises = DG.authorizationController.loadExternalDocuments(externalDocIds);

    Promise.all(promises).then(function() {
      try {
        var docArchive = SC.json.decode( iDocText),
          validationErrors;

        validationErrors = this.isValidJsonDocument(docArchive);
        if (validationErrors.length > 0) {
          deferred.reject(new Error('DG.AppController.validateDocument.invalidDocument'.loc(
            JSON.stringify(validationErrors))));
        }
        else {
          DG.store = DG.ModelStore.create();
          deferred.resolve(DG.Document.createDocument(docArchive));
        }
      } catch (ex) {
        deferred.reject(ex);
      }
      DG.ExternalDocumentCache.clear();
      DG.busyCursor.hide();
    }.bind(this));
    return deferred;
  },

  importCSV: function (iText, iFileName) {

    // trims empty columns from right side of
    function trimTrailingColumns(arr) {
      var newArr = [];
      arr.forEach(function (row) {
        var value;
        if (Array.isArray(row)) {
          do {
            value = row.pop();
          } while(value === '');
          if (!SC.empty(value)) row.push(value);
        }
        if (row.length) {
          newArr.push(row);
        }
      });
      return newArr;
    }
    function parseText() {
      var tValuesArray,
        tCollectionRow,
        tChildName = 'children',// Child Collection Name: should be first
                                // line of CSV
        tAttrNamesRow,// Column Header Names: should be second row
        tDoc = {
          name: 'DG.Document.defaultDocumentName'.loc(),
          components: [],
          contexts: [
            {
              "type": "DG.DataContext",
              "collections": [
                {
                  "attrs": [],
                  "cases": []
                }
              ],
              "contextStorage": {
                "gameUrl": iFileName
              }
            }
          ],
          appName: DG.APPNAME,
          appVersion: DG.VERSION,
          appBuildNum: DG.BUILD_NUM,
          globalValues: []
        },
        tAttrsArray = tDoc.contexts[0].collections[0].attrs,
        tCasesArray = tDoc.contexts[0].collections[0].cases;

      CSV.RELAXED = true;
      CSV.IGNORE_RECORD_LENGTH = true;
      tValuesArray = CSV.parse(iText);
      if (tValuesArray && tValuesArray.length >= 2) {
        tValuesArray = trimTrailingColumns(tValuesArray);
        tCollectionRow = tValuesArray.shift();
        tAttrNamesRow = tValuesArray[0];

        if (Array.isArray(tCollectionRow)) {
          // check if it looks like name row is missing
          if ((tAttrNamesRow.length === tCollectionRow.length) && (tAttrNamesRow.length > 1)) {
            tAttrNamesRow = tCollectionRow;
          } else {
            tChildName = tCollectionRow[0];
            tValuesArray.shift();
          }
        }
        else {
          tChildName = tCollectionRow;
          tValuesArray.shift();
        }
        tDoc.contexts[0].collections[0].name = tChildName;

        tAttrNamesRow.forEach(function (iName) {
          tAttrsArray.push( {
            name: iName
          });
        });

        tValuesArray.forEach( function( iValues) {
          var tCase = {
            values: {}
          };
          tAttrNamesRow.forEach( function( iName, iIndex) {
            tCase.values[ iName] = iValues[ iIndex];
          });
          tCasesArray.push( tCase);
        });

        return tDoc;
      }
    } // parseText

    var docArchive = parseText( );
    return docArchive;
  },
  /**
   *
   * @param iText - csv or tab-delimited
   * @returns {DG.Document}
   */
  importTextIntoDocument: function( iText, iName) {
    var docArchive = this.importCSV(iText, iName);

    return DG.Document.createDocument(docArchive);
  },

  /**
   *
   * @param iURL - URL of data interactive
   * @returns {DG.Document}
   */
  importURLIntoDocument: function (iURL) {
    var tDoc = {
      name: 'DG.Document.defaultDocumentName'.loc(),
      guid: 1,
      components: [
        {
          "type": "DG.GameView",
          "componentStorage": {
            "currentGameName": "",
            "currentGameUrl": iURL
          }
        }
      ],
      appName: DG.APPNAME,
      appVersion: DG.VERSION,
      appBuildNum: DG.BUILD_NUM,
      globalValues: []
    };

    return DG.Document.createDocument(tDoc);
  },

  /**
    Save the specified document in its JSON-text form.
    @param    {DG.Document}   iDocument   The document whose contents are to be archived

    signature of `callback`:
    @param  {Object} docArchive      An object suitable for JSON encoding
   */
  saveDocument: function( iDocument, callback, fullData) {

    // Prepare the context-specific storage for saving.
    // Start by saving the state of the current game in the appropriate context.
    // Callback below executes after the state has been saved
    DG.currDocumentController().saveCurrentGameState(function() {
      var docController = DG.currDocumentController();
      DG.DataContext.forEachContextInMap( iDocument.get('id'),
        function( iContextID, iContext) {
          iContext.willSaveContext();
        });
      if( docController) {
        // Prepare the component-specific storage for saving
        DG.ObjectMap.forEach( docController.componentControllersMap,
          function( iComponentID, iController) {
            iController.willSaveComponent();
          });
      }

      callback(iDocument.toArchive(fullData));
    });
  },

  saveDataContexts: function( iDocument, callback, saveAll) {
    var model,
        deferred = $.Deferred();
    DG.currDocumentController().saveCurrentGameState(function() {
      DG.DataContext.forEachContextInMap( iDocument.get('id'),
        function( iContextID, iContext) {
          iContext.willSaveContext();
          model = iContext.get('model');
          if ( saveAll || !SC.none(model.get('externalDocumentId'))) {
            callback(model, model.toArchive(true));
          }
        });
      deferred.resolve();
    });
    return deferred;
  }

});

<|endoftext|>