Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement markdown cell attachments. Allow drag’n’drop of images into… #621

Merged
merged 24 commits into from
Mar 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3462b34
Implement markdown cell attachments. Allow drag’n’drop of images into…
julienr Oct 19, 2015
398c90b
Use the cell ‘attachments’ property instead of ‘metadata.attachments’.
julienr Oct 20, 2015
3414b83
Insert markdown markup for attachments images instead of HTML
julienr Oct 20, 2015
1c6f589
Add attachments cell toolbar option which opens a metadata-like JSON …
julienr Oct 20, 2015
13bcdc9
Keep the attachments even if the cell type changes
julienr Oct 20, 2015
480c90e
Refactor the inline image drop logic. Preliminary copy/paste support.
julienr Oct 20, 2015
22fccff
Add a new insert-image action and corresponding dialog to insert inli…
julienr Oct 22, 2015
c4bd66e
Nicer attachments editing dialog
julienr Oct 26, 2015
b21348f
Attachments dialog only apply deletion on ‘Apply’.
julienr Oct 27, 2015
e66ff8f
Fix drag/drop event handler to be compatible with codemirror 5.8, which
julienr Oct 27, 2015
646619d
Check that we have items in markdown cell paste handler
Oct 30, 2015
865de47
Add Cut/Copy/Paste Cell attachments menu items
Oct 30, 2015
6cc468d
Add tests for attachments insert menuitem
Nov 9, 2015
aa460ff
Fix speed issues when attaching large images
Feb 9, 2016
c012835
Fix 'insert-image' menu item toggling. Instead of disabling the menui…
Feb 9, 2016
84f38c2
Make attachments textcell-specific
Feb 9, 2016
ac9a345
Bump nbformat minor to 4.1 to support attachments
Feb 9, 2016
c8266aa
Fix cell attachments copy/paste
Feb 9, 2016
a0920c4
Simplify attachments handling code by having an attachment attribute …
julienr Feb 12, 2016
c7186d5
Remove unused attachments on manual save
julienr Feb 19, 2016
ea57ff7
Explain why we disable caja uri checks on img::src
Feb 24, 2016
cc58b28
Remove jquery from dependencies since it’s a global
julienr Feb 25, 2016
51cab51
Add Copy/Paste cell attachments test
julienr Feb 25, 2016
f50474d
Add test for attachments garbage collection
julienr Feb 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions notebook/static/base/js/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ define(function(require) {
.addClass("btn btn-default btn-sm")
.attr("data-dismiss", "modal")
.text(label);
if (btn_opts.id) {
button.attr('id', btn_opts.id);
}
if (btn_opts.click) {
button.click($.proxy(btn_opts.click, dialog_content));
}
Expand Down Expand Up @@ -207,11 +210,185 @@ define(function(require) {

modal_obj.on('shown.bs.modal', function(){ editor.refresh(); });
};

var edit_attachments = function (options) {
// This shows the Edit Attachments dialog. This dialog allows the
// user to delete attachments. We show a list of attachments to
// the user and he can mark some of them for deletion. The deletion
// is applied when the 'Apply' button of this dialog is pressed.
var message;
var attachments_list;
if (Object.keys(options.attachments).length == 0) {
message = "There are no attachments for this cell.";
attachments_list = $('<div>');
} else {
message = "Current cell attachments";

attachments_list = $('<div>')
.addClass('list_container')
.append(
$('<div>')
.addClass('row list_header')
.append(
$('<div>')
.text('Attachments')
)
);

// This is a set containing keys of attachments to be deleted when
// the Apply button is clicked
var to_delete = {};

var refresh_attachments_list = function() {
$(attachments_list).find('.row').remove();
for (var key in options.attachments) {
var mime = Object.keys(options.attachments[key])[0];
var deleted = key in to_delete;

// This ensures the current value of key is captured since
// javascript only has function scope
var btn;
// Trash/restore button
(function(){
var _key = key;
btn = $('<button>')
.addClass('btn btn-default btn-xs')
.css('display', 'inline-block');
if (deleted) {
btn.attr('title', 'Restore')
.append(
$('<i>')
.addClass('fa fa-plus')
);
btn.click(function() {
delete to_delete[_key];
refresh_attachments_list();
});
} else {
btn.attr('title', 'Delete')
.addClass('btn-danger')
.append(
$('<i>')
.addClass('fa fa-trash')
);
btn.click(function() {
to_delete[_key] = true;
refresh_attachments_list();
});
}
return btn;
})();
var row = $('<div>')
.addClass('col-md-12 att_row')
.append(
$('<div>')
.addClass('row')
.append(
$('<div>')
.addClass('att-name col-xs-4')
.text(key)
)
.append(
$('<div>')
.addClass('col-xs-4 text-muted')
.text(mime)
)
.append(
$('<div>')
.addClass('item-buttons pull-right')
.append(btn)
)
);
if (deleted) {
row.find('.att-name')
.css('text-decoration', 'line-through');
}

attachments_list.append($('<div>')
.addClass('list_item row')
.append(row)
);
}
};
refresh_attachments_list();
}

var dialogform = $('<div/>')
.attr('title', 'Edit attachments')
.append(message)
.append('<br />')
.append(attachments_list)
var modal_obj = modal({
title: "Edit " + options.name + " Attachments",
body: dialogform,
buttons: {
Apply: { class : "btn-primary",
click: function() {
for (var key in to_delete) {
delete options.attachments[key];
}
options.callback(options.attachments);
}
},
Cancel: {}
},
notebook: options.notebook,
keyboard_manager: options.keyboard_manager,
});
};

var insert_image = function (options) {
var message =
"Select a file to insert.";
var file_input = $('<input/>')
.attr('type', 'file')
.attr('accept', 'image/*')
.attr('name', 'file')
.on('change', function(file) {
var $btn = $(modal_obj).find('#btn_ok');
if (this.files.length > 0) {
$btn.removeClass('disabled');
} else {
$btn.addClass('disabled');
}
});
var dialogform = $('<div/>').attr('title', 'Edit attachments')
.append(
$('<form id="insert-image-form" />').append(
$('<fieldset/>').append(
$('<label/>')
.attr('for','file')
.text(message)
)
.append($('<br/>'))
.append(file_input)
)
);
var modal_obj = modal({
title: "Pick a file",
body: dialogform,
buttons: {
OK: {
id : 'btn_ok',
class : "btn-primary disabled",
click: function() {
options.callback(file_input[0].files[0]);
}
},
Cancel: {}
},
notebook: options.notebook,
keyboard_manager: options.keyboard_manager,
});
};


var dialog = {
modal : modal,
kernel_modal : kernel_modal,
edit_metadata : edit_metadata,
edit_attachments : edit_attachments,
insert_image : insert_image
};

return dialog;
Expand Down
6 changes: 6 additions & 0 deletions notebook/static/base/js/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ define([
}
}
}
// Caja doesn't allow data uri for img::src, see
// https://github.com/google/caja/issues/1558
// This is not a security issue for browser post ie6 though, so we
// disable the check
// https://www.owasp.org/index.php/Script_in_IMG_tags
ATTRIBS['img::src'] = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this removes a security check, we need to make sure that there are no possible img.src attributes that can execute javascript on any supported browser before we can allow this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The good solution would be to only allow data uris (and disallow javascript: uri). Unfortunately, it seems that caja has hardcoded allowed uri schemes (and data is not included):

https://github.com/minrk/google-caja-bower/blob/master/html-sanitizer.js#L813

Should I just monkey patch caja.html.ALLOWED_URI_SCHEMES to allow data uris ?

There is an open issue on caja on this subject, but it doesn't seem very alive :
googlearchive/caja#1558

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a bit of reading, I believe this is not an issue on any supported browser. Can you make a note here that img:src is safe on browsers post ie6?

return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
};

Expand Down
22 changes: 22 additions & 0 deletions notebook/static/base/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,27 @@ define([
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
});
};

