Home Reference Source

cables_dev/cables_electron/src/electron/electron_endpoint.js

import { protocol, session, net, shell } from "electron";
import fs from "fs";
import path from "path";
import mime from "mime";

import cables from "../cables.js";
import logger from "../utils/logger.js";
import doc from "../utils/doc_util.js";
import opsUtil from "../utils/ops_util.js";
import subPatchOpUtil from "../utils/subpatchop_util.js";
import settings from "./electron_settings.js";
import helper from "../utils/helper_util.js";
import electronApp from "./main.js";
import projectsUtil from "../utils/projects_util.js";


protocol.registerSchemesAsPrivileged([
    {
        "scheme": "cables",
        "privileges": {
            "bypassCSP": true,
            "supportFetchAPI": true
        }
    },
    {
        "scheme": "file",
        "privileges": {
            "stream": true,
            "bypassCSP": true,
            "supportFetchAPI": true
        }
    }
]);

class ElectronEndpoint
{
    constructor()
    {
        this._log = logger;
    }

    init()
    {
        const partition = settings.SESSION_PARTITION;
        const ses = session.fromPartition(partition, { "cache": false });

        ses.protocol.handle("file", async (request) =>
        {
            let urlFile = request.url;
            let absoluteFile = helper.fileURLToPath(urlFile, false);
            let projectFile = helper.fileURLToPath(urlFile, true);
            if (fs.existsSync(absoluteFile))
            {
                const response = await net.fetch(helper.pathToFileURL(absoluteFile), { "bypassCustomProtocolHandlers": true });
                this._addDefaultHeaders(response, absoluteFile);
                return response;
            }
            else if (fs.existsSync(projectFile))
            {
                const response = await net.fetch(helper.pathToFileURL(projectFile), { "bypassCustomProtocolHandlers": true });
                this._addDefaultHeaders(response, projectFile);
                return response;
            }
            else
            {
                try
                {
                    if (projectFile.includes("?"))
                    {
                        projectFile = projectFile.split("?")[0];
                    }
                    if (fs.existsSync(projectFile))
                    {
                        const response = await net.fetch(helper.pathToFileURL(projectFile), { "bypassCustomProtocolHandlers": true });
                        this._addDefaultHeaders(response, projectFile);
                        return response;
                    }
                    else
                    {
                        return new Response(null, { "headers": { "status": 404 } });
                    }
                }
                catch (e)
                {
                    return net.fetch(request.url, { "bypassCustomProtocolHandlers": true });
                }
            }
        });

        ses.protocol.handle("cables", async (request) =>
        {
            const url = new URL(request.url);
            const urlPath = url.pathname;
            if (urlPath.startsWith("/api/corelib/"))
            {
                const libName = urlPath.split("/", 4)[3];
                const libCode = this.apiGetCoreLibs(libName);
                if (libCode)
                {
                    return new Response(libCode, {
                        "headers": { "content-type": "application/javascript" }
                    });
                }
                else
                {
                    return new Response(libCode, {
                        "headers": { "content-type": "application/javascript" },
                        "status": 500
                    });
                }
            }
            else if (urlPath.startsWith("/api/lib/"))
            {
                const libName = urlPath.split("/", 4)[3];
                const libCode = this.apiGetLibs(libName);
                if (libCode)
                {
                    return new Response(libCode, {
                        "headers": { "content-type": "application/javascript" }
                    });
                }
                else
                {
                    return new Response(libCode, {
                        "headers": { "content-type": "application/javascript" },
                        "status": 500
                    });
                }
            }
            else if (urlPath === "/api/errorReport")
            {
                return new Response(JSON.stringify(this.apiErrorReport(request)));
            }
            else if (urlPath === "/api/changelog")
            {
                return new Response(JSON.stringify(this.apiGetChangelog()), {
                    "headers": { "content-type": "application/json" }
                });
            }
            else if (urlPath.startsWith("/api/ops/code/project"))
            {
                const code = this.apiGetProjectOpsCode();
                return new Response(code, {
                    "headers": { "content-type": "application/json" }
                });
            }
            else if (urlPath.startsWith("/api/ops/code"))
            {
                const code = this.apiGetCoreOpsCode();
                if (code)
                {
                    return new Response(code, {
                        "headers": { "content-type": "application/javascript" }
                    });
                }
                else
                {
                    return new Response(code, {
                        "headers": { "content-type": "application/javascript" },
                        "status": 500
                    });
                }
            }
            else if (urlPath.startsWith("/api/op/layout/"))
            {
                let opName = urlPath.split("/", 5)[4];
                if (opsUtil.isOpId(opName))
                {
                    opName = opsUtil.getOpNameById(opName);
                }
                const layoutSvg = this.apiOpLayout(opName);
                if (layoutSvg)
                {
                    return new Response(layoutSvg, {
                        "headers": { "content-type": "image/svg+xml" }
                    });
                }
                else
                {
                    return new Response("", {
                        "headers": { "content-type": "image/svg+xml" },
                        "status": 500
                    });
                }
            }
            else if (urlPath.startsWith("/api/op/"))
            {
                let opName = urlPath.split("/", 4)[3];
                if (opsUtil.isOpId(opName))
                {
                    opName = opsUtil.getOpNameById(opName);
                }
                if (opName)
                {
                    const opCode = this.apiGetOpCode({ "opName": opName });
                    if (opCode)
                    {
                        return new Response(opCode, {
                            "headers": { "content-type": "application/javascript" }
                        });
                    }
                    else
                    {
                        return new Response(opCode, {
                            "headers": { "content-type": "application/javascript" },
                            "status": 500
                        });
                    }
                }
                else
                {
                    return new Response("", {
                        "headers": { "content-type": "application/javascript" },
                        "status": 404
                    });
                }
            }
            else if (urlPath.startsWith("/op/screenshot"))
            {
                let opName = urlPath.split("/", 4)[3];
                if (opName) opName = opName.replace(/.png$/, "");
                const absoluteFile = opsUtil.getOpAbsolutePath(opName);
                const file = path.join(absoluteFile, "screenshot.png");
                const response = await net.fetch(helper.pathToFileURL(file), { "bypassCustomProtocolHandlers": true });
                this._addDefaultHeaders(response, file);
                return response;
            }
            else if (urlPath.startsWith("/edit/"))
            {
                let patchId = urlPath.split("/", 3)[2];
                let projectFile = null;
                if (patchId)
                {
                    projectFile = settings.getRecentProjectFile(patchId);
                }
                if (projectFile)
                {
                    await electronApp.openPatch(projectFile, true);
                }
                else
                {
                    await electronApp.pickProjectFileDialog();
                }
                return new Response(null, { "status": 302 });
            }
            else if (urlPath.startsWith("/openDir/"))
            {
                let dir = urlPath.replace("/openDir/", "");
                // dir = path.dirname(dir);
                await shell.showItemInFolder(dir);
                return new Response(null, { "status": 404 });
            }
            else
            {
                return new Response("", {
                    "headers": { "content-type": "application/javascript" },
                    "status": 404
                });
            }
        });
    }


