1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.serverManifestJson = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const crypto = require("crypto");
|
6 | const css2json = require("css2json");
|
7 | const debug_ = require("debug");
|
8 | const DotProp = require("dot-prop");
|
9 | const express = require("express");
|
10 | const jsonMarkup = require("json-markup");
|
11 | const path = require("path");
|
12 | const serializable_1 = require("r2-lcp-js/dist/es7-es2016/src/serializable");
|
13 | const epub_1 = require("r2-shared-js/dist/es7-es2016/src/parser/epub");
|
14 | const UrlUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/http/UrlUtils");
|
15 | const JsonUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/JsonUtils");
|
16 | const json_schema_validate_1 = require("../utils/json-schema-validate");
|
17 | const request_ext_1 = require("./request-ext");
|
18 | const debug = debug_("r2:streamer#http/server-manifestjson");
|
19 | function 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 | }
|
325 | exports.serverManifestJson = serverManifestJson;
|
326 | function 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 |
|
\ | No newline at end of file |