##// END OF EJS Templates
Merge Security Pull Request: google-caja...
MinRK -
r15674:f33c5e99 merge
parent child Browse files
Show More
@@ -0,0 +1,126 b''
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2014 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // Utilities
10 //============================================================================
11 IPython.namespace('IPython.security');
12
13 IPython.security = (function (IPython) {
14 "use strict";
15
16 var utils = IPython.utils;
17
18 var noop = function (x) { return x; };
19
20 var caja;
21 if (window && window.html) {
22 caja = window.html;
23 caja.html4 = window.html4;
24 caja.sanitizeStylesheet = window.sanitizeStylesheet;
25 }
26
27 var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
28 // add trusting data-attributes to the default sanitizeAttribs from caja
29 // this function is mostly copied from the caja source
30 var ATTRIBS = caja.html4.ATTRIBS;
31 for (var i = 0; i < attribs.length; i += 2) {
32 var attribName = attribs[i];
33 if (attribName.substr(0,5) == 'data-') {
34 var attribKey = '*::' + attribName;
35 if (!ATTRIBS.hasOwnProperty(attribKey)) {
36 ATTRIBS[attribKey] = 0;
37 }
38 }
39 }
40 return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
41 };
42
43 var sanitize_css = function (css, tagPolicy) {
44 // sanitize CSS
45 // like sanitize_html, but for CSS
46 // called by sanitize_stylesheets
47 return caja.sanitizeStylesheet(
48 window.location.pathname,
49 css,
50 {
51 containerClass: null,
52 idSuffix: '',
53 tagPolicy: tagPolicy,
54 virtualizeAttrName: noop
55 },
56 noop
57 );
58 };
59
60 var sanitize_stylesheets = function (html, tagPolicy) {
61 // sanitize just the css in style tags in a block of html
62 // called by sanitize_html, if allow_css is true
63 var h = $("<div/>").append(html);
64 var style_tags = h.find("style");
65 if (!style_tags.length) {
66 // no style tags to sanitize
67 return html;
68 }
69 style_tags.each(function(i, style) {
70 style.innerHTML = sanitize_css(style.innerHTML, tagPolicy);
71 });
72 return h.html();
73 };
74
75 var sanitize_html = function (html, allow_css) {
76 // sanitize HTML
77 // if allow_css is true (default: false), CSS is sanitized as well.
78 // otherwise, CSS elements and attributes are simply removed.
79 var html4 = caja.html4;
80
81 if (allow_css) {
82 // allow sanitization of style tags,
83 // not just scrubbing
84 html4.ELEMENTS.style &= ~html4.eflags.UNSAFE;
85 html4.ATTRIBS.style = html4.atype.STYLE;
86 } else {
87 // scrub all CSS
88 html4.ELEMENTS.style |= html4.eflags.UNSAFE;
89 html4.ATTRIBS.style = html4.atype.SCRIPT;
90 }
91
92 var record_messages = function (msg, opts) {
93 console.log("HTML Sanitizer", msg, opts);
94 };
95
96 var policy = function (tagName, attribs) {
97 if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) {
98 return {
99 'attribs': sanitizeAttribs(tagName, attribs,
100 noop, noop, record_messages)
101 };
102 } else {
103 record_messages(tagName + " removed", {
104 change: "removed",
105 tagName: tagName
106 });
107 }
108 };
109
110 var sanitized = caja.sanitizeWithPolicy(html, policy);
111
112 if (allow_css) {
113 // sanitize style tags as stylesheets
114 sanitized = sanitize_stylesheets(result.sanitized, policy);
115 }
116
117 return sanitized;
118 };
119
120 return {
121 caja: caja,
122 sanitize_html: sanitize_html
123 };
124
125 }(IPython));
126
@@ -0,0 +1,57 b''
1 safe_tests = [
2 "<p>Hi there</p>",
3 '<h1 class="foo">Hi There!</h1>',
4 '<a data-cite="foo">citation</a>',
5 '<div><span>Hi There</span></div>',
6 ];
7
8 unsafe_tests = [
9 "<script>alert(999);</script>",
10 '<a onmouseover="alert(999)">999</a>',
11 '<a onmouseover=alert(999)>999</a>',
12 '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
13 '<IMG SRC=# onmouseover="alert(999)">',
14 '<<SCRIPT>alert(999);//<</SCRIPT>',
15 '<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >',
16 '<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">',
17 '<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(999);">',
18 '<IFRAME SRC="javascript:alert(999);"></IFRAME>',
19 '<IFRAME SRC=# onmouseover="alert(document.cookie)"></IFRAME>',
20 '<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>',
21 // CSS is scrubbed
22 '<style src="http://untrusted/style.css"></style>',
23 '<style>div#notebook { background-color: alert-red; }</style>',
24 '<div style="background-color: alert-red;"></div>',
25 ];
26
27 var truncate = function (s, n) {
28 // truncate a string with an ellipsis
29 if (s.length > n) {
30 return s.substr(0, n-3) + '...';
31 } else {
32 return s;
33 }
34 };
35
36 casper.notebook_test(function () {
37 this.each(safe_tests, function (self, item) {
38 var sanitized = self.evaluate(function (item) {
39 return IPython.security.sanitize_html(item);
40 }, item);
41
42 // string equality may be too strict, but it works for now
43 this.test.assertEquals(sanitized, item, "Safe: '" + truncate(item, 32) + "'");
44 });
45
46 this.each(unsafe_tests, function (self, item) {
47 var sanitized = self.evaluate(function (item) {
48 return IPython.security.sanitize_html(item);
49 }, item);
50
51 this.test.assertNotEquals(sanitized, item,
52 "Sanitized: '" + truncate(item, 32) +
53 "' => '" + truncate(sanitized, 32) + "'"
54 );
55 this.test.assertEquals(sanitized.indexOf("alert"), -1, "alert removed");
56 });
57 }); No newline at end of file
@@ -278,7 +278,7 b' class FileNotebookManager(NotebookManager):'
278 nb = current.read(f, u'json')
278 nb = current.read(f, u'json')
279 except Exception as e:
279 except Exception as e:
280 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
280 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
281 self.mark_trusted_cells(nb, path, name)
281 self.mark_trusted_cells(nb, name, path)
282 model['content'] = nb
282 model['content'] = nb
283 return model
283 return model
284
284
@@ -303,7 +303,7 b' class FileNotebookManager(NotebookManager):'
303 os_path = self._get_os_path(new_name, new_path)
303 os_path = self._get_os_path(new_name, new_path)
304 nb = current.to_notebook_json(model['content'])
304 nb = current.to_notebook_json(model['content'])
305
305
306 self.check_and_sign(nb, new_path, new_name)
306 self.check_and_sign(nb, new_name, new_path)
307
307
308 if 'name' in nb['metadata']:
308 if 'name' in nb['metadata']:
309 nb['metadata']['name'] = u''
309 nb['metadata']['name'] = u''
@@ -286,5 +286,3 b' default_handlers = ['
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
287 ]
287 ]
288
288
289
290
@@ -52,7 +52,7 b' class NotebookManager(LoggingConfigurable):'
52 Parameters
52 Parameters
53 ----------
53 ----------
54 path : string
54 path : string
55 The
55 The path to check
56
56
57 Returns
57 Returns
58 -------
58 -------
@@ -224,22 +224,54 b' class NotebookManager(LoggingConfigurable):'
224 def log_info(self):
224 def log_info(self):
225 self.log.info(self.info_string())
225 self.log.info(self.info_string())
226
226
227 # NotebookManager methods provided for use in subclasses.
227 def trust_notebook(self, name, path=''):
228
228 """Explicitly trust a notebook
229 def check_and_sign(self, nb, path, name):
229
230 Parameters
231 ----------
232 name : string
233 The filename of the notebook
234 path : string
235 The notebook's directory
236 """
237 model = self.get_notebook(name, path)
238 nb = model['content']
239 self.log.warn("Trusting notebook %s/%s", path, name)
240 self.notary.mark_cells(nb, True)
241 self.save_notebook(model, name, path)
242
243 def check_and_sign(self, nb, name, path=''):
230 """Check for trusted cells, and sign the notebook.
244 """Check for trusted cells, and sign the notebook.
231
245
232 Called as a part of saving notebooks.
246 Called as a part of saving notebooks.
247
248 Parameters
249 ----------
250 nb : dict
251 The notebook structure
252 name : string
253 The filename of the notebook
254 path : string
255 The notebook's directory
233 """
256 """
234 if self.notary.check_cells(nb):
257 if self.notary.check_cells(nb):
235 self.notary.sign(nb)
258 self.notary.sign(nb)
236 else:
259 else:
237 self.log.warn("Saving untrusted notebook %s/%s", path, name)
260 self.log.warn("Saving untrusted notebook %s/%s", path, name)
238
261
239 def mark_trusted_cells(self, nb, path, name):
262 def mark_trusted_cells(self, nb, name, path=''):
240 """Mark cells as trusted if the notebook signature matches.
263 """Mark cells as trusted if the notebook signature matches.
241
264
242 Called as a part of loading notebooks.
265 Called as a part of loading notebooks.
266
267 Parameters
268 ----------
269 nb : dict
270 The notebook structure
271 name : string
272 The filename of the notebook
273 path : string
274 The notebook's directory
243 """
275 """
244 trusted = self.notary.check_signature(nb)
276 trusted = self.notary.check_signature(nb)
245 if not trusted:
277 if not trusted:
@@ -2,12 +2,15 b''
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import logging
5 import os
6 import os
6
7
7 from tornado.web import HTTPError
8 from tornado.web import HTTPError
8 from unittest import TestCase
9 from unittest import TestCase
9 from tempfile import NamedTemporaryFile
10 from tempfile import NamedTemporaryFile
10
11
12 from IPython.nbformat import current
13
11 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
12 from IPython.utils.traitlets import TraitError
15 from IPython.utils.traitlets import TraitError
13 from IPython.html.utils import url_path_join
16 from IPython.html.utils import url_path_join
@@ -55,6 +58,17 b' class TestFileNotebookManager(TestCase):'
55
58
56 class TestNotebookManager(TestCase):
59 class TestNotebookManager(TestCase):
57
60
61 def setUp(self):
62 self._temp_dir = TemporaryDirectory()
63 self.td = self._temp_dir.name
64 self.notebook_manager = FileNotebookManager(
65 notebook_dir=self.td,
66 log=logging.getLogger()
67 )
68
69 def tearDown(self):
70 self._temp_dir.cleanup()
71
58 def make_dir(self, abs_path, rel_path):
72 def make_dir(self, abs_path, rel_path):
59 """make subdirectory, rel_path is the relative path
73 """make subdirectory, rel_path is the relative path
60 to that directory from the location where the server started"""
74 to that directory from the location where the server started"""
@@ -63,183 +77,230 b' class TestNotebookManager(TestCase):'
63 os.makedirs(os_path)
77 os.makedirs(os_path)
64 except OSError:
78 except OSError:
65 print("Directory already exists: %r" % os_path)
79 print("Directory already exists: %r" % os_path)
66
80
81 def add_code_cell(self, nb):
82 output = current.new_output("display_data", output_javascript="alert('hi');")
83 cell = current.new_code_cell("print('hi')", outputs=[output])
84 if not nb.worksheets:
85 nb.worksheets.append(current.new_worksheet())
86 nb.worksheets[0].cells.append(cell)
87
88 def new_notebook(self):
89 nbm = self.notebook_manager
90 model = nbm.create_notebook()
91 name = model['name']
92 path = model['path']
93
94 full_model = nbm.get_notebook(name, path)
95 nb = full_model['content']
96 self.add_code_cell(nb)
97
98 nbm.save_notebook(full_model, name, path)
99 return nb, name, path
100
67 def test_create_notebook(self):
101 def test_create_notebook(self):
68 with TemporaryDirectory() as td:
102 nm = self.notebook_manager
69 # Test in root directory
103 # Test in root directory
70 nm = FileNotebookManager(notebook_dir=td)
104 model = nm.create_notebook()
71 model = nm.create_notebook()
105 assert isinstance(model, dict)
72 assert isinstance(model, dict)
106 self.assertIn('name', model)
73 self.assertIn('name', model)
107 self.assertIn('path', model)
74 self.assertIn('path', model)
108 self.assertEqual(model['name'], 'Untitled0.ipynb')
75 self.assertEqual(model['name'], 'Untitled0.ipynb')
109 self.assertEqual(model['path'], '')
76 self.assertEqual(model['path'], '')
110
77
111 # Test in sub-directory
78 # Test in sub-directory
112 sub_dir = '/foo/'
79 sub_dir = '/foo/'
113 self.make_dir(nm.notebook_dir, 'foo')
80 self.make_dir(nm.notebook_dir, 'foo')
114 model = nm.create_notebook(None, sub_dir)
81 model = nm.create_notebook(None, sub_dir)
115 assert isinstance(model, dict)
82 assert isinstance(model, dict)
116 self.assertIn('name', model)
83 self.assertIn('name', model)
117 self.assertIn('path', model)
84 self.assertIn('path', model)
118 self.assertEqual(model['name'], 'Untitled0.ipynb')
85 self.assertEqual(model['name'], 'Untitled0.ipynb')
119 self.assertEqual(model['path'], sub_dir.strip('/'))
86 self.assertEqual(model['path'], sub_dir.strip('/'))
87
120
88 def test_get_notebook(self):
121 def test_get_notebook(self):
89 with TemporaryDirectory() as td:
122 nm = self.notebook_manager
90 # Test in root directory
123 # Create a notebook
91 # Create a notebook
124 model = nm.create_notebook()
92 nm = FileNotebookManager(notebook_dir=td)
125 name = model['name']
93 model = nm.create_notebook()
126 path = model['path']
94 name = model['name']
127
95 path = model['path']
128 # Check that we 'get' on the notebook we just created
96
129 model2 = nm.get_notebook(name, path)
97 # Check that we 'get' on the notebook we just created
130 assert isinstance(model2, dict)
98 model2 = nm.get_notebook(name, path)
131 self.assertIn('name', model2)
99 assert isinstance(model2, dict)
132 self.assertIn('path', model2)
100 self.assertIn('name', model2)
133 self.assertEqual(model['name'], name)
101 self.assertIn('path', model2)
134 self.assertEqual(model['path'], path)
102 self.assertEqual(model['name'], name)
135
103 self.assertEqual(model['path'], path)
136 # Test in sub-directory
104
137 sub_dir = '/foo/'
105 # Test in sub-directory
138 self.make_dir(nm.notebook_dir, 'foo')
106 sub_dir = '/foo/'
139 model = nm.create_notebook(None, sub_dir)
107 self.make_dir(nm.notebook_dir, 'foo')
140 model2 = nm.get_notebook(name, sub_dir)
108 model = nm.create_notebook(None, sub_dir)
141 assert isinstance(model2, dict)
109 model2 = nm.get_notebook(name, sub_dir)
142 self.assertIn('name', model2)
110 assert isinstance(model2, dict)
143 self.assertIn('path', model2)
111 self.assertIn('name', model2)
144 self.assertIn('content', model2)
112 self.assertIn('path', model2)
145 self.assertEqual(model2['name'], 'Untitled0.ipynb')
113 self.assertIn('content', model2)
146 self.assertEqual(model2['path'], sub_dir.strip('/'))
114 self.assertEqual(model2['name'], 'Untitled0.ipynb')
115 self.assertEqual(model2['path'], sub_dir.strip('/'))
116
147
117 def test_update_notebook(self):
148 def test_update_notebook(self):
118 with TemporaryDirectory() as td:
149 nm = self.notebook_manager
119 # Test in root directory
150 # Create a notebook
120 # Create a notebook
151 model = nm.create_notebook()
121 nm = FileNotebookManager(notebook_dir=td)
152 name = model['name']
122 model = nm.create_notebook()
153 path = model['path']
123 name = model['name']
154
124 path = model['path']
155 # Change the name in the model for rename
125
156 model['name'] = 'test.ipynb'
126 # Change the name in the model for rename
157 model = nm.update_notebook(model, name, path)
127 model['name'] = 'test.ipynb'
158 assert isinstance(model, dict)
128 model = nm.update_notebook(model, name, path)
159 self.assertIn('name', model)
129 assert isinstance(model, dict)
160 self.assertIn('path', model)
130 self.assertIn('name', model)
161 self.assertEqual(model['name'], 'test.ipynb')
131 self.assertIn('path', model)
162
132 self.assertEqual(model['name'], 'test.ipynb')
163 # Make sure the old name is gone
133
164 self.assertRaises(HTTPError, nm.get_notebook, name, path)
134 # Make sure the old name is gone
165
135 self.assertRaises(HTTPError, nm.get_notebook, name, path)
166 # Test in sub-directory
136
167 # Create a directory and notebook in that directory
137 # Test in sub-directory
168 sub_dir = '/foo/'
138 # Create a directory and notebook in that directory
169 self.make_dir(nm.notebook_dir, 'foo')
139 sub_dir = '/foo/'
170 model = nm.create_notebook(None, sub_dir)
140 self.make_dir(nm.notebook_dir, 'foo')
171 name = model['name']
141 model = nm.create_notebook(None, sub_dir)
172 path = model['path']
142 name = model['name']
173
143 path = model['path']
174 # Change the name in the model for rename
144
175 model['name'] = 'test_in_sub.ipynb'
145 # Change the name in the model for rename
176 model = nm.update_notebook(model, name, path)
146 model['name'] = 'test_in_sub.ipynb'
177 assert isinstance(model, dict)
147 model = nm.update_notebook(model, name, path)
178 self.assertIn('name', model)
148 assert isinstance(model, dict)
179 self.assertIn('path', model)
149 self.assertIn('name', model)
180 self.assertEqual(model['name'], 'test_in_sub.ipynb')
150 self.assertIn('path', model)
181 self.assertEqual(model['path'], sub_dir.strip('/'))
151 self.assertEqual(model['name'], 'test_in_sub.ipynb')
182
152 self.assertEqual(model['path'], sub_dir.strip('/'))
183 # Make sure the old name is gone
153
184 self.assertRaises(HTTPError, nm.get_notebook, name, path)
154 # Make sure the old name is gone
155 self.assertRaises(HTTPError, nm.get_notebook, name, path)
156
185
157 def test_save_notebook(self):
186 def test_save_notebook(self):
158 with TemporaryDirectory() as td:
187 nm = self.notebook_manager
159 # Test in the root directory
188 # Create a notebook
160 # Create a notebook
189 model = nm.create_notebook()
161 nm = FileNotebookManager(notebook_dir=td)
190 name = model['name']
162 model = nm.create_notebook()
191 path = model['path']
163 name = model['name']
192
164 path = model['path']
193 # Get the model with 'content'
165
194 full_model = nm.get_notebook(name, path)
166 # Get the model with 'content'
195
167 full_model = nm.get_notebook(name, path)
196 # Save the notebook
168
197 model = nm.save_notebook(full_model, name, path)
169 # Save the notebook
198 assert isinstance(model, dict)
170 model = nm.save_notebook(full_model, name, path)
199 self.assertIn('name', model)
171 assert isinstance(model, dict)
200 self.assertIn('path', model)
172 self.assertIn('name', model)
201 self.assertEqual(model['name'], name)
173 self.assertIn('path', model)
202 self.assertEqual(model['path'], path)
174 self.assertEqual(model['name'], name)
203
175 self.assertEqual(model['path'], path)
204 # Test in sub-directory
176
205 # Create a directory and notebook in that directory
177 # Test in sub-directory
206 sub_dir = '/foo/'
178 # Create a directory and notebook in that directory
207 self.make_dir(nm.notebook_dir, 'foo')
179 sub_dir = '/foo/'
208 model = nm.create_notebook(None, sub_dir)
180 self.make_dir(nm.notebook_dir, 'foo')
209 name = model['name']
181 model = nm.create_notebook(None, sub_dir)
210 path = model['path']
182 name = model['name']
211 model = nm.get_notebook(name, path)
183 path = model['path']
212
184 model = nm.get_notebook(name, path)
213 # Change the name in the model for rename
185
214 model = nm.save_notebook(model, name, path)
186 # Change the name in the model for rename
215 assert isinstance(model, dict)
187 model = nm.save_notebook(model, name, path)
216 self.assertIn('name', model)
188 assert isinstance(model, dict)
217 self.assertIn('path', model)
189 self.assertIn('name', model)
218 self.assertEqual(model['name'], 'Untitled0.ipynb')
190 self.assertIn('path', model)
219 self.assertEqual(model['path'], sub_dir.strip('/'))
191 self.assertEqual(model['name'], 'Untitled0.ipynb')
192 self.assertEqual(model['path'], sub_dir.strip('/'))
193
220
194 def test_save_notebook_with_script(self):
221 def test_save_notebook_with_script(self):
195 with TemporaryDirectory() as td:
222 nm = self.notebook_manager
196 # Create a notebook
223 # Create a notebook
197 nm = FileNotebookManager(notebook_dir=td)
224 model = nm.create_notebook()
198 nm.save_script = True
225 nm.save_script = True
199 model = nm.create_notebook()
226 model = nm.create_notebook()
200 name = model['name']
227 name = model['name']
201 path = model['path']
228 path = model['path']
202
229
203 # Get the model with 'content'
230 # Get the model with 'content'
204 full_model = nm.get_notebook(name, path)
231 full_model = nm.get_notebook(name, path)
205
232
206 # Save the notebook
233 # Save the notebook
207 model = nm.save_notebook(full_model, name, path)
234 model = nm.save_notebook(full_model, name, path)
208
235
209 # Check that the script was created
236 # Check that the script was created
210 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
237 py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py')
211 assert os.path.exists(py_path), py_path
238 assert os.path.exists(py_path), py_path
212
239
213 def test_delete_notebook(self):
240 def test_delete_notebook(self):
214 with TemporaryDirectory() as td:
241 nm = self.notebook_manager
215 # Test in the root directory
242 # Create a notebook
216 # Create a notebook
243 nb, name, path = self.new_notebook()
217 nm = FileNotebookManager(notebook_dir=td)
244
218 model = nm.create_notebook()
245 # Delete the notebook
219 name = model['name']
246 nm.delete_notebook(name, path)
220 path = model['path']
247
221
248 # Check that a 'get' on the deleted notebook raises and error
222 # Delete the notebook
249 self.assertRaises(HTTPError, nm.get_notebook, name, path)
223 nm.delete_notebook(name, path)
224
225 # Check that a 'get' on the deleted notebook raises and error
226 self.assertRaises(HTTPError, nm.get_notebook, name, path)
227
250
228 def test_copy_notebook(self):
251 def test_copy_notebook(self):
229 with TemporaryDirectory() as td:
252 nm = self.notebook_manager
230 # Test in the root directory
253 path = u'å b'
231 # Create a notebook
254 name = u'nb √.ipynb'
232 nm = FileNotebookManager(notebook_dir=td)
255 os.mkdir(os.path.join(nm.notebook_dir, path))
233 path = u'å b'
256 orig = nm.create_notebook({'name' : name}, path=path)
234 name = u'nb √.ipynb'
257
235 os.mkdir(os.path.join(td, path))
258 # copy with unspecified name
236 orig = nm.create_notebook({'name' : name}, path=path)
259 copy = nm.copy_notebook(name, path=path)
237
260 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
238 # copy with unspecified name
261
239 copy = nm.copy_notebook(name, path=path)
262 # copy with specified name
240 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
263 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
241
264 self.assertEqual(copy2['name'], u'copy 2.ipynb')
242 # copy with specified name
243 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
244 self.assertEqual(copy2['name'], u'copy 2.ipynb')
245
265
266 def test_trust_notebook(self):
267 nbm = self.notebook_manager
268 nb, name, path = self.new_notebook()
269
270 untrusted = nbm.get_notebook(name, path)['content']
271 assert not nbm.notary.check_cells(untrusted)
272
273 # print(untrusted)
274 nbm.trust_notebook(name, path)
275 trusted = nbm.get_notebook(name, path)['content']
276 # print(trusted)
277 assert nbm.notary.check_cells(trusted)
278
279 def test_mark_trusted_cells(self):
280 nbm = self.notebook_manager
281 nb, name, path = self.new_notebook()
282
283 nbm.mark_trusted_cells(nb, name, path)
284 for cell in nb.worksheets[0].cells:
285 if cell.cell_type == 'code':
286 assert not cell.trusted
287
288 nbm.trust_notebook(name, path)
289 nb = nbm.get_notebook(name, path)['content']
290 for cell in nb.worksheets[0].cells:
291 if cell.cell_type == 'code':
292 assert cell.trusted
293
294 def test_check_and_sign(self):
295 nbm = self.notebook_manager
296 nb, name, path = self.new_notebook()
297
298 nbm.mark_trusted_cells(nb, name, path)
299 nbm.check_and_sign(nb, name, path)
300 assert not nbm.notary.check_signature(nb)
301
302 nbm.trust_notebook(name, path)
303 nb = nbm.get_notebook(name, path)['content']
304 nbm.mark_trusted_cells(nb, name, path)
305 nbm.check_and_sign(nb, name, path)
306 assert nbm.notary.check_signature(nb)
@@ -488,7 +488,6 b' IPython.utils = (function (IPython) {'
488 }
488 }
489 }
489 }
490
490
491
492 return {
491 return {
493 regex_split : regex_split,
492 regex_split : regex_split,
494 uuid : uuid,
493 uuid : uuid,
@@ -1,1 +1,1 b''
1 Subproject commit 2f8958788c7e0416e5c44f532e9630a658df11fd
1 Subproject commit 7a9ba818b3e13123621cb5ff336c002d49470f55
@@ -133,6 +133,20 b' var IPython = (function (IPython) {'
133 });
133 });
134 this.element.find('#restore_checkpoint').click(function () {
134 this.element.find('#restore_checkpoint').click(function () {
135 });
135 });
136 this.element.find('#trust_notebook').click(function () {
137 IPython.notebook.trust_notebook();
138 });
139 $([IPython.events]).on('trust_changed.Notebook', function (event, trusted) {
140 if (trusted) {
141 that.element.find('#trust_notebook')
142 .addClass("disabled")
143 .find("a").text("Trusted Notebook");
144 } else {
145 that.element.find('#trust_notebook')
146 .removeClass("disabled")
147 .find("a").text("Trust Notebook");
148 }
149 });
136 this.element.find('#kill_and_exit').click(function () {
150 this.element.find('#kill_and_exit').click(function () {
137 IPython.notebook.session.delete();
151 IPython.notebook.session.delete();
138 setTimeout(function(){
152 setTimeout(function(){
@@ -110,6 +110,10 b' var IPython = (function (IPython) {'
110 that.dirty = data.value;
110 that.dirty = data.value;
111 });
111 });
112
112
113 $([IPython.events]).on('trust_changed.Notebook', function (event, data) {
114 that.trusted = data.value;
115 });
116
113 $([IPython.events]).on('select.Cell', function (event, data) {
117 $([IPython.events]).on('select.Cell', function (event, data) {
114 var index = that.find_cell_index(data.cell);
118 var index = that.find_cell_index(data.cell);
115 that.select(index);
119 that.select(index);
@@ -1607,6 +1611,7 b' var IPython = (function (IPython) {'
1607 // Save the metadata and name.
1611 // Save the metadata and name.
1608 this.metadata = content.metadata;
1612 this.metadata = content.metadata;
1609 this.notebook_name = data.name;
1613 this.notebook_name = data.name;
1614 var trusted = true;
1610 // Only handle 1 worksheet for now.
1615 // Only handle 1 worksheet for now.
1611 var worksheet = content.worksheets[0];
1616 var worksheet = content.worksheets[0];
1612 if (worksheet !== undefined) {
1617 if (worksheet !== undefined) {
@@ -1627,8 +1632,15 b' var IPython = (function (IPython) {'
1627
1632
1628 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1633 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1629 new_cell.fromJSON(cell_data);
1634 new_cell.fromJSON(cell_data);
1635 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1636 trusted = false;
1637 }
1630 }
1638 }
1631 }
1639 }
1640 if (trusted != this.trusted) {
1641 this.trusted = trusted;
1642 $([IPython.events]).trigger("trust_changed.Notebook", trusted);
1643 }
1632 if (content.worksheets.length > 1) {
1644 if (content.worksheets.length > 1) {
1633 IPython.dialog.modal({
1645 IPython.dialog.modal({
1634 title : "Multiple worksheets",
1646 title : "Multiple worksheets",
@@ -1654,8 +1666,13 b' var IPython = (function (IPython) {'
1654 var cells = this.get_cells();
1666 var cells = this.get_cells();
1655 var ncells = cells.length;
1667 var ncells = cells.length;
1656 var cell_array = new Array(ncells);
1668 var cell_array = new Array(ncells);
1669 var trusted = true;
1657 for (var i=0; i<ncells; i++) {
1670 for (var i=0; i<ncells; i++) {
1658 cell_array[i] = cells[i].toJSON();
1671 var cell = cells[i];
1672 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1673 trusted = false;
1674 }
1675 cell_array[i] = cell.toJSON();
1659 }
1676 }
1660 var data = {
1677 var data = {
1661 // Only handle 1 worksheet for now.
1678 // Only handle 1 worksheet for now.
@@ -1665,6 +1682,10 b' var IPython = (function (IPython) {'
1665 }],
1682 }],
1666 metadata : this.metadata
1683 metadata : this.metadata
1667 };
1684 };
1685 if (trusted != this.trusted) {
1686 this.trusted = trusted;
1687 $([IPython.events]).trigger("trust_changed.Notebook", trusted);
1688 }
1668 return data;
1689 return data;
1669 };
1690 };
1670
1691
@@ -1786,6 +1807,54 b' var IPython = (function (IPython) {'
1786 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1807 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1787 };
1808 };
1788
1809
1810 /**
1811 * Explicitly trust the output of this notebook.
1812 *
1813 * @method trust_notebook
1814 */
1815 Notebook.prototype.trust_notebook = function (extra_settings) {
1816 var body = $("<div>").append($("<p>")
1817 .text("A trusted IPython notebook may execute hidden malicious code ")
1818 .append($("<strong>")
1819 .append(
1820 $("<em>").text("when you open it")
1821 )
1822 ).append(".").append(
1823 " Selecting trust will immediately reload this notebook in a trusted state."
1824 ).append(
1825 " For more information, see the "
1826 ).append($("<a>").attr("href", "http://ipython.org/security.html")
1827 .text("IPython security documentation")
1828 ).append(".")
1829 );
1830
1831 var nb = this;
1832 IPython.dialog.modal({
1833 title: "Trust this notebook?",
1834 body: body,
1835
1836 buttons: {
1837 Cancel : {},
1838 Trust : {
1839 class : "btn-danger",
1840 click : function () {
1841 var cells = nb.get_cells();
1842 for (var i = 0; i < cells.length; i++) {
1843 var cell = cells[i];
1844 if (cell.cell_type == 'code') {
1845 cell.output_area.trusted = true;
1846 }
1847 }
1848 $([IPython.events]).on('notebook_saved.Notebook', function () {
1849 window.location.reload();
1850 });
1851 nb.save_notebook();
1852 }
1853 }
1854 }
1855 });
1856 };
1857
1789 Notebook.prototype.new_notebook = function(){
1858 Notebook.prototype.new_notebook = function(){
1790 var path = this.notebook_path;
1859 var path = this.notebook_path;
1791 var base_url = this.base_url;
1860 var base_url = this.base_url;
@@ -480,6 +480,7 b' var IPython = (function (IPython) {'
480
480
481 OutputArea.safe_outputs = {
481 OutputArea.safe_outputs = {
482 'text/plain' : true,
482 'text/plain' : true,
483 'text/latex' : true,
483 'image/png' : true,
484 'image/png' : true,
484 'image/jpeg' : true
485 'image/jpeg' : true
485 };
486 };
@@ -489,18 +490,20 b' var IPython = (function (IPython) {'
489 var type = OutputArea.display_order[type_i];
490 var type = OutputArea.display_order[type_i];
490 var append = OutputArea.append_map[type];
491 var append = OutputArea.append_map[type];
491 if ((json[type] !== undefined) && append) {
492 if ((json[type] !== undefined) && append) {
493 var value = json[type];
492 if (!this.trusted && !OutputArea.safe_outputs[type]) {
494 if (!this.trusted && !OutputArea.safe_outputs[type]) {
493 // not trusted show warning and do not display
495 // not trusted, sanitize HTML
494 var content = {
496 if (type==='text/html' || type==='text/svg') {
495 text : "Untrusted " + type + " output ignored.",
497 value = IPython.security.sanitize_html(value);
496 stream : "stderr"
498 } else {
499 // don't display if we don't know how to sanitize it
500 console.log("Ignoring untrusted " + type + " output.");
501 continue;
497 }
502 }
498 this.append_stream(content);
499 continue;
500 }
503 }
501 var md = json.metadata || {};
504 var md = json.metadata || {};
502 var toinsert = append.apply(this, [json[type], md, element]);
505 var toinsert = append.apply(this, [value, md, element]);
503 $([IPython.events]).trigger('output_appended.OutputArea', [type, json[type], md, toinsert]);
506 $([IPython.events]).trigger('output_appended.OutputArea', [type, value, md, toinsert]);
504 return true;
507 return true;
505 }
508 }
506 }
509 }
@@ -21,6 +21,7 b' var IPython = (function (IPython) {'
21
21
22 // TextCell base class
22 // TextCell base class
23 var keycodes = IPython.keyboard.keycodes;
23 var keycodes = IPython.keyboard.keycodes;
24 var security = IPython.security;
24
25
25 /**
26 /**
26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
@@ -344,23 +345,12 b' var IPython = (function (IPython) {'
344 text = text_and_math[0];
345 text = text_and_math[0];
345 math = text_and_math[1];
346 math = text_and_math[1];
346 var html = marked.parser(marked.lexer(text));
347 var html = marked.parser(marked.lexer(text));
347 html = $(IPython.mathjaxutils.replace_math(html, math));
348 html = IPython.mathjaxutils.replace_math(html, math);
348 // Links in markdown cells should open in new tabs.
349 html = security.sanitize_html(html);
350 html = $(html);
351 // links in markdown cells should open in new tabs
349 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
352 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
350 try {
353 this.set_rendered(html);
351 // TODO: This HTML needs to be treated as potentially dangerous
352 // user input and should be handled before set_rendered.
353 this.set_rendered(html);
354 } catch (e) {
355 console.log("Error running Javascript in Markdown:");
356 console.log(e);
357 this.set_rendered(
358 $("<div/>")
359 .append($("<div/>").text('Error rendering Markdown!').addClass("js-error"))
360 .append($("<div/>").text(e.toString()).addClass("js-error"))
361 .html()
362 );
363 }
364 this.element.find('div.input_area').hide();
354 this.element.find('div.input_area').hide();
365 this.element.find("div.text_cell_render").show();
355 this.element.find("div.text_cell_render").show();
366 this.typeset();
356 this.typeset();
@@ -528,7 +518,9 b' var IPython = (function (IPython) {'
528 text = text_and_math[0];
518 text = text_and_math[0];
529 math = text_and_math[1];
519 math = text_and_math[1];
530 var html = marked.parser(marked.lexer(text));
520 var html = marked.parser(marked.lexer(text));
531 var h = $(IPython.mathjaxutils.replace_math(html, math));
521 html = IPython.mathjaxutils.replace_math(html, math);
522 html = security.sanitize_html(html);
523 var h = $(html);
532 // add id and linkback anchor
524 // add id and linkback anchor
533 var hash = h.text().replace(/ /g, '-');
525 var hash = h.text().replace(/ /g, '-');
534 h.attr('id', hash);
526 h.attr('id', hash);
@@ -538,13 +530,10 b' var IPython = (function (IPython) {'
538 .attr('href', '#' + hash)
530 .attr('href', '#' + hash)
539 .text('¶')
531 .text('¶')
540 );
532 );
541 // TODO: This HTML needs to be treated as potentially dangerous
542 // user input and should be handled before set_rendered.
543 this.set_rendered(h);
533 this.set_rendered(h);
544 this.typeset();
534 this.element.find('div.text_cell_input').hide();
545 this.element.find('div.input_area').hide();
546 this.element.find("div.text_cell_render").show();
535 this.element.find("div.text_cell_render").show();
547
536 this.typeset();
548 }
537 }
549 return cont;
538 return cont;
550 };
539 };
@@ -7,4 +7,4 b''
7 @import "outputarea.less";
7 @import "outputarea.less";
8 @import "renderedhtml.less";
8 @import "renderedhtml.less";
9 @import "textcell.less";
9 @import "textcell.less";
10 @import "widgets.less";
10 @import "../../widgets/less/widgets.less";
@@ -86,7 +86,10 b' class="notebook_app"'
86 </ul>
86 </ul>
87 </li>
87 </li>
88 <li class="divider"></li>
88 <li class="divider"></li>
89
89 <li id="trust_notebook"
90 title="Trust the output of this notebook">
91 <a href="#" >Trust Notebook</a></li>
92 <li class="divider"></li>
90 <li id="kill_and_exit"
93 <li id="kill_and_exit"
91 title="Shutdown this notebook's kernel, and close this window">
94 title="Shutdown this notebook's kernel, and close this window">
92 <a href="#" >Close and halt</a></li>
95 <a href="#" >Close and halt</a></li>
@@ -291,6 +294,7 b' class="notebook_app"'
291
294
292 {{super()}}
295 {{super()}}
293
296
297 <script src="{{ static_url("components/google-caja/html-css-sanitizer-minified.js") }}" charset="utf-8"></script>
294 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
298 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
295 <script type="text/javascript">
299 <script type="text/javascript">
296 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js", include_version=False) }}";
300 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js", include_version=False) }}";
@@ -318,6 +322,7 b' class="notebook_app"'
318 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
322 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
319 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
323 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
320 <script src="{{ static_url("base/js/keyboard.js") }}" type="text/javascript" charset="utf-8"></script>
324 <script src="{{ static_url("base/js/keyboard.js") }}" type="text/javascript" charset="utf-8"></script>
325 <script src="{{ static_url("base/js/security.js") }}" type="text/javascript" charset="utf-8"></script>
321 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
326 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
322 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
327 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
323 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
328 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
@@ -150,6 +150,7 b' def find_package_data():'
150 pjoin(components, "backbone", "backbone-min.js"),
150 pjoin(components, "backbone", "backbone-min.js"),
151 pjoin(components, "bootstrap", "bootstrap", "js", "bootstrap.min.js"),
151 pjoin(components, "bootstrap", "bootstrap", "js", "bootstrap.min.js"),
152 pjoin(components, "font-awesome", "font", "*.*"),
152 pjoin(components, "font-awesome", "font", "*.*"),
153 pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
153 pjoin(components, "highlight.js", "build", "highlight.pack.js"),
154 pjoin(components, "highlight.js", "build", "highlight.pack.js"),
154 pjoin(components, "jquery", "jquery.min.js"),
155 pjoin(components, "jquery", "jquery.min.js"),
155 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
156 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
General Comments 0
You need to be logged in to leave comments. Login now