summaryrefslogtreecommitdiff
path: root/src/content/staticNS.js
blob: 236abb4778945e301f9ad48b4da1c413ce752917 (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
{
  'use strict';
  let listenersMap = new Map();
  let backlog = new Set();

  let ns = {
    debug: true, // DEV_ONLY
    get embeddingDocument() {
      delete this.embeddingDocument;
      return this.embeddingDocument = CSP.isEmbedType(document.contentType);
    },
    on(eventName, listener) {
      let listeners = listenersMap.get(eventName);
      if (!listeners) listenersMap.set(eventName, listeners = new Set());
      listeners.add(listener);
      if (backlog.has(eventName)) this.fire(eventName, listener);
    },
    detach(eventName, listener) {
      let listeners = listenersMap.get(eventName);
      if (listeners) listeners.delete(listener);
    },
    fire(eventName, listener = null) {
      if (listener) {
        listener({type:eventName, source: this});
        return;
      }
      let listeners = listenersMap.get(eventName);
      if (listeners) {
        for (let l of listeners) {
          this.fire(eventName, l);
        }
      }
      backlog.add(eventName);
    },

    fetchPolicy() {
      let url = document.URL;
      debug(`Fetching policy from document %s, readyState %s`,
        url, document.readyState
        , document.documentElement.outerHTML, // DEV_ONLY
        document.domain, document.baseURI, window.isSecureContext // DEV_ONLY
      );

      if (!/^(?:file|ftp|https?):/i.test(url)) {
        if (/^(javascript|about):/.test(url)) {
          url = document.readyState === "loading"
          ? document.baseURI
          : `${window.isSecureContext ? "https" : "http"}://${document.domain}`;
          debug("Fetching policy for actual URL %s (was %s)", url, document.URL);
        }
        (async () => {
          let policy;
          try {
            policy = await Messages.send("fetchChildPolicy", {url, contextUrl: url});
          } catch (e) {
            console.error("Error while fetching policy", e);
          }
          if (policy === undefined) {
            log("Policy was undefined, retrying in 1/2 sec...");
            setTimeout(() => this.fetchPolicy(), 500);
            return;
          }
          this.setup(policy);
        })();
        return;
      }

      let originalState = document.readyState;
      let blockedScripts = [];

      if (/^(?:ftp|file):/.test(url)) {
        addEventListener("beforescriptexecute", e => {
          // safety net for synchronous loads on Firefox
          if (!this.canScript) {
            e.preventDefault();
            let script = e.target;
            blockedScripts.push(script)
            log("Some script managed to be inserted in the DOM while fetching policy, blocking it.\n", script);
          }
        }, true);
      }

      let policy = null;

      let setup = policy => {
        debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY
        this.setup(policy);
        if (this.canScript && blockedScripts.length && originalState === "loading") {
          log("Running suspended scripts which are permitted by %s policy.", url)
          // something went wrong, e.g. with session restore.
          for (let s of blockedScripts) {
            // reinsert the script:
            // just s.cloneNode(true) doesn't work, the script wouldn't run,
            // let's clone it the hard way...
            try {
              s.replaceWith(document.createRange().createContextualFragment(s.outerHTML));
            } catch (e) {
              error(e);
            }
          }
        }
      }

      for (;;) {
        try {
          policy = browser.runtime.sendSyncMessage(
            {id: "fetchPolicy", url, contextUrl: url}, setup);
          break;
        } catch (e) {
          if (!Messages.isMissingEndpoint(e)) {
            error(e);
            break;
          }
          error("Background page not ready yet, retrying to fetch policy...")
        }
      }

    },

    setup(policy) {
      debug("%s, %s, %o", document.URL, document.readyState, policy);
      if (!policy) {
        policy = {permissions: {capabilities: []}, localFallback: true};
      }
      this.policy = policy;

      if (!policy.permissions || policy.unrestricted) {
        this.allows = () => true;
        this.capabilities =  Object.assign(
          new Set(["script"]), { has() { return true; } });
      } else {
        let perms = policy.permissions;
        this.capabilities = new Set(perms.capabilities);
        new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument);
      }

      this.canScript = this.allows("script");
      this.fire("capabilities");
    },

    policy: null,

    allows(cap) {
      return this.capabilities && this.capabilities.has(cap);
    },

    getWindowName() {
      return window.name;
    }
  };

  if (this.ns) {
    this.ns.merge(ns);
  } else {
    this.ns = ns;
  }
}