Home Reference Source

cables_dev/cables_electron/src/electron/electron_endpoint.js

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