    apiGetCoreOpsCode()
    {
        const opDocs = doc.getOpDocs();
        const code = opsUtil.buildCode(cables.getCoreOpsPath(), null, true, true, opDocs);
        if (!code) this._log.warn("FAILED TO GET CODE FOR COREOPS FROM", cables.getCoreOpsPath());
        return code;
    }

    apiGetProjectOpsCode()
    {
        const project = settings.getCurrentProject();

        let code = "";
        let missingOps = [];
        if (project)
        {
            let opDocs = doc.getOpDocs(false, false);
            let allOps = [];
            if (project.ops) allOps = project.ops.filter((op) => { return !opDocs.some((d) => { return d.id === op.opId; }); });
            const opsInProjectDir = projectsUtil.getOpDocsInProjectDirs(project);
            const ops = subPatchOpUtil.getOpsUsedInSubPatches(project);
            allOps = allOps.concat(opsInProjectDir);
            allOps = allOps.concat(ops);
            missingOps = allOps.filter((op) => { return !opDocs.some((d) => { return d.id === op.opId || d.id === op.id; }); });
        }

        const opsWithCode = [];
        let codeNamespaces = [];

        missingOps.forEach((missingOp) =>
        {
            const opId = missingOp.opId || missingOp.id;
            const opName = missingOp.name || opsUtil.getOpNameById(opId);
            if (opId && opName)
            {
                if (!opsWithCode.includes(opName))
                {
                    const parts = opName.split(".");
                    for (let k = 1; k < parts.length; k++)
                    {
                        let partPartname = "";
                        for (let j = 0; j < k; j++) partPartname += parts[j] + ".";

                        partPartname = partPartname.substr(0, partPartname.length - 1);
                        codeNamespaces.push(partPartname + "=" + partPartname + " || {};");
                    }
                    const fn = opsUtil.getOpAbsoluteFileName(opName);
                    if (fn)
                    {
                        code += opsUtil.getOpFullCode(fn, opName, opId);
                        opsWithCode.push(opName);
                    }
                }
                doc.addOpToLookup(opId, opName);
            }
        });

        codeNamespaces = helper.sortAndReduce(codeNamespaces);
        let fullCode = opsUtil.OPS_CODE_PREFIX;
        if (codeNamespaces && codeNamespaces.length > 0)
        {
            codeNamespaces[0] = "var " + codeNamespaces[0];
            fullCode += codeNamespaces.join("\n") + "\n\n";
        }

        fullCode += code;
        return fullCode;
    }

