Skip to content

Commit

Permalink
Merge pull request ipython#4565 from minrk/format_check
Browse files Browse the repository at this point in the history
various display type validations

to protect notebooks from invalid display data, and add informative errors for invalid data.
  • Loading branch information
minrk committed Jan 13, 2014
2 parents 89b8f9d + 2e46cbd commit 111a263
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 14 deletions.
23 changes: 17 additions & 6 deletions IPython/core/display.py
Expand Up @@ -304,6 +304,11 @@ def __init__(self, data=None, url=None, filename=None):
self.filename = None if filename is None else unicode_type(filename)

self.reload()
self._check_data()

def _check_data(self):
"""Override in subclasses if there's something to check."""
pass

def reload(self):
"""Reload the raw data from file or URL."""
Expand Down Expand Up @@ -331,13 +336,19 @@ def reload(self):
except:
self.data = None

class Pretty(DisplayObject):
class TextDisplayObject(DisplayObject):
"""Validate that display data is text"""
def _check_data(self):
if self.data is not None and not isinstance(self.data, string_types):
raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data))

class Pretty(TextDisplayObject):

def _repr_pretty_(self):
return self.data


class HTML(DisplayObject):
class HTML(TextDisplayObject):

def _repr_html_(self):
return self.data
Expand All @@ -351,14 +362,14 @@ def __html__(self):
return self._repr_html_()


class Math(DisplayObject):
class Math(TextDisplayObject):

def _repr_latex_(self):
s = self.data.strip('$')
return "$$%s$$" % s


class Latex(DisplayObject):
class Latex(TextDisplayObject):

def _repr_latex_(self):
return self.data
Expand Down Expand Up @@ -398,7 +409,7 @@ def _repr_svg_(self):
return self.data


class JSON(DisplayObject):
class JSON(TextDisplayObject):

def _repr_json_(self):
return self.data
Expand All @@ -415,7 +426,7 @@ def _repr_json_(self):
lib_t2 = """});
"""

class Javascript(DisplayObject):
class Javascript(TextDisplayObject):

def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
"""Create a Javascript display object given raw data.
Expand Down
21 changes: 17 additions & 4 deletions IPython/core/formatters.py
Expand Up @@ -33,12 +33,13 @@
# Our own imports
from IPython.config.configurable import Configurable
from IPython.lib import pretty
from IPython.utils import io
from IPython.utils.traitlets import (
Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List,
)
from IPython.utils.warn import warn
from IPython.utils.py3compat import (
unicode_to_str, with_metaclass, PY3, string_types,
unicode_to_str, with_metaclass, PY3, string_types, unicode_type,
)

if PY3:
Expand Down Expand Up @@ -182,15 +183,22 @@ def format_types(self):
# Formatters for specific format types (text, html, svg, etc.)
#-----------------------------------------------------------------------------


@decorator
def warn_format_error(method, self, *args, **kwargs):
"""decorator for warning on failed format call"""
try:
return method(self, *args, **kwargs)
r = method(self, *args, **kwargs)
except Exception as e:
warn("Exception in %s formatter: %s" % (self.format_type, e))
return None
if r is None or isinstance(r, self._return_type) or \
(isinstance(r, tuple) and r and isinstance(r[0], self._return_type)):
return r
else:
warn("%s formatter returned invalid type %s (expected %s) for object: %s" % (
self.format_type, type(r), self._return_type, pretty._safe_repr(args[0])
))



class FormatterABC(with_metaclass(abc.ABCMeta, object)):
Expand Down Expand Up @@ -262,6 +270,7 @@ class BaseFormatter(Configurable):
"""

format_type = Unicode('text/plain')
_return_type = string_types

enabled = Bool(True, config=True)

Expand All @@ -278,7 +287,7 @@ class BaseFormatter(Configurable):
# The deferred-import type-specific printers.
# Map (modulename, classname) pairs to the format functions.
deferred_printers = Dict(config=True)

