diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index ad038722ebf..aa4dcbe0d0f 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -419,6 +419,19 @@ IPython.utils = (function (IPython) { } return url; }; + + + var encode_uri_components = function (uri) { + // encode just the components of a multi-segment uri, + // leaving '/' separators + return uri.split('/').map(encodeURIComponent).join('/'); + } + + var url_join_encode = function () { + // join a sequence of url components with '/', + // encoding each component with encodeURIComponent + return encode_uri_components(url_path_join.apply(null, arguments)); + }; var splitext = function (filename) { @@ -458,6 +471,8 @@ IPython.utils = (function (IPython) { autoLinkUrls : autoLinkUrls, points_to_pixels : points_to_pixels, url_path_join : url_path_join, + url_join_encode : url_join_encode, + encode_uri_components : encode_uri_components, splitext : splitext, always_new : always_new, browser : browser diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index 85419f5099c..7de234506fc 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -77,7 +77,7 @@ var IPython = (function (IPython) { IPython.notebook.new_notebook(); }); this.element.find('#open_notebook').click(function () { - window.open(utils.url_path_join( + window.open(utils.url_join_encode( that.baseProjectUrl(), 'tree', that.notebookPath() @@ -93,7 +93,7 @@ var IPython = (function (IPython) { IPython.notebook.save_notebook({async : false}); } - var url = utils.url_path_join( + var url = utils.url_join_encode( that.baseProjectUrl(), 'files', that.notebookPath(), diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 5f53e1bd5f2..e396b120723 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -79,15 +79,11 @@ var IPython = (function (IPython) { }; Notebook.prototype.notebookName = function() { - var name = $('body').data('notebookName'); - name = decodeURIComponent(name); - return name; + return $('body').data('notebookName'); }; Notebook.prototype.notebookPath = function() { - var path = $('body').data('notebookPath'); - path = decodeURIComponent(path); - return path + return $('body').data('notebookPath'); }; /** @@ -1689,10 +1685,10 @@ var IPython = (function (IPython) { } } $([IPython.events]).trigger('notebook_saving.Notebook'); - var url = utils.url_path_join( - this.baseProjectUrl(), + var url = utils.url_join_encode( + this._baseProjectUrl, 'api/notebooks', - this.notebookPath(), + this.notebook_path, this.notebook_name ); $.ajax(url, settings); @@ -1743,15 +1739,15 @@ var IPython = (function (IPython) { * @method save_notebook_error * @param {jqXHR} xhr jQuery Ajax object * @param {String} status Description of response status - * @param {String} error_msg HTTP error message + * @param {String} error HTTP error message */ - Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) { - $([IPython.events]).trigger('notebook_save_failed.Notebook'); + Notebook.prototype.save_notebook_error = function (xhr, status, error) { + $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]); }; Notebook.prototype.new_notebook = function(){ - var path = this.notebookPath(); - var base_project_url = this.baseProjectUrl(); + var path = this.notebook_path; + var base_project_url = this._baseProjectUrl; var settings = { processData : false, cache : false, @@ -1761,7 +1757,7 @@ var IPython = (function (IPython) { success : function (data, status, xhr){ var notebook_name = data.name; window.open( - utils.url_path_join( + utils.url_join_encode( base_project_url, 'notebooks', path, @@ -1771,7 +1767,7 @@ var IPython = (function (IPython) { ); } }; - var url = utils.url_path_join( + var url = utils.url_join_encode( base_project_url, 'api/notebooks', path @@ -1781,8 +1777,8 @@ var IPython = (function (IPython) { Notebook.prototype.copy_notebook = function(){ - var path = this.notebookPath(); - var base_project_url = this.baseProjectUrl(); + var path = this.notebook_path; + var base_project_url = this._baseProjectUrl; var settings = { processData : false, cache : false, @@ -1791,7 +1787,7 @@ var IPython = (function (IPython) { data : JSON.stringify({copy_from : this.notebook_name}), async : false, success : function (data, status, xhr) { - window.open(utils.url_path_join( + window.open(utils.url_join_encode( base_project_url, 'notebooks', data.path, @@ -1799,7 +1795,7 @@ var IPython = (function (IPython) { ), '_blank'); } }; - var url = utils.url_path_join( + var url = utils.url_join_encode( base_project_url, 'api/notebooks', path @@ -1821,10 +1817,10 @@ var IPython = (function (IPython) { error : $.proxy(that.rename_error, this) }; $([IPython.events]).trigger('rename_notebook.Notebook', data); - var url = utils.url_path_join( - this.baseProjectUrl(), + var url = utils.url_join_encode( + this._baseProjectUrl, 'api/notebooks', - this.notebookPath(), + this.notebook_path, this.notebook_name ); $.ajax(url, settings); @@ -1832,19 +1828,20 @@ var IPython = (function (IPython) { Notebook.prototype.rename_success = function (json, status, xhr) { - this.notebook_name = json.name - var name = this.notebook_name - var path = json.path + this.notebook_name = json.name; + var name = this.notebook_name; + var path = json.path; this.session.rename_notebook(name, path); $([IPython.events]).trigger('notebook_renamed.Notebook', json); } - Notebook.prototype.rename_error = function (json, status, xhr) { + Notebook.prototype.rename_error = function (xhr, status, error) { var that = this; var dialog = $('
').append( $("

").addClass("rename-message") .html('This notebook name already exists.') ) + $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]); IPython.dialog.modal({ title: "Notebook Rename Error!", body: dialog, @@ -1889,10 +1886,10 @@ var IPython = (function (IPython) { error : $.proxy(this.load_notebook_error,this), }; $([IPython.events]).trigger('notebook_loading.Notebook'); - var url = utils.url_path_join( + var url = utils.url_join_encode( this._baseProjectUrl, 'api/notebooks', - this.notebookPath(), + this.notebook_path, this.notebook_name ); $.ajax(url, settings); @@ -1974,12 +1971,13 @@ var IPython = (function (IPython) { * * @method load_notebook_error * @param {jqXHR} xhr jQuery Ajax object - * @param {String} textStatus Description of response status - * @param {String} errorThrow HTTP error message + * @param {String} status Description of response status + * @param {String} error HTTP error message */ - Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) { + Notebook.prototype.load_notebook_error = function (xhr, status, error) { + $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]); if (xhr.status === 400) { - var msg = errorThrow; + var msg = error; } else if (xhr.status === 500) { var msg = "An unknown error occurred while loading this notebook. " + "This version can load notebook formats " + @@ -2034,10 +2032,10 @@ var IPython = (function (IPython) { * @method list_checkpoints */ Notebook.prototype.list_checkpoints = function () { - var url = utils.url_path_join( - this.baseProjectUrl(), + var url = utils.url_join_encode( + this._baseProjectUrl, 'api/notebooks', - this.notebookPath(), + this.notebook_path, this.notebook_name, 'checkpoints' ); @@ -2085,8 +2083,8 @@ var IPython = (function (IPython) { * @method create_checkpoint */ Notebook.prototype.create_checkpoint = function () { - var url = utils.url_path_join( - this.baseProjectUrl(), + var url = utils.url_join_encode( + this._baseProjectUrl, 'api/notebooks', this.notebookPath(), this.notebook_name, @@ -2172,8 +2170,8 @@ var IPython = (function (IPython) { */ Notebook.prototype.restore_checkpoint = function (checkpoint) { $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint); - var url = utils.url_path_join( - this.baseProjectUrl(), + var url = utils.url_join_encode( + this._baseProjectUrl, 'api/notebooks', this.notebookPath(), this.notebook_name, @@ -2220,8 +2218,8 @@ var IPython = (function (IPython) { */ Notebook.prototype.delete_checkpoint = function (checkpoint) { $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint); - var url = utils.url_path_join( - this.baseProjectUrl(), + var url = utils.url_join_encode( + this._baseProjectUrl, 'api/notebooks', this.notebookPath(), this.notebook_name, diff --git a/IPython/html/static/notebook/js/savewidget.js b/IPython/html/static/notebook/js/savewidget.js index 56e62f3eae9..8a0183ca79e 100644 --- a/IPython/html/static/notebook/js/savewidget.js +++ b/IPython/html/static/notebook/js/savewidget.js @@ -128,8 +128,8 @@ var IPython = (function (IPython) { SaveWidget.prototype.update_address_bar = function(){ var nbname = IPython.notebook.notebook_name; var path = IPython.notebook.notebookPath(); - var state = {path : utils.url_path_join(path,nbname)}; - window.history.replaceState(state, "", utils.url_path_join( + var state = {path : utils.url_join_encode(path, nbname)}; + window.history.replaceState(state, "", utils.url_join_encode( "/notebooks", path, nbname) diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 9aa91aa2ce8..c1badfbdad0 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -113,7 +113,7 @@ var IPython = (function (IPython) { $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this}); if (this.running) { this.stop_channels(); - var url = utils.url_path_join(this.kernel_url, "restart"); + var url = utils.url_join_encode(this.kernel_url, "restart"); $.post(url, $.proxy(this._kernel_started, this), 'json' @@ -133,7 +133,7 @@ var IPython = (function (IPython) { ws_url = prot + location.host + ws_url; } this.ws_url = ws_url; - this.kernel_url = utils.url_path_join(this.base_url, this.kernel_id); + this.kernel_url = utils.url_join_encode(this.base_url, this.kernel_id); this.start_channels(); }; diff --git a/IPython/html/static/services/sessions/js/session.js b/IPython/html/static/services/sessions/js/session.js index a2bf694696b..0f0c4a99733 100644 --- a/IPython/html/static/services/sessions/js/session.js +++ b/IPython/html/static/services/sessions/js/session.js @@ -44,7 +44,7 @@ var IPython = (function (IPython) { } }, }; - var url = utils.url_path_join(this._baseProjectUrl, 'api/sessions'); + var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions'); $.ajax(url, settings); }; @@ -64,7 +64,7 @@ var IPython = (function (IPython) { data: JSON.stringify(model), dataType : "json", }; - var url = utils.url_path_join(this._baseProjectUrl, 'api/sessions', this.id); + var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions', this.id); $.ajax(url, settings); }; @@ -76,7 +76,7 @@ var IPython = (function (IPython) { dataType : "json", }; this.kernel.running = false; - var url = utils.url_path_join(this._baseProjectUrl, 'api/sessions', this.id); + var url = utils.url_join_encode(this._baseProjectUrl, 'api/sessions', this.id); $.ajax(url, settings); }; diff --git a/IPython/html/static/tree/js/clusterlist.js b/IPython/html/static/tree/js/clusterlist.js index 8be3b30af1f..4108c125422 100644 --- a/IPython/html/static/tree/js/clusterlist.js +++ b/IPython/html/static/tree/js/clusterlist.js @@ -51,7 +51,7 @@ var IPython = (function (IPython) { dataType : "json", success : $.proxy(this.load_list_success, this) }; - var url = utils.url_path_join(this.baseProjectUrl(), 'clusters'); + var url = utils.url_join_encode(this.baseProjectUrl(), 'clusters'); $.ajax(url, settings); }; @@ -137,7 +137,7 @@ var IPython = (function (IPython) { } }; status_col.html('starting'); - var url = utils.url_path_join( + var url = utils.url_join_encode( that.baseProjectUrl(), 'clusters', that.data.profile, @@ -179,7 +179,7 @@ var IPython = (function (IPython) { } }; status_col.html('stopping'); - var url = utils.url_path_join( + var url = utils.url_join_encode( that.baseProjectUrl(), 'clusters', that.data.profile, diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 5367c65c3a7..2ee7a190057 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -147,7 +147,7 @@ var IPython = (function (IPython) { },this) }; - var url = utils.url_path_join( + var url = utils.url_join_encode( this.baseProjectUrl(), 'api', 'notebooks', @@ -177,7 +177,7 @@ var IPython = (function (IPython) { var nbname = utils.splitext(name)[0]; var item = this.new_notebook_item(i); this.add_link(path, nbname, item); - name = utils.url_path_join(this.notebookPath(), name); + name = utils.url_path_join(path, name); if(this.sessions[name] === undefined){ this.add_delete_button(item); } else { @@ -214,10 +214,10 @@ var IPython = (function (IPython) { item.find(".item_name").text(nbname); item.find("a.item_link") .attr('href', - utils.url_path_join( + utils.url_join_encode( this.baseProjectUrl(), "notebooks", - this.notebookPath(), + path, nbname + ".ipynb" ) ).attr('target','_blank'); @@ -254,7 +254,7 @@ var IPython = (function (IPython) { that.load_sessions(); } }; - var url = utils.url_path_join( + var url = utils.url_join_encode( that.baseProjectUrl(), 'api/sessions', session @@ -294,11 +294,11 @@ var IPython = (function (IPython) { parent_item.remove(); } }; - var url = utils.url_path_join( + var url = utils.url_join_encode( notebooklist.baseProjectUrl(), 'api/notebooks', notebooklist.notebookPath(), - nbname + '.ipynb' + nbname + '.ipynb' ); $.ajax(url, settings); } @@ -339,7 +339,7 @@ var IPython = (function (IPython) { } }; - var url = utils.url_path_join( + var url = utils.url_join_encode( that.baseProjectUrl(), 'api/notebooks', that.notebookPath(), @@ -373,7 +373,7 @@ var IPython = (function (IPython) { success : function (data, status, xhr) { var notebook_name = data.name; window.open( - utils.url_path_join( + utils.url_join_encode( base_project_url, 'notebooks', path, @@ -382,7 +382,7 @@ var IPython = (function (IPython) { ); } }; - var url = utils.url_path_join( + var url = utils.url_join_encode( base_project_url, 'api/notebooks', path diff --git a/IPython/html/tests/casperjs/test_cases/save_notebook.js b/IPython/html/tests/casperjs/test_cases/save_notebook.js new file mode 100644 index 00000000000..affe05454e6 --- /dev/null +++ b/IPython/html/tests/casperjs/test_cases/save_notebook.js @@ -0,0 +1,105 @@ +// +// Test saving a notebook with escaped characters +// + +casper.notebook_test(function () { + // don't use unicode with ambiguous composed/decomposed normalization + // because the filesystem may use a different normalization than literals. + // This causes no actual problems, but will break string comparison. + var nbname = "has#hash and space and unicø∂e.ipynb"; + + this.evaluate(function (nbname) { + IPython.notebook.notebook_name = nbname; + IPython._save_success = IPython._save_failed = false; + $([IPython.events]).on('notebook_saved.Notebook', function () { + IPython._save_success = true; + }); + $([IPython.events]).on('notebook_save_failed.Notebook', + function (event, xhr, status, error) { + IPython._save_failed = "save failed with " + xhr.status + xhr.responseText; + }); + IPython.notebook.save_notebook(); + }, {nbname:nbname}); + + this.waitFor(function () { + return this.evaluate(function(){ + return IPython._save_failed || IPython._save_success; + }); + }); + + this.then(function(){ + var success_failure = this.evaluate(function(){ + return [IPython._save_success, IPython._save_failed]; + }); + this.test.assertEquals(success_failure[1], false, "Save did not fail"); + this.test.assertEquals(success_failure[0], true, "Save OK"); + + var current_name = this.evaluate(function(){ + return IPython.notebook.notebook_name; + }); + this.test.assertEquals(current_name, nbname, "Save with complicated name"); + }); + + this.thenEvaluate(function(){ + $([IPython.events]).on('checkpoint_created.Notebook', function (evt, data) { + IPython._checkpoint_created = true; + }); + IPython._checkpoint_created = false; + IPython.notebook.save_checkpoint(); + }); + + this.waitFor(function () { + return this.evaluate(function(){ + return IPython._checkpoint_created; + }); + }); + + this.then(function(){ + var checkpoints = this.evaluate(function(){ + return IPython.notebook.checkpoints; + }); + this.test.assertEquals(checkpoints.length, 1, "checkpoints OK"); + }); + + this.then(function(){ + var baseUrl = this.get_notebook_server(); + this.open(baseUrl); + }); + + this.waitForSelector('.list_item'); + + this.then(function(){ + var notebook_url = this.evaluate(function(nbname){ + var escaped_name = encodeURIComponent(nbname); + var return_this_thing; + $("a.item_link").map(function (i,a) { + if (a.href.indexOf(escaped_name) >= 0) { + return_this_thing = a.href; + return; + } + }); + return return_this_thing; + }, {nbname:nbname}); + this.test.assertEquals(notebook_url == null, false, "Escaped URL in notebook list"); + // open the notebook + this.open(notebook_url); + }); + + // wait for the notebook + this.waitForSelector("#notebook"); + + this.waitFor(function(){ + return this.evaluate(function(){ + return IPython.notebook || false; + }); + }); + + this.then(function(){ + // check that the notebook name is correct + var notebook_name = this.evaluate(function(){ + return IPython.notebook.notebook_name; + }); + this.test.assertEquals(notebook_name, nbname, "Notebook name is correct"); + }); + +});