Files
meteor/packages/appcache/appcache-server.js
Ben Newman 7a6ee9e129 Revert changes to webapp package since version 1.8.0.
We accidentally published changes to webapp that should have been
restricted to Meteor 1.10 as part of the 1.8.1 version. This commit
reverts commits to packages/webapp since Meteor 1.9, so that we can
republish the 1.8.0 content as version 1.8.2. We will then bump the webapp
version to 1.9.0 on the release-1.10 branch and publish the new content
only on that branch.

Revert "Allow to exclude web architectures in development mode (#10824)"
This reverts commit a205967186.

Revert "Updates cordova-plugin-meteor-webapp to 1.7.1"
This reverts commit a1e4d27822.

Revert "Update cordova-plugin-wkwebview-engine to 1.2.1."
This reverts commit 3f9a69d7c4.

Revert "Update cordova-plugin-whitelist to 1.3.4."
This reverts commit 979273333b.

Revert "Update cordova-plugin-meteor-webapp to 1.7.1-beta.1."
This reverts commit 565c4254f1.

Revert "Update accounts-password to version 1.5.2."
This reverts commit b827d1da2f.
2020-01-16 11:15:25 -05:00

297 lines
9.1 KiB
JavaScript
Executable File

import { Meteor } from 'meteor/meteor'
import { isModern } from "meteor/modern-browsers";
import { WebApp } from "meteor/webapp";
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
let _disableSizeCheck = false;
let disabledBrowsers = {};
Meteor.AppCache = {
config: options => {
Object.keys(options).forEach(option => {
value = options[option];
if (option === 'browsers') {
disabledBrowsers = {};
value.each(browser => disabledBrowsers[browser] = false);
}
else if (option === 'onlineOnly') {
value.forEach(urlPrefix =>
RoutePolicy.declare(urlPrefix, 'static-online')
);
}
// option to suppress warnings for tests.
else if (option === '_disableSizeCheck') {
_disableSizeCheck = value;
}
else if (value === false) {
disabledBrowsers[option] = true;
}
else if (value === true) {
disabledBrowsers[option] = false;
} else {
throw new Error('Invalid AppCache config option: ' + option);
}
});
}
};
const browserDisabled = request => disabledBrowsers[request.browser.name];
// Cache of previously computed app.manifest files.
const manifestCache = new Map;
const shouldSkip = resource =>
resource.type === 'dynamic js' ||
(resource.type === 'json' &&
(resource.url.endsWith('.map') ||
resource.url.endsWith('.stats.json?meteor_js_resource=true')));
WebApp.addHtmlAttributeHook(request =>
browserDisabled(request) ?
null :
{ manifest: "/app.manifest" }
);
WebApp.connectHandlers.use((req, res, next) => {
if (req.url !== '/app.manifest') {
return next();
}
const request = WebApp.categorizeRequest(req);
// Browsers will get confused if we unconditionally serve the
// manifest and then disable the app cache for that browser. If
// the app cache had previously been enabled for a browser, it
// will continue to fetch the manifest as long as it's available,
// even if we now are not including the manifest attribute in the
// app HTML. (Firefox for example will continue to display "this
// website is asking to store data on your computer for offline
// use"). Returning a 404 gets the browser to really turn off the
// app cache.
if (browserDisabled(request)) {
res.writeHead(404);
res.end();
return;
}
const cacheInfo = {
modern: isModern(request.browser),
};
cacheInfo.arch = cacheInfo.modern
? "web.browser"
: "web.browser.legacy";
// The true hash of the client manifest for this arch, regardless of
// AUTOUPDATE_VERSION or Autoupdate.autoupdateVersion.
cacheInfo.clientHash = WebApp.clientHash(cacheInfo.arch);
if (Package.autoupdate) {
const {
// New in Meteor 1.7.1 (autoupdate@1.5.0), this versions object maps
// client architectures (e.g. "web.browser") to client hashes that
// reflect AUTOUPDATE_VERSION and Autoupdate.autoupdateVersion.
versions,
// The legacy way of forcing a particular version, supported here
// just in case Autoupdate.versions is not defined.
autoupdateVersion,
} = Package.autoupdate.Autoupdate;
const version = versions
? versions[cacheInfo.arch].version
: autoupdateVersion;
if (typeof version === "string" &&
version !== cacheInfo.clientHash) {
cacheInfo.autoupdateVersion = version;
}
}
const cacheKey = JSON.stringify(cacheInfo);
if (! manifestCache.has(cacheKey)) {
manifestCache.set(cacheKey, computeManifest(cacheInfo));
}
const manifest = manifestCache.get(cacheKey);
res.setHeader('Content-Type', 'text/cache-manifest');
res.setHeader('Content-Length', manifest.length);
return res.end(manifest);
});
function computeManifest(cacheInfo) {
let manifest = "CACHE MANIFEST\n\n";
// After the browser has downloaded the app files from the server and
// has populated the browser's application cache, the browser will
// *only* connect to the server and reload the application if the
// *contents* of the app manifest file has changed.
//
// So to ensure that the client updates if client resources change,
// include a hash of client resources in the manifest.
manifest += `# ${cacheInfo.clientHash}\n`;
// When using the autoupdate package, also include
// AUTOUPDATE_VERSION. Otherwise the client will get into an
// infinite loop of reloads when the browser doesn't fetch the new
// app HTML which contains the new version, and autoupdate will
// reload again trying to get the new code.
if (typeof cacheInfo.autoupdateVersion === "string") {
manifest += `# ${cacheInfo.autoupdateVersion}\n`;
}
manifest += "\n";
manifest += "CACHE:\n";
manifest += "/\n";
eachResource(cacheInfo, resource => {
const { url } = resource;
if (resource.where !== 'client' ||
RoutePolicy.classify(url) ||
shouldSkip(resource)) {
return;
}
manifest += url;
// If the resource is not already cacheable (has a query parameter,
// presumably with a hash or version of some sort), put a version with
// a hash in the cache.
//
// Avoid putting a non-cacheable asset into the cache, otherwise the
// user can't modify the asset until the cache headers expire.
if (! resource.cacheable) {
manifest += `?${resource.hash}`;
}
manifest += "\n";
});
manifest += "\n";
manifest += "FALLBACK:\n";
manifest += "/ /\n";
eachResource(cacheInfo, (resource, arch, prefix) => {
const { url } = resource;
if (resource.where !== 'client' ||
RoutePolicy.classify(url) ||
shouldSkip(resource)) {
return;
}
if (! resource.cacheable) {
// Add a fallback entry for each uncacheable asset we added above.
//
// This means requests for the bare url ("/image.png" instead of
// "/image.png?hash") will work offline. Online, however, the
// browser will send a request to the server. Users can remove this
// extra request to the server and have the asset served from cache
// by specifying the full URL with hash in their code (manually,
// with some sort of URL rewriting helper)
manifest += `${url} ${url}?${resource.hash}\n`;
}
if (resource.type === 'asset' &&
prefix.length > 0 &&
url.startsWith(prefix)) {
// If the URL has a prefix like /__browser.legacy or /__cordova, add
// a fallback from the un-prefixed URL to the fully prefixed URL, so
// that legacy/cordova browsers can load assets offline without
// using an explicit prefix. When the client is online, these assets
// will simply come from the modern web.browser bundle, which does
// not prefix its asset URLs. Using a fallback rather than just
// duplicating the resources in the manifest is important because of
// appcache size limits.
manifest += `${url.slice(prefix.length)} ${url}?${resource.hash}\n`;
}
});
manifest += "\n";
manifest += "NETWORK:\n";
// TODO adding the manifest file to NETWORK should be unnecessary?
// Want more testing to be sure.
manifest += "/app.manifest\n";
[
...RoutePolicy.urlPrefixesFor('network'),
...RoutePolicy.urlPrefixesFor('static-online')
].forEach(urlPrefix => manifest += `${urlPrefix}\n`);
manifest += "*\n";
// content length needs to be based on bytes
return Buffer.from(manifest, "utf8");
}
function eachResource({
modern,
arch,
}, callback) {
const manifest = WebApp.clientPrograms[arch].manifest;
let prefix = "";
if (! modern) {
manifest.some(({ url }) => {
if (url && url.startsWith("/__")) {
prefix = url.split("/", 2).join("/");
return true;
}
});
}
manifest.forEach(resource => {
callback(resource, arch, prefix);
});
}
function sizeCheck() {
const sizes = [ // Check size of each known architecture independently.
"web.browser",
"web.browser.legacy",
].reduce((filt, arch) => {
let totalSize = 0;
WebApp.clientPrograms[arch].manifest.forEach(resource => {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url) &&
! shouldSkip(resource)) {
totalSize += resource.size;
}
});
if (totalSize > 5 * 1024 * 1024) {
filt.push({
arch,
size: totalSize
});
}
return filt;
}, []);
if (sizes.length > 0) {
Meteor._debug([
"** You are using the appcache package, but the size of",
"** one or more of your cached resources is larger than",
"** the recommended maximum size of 5MB which may break",
"** your app in some browsers!",
"** ",
...sizes.map(data => `** ${data.arch}: ${(data.size / 1024 / 1024).toFixed(1)}MB`),
"** ",
"** See http://docs.meteor.com/#appcache for more",
"** information and fixes."
].join("\n"));
}
}
// Run the size check after user code has had a chance to run. That way,
// the size check can take into account files that the user does not
// want cached. Otherwise, the size check warning will still print even
// if the user excludes their large files with
// `Meteor.AppCache.config({onlineOnly: files})`.
Meteor.startup(() => _disableSizeCheck || sizeCheck());