@warn_format_error
def __call__(self, obj):
"""Compute the format for an object."""
Expand Down Expand Up @@ -679,6 +688,8 @@ class PNGFormatter(BaseFormatter):
format_type = Unicode('image/png')

print_method = ObjectName('_repr_png_')

_return_type = (bytes, unicode_type)


class JPEGFormatter(BaseFormatter):
Expand All @@ -696,6 +707,8 @@ class JPEGFormatter(BaseFormatter):

print_method = ObjectName('_repr_jpeg_')

_return_type = (bytes, unicode_type)


class LatexFormatter(BaseFormatter):
"""A LaTeX formatter.
Expand Down
30 changes: 28 additions & 2 deletions IPython/html/static/notebook/js/outputarea.js
Expand Up @@ -276,7 +276,7 @@ var IPython = (function (IPython) {
"json" : "application/json",
"javascript" : "application/javascript",
};

OutputArea.prototype.rename_keys = function (data, key_map) {
var remapped = {};
for (var key in data) {
Expand All @@ -285,7 +285,30 @@ var IPython = (function (IPython) {
}
return remapped;
};



OutputArea.output_types = [
'application/javascript',
'text/html',
'text/latex',
'image/svg+xml',
'image/png',
'image/jpeg',
'text/plain'
];

OutputArea.prototype.validate_output = function (json) {
// scrub invalid outputs
// TODO: right now everything is a string, but JSON really shouldn't be.
// nbformat 4 will fix that.
$.map(OutputArea.output_types, function(key){
if (json[key] !== undefined && typeof json[key] !== 'string') {
console.log("Invalid type for " + key, json[key]);
delete json[key];
}
});
return json;
};

OutputArea.prototype.append_output = function (json) {
this.expand();
Expand All @@ -295,6 +318,9 @@ var IPython = (function (IPython) {
this.clear_output(false);
needs_height_reset = true;
}

// validate output data types
json = this.validate_output(json);

if (json.output_type === 'pyout') {
this.append_pyout(json);
Expand Down
4 changes: 2 additions & 2 deletions IPython/html/tests/casperjs/test_cases/nb_roundtrip.js
Expand Up @@ -15,8 +15,8 @@ mime = {
"javascript" : "application/javascript",
};

var black_dot_jpeg="\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\"";
var black_dot_png = '\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"';
var black_dot_jpeg="u\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\"";
var black_dot_png = 'u\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"';
var svg = "\"<svg width='1cm' height='1cm' viewBox='0 0 1000 500'><defs><style>rect {fill:red;}; </style></defs><rect id='r1' x='200' y='100' width='600' height='300' /></svg>\"";

// helper function to ensure that the short_name is found in the toJSON
Expand Down
32 changes: 32 additions & 0 deletions IPython/html/tests/casperjs/test_cases/safe_append_output.js
@@ -0,0 +1,32 @@
//
// Test validation in append_output
//
// Invalid output data is stripped and logged.
//

casper.notebook_test(function () {
// this.printLog();
var messages = [];
this.on('remote.message', function (msg) {
messages.push(msg);
});

this.evaluate(function () {
var cell = IPython.notebook.get_cell(0);
cell.set_text( "dp = get_ipython().display_pub\n" +
"dp.publish('test', {'text/plain' : '5', 'image/png' : 5})"
);
cell.execute();
});

this.wait_for_output(0);
this.on('remote.message', function () {});

this.then(function () {
var output = this.get_output_cell(0);
this.test.assert(messages.length > 0, "Captured log message");
this.test.assertEquals(messages[messages.length-1], "Invalid type for image/png 5", "Logged Invalid type message");
this.test.assertEquals(output['image/png'], undefined, "Non-string png data was stripped");
this.test.assertEquals(output['text/plain'], '5', "text data is fine");
});
});

0 comments on commit 111a263

Please sign in to comment.