summaryrefslogtreecommitdiff
path: root/qutebrowser/javascript/greasemonkey_wrapper.js
blob: 8a0a12c0db150ac2ca9edf2bb0cb9a025a151063 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
(function() {
    const _qute_script_id = "__gm_{{ scriptName }}";

    function GM_log(text) {
        console.log(text);
    }

    const GM_info = {
        'script': {{ scriptInfo }},
        'scriptMetaStr': "{{ scriptMeta }}",
        'scriptWillUpdate': false,
        'version': "0.0.1",
        // so scripts don't expect exportFunction
        'scriptHandler': 'Tampermonkey',
    };

    function checkKey(key, funcName) {
        if (typeof key !== "string") {
          throw new Error(`${funcName} requires the first parameter to be of type string, not '${typeof key}'`);
        }
    }

    function GM_setValue(key, value) {
        checkKey(key, "GM_setValue");
        if (typeof value !== "string" &&
            typeof value !== "number" &&
            typeof value !== "boolean") {
          throw new Error(`GM_setValue requires the second parameter to be of type string, number or boolean, not '${typeof value}'`);
        }
        localStorage.setItem(_qute_script_id + key, value);
    }

    function GM_getValue(key, default_) {
        checkKey(key, "GM_getValue");
        return localStorage.getItem(_qute_script_id + key) || default_;
    }

    function GM_deleteValue(key) {
        checkKey(key, "GM_deleteValue");
        localStorage.removeItem(_qute_script_id + key);
    }

    function GM_listValues() {
        const keys = [];
        for (let i = 0; i < localStorage.length; i++) {
            if (localStorage.key(i).startsWith(_qute_script_id)) {
                keys.push(localStorage.key(i).slice(_qute_script_id.length));
            }
        }
        return keys;
    }

    function GM_openInTab(url) {
        window.open(url);
    }


    // Almost verbatim copy from Eric
    function GM_xmlhttpRequest(/* object */ details) {
        details.method = details.method ? details.method.toUpperCase() : "GET";

        if (!details.url) {
            throw new Error("GM_xmlhttpRequest requires a URL.");
        }

        // build XMLHttpRequest object
        const oXhr = new XMLHttpRequest();
        // run it
        if ("onreadystatechange" in details) {
            oXhr.onreadystatechange = function() {
                details.onreadystatechange(oXhr);
            };
        }
        if ("onload" in details) {
            oXhr.onload = function() { details.onload(oXhr); };
        }
        if ("onerror" in details) {
            oXhr.onerror = function () { details.onerror(oXhr); };
        }
        if ("overrideMimeType" in details) {
            oXhr.overrideMimeType(details.overrideMimeType);
        }

        oXhr.open(details.method, details.url, true);

        if ("headers" in details) {
            for (const header in details.headers) {
                oXhr.setRequestHeader(header, details.headers[header]);
            }
        }

        if ("data" in details) {
            oXhr.send(details.data);
        } else {
            oXhr.send();
        }
    }

    function GM_addStyle(/* String */ styles) {
        const oStyle = document.createElement("style");
        oStyle.setAttribute("type", "text/css");
        oStyle.appendChild(document.createTextNode(styles));

        const head = document.getElementsByTagName("head")[0];
        if (head === undefined) {
            // no head yet, stick it wherever
            document.documentElement.appendChild(oStyle);
        } else {
            head.appendChild(oStyle);
        }
    }

    // Stub these two so that the gm4 polyfill script doesn't try to
    // create broken versions as attributes of window.
    function GM_getResourceText(caption, commandFunc, accessKey) {
        console.error(`${GM_info.script.name} called unimplemented GM_getResourceText`);
    }

    function GM_registerMenuCommand(caption, commandFunc, accessKey) {
        console.error(`${GM_info.script.name} called unimplemented GM_registerMenuCommand`);
    }

    // Mock the greasemonkey 4.0 async API.
    const GM = {};
    GM.info = GM_info;
    const entries = {
        'log': GM_log,
        'addStyle': GM_addStyle,
        'deleteValue': GM_deleteValue,
        'getValue': GM_getValue,
        'listValues': GM_listValues,
        'openInTab': GM_openInTab,
        'setValue': GM_setValue,
        'xmlHttpRequest': GM_xmlhttpRequest,
    }
    for (newKey in entries) {
        let old = entries[newKey];
        if (old && (typeof GM[newKey] == 'undefined')) {
            GM[newKey] = function(...args) {
                return new Promise((resolve, reject) => {
                    try {
                        resolve(old(...args));
                    } catch (e) {
                        reject(e);
                    }
                });
            };
        }
    };

    const unsafeWindow = window;
    {% if use_proxy %}
    /*
     * Try to give userscripts an environment that they expect. Which seems
     * to be that the global window object should look the same as the page's
     * one and that if a script writes to an attribute of window all other
     * scripts should be able to access that variable in the global scope.
     * Use a Proxy to stop scripts from actually changing the global window
     * (that's what unsafeWindow is for). Use the "with" statement to make
     * the proxy provide what looks like global scope.
     *
     * There are other Proxy functions that we may need to override.  set,
     * get and has are definitely required.
     */

    if (!window._qute_gm_window_proxy) {
        const qute_gm_window_shadow = {}; // stores local changes to window
        const qute_gm_windowProxyHandler = {
            get: function (target, prop) {
                if (prop in qute_gm_window_shadow)
                    return qute_gm_window_shadow[prop];
                if (prop in target) {
                    if (typeof target[prop] === 'function' && typeof target[prop].prototype == 'undefined')
                        // Getting TypeError: Illegal Execution when callers try
                        // to execute eg addEventListener from here because they
                        // were returned unbound
                        return target[prop].bind(target);
                    return target[prop];
                }
            },
            set: function(target, prop, val) {
                return qute_gm_window_shadow[prop] = val;
            },
            has: function(target, key) {
                return key in qute_gm_window_shadow || key in target;
            }
        };
        window._qute_gm_window_proxy = new Proxy(unsafeWindow, qute_gm_windowProxyHandler);
    }
    const qute_gm_window_proxy = window._qute_gm_window_proxy;
    with (qute_gm_window_proxy) {
        // We can't return `this` or `qute_gm_window_proxy` from
        // `qute_gm_window_proxy.get('window')` because the Proxy implementation
        // does typechecking on read-only things. So we have to shadow `window`
        // more conventionally here.
        const window = qute_gm_window_proxy;
        // ====== The actual user script source ====== //
{{ scriptSource }}
        // ====== End User Script ====== //
    };
    {% else %}
        // ====== The actual user script source ====== //
{{ scriptSource }}
        // ====== End User Script ====== //
    {% endif %}
})();