streamToString(readStream, async htmlDoc => { let jsonml = htmlToJsonML(htmlDoc); // MongoDB doesn't accept periods in keys, so we replace them with // `˙`s when storing them in the database. jsonml = replaceInKeys(jsonml, '.', '˙'); let snapshot = { type: 'http://sharejs.org/types/JSONv0', data: jsonml }; const userPermissions = await permissionManager .getUserPermissionsFromSnapshot(req.user.username, req.user.provider, snapshot); // If user doesn't have write permissions to the docuemnt, add them if // the user is logged in, otherwise just delete all permissions on the // new document. if (!userPermissions.includes('w')) { if (req.user.username === 'anonymous' && req.user.provider === '') { snapshot = permissionManager.clearPermissionsFromSnapshot(snapshot); } else { snapshot = await permissionManager .addPermissionsToSnapshot(req.user.username, req.user.provider, 'rw', snapshot); } } documentManager.createNewDocument({ webstrateId, snapshot }, function(err, _webstrateId) { if (err) { console.error(err); return reject(err); } createdWebstrate = true; }); });
}, async function(err, snapshot) { if (err) { console.error(err); return res.status(409).send(String(err)); } req.user.permissions = await permissionManager.getUserPermissionsFromSnapshot(req.user.username, req.user.provider, snapshot); // If the webstrate doesn't exist, write permissions are required to create it. if (!snapshot.type && !req.user.permissions.includes('w')) { return res.status(403).send('Insufficient permissions.'); } // If the webstrate does exist, read permissions are required to access it (or any of its // assets). if (!req.user.permissions.includes('r')) { return res.status(403).send('Insufficient permissions.'); } // Set CORS header on a response, assuming the requesting host is allowed it. setCorsHeaders(req, res, snapshot); // Requesting an asset. if (req.assetName) { try { const asset = await assetManager.getAsset({ webstrateId: req.webstrateId, assetName: req.assetName, version: snapshot.v }); if (!asset) { return res.status(404).send(`Asset "${req.assetName}" not found.`); } if ('dir' in req.query) { const zipStructure = await getZipStructure(asset.fileName); res.json(zipStructure); return; } if (req.assetPath) { return yauzl.open(APP_PATH + '/uploads/' + asset.fileName, { lazyEntries: true }, (err, zipFile) => { if (err) { return res.status(400).send(`"${req.assetName}" is not a valid ZIP file.`); } zipFile.on('entry', async entry => { if (req.assetPath !== entry.fileName) { return zipFile.readEntry(); } // If requested file is a directory, list directory files. if (entry.fileName.endsWith('/')) { const zipStructure = await getZipStructure(asset.fileName); const filteredZipStructure = zipStructure.filter(path => path.startsWith(entry.fileName)); return res.json(filteredZipStructure); } zipFile.openReadStream(entry, (err, readStream) => { res.type(mime.lookup(entry.fileName) || 'text/plain'); // Getting this from a ZIP might be a little heavy, so we cache it for a year, // even though the ZIP asset could in fact get overwritten. res.setHeader('Cache-Control', 'public, max-age=31557600'); readStream.pipe(res); }); }); zipFile.readEntry(); zipFile.once('end', async () => { res.status(404).send(`File "${req.assetPath}" not found in asset ` + `"${req.assetName}".\n`); }); }); } // `/<webstrateId>/<asset>` may not always refer to the same asset, but to optimize rapid // requests, we set a maxAge anyway. If the requested asset includes a specific version, // it'll always refer to the same thing, allowing us to set a longer maxAge. var maxAge = req.version ? '1y' : (config.maxAge || '1m'); res.type(asset.mimeType); return res.sendFile(APP_PATH + '/uploads/' + asset.fileName, { maxAge }); } catch (error) { console.error(error); return res.status(409).send(String(err)); } } // Requesting current document version number by calling `/<id>?v` or `/<id>?version`. if ('v' in req.query || 'version' in req.query) { return serveVersion(req, res, snapshot); } // Requesting a list of operations by calling `/<id>?ops`. if ('ops' in req.query) { return serveOps(req, res); } // Requesting a list of tags by calling `/<id>?tags`. if ('tags' in req.query) { return serveTags(req, res); } // Requesting a list of assets by calling `/<id>?assets`. if ('assets' in req.query) { return serveAssets(req, res); } // Requesting a JsonML version of the webstrate by calling `/<id>?json`. if ('json' in req.query) { if (!snapshot.type) { return res.status(404).send('Document doesn\'t exist.'); } return serveJsonMLWebstrate(req, res, snapshot); } // Requesting a raw version of the webstrate (i.e. a server-generated HTML file) by calling // `/<id>?raw`. if ('raw' in req.query) { if (!snapshot.type) { return res.status(404).send('Document doesn\'t exist.'); } return serveRawWebstrate(req, res, snapshot); } if ('dl' in req.query) { if (!snapshot.type) { return res.status(404).send('Document doesn\'t exist.'); } return serveCompressedWebstrate(req, res, snapshot); } if ('tokens' in req.query) { return serveTokenList(req, res); } // Requesting a copy of the webstrate. if ('copy' in req.query) { var defaultPermissions = permissionManager.getDefaultPermissions(req.user.username, req.user.provider); // If a user is required to be logged in (through loggedInToCreateWebstrates) to create a // webstrate, we also require them to be logged in to copy a webstrate. if (!permissionManager.userIsAllowedToCreateWebstrate(req.user)) { let err = 'Must be logged in to copy a webstrate.'; if (Array.isArray(config.loggedInToCreateWebstrates)) { const allowedProviders = config.loggedInToCreateWebstrates.join(' or '); err = `Must be logged in with ${allowedProviders} to copy a webstrate.`; } return res.status(403).send(err); } // If the user has no default write permissions, they're not allowed to create documents. if (!defaultPermissions.includes('w')) { return res.status(403).send('Write permissions are required to create a new document.'); } return copyWebstrate(req, res, snapshot); } // Requesting to restore document to a previous version or tag by calling: // `/<id>/?restore=<version|tag>`. if ('restore' in req.query) { if (!req.user.permissions.includes('w')) { return res.status(403).send('Write permissions are required to restore a document.'); } // If the document contains a user with admin permissions, only admins can restore the // document. if (!req.user.permissions.includes('a') && await permissionManager.webstrateHasAdmin(req.webstrateId)) { return res.status(403).send('Admin permissions are required to restore this document.'); } return restoreWebstrate(req, res, snapshot); } if ('delete' in req.query) { // If a user is required to be logged in (through loggedInToCreateWebstrates) to create a // webstrate, we also require them to be logged in to delete a webstrate. if (!permissionManager.userIsAllowedToCreateWebstrate(req.user)) { let err = 'Must be logged in to delete a webstrate.'; if (Array.isArray(config.loggedInToCreateWebstrates)) { const allowedProviders = config.loggedInToCreateWebstrates.join(' or '); err = `Must be logged in with ${allowedProviders} to delete a webstrate.`; } return res.status(403).send(err); } if (!req.user.permissions.includes('w')) { return res.status(403).send('Write permissions are required to delete a document.'); } // If the document contains a user with admin permissions, only admins can delete the // document. if (!req.user.permissions.includes('a') && await permissionManager.webstrateHasAdmin(req.webstrateId)) { return res.status(403).send('Admin permissions are required to delete this document.'); } return deleteWebstrate(req, res); } // We don't need to check for "static" in req.query, because this happens on the client side. return serveWebstrate(req, res); });