Home Reference Source

cables_dev/cables_electron/src/electron/electron_endpoint.js

  1. // eslint-disable-next-line import/no-extraneous-dependencies
  2. import { net, protocol, session, shell } from "electron";
  3. import fs from "fs";
  4. import path from "path";
  5. import mime from "mime";
  6. import cables from "../cables.js";
  7. import logger from "../utils/logger.js";
  8. import doc from "../utils/doc_util.js";
  9. import opsUtil from "../utils/ops_util.js";
  10. import subPatchOpUtil from "../utils/subpatchop_util.js";
  11. import settings from "./electron_settings.js";
  12. import helper from "../utils/helper_util.js";
  13. import electronApp from "./main.js";
  14. import projectsUtil from "../utils/projects_util.js";
  15. protocol.registerSchemesAsPrivileged([
  16. {
  17. "scheme": "cables",
  18. "privileges": {
  19. "bypassCSP": true,
  20. "supportFetchAPI": true
  21. }
  22. },
  23. {
  24. "scheme": "file",
  25. "privileges": {
  26. "stream": true,
  27. "bypassCSP": true,
  28. "supportFetchAPI": true
  29. }
  30. }
  31. ]);
  32. class ElectronEndpoint
  33. {
  34. constructor()
  35. {
  36. this._log = logger;
  37. }
  38. init()
  39. {
  40. const partition = settings.SESSION_PARTITION;
  41. const ses = session.fromPartition(partition, { "cache": false });
  42. ses.protocol.handle("file", async (request) =>
  43. {
  44. let urlFile = request.url;
  45. let absoluteFile = helper.fileURLToPath(urlFile, false);
  46. let projectFile = helper.fileURLToPath(urlFile, true);
  47. if (fs.existsSync(absoluteFile))
  48. {
  49. Object.defineProperty(request, "url", { "value": helper.pathToFileURL(absoluteFile) });
  50. const response = await net.fetch(request, { "bypassCustomProtocolHandlers": true });
  51. this._addDefaultHeaders(request, response, absoluteFile);
  52. return response;
  53. }
  54. else if (fs.existsSync(projectFile))
  55. {
  56. Object.defineProperty(request, "url", { "value": helper.pathToFileURL(projectFile) });
  57. const response = await net.fetch(request, { "bypassCustomProtocolHandlers": true });
  58. this._addDefaultHeaders(request, response, projectFile);
  59. return response;
  60. }
  61. else
  62. {
  63. try
  64. {
  65. if (projectFile.includes("?"))
  66. {
  67. projectFile = projectFile.split("?")[0];
  68. }
  69. if (fs.existsSync(projectFile))
  70. {
  71. const response = await net.fetch(helper.pathToFileURL(projectFile), { "bypassCustomProtocolHandlers": true });
  72. this._addDefaultHeaders(request, response, projectFile);
  73. return response;
  74. }
  75. else
  76. {
  77. return new Response(null, { "headers": { "status": 404 } });
  78. }
  79. }
  80. catch (e)
  81. {
  82. return net.fetch(request.url, { "bypassCustomProtocolHandlers": true });
  83. }
  84. }
  85. });
  86. ses.protocol.handle("cables", async (request) =>
  87. {
  88. const url = new URL(request.url);
  89. const urlPath = url.pathname;
  90. const queryParams = new URLSearchParams(url.search);
  91. const params = {};
  92. const req = request;
  93. req.params = params;
  94. req.query = {};
  95. for (let key in queryParams)
  96. {
  97. req.query[key] = queryParams.get(key);
  98. }
  99. if (urlPath.startsWith("/api/corelib/"))
  100. {
  101. req.params.name = urlPath.split("/", 4)[3];
  102. const libCode = this.apiGetCoreLibs(req);
  103. if (libCode)
  104. {
  105. return new Response(libCode, {
  106. "headers": { "content-type": "application/javascript" }
  107. });
  108. }
  109. else
  110. {
  111. return new Response(libCode, {
  112. "headers": { "content-type": "application/javascript" },
  113. "status": 500
  114. });
  115. }
  116. }
  117. else if (urlPath.startsWith("/api/lib/"))
  118. {
  119. req.params.name = urlPath.split("/", 4)[3];
  120. const libCode = this.apiGetLibs(req);
  121. if (libCode)
  122. {
  123. return new Response(libCode, {
  124. "headers": { "content-type": "application/javascript" }
  125. });
  126. }
  127. else
  128. {
  129. return new Response(libCode, {
  130. "headers": { "content-type": "application/javascript" },
  131. "status": 500
  132. });
  133. }
  134. }
  135. else if (urlPath.startsWith("/api/oplib/"))
  136. {
  137. const parts = urlPath.split("/", 5);
  138. let opName = parts[3];
  139. let libName = parts[4];
  140. if (opsUtil.isOpId(opName))
  141. {
  142. opName = opsUtil.getOpNameById(opName);
  143. }
  144. if (opName)
  145. {
  146. const opPath = opsUtil.getOpAbsolutePath(opName);
  147. const libPath = path.join(opPath, libName);
  148. const libUrl = helper.pathToFileURL(libPath);
  149. const response = await net.fetch(libUrl, { "bypassCustomProtocolHandlers": true });
  150. this._addDefaultHeaders(request, response, libPath);
  151. return response;
  152. }
  153. else
  154. {
  155. return new Response("", {
  156. "headers": { "content-type": "application/javascript" },
  157. "status": 404
  158. });
  159. }
  160. }
  161. else if (urlPath === "/api/changelog")
  162. {
  163. return new Response(JSON.stringify(this.apiGetChangelog(req)), {
  164. "headers": { "content-type": "application/json" }
  165. });
  166. }
  167. else if (urlPath.startsWith("/api/ops/code/project"))
  168. {
  169. const code = this.apiGetProjectOpsCode(req);
  170. return new Response(code, {
  171. "headers": { "content-type": "application/json" }
  172. });
  173. }
  174. else if (urlPath.startsWith("/api/ops/code"))
  175. {
  176. const code = this.apiGetCoreOpsCode(req);
  177. if (code)
  178. {
  179. return new Response(code, {
  180. "headers": { "content-type": "application/javascript" }
  181. });
  182. }
  183. else
  184. {
  185. return new Response(code, {
  186. "headers": { "content-type": "application/javascript" },
  187. "status": 500
  188. });
  189. }
  190. }
  191. else if (urlPath.startsWith("/api/op/layout/"))
  192. {
  193. let opName = urlPath.split("/", 5)[4];
  194. if (opsUtil.isOpId(opName))
  195. {
  196. opName = opsUtil.getOpNameById(opName);
  197. }
  198. req.params.opName = opName;
  199. const layoutSvg = this.apiOpLayout(req);
  200. if (layoutSvg)
  201. {
  202. return new Response(layoutSvg, {
  203. "headers": { "content-type": "image/svg+xml" }
  204. });
  205. }
  206. else
  207. {
  208. return new Response("", {
  209. "headers": { "content-type": "image/svg+xml" },
  210. "status": 500
  211. });
  212. }
  213. }
  214. else if (urlPath.startsWith("/api/op/"))
  215. {
  216. let opName = urlPath.split("/", 4)[3];
  217. if (opsUtil.isOpId(opName))
  218. {
  219. opName = opsUtil.getOpNameById(opName);
  220. }
  221. if (opName)
  222. {
  223. req.params.opName = opName;
  224. const opCode = this.apiGetOpCode(req);
  225. if (opCode)
  226. {
  227. return new Response(opCode, {
  228. "headers": { "content-type": "application/javascript" }
  229. });
  230. }
  231. else
  232. {
  233. return new Response(opCode, {
  234. "headers": { "content-type": "application/javascript" },
  235. "status": 500
  236. });
  237. }
  238. }
  239. else
  240. {
  241. return new Response("", {
  242. "headers": { "content-type": "application/javascript" },
  243. "status": 404
  244. });
  245. }
  246. }
  247. else if (urlPath.startsWith("/op/screenshot"))
  248. {
  249. let opName = urlPath.split("/", 4)[3];
  250. if (opName) opName = opName.replace(/.png$/, "");
  251. const absoluteFile = opsUtil.getOpAbsolutePath(opName);
  252. const file = path.join(absoluteFile, "screenshot.png");
  253. const response = await net.fetch(helper.pathToFileURL(file), { "bypassCustomProtocolHandlers": true });
  254. this._addDefaultHeaders(request, response, file);
  255. return response;
  256. }
  257. else if (urlPath.startsWith("/edit/"))
  258. {
  259. let patchId = urlPath.split("/", 3)[2];
  260. let projectFile = null;
  261. if (patchId)
  262. {
  263. projectFile = settings.getRecentProjectFile(patchId);
  264. }
  265. if (projectFile)
  266. {
  267. await electronApp.openPatch(projectFile, true);
  268. }
  269. else
  270. {
  271. await electronApp.pickProjectFileDialog();
  272. }
  273. return new Response(null, { "status": 302 });
  274. }
  275. else if (urlPath.startsWith("/openDir/"))
  276. {
  277. let dir = urlPath.replace("/openDir/", "");
  278. await shell.showItemInFolder(dir);
  279. return new Response(null, { "status": 404 });
  280. }
  281. else
  282. {
  283. return new Response("", {
  284. "headers": { "content-type": "application/javascript" },
  285. "status": 404
  286. });
  287. }
  288. });
  289. }
  290. apiGetCoreOpsCode(req)
  291. {
  292. const preview = req.query.preview;
  293. const opDocs = doc.getOpDocs();
  294. const code = opsUtil.buildCode(cables.getCoreOpsPath(), null, true, true, opDocs, preview);
  295. if (!code) this._log.warn("FAILED TO GET CODE FOR COREOPS FROM", cables.getCoreOpsPath());
  296. return code;
  297. }
  298. apiGetProjectOpsCode(req)
  299. {
  300. const preview = req.query.preview;
  301. const project = settings.getCurrentProject();
  302. let code = "";
  303. let missingOps = [];
  304. if (project)
  305. {
  306. let opDocs = doc.getOpDocs(true, true);
  307. let allOps = [];
  308. if (project.ops) allOps = project.ops.filter((op) => { return !opDocs.some((d) => { return d.id === op.opId; }); });
  309. const opsInProjectDir = projectsUtil.getOpDocsInProjectDirs(project);
  310. const opsInSubPatches = subPatchOpUtil.getOpsUsedInSubPatches(project);
  311. allOps = allOps.concat(opsInProjectDir);
  312. allOps = allOps.concat(opsInSubPatches);
  313. missingOps = allOps.filter((op) => { return !opDocs.some((d) => { return d.id === op.opId || d.id === op.id; }); });
  314. }
  315. const opsWithCode = [];
  316. let codeNamespaces = [];
  317. missingOps.forEach((missingOp) =>
  318. {
  319. const opId = missingOp.opId || missingOp.id;
  320. const opName = missingOp.name || opsUtil.getOpNameById(opId);
  321. if (opId && opName)
  322. {
  323. if (!opsWithCode.includes(opName))
  324. {
  325. const parts = opName.split(".");
  326. for (let k = 1; k < parts.length; k++)
  327. {
  328. let partPartname = "";
  329. for (let j = 0; j < k; j++) partPartname += parts[j] + ".";
  330. partPartname = partPartname.substr(0, partPartname.length - 1);
  331. codeNamespaces.push(partPartname + "=" + partPartname + " || {};");
  332. }
  333. const fn = opsUtil.getOpAbsoluteFileName(opName);
  334. if (fn)
  335. {
  336. code += opsUtil.getOpFullCode(fn, opName, opId);
  337. opsWithCode.push(opName);
  338. }
  339. }
  340. doc.addOpToLookup(opId, opName);
  341. }
  342. });
  343. codeNamespaces = helper.sortAndReduce(codeNamespaces);
  344. let fullCode = opsUtil.OPS_CODE_PREFIX;
  345. if (codeNamespaces && codeNamespaces.length > 0)
  346. {
  347. codeNamespaces[0] = "var " + codeNamespaces[0];
  348. fullCode += codeNamespaces.join("\n") + "\n\n";
  349. }
  350. fullCode += code;
  351. return fullCode;
  352. }
  353. apiGetOpCode(req)
  354. {
  355. const preview = !!req.query.preview;
  356. const opName = req.params.opName;
  357. let code = "";
  358. const currentProject = settings.getCurrentProject();
  359. try
  360. {
  361. const attachmentOps = opsUtil.getSubPatchOpAttachment(opName);
  362. const bpOps = subPatchOpUtil.getOpsUsedInSubPatches(attachmentOps);
  363. if (!bpOps)
  364. {
  365. return code;
  366. }
  367. else
  368. {
  369. let opNames = [];
  370. for (let i = 0; i < bpOps.length; i++)
  371. {
  372. const bpOp = bpOps[i];
  373. const bpOpName = opsUtil.getOpNameById(bpOp.opId);
  374. if (opsUtil.isCoreOp(bpOpName) && (!opsUtil.isOpOldVersion(bpOpName) && !opsUtil.isDeprecated(bpOpName))) continue;
  375. if (currentProject && currentProject.ops && currentProject.ops.some((projectOp) => { return projectOp.opId === bpOp.opId; })) continue;
  376. opNames.push(bpOpName);
  377. }
  378. if (opsUtil.isExtension(opName) || opsUtil.isTeamNamespace(opName))
  379. {
  380. const collectionName = opsUtil.getCollectionNamespace(opName);
  381. opNames = opNames.concat(opsUtil.getCollectionOpNames(collectionName));
  382. opNames.push(opName);
  383. }
  384. else
  385. {
  386. opNames.push(opName);
  387. }
  388. const ops = [];
  389. opNames.forEach((name) =>
  390. {
  391. ops.push({
  392. "objName": name,
  393. "opId": opsUtil.getOpIdByObjName(name)
  394. });
  395. });
  396. code = preview ? opsUtil.buildPreviewCode(ops) : opsUtil.buildFullCode(ops, "none");
  397. return code;
  398. }
  399. }
  400. catch (e)
  401. {
  402. this._log.error("FAILED TO BUILD OPCODE FOR", opName, e);
  403. return code;
  404. }
  405. }
  406. apiGetCoreLibs(req)
  407. {
  408. const name = req.params.name;
  409. const fn = path.join(cables.getCoreLibsPath(), name + ".js");
  410. if (fs.existsSync(fn))
  411. {
  412. return fs.readFileSync(fn);
  413. }
  414. else
  415. {
  416. this._log.error("COULD NOT FIND CORELIB FILE AT", fn);
  417. return "";
  418. }
  419. }
  420. apiGetLibs(req)
  421. {
  422. const name = req.params.name;
  423. const fn = path.join(cables.getLibsPath(), name);
  424. if (fs.existsSync(fn))
  425. {
  426. return fs.readFileSync(fn);
  427. }
  428. else
  429. {
  430. this._log.error("COULD NOT FIND LIB FILE AT", fn);
  431. return "";
  432. }
  433. }
  434. apiGetChangelog()
  435. {
  436. return {
  437. "ts": Date.now(),
  438. "items": []
  439. };
  440. }
  441. apiOpLayout(req)
  442. {
  443. const opName = req.params.opName;
  444. return opsUtil.getOpSVG(opName);
  445. }
  446. _addDefaultHeaders(request, response, existingFile)
  447. {
  448. try
  449. {
  450. const stats = fs.statSync(existingFile);
  451. if (stats)
  452. {
  453. response.headers.append("Accept-Ranges", "bytes");
  454. response.headers.append("Last-Modified", stats.mtime.toUTCString());
  455. // large mp4 and range headers cause problems somehow...
  456. // https://github.com/laurent22/joplin/blob/e607a7376f8403082e87087a3e07f37cb2e1ce76/packages/app-desktop/utils/customProtocols/handleCustomProtocols.ts#L106
  457. const rangeHeader = request.headers.get("Range");
  458. const startByte = Number(rangeHeader.match(/(\d+)-/)?.[1] || "0");
  459. const endByte = Number(rangeHeader.match(/-(\d+)/)?.[1] || stats.size - 1);
  460. response.headers.append("Content-Range", "bytes 0-" + stats.size + "/" + (stats.size + 1));
  461. response.headers.append("Content-Length", (endByte + 1) - startByte);
  462. }
  463. let mimeType = mime.getType(existingFile);
  464. if (mimeType)
  465. {
  466. if (mimeType === "application/node") mimeType = "text/javascript";
  467. response.headers.set("Content-Type", mimeType);
  468. }
  469. }
  470. catch (e) {}
  471. return response;
  472. }
  473. }
  474. export default new ElectronEndpoint();