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