1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.serverOPDS_local_feed = exports.serverOPDS_local_feed_PATH_ = exports.serverOPDS_local_feed_PATH = void 0;
|
4 | const crypto = require("crypto");
|
5 | const css2json = require("css2json");
|
6 | const debug_ = require("debug");
|
7 | const DotProp = require("dot-prop");
|
8 | const express = require("express");
|
9 | const jsonMarkup = require("json-markup");
|
10 | const path = require("path");
|
11 | const serializable_1 = require("r2-lcp-js/dist/es7-es2016/src/serializable");
|
12 | const opds2_link_1 = require("r2-opds-js/dist/es7-es2016/src/opds/opds2/opds2-link");
|
13 | const UrlUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/http/UrlUtils");
|
14 | const JsonUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/JsonUtils");
|
15 | const json_schema_validate_1 = require("../utils/json-schema-validate");
|
16 | const request_ext_1 = require("./request-ext");
|
17 | const server_trailing_slash_redirect_1 = require("./server-trailing-slash-redirect");
|
18 | const debug = debug_("r2:streamer#http/server-opds-local-feed");
|
19 | exports.serverOPDS_local_feed_PATH = "/opds2";
|
20 | exports.serverOPDS_local_feed_PATH_ = "/publications.json";
|
21 | function serverOPDS_local_feed(server, topRouter) {
|
22 | const jsonStyle = `
|
23 | .json-markup {
|
24 | line-height: 17px;
|
25 | font-size: 13px;
|
26 | font-family: monospace;
|
27 | white-space: pre;
|
28 | }
|
29 | .json-markup-key {
|
30 | font-weight: bold;
|
31 | }
|
32 | .json-markup-bool {
|
33 | color: firebrick;
|
34 | }
|
35 | .json-markup-string {
|
36 | color: green;
|
37 | }
|
38 | .json-markup-null {
|
39 | color: gray;
|
40 | }
|
41 | .json-markup-number {
|
42 | color: blue;
|
43 | }
|
44 | `;
|
45 | const routerOPDS_local_feed = express.Router({ strict: false });
|
46 | routerOPDS_local_feed.get(["/", "/" + request_ext_1._show + "/:" + request_ext_1._jsonPath + "?"], (req, res) => {
|
47 | const reqparams = req.params;
|
48 | const isShow = req.url.indexOf("/show") >= 0 || req.query.show;
|
49 | if (!reqparams.jsonPath && req.query.show) {
|
50 | reqparams.jsonPath = req.query.show;
|
51 | }
|
52 | const isCanonical = req.query.canonical &&
|
53 | req.query.canonical === "true";
|
54 | const isSecureHttp = req.secure ||
|
55 | req.protocol === "https" ||
|
56 | req.get("X-Forwarded-Proto") === "https";
|
57 | const rootUrl = (isSecureHttp ? "https://" : "http://")
|
58 | + req.headers.host;
|
59 | const selfURL = rootUrl + exports.serverOPDS_local_feed_PATH + exports.serverOPDS_local_feed_PATH_;
|
60 | const feed = server.publicationsOPDS();
|
61 | if (!feed) {
|
62 | const err = "Publications OPDS2 feed not available yet, try again later.";
|
63 | debug(err);
|
64 | res.status(503).send("<html><body><p>Resource temporarily unavailable</p><p>"
|
65 | + err + "</p></body></html>");
|
66 | return;
|
67 | }
|
68 | if (!feed.findFirstLinkByRel("self")) {
|
69 | feed.Links = [];
|
70 | const selfLink = new opds2_link_1.OPDSLink();
|
71 | selfLink.Href = selfURL;
|
72 | selfLink.TypeLink = "application/opds+json";
|
73 | selfLink.AddRel("self");
|
74 | feed.Links.push(selfLink);
|
75 | }
|
76 | function absoluteURL(href) {
|
77 | return rootUrl + "/pub/" + href;
|
78 | }
|
79 | function absolutizeURLs(jsonObj) {
|
80 | JsonUtils_1.traverseJsonObjects(jsonObj, (obj) => {
|
81 | if (obj.href && typeof obj.href === "string") {
|
82 | if (!UrlUtils_1.isHTTP(obj.href)) {
|
83 | obj.href = absoluteURL(obj.href);
|
84 | }
|
85 | if (isShow &&
|
86 | (obj.type === "application/webpub+json"
|
87 | || obj.type === "application/audiobook+json"
|
88 | || obj.type === "application/divina+json") &&
|
89 | obj.rel === "http://opds-spec.org/acquisition" &&
|
90 | obj.href.endsWith("/manifest.json")) {
|
91 | obj.href += "/show";
|
92 | }
|
93 | }
|
94 | });
|
95 | }
|
96 | if (isShow) {
|
97 | let objToSerialize = null;
|
98 | if (reqparams.jsonPath) {
|
99 | switch (reqparams.jsonPath) {
|
100 | case "all": {
|
101 | objToSerialize = feed;
|
102 | break;
|
103 | }
|
104 | case "metadata": {
|
105 | objToSerialize = feed.Metadata;
|
106 | break;
|
107 | }
|
108 | case "links": {
|
109 | objToSerialize = feed.Links;
|
110 | break;
|
111 | }
|
112 | case "publications": {
|
113 | objToSerialize = feed.Publications;
|
114 | break;
|
115 | }
|
116 | default: {
|
117 | objToSerialize = null;
|
118 | }
|
119 | }
|
120 | }
|
121 | else {
|
122 | objToSerialize = feed;
|
123 | }
|
124 | if (!objToSerialize) {
|
125 | objToSerialize = {};
|
126 | }
|
127 | const jsonObj = serializable_1.TaJsonSerialize(objToSerialize);
|
128 | let validationStr;
|
129 | const doValidate = !reqparams.jsonPath || reqparams.jsonPath === "all";
|
130 | if (doValidate) {
|
131 | const jsonSchemasRootpath = path.join(process.cwd(), "misc", "json-schema");
|
132 | const jsonSchemasNames = [
|
133 | "opds/feed",
|
134 | "opds/publication",
|
135 | "opds/acquisition-object",
|
136 | "opds/catalog-entry",
|
137 | "opds/feed-metadata",
|
138 | "opds/properties",
|
139 | "webpub-manifest/publication",
|
140 | "webpub-manifest/contributor-object",
|
141 | "webpub-manifest/contributor",
|
142 | "webpub-manifest/link",
|
143 | "webpub-manifest/metadata",
|
144 | "webpub-manifest/subcollection",
|
145 | "webpub-manifest/properties",
|
146 | "webpub-manifest/subject",
|
147 | "webpub-manifest/subject-object",
|
148 | "webpub-manifest/extensions/epub/metadata",
|
149 | "webpub-manifest/extensions/epub/subcollections",
|
150 | "webpub-manifest/extensions/epub/properties",
|
151 | "webpub-manifest/extensions/presentation/metadata",
|
152 | "webpub-manifest/extensions/presentation/properties",
|
153 | "webpub-manifest/language-map",
|
154 | ];
|
155 | const validationErrors = json_schema_validate_1.jsonSchemaValidate(jsonSchemasRootpath, jsonSchemasNames, jsonObj);
|
156 | if (validationErrors) {
|
157 | validationStr = "";
|
158 | for (const err of validationErrors) {
|
159 | debug("JSON Schema validation FAIL.");
|
160 | debug(err);
|
161 | const val = DotProp.get(jsonObj, err.jsonPath);
|
162 | const valueStr = (typeof val === "string") ?
|
163 | `${val}` :
|
164 | ((val instanceof Array || typeof val === "object") ?
|
165 | `${JSON.stringify(val)}` :
|
166 | "");
|
167 | debug(valueStr);
|
168 | let title = "";
|
169 | let pubIndex = "";
|
170 | if (/^publications\.[0-9]+/.test(err.jsonPath)) {
|
171 | const jsonPubTitlePath = err.jsonPath.replace(/^(publications\.[0-9]+).*/, "$1.metadata.title");
|
172 | debug(jsonPubTitlePath);
|
173 | title = DotProp.get(jsonObj, jsonPubTitlePath);
|
174 | debug(title);
|
175 | pubIndex = err.jsonPath.replace(/^publications\.([0-9]+).*/, "$1");
|
176 | debug(pubIndex);
|
177 | }
|
178 | validationStr +=
|
179 | `\n___________INDEX___________ #${pubIndex} "${title}"\n\n${err.ajvMessage}: ${valueStr}\n\n'${err.ajvDataPath.replace(/^\./, "")}' (${err.ajvSchemaPath})\n\n`;
|
180 | }
|
181 | }
|
182 | }
|
183 | absolutizeURLs(jsonObj);
|
184 | if (jsonObj.publications && jsonObj.publications.length) {
|
185 | let i = 0;
|
186 | jsonObj.publications.forEach((pub) => {
|
187 | pub.___________INDEX___________ = i++;
|
188 | });
|
189 | }
|
190 | const jsonPretty = jsonMarkup(jsonObj, css2json(jsonStyle));
|
191 | res.status(200).send("<html><body>" +
|
192 | "<h1>OPDS2 JSON feed</h1>" +
|
193 | "<hr><p><pre>" + jsonPretty + "</pre></p>" +
|
194 | (doValidate ? (validationStr ? ("<hr><p><pre>" + validationStr + "</pre></p>") : ("<hr><p>JSON SCHEMA OK.</p>")) : "") +
|
195 | "</body></html>");
|
196 | }
|
197 | else {
|
198 | server.setResponseCORS(res);
|
199 | res.set("Content-Type", "application/opds+json; charset=utf-8");
|
200 | const publicationsJsonObj = serializable_1.TaJsonSerialize(feed);
|
201 | absolutizeURLs(publicationsJsonObj);
|
202 | const publicationsJsonStr = isCanonical ?
|
203 | global.JSON.stringify(JsonUtils_1.sortObject(publicationsJsonObj), null, "") :
|
204 | global.JSON.stringify(publicationsJsonObj, null, " ");
|
205 | const checkSum = crypto.createHash("sha256");
|
206 | checkSum.update(publicationsJsonStr);
|
207 | const hash = checkSum.digest("hex");
|
208 | const match = req.header("If-None-Match");
|
209 | if (match === hash) {
|
210 | debug("opds2 publications.json cache");
|
211 | res.status(304);
|
212 | res.end();
|
213 | return;
|
214 | }
|
215 | res.setHeader("ETag", hash);
|
216 | res.status(200).send(publicationsJsonStr);
|
217 | }
|
218 | });
|
219 | const routerOPDS_local_feed_ = express.Router({ strict: false });
|
220 | routerOPDS_local_feed_.use(server_trailing_slash_redirect_1.trailingSlashRedirect);
|
221 | routerOPDS_local_feed_.get("/", (req, res) => {
|
222 | const i = req.originalUrl.indexOf("?");
|
223 | let pathWithoutQuery = req.originalUrl;
|
224 | if (i >= 0) {
|
225 | pathWithoutQuery = pathWithoutQuery.substr(0, i);
|
226 | }
|
227 | let redirect = pathWithoutQuery +
|
228 | exports.serverOPDS_local_feed_PATH_ + "/show";
|
229 | redirect = redirect.replace("//", "/");
|
230 | if (i >= 0) {
|
231 | redirect += req.originalUrl.substr(i);
|
232 | }
|
233 | debug(`REDIRECT: ${req.originalUrl} ==> ${redirect}`);
|
234 | res.redirect(301, redirect);
|
235 | });
|
236 | routerOPDS_local_feed_.use(exports.serverOPDS_local_feed_PATH_, routerOPDS_local_feed);
|
237 | topRouter.use(exports.serverOPDS_local_feed_PATH, routerOPDS_local_feed_);
|
238 | }
|
239 | exports.serverOPDS_local_feed = serverOPDS_local_feed;
|
240 |
|
\ | No newline at end of file |