var parse_b64_data_uri = function(uri) {
/**
* Parses a base64 encoded data-uri to extract mimetype and the
* base64 string.
*
* For example, given 'data:image/png;base64,iVBORw', it will return
* ["image/png", "iVBORw"]
*
* Parameters
*/
// For performance reasons, the non-greedy ? qualifiers are crucial so
// that the matcher stops early on big blobs. Without them, it will try
// to match the whole blob which can take ages
var regex = /^data:(.+?\/.+?);base64,/;
var matches = uri.match(regex);
var mime = matches[1];
// matches[0] contains the whole data-uri prefix
var b64_data = uri.slice(matches[0].length);
return [mime, b64_data];
};

var time = {};
time.milliseconds = {};
Expand Down Expand Up @@ -877,6 +898,7 @@ define([
resolve_promises_dict: resolve_promises_dict,
reject: reject,
typeset: typeset,
parse_b64_data_uri: parse_b64_data_uri,
time: time,
format_datetime: format_datetime,
datetime_sort_helper: datetime_sort_helper,
Expand Down
28 changes: 28 additions & 0 deletions notebook/static/notebook/js/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,34 @@ define(function(require){
env.notebook.command_mode();
}
},
'insert-image': {
help : 'insert image',
help_index : 'dz',
handler : function (env) {
env.notebook.insert_image();
}
},
'cut-cell-attachments': {
help : 'cut cell attachments',
help_index : 'dza',
handler: function (env) {
env.notebook.cut_cell_attachments();
}
},
'copy-cell-attachments': {
help : 'copy cell attachments',
help_index: 'dzb',
handler: function (env) {
env.notebook.copy_cell_attachments();
}
},
'paste-cell-attachments': {
help : 'paste cell attachments',
help_index: 'dzc',
handler: function (env) {
env.notebook.paste_cell_attachments();
}
},
'split-cell-at-cursor': {
help : 'split cell',
help_index : 'ea',
Expand Down
27 changes: 26 additions & 1 deletion notebook/static/notebook/js/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ define([
}
});


// backward compat.
Object.defineProperty(this, 'cm_config', {
get: function() {
Expand Down Expand Up @@ -99,6 +100,12 @@ define([
this.cell_type = this.cell_type || null;
this.code_mirror = null;

// The nbformat only specifies attachments for textcell, but to avoid
// data loss when switching between cell types in the UI, all cells
// have an attachments property here. It is only saved to disk
// for textcell though (in toJSON)
this.attachments = {};

this.create_element();
if (this.element !== null) {
this.element.data("cell", this);
Expand Down Expand Up @@ -272,6 +279,9 @@ define([
this.element.addClass('selected');
this.element.removeClass('unselected');
this.selected = true;
// disable 'insert image' menu item (specific cell types will enable
// it in their override select())
this.notebook.set_insert_image_enabled(false);
return true;
} else {
return false;
Expand Down Expand Up @@ -340,6 +350,16 @@ define([
}
};

/**
* Garbage collects unused attachments in this cell
* @method remove_unused_attachments
*/
Cell.prototype.remove_unused_attachments = function () {
// Cell subclasses which support attachments should override this
// and keep them when needed
this.attachments = {};
};

/**
* Delegates keyboard shortcut handling to either Jupyter keyboard
* manager when in command mode, or CodeMirror when in edit mode
Expand Down Expand Up @@ -735,7 +755,12 @@ define([
cell.append(inner_cell);
this.element = cell;
};


UnrecognizedCell.prototype.remove_unused_attachments = function () {
// Do nothing to avoid removing attachments from a possible future
// attachment-supporting cell type
};

UnrecognizedCell.prototype.bind_events = function () {
Cell.prototype.bind_events.apply(this, arguments);
var cell = this;
Expand Down
50 changes: 50 additions & 0 deletions notebook/static/notebook/js/celltoolbarpresets/attachments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

define([
'notebook/js/celltoolbar',
'base/js/dialog',
], function(celltoolbar, dialog) {
"use strict";

var CellToolbar = celltoolbar.CellToolbar;

var edit_attachments_dialog = function(cell) {
dialog.edit_attachments({
attachments: cell.attachments,
callback: function(attachments) {
cell.attachments = attachments;
// Force cell refresh
cell.unrender();
cell.render();
},
name: 'cell',
notebook: cell.notebook,
keyboard_manager: cell.keyboard_manager
});
};

var add_dialog_button = function(div, cell) {
var button_container = $(div);
var button = $('<button />')
.addClass('btn btn-default btn-xs')
.text('Edit Attachments')
.click( function() {
edit_attachments_dialog(cell);
return false;
});
button_container.append(button);
};

var register = function(notebook) {
CellToolbar.register_callback('attachments.edit', add_dialog_button);

var attachments_preset = [];
attachments_preset.push('attachments.edit');

CellToolbar.register_preset('Attachments', attachments_preset, notebook);
console.log('Attachments editing toolbar loaded.');

};
return {'register' : register}
});
Loading