    apiGetOpCode(params)
    {
        const opName = params.opName;
        let code = "";
        const currentProject = settings.getCurrentProject();
        try
        {
            const attachmentOps = opsUtil.getSubPatchOpAttachment(opName);
            const bpOps = subPatchOpUtil.getOpsUsedInSubPatches(attachmentOps);

            if (!bpOps)
            {
                return code;
            }
            else
            {
                let opNames = [];
                for (let i = 0; i < bpOps.length; i++)
                {
                    const bpOp = bpOps[i];
                    const bpOpName = opsUtil.getOpNameById(bpOp.opId);
                    if (opsUtil.isCoreOp(bpOpName) && (!opsUtil.isOpOldVersion(bpOpName) && !opsUtil.isDeprecated(bpOpName))) continue;
                    if (currentProject && currentProject.ops && currentProject.ops.some((projectOp) => { return projectOp.opId === bpOp.opId; })) continue;
                    opNames.push(bpOpName);
                }

                if (opsUtil.isExtension(opName) || opsUtil.isTeamNamespace(opName))
                {
                    const collectionName = opsUtil.getCollectionNamespace(opName);
                    opNames = opNames.concat(opsUtil.getCollectionOpNames(collectionName));
                    opNames.push(opName);
                }
                else
                {
                    opNames.push(opName);
                }

                const ops = [];
                opNames.forEach((name) =>
                {
                    ops.push({
                        "objName": name,
                        "opId": opsUtil.getOpIdByObjName(name)
                    });
                });

                code = opsUtil.buildFullCode(ops, "none");
                return code;
            }
        }
        catch (e)
        {
            this._log.error("FAILED TO BUILD OPCODE FOR", opName, e);
            return code;
        }
    }

    apiGetCoreLibs(name)
    {
        const fn = path.join(cables.getCoreLibsPath(), name + ".js");

        if (fs.existsSync(fn))
        {
            let info = fs.readFileSync(fn);
            info += "\n\nCABLES.loadedCoreLib(\"" + name + "\")";
            return info;
        }
        else
        {
            this._log.error("COULD NOT FIND CORELIB FILE AT", fn);
            return "";
        }
    }

    apiGetLibs(name)
    {
        const fn = path.join(cables.getLibsPath(), name);
        if (fs.existsSync(fn))
        {
            let info = fs.readFileSync(fn);
            info = info + "\n\nCABLES.loadedLib(\"" + name + "\")";
            return info;
        }
        else
        {
            this._log.error("COULD NOT FIND LIB FILE AT", fn);
            return "";
        }
    }

    apiGetChangelog()
    {
        return {
            "ts": Date.now(),
            "items": []
        };
    }

    apiOpLayout(opName)
    {
        return opsUtil.getOpSVG(opName);
    }

    _addDefaultHeaders(response, existingFile)
    {
        try
        {
            const stats = fs.statSync(existingFile);
            if (stats)
            {
                response.headers.append("Accept-Ranges", "bytes");
                response.headers.append("Content-Length", stats.size);
                response.headers.append("Content-Range", "bytes 0-" + stats.size + "/" + (stats.size + 1));
                response.headers.append("Last-Modified", stats.mtime.toUTCString());
            }
            let mimeType = mime.getType(existingFile);
            if (mimeType)
            {
                if (mimeType === "application/node") mimeType = "text/javascript";
                response.headers.set("Content-Type", mimeType);
            }
        }
        catch (e) {}
        return response;
    }

    apiErrorReport(request)
    {
        try
        {
            request.json().then((report) =>
            {
                const communityUrl = cables.getCommunityUrl();
                if (cables.sendErrorReports() && communityUrl)
                {
                    try
                    {
                        const errorReportSend = net.request({
                            "url": path.join(communityUrl, "/api/errorReport"),
                            "method": "POST",
                        });
                        delete report.url;
                        delete report.file;
                        if (report.log)
                        {
                            report.log.forEach((log) =>
                            {
                                if (log.errorStack)
                                {
                                    log.errorStack.forEach((stack) =>
                                    {
                                        if (stack.fileName)
                                        {
                                            stack.fileName = path.basename(stack.fileName);
                                        }
                                        if (stack.source)
                                        {
                                            delete stack.source;
                                        }
                                    });
                                }
                            });
                        }
                        report.username = "standalone";
                        errorReportSend.setHeader("Content-Type", "application/json");
                        errorReportSend.write(JSON.stringify(report), "utf-8");
                        errorReportSend.end();
                    }
                    catch (e)
                    {
                        this._log.debug("failed to send error report", e);
                    }
                }
            });
        }
        catch (e)
        {
            this._log.info("failed to parse error report", e);
        }
        return { "success": true };
    }
}

export default new ElectronEndpoint();