WIP: Note editing, markdown to html
[oweals/karmaworld.git] / karmaworld / apps / wysihtml5 / static / wysihtml5 / wysihtml-0.4.17 / src / dom / sandbox.js
1 /**
2  * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
3  *
4  * Browser Compatibility:
5  *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
6  *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
7  *
8  * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
9  *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
10  *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
11  *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
12  *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
13  *      can do anything as if the sandbox attribute wasn't set
14  *
15  * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
16  * @param {Object} [config] Optional parameters
17  *
18  * @example
19  *    new wysihtml5.dom.Sandbox(function(sandbox) {
20  *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
21  *    });
22  */
23 (function(wysihtml5) {
24   var /**
25        * Default configuration
26        */
27       doc                 = document,
28       /**
29        * Properties to unset/protect on the window object
30        */
31       windowProperties    = [
32         "parent", "top", "opener", "frameElement", "frames",
33         "localStorage", "globalStorage", "sessionStorage", "indexedDB"
34       ],
35       /**
36        * Properties on the window object which are set to an empty function
37        */
38       windowProperties2   = [
39         "open", "close", "openDialog", "showModalDialog",
40         "alert", "confirm", "prompt",
41         "openDatabase", "postMessage",
42         "XMLHttpRequest", "XDomainRequest"
43       ],
44       /**
45        * Properties to unset/protect on the document object
46        */
47       documentProperties  = [
48         "referrer",
49         "write", "open", "close"
50       ];
51
52   wysihtml5.dom.Sandbox = Base.extend(
53     /** @scope wysihtml5.dom.Sandbox.prototype */ {
54
55     constructor: function(readyCallback, config) {
56       this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
57       this.config   = wysihtml5.lang.object({}).merge(config).get();
58       this.editableArea   = this._createIframe();
59     },
60
61     insertInto: function(element) {
62       if (typeof(element) === "string") {
63         element = doc.getElementById(element);
64       }
65
66       element.appendChild(this.editableArea);
67     },
68
69     getIframe: function() {
70       return this.editableArea;
71     },
72
73     getWindow: function() {
74       this._readyError();
75     },
76
77     getDocument: function() {
78       this._readyError();
79     },
80
81     destroy: function() {
82       var iframe = this.getIframe();
83       iframe.parentNode.removeChild(iframe);
84     },
85
86     _readyError: function() {
87       throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
88     },
89
90     /**
91      * Creates the sandbox iframe
92      *
93      * Some important notes:
94      *  - We can't use HTML5 sandbox for now:
95      *    setting it causes that the iframe's dom can't be accessed from the outside
96      *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
97      *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
98      *    In order to make this happen we need to set the "allow-scripts" flag.
99      *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
100      *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
101      *  - IE needs to have the security="restricted" attribute set before the iframe is
102      *    inserted into the dom tree
103      *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
104      *    though it supports it
105      *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
106      *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
107      *    on the onreadystatechange event
108      */
109     _createIframe: function() {
110       var that   = this,
111           iframe = doc.createElement("iframe");
112       iframe.className = "wysihtml5-sandbox";
113       wysihtml5.dom.setAttributes({
114         "security":           "restricted",
115         "allowtransparency":  "true",
116         "frameborder":        0,
117         "width":              0,
118         "height":             0,
119         "marginwidth":        0,
120         "marginheight":       0
121       }).on(iframe);
122
123       // Setting the src like this prevents ssl warnings in IE6
124       if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
125         iframe.src = "javascript:'<html></html>'";
126       }
127
128       iframe.onload = function() {
129         iframe.onreadystatechange = iframe.onload = null;
130         that._onLoadIframe(iframe);
131       };
132
133       iframe.onreadystatechange = function() {
134         if (/loaded|complete/.test(iframe.readyState)) {
135           iframe.onreadystatechange = iframe.onload = null;
136           that._onLoadIframe(iframe);
137         }
138       };
139
140       return iframe;
141     },
142
143     /**
144      * Callback for when the iframe has finished loading
145      */
146     _onLoadIframe: function(iframe) {
147       // don't resume when the iframe got unloaded (eg. by removing it from the dom)
148       if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
149         return;
150       }
151
152       var that           = this,
153           iframeWindow   = iframe.contentWindow,
154           iframeDocument = iframe.contentWindow.document,
155           charset        = doc.characterSet || doc.charset || "utf-8",
156           sandboxHtml    = this._getHtml({
157             charset:      charset,
158             stylesheets:  this.config.stylesheets
159           });
160
161       // Create the basic dom tree including proper DOCTYPE and charset
162       iframeDocument.open("text/html", "replace");
163       iframeDocument.write(sandboxHtml);
164       iframeDocument.close();
165
166       this.getWindow = function() { return iframe.contentWindow; };
167       this.getDocument = function() { return iframe.contentWindow.document; };
168
169       // Catch js errors and pass them to the parent's onerror event
170       // addEventListener("error") doesn't work properly in some browsers
171       // TODO: apparently this doesn't work in IE9!
172       iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
173         throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
174       };
175
176       if (!wysihtml5.browser.supportsSandboxedIframes()) {
177         // Unset a bunch of sensitive variables
178         // Please note: This isn't hack safe!
179         // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
180         // IE is secure though, which is the most important thing, since IE is the only browser, who
181         // takes over scripts & styles into contentEditable elements when copied from external websites
182         // or applications (Microsoft Word, ...)
183         var i, length;
184         for (i=0, length=windowProperties.length; i<length; i++) {
185           this._unset(iframeWindow, windowProperties[i]);
186         }
187         for (i=0, length=windowProperties2.length; i<length; i++) {
188           this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
189         }
190         for (i=0, length=documentProperties.length; i<length; i++) {
191           this._unset(iframeDocument, documentProperties[i]);
192         }
193         // This doesn't work in Safari 5
194         // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
195         this._unset(iframeDocument, "cookie", "", true);
196       }
197
198       this.loaded = true;
199
200       // Trigger the callback
201       setTimeout(function() { that.callback(that); }, 0);
202     },
203
204     _getHtml: function(templateVars) {
205       var stylesheets = templateVars.stylesheets,
206           html        = "",
207           i           = 0,
208           length;
209       stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
210       if (stylesheets) {
211         length = stylesheets.length;
212         for (; i<length; i++) {
213           html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
214         }
215       }
216       templateVars.stylesheets = html;
217
218       return wysihtml5.lang.string(
219         '<!DOCTYPE html><html><head>'
220         + '<meta charset="#{charset}">#{stylesheets}</head>'
221         + '<body></body></html>'
222       ).interpolate(templateVars);
223     },
224
225     /**
226      * Method to unset/override existing variables
227      * @example
228      *    // Make cookie unreadable and unwritable
229      *    this._unset(document, "cookie", "", true);
230      */
231     _unset: function(object, property, value, setter) {
232       try { object[property] = value; } catch(e) {}
233
234       try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
235       if (setter) {
236         try { object.__defineSetter__(property, function() {}); } catch(e) {}
237       }
238
239       if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
240         try {
241           var config = {
242             get: function() { return value; }
243           };
244           if (setter) {
245             config.set = function() {};
246           }
247           Object.defineProperty(object, property, config);
248         } catch(e) {}
249       }
250     }
251   });
252 })(wysihtml5);