UNPKG

14.8 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.serverManifestJson = void 0;
4const tslib_1 = require("tslib");
5const crypto = require("crypto");
6const css2json = require("css2json");
7const debug_ = require("debug");
8const DotProp = require("dot-prop");
9const express = require("express");
10const jsonMarkup = require("json-markup");
11const path = require("path");
12const serializable_1 = require("r2-lcp-js/dist/es7-es2016/src/serializable");
13const epub_1 = require("r2-shared-js/dist/es7-es2016/src/parser/epub");
14const UrlUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/http/UrlUtils");
15const JsonUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/JsonUtils");
16const json_schema_validate_1 = require("../utils/json-schema-validate");
17const request_ext_1 = require("./request-ext");
18const debug = debug_("r2:streamer#http/server-manifestjson");
19function serverManifestJson(server, routerPathBase64) {
20 const jsonStyle = `
21.json-markup {
22 line-height: 17px;
23 font-size: 13px;
24 font-family: monospace;
25 white-space: pre;
26}
27.json-markup-key {
28 font-weight: bold;
29}
30.json-markup-bool {
31 color: firebrick;
32}
33.json-markup-string {
34 color: green;
35}
36.json-markup-null {
37 color: gray;
38}
39.json-markup-number {
40 color: blue;
41}
42`;
43 const routerManifestJson = express.Router({ strict: false });
44 routerManifestJson.get(["/", "/" + request_ext_1._show + "/:" + request_ext_1._jsonPath + "?"], (req, res) => tslib_1.__awaiter(this, void 0, void 0, function* () {
45 const reqparams = req.params;
46 if (!reqparams.pathBase64) {
47 reqparams.pathBase64 = req.pathBase64;
48 }
49 if (!reqparams.lcpPass64) {
50 reqparams.lcpPass64 = req.lcpPass64;
51 }
52 const isShow = req.url.indexOf("/show") >= 0 || req.query.show;
53 if (!reqparams.jsonPath && req.query.show) {
54 reqparams.jsonPath = req.query.show;
55 }
56 const isHead = req.method.toLowerCase() === "head";
57 if (isHead) {
58 debug("HEAD !!!!!!!!!!!!!!!!!!!");
59 }
60 const isCanonical = req.query.canonical &&
61 req.query.canonical === "true";
62 const isSecureHttp = req.secure ||
63 req.protocol === "https" ||
64 req.get("X-Forwarded-Proto") === "https";
65 const pathBase64Str = Buffer.from(reqparams.pathBase64, "base64").toString("utf8");
66 let publication;
67 try {
68 publication = yield server.loadOrGetCachedPublication(pathBase64Str);
69 }
70 catch (err) {
71 debug(err);
72 res.status(500).send("<html><body><p>Internal Server Error</p><p>"
73 + err + "</p></body></html>");
74 return;
75 }
76 if (reqparams.lcpPass64 && !server.disableDecryption) {
77 const lcpPass = Buffer.from(reqparams.lcpPass64, "base64").toString("utf8");
78 if (publication.LCP) {
79 try {
80 yield publication.LCP.tryUserKeys([lcpPass]);
81 }
82 catch (err) {
83 publication.LCP.ContentKey = undefined;
84 debug(err);
85 const errMsg = "FAIL publication.LCP.tryUserKeys(): " + err;
86 debug(errMsg);
87 res.status(500).send("<html><body><p>Internal Server Error</p><p>"
88 + errMsg + "</p></body></html>");
89 return;
90 }
91 }
92 }
93 const rootUrl = (isSecureHttp ? "https://" : "http://")
94 + req.headers.host + "/pub/"
95 + (reqparams.lcpPass64 ?
96 (server.lcpBeginToken + UrlUtils_1.encodeURIComponent_RFC3986(reqparams.lcpPass64) + server.lcpEndToken) :
97 "")
98 + UrlUtils_1.encodeURIComponent_RFC3986(reqparams.pathBase64);
99 const manifestURL = rootUrl + "/" + "manifest.json";
100 const contentType = (publication.Metadata && publication.Metadata.RDFType &&
101 /http[s]?:\/\/schema\.org\/Audiobook$/.test(publication.Metadata.RDFType)) ?
102 "application/audiobook+json" : ((publication.Metadata && publication.Metadata.RDFType &&
103 (/http[s]?:\/\/schema\.org\/ComicStory$/.test(publication.Metadata.RDFType) ||
104 /http[s]?:\/\/schema\.org\/VisualNarrative$/.test(publication.Metadata.RDFType))) ? "application/divina+json" :
105 "application/webpub+json");
106 const selfLink = publication.searchLinkByRel("self");
107 if (!selfLink) {
108 publication.AddLink(contentType, ["self"], manifestURL, undefined);
109 }
110 function absoluteURL(href) {
111 return rootUrl + "/" + href;
112 }
113 function absolutizeURLs(jsonObj) {
114 JsonUtils_1.traverseJsonObjects(jsonObj, (obj) => {
115 if (obj.href && typeof obj.href === "string"
116 && !UrlUtils_1.isHTTP(obj.href)) {
117 obj.href = absoluteURL(obj.href);
118 }
119 if (obj["media-overlay"] && typeof obj["media-overlay"] === "string"
120 && !UrlUtils_1.isHTTP(obj["media-overlay"])) {
121 obj["media-overlay"] = absoluteURL(obj["media-overlay"]);
122 }
123 });
124 }
125 let hasMO = false;
126 if (publication.Spine) {
127 const link = publication.Spine.find((l) => {
128 if (l.Properties && l.Properties.MediaOverlay) {
129 return true;
130 }
131 return false;
132 });
133 if (link) {
134 hasMO = true;
135 }
136 }
137 if (hasMO) {
138 const moLink = publication.searchLinkByRel("media-overlay");
139 if (!moLink) {
140 const moURL = epub_1.mediaOverlayURLPath +
141 "?" + epub_1.mediaOverlayURLParam + "={path}";
142 publication.AddLink("application/vnd.syncnarr+json", ["media-overlay"], moURL, true);
143 }
144 }
145 let coverImage;
146 const coverLink = publication.GetCover();
147 if (coverLink) {
148 coverImage = coverLink.Href;
149 if (coverImage && !UrlUtils_1.isHTTP(coverImage)) {
150 coverImage = absoluteURL(coverImage);
151 }
152 }
153 if (isShow) {
154 let objToSerialize = null;
155 if (reqparams.jsonPath) {
156 switch (reqparams.jsonPath) {
157 case "all": {
158 objToSerialize = publication;
159 break;
160 }
161 case "cover": {
162 objToSerialize = publication.GetCover();
163 break;
164 }
165 case "mediaoverlays": {
166 try {
167 objToSerialize = yield epub_1.getAllMediaOverlays(publication);
168 }
169 catch (err) {
170 debug(err);
171 res.status(500).send("<html><body><p>Internal Server Error</p><p>"
172 + err + "</p></body></html>");
173 return;
174 }
175 break;
176 }
177 case "spine": {
178 objToSerialize = publication.Spine;
179 break;
180 }
181 case "pagelist": {
182 objToSerialize = publication.PageList;
183 break;
184 }
185 case "landmarks": {
186 objToSerialize = publication.Landmarks;
187 break;
188 }
189 case "links": {
190 objToSerialize = publication.Links;
191 break;
192 }
193 case "resources": {
194 objToSerialize = publication.Resources;
195 break;
196 }
197 case "toc": {
198 objToSerialize = publication.TOC;
199 break;
200 }
201 case "metadata": {
202 objToSerialize = publication.Metadata;
203 break;
204 }
205 default: {
206 objToSerialize = null;
207 }
208 }
209 }
210 else {
211 objToSerialize = publication;
212 }
213 if (!objToSerialize) {
214 objToSerialize = {};
215 }
216 const jsonObj = serializable_1.TaJsonSerialize(objToSerialize);
217 let validationStr;
218 const doValidate = !reqparams.jsonPath || reqparams.jsonPath === "all";
219 if (doValidate) {
220 const jsonSchemasRootpath = path.join(process.cwd(), "misc", "json-schema");
221 const jsonSchemasNames = [
222 "webpub-manifest/publication",
223 "webpub-manifest/contributor-object",
224 "webpub-manifest/contributor",
225 "webpub-manifest/link",
226 "webpub-manifest/metadata",
227 "webpub-manifest/subcollection",
228 "webpub-manifest/properties",
229 "webpub-manifest/subject",
230 "webpub-manifest/subject-object",
231 "webpub-manifest/extensions/epub/metadata",
232 "webpub-manifest/extensions/epub/subcollections",
233 "webpub-manifest/extensions/epub/properties",
234 "webpub-manifest/extensions/presentation/metadata",
235 "webpub-manifest/extensions/presentation/properties",
236 "webpub-manifest/language-map",
237 "opds/acquisition-object",
238 "opds/catalog-entry",
239 "opds/properties",
240 ];
241 const validationErrors = json_schema_validate_1.jsonSchemaValidate(jsonSchemasRootpath, jsonSchemasNames, jsonObj);
242 if (validationErrors) {
243 validationStr = "";
244 for (const err of validationErrors) {
245 debug("JSON Schema validation FAIL.");
246 debug(err);
247 const val = DotProp.get(jsonObj, err.jsonPath);
248 const valueStr = (typeof val === "string") ?
249 `${val}` :
250 ((val instanceof Array || typeof val === "object") ?
251 `${JSON.stringify(val)}` :
252 "");
253 debug(valueStr);
254 const title = DotProp.get(jsonObj, "metadata.title");
255 debug(title);
256 validationStr +=
257 `\n"${title}"\n\n${err.ajvMessage}: ${valueStr}\n\n'${err.ajvDataPath.replace(/^\./, "")}' (${err.ajvSchemaPath})\n\n`;
258 }
259 }
260 }
261 absolutizeURLs(jsonObj);
262 let jsonPretty = jsonMarkup(jsonObj, css2json(jsonStyle));
263 const regex = new RegExp(">" + rootUrl + "/([^<]+</a>)", "g");
264 jsonPretty = jsonPretty.replace(regex, ">$1");
265 jsonPretty = jsonPretty.replace(/>manifest.json<\/a>/, ">" + rootUrl + "/manifest.json</a>");
266 res.status(200).send("<html>" +
267 "<head><script type=\"application/ld+json\" href=\"" +
268 manifestURL +
269 "\"></script></head>" +
270 "<body>" +
271 "<h1>" + path.basename(pathBase64Str) + "</h1>" +
272 (coverImage ? "<a href=\"" + coverImage + "\"><div style=\"width: 400px;\"><img src=\"" + coverImage + "\" alt=\"\" style=\"display: block; width: 100%; height: auto;\"/></div></a>" : "") +
273 "<hr><p><pre>" + jsonPretty + "</pre></p>" +
274 (doValidate ? (validationStr ? ("<hr><p><pre>" + validationStr + "</pre></p>") : ("<hr><p>JSON SCHEMA OK.</p>")) : "") +
275 "</body></html>");
276 }
277 else {
278 server.setResponseCORS(res);
279 res.set("Content-Type", `${contentType}; charset=utf-8`);
280 const publicationJsonObj = serializable_1.TaJsonSerialize(publication);
281 if (isCanonical) {
282 if (publicationJsonObj.links) {
283 delete publicationJsonObj.links;
284 }
285 }
286 const publicationJsonStr = isCanonical ?
287 global.JSON.stringify(JsonUtils_1.sortObject(publicationJsonObj), null, "") :
288 global.JSON.stringify(publicationJsonObj, null, " ");
289 const checkSum = crypto.createHash("sha256");
290 checkSum.update(publicationJsonStr);
291 const hash = checkSum.digest("hex");
292 const match = req.header("If-None-Match");
293 if (match === hash) {
294 debug("manifest.json cache");
295 res.status(304);
296 res.end();
297 return;
298 }
299 res.setHeader("ETag", hash);
300 const links = getPreFetchResources(publication);
301 if (links && links.length) {
302 let n = 0;
303 let prefetch = "";
304 for (const l of links) {
305 n++;
306 if (n > server.maxPrefetchLinks) {
307 break;
308 }
309 const href = absoluteURL(l.Href);
310 prefetch += "<" + href + ">;" + "rel=prefetch,";
311 }
312 res.setHeader("Link", prefetch);
313 }
314 res.status(200);
315 if (isHead) {
316 res.end();
317 }
318 else {
319 res.send(publicationJsonStr);
320 }
321 }
322 }));
323 routerPathBase64.use("/:" + request_ext_1._pathBase64 + "/manifest.json", routerManifestJson);
324}
325exports.serverManifestJson = serverManifestJson;
326function getPreFetchResources(publication) {
327 const links = [];
328 if (publication.Resources) {
329 const mediaTypes = ["text/css",
330 "text/javascript", "application/javascript",
331 "application/vnd.ms-opentype", "font/otf", "application/font-sfnt",
332 "font/ttf", "application/font-sfnt",
333 "font/woff", "application/font-woff", "font/woff2"];
334 for (const mediaType of mediaTypes) {
335 for (const link of publication.Resources) {
336 if (link.TypeLink === mediaType) {
337 links.push(link);
338 }
339 }
340 }
341 }
342 return links;
343}
344//# sourceMappingURL=server-manifestjson.js.map
\No newline at end of file