diff options
Diffstat (limited to 'share/www')
100 files changed, 15493 insertions, 3702 deletions
diff --git a/share/www/_sidebar.html b/share/www/_sidebar.html index 6c7abc99..13727cbd 100644 --- a/share/www/_sidebar.html +++ b/share/www/_sidebar.html @@ -35,12 +35,12 @@ specific language governing permissions and limitations under the License. <a href="#" class="signup">Signup</a> or <a href="#" class="login">Login</a> </span> <span class="loggedin"> - Welcome <a class="username">?</a>! + Welcome <a class="name">?</a>! <br/> <a href="#" class="logout">Logout</a> </span> <span class="loggedinadmin"> - Welcome <a class="username">?</a>! + Welcome <a class="name">?</a>! <br/> <a href="#" class="createadmin">Setup more admins</a> or <a href="#" class="logout">Logout</a> diff --git a/share/www/config.html b/share/www/config.html index 8f788041..9863c8b5 100644 --- a/share/www/config.html +++ b/share/www/config.html @@ -20,7 +20,7 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> @@ -47,7 +47,7 @@ specific language governing permissions and limitations under the License. var prev = null; $.each(options, function(idx, optionName) { var cur = idx == 0 ? row : $("<tr></tr>"); - $("<td class='name'><b></b></td>") + $("<td class='name' section="+sectionName+"><b></b></td>") .find("b").text(optionName).end().appendTo(cur); $("<td class='value'><code></code></td>") .find("code").text(section[optionName]).end().appendTo(cur); @@ -58,7 +58,6 @@ specific language governing permissions and limitations under the License. row.find("th").attr("rowspan", options.length); }); $("#config tbody tr").removeClass("odd").filter(":odd").addClass("odd"); - $("#config tbody td.value code").makeEditable({ accept: function(newValue) { var row = $(this).parents("tr").eq(0); @@ -67,7 +66,40 @@ specific language governing permissions and limitations under the License. row.find("td.value code").text(newValue); }}, row.data("section"), row.data("option"), newValue); } - }); + }).parent().parent() + .append($('<td><div style="text-align:center;""><a class="remove" href="#remove">x</a></div></td>')) + .click(function (ev) { + // There is a listener that stops all events below this element which is + // why the remove link listener has to be here + var n = $(ev.target).parent().parent().parent(); + if ($(ev.target).attr('href') === "#remove" ) { + $.couch.config({ success: function () {location = location.href.split('#')[0];} } + , n.find('td.name').attr("section"), n.find('td.name').text(), null); + } + }) + var add = $('<a href="#add">Add a new section</a>').click(function () { + $.showDialog("dialog/_create_config.html", { + submit: function(data, callback) { + var fail = false; + if (!data.section || data.section.length == 0) { + callback({section: "Please enter a section."}); + fail = true; + } + if (!data.option || data.option.length == 0) { + callback({option: "Please enter a option."}); + fail = true; + } + if (!data.val || data.val.length == 0) { + callback({val: "Please enter a value."}); + fail = true; + } + if (fail) {return} + $.couch.config({ success: function () {callback();location = location.href.split('#')[0]} } + , data.section, data.option, data.val); + } + }); + }) + $("div#content").append(add) } }); @@ -84,13 +116,16 @@ specific language governing permissions and limitations under the License. <strong>Note:</strong> Some configuration options may require restarting the server to take effect after modification. </p> - + <p class="help"> + For the strongest consistency guarantees, <tt>delayed_commits</tt> should be set to <tt>false</tt>. The default value of <tt>true</tt> is designed for single-user performance. For more details see <a href="http://wiki.apache.org/couchdb/Durability_Matrix">a discussion of durability on the CouchDB wiki</a>. + </p> <table id="config" class="listing" cellspacing="0"> <caption>Configuration</caption> <thead><tr> <th>Section</th> <th>Option</th> <th>Value</th> + <th>Delete</th> </tr></thead> <tbody class="content"></tbody> </table> diff --git a/share/www/couch_tests.html b/share/www/couch_tests.html index fa347764..f10bad23 100644 --- a/share/www/couch_tests.html +++ b/share/www/couch_tests.html @@ -20,7 +20,7 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> @@ -35,6 +35,14 @@ specific language governing permissions and limitations under the License. $("#toolbar button.load").click(function() { location.reload(true); }); + $("#toolbar button.share").click(function() { + $.showDialog("dialog/_share_test_reports.html", { + submit: function(data, callback) { + $.couch.replicate("test_suite_reports", "http://couchdb.couchdb.org/test_suite_reports"); + callback(); + } + }); + }); $("#toolbar button.add").click(function() { location = "custom_test.html"; }); @@ -52,13 +60,18 @@ specific language governing permissions and limitations under the License. <ul id="toolbar"> <li><button class="run">Run All</button></li> <li><button class="load">Reload</button></li> + <li><button class="share">Share Test Reports</button></li> <li><button class="add">Custom Test</button></li> + <li class="current"></li> </ul> <p class="help"> <strong>Note:</strong> Each of the tests will block the browser. If the connection to your CouchDB server is slow, running the tests will take some time, and you'll not be able to do much with your browser while - a test is being executed. + a test is being executed. <strong>Also:</strong> The test suite is designed + to work with Firefox (with Firebug disabled). Patches are welcome for + convenience compatibility with other browsers, but official support is + for Firefox (latest stable version) only. </p> <table class="listing" id="tests" cellspacing="0"> diff --git a/share/www/custom_test.html b/share/www/custom_test.html index 6106c879..2566a000 100644 --- a/share/www/custom_test.html +++ b/share/www/custom_test.html @@ -20,12 +20,14 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> <script src="script/jquery.resizer.js?0.11.0"></script> <script src="script/couch.js?0.11.0"></script> + <script src="script/couch_test_runner.js?0.11.0"></script> + <script src="script/couch_tests.js"></script> <script> function T(arg, desc) { if(!arg) { diff --git a/share/www/database.html b/share/www/database.html index bafac223..2802ad78 100644 --- a/share/www/database.html +++ b/share/www/database.html @@ -20,7 +20,7 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> @@ -37,7 +37,7 @@ specific language governing permissions and limitations under the License. $(function() { if (page.redirecting) return; - $("h1 strong").html('<a href="?' + page.db.name + '">' + page.db.name + '</a>'); + $("h1 strong").text(page.db.name); var viewPath = page.viewName || "_all_docs"; if (viewPath != "_temp_view" && viewPath != "_design_docs") { $("h1 a.raw").attr("href", "/" + encodeURIComponent(page.db.name) + @@ -71,15 +71,18 @@ specific language governing permissions and limitations under the License. }); // Restore preferences/state - $("#documents thead th.key").toggleClass("desc", $.futon.storage.get("desc")); - var reduce = $.futon.storage.get("reduce"); + $("#documents thead th.key").toggleClass("desc", !!$.futon.storage.get("desc")); + var reduce = !!$.futon.storage.get("reduce"); $("#reduce :checkbox")[0].checked = reduce; - $("#grouplevel select").val($.futon.storage.get("group_level")); + $("#grouplevel select").val(parseInt($.futon.storage.get("group_level"))); $("#grouplevel").toggleClass("disabled", !reduce).find("select").each(function() { this.disabled = !reduce; }); - $("#perpage").val($.futon.storage.get("per_page")); + $("#perpage").val(parseInt($.futon.storage.get("per_page"))); + + var staleViews = !!$.futon.storage.get("stale"); + $("#staleviews :checkbox")[0].checked = staleViews; page.populateViewsMenu(); page.populateViewEditor(); @@ -94,6 +97,9 @@ specific language governing permissions and limitations under the License. location.href = "?" + encodeURIComponent(page.db.name) + (viewName ? "/" + viewName : ""); }); + $("#staleviews :checkbox").click(function() { + $.futon.storage.set("stale", this.checked); + }); $("#documents thead th.key span").click(function() { $(this).closest("th").toggleClass("desc"); page.updateDocumentListing(); @@ -115,12 +121,11 @@ specific language governing permissions and limitations under the License. $.futon.storage.set("per_page", this.value); }); $("#toolbar button.add").click(page.newDocument); - $("#toolbar button.compact").click(page.compactDatabase); - $("#toolbar button.viewcleanup").click(page.viewCleanup); + $("#toolbar button.compact").click(page.compactAndCleanup); $("#toolbar button.delete").click(page.deleteDatabase); - $("#toolbar button.compactview").click(page.compactView); + $("#toolbar button.security").click(page.databaseSecurity); - $('#jumpto input').addPlaceholder("Document ID").suggest(function(text, callback) { + $('#jumpto input').suggest(function(text, callback) { page.db.allDocs({ limit: 10, startkey: text, endkey: text + 'zzz', success: function(docs) { @@ -149,6 +154,11 @@ specific language governing permissions and limitations under the License. <a class="raw" title="Raw view"></a> </h1> <div id="content"> + <div id="staleviews"> + <label>Stale views + <input name="staleviews" type="checkbox" /> + </label> + </div> <div id="switch"> <label>View: <select autocomplete="false"> <option value="_all_docs">All documents</option> @@ -158,15 +168,14 @@ specific language governing permissions and limitations under the License. </div> <div id="jumpto"> <label>Jump to: - <input type="text" name="docid" autocomplete="off" /> + <input type="text" name="docid" placeholder="Document ID" autocomplete="off" /> </label> </div> <ul id="toolbar"> <li><button class="add">New Document</button></li> - <li><button class="compact">Compact Database…</button></li> - <li><button class="viewcleanup">View Cleanup…</button></li> + <li><button class="security">Security…</button></li> + <li><button class="compact">Compact & Cleanup…</button></li> <li><button class="delete">Delete Database…</button></li> - <li><button class="compactview" style="display: none">Compact View…</button></li> </ul> <div id="viewcode" class="collapsed" style="display: none"> diff --git a/share/www/dialog/_admin_party.html b/share/www/dialog/_admin_party.html index f52099a6..ea9fb15a 100644 --- a/share/www/dialog/_admin_party.html +++ b/share/www/dialog/_admin_party.html @@ -22,7 +22,7 @@ specific language governing permissions and limitations under the License. both performance and security. </p> <p class="help"> - Clicking start will remove all admins from the configuration. You will + Clicking “Remove Admins” will remove all admins from the configuration. You will have to recreate any admins by hand after the tests have finished. </p> </fieldset> diff --git a/share/www/dialog/_compact_view.html b/share/www/dialog/_compact_cleanup.html index e64d97e3..506417f4 100644 --- a/share/www/dialog/_compact_view.html +++ b/share/www/dialog/_compact_cleanup.html @@ -13,16 +13,39 @@ specific language governing permissions and limitations under the License. --> <form action="" method="post"> - <h2>Compact View</h2> - <fieldset> + <h2>Compact & Cleanup</h2> + <fieldset class="radiogroup"> + <label> + <input type="radio" name="action" value="compact_database" checked> + Compact Database + </label> + <p class="help"> + Compacting a database removes deleted documents and previous revisions. + It is an <strong>irreversible operation</strong> and may take + a while to complete for large databases. + </p> + <hr> + <label> + <input type="radio" name="action" value="compact_views"> + Compact Views + </label> <p class="help"> View compaction will affect all views in this design document. This operation may take some time to complete. Your views will still operate normally during compaction. </p> + <hr> + <label> + <input type="radio" name="action" value="view_cleanup"> + Cleanup Views + </label> + <p class="help"> + Cleaning up views in a database removes old view files still stored + on the filesystem. It is an <strong>irreversible operation</strong>. + </p> </fieldset> <div class="buttons"> - <button type="submit">Compact</button> + <button type="submit">Run</button> <button type="button" class="cancel">Cancel</button> </div> </form> diff --git a/share/www/dialog/_create_admin.html b/share/www/dialog/_create_admin.html index e4141e1d..d4aec95a 100644 --- a/share/www/dialog/_create_admin.html +++ b/share/www/dialog/_create_admin.html @@ -27,7 +27,7 @@ specific language governing permissions and limitations under the License. </p> <table summary=""><tbody><tr> <th><label>Username:</label></th> - <td><input type="text" name="username" size="24"></td> + <td><input type="text" name="name" size="24"></td> </tr><tr> <th><label>Password:</label></th> <td><input type="password" name="password" size="24"></td> diff --git a/share/www/dialog/_compact_database.html b/share/www/dialog/_create_config.html index 5c898a3c..79e08b08 100644 --- a/share/www/dialog/_compact_database.html +++ b/share/www/dialog/_create_config.html @@ -13,16 +13,30 @@ specific language governing permissions and limitations under the License. --> <form action="" method="post"> - <h2>Compact Database</h2> + <h2>Create New Config Option</h2> <fieldset> <p class="help"> - Compacting a database removes deleted documents and previous revisions. - It is an <strong>irreversible operation</strong> and may take - a while to complete for large databases. + Please enter the section, option, and value. </p> + <table summary=""> + <tbody> + <tr> + <th><label>section:</label></th> + <td><input type="text" name="section" size="24"></td> + </tr> + <tr> + <th><label>option:</label></th> + <td><input type="text" name="option" size="24"></td> + </tr> + <tr> + <th><label>value:</label></th> + <td><input type="text" name="val" size="24"></td> + </tr> + </tbody> + </table> </fieldset> <div class="buttons"> - <button type="submit">Compact</button> + <button type="submit">Create</button> <button type="button" class="cancel">Cancel</button> </div> </form> diff --git a/share/www/dialog/_database_security.html b/share/www/dialog/_database_security.html new file mode 100644 index 00000000..d63fa787 --- /dev/null +++ b/share/www/dialog/_database_security.html @@ -0,0 +1,50 @@ +<!-- + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +--> +<form action="" method="post"> + <h2>Security</h2> + <fieldset> + <p class="help"> + Each database contains lists of admins and readers. + Admins and readers are each defined by <tt>names</tt> and <tt>roles</tt>, which are lists of strings. + </p> + + <h3>Admins</h3> + <p class="help">Database admins can update design documents and edit the readers list.</p> + <table summary=""><tbody><tr> + <th><label>Names:</label></th> + <td><input type="text" name="admin_names" size="40"></td> + </tr><tr> + <th><label>Roles:</label></th> + <td><input type="text" name="admin_roles" size="40"></td> + </tr> + </tbody></table> + + <h3>Readers</h3> + <p class="help">Database readers can access the database. If no readers are defined, the database is public.</p> + <table summary=""><tbody><tr> + <th><label>Names:</label></th> + <td><input type="text" name="reader_names" size="40"></td> + </tr><tr> + <th><label>Roles:</label></th> + <td><input type="text" name="reader_roles" size="40"></td> + </tr> + </tbody></table> + + </fieldset> + <div class="buttons"> + <button type="submit">Update</button> + <button type="button" class="cancel">Cancel</button> + </div> +</form> diff --git a/share/www/dialog/_login.html b/share/www/dialog/_login.html index 959f7233..f05a5fdc 100644 --- a/share/www/dialog/_login.html +++ b/share/www/dialog/_login.html @@ -16,11 +16,11 @@ specific language governing permissions and limitations under the License. <h2>Login</h2> <fieldset> <p class="help"> - Login to CouchDB with your username and password. + Login to CouchDB with your name and password. </p> <table summary=""><tbody><tr> <th><label>Username:</label></th> - <td><input type="text" name="username" size="24"></td> + <td><input type="text" name="name" size="24"></td> </tr><tr> <th><label>Password:</label></th> <td><input type="password" name="password" size="24"></td> diff --git a/share/www/dialog/_share_test_reports.html b/share/www/dialog/_share_test_reports.html new file mode 100644 index 00000000..82b49a74 --- /dev/null +++ b/share/www/dialog/_share_test_reports.html @@ -0,0 +1,42 @@ +<!-- + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +--> +<form action="" method="post"> + <h2>Share Test Reports</h2> + <fieldset> + <p class="help"> + After each test run, a results summary document is stored in + <a href="/_utils/database.html?test_suite_reports">your local + <tt>test_suite_reports</tt> database.</a> The data has no personally + identifying information, just details about the test run and your CouchDB + and browser versions. (Click the red link above to see what's stored.) + The data remains private until you click the "share" button below. + </p> + <p class="help"> + Test reports are very valuable to the CouchDB community, and are easy to share. + Clicking the "share" button below triggers replication from + your local <tt>test_suite_reports</tt> database, to a database hosted by the + project. + </p> + <p class="help"> + <a href="http://couchdb.couchdb.org/_utils/database.html?test_suite_reports"> + Browse test reports shared by other users.</a> + Thank you for sharing! + </p> + </fieldset> + <div class="buttons"> + <button type="submit">Share</button> + <button type="button" class="cancel">Cancel</button> + </div> +</form> diff --git a/share/www/dialog/_signup.html b/share/www/dialog/_signup.html index 884b4be2..7ba3448a 100644 --- a/share/www/dialog/_signup.html +++ b/share/www/dialog/_signup.html @@ -21,7 +21,7 @@ specific language governing permissions and limitations under the License. </p> <table summary=""><tbody><tr> <th><label>Username:</label></th> - <td><input type="text" name="username" size="24"></td> + <td><input type="text" name="name" size="24"></td> </tr><tr> <th><label>Password:</label></th> <td><input type="password" name="password" size="24"></td> diff --git a/share/www/dialog/_view_cleanup.html b/share/www/dialog/_view_cleanup.html deleted file mode 100644 index 2ede89a5..00000000 --- a/share/www/dialog/_view_cleanup.html +++ /dev/null @@ -1,28 +0,0 @@ -<!-- - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. - ---> -<form action="" method="post"> - <h2>View Cleanup</h2> - <fieldset> - <p class="help"> - Cleaning up views in a database removes old view files still stored - on the filesystem. It is an <strong>irreversible operation</strong>. - </p> - </fieldset> - <div class="buttons"> - <button type="submit">Cleanup</button> - <button type="button" class="cancel">Cancel</button> - </div> -</form> - diff --git a/share/www/document.html b/share/www/document.html index 772876aa..b6f42018 100644 --- a/share/www/document.html +++ b/share/www/document.html @@ -20,7 +20,8 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/base64.js"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> @@ -28,7 +29,7 @@ specific language governing permissions and limitations under the License. <script src="script/futon.browse.js?0.11.0"></script> <script src="script/futon.format.js?0.11.0"></script> <script src="script/jquery.editinline.js?0.11.0"></script> - <script src="script/jquery.form.js?0.11.0"></script> + <script src="script/jquery.form.js?2.36"></script> <script> var page = new $.futon.CouchDocumentPage(); diff --git a/share/www/image/key.png b/share/www/image/key.png Binary files differnew file mode 100644 index 00000000..e04ed108 --- /dev/null +++ b/share/www/image/key.png diff --git a/share/www/image/spinner.gif b/share/www/image/spinner.gif Binary files differindex 19a8d4df..6239655e 100644 --- a/share/www/image/spinner.gif +++ b/share/www/image/spinner.gif diff --git a/share/www/index.html b/share/www/index.html index 2cf04c87..975f5986 100644 --- a/share/www/index.html +++ b/share/www/index.html @@ -20,7 +20,7 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> @@ -34,7 +34,7 @@ specific language governing permissions and limitations under the License. this.updateSelection(location.pathname + "index.html"); }); } - var dbsPerPage = $.futon.storage.get("per_page"); + var dbsPerPage = parseInt($.futon.storage.get("per_page")); if (dbsPerPage) $("#perpage").val(dbsPerPage); $("#perpage").change(function() { page.updateDatabaseListing(); diff --git a/share/www/replicator.html b/share/www/replicator.html index 5a09ca16..32f3e20f 100644 --- a/share/www/replicator.html +++ b/share/www/replicator.html @@ -20,7 +20,7 @@ specific language governing permissions and limitations under the License. <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> <script src="script/sha1.js"></script> - <script src="script/jquery.js?1.3.2"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> @@ -41,6 +41,7 @@ specific language governing permissions and limitations under the License. $.couch.allDbs({ success: function(dbs) { + dbs.sort(); $("fieldset select").each(function() { var select = this; $.each(dbs, function(idx, dbName) { @@ -77,17 +78,28 @@ specific language governing permissions and limitations under the License. $("#records tbody.content").empty(); var source = $("#from_local")[0].checked ? $("#from_name").val() : $("#from_url").val(); var target = $("#to_local")[0].checked ? $("#to_name").val() : $("#to_url").val(); + var repOpts = {}; + if ($("#continuous")[0].checked) { + repOpts.continuous = true; + } $.couch.replicate(source, target, { success: function(resp) { - $.each(resp.history, function(idx, record) { + if (resp._local_id) { $("<tr><th></th></tr>") - .find("th").text(JSON.stringify(record)).end() + .find("th").text(JSON.stringify(resp)).end() .appendTo("#records tbody.content"); - }); - $("#records tbody tr").removeClass("odd").filter(":odd").addClass("odd"); - $("#records tbody.footer td").text("Replication session " + resp.session_id); + $("#records tbody tr").removeClass("odd").filter(":odd").addClass("odd"); + } else { + $.each(resp.history, function(idx, record) { + $("<tr><th></th></tr>") + .find("th").text(JSON.stringify(record)).end() + .appendTo("#records tbody.content"); + }); + $("#records tbody tr").removeClass("odd").filter(":odd").addClass("odd"); + $("#records tbody.footer td").text("Replication session " + resp.session_id); + } } - }); + }, repOpts); }); }); </script> @@ -103,25 +115,30 @@ specific language governing permissions and limitations under the License. <fieldset id="source"> <legend>Replicate changes from:</legend> <p> - <label><input type="radio" id="from_local" name="from_type" value="local" checked> Local</label> - <label>database: <select id="from_name" name="from_name"></select></label> + <input type="radio" id="from_local" name="from_type" value="local" checked> + <label for="from_local">Local Database: </label> + <select id="from_name" name="from_name"></select> </p><p> - <label><input type="radio" id="from_to_remote" name="from_type" value="remote"> Remote</label> - <label>database: <input type="text" id="from_url" name="from_url" size="30" value="http://" disabled></label> + <input type="radio" id="from_to_remote" name="from_type" value="remote"> + <label for="from_to_remote">Remote database: </label> + <input type="text" id="from_url" name="from_url" size="30" value="http://" disabled> </p> </fieldset> <p class="swap"><button id="swap" tabindex="99">⇄</button></p> <fieldset id="target"> <legend>to:</legend> <p> - <label><input type="radio" id="to_local" name="to_type" value="local" checked> Local</label> - <label>database: <select id="to_name" name="to_name"></select></label> + <input type="radio" id="to_local" name="to_type" value="local" checked> + <label for="to_local">Local database: </label> + <select id="to_name" name="to_name"></select> </p><p> - <label><input type="radio" id="to_remote" name="to_type" value="remote"> Remote</label> - <label>database: <input type="text" id="to_url" name="to_url" size="30" value="http://" disabled></label> + <input type="radio" id="to_remote" name="to_type" value="remote"> + <label for="to_remote">Remote database: </label> + <input type="text" id="to_url" name="to_url" size="30" value="http://" disabled> </p> </fieldset> <p class="actions"> + <label><input type="checkbox" name="continuous" value="continuous" id="continuous"> Continuous</label> <button id="replicate" type="button">Replicate</button> </p> </form> diff --git a/share/www/script/base64.js b/share/www/script/base64.js new file mode 100644 index 00000000..e0aab303 --- /dev/null +++ b/share/www/script/base64.js @@ -0,0 +1,124 @@ +/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp> + * Version: 1.0 + * LastModified: Dec 25 1999 + * This library is free. You can redistribute it and/or modify it. + */ + /* Modified by Chris Anderson to not use CommonJS */ + /* Modified by Dan Webb not to require Narwhal's binary library */ + +var Base64 = {}; +(function(exports) { + + var encodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var decodeChars = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 + ]; + + exports.encode = function (str) { + var out, i, length; + var c1, c2, c3; + + length = len(str); + i = 0; + out = []; + while(i < length) { + c1 = str.charCodeAt(i++) & 0xff; + if(i == length) + { + out.push(encodeChars.charCodeAt(c1 >> 2)); + out.push(encodeChars.charCodeAt((c1 & 0x3) << 4)); + out.push("=".charCodeAt(0)); + out.push("=".charCodeAt(0)); + break; + } + c2 = str.charCodeAt(i++); + if(i == length) + { + out.push(encodeChars.charCodeAt(c1 >> 2)); + out.push(encodeChars.charCodeAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4))); + out.push(encodeChars.charCodeAt((c2 & 0xF) << 2)); + out.push("=".charCodeAt(0)); + break; + } + c3 = str.charCodeAt(i++); + out.push(encodeChars.charCodeAt(c1 >> 2)); + out.push(encodeChars.charCodeAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4))); + out.push(encodeChars.charCodeAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >>6))); + out.push(encodeChars.charCodeAt(c3 & 0x3F)); + } + + var str = ""; + out.forEach(function(chr) { str += String.fromCharCode(chr) }); + return str; + }; + + exports.decode = function (str) { + var c1, c2, c3, c4; + var i, length, out; + + length = len(str); + i = 0; + out = []; + while(i < length) { + /* c1 */ + do { + c1 = decodeChars[str.charCodeAt(i++) & 0xff]; + } while(i < length && c1 == -1); + if(c1 == -1) + break; + + /* c2 */ + do { + c2 = decodeChars[str.charCodeAt(i++) & 0xff]; + } while(i < length && c2 == -1); + if(c2 == -1) + break; + + out.push(String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4))); + + /* c3 */ + do { + c3 = str.charCodeAt(i++) & 0xff; + if(c3 == 61) + return out.join(''); + c3 = decodeChars[c3]; + } while(i < length && c3 == -1); + if(c3 == -1) + break; + + out.push(String.fromCharCode(((c2 & 0xF) << 4) | ((c3 & 0x3C) >> 2))); + + /* c4 */ + do { + c4 = str.charCodeAt(i++) & 0xff; + if(c4 == 61) + return out.join(''); + c4 = decodeChars[c4]; + } while(i < length && c4 == -1); + + if(c4 == -1) + break; + + out.push(String.fromCharCode(((c3 & 0x03) << 6) | c4)); + } + + return out.join(''); + }; + + var len = function (object) { + if (object.length !== undefined) { + return object.length; + } else if (object.getLength !== undefined) { + return object.getLength(); + } else { + return undefined; + } + }; +})(Base64); diff --git a/share/www/script/couch.js b/share/www/script/couch.js index 21ea39b3..bcc19652 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -22,17 +22,17 @@ function CouchDB(name, httpHeaders) { this.last_req = null; this.request = function(method, uri, requestOptions) { - requestOptions = requestOptions || {} - requestOptions.headers = combine(requestOptions.headers, httpHeaders) - return CouchDB.request(method, uri, requestOptions); - } + requestOptions = requestOptions || {}; + requestOptions.headers = combine(requestOptions.headers, httpHeaders); + return CouchDB.request(method, uri, requestOptions); + }; // Creates the database on the server this.createDb = function() { this.last_req = this.request("PUT", this.uri); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // Deletes the database on the server this.deleteDb = function() { @@ -42,7 +42,7 @@ function CouchDB(name, httpHeaders) { } CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // Save a document to the database this.save = function(doc, options) { @@ -57,7 +57,7 @@ function CouchDB(name, httpHeaders) { var result = JSON.parse(this.last_req.responseText); doc._rev = result.rev; return result; - } + }; // Open a document from the database this.open = function(docId, options) { @@ -68,7 +68,7 @@ function CouchDB(name, httpHeaders) { } CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // Deletes a document from the database this.deleteDoc = function(doc) { @@ -79,7 +79,7 @@ function CouchDB(name, httpHeaders) { doc._rev = result.rev; //record rev in input document doc._deleted = true; return result; - } + }; // Deletes an attachment from a document this.deleteDocAttachment = function(doc, attachment_name) { @@ -89,18 +89,18 @@ function CouchDB(name, httpHeaders) { var result = JSON.parse(this.last_req.responseText); doc._rev = result.rev; //record rev in input document return result; - } + }; this.bulkSave = function(docs, options) { // first prepoulate the UUIDs for new documents - var newCount = 0 + var newCount = 0; for (var i=0; i<docs.length; i++) { if (docs[i]._id == undefined) { newCount++; } } var newUuids = CouchDB.newUuids(docs.length); - var newCount = 0 + var newCount = 0; for (var i=0; i<docs.length; i++) { if (docs[i]._id == undefined) { docs[i]._id = newUuids.pop(); @@ -121,19 +121,19 @@ function CouchDB(name, httpHeaders) { CouchDB.maybeThrowError(this.last_req); var results = JSON.parse(this.last_req.responseText); for (var i = 0; i < docs.length; i++) { - if(results[i].rev) { + if(results[i] && results[i].rev) { docs[i]._rev = results[i].rev; } } return results; } - } + }; this.ensureFullCommit = function() { this.last_req = this.request("POST", this.uri + "_ensure_full_commit"); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // Applies the map function to the contents of database and returns the results. this.query = function(mapFun, reduceFun, options, keys, language) { @@ -163,7 +163,7 @@ function CouchDB(name, httpHeaders) { }); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; this.view = function(viewname, options, keys) { var viewParts = viewname.split('/'); @@ -182,27 +182,21 @@ function CouchDB(name, httpHeaders) { } CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // gets information about the database this.info = function() { this.last_req = this.request("GET", this.uri); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // gets information about a design doc this.designInfo = function(docid) { this.last_req = this.request("GET", this.uri + docid + "/_info"); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } - - this.viewCleanup = function() { - this.last_req = this.request("POST", this.uri + "_view_cleanup"); - CouchDB.maybeThrowError(this.last_req); - return JSON.parse(this.last_req.responseText); - } + }; this.allDocs = function(options,keys) { if(!keys) { @@ -217,37 +211,30 @@ function CouchDB(name, httpHeaders) { } CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; this.designDocs = function() { return this.allDocs({startkey:"_design", endkey:"_design0"}); }; - this.changes = function(options,keys) { - var req = null; - if(!keys) { - req = this.request("GET", this.uri + "_changes" + encodeOptions(options)); - } else { - req = this.request("POST", this.uri + "_changes" + encodeOptions(options), { - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({keys:keys}) - }); - } - CouchDB.maybeThrowError(req); - return JSON.parse(req.responseText); - } + this.changes = function(options) { + this.last_req = this.request("GET", this.uri + "_changes" + + encodeOptions(options)); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); + }; this.compact = function() { this.last_req = this.request("POST", this.uri + "_compact"); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; this.viewCleanup = function() { this.last_req = this.request("POST", this.uri + "_view_cleanup"); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; this.setDbProperty = function(propId, propValue) { this.last_req = this.request("PUT", this.uri + propId,{ @@ -255,37 +242,37 @@ function CouchDB(name, httpHeaders) { }); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; this.getDbProperty = function(propId) { this.last_req = this.request("GET", this.uri + propId); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; - this.setAdmins = function(adminsArray) { - this.last_req = this.request("PUT", this.uri + "_admins",{ - body:JSON.stringify(adminsArray) + this.setSecObj = function(secObj) { + this.last_req = this.request("PUT", this.uri + "_security",{ + body:JSON.stringify(secObj) }); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; - this.getAdmins = function() { - this.last_req = this.request("GET", this.uri + "_admins"); + this.getSecObj = function() { + this.last_req = this.request("GET", this.uri + "_security"); CouchDB.maybeThrowError(this.last_req); return JSON.parse(this.last_req.responseText); - } + }; // Convert a options object to an url query string. // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"' function encodeOptions(options) { - var buf = [] + var buf = []; if (typeof(options) == "object" && options !== null) { for (var name in options) { - if (!options.hasOwnProperty(name)) { continue }; + if (!options.hasOwnProperty(name)) { continue; }; var value = options[name]; - if (name == "key" || name == "startkey" || name == "endkey") { + if (name == "key" || name == "keys" || name == "startkey" || name == "endkey") { value = toJSON(value); } buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); @@ -321,12 +308,13 @@ function CouchDB(name, httpHeaders) { // CouchDB.* functions (except for calls to request itself). // Use this from callers to check HTTP status or header values of requests. CouchDB.last_req = null; +CouchDB.urlPrefix = ''; -CouchDB.login = function(username, password) { +CouchDB.login = function(name, password) { CouchDB.last_req = CouchDB.request("POST", "/_session", { headers: {"Content-Type": "application/x-www-form-urlencoded", "X-CouchDB-WWW-Authenticate": "Cookie"}, - body: "username=" + encodeURIComponent(username) + "&password=" + body: "name=" + encodeURIComponent(name) + "&password=" + encodeURIComponent(password) }); return JSON.parse(CouchDB.last_req.responseText); @@ -338,7 +326,7 @@ CouchDB.logout = function() { "X-CouchDB-WWW-Authenticate": "Cookie"} }); return JSON.parse(CouchDB.last_req.responseText); -} +}; CouchDB.session = function(options) { options = options || {}; @@ -350,7 +338,7 @@ CouchDB.session = function(options) { CouchDB.user_prefix = "org.couchdb.user:"; CouchDB.prepareUserDoc = function(user_doc, new_password) { - user_doc._id = user_doc._id || CouchDB.user_prefix + user_doc.username; + user_doc._id = user_doc._id || CouchDB.user_prefix + user_doc.name; if (new_password) { // handle the password crypto user_doc.salt = CouchDB.newUuids(1)[0]; @@ -358,7 +346,7 @@ CouchDB.prepareUserDoc = function(user_doc, new_password) { } user_doc.type = "user"; if (!user_doc.roles) { - user_doc.roles = [] + user_doc.roles = []; } return user_doc; }; @@ -382,7 +370,7 @@ CouchDB.getVersion = function() { CouchDB.last_req = CouchDB.request("GET", "/"); CouchDB.maybeThrowError(CouchDB.last_req); return JSON.parse(CouchDB.last_req.responseText).version; -} +}; CouchDB.replicate = function(source, target, rep_options) { rep_options = rep_options || {}; @@ -396,7 +384,7 @@ CouchDB.replicate = function(source, target, rep_options) { }); CouchDB.maybeThrowError(CouchDB.last_req); return JSON.parse(CouchDB.last_req.responseText); -} +}; CouchDB.newXhr = function() { if (typeof(XMLHttpRequest) != "undefined") { @@ -406,11 +394,17 @@ CouchDB.newXhr = function() { } else { throw new Error("No XMLHTTPRequest support detected"); } -} +}; CouchDB.request = function(method, uri, options) { - options = options || {}; + options = typeof(options) == 'object' ? options : {}; + options.headers = typeof(options.headers) == 'object' ? options.headers : {}; + options.headers["Content-Type"] = options.headers["Content-Type"] || options.headers["content-type"] || "application/json"; + options.headers["Accept"] = options.headers["Accept"] || options.headers["accept"] || "application/json"; var req = CouchDB.newXhr(); + if(uri.substr(0, CouchDB.protocol.length) != CouchDB.protocol) { + uri = CouchDB.urlPrefix + uri; + } req.open(method, uri, false); if (options.headers) { var headers = options.headers; @@ -421,7 +415,7 @@ CouchDB.request = function(method, uri, options) { } req.send(options.body || ""); return req; -} +}; CouchDB.requestStats = function(module, key, test) { var query_arg = ""; @@ -432,7 +426,7 @@ CouchDB.requestStats = function(module, key, test) { var url = "/_stats/" + module + "/" + key + query_arg; var stat = CouchDB.request("GET", url).responseText; return JSON.parse(stat)[module][key]; -} +}; CouchDB.uuids_cache = []; @@ -455,7 +449,7 @@ CouchDB.newUuids = function(n, buf) { CouchDB.uuids_cache.concat(result.uuids.slice(0, buf)); return result.uuids.slice(buf); } -} +}; CouchDB.maybeThrowError = function(req) { if (req.status >= 400) { diff --git a/share/www/script/couch_test_runner.js b/share/www/script/couch_test_runner.js index 237f9312..56787e9a 100644 --- a/share/www/script/couch_test_runner.js +++ b/share/www/script/couch_test_runner.js @@ -14,6 +14,13 @@ function loadScript(url) { + // disallow loading remote URLs + if((url.substr(0, 7) == "http://") + || (url.substr(0, 2) == "//") + || (url.substr(0, 5) == "data:") + || (url.substr(0, 11) == "javsacript:")) { + throw "Not loading remote test scripts"; + } if (typeof document != "undefined") document.write('<script src="'+url+'"></script>'); }; @@ -21,7 +28,7 @@ function patchTest(fun) { var source = fun.toString(); var output = ""; var i = 0; - var testMarker = "T(" + var testMarker = "T("; while (i < source.length) { var testStart = source.indexOf(testMarker, i); if (testStart == -1) { @@ -72,6 +79,7 @@ function runTest(button, callback, debug, noSave) { var row = currentRow = $(button).parents("tr").get(0); $("td.status", row).removeClass("error").removeClass("failure").removeClass("success"); $("td", row).text(""); + $("#toolbar li.current").text("Running: "+row.id); var testFun = couchTests[row.id]; function run() { numFailures = 0; @@ -103,6 +111,7 @@ function runTest(button, callback, debug, noSave) { var duration = new Date().getTime() - start; $("td.status", row).removeClass("running").addClass(status).text(status); $("td.duration", row).text(duration + "ms"); + $("#toolbar li.current").text("Finished: "+row.id); updateTestsFooter(); currentRow = null; if (callback) callback(); @@ -138,7 +147,8 @@ function setupAdminParty(fun) { } }; $.couch.session({ - success : function(userCtx) { + success : function(resp) { + var userCtx = resp.userCtx; if (userCtx.name && userCtx.roles.indexOf("_admin") != -1) { // admin but not admin party. dialog offering to make admin party $.showDialog("dialog/_admin_party.html", { @@ -229,13 +239,13 @@ function saveTestReport(report) { $.couch.info({success : function(node_info) { report.node = node_info; db.saveDoc(report); - }}) + }}); }; var createDb = function() { db.create({success: function() { db.info({success:saveReport}); }}); - } + }; db.info({error: createDb, success:saveReport}); } }; @@ -299,12 +309,18 @@ function T(arg1, arg2, testName) { .find("code").text(message).end() .appendTo($("td.details ol", currentRow)); } - numFailures += 1 + numFailures += 1; } } function TEquals(expected, actual, testName) { - T(equals(expected, actual), "expected '" + expected + "', got '" + actual + "'", testName); + T(equals(expected, actual), "expected '" + repr(expected) + + "', got '" + repr(actual) + "'", testName); +} + +function TEqualsIgnoreCase(expected, actual, testName) { + T(equals(expected.toUpperCase(), actual.toUpperCase()), "expected '" + repr(expected) + + "', got '" + repr(actual) + "'", testName); } function equals(a,b) { @@ -326,3 +342,96 @@ function repr(val) { } } +function makeDocs(start, end, templateDoc) { + var templateDocSrc = templateDoc ? JSON.stringify(templateDoc) : "{}"; + if (end === undefined) { + end = start; + start = 0; + } + var docs = []; + for (var i = start; i < end; i++) { + var newDoc = eval("(" + templateDocSrc + ")"); + newDoc._id = (i).toString(); + newDoc.integer = i; + newDoc.string = (i).toString(); + docs.push(newDoc); + } + return docs; +} + +function run_on_modified_server(settings, fun) { + try { + // set the settings + for(var i=0; i < settings.length; i++) { + var s = settings[i]; + var xhr = CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, { + body: JSON.stringify(s.value), + headers: {"X-Couch-Persist": "false"} + }); + CouchDB.maybeThrowError(xhr); + s.oldValue = xhr.responseText; + } + // run the thing + fun(); + } finally { + // unset the settings + for(var j=0; j < i; j++) { + var s = settings[j]; + if(s.oldValue == "\"\"\n") { // unset value + CouchDB.request("DELETE", "/_config/" + s.section + "/" + s.key, { + headers: {"X-Couch-Persist": "false"} + }); + } else { + CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, { + body: s.oldValue, + headers: {"X-Couch-Persist": "false"} + }); + } + } + } +} + +function stringFun(fun) { + var string = fun.toSource ? fun.toSource() : "(" + fun.toString() + ")"; + return string; +} + +function waitForSuccess(fun, tag) { + var start = new Date(); + while(true) { + if (new Date() - start > 5000) { + throw("timeout: "+tag); + } else { + try { + fun(); + break; + } catch (e) {} + // sync http req allow async req to happen + CouchDB.request("GET", "/test_suite_db/?tag="+encodeURIComponent(tag)); + } + } +} + +function waitForRestart() { + var waiting = true; + while (waiting) { + try { + CouchDB.request("GET", "/"); + CouchDB.request("GET", "/"); + waiting = false; + } catch(e) { + // the request will fail until restart completes + } + } +}; + +function restartServer() { + var xhr; + try { + CouchDB.request("POST", "/_restart"); + } catch(e) { + // this request may sometimes fail + } + waitForRestart(); +} + diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index 5ae4f1d9..0b8f9288 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -13,12 +13,15 @@ // Used by replication test if (typeof window == 'undefined' || !window) { CouchDB.host = "127.0.0.1:5984"; + CouchDB.protocol = "http://"; CouchDB.inBrowser = false; } else { CouchDB.host = window.location.host; CouchDB.inBrowser = true; + CouchDB.protocol = window.location.protocol + "//"; } +CouchDB.urlPrefix = ".."; var couchTests = {}; function loadTest(file) { @@ -33,7 +36,9 @@ loadTest("attachments.js"); loadTest("attachments_multipart.js"); loadTest("attachment_names.js"); loadTest("attachment_paths.js"); +loadTest("attachment_ranges.js"); loadTest("attachment_views.js"); +loadTest("auth_cache.js"); loadTest("batch_save.js"); loadTest("bulk_docs.js"); loadTest("changes.js"); @@ -57,18 +62,23 @@ loadTest("jsonp.js"); loadTest("large_docs.js"); loadTest("list_views.js"); loadTest("lots_of_docs.js"); +loadTest("method_override.js"); loadTest("multiple_rows.js"); loadScript("script/oauth.js"); loadScript("script/sha1.js"); loadTest("oauth.js"); +loadTest("proxyauth.js"); loadTest("purge.js"); +loadTest("reader_acl.js"); loadTest("recreate_doc.js"); loadTest("reduce.js"); loadTest("reduce_builtin.js"); loadTest("reduce_false.js"); loadTest("reduce_false_temp.js"); loadTest("replication.js"); +loadTest("replicator_db.js"); loadTest("rev_stemming.js"); +loadTest("rewrite.js"); loadTest("security_validation.js"); loadTest("show_documents.js"); loadTest("stats.js"); @@ -79,6 +89,7 @@ loadTest("uuids.js"); loadTest("view_collation.js"); loadTest("view_collation_raw.js"); loadTest("view_conflicts.js"); +loadTest("view_compaction.js"); loadTest("view_errors.js"); loadTest("view_include_docs.js"); loadTest("view_multi_key_all_docs.js"); @@ -87,64 +98,7 @@ loadTest("view_multi_key_temp.js"); loadTest("view_offsets.js"); loadTest("view_pagination.js"); loadTest("view_sandboxing.js"); +loadTest("view_update_seq.js"); loadTest("view_xml.js"); // keep sorted - -function makeDocs(start, end, templateDoc) { - var templateDocSrc = templateDoc ? JSON.stringify(templateDoc) : "{}" - if (end === undefined) { - end = start; - start = 0; - } - var docs = [] - for (var i = start; i < end; i++) { - var newDoc = eval("(" + templateDocSrc + ")"); - newDoc._id = (i).toString(); - newDoc.integer = i; - newDoc.string = (i).toString(); - docs.push(newDoc) - } - return docs; -} - -function run_on_modified_server(settings, fun) { - try { - // set the settings - for(var i=0; i < settings.length; i++) { - var s = settings[i]; - var xhr = CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, { - body: JSON.stringify(s.value), - headers: {"X-Couch-Persist": "false"} - }); - CouchDB.maybeThrowError(xhr); - s.oldValue = xhr.responseText; - } - // run the thing - fun(); - } finally { - // unset the settings - for(var j=0; j < i; j++) { - var s = settings[j]; - if(s.oldValue == "\"\"\n") { // unset value - CouchDB.request("DELETE", "/_config/" + s.section + "/" + s.key, { - headers: {"X-Couch-Persist": "false"} - }); - } else { - CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, { - body: s.oldValue, - headers: {"X-Couch-Persist": "false"} - }); - } - } - } -} - -function stringFun(fun) { - var string = fun.toSource ? fun.toSource() : "(" + fun.toString() + ")"; - return string; -} - -function restartServer() { - CouchDB.request("POST", "/_restart"); -} diff --git a/share/www/script/futon.browse.js b/share/www/script/futon.browse.js index 4d06d283..17975de2 100644 --- a/share/www/script/futon.browse.js +++ b/share/www/script/futon.browse.js @@ -62,7 +62,8 @@ .find("td.size").text($.futon.formatSize(info.disk_size)).end() .find("td.count").text(info.doc_count).end() .find("td.seq").text(info.update_seq); - } + }, + error : function() {} }); }); $("#databases tbody tr:odd").addClass("odd"); @@ -96,7 +97,10 @@ // Page class for browse/database.html CouchDatabasePage: function() { var urlParts = location.search.substr(1).split("/"); - var dbName = decodeURIComponent(urlParts.shift()); + var dbName = decodeURIComponent(urlParts.shift()) + + var dbNameRegExp = new RegExp("[^a-z0-9\_\$\(\)\+\/\-]", "g"); + dbName = dbName.replace(dbNameRegExp, ""); $.futon.storage.declareWithPrefix(dbName + ".", { desc: {}, @@ -106,7 +110,8 @@ reduce: {}, group_level: {defaultValue: 100}, per_page: {defaultValue: 10}, - view: {defaultValue: ""} + view: {defaultValue: ""}, + stale: {defaultValue: false} }); var viewName = (urlParts.length > 0) ? urlParts.join("/") : null; @@ -117,7 +122,7 @@ if (viewName) { this.redirecting = true; location.href = "database.html?" + encodeURIComponent(dbName) + - "/" + viewName; + "/" + encodeURIComponent(viewName); } } var db = $.couch.db(dbName); @@ -133,47 +138,33 @@ var templates = { javascript: "function(doc) {\n emit(null, doc);\n}", python: "def fun(doc):\n yield None, doc", - ruby: "{|doc|\n emit(nil, doc);\n}" + ruby: "lambda {|doc|\n emit(nil, doc);\n}" } this.newDocument = function() { location.href = "document.html?" + encodeURIComponent(db.name); } - this.compactDatabase = function() { - $.showDialog("dialog/_compact_database.html", { - submit: function(data, callback) { - db.compact({ - success: function(resp) { - callback(); - } - }); - } - }); - } - - this.viewCleanup = function() { - $.showDialog("dialog/_view_cleanup.html", { - submit: function(data, callback) { - db.viewCleanup({ - success: function(resp) { - callback(); - } - }); - } - }); - } - - this.compactView = function() { - var groupname = page.viewName.substring(8, - page.viewName.indexOf('/_view')); - $.showDialog("dialog/_compact_view.html", { + this.compactAndCleanup = function() { + $.showDialog("dialog/_compact_cleanup.html", { submit: function(data, callback) { - db.compactView(groupname, { - success: function(resp) { - callback(); - } - }); + switch (data.action) { + case "compact_database": + db.compact({success: function(resp) { callback() }}); + break; + case "compact_views": + var idx = page.viewName.indexOf("/_view"); + if (idx == -1) { + alert("Compact Views requires focus on a view!"); + } else { + var groupname = page.viewName.substring(8, idx); + db.compactView(groupname, {success: function(resp) { callback() }}); + } + break; + case "view_cleanup": + db.viewCleanup({success: function(resp) { callback() }}); + break; + } } }); } @@ -197,6 +188,76 @@ }); } + this.databaseSecurity = function() { + $.showDialog("dialog/_database_security.html", { + load : function(d) { + db.getDbProperty("_security", { + success: function(r) { + ["admin", "reader"].forEach(function(key) { + var names = []; + var roles = []; + + if (r && typeof r[key + "s"] === "object") { + if ($.isArray(r[key + "s"]["names"])) { + names = r[key + "s"]["names"]; + } + if ($.isArray(r[key + "s"]["roles"])) { + roles = r[key + "s"]["roles"]; + } + } + + $("input[name=" + key + "_names]", d).val(JSON.stringify(names)); + $("input[name=" + key + "_roles]", d).val(JSON.stringify(roles)); + }); + } + }); + }, + // maybe this should be 2 forms + submit: function(data, callback) { + var errors = {}; + var secObj = { + admins: { + names: [], + roles: [] + }, + readers: { + names: [], + roles: [] + } + }; + + ["admin", "reader"].forEach(function(key) { + var names, roles; + + try { + names = JSON.parse(data[key + "_names"]); + } catch(e) { } + try { + roles = JSON.parse(data[key + "_roles"]); + } catch(e) { } + + if ($.isArray(names)) { + secObj[key + "s"]["names"] = names; + } else { + errors[key + "_names"] = "The " + key + + " names must be an array of strings"; + } + if ($.isArray(roles)) { + secObj[key + "s"]["roles"] = roles; + } else { + errors[key + "_roles"] = "The " + key + + " roles must be an array of strings"; + } + }); + + if ($.isEmptyObject(errors)) { + db.setDbProperty("_security", secObj); + } + callback(errors); + } + }); + } + this.populateViewEditor = function() { if (viewName.match(/^_design\//)) { page.revertViewChanges(function() { @@ -318,7 +379,8 @@ var path = $.couch.encodeDocId(doc._id) + "/_view/" + encodeURIComponent(viewNames[j]); var option = $(document.createElement("option")) - .attr("value", path).text(viewNames[j]).appendTo(optGroup); + .attr("value", path).text(encodeURIComponent(viewNames[j])) + .appendTo(optGroup); if (path == viewName) { option[0].selected = true; } @@ -354,7 +416,7 @@ } var viewCode = resp.views[localViewName]; page.viewLanguage = resp.language || "javascript"; - $("#language").val(page.viewLanguage); + $("#language").val(encodeURIComponent(page.viewLanguage)); page.updateViewEditor(viewCode.map, viewCode.reduce || ""); $("#viewcode button.revert, #viewcode button.save").attr("disabled", "disabled"); page.storedViewCode = viewCode; @@ -366,7 +428,7 @@ page.updateViewEditor(page.storedViewCode.map, page.storedViewCode.reduce || ""); page.viewLanguage = page.storedViewLanguage; - $("#language").val(page.viewLanguage); + $("#language").val(encodeURIComponent(page.viewLanguage)); $("#viewcode button.revert, #viewcode button.save").attr("disabled", "disabled"); page.isDirty = false; if (callback) callback(); @@ -450,7 +512,8 @@ callback({ docid: "Cannot save to " + data.docid + " because its language is \"" + doc.language + - "\", not \"" + page.viewLanguage + "\"." + "\", not \"" + + encodeURIComponent(page.viewLanguage) + "\"." }); return; } @@ -620,7 +683,7 @@ if (row.id) { $("<td class='key'><a href='document.html?" + encodeURIComponent(db.name) + "/" + $.couch.encodeDocId(row.id) + "'><strong></strong><br>" + - "<span class='docid'>ID: " + row.id + "</span></a></td>") + "<span class='docid'>ID: " + $.futon.escape(row.id) + "</span></a></td>") .find("strong").text(key).end() .appendTo(tr); } else { @@ -712,6 +775,11 @@ db.query(currentMapCode, currentReduceCode, page.viewLanguage, options); } else { var viewParts = viewName.split('/'); + + if ($.futon.storage.get("stale")) { + options.stale = "ok"; + } + db.view(viewParts[1] + "/" + viewParts[3], options); } } @@ -774,7 +842,8 @@ $(this).html($("<pre></pre>").html($.futon.formatJSON(page.doc, {html: true}))) .makeEditable({allowEmpty: false, createInput: function(value) { - return $("<textarea rows='8' cols='80' spellcheck='false'></textarea>").enableTabInsertion(); + var rows = value.split("\n").length; + return $("<textarea rows='" + rows + "' cols='80' spellcheck='false'></textarea>").enableTabInsertion(); }, prepareInput: function(input) { $(input).makeResizable({vertical: true}); @@ -798,7 +867,7 @@ return true; } catch (err) { var msg = err.message; - if (msg == "parseJSON" || msg == "JSON.parse") { + if (msg == "parseJSON" || msg == "JSON.parse") { msg = "There is a syntax error in the document."; } $("<div class='error'></div>").text(msg).appendTo(this); @@ -1042,10 +1111,11 @@ row.find("td").makeEditable({acceptOnBlur: false, allowEmpty: true, createInput: function(value) { + value = doc[row.data("name")]; var elem = $(this); if (elem.find("dl").length > 0 || elem.find("code").is(".array, .object") || - elem.find("code.string").text().length > 60) { + typeof(value) == "string" && (value.length > 60 || value.match(/\n/))) { return $("<textarea rows='1' cols='40' spellcheck='false'></textarea>"); } return $("<input type='text' spellcheck='false'>"); @@ -1117,7 +1187,40 @@ html: true, escapeStrings: false }); - return $(html); + var n = $(html); + if (n.text().length > 140) { + // This code reduces a long string in to a summarized string with a link to expand it. + // Someone, somewhere, is doing something nasty with the event after it leaves these handlers. + // At this time I can't track down the offender, it might actually be a jQuery propogation issue. + var fulltext = n.text(); + var mintext = n.text().slice(0, 140); + var e = $('<a href="#expand">...</a>'); + var m = $('<a href="#min">X</a>'); + var expand = function (evt) { + n.empty(); + n.text(fulltext); + n.append(m); + evt.stopPropagation(); + evt.stopImmediatePropagation(); + evt.preventDefault(); + } + var minimize = function (evt) { + n.empty(); + n.text(mintext); + // For some reason the old element's handler won't fire after removed and added again. + e = $('<a href="#expand">...</a>'); + e.click(expand); + n.append(e); + evt.stopPropagation(); + evt.stopImmediatePropagation(); + evt.preventDefault(); + } + e.click(expand); + n.click(minimize); + n.text(mintext); + n.append(e) + } + return n; } } var elem = render(value); @@ -1179,7 +1282,7 @@ for (var i=0; i < parts.length; i++) { encoded.push(encodeURIComponent(parts[i])); }; - return encoded.join('/'); + return encoded.join('%2f'); } })(jQuery); diff --git a/share/www/script/futon.format.js b/share/www/script/futon.format.js index 5feee817..8d9b7f5c 100644 --- a/share/www/script/futon.format.js +++ b/share/www/script/futon.format.js @@ -13,6 +13,14 @@ (function($) { $.futon = $.futon || {}; $.extend($.futon, { + escape: function(string) { + return string.replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/, """) + .replace(/'/, "'") + ; + }, // JSON pretty printing formatJSON: function(val, options) { @@ -24,12 +32,6 @@ }, options || {}); var itemsep = options.linesep.length ? "," + options.linesep : ", "; - function escape(string) { - return string.replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">"); - } - function format(val, depth) { var tab = []; for (var i = 0; i < options.indent * depth; i++) tab.push(""); @@ -44,7 +46,11 @@ if (type == "string" && !options.escapeStrings) { retval = indentLines(retval.replace(/\r\n/g, "\n"), tab.substr(options.indent)); } else { - retval = escape(JSON.stringify(val)); + if (options.html) { + retval = $.futon.escape(JSON.stringify(val)); + } else { + retval = JSON.stringify(val); + } } if (options.html) { retval = "<code class='" + type + "'>" + retval + "</code>"; @@ -88,7 +94,7 @@ if (options.quoteKeys) { keyDisplay = keyDisplay.substr(1, keyDisplay.length - 2); } - keyDisplay = "<code class='key'>" + escape(keyDisplay) + "</code>"; + keyDisplay = "<code class='key'>" + $.futon.escape(keyDisplay) + "</code>"; if (options.quoteKeys) { keyDisplay = '"' + keyDisplay + '"'; } @@ -114,7 +120,7 @@ function indentLines(text, tab) { var lines = text.split("\n"); for (var i in lines) { - lines[i] = (i > 0 ? tab : "") + escape(lines[i]); + lines[i] = (i > 0 ? tab : "") + $.futon.escape(lines[i]); } return lines.join("<br>"); } diff --git a/share/www/script/futon.js b/share/www/script/futon.js index 33c72318..c4647ed1 100644 --- a/share/www/script/futon.js +++ b/share/www/script/futon.js @@ -10,13 +10,25 @@ // License for the specific language governing permissions and limitations under // the License. +// $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/ +function $$(node) { + var data = $(node).data("$$"); + if (data) { + return data; + } else { + data = {}; + $(node).data("$$", data); + return data; + } +}; + (function($) { function Session() { - function doLogin(username, password, callback) { + function doLogin(name, password, callback) { $.couch.login({ - username : username, + name : name, password : password, success : function() { $.futon.session.sidebar(); @@ -24,36 +36,36 @@ }, error : function(code, error, reason) { $.futon.session.sidebar(); - callback({username : "Error logging in: "+reason}); + callback({name : "Error logging in: "+reason}); } }); }; - function doSignup(username, password, callback, runLogin) { + function doSignup(name, password, callback, runLogin) { $.couch.signup({ - username : username + name : name }, password, { success : function() { if (runLogin) { - doLogin(username, password, callback); + doLogin(name, password, callback); } else { callback(); } }, error : function(status, error, reason) { $.futon.session.sidebar(); - if (error = "conflict") { - callback({username : "Name '"+username+"' is taken"}); + if (error == "conflict") { + callback({name : "Name '"+name+"' is taken"}); } else { - callback({username : "Signup error: "+reason}); + callback({name : "Signup error: "+reason}); } } }); }; function validateUsernameAndPassword(data, callback) { - if (!data.username || data.username.length == 0) { - callback({username: "Please enter a username."}); + if (!data.name || data.name.length == 0) { + callback({name: "Please enter a name."}); return false; }; if (!data.password || data.password.length == 0) { @@ -69,11 +81,21 @@ if (!validateUsernameAndPassword(data, callback)) return; $.couch.config({ success : function() { - callback(); - doLogin(data.username, data.password, callback); - doSignup(data.username, null, callback, false); + doLogin(data.name, data.password, function(errors) { + if(!$.isEmptyObject(errors)) { + callback(errors); + return; + } + doSignup(data.name, null, function(errors) { + if (errors && errors.name && errors.name.indexOf && errors.name.indexOf("taken") == -1) { + callback(errors); + } else { + callback(); + } + }, false); + }); } - }, "admins", data.username, data.password); + }, "admins", data.name, data.password); } }); return false; @@ -83,7 +105,7 @@ $.showDialog("dialog/_login.html", { submit: function(data, callback) { if (!validateUsernameAndPassword(data, callback)) return; - doLogin(data.username, data.password, callback); + doLogin(data.name, data.password, callback); } }); return false; @@ -101,7 +123,7 @@ $.showDialog("dialog/_signup.html", { submit: function(data, callback) { if (!validateUsernameAndPassword(data, callback)) return; - doSignup(data.username, data.password, callback, true); + doSignup(data.name, data.password, callback, true); } }); return false; @@ -118,9 +140,11 @@ // get users db info? $("#userCtx span").hide(); $.couch.session({ - success : function(userCtx) { + success : function(r) { + var userCtx = r.userCtx; + $$("#userCtx").userCtx = userCtx; if (userCtx.name) { - $("#userCtx .username").text(userCtx.name).attr({href : "/_utils/document.html?users/org.couchdb.user%3A"+userCtx.name}); + $("#userCtx .name").text(userCtx.name).attr({href : $.couch.urlPrefix + "/_utils/document.html?"+encodeURIComponent(r.info.authentication_db)+"/org.couchdb.user%3A"+encodeURIComponent(userCtx.name)}); if (userCtx.roles.indexOf("_admin") != -1) { $("#userCtx .loggedinadmin").show(); } else { @@ -159,7 +183,8 @@ } this.addDatabase = function(name) { - var recentDbs = $.futon.storage.get("recent", "").split(","); + var current = $.futon.storage.get("recent", ""); + var recentDbs = current ? current.split(",") : []; if ($.inArray(name, recentDbs) == -1) { recentDbs.unshift(name); if (recentDbs.length > 10) recentDbs.length = 10; @@ -170,7 +195,8 @@ this.removeDatabase = function(name) { // remove database from recent databases list - var recentDbs = $.futon.storage.get("recent").split(","); + var current = $.futon.storage.get("recent", ""); + var recentDbs = current ? current.split(",") : []; var recentIdx = $.inArray(name, recentDbs); if (recentIdx >= 0) { recentDbs.splice(recentIdx, 1); @@ -189,9 +215,10 @@ recentDbs.sort(); $.each(recentDbs, function(idx, name) { if (name) { + name = encodeURIComponent(name); $("#dbs").append("<li>" + "<button class='remove' title='Remove from list' value='" + name + "'></button>" + - "<a href='database.html?" + encodeURIComponent(name) + "' title='" + name + "'>" + name + + "<a href='database.html?" + name + "' title='" + name + "'>" + name + "</a></li>"); } }); @@ -308,11 +335,22 @@ return callback(decl); } + function windowName() { + try { + return JSON.parse(window.name || "{}"); + } catch (e) { + return {}; + } + } + + // add suffix to cookie names to be able to separate between ports + var cookiePrefix = location.port + "_"; + var handlers = { "cookie": { get: function(name) { - var nameEq = name + "="; + var nameEq = cookiePrefix + name + "="; var parts = document.cookie.split(';'); for (var i = 0; i < parts.length; i++) { var part = parts[i].replace(/^\s+/, ""); @@ -324,27 +362,28 @@ set: function(name, value) { var date = new Date(); date.setTime(date.getTime() + 14*24*60*60*1000); // two weeks - document.cookie = name + "=" + escape(value) + "; expires=" + - date.toGMTString(); + document.cookie = cookiePrefix + name + "=" + escape(value) + + "; expires=" + date.toGMTString(); }, del: function(name) { var date = new Date(); date.setTime(date.getTime() - 24*60*60*1000); // yesterday - document.cookie = name + "=; expires=" + date.toGMTString(); + document.cookie = cookiePrefix + name + "=" + + "; expires=" + date.toGMTString(); } }, "window": { get: function(name) { - return JSON.parse(window.name || "{}")[name]; + return windowName()[name]; }, set: function(name, value) { - var obj = JSON.parse(window.name || "{}"); + var obj = windowName(); obj[name] = value || null; window.name = JSON.stringify(obj); }, del: function(name) { - var obj = JSON.parse(window.name || "{}"); + var obj = windowName(); delete obj[name]; window.name = JSON.stringify(obj); } @@ -354,6 +393,7 @@ } + $.couch.urlPrefix = ".."; $.futon = $.futon || {}; $.extend($.futon, { navigation: new Navigation(), @@ -361,31 +401,21 @@ storage: new Storage() }); - $.fn.addPlaceholder = function(text) { - return this.each(function() { + $.fn.addPlaceholder = function() { + if (this[0] && "placeholder" in document.createElement("input")) { + return; // found native placeholder support + } + return this.live('focusin', function() { var input = $(this); - if ($.browser.safari) { - input.attr("placeholder", text); - return; + if (input.val() === input.attr("placeholder")) { + input.removeClass("placeholder").val(""); } - input.blur(function() { - if ($.trim(input.val()) == "") { - input.addClass("placeholder").val(text); - } else { - input.removeClass("placeholder"); - } - }).triggerHandler("blur") - input.focus(function() { - if (input.is(".placeholder")) { - input.val("").removeClass("placeholder"); - } - }); - $(this.form).submit(function() { - if (input.is(".placeholder")) { - input.val(""); - } - }); - }); + }).live("focusout", function() { + var input = $(this); + if (input.val() === "") { + input.val(input.attr("placeholder")).addClass("placeholder"); + } + }).trigger("focusout"); } $.fn.enableTabInsertion = function(chars) { @@ -421,6 +451,8 @@ // doing this as early as possible prevents flickering $(document.body).addClass("fullwidth"); } + $("input[placeholder]").addPlaceholder(); + $.get("_sidebar.html", function(resp) { $("#wrap").append(resp) .find("#sidebar-toggle").click(function(e) { @@ -442,7 +474,6 @@ $("#version").text(info.version); } }); - }); }); diff --git a/share/www/script/jquery.couch.js b/share/www/script/jquery.couch.js index 7e8a0236..8d86b679 100644 --- a/share/www/script/jquery.couch.js +++ b/share/www/script/jquery.couch.js @@ -28,7 +28,7 @@ return; } var user_prefix = "org.couchdb.user:"; - user_doc._id = user_doc._id || user_prefix + user_doc.username; + user_doc._id = user_doc._id || user_prefix + user_doc.name; if (new_password) { // handle the password crypto user_doc.salt = $.couch.newUUID(); @@ -36,17 +36,18 @@ } user_doc.type = "user"; if (!user_doc.roles) { - user_doc.roles = [] + user_doc.roles = []; } return user_doc; }; - uuidCache = []; + var uuidCache = []; $.extend($.couch, { + urlPrefix: '', activeTasks: function(options) { ajax( - {url: "/_active_tasks"}, + {url: this.urlPrefix + "/_active_tasks"}, options, "Active task status could not be retrieved" ); @@ -54,14 +55,14 @@ allDbs: function(options) { ajax( - {url: "/_all_dbs"}, + {url: this.urlPrefix + "/_all_dbs"}, options, "An error occurred retrieving the list of all databases" ); }, config: function(options, section, option, value) { - var req = {url: "/_config/"}; + var req = {url: this.urlPrefix + "/_config/"}; if (section) { req.url += encodeURIComponent(section) + "/"; if (option) { @@ -85,7 +86,7 @@ session: function(options) { options = options || {}; $.ajax({ - type: "GET", url: "/_session", + type: "GET", url: this.urlPrefix + "/_session", complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 200) { @@ -102,7 +103,7 @@ userDb : function(callback) { $.couch.session({ success : function(resp) { - var userDb = $.couch.db(resp.info.user_db); + var userDb = $.couch.db(resp.info.authentication_db); callback(userDb); } }); @@ -114,14 +115,14 @@ user_doc = prepareUserDoc(user_doc, password); $.couch.userDb(function(db) { db.saveDoc(user_doc, options); - }) + }); }, login: function(options) { options = options || {}; $.ajax({ - type: "POST", url: "/_session", dataType: "json", - data: {username: options.username, password: options.password}, + type: "POST", url: this.urlPrefix + "/_session", dataType: "json", + data: {name: options.name, password: options.password}, complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 200) { @@ -137,7 +138,7 @@ logout: function(options) { options = options || {}; $.ajax({ - type: "DELETE", url: "/_session", dataType: "json", + type: "DELETE", url: this.urlPrefix + "/_session", dataType: "json", username : "_", password : "_", complete: function(req) { var resp = $.httpData(req, "json"); @@ -152,10 +153,28 @@ }); }, - db: function(name) { + db: function(name, db_opts) { + db_opts = db_opts || {}; + var rawDocs = {}; + function maybeApplyVersion(doc) { + if (doc._id && doc._rev && rawDocs[doc._id] && rawDocs[doc._id].rev == doc._rev) { + // todo: can we use commonjs require here? + if (typeof Base64 == "undefined") { + alert("please include /_utils/script/base64.js in the page for base64 support"); + return false; + } else { + doc._attachments = doc._attachments || {}; + doc._attachments["rev-"+doc._rev.split("-")[0]] = { + content_type :"application/json", + data : Base64.encode(rawDocs[doc._id].raw) + }; + return true; + } + } + }; return { name: name, - uri: "/" + encodeURIComponent(name) + "/", + uri: this.urlPrefix + "/" + encodeURIComponent(name) + "/", compact: function(options) { $.extend(options, {successStatus: 202}); @@ -211,9 +230,79 @@ "Database information could not be retrieved" ); }, + changes: function(since, options) { + options = options || {}; + // set up the promise object within a closure for this handler + var timeout = 100, db = this, active = true, + listeners = [], + promise = { + onChange : function(fun) { + listeners.push(fun); + }, + stop : function() { + active = false; + } + }; + // call each listener when there is a change + function triggerListeners(resp) { + $.each(listeners, function() { + this(resp); + }); + }; + // when there is a change, call any listeners, then check for another change + options.success = function(resp) { + timeout = 100; + if (active) { + since = resp.last_seq; + triggerListeners(resp); + getChangesSince(); + }; + }; + options.error = function() { + if (active) { + setTimeout(getChangesSince, timeout); + timeout = timeout * 2; + } + }; + // actually make the changes request + function getChangesSince() { + var opts = $.extend({heartbeat : 10 * 1000}, options, { + feed : "longpoll", + since : since + }); + ajax( + {url: db.uri + "_changes"+encodeOptions(opts)}, + options, + "Error connecting to "+db.uri+"/_changes." + ); + } + // start the first request + if (since) { + getChangesSince(); + } else { + db.info({ + success : function(info) { + since = info.update_seq; + getChangesSince(); + } + }); + } + return promise; + }, allDocs: function(options) { - ajax( - {url: this.uri + "_all_docs" + encodeOptions(options)}, + var type = "GET"; + var data = null; + if (options["keys"]) { + type = "POST"; + var keys = options["keys"]; + delete options["keys"]; + data = toJSON({ "keys": keys }); + } + ajax({ + type: type, + data: data, + url: this.uri + "_all_docs" + encodeOptions(options) + }, options, "An error occurred retrieving a list of all documents" ); @@ -246,10 +335,32 @@ } }); } else { - alert("please provide an eachApp function for allApps()"); + alert("Please provide an eachApp function for allApps()"); } }, openDoc: function(docId, options, ajaxOptions) { + options = options || {}; + if (db_opts.attachPrevRev || options.attachPrevRev) { + $.extend(options, { + beforeSuccess : function(req, doc) { + rawDocs[doc._id] = { + rev : doc._rev, + raw : req.responseText + }; + } + }); + } else { + $.extend(options, { + beforeSuccess : function(req, doc) { + if (doc["jquery.couch.attachPrevRev"]) { + rawDocs[doc._id] = { + rev : doc._rev, + raw : req.responseText + }; + } + } + }); + } ajax({url: this.uri + encodeDocId(docId) + encodeOptions(options)}, options, "The document could not be retrieved", @@ -258,6 +369,8 @@ }, saveDoc: function(doc, options) { options = options || {}; + var db = this; + var beforeSend = fullCommit(options); if (doc._id === undefined) { var method = "POST"; var uri = this.uri; @@ -265,16 +378,28 @@ var method = "PUT"; var uri = this.uri + encodeDocId(doc._id); } + var versioned = maybeApplyVersion(doc); $.ajax({ type: method, url: uri + encodeOptions(options), contentType: "application/json", dataType: "json", data: toJSON(doc), + beforeSend : beforeSend, complete: function(req) { var resp = $.httpData(req, "json"); - if (req.status == 201) { + if (req.status == 200 || req.status == 201 || req.status == 202) { doc._id = resp.id; doc._rev = resp.rev; - if (options.success) options.success(resp); + if (versioned) { + db.openDoc(doc._id, { + attachPrevRev : true, + success : function(d) { + doc._attachments = d._attachments; + if (options.success) options.success(resp); + } + }); + } else { + if (options.success) options.success(resp); + } } else if (options.error) { options.error(req.status, resp.error, resp.reason); } else { @@ -284,10 +409,12 @@ }); }, bulkSave: function(docs, options) { - $.extend(options, {successStatus: 201}); + var beforeSend = fullCommit(options); + $.extend(options, {successStatus: 201, beforeSend : beforeSend}); ajax({ type: "POST", - url: this.uri + "_bulk_docs" + encodeOptions(options) + url: this.uri + "_bulk_docs" + encodeOptions(options), + contentType: "application/json", data: toJSON(docs) }, options, "The documents could not be saved" @@ -304,13 +431,27 @@ "The document could not be deleted" ); }, - copyDoc: function(doc, options, ajaxOptions) { + bulkRemove: function(docs, options){ + docs.docs = $.each( + docs.docs, function(i, doc){ + doc._deleted = true; + } + ); + $.extend(options, {successStatus: 201}); + ajax({ + type: "POST", + url: this.uri + "_bulk_docs" + encodeOptions(options), + data: toJSON(docs) + }, + options, + "The documents could not be deleted" + ); + }, + copyDoc: function(docId, options, ajaxOptions) { ajaxOptions = $.extend(ajaxOptions, { complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 201) { - doc._id = resp.id; - doc._rev = resp.rev; if (options.success) options.success(resp); } else if (options.error) { options.error(req.status, resp.error, resp.reason); @@ -321,9 +462,7 @@ }); ajax({ type: "COPY", - url: this.uri + - encodeDocId(doc._id) + - encodeOptions({rev: doc._rev}) + url: this.uri + encodeDocId(docId) }, options, "The document could not be copied", @@ -350,6 +489,26 @@ "An error occurred querying the database" ); }, + list: function(list, view, options) { + var list = list.split('/'); + var options = options || {}; + var type = 'GET'; + var data = null; + if (options['keys']) { + type = 'POST'; + var keys = options['keys']; + delete options['keys']; + data = toJSON({'keys': keys }); + } + ajax({ + type: type, + data: data, + url: this.uri + '_design/' + list[0] + + '/_list/' + list[1] + '/' + view + encodeOptions(options) + }, + options, 'An error occured accessing the list' + ); + }, view: function(name, options) { var name = name.split('/'); var options = options || {}; @@ -369,6 +528,25 @@ }, options, "An error occurred accessing the view" ); + }, + getDbProperty: function(propName, options, ajaxOptions) { + ajax({url: this.uri + propName + encodeOptions(options)}, + options, + "The property could not be retrieved", + ajaxOptions + ); + }, + + setDbProperty: function(propName, propValue, options, ajaxOptions) { + ajax({ + type: "PUT", + url: this.uri + propName + encodeOptions(options), + data : JSON.stringify(propValue) + }, + options, + "The property could not be updated", + ajaxOptions + ); } }; }, @@ -377,19 +555,23 @@ info: function(options) { ajax( - {url: "/"}, + {url: this.urlPrefix + "/"}, options, "Server information could not be retrieved" ); }, - replicate: function(source, target, options) { + replicate: function(source, target, ajaxOptions, repOpts) { + repOpts = $.extend({source: source, target: target}, repOpts); + if (repOpts.continuous) { + ajaxOptions.successStatus = 202; + } ajax({ - type: "POST", url: "/_replicate", - data: JSON.stringify({source: source, target: target}), + type: "POST", url: this.urlPrefix + "/_replicate", + data: JSON.stringify(repOpts), contentType: "application/json" }, - options, + ajaxOptions, "Replication failed" ); }, @@ -399,9 +581,9 @@ cacheNum = 1; } if (!uuidCache.length) { - ajax({url: "/_uuids", data: {count: cacheNum}, async: false}, { + ajax({url: this.urlPrefix + "/_uuids", data: {count: cacheNum}, async: false}, { success: function(resp) { - uuidCache = resp.uuids + uuidCache = resp.uuids; } }, "Failed to retrieve UUID batch." @@ -409,21 +591,40 @@ } return uuidCache.shift(); } - }); function ajax(obj, options, errorMessage, ajaxOptions) { options = $.extend({successStatus: 200}, options); + ajaxOptions = $.extend({contentType: "application/json"}, ajaxOptions); errorMessage = errorMessage || "Unknown error"; - $.ajax($.extend($.extend({ - type: "GET", dataType: "json", + type: "GET", dataType: "json", cache : !$.browser.msie, + beforeSend: function(xhr){ + if(ajaxOptions && ajaxOptions.headers){ + for (var header in ajaxOptions.headers){ + xhr.setRequestHeader(header, ajaxOptions.headers[header]); + } + } + }, complete: function(req) { - var resp = $.httpData(req, "json"); + try { + var resp = $.httpData(req, "json"); + } catch(e) { + if (options.error) { + options.error(req.status, req, e); + } else { + alert(errorMessage + ": " + e); + } + return; + } + if (options.ajaxStart) { + options.ajaxStart(resp); + } if (req.status == options.successStatus) { + if (options.beforeSuccess) options.beforeSuccess(req, resp); if (options.success) options.success(resp); } else if (options.error) { - options.error(req.status, resp.error, resp.reason); + options.error(req.status, resp && resp.error || errorMessage, resp && resp.reason || "no response"); } else { alert(errorMessage + ": " + resp.reason); } @@ -431,13 +632,24 @@ }, obj), ajaxOptions)); } + function fullCommit(options) { + var options = options || {}; + if (typeof options.ensure_full_commit !== "undefined") { + var commit = options.ensure_full_commit; + delete options.ensure_full_commit; + return function(xhr) { + xhr.setRequestHeader("X-Couch-Full-Commit", commit.toString()); + }; + } + }; + // Convert a options object to an url query string. // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"' function encodeOptions(options) { var buf = []; if (typeof(options) === "object" && options !== null) { for (var name in options) { - if ($.inArray(name, ["error", "success"]) >= 0) + if ($.inArray(name, ["error", "success", "beforeSuccess", "ajaxStart"]) >= 0) continue; var value = options[name]; if ($.inArray(name, ["key", "startkey", "endkey"]) >= 0) { diff --git a/share/www/script/jquery.dialog.js b/share/www/script/jquery.dialog.js index 41dd08e6..02c0c497 100644 --- a/share/www/script/jquery.dialog.js +++ b/share/www/script/jquery.dialog.js @@ -79,7 +79,7 @@ data[this.name] = this.value; // file inputs need special handling }); options.submit(data, function callback(errors) { - if (errors == null || errors == {}) { + if ($.isEmptyObject(errors)) { dismiss(); } else { for (var name in errors) { diff --git a/share/www/script/jquery.form.js b/share/www/script/jquery.form.js index e35ef0ef..dde39427 100644 --- a/share/www/script/jquery.form.js +++ b/share/www/script/jquery.form.js @@ -1,44 +1,42 @@ /* * jQuery Form Plugin - * version: 2.18 (06-JAN-2009) - * @requires jQuery v1.2.2 or later + * version: 2.36 (07-NOV-2009) + * @requires jQuery v1.2.6 or later * * Examples and documentation at: http://malsup.com/jquery/form/ * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html - * - * Revision: $Id$ */ ;(function($) { /* - Usage Note: - ----------- - Do not use both ajaxSubmit and ajaxForm on the same form. These - functions are intended to be exclusive. Use ajaxSubmit if you want - to bind your own submit handler to the form. For example, - - $(document).ready(function() { - $('#myForm').bind('submit', function() { - $(this).ajaxSubmit({ - target: '#output' - }); - return false; // <-- important! - }); - }); - - Use ajaxForm when you want the plugin to manage all the event binding - for you. For example, - - $(document).ready(function() { - $('#myForm').ajaxForm({ - target: '#output' - }); - }); - - When using ajaxForm, the ajaxSubmit function will be invoked for you - at the appropriate time. + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are intended to be exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').bind('submit', function() { + $(this).ajaxSubmit({ + target: '#output' + }); + return false; // <-- important! + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. */ /** @@ -46,292 +44,319 @@ * an HTML form using AJAX. */ $.fn.ajaxSubmit = function(options) { - // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) - if (!this.length) { - log('ajaxSubmit: skipping submit process - no element selected'); - return this; - } - - if (typeof options == 'function') - options = { success: options }; - - options = $.extend({ - url: this.attr('action') || window.location.toString(), - type: this.attr('method') || 'GET' - }, options || {}); - - // hook for manipulating the form data before it is extracted; - // convenient for use with rich editors like tinyMCE or FCKEditor - var veto = {}; - this.trigger('form-pre-serialize', [this, options, veto]); - if (veto.veto) { - log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); - return this; - } - - // provide opportunity to alter form data before it is serialized - if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { - log('ajaxSubmit: submit aborted via beforeSerialize callback'); - return this; - } - - var a = this.formToArray(options.semantic); - if (options.data) { - options.extraData = options.data; - for (var n in options.data) { - if(options.data[n] instanceof Array) { - for (var k in options.data[n]) - a.push( { name: n, value: options.data[n][k] } ) - } - else - a.push( { name: n, value: options.data[n] } ); - } - } - - // give pre-submit callback an opportunity to abort the submit - if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { - log('ajaxSubmit: submit aborted via beforeSubmit callback'); - return this; - } - - // fire vetoable 'validate' event - this.trigger('form-submit-validate', [a, this, options, veto]); - if (veto.veto) { - log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); - return this; - } - - var q = $.param(a); - - if (options.type.toUpperCase() == 'GET') { - options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; - options.data = null; // data is null for 'get' - } - else - options.data = q; // data is the query string for 'post' - - var $form = this, callbacks = []; - if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); - if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); - - // perform a load on the target only if dataType is not provided - if (!options.dataType && options.target) { - var oldSuccess = options.success || function(){}; - callbacks.push(function(data) { - $(options.target).html(data).each(oldSuccess, arguments); - }); - } - else if (options.success) - callbacks.push(options.success); - - options.success = function(data, status) { - for (var i=0, max=callbacks.length; i < max; i++) - callbacks[i].apply(options, [data, status, $form]); - }; - - // are there files to upload? - var files = $('input:file', this).fieldValue(); - var found = false; - for (var j=0; j < files.length; j++) - if (files[j]) - found = true; - - // options.iframe allows user to force iframe mode - if (options.iframe || found) { - // hack to fix Safari hang (thanks to Tim Molendijk for this) - // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d - if ($.browser.safari && options.closeKeepAlive) - $.get(options.closeKeepAlive, fileUpload); - else - fileUpload(); - } + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + if (typeof options == 'function') + options = { success: options }; + + var url = $.trim(this.attr('action')); + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + url = url || window.location.href || ''; + + options = $.extend({ + url: url, + type: this.attr('method') || 'GET', + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options || {}); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var a = this.formToArray(options.semantic); + if (options.data) { + options.extraData = options.data; + for (var n in options.data) { + if(options.data[n] instanceof Array) { + for (var k in options.data[n]) + a.push( { name: n, value: options.data[n][k] } ); + } + else + a.push( { name: n, value: options.data[n] } ); + } + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a); + + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status) { + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i].apply(options, [data, status, $form]); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + var multipart = false; +// var mp = 'multipart/form-data'; +// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if ((files.length && options.iframe !== false) || options.iframe || found || multipart) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) + $.get(options.closeKeepAlive, fileUpload); + else + fileUpload(); + } else - $.ajax(options); - - // fire 'notify' event - this.trigger('form-submit-notify', [this, options]); - return this; - - - // private function for handling file uploads (hat tip to YAHOO!) - function fileUpload() { - var form = $form[0]; - - if ($(':input[name=submit]', form).length) { - alert('Error: Form elements must not be named "submit".'); - return; - } - - var opts = $.extend({}, $.ajaxSettings, options); - var s = jQuery.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); - - var id = 'jqFormIO' + (new Date().getTime()); - var $io = $('<iframe id="' + id + '" name="' + id + '" />'); - var io = $io[0]; - - if ($.browser.msie || $.browser.opera) - io.src = 'javascript:false;document.write("");'; - $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' }); - - var xhr = { // mock object - aborted: 0, - responseText: null, - responseXML: null, - status: 0, - statusText: 'n/a', - getAllResponseHeaders: function() {}, - getResponseHeader: function() {}, - setRequestHeader: function() {}, - abort: function() { - this.aborted = 1; - $io.attr('src','about:blank'); // abort op in progress - } - }; - - var g = opts.global; - // trigger ajax global events so that activity/block indicators work like normal - if (g && ! $.active++) $.event.trigger("ajaxStart"); - if (g) $.event.trigger("ajaxSend", [xhr, opts]); + $.ajax(options); + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + + if ($(':input[name=submit]', form).length) { + alert('Error: Form elements must not be named "submit".'); + return; + } + + var opts = $.extend({}, $.ajaxSettings, options); + var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); + + var id = 'jqFormIO' + (new Date().getTime()); + var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" />'); + var io = $io[0]; + + $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' }); + + var xhr = { // mock object + aborted: 0, + responseText: null, + responseXML: null, + status: 0, + statusText: 'n/a', + getAllResponseHeaders: function() {}, + getResponseHeader: function() {}, + setRequestHeader: function() {}, + abort: function() { + this.aborted = 1; + $io.attr('src', opts.iframeSrc); // abort op in progress + } + }; + + var g = opts.global; + // trigger ajax global events so that activity/block indicators work like normal + if (g && ! $.active++) $.event.trigger("ajaxStart"); + if (g) $.event.trigger("ajaxSend", [xhr, opts]); if (s.beforeSend && s.beforeSend(xhr, s) === false) { - s.global && jQuery.active--; + s.global && $.active--; + return; + } + if (xhr.aborted) return; - } - if (xhr.aborted) - return; - - var cbInvoked = 0; - var timedOut = 0; - - // add submitting element to data if we know it - var sub = form.clk; - if (sub) { - var n = sub.name; - if (n && !sub.disabled) { - options.extraData = options.extraData || {}; - options.extraData[n] = sub.value; - if (sub.type == "image") { - options.extraData[name+'.x'] = form.clk_x; - options.extraData[name+'.y'] = form.clk_y; - } - } - } - - // take a breath so that pending repaints get some cpu time before the upload starts - setTimeout(function() { - // make sure form attrs are set - var t = $form.attr('target'), a = $form.attr('action'); - $form.attr({ - target: id, - method: 'POST', - action: opts.url - }); - - // ie borks in some cases when setting encoding - if (! options.skipEncodingOverride) { - $form.attr({ - encoding: 'multipart/form-data', - enctype: 'multipart/form-data' - }); - } - - // support timout - if (opts.timeout) - setTimeout(function() { timedOut = true; cb(); }, opts.timeout); - - // add "extra" data to form if provided in options - var extraInputs = []; - try { - if (options.extraData) - for (var n in options.extraData) - extraInputs.push( - $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />') - .appendTo(form)[0]); - - // add iframe to doc and submit the form - $io.appendTo('body'); - io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false); - form.submit(); - } - finally { - // reset attrs and remove "extra" input elements - $form.attr('action', a); - t ? $form.attr('target', t) : $form.removeAttr('target'); - $(extraInputs).remove(); - } - }, 10); - - function cb() { - if (cbInvoked++) return; - - io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false); - - var operaHack = 0; - var ok = true; - try { - if (timedOut) throw 'timeout'; - // extract the server response from the iframe - var data, doc; - - doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document; - - if (doc.body == null && !operaHack && $.browser.opera) { - // In Opera 9.2.x the iframe DOM is not always traversable when - // the onload callback fires so we give Opera 100ms to right itself - operaHack = 1; - cbInvoked--; - setTimeout(cb, 100); - return; - } - - xhr.responseText = doc.body ? doc.body.innerHTML : null; - xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc; - xhr.getResponseHeader = function(header){ - var headers = {'content-type': opts.dataType}; - return headers[header]; - }; - - if (opts.dataType == 'json' || opts.dataType == 'script') { - var ta = doc.getElementsByTagName('textarea')[0]; - xhr.responseText = ta ? ta.value : xhr.responseText; - } - else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) { - xhr.responseXML = toXml(xhr.responseText); - } - data = $.httpData(xhr, opts.dataType); - } - catch(e){ - ok = false; - $.handleError(opts, xhr, 'error', e); - } - - // ordering of these callbacks/triggers is odd, but that's how $.ajax does it - if (ok) { - opts.success(data, 'success'); - if (g) $.event.trigger("ajaxSuccess", [xhr, opts]); - } - if (g) $.event.trigger("ajaxComplete", [xhr, opts]); - if (g && ! --$.active) $.event.trigger("ajaxStop"); - if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error'); - - // clean up - setTimeout(function() { - $io.remove(); - xhr.responseXML = null; - }, 100); - }; - - function toXml(s, doc) { - if (window.ActiveXObject) { - doc = new ActiveXObject('Microsoft.XMLDOM'); - doc.async = 'false'; - doc.loadXML(s); - } - else - doc = (new DOMParser()).parseFromString(s, 'text/xml'); - return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null; - }; - }; + + var cbInvoked = 0; + var timedOut = 0; + + // add submitting element to data if we know it + var sub = form.clk; + if (sub) { + var n = sub.name; + if (n && !sub.disabled) { + options.extraData = options.extraData || {}; + options.extraData[n] = sub.value; + if (sub.type == "image") { + options.extraData[name+'.x'] = form.clk_x; + options.extraData[name+'.y'] = form.clk_y; + } + } + } + + // take a breath so that pending repaints get some cpu time before the upload starts + setTimeout(function() { + // make sure form attrs are set + var t = $form.attr('target'), a = $form.attr('action'); + + // update form attrs in IE friendly way + form.setAttribute('target',id); + if (form.getAttribute('method') != 'POST') + form.setAttribute('method', 'POST'); + if (form.getAttribute('action') != opts.url) + form.setAttribute('action', opts.url); + + // ie borks in some cases when setting encoding + if (! options.skipEncodingOverride) { + $form.attr({ + encoding: 'multipart/form-data', + enctype: 'multipart/form-data' + }); + } + + // support timout + if (opts.timeout) + setTimeout(function() { timedOut = true; cb(); }, opts.timeout); + + // add "extra" data to form if provided in options + var extraInputs = []; + try { + if (options.extraData) + for (var n in options.extraData) + extraInputs.push( + $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />') + .appendTo(form)[0]); + + // add iframe to doc and submit the form + $io.appendTo('body'); + io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false); + form.submit(); + } + finally { + // reset attrs and remove "extra" input elements + form.setAttribute('action',a); + t ? form.setAttribute('target', t) : $form.removeAttr('target'); + $(extraInputs).remove(); + } + }, 10); + + var domCheckCount = 50; + + function cb() { + if (cbInvoked++) return; + + io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false); + + var ok = true; + try { + if (timedOut) throw 'timeout'; + // extract the server response from the iframe + var data, doc; + + doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document; + + var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc); + log('isXml='+isXml); + if (!isXml && (doc.body == null || doc.body.innerHTML == '')) { + if (--domCheckCount) { + // in some browsers (Opera) the iframe DOM is not always traversable when + // the onload callback fires, so we loop a bit to accommodate + cbInvoked = 0; + setTimeout(cb, 100); + return; + } + log('Could not access iframe DOM after 50 tries.'); + return; + } + + xhr.responseText = doc.body ? doc.body.innerHTML : null; + xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc; + xhr.getResponseHeader = function(header){ + var headers = {'content-type': opts.dataType}; + return headers[header]; + }; + + if (opts.dataType == 'json' || opts.dataType == 'script') { + // see if user embedded response in textarea + var ta = doc.getElementsByTagName('textarea')[0]; + if (ta) + xhr.responseText = ta.value; + else { + // account for browsers injecting pre around json response + var pre = doc.getElementsByTagName('pre')[0]; + if (pre) + xhr.responseText = pre.innerHTML; + } + } + else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) { + xhr.responseXML = toXml(xhr.responseText); + } + data = $.httpData(xhr, opts.dataType); + } + catch(e){ + ok = false; + $.handleError(opts, xhr, 'error', e); + } + + // ordering of these callbacks/triggers is odd, but that's how $.ajax does it + if (ok) { + opts.success(data, 'success'); + if (g) $.event.trigger("ajaxSuccess", [xhr, opts]); + } + if (g) $.event.trigger("ajaxComplete", [xhr, opts]); + if (g && ! --$.active) $.event.trigger("ajaxStop"); + if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error'); + + // clean up + setTimeout(function() { + $io.remove(); + xhr.responseXML = null; + }, 100); + }; + + function toXml(s, doc) { + if (window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + doc.loadXML(s); + } + else + doc = (new DOMParser()).parseFromString(s, 'text/xml'); + return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null; + }; + }; }; /** @@ -340,9 +365,9 @@ $.fn.ajaxSubmit = function(options) { * The advantages of using this method instead of ajaxSubmit() are: * * 1: This method will include coordinates for <input type="image" /> elements (if the element - * is used to submit the form). + * is used to submit the form). * 2. This method will include the submit element's name/value data (for the element that was - * used to submit the form). + * used to submit the form). * 3. This method binds the submit() method to the form for you. * * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely @@ -350,40 +375,42 @@ $.fn.ajaxSubmit = function(options) { * the form itself. */ $.fn.ajaxForm = function(options) { - return this.ajaxFormUnbind().bind('submit.form-plugin',function() { - $(this).ajaxSubmit(options); - return false; - }).each(function() { - // store options in hash - $(":submit,input:image", this).bind('click.form-plugin',function(e) { - var form = this.form; - form.clk = this; - if (this.type == 'image') { - if (e.offsetX != undefined) { - form.clk_x = e.offsetX; - form.clk_y = e.offsetY; - } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin - var offset = $(this).offset(); - form.clk_x = e.pageX - offset.left; - form.clk_y = e.pageY - offset.top; - } else { - form.clk_x = e.pageX - this.offsetLeft; - form.clk_y = e.pageY - this.offsetTop; - } - } - // clear form vars - setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10); - }); - }); + return this.ajaxFormUnbind().bind('submit.form-plugin', function() { + $(this).ajaxSubmit(options); + return false; + }).bind('click.form-plugin', function(e) { + var target = e.target; + var $el = $(target); + if (!($el.is(":submit,input:image"))) { + // is this a child element of the submit el? (ex: a span within a button) + var t = $el.closest(':submit'); + if (t.length == 0) + return; + target = t[0]; + } + var form = this; + form.clk = target; + if (target.type == 'image') { + if (e.offsetX != undefined) { + form.clk_x = e.offsetX; + form.clk_y = e.offsetY; + } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin + var offset = $el.offset(); + form.clk_x = e.pageX - offset.left; + form.clk_y = e.pageY - offset.top; + } else { + form.clk_x = e.pageX - target.offsetLeft; + form.clk_y = e.pageY - target.offsetTop; + } + } + // clear form vars + setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100); + }); }; // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm $.fn.ajaxFormUnbind = function() { - this.unbind('submit.form-plugin'); - return this.each(function() { - $(":submit,input:image", this).unbind('click.form-plugin'); - }); - + return this.unbind('submit.form-plugin click.form-plugin'); }; /** @@ -398,44 +425,44 @@ $.fn.ajaxFormUnbind = function() { * ajaxSubmit() and ajaxForm() methods. */ $.fn.formToArray = function(semantic) { - var a = []; - if (this.length == 0) return a; - - var form = this[0]; - var els = semantic ? form.getElementsByTagName('*') : form.elements; - if (!els) return a; - for(var i=0, max=els.length; i < max; i++) { - var el = els[i]; - var n = el.name; - if (!n) continue; - - if (semantic && form.clk && el.type == "image") { - // handle image inputs on the fly when semantic == true - if(!el.disabled && form.clk == el) - a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); - continue; - } - - var v = $.fieldValue(el, true); - if (v && v.constructor == Array) { - for(var j=0, jmax=v.length; j < jmax; j++) - a.push({name: n, value: v[j]}); - } - else if (v !== null && typeof v != 'undefined') - a.push({name: n, value: v}); - } - - if (!semantic && form.clk) { - // input type=='image' are not found in elements array! handle them here - var inputs = form.getElementsByTagName("input"); - for(var i=0, max=inputs.length; i < max; i++) { - var input = inputs[i]; - var n = input.name; - if(n && !input.disabled && input.type == "image" && form.clk == input) - a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); - } - } - return a; + var a = []; + if (this.length == 0) return a; + + var form = this[0]; + var els = semantic ? form.getElementsByTagName('*') : form.elements; + if (!els) return a; + for(var i=0, max=els.length; i < max; i++) { + var el = els[i]; + var n = el.name; + if (!n) continue; + + if (semantic && form.clk && el.type == "image") { + // handle image inputs on the fly when semantic == true + if(!el.disabled && form.clk == el) { + a.push({name: n, value: $(el).val()}); + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + continue; + } + + var v = $.fieldValue(el, true); + if (v && v.constructor == Array) { + for(var j=0, jmax=v.length; j < jmax; j++) + a.push({name: n, value: v[j]}); + } + else if (v !== null && typeof v != 'undefined') + a.push({name: n, value: v}); + } + + if (!semantic && form.clk) { + // input type=='image' are not found in elements array! handle it here + var $input = $(form.clk), input = $input[0], n = input.name; + if (n && !input.disabled && input.type == 'image') { + a.push({name: n, value: $input.val()}); + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + } + return a; }; /** @@ -443,8 +470,8 @@ $.fn.formToArray = function(semantic) { * in the format: name1=value1&name2=value2 */ $.fn.formSerialize = function(semantic) { - //hand off to jQuery.param for proper encoding - return $.param(this.formToArray(semantic)); + //hand off to jQuery.param for proper encoding + return $.param(this.formToArray(semantic)); }; /** @@ -452,32 +479,32 @@ $.fn.formSerialize = function(semantic) { * This method will return a string in the format: name1=value1&name2=value2 */ $.fn.fieldSerialize = function(successful) { - var a = []; - this.each(function() { - var n = this.name; - if (!n) return; - var v = $.fieldValue(this, successful); - if (v && v.constructor == Array) { - for (var i=0,max=v.length; i < max; i++) - a.push({name: n, value: v[i]}); - } - else if (v !== null && typeof v != 'undefined') - a.push({name: this.name, value: v}); - }); - //hand off to jQuery.param for proper encoding - return $.param(a); + var a = []; + this.each(function() { + var n = this.name; + if (!n) return; + var v = $.fieldValue(this, successful); + if (v && v.constructor == Array) { + for (var i=0,max=v.length; i < max; i++) + a.push({name: n, value: v[i]}); + } + else if (v !== null && typeof v != 'undefined') + a.push({name: this.name, value: v}); + }); + //hand off to jQuery.param for proper encoding + return $.param(a); }; /** * Returns the value(s) of the element in the matched set. For example, consider the following form: * * <form><fieldset> - * <input name="A" type="text" /> - * <input name="A" type="text" /> - * <input name="B" type="checkbox" value="B1" /> - * <input name="B" type="checkbox" value="B2"/> - * <input name="C" type="radio" value="C1" /> - * <input name="C" type="radio" value="C2" /> + * <input name="A" type="text" /> + * <input name="A" type="text" /> + * <input name="B" type="checkbox" value="B1" /> + * <input name="B" type="checkbox" value="B2"/> + * <input name="C" type="radio" value="C1" /> + * <input name="C" type="radio" value="C2" /> * </fieldset></form> * * var v = $(':text').fieldValue(); @@ -504,50 +531,51 @@ $.fn.fieldSerialize = function(successful) { * for each element is returned. * * Note: This method *always* returns an array. If no valid value can be determined the - * array will be empty, otherwise it will contain one or more values. + * array will be empty, otherwise it will contain one or more values. */ $.fn.fieldValue = function(successful) { - for (var val=[], i=0, max=this.length; i < max; i++) { - var el = this[i]; - var v = $.fieldValue(el, successful); - if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) - continue; - v.constructor == Array ? $.merge(val, v) : val.push(v); - } - return val; + for (var val=[], i=0, max=this.length; i < max; i++) { + var el = this[i]; + var v = $.fieldValue(el, successful); + if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) + continue; + v.constructor == Array ? $.merge(val, v) : val.push(v); + } + return val; }; /** * Returns the value of the field element. */ $.fieldValue = function(el, successful) { - var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); - if (typeof successful == 'undefined') successful = true; - - if (successful && (!n || el.disabled || t == 'reset' || t == 'button' || - (t == 'checkbox' || t == 'radio') && !el.checked || - (t == 'submit' || t == 'image') && el.form && el.form.clk != el || - tag == 'select' && el.selectedIndex == -1)) - return null; - - if (tag == 'select') { - var index = el.selectedIndex; - if (index < 0) return null; - var a = [], ops = el.options; - var one = (t == 'select-one'); - var max = (one ? index+1 : ops.length); - for(var i=(one ? index : 0); i < max; i++) { - var op = ops[i]; - if (op.selected) { - // extra pain for IE... - var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value; - if (one) return v; - a.push(v); - } - } - return a; - } - return el.value; + var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); + if (typeof successful == 'undefined') successful = true; + + if (successful && (!n || el.disabled || t == 'reset' || t == 'button' || + (t == 'checkbox' || t == 'radio') && !el.checked || + (t == 'submit' || t == 'image') && el.form && el.form.clk != el || + tag == 'select' && el.selectedIndex == -1)) + return null; + + if (tag == 'select') { + var index = el.selectedIndex; + if (index < 0) return null; + var a = [], ops = el.options; + var one = (t == 'select-one'); + var max = (one ? index+1 : ops.length); + for(var i=(one ? index : 0); i < max; i++) { + var op = ops[i]; + if (op.selected) { + var v = op.value; + if (!v) // extra pain for IE... + v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value; + if (one) return v; + a.push(v); + } + } + return a; + } + return el.value; }; /** @@ -559,46 +587,46 @@ $.fieldValue = function(el, successful) { * - button elements will *not* be effected */ $.fn.clearForm = function() { - return this.each(function() { - $('input,select,textarea', this).clearFields(); - }); + return this.each(function() { + $('input,select,textarea', this).clearFields(); + }); }; /** * Clears the selected form elements. */ $.fn.clearFields = $.fn.clearInputs = function() { - return this.each(function() { - var t = this.type, tag = this.tagName.toLowerCase(); - if (t == 'text' || t == 'password' || tag == 'textarea') - this.value = ''; - else if (t == 'checkbox' || t == 'radio') - this.checked = false; - else if (tag == 'select') - this.selectedIndex = -1; - }); + return this.each(function() { + var t = this.type, tag = this.tagName.toLowerCase(); + if (t == 'text' || t == 'password' || tag == 'textarea') + this.value = ''; + else if (t == 'checkbox' || t == 'radio') + this.checked = false; + else if (tag == 'select') + this.selectedIndex = -1; + }); }; /** * Resets the form data. Causes all form elements to be reset to their original value. */ $.fn.resetForm = function() { - return this.each(function() { - // guard against an input with the name of 'reset' - // note that IE reports the reset function as an 'object' - if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) - this.reset(); - }); + return this.each(function() { + // guard against an input with the name of 'reset' + // note that IE reports the reset function as an 'object' + if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) + this.reset(); + }); }; /** * Enables or disables any matching elements. */ $.fn.enable = function(b) { - if (b == undefined) b = true; - return this.each(function() { - this.disabled = !b - }); + if (b == undefined) b = true; + return this.each(function() { + this.disabled = !b; + }); }; /** @@ -606,27 +634,27 @@ $.fn.enable = function(b) { * selects/deselects and matching option elements. */ $.fn.selected = function(select) { - if (select == undefined) select = true; - return this.each(function() { - var t = this.type; - if (t == 'checkbox' || t == 'radio') - this.checked = select; - else if (this.tagName.toLowerCase() == 'option') { - var $sel = $(this).parent('select'); - if (select && $sel[0] && $sel[0].type == 'select-one') { - // deselect all other options - $sel.find('option').selected(false); - } - this.selected = select; - } - }); + if (select == undefined) select = true; + return this.each(function() { + var t = this.type; + if (t == 'checkbox' || t == 'radio') + this.checked = select; + else if (this.tagName.toLowerCase() == 'option') { + var $sel = $(this).parent('select'); + if (select && $sel[0] && $sel[0].type == 'select-one') { + // deselect all other options + $sel.find('option').selected(false); + } + this.selected = select; + } + }); }; // helper fn for console logging // set $.fn.ajaxSubmit.debug to true to enable debug logging function log() { - if ($.fn.ajaxSubmit.debug && window.console && window.console.log) - window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,'')); + if ($.fn.ajaxSubmit.debug && window.console && window.console.log) + window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,'')); }; -})(jQuery);
\ No newline at end of file +})(jQuery); diff --git a/share/www/script/jquery.js b/share/www/script/jquery.js index 92635743..fff67764 100644 --- a/share/www/script/jquery.js +++ b/share/www/script/jquery.js @@ -1,975 +1,1462 @@ /*! - * jQuery JavaScript Library v1.3.2 + * jQuery JavaScript Library v1.4.2 * http://jquery.com/ * - * Copyright (c) 2009 John Resig - * Dual licensed under the MIT and GPL licenses. - * http://docs.jquery.com/License + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license * - * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) - * Revision: 6246 + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Sat Feb 13 22:33:48 2010 -0500 */ -(function(){ +(function( window, undefined ) { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, -var - // Will speed up references to window, and allows munging its name. - window = this, - // Will speed up references to undefined, and allows munging its name. - undefined, // Map over jQuery in case of overwrite _jQuery = window.jQuery, + // Map over the $ in case of overwrite _$ = window.$, - jQuery = window.jQuery = window.$ = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context ); - }, + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + // A central reference to the root jQuery(document) + rootjQuery, // A simple way to check for HTML strings or ID strings // (both of which we optimize for) - quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, + quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector - isSimple = /^.[^:#\[\.,]*$/; + isSimple = /^.[^:#\[\.,]*$/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // Has the ready events already been bound? + readyBound = false, + + // The functions to execute on DOM ready + readyList = [], + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwnProperty = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + indexOf = Array.prototype.indexOf; jQuery.fn = jQuery.prototype = { init: function( selector, context ) { - // Make sure that a selection was provided - selector = selector || document; + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } // Handle $(DOMElement) if ( selector.nodeType ) { - this[0] = selector; + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; this.length = 1; - this.context = selector; return this; } + // Handle HTML strings if ( typeof selector === "string" ) { // Are we dealing with HTML string or an ID? - var match = quickExpr.exec( selector ); + match = quickExpr.exec( selector ); // Verify a match, and that no context was specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) - if ( match[1] ) - selector = jQuery.clean( [ match[1] ], context ); + if ( match[1] ) { + doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = buildFragment( [ match[1] ], [ doc ] ); + selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; + } + + return jQuery.merge( this, selector ); + // HANDLE: $("#id") - else { - var elem = document.getElementById( match[3] ); - - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem && elem.id != match[3] ) - return jQuery().find( selector ); - - // Otherwise, we inject the element directly into the jQuery object - var ret = jQuery( elem || [] ); - ret.context = document; - ret.selector = selector; - return ret; + } else { + elem = document.getElementById( match[2] ); + + if ( elem ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; } - // HANDLE: $(expr, [context]) - // (which is just equivalent to: $(content).find(expr) - } else + // HANDLE: $("TAG") + } else if ( !context && /^\w+$/.test( selector ) ) { + this.selector = selector; + this.context = document; + selector = document.getElementsByTagName( selector ); + return jQuery.merge( this, selector ); + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return (context || rootjQuery).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { return jQuery( context ).find( selector ); + } // HANDLE: $(function) // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) - return jQuery( document ).ready( selector ); + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } - // Make sure that old selector state is passed along - if ( selector.selector && selector.context ) { + if (selector.selector !== undefined) { this.selector = selector.selector; this.context = selector.context; } - return this.setArray(jQuery.isArray( selector ) ? - selector : - jQuery.makeArray(selector)); + return jQuery.makeArray( selector, this ); }, // Start with an empty selector selector: "", // The current version of jQuery being used - jquery: "1.3.2", + jquery: "1.4.2", + + // The default length of a jQuery object is 0 + length: 0, // The number of elements contained in the matched element set size: function() { return this.length; }, + toArray: function() { + return slice.call( this, 0 ); + }, + // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { - return num === undefined ? + return num == null ? // Return a 'clean' array - Array.prototype.slice.call( this ) : + this.toArray() : // Return just the object - this[ num ]; + ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set - var ret = jQuery( elems ); + var ret = jQuery(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } // Add the old object onto the stack (as a reference) ret.prevObject = this; ret.context = this.context; - if ( name === "find" ) + if ( name === "find" ) { ret.selector = this.selector + (this.selector ? " " : "") + selector; - else if ( name ) + } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; + } // Return the newly-formed element set return ret; }, - // Force the current matched set of elements to become - // the specified array of elements (destroying the stack in the process) - // You should use pushStack() in order to do this, but maintain the stack - setArray: function( elems ) { - // Resetting the length to 0, then using the native Array push - // is a super-fast way to populate an object with array-like properties - this.length = 0; - Array.prototype.push.apply( this, elems ); - - return this; - }, - // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) each: function( callback, args ) { return jQuery.each( this, callback, args ); }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem && elem.jquery ? elem[0] : elem - , this ); - }, + // If the DOM is already ready + if ( jQuery.isReady ) { + // Execute the function immediately + fn.call( document, jQuery ); - attr: function( name, value, type ) { - var options = name; + // Otherwise, remember the function for later + } else if ( readyList ) { + // Add the function to the wait list + readyList.push( fn ); + } - // Look for the case where we're accessing a style value - if ( typeof name === "string" ) - if ( value === undefined ) - return this[0] && jQuery[ type || "attr" ]( this[0], name ); + return this; + }, + + eq: function( i ) { + return i === -1 ? + this.slice( i ) : + this.slice( i, +i + 1 ); + }, - else { - options = {}; - options[ name ] = value; - } + first: function() { + return this.eq( 0 ); + }, - // Check to see if we're setting style values - return this.each(function(i){ - // Set all the styles - for ( name in options ) - jQuery.attr( - type ? - this.style : - this, - name, jQuery.prop( this, options[ name ], type, i, name ) - ); - }); + last: function() { + return this.eq( -1 ); }, - css: function( key, value ) { - // ignore negative width and height values - if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) - value = undefined; - return this.attr( key, value, "curCSS" ); + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); }, - text: function( text ) { - if ( typeof text !== "object" && text != null ) - return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || jQuery(null); + }, - var ret = ""; + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; - jQuery.each( text || this, function(){ - jQuery.each( this.childNodes, function(){ - if ( this.nodeType != 8 ) - ret += this.nodeType != 1 ? - this.nodeValue : - jQuery.fn.text( [ this ] ); - }); - }); +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; - return ret; - }, +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; - wrapAll: function( html ) { - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).clone(); + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } - if ( this[0].parentNode ) - wrap.insertBefore( this[0] ); + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } - wrap.map(function(){ - var elem = this; + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } - while ( elem.firstChild ) - elem = elem.firstChild; + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; - return elem; - }).append(this); + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging object literal values or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) { + var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src + : jQuery.isArray(copy) ? [] : {}; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } } + } - return this; - }, + // Return the modified object + return target; +}; - wrapInner: function( html ) { - return this.each(function(){ - jQuery( this ).contents().wrapAll( html ); - }); +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) { + window.jQuery = _jQuery; + } + + return jQuery; }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // Handle when the DOM is ready + ready: function() { + // Make sure that the DOM is not already loaded + if ( !jQuery.isReady ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 13 ); + } - wrap: function( html ) { - return this.each(function(){ - jQuery( this ).wrapAll( html ); - }); + // Remember that the DOM is ready + jQuery.isReady = true; + + // If there are functions bound, to execute + if ( readyList ) { + // Execute all of them + var fn, i = 0; + while ( (fn = readyList[ i++ ]) ) { + fn.call( document, jQuery ); + } + + // Reset the list of functions + readyList = null; + } + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + } + } }, + + bindReady: function() { + if ( readyBound ) { + return; + } - append: function() { - return this.domManip(arguments, true, function(elem){ - if (this.nodeType == 1) - this.appendChild( elem ); - }); + readyBound = true; + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + return jQuery.ready(); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } }, - prepend: function() { - return this.domManip(arguments, true, function(elem){ - if (this.nodeType == 1) - this.insertBefore( elem, this.firstChild ); - }); + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return toString.call(obj) === "[object Function]"; }, - before: function() { - return this.domManip(arguments, false, function(elem){ - this.parentNode.insertBefore( elem, this ); - }); + isArray: function( obj ) { + return toString.call(obj) === "[object Array]"; }, - after: function() { - return this.domManip(arguments, false, function(elem){ - this.parentNode.insertBefore( elem, this.nextSibling ); - }); + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor + && !hasOwnProperty.call(obj, "constructor") + && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwnProperty.call( obj, key ); }, - end: function() { - return this.prevObject || jQuery( [] ); + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw msg; }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: [].push, - sort: [].sort, - splice: [].splice, + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@") + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]") + .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) { + + // Try to use the native JSON parser first + return window.JSON && window.JSON.parse ? + window.JSON.parse( data ) : + (new Function("return " + data))(); - find: function( selector ) { - if ( this.length === 1 ) { - var ret = this.pushStack( [], "find", selector ); - ret.length = 0; - jQuery.find( selector, this[0], ret ); - return ret; } else { - return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ - return jQuery.find( selector, elem ); - })), "find", selector ); + jQuery.error( "Invalid JSON: " + data ); } }, - clone: function( events ) { - // Do the clone - var ret = this.map(function(){ - if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { - // IE copies events bound via attachEvent when - // using cloneNode. Calling detachEvent on the - // clone will also remove the events from the orignal - // In order to get around this, we use innerHTML. - // Unfortunately, this means some modifications to - // attributes in IE that are actually only stored - // as properties will not be copied (such as the - // the name attribute on an input). - var html = this.outerHTML; - if ( !html ) { - var div = this.ownerDocument.createElement("div"); - div.appendChild( this.cloneNode(true) ); - html = div.innerHTML; - } + noop: function() {}, - return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; - } else - return this.cloneNode(true); - }); + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && rnotwhite.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); - // Copy the events from the original to the clone - if ( events === true ) { - var orig = this.find("*").andSelf(), i = 0; + script.type = "text/javascript"; - ret.find("*").andSelf().each(function(){ - if ( this.nodeName !== orig[i].nodeName ) - return; + if ( jQuery.support.scriptEval ) { + script.appendChild( document.createTextNode( data ) ); + } else { + script.text = data; + } + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, - var events = jQuery.data( orig[i], "events" ); + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction(object); - for ( var type in events ) { - for ( var handler in events[ type ] ) { - jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; } } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } - i++; - }); + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + } } - // Return the cloned set - return ret; + return object; }, - filter: function( selector ) { - return this.pushStack( - jQuery.isFunction( selector ) && - jQuery.grep(this, function(elem, i){ - return selector.call( elem, i ); - }) || - - jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ - return elem.nodeType === 1; - }) ), "filter", selector ); - }, - - closest: function( selector ) { - var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, - closer = 0; - - return this.map(function(){ - var cur = this; - while ( cur && cur.ownerDocument ) { - if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { - jQuery.data(cur, "closest", closer); - return cur; - } - cur = cur.parentNode; - closer++; - } - }); + trim: function( text ) { + return (text || "").replace( rtrim, "" ); }, - not: function( selector ) { - if ( typeof selector === "string" ) - // test special case where just one selector is passed in - if ( isSimple.test( selector ) ) - return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); - else - selector = jQuery.multiFilter( selector, this ); - - var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; - return this.filter(function() { - return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; - }); - }, + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; - add: function( selector ) { - return this.pushStack( jQuery.unique( jQuery.merge( - this.get(), - typeof selector === "string" ? - jQuery( selector ) : - jQuery.makeArray( selector ) - ))); - }, + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // The extra typeof function check is to prevent crashes + // in Safari 2 (See: #3039) + if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } - is: function( selector ) { - return !!selector && jQuery.multiFilter( selector, this ).length > 0; + return ret; }, - hasClass: function( selector ) { - return !!selector && this.is( "." + selector ); + inArray: function( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; }, - val: function( value ) { - if ( value === undefined ) { - var elem = this[0]; + merge: function( first, second ) { + var i = first.length, j = 0; - if ( elem ) { - if( jQuery.nodeName( elem, 'option' ) ) - return (elem.attributes.value || {}).specified ? elem.value : elem.text; - - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type == "select-one"; + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } - // Nothing was selected - if ( index < 0 ) - return null; + first.length = i; - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; + return first; + }, - if ( option.selected ) { - // Get the specifc value for the option - value = jQuery(option).val(); + grep: function( elems, callback, inv ) { + var ret = []; - // We don't need an array for one selects - if ( one ) - return value; + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + if ( !inv !== !callback( elems[ i ], i ) ) { + ret.push( elems[ i ] ); + } + } - // Multi-Selects return an array - values.push( value ); - } - } + return ret; + }, - return values; - } + // arg is for internal usage only + map: function( elems, callback, arg ) { + var ret = [], value; - // Everything else, we just grab the value - return (elem.value || "").replace(/\r/g, ""); + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, length = elems.length; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + if ( value != null ) { + ret[ ret.length ] = value; } - - return undefined; } - if ( typeof value === "number" ) - value += ''; + return ret.concat.apply( [], ret ); + }, - return this.each(function(){ - if ( this.nodeType != 1 ) - return; + // A global GUID counter for objects + guid: 1, - if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) - this.checked = (jQuery.inArray(this.value, value) >= 0 || - jQuery.inArray(this.name, value) >= 0); + proxy: function( fn, proxy, thisObject ) { + if ( arguments.length === 2 ) { + if ( typeof proxy === "string" ) { + thisObject = fn; + fn = thisObject[ proxy ]; + proxy = undefined; - else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(value); + } else if ( proxy && !jQuery.isFunction( proxy ) ) { + thisObject = proxy; + proxy = undefined; + } + } - jQuery( "option", this ).each(function(){ - this.selected = (jQuery.inArray( this.value, values ) >= 0 || - jQuery.inArray( this.text, values ) >= 0); - }); + if ( !proxy && fn ) { + proxy = function() { + return fn.apply( thisObject || this, arguments ); + }; + } - if ( !values.length ) - this.selectedIndex = -1; + // Set the guid of unique handler to the same of original handler, so it can be removed + if ( fn ) { + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + } - } else - this.value = value; - }); + // So proxy can be declared as an argument + return proxy; }, - html: function( value ) { - return value === undefined ? - (this[0] ? - this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : - null) : - this.empty().append( value ); - }, + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); - replaceWith: function( value ) { - return this.after( value ).remove(); - }, + var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) || + []; - eq: function( i ) { - return this.slice( i, +i + 1 ); + return { browser: match[1] || "", version: match[2] || "0" }; }, - slice: function() { - return this.pushStack( Array.prototype.slice.apply( this, arguments ), - "slice", Array.prototype.slice.call(arguments).join(",") ); - }, + browser: {} +}); - map: function( callback ) { - return this.pushStack( jQuery.map(this, function(elem, i){ - return callback.call( elem, i, elem ); - })); - }, +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} - andSelf: function() { - return this.add( this.prevObject ); - }, +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} - domManip: function( args, table, callback ) { - if ( this[0] ) { - var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), - scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), - first = fragment.firstChild; +if ( indexOf ) { + jQuery.inArray = function( elem, array ) { + return indexOf.call( array, elem ); + }; +} - if ( first ) - for ( var i = 0, l = this.length; i < l; i++ ) - callback.call( root(this[i], first), this.length > 1 || i > 0 ? - fragment.cloneNode(true) : fragment ); - - if ( scripts ) - jQuery.each( scripts, evalScript ); - } +// All jQuery objects should point back to these +rootjQuery = jQuery(document); - return this; - - function root( elem, cur ) { - return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? - (elem.getElementsByTagName("tbody")[0] || - elem.appendChild(elem.ownerDocument.createElement("tbody"))) : - elem; +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; } -}; -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch( error ) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} function evalScript( i, elem ) { - if ( elem.src ) + if ( elem.src ) { jQuery.ajax({ url: elem.src, async: false, dataType: "script" }); - - else + } else { jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + } - if ( elem.parentNode ) + if ( elem.parentNode ) { elem.parentNode.removeChild( elem ); + } } -function now(){ - return +new Date; +// Mutifunctional method to get and set values to a collection +// The value/s can be optionally by executed if its a function +function access( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; } -jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; +function now() { + return (new Date).getTime(); +} +(function() { - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; + jQuery.support = {}; + + var root = document.documentElement, + script = document.createElement("script"), + div = document.createElement("div"), + id = "script" + now(); + + div.style.display = "none"; + div.innerHTML = " <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>"; + + var all = div.getElementsByTagName("*"), + a = div.getElementsByTagName("a")[0]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return; } - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) - target = {}; + jQuery.support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, - // extend jQuery itself if only one argument is passed - if ( length == i ) { - target = this; - --i; + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText insted) + style: /red/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55$/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: div.getElementsByTagName("input")[0].value === "on", + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected, + + parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null, + + // Will be defined later + deleteExpando: true, + checkClone: false, + scriptEval: false, + noCloneEvent: true, + boxModel: null + }; + + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} + + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support.scriptEval = true; + delete window[ id ]; } - for ( ; i < length; i++ ) - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) - // Extend the base object - for ( var name in options ) { - var src = target[ name ], copy = options[ name ]; + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete script.test; + + } catch(e) { + jQuery.support.deleteExpando = false; + } - // Prevent never-ending loop - if ( target === copy ) - continue; + root.removeChild( script ); - // Recurse if we're merging object values - if ( deep && copy && typeof copy === "object" && !copy.nodeType ) - target[ name ] = jQuery.extend( deep, - // Never move original objects, clone them - src || ( copy.length != null ? [ ] : { } ) - , copy ); + if ( div.attachEvent && div.fireEvent ) { + div.attachEvent("onclick", function click() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + jQuery.support.noCloneEvent = false; + div.detachEvent("onclick", click); + }); + div.cloneNode(true).fireEvent("onclick"); + } - // Don't bring in undefined values - else if ( copy !== undefined ) - target[ name ] = copy; + div = document.createElement("div"); + div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>"; - } + var fragment = document.createDocumentFragment(); + fragment.appendChild( div.firstChild ); - // Return the modified object - return target; -}; + // WebKit doesn't clone checked state correctly in fragments + jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; -// exclude the following css properties to add px -var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, - // cache defaultView - defaultView = document.defaultView || {}, - toString = Object.prototype.toString; + // Figure out if the W3C box model works as expected + // document.body must exist before we can do this + jQuery(function() { + var div = document.createElement("div"); + div.style.width = div.style.paddingLeft = "1px"; -jQuery.extend({ - noConflict: function( deep ) { - window.$ = _$; + document.body.appendChild( div ); + jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + document.body.removeChild( div ).style.display = 'none'; - if ( deep ) - window.jQuery = _jQuery; + div = null; + }); - return jQuery; - }, + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return toString.call(obj) === "[object Function]"; - }, + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; - isArray: function( obj ) { - return toString.call(obj) === "[object Array]"; - }, + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); - // check if an element is in a (or is an) XML document - isXMLDoc: function( elem ) { - return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || - !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); + // release memory in IE + root = script = div = all = a = null; +})(); + +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; +var expando = "jQuery" + now(), uuid = 0, windowData = {}; + +jQuery.extend({ + cache: {}, + + expando:expando, + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + "object": true, + "applet": true }, - // Evalulates a script in a global context - globalEval: function( data ) { - if ( data && /\S/.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); + data: function( elem, name, data ) { + if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { + return; + } - script.type = "text/javascript"; - if ( jQuery.support.scriptEval ) - script.appendChild( document.createTextNode( data ) ); - else - script.text = data; + elem = elem == window ? + windowData : + elem; - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); + var id = elem[ expando ], cache = jQuery.cache, thisCache; + + if ( !id && typeof name === "string" && data === undefined ) { + return null; } - }, - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); - }, + // Compute a unique ID for the element + if ( !id ) { + id = ++uuid; + } - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, length = object.length; + // Avoid generating a new cache unless none exists and we + // want to manipulate it. + if ( typeof name === "object" ) { + elem[ expando ] = id; + thisCache = cache[ id ] = jQuery.extend(true, {}, name); - if ( args ) { - if ( length === undefined ) { - for ( name in object ) - if ( callback.apply( object[ name ], args ) === false ) - break; - } else - for ( ; i < length; ) - if ( callback.apply( object[ i++ ], args ) === false ) - break; + } else if ( !cache[ id ] ) { + elem[ expando ] = id; + cache[ id ] = {}; + } - // A special, fast, case for the most common use of each - } else { - if ( length === undefined ) { - for ( name in object ) - if ( callback.call( object[ name ], name, object[ name ] ) === false ) - break; - } else - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + thisCache = cache[ id ]; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) { + thisCache[ name ] = data; } - return object; + return typeof name === "string" ? thisCache[ name ] : thisCache; }, - prop: function( elem, value, type, i, name ) { - // Handle executable functions - if ( jQuery.isFunction( value ) ) - value = value.call( elem, i ); + removeData: function( elem, name ) { + if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { + return; + } + + elem = elem == window ? + windowData : + elem; - // Handle passing in a number to a CSS property - return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? - value + "px" : - value; - }, + var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( thisCache ) { + // Remove the section of cache data + delete thisCache[ name ]; + + // If we've removed all the data, remove the element's cache + if ( jQuery.isEmptyObject(thisCache) ) { + jQuery.removeData( elem ); + } + } + + // Otherwise, we want to remove all of the element's data + } else { + if ( jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } - className: { - // internal only, use addClass("class") - add: function( elem, classNames ) { - jQuery.each((classNames || "").split(/\s+/), function(i, className){ - if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) - elem.className += (elem.className ? " " : "") + className; + // Completely remove the data cache + delete cache[ id ]; + } + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + if ( typeof key === "undefined" && this.length ) { + return jQuery.data( this[0] ); + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); }); - }, + } - // internal only, use removeClass("class") - remove: function( elem, classNames ) { - if (elem.nodeType == 1) - elem.className = classNames !== undefined ? - jQuery.grep(elem.className.split(/\s+/), function(className){ - return !jQuery.className.has( classNames, className ); - }).join(" ") : - ""; - }, + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - // internal only, use hasClass("class") - has: function( elem, className ) { - return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + } + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } else { + return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() { + jQuery.data( this, key, value ); + }); } }, - // A method for quickly swapping in/out CSS properties to get correct calculations - swap: function( elem, options, callback ) { - var old = {}; - // Remember the old values, and insert the new ones - for ( var name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); +jQuery.extend({ + queue: function( elem, type, data ) { + if ( !elem ) { + return; } - callback.call( elem ); + type = (type || "fx") + "queue"; + var q = jQuery.data( elem, type ); - // Revert the old values - for ( var name in options ) - elem.style[ name ] = old[ name ]; + // Speed up dequeue by getting out quickly if this is just a lookup + if ( !data ) { + return q || []; + } + + if ( !q || jQuery.isArray(data) ) { + q = jQuery.data( elem, type, jQuery.makeArray(data) ); + + } else { + q.push( data ); + } + + return q; }, - css: function( elem, name, force, extra ) { - if ( name == "width" || name == "height" ) { - var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + dequeue: function( elem, type ) { + type = type || "fx"; - function getWH() { - val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + var queue = jQuery.queue( elem, type ), fn = queue.shift(); - if ( extra === "border" ) - return; + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } - jQuery.each( which, function() { - if ( !extra ) - val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; - if ( extra === "margin" ) - val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; - else - val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; - }); + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift("inprogress"); } - if ( elem.offsetWidth !== 0 ) - getWH(); - else - jQuery.swap( elem, props, getWH ); + fn.call(elem, function() { + jQuery.dequeue(elem, type); + }); + } + } +}); - return Math.max(0, Math.round(val)); +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; } - return jQuery.curCSS( elem, name, force ); - }, + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function( i, elem ) { + var queue = jQuery.queue( this, type, data ); - curCSS: function( elem, name, force ) { - var ret, style = elem.style; + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, - // We need to handle opacity special in IE - if ( name == "opacity" && !jQuery.support.opacity ) { - ret = jQuery.attr( style, "opacity" ); + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + type = type || "fx"; + + return this.queue( type, function() { + var elem = this; + setTimeout(function() { + jQuery.dequeue( elem, type ); + }, time ); + }); + }, - return ret == "" ? - "1" : - ret; - } + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + } +}); +var rclass = /[\n\t]/g, + rspace = /\s+/, + rreturn = /\r/g, + rspecialurl = /href|src|style/, + rtype = /(button|input)/i, + rfocusable = /(button|input|object|select|textarea)/i, + rclickable = /^(a|area)$/i, + rradiocheck = /radio|checkbox/; - // Make sure we're using the right name for getting the float value - if ( name.match( /float/i ) ) - name = styleFloat; +jQuery.fn.extend({ + attr: function( name, value ) { + return access( this, name, value, true, jQuery.attr ); + }, - if ( !force && style && style[ name ] ) - ret = style[ name ]; + removeAttr: function( name, fn ) { + return this.each(function(){ + jQuery.attr( this, name, "" ); + if ( this.nodeType === 1 ) { + this.removeAttribute( name ); + } + }); + }, - else if ( defaultView.getComputedStyle ) { + addClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.addClass( value.call(this, i, self.attr("class")) ); + }); + } - // Only "float" is needed here - if ( name.match( /float/i ) ) - name = "float"; + if ( value && typeof value === "string" ) { + var classNames = (value || "").split( rspace ); - name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; - var computedStyle = defaultView.getComputedStyle( elem, null ); + if ( elem.nodeType === 1 ) { + if ( !elem.className ) { + elem.className = value; - if ( computedStyle ) - ret = computedStyle.getPropertyValue( name ); + } else { + var className = " " + elem.className + " ", setClass = elem.className; + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { + setClass += " " + classNames[c]; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } - // We should always get a number back from opacity - if ( name == "opacity" && ret == "" ) - ret = "1"; + return this; + }, - } else if ( elem.currentStyle ) { - var camelCase = name.replace(/\-(\w)/g, function(all, letter){ - return letter.toUpperCase(); + removeClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.removeClass( value.call(this, i, self.attr("class")) ); }); + } - ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + if ( (value && typeof value === "string") || value === undefined ) { + var classNames = (value || "").split(rspace); - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { - // Remember the original values - var left = style.left, rsLeft = elem.runtimeStyle.left; - - // Put in the new values to get a computed value out - elem.runtimeStyle.left = elem.currentStyle.left; - style.left = ret || 0; - ret = style.pixelLeft + "px"; + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + var className = (" " + elem.className + " ").replace(rclass, " "); + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[c] + " ", " "); + } + elem.className = jQuery.trim( className ); - // Revert the changed values - style.left = left; - elem.runtimeStyle.left = rsLeft; + } else { + elem.className = ""; + } + } } } - return ret; + return this; }, - clean: function( elems, context, fragment ) { - context = context || document; - - // !context.createElement fails in IE with an error but returns typeof 'object' - if ( typeof context.createElement === "undefined" ) - context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + toggleClass: function( value, stateVal ) { + var type = typeof value, isBool = typeof stateVal === "boolean"; - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { - var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); - if ( match ) - return [ context.createElement( match[1] ) ]; + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this); + self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + }); } - var ret = [], scripts = [], div = context.createElement("div"); + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, i = 0, self = jQuery(this), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } - jQuery.each(elems, function(i, elem){ - if ( typeof elem === "number" ) - elem += ''; + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery.data( this, "__className__", this.className ); + } - if ( !elem ) - return; + // toggle whole className + this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + } + }); + }, - // Convert html string into DOM nodes - if ( typeof elem === "string" ) { - // Fix "XHTML"-style tags in all browsers - elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ - return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? - all : - front + "></" + tag + ">"; - }); + hasClass: function( selector ) { + var className = " " + selector + " "; + for ( var i = 0, l = this.length; i < l; i++ ) { + if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } - // Trim whitespace, otherwise indexOf won't work as expected - var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); + return false; + }, - var wrap = - // option or optgroup - !tags.indexOf("<opt") && - [ 1, "<select multiple='multiple'>", "</select>" ] || + val: function( value ) { + if ( value === undefined ) { + var elem = this[0]; - !tags.indexOf("<leg") && - [ 1, "<fieldset>", "</fieldset>" ] || + if ( elem ) { + if ( jQuery.nodeName( elem, "option" ) ) { + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + } - tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && - [ 1, "<table>", "</table>" ] || + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; - !tags.indexOf("<tr") && - [ 2, "<table><tbody>", "</tbody></table>" ] || + // Nothing was selected + if ( index < 0 ) { + return null; + } - // <thead> matched above - (!tags.indexOf("<td") || !tags.indexOf("<th")) && - [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] || + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; - !tags.indexOf("<col") && - [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] || + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery(option).val(); - // IE can't serialize <link> and <script> tags normally - !jQuery.support.htmlSerialize && - [ 1, "div<div>", "</div>" ] || + // We don't need an array for one selects + if ( one ) { + return value; + } - [ 0, "", "" ]; + // Multi-Selects return an array + values.push( value ); + } + } - // Go to html and back, then peel off extra wrappers - div.innerHTML = wrap[1] + elem + wrap[2]; + return values; + } - // Move to the right depth - while ( wrap[0]-- ) - div = div.lastChild; + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { + return elem.getAttribute("value") === null ? "on" : elem.value; + } + - // Remove IE's autoinserted <tbody> from table fragments - if ( !jQuery.support.tbody ) { + // Everything else, we just grab the value + return (elem.value || "").replace(rreturn, ""); - // String was a <table>, *may* have spurious <tbody> - var hasBody = /<tbody/i.test(elem), - tbody = !tags.indexOf("<table") && !hasBody ? - div.firstChild && div.firstChild.childNodes : + } - // String was a bare <thead> or <tfoot> - wrap[1] == "<table>" && !hasBody ? - div.childNodes : - []; + return undefined; + } - for ( var j = tbody.length - 1; j >= 0 ; --j ) - if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) - tbody[ j ].parentNode.removeChild( tbody[ j ] ); + var isFunction = jQuery.isFunction(value); - } + return this.each(function(i) { + var self = jQuery(this), val = value; - // IE completely kills leading whitespace when innerHTML is used - if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) ) - div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild ); - - elem = jQuery.makeArray( div.childNodes ); + if ( this.nodeType !== 1 ) { + return; } - if ( elem.nodeType ) - ret.push( elem ); - else - ret = jQuery.merge( ret, elem ); + if ( isFunction ) { + val = value.call(this, i, self.val()); + } - }); + // Typecast each time if the value is a Function and the appended + // value is therefore different each time. + if ( typeof val === "number" ) { + val += ""; + } - if ( fragment ) { - for ( var i = 0; ret[i]; i++ ) { - if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { - scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); - } else { - if ( ret[i].nodeType === 1 ) - ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) ); - fragment.appendChild( ret[i] ); + if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { + this.checked = jQuery.inArray( self.val(), val ) >= 0; + + } else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(val); + + jQuery( "option", this ).each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + this.selectedIndex = -1; } + + } else { + this.value = val; } - - return scripts; - } + }); + } +}); - return ret; +jQuery.extend({ + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true }, - - attr: function( elem, name, value ) { + + attr: function( elem, name, value, pass ) { // don't set attributes on text and comment nodes - if (!elem || elem.nodeType == 3 || elem.nodeType == 8) + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { return undefined; + } - var notxml = !jQuery.isXMLDoc( elem ), + if ( pass && name in jQuery.attrFn ) { + return jQuery(elem)[name](value); + } + + var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), // Whether we are setting (or getting) set = value !== undefined; @@ -977,472 +1464,1209 @@ jQuery.extend({ name = notxml && jQuery.props[ name ] || name; // Only do all the following if this is a node (faster for style) - // IE elem.getAttribute passes even for style - if ( elem.tagName ) { - + if ( elem.nodeType === 1 ) { // These attributes require special treatment - var special = /href|src|style/.test( name ); + var special = rspecialurl.test( name ); - // Safari mis-reports the default selected property of a hidden option + // Safari mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it - if ( name == "selected" && elem.parentNode ) - elem.parentNode.selectedIndex; + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } // If applicable, access the attribute via the DOM 0 way if ( name in elem && notxml && !special ) { - if ( set ){ + if ( set ) { // We can't allow the type property to be changed (since it causes problems in IE) - if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode ) - throw "type property can't be changed"; + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } elem[ name ] = value; } // browsers index elements by id/name on forms, give priority to attributes. - if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { return elem.getAttributeNode( name ).nodeValue; + } // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name == "tabIndex" ) { + if ( name === "tabIndex" ) { var attributeNode = elem.getAttributeNode( "tabIndex" ); - return attributeNode && attributeNode.specified - ? attributeNode.value - : elem.nodeName.match(/(button|input|object|select|textarea)/i) - ? 0 - : elem.nodeName.match(/^(a|area)$/i) && elem.href - ? 0 - : undefined; + + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; } return elem[ name ]; } - if ( !jQuery.support.style && notxml && name == "style" ) - return jQuery.attr( elem.style, "cssText", value ); + if ( !jQuery.support.style && notxml && name === "style" ) { + if ( set ) { + elem.style.cssText = "" + value; + } + + return elem.style.cssText; + } - if ( set ) + if ( set ) { // convert the value to a string (all browsers do this but IE) see #1070 elem.setAttribute( name, "" + value ); + } - var attr = !jQuery.support.hrefNormalized && notxml && special + var attr = !jQuery.support.hrefNormalized && notxml && special ? // Some attributes require a special call on IE - ? elem.getAttribute( name, 2 ) - : elem.getAttribute( name ); + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined return attr === null ? undefined : attr; } // elem is actually elem.style ... set the style + // Using attr for specific style information is now deprecated. Use style instead. + return jQuery.style( elem, name, value ); + } +}); +var rnamespaces = /\.(.*)$/, + fcleanup = function( nm ) { + return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { + return "\\" + ch; + }); + }; - // IE uses filters for opacity - if ( !jQuery.support.opacity && name == "opacity" ) { - if ( set ) { - // IE has trouble with opacity if it does not have layout - // Force it by setting the zoom level - elem.zoom = 1; +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code originated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { - // Set the alpha filter to set the opacity - elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) + - (parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")"); - } + // Bind an event to an element + // Original by Dean Edwards + add: function( elem, types, handler, data ) { + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } - return elem.filter && elem.filter.indexOf("opacity=") >= 0 ? - (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '': - ""; + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) { + elem = window; } - name = name.replace(/-([a-z])/ig, function(all, letter){ - return letter.toUpperCase(); - }); + var handleObjIn, handleObj; - if ( set ) - elem[ name ] = value; + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } - return elem[ name ]; - }, + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } - trim: function( text ) { - return (text || "").replace( /^\s+|\s+$/g, "" ); - }, + // Init the element's event structure + var elemData = jQuery.data( elem ); - makeArray: function( array ) { - var ret = []; + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; + } - if( array != null ){ - var i = array.length; - // The window, strings (and functions) also have 'length' - if( i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval ) - ret[0] = array; - else - while( i ) - ret[--i] = array[i]; + var events = elemData.events = elemData.events || {}, + eventHandle = elemData.handle, eventHandle; + + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + return typeof jQuery !== "undefined" && !jQuery.event.triggered ? + jQuery.event.handle.apply( eventHandle.elem, arguments ) : + undefined; + }; } - return ret; - }, + // Add elem as a property of the handle function + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; - inArray: function( elem, array ) { - for ( var i = 0, length = array.length; i < length; i++ ) - // Use === because on IE, window == document - if ( array[ i ] === elem ) - return i; + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = types.split(" "); - return -1; + var type, i = 0, namespaces; + + while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + + // Namespaced event handlers + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); + + } else { + namespaces = []; + handleObj.namespace = ""; + } + + handleObj.type = type; + handleObj.guid = handler.guid; + + // Get the current list of functions bound to this event + var handlers = events[ type ], + special = jQuery.event.special[ type ] || {}; + + // Init the event handler queue + if ( !handlers ) { + handlers = events[ type ] = []; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add the function to the element's handler list + handlers.push( handleObj ); + + // Keep track of which events have been used, for global triggering + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; }, - merge: function( first, second ) { - // We have to loop this way because IE & Opera overwrite the length - // expando of getElementsByTagName - var i = 0, elem, pos = first.length; - // Also, we need to make sure that the correct elements are being returned - // (IE returns comment nodes in a '*' query) - if ( !jQuery.support.getAll ) { - while ( (elem = second[ i++ ]) != null ) - if ( elem.nodeType != 8 ) - first[ pos++ ] = elem; - - } else - while ( (elem = second[ i++ ]) != null ) - first[ pos++ ] = elem; + global: {}, - return first; + // Detach an event or set of events from an element + remove: function( elem, types, handler, pos ) { + // don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + elemData = jQuery.data( elem ), + events = elemData && elemData.events; + + if ( !elemData || !events ) { + return; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)") + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( var j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } + } + + continue; + } + + special = jQuery.event.special[ type ] || {}; + + for ( var j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); + } + + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + + if ( pos != null ) { + break; + } + } + } + + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + removeEvent( elem, type, elemData.handle ); + } + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem ); + } + } }, - unique: function( array ) { - var ret = [], done = {}; + // bubbling is internal + trigger: function( event, data, elem /*, bubbling */ ) { + // Event object or event type + var type = event.type || event, + bubbling = arguments[3]; + + if ( !bubbling ) { + event = typeof event === "object" ? + // jQuery.Event object + event[expando] ? event : + // Object literal + jQuery.extend( jQuery.Event(type), event ) : + // Just the event type (string) + jQuery.Event(type); + + if ( type.indexOf("!") >= 0 ) { + event.type = type = type.slice(0, -1); + event.exclusive = true; + } + + // Handle a global trigger + if ( !elem ) { + // Don't bubble custom events when global (to avoid too much overhead) + event.stopPropagation(); + + // Only trigger if we've ever bound an event for it + if ( jQuery.event.global[ type ] ) { + jQuery.each( jQuery.cache, function() { + if ( this.events && this.events[type] ) { + jQuery.event.trigger( event, data, this.handle.elem ); + } + }); + } + } + + // Handle triggering a single element + // don't do events on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + // Clean up in case it is reused + event.result = undefined; + event.target = elem; + + // Clone the incoming data, if any + data = jQuery.makeArray( data ); + data.unshift( event ); + } + + event.currentTarget = elem; + + // Trigger the event, it is assumed that "handle" is a function + var handle = jQuery.data( elem, "handle" ); + if ( handle ) { + handle.apply( elem, data ); + } + + var parent = elem.parentNode || elem.ownerDocument; + + // Trigger an inline bound script try { + if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { + if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { + event.result = false; + } + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (e) {} + + if ( !event.isPropagationStopped() && parent ) { + jQuery.event.trigger( event, data, parent, true ); + + } else if ( !event.isDefaultPrevented() ) { + var target = event.target, old, + isClick = jQuery.nodeName(target, "a") && type === "click", + special = jQuery.event.special[ type ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + + try { + if ( target[ type ] ) { + // Make sure that we don't accidentally re-trigger the onFOO events + old = target[ "on" + type ]; - for ( var i = 0, length = array.length; i < length; i++ ) { - var id = jQuery.data( array[ i ] ); + if ( old ) { + target[ "on" + type ] = null; + } + + jQuery.event.triggered = true; + target[ type ](); + } - if ( !done[ id ] ) { - done[ id ] = true; - ret.push( array[ i ] ); + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (e) {} + + if ( old ) { + target[ "on" + type ] = old; } + + jQuery.event.triggered = false; } + } + }, + + handle: function( event ) { + var all, handlers, namespaces, namespace, events; - } catch( e ) { - ret = array; + event = arguments[0] = jQuery.event.fix( event || window.event ); + event.currentTarget = this; + + // Namespaced event handlers + all = event.type.indexOf(".") < 0 && !event.exclusive; + + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); } - return ret; + var events = jQuery.data(this, "events"), handlers = events[ event.type ]; + + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + + // Filter the functions by class + if ( all || namespace.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, arguments ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + } + + return event.result; }, - grep: function( elems, callback, inv ) { - var ret = []; + props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) - if ( !inv != !callback( elems[ i ], i ) ) - ret.push( elems[ i ] ); + fix: function( event ) { + if ( event[ expando ] ) { + return event; + } - return ret; + // store a copy of the original event object + // and "clone" to set read-only properties + var originalEvent = event; + event = jQuery.Event( originalEvent ); + + for ( var i = this.props.length, prop; i; ) { + prop = this.props[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary + if ( !event.target ) { + event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + } + + // check if target is a textnode (safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var doc = document.documentElement, body = document.body; + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Add which for key events + if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) { + event.which = event.charCode || event.keyCode; + } + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) { + event.metaKey = event.ctrlKey; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button !== undefined ) { + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } + + return event; }, - map: function( elems, callback ) { - var ret = []; + // Deprecated, use jQuery.guid instead + guid: 1E8, - // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - var value = callback( elems[ i ], i ); + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, - if ( value != null ) - ret[ ret.length ] = value; + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady, + teardown: jQuery.noop + }, + + live: { + add: function( handleObj ) { + jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); + }, + + remove: function( handleObj ) { + var remove = true, + type = handleObj.origType.replace(rnamespaces, ""); + + jQuery.each( jQuery.data(this, "events").live || [], function() { + if ( type === this.origType.replace(rnamespaces, "") ) { + remove = false; + return false; + } + }); + + if ( remove ) { + jQuery.event.remove( this, handleObj.origType, liveHandler ); + } + } + + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( this.setInterval ) { + this.onbeforeunload = eventHandle; + } + + return false; + }, + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } } + } +}; - return ret.concat.apply( [], ret ); +var removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + elem.removeEventListener( type, handle, false ); + } : + function( elem, type, handle ) { + elem.detachEvent( "on" + type, handle ); + }; + +jQuery.Event = function( src ) { + // Allow instantiation without the 'new' keyword + if ( !this.preventDefault ) { + return new jQuery.Event( src ); } -}); -// Use of jQuery.browser is deprecated. -// It's included for backwards compatibility and plugins, -// although they should work to migrate away. + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + // Event type + } else { + this.type = src; + } -var userAgent = navigator.userAgent.toLowerCase(); + // timeStamp is buggy for some events on Firefox(#3843) + // So we won't rely on the native value + this.timeStamp = now(); -// Figure out what browser is being used -jQuery.browser = { - version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1], - safari: /webkit/.test( userAgent ), - opera: /opera/.test( userAgent ), - msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ), - mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent ) + // Mark it as fixed + this[ expando ] = true; }; -jQuery.each({ - parent: function(elem){return elem.parentNode;}, - parents: function(elem){return jQuery.dir(elem,"parentNode");}, - next: function(elem){return jQuery.nth(elem,2,"nextSibling");}, - prev: function(elem){return jQuery.nth(elem,2,"previousSibling");}, - nextAll: function(elem){return jQuery.dir(elem,"nextSibling");}, - prevAll: function(elem){return jQuery.dir(elem,"previousSibling");}, - siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);}, - children: function(elem){return jQuery.sibling(elem.firstChild);}, - contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);} -}, function(name, fn){ - jQuery.fn[ name ] = function( selector ) { - var ret = jQuery.map( this, fn ); +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + } + // otherwise set the returnValue property of the original event to false (IE) + e.returnValue = false; + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function( event ) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { + parent = parent.parentNode; + } + + if ( parent !== this ) { + // set the correct event type + event.type = event.data; + + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } +}, - if ( selector && typeof selector == "string" ) - ret = jQuery.multiFilter( selector, ret ); +// In case of event delegation, we only need to rename the event.type, +// liveHandler will take care of the rest. +delegate = function( event ) { + event.type = event.data; + jQuery.event.handle.apply( this, arguments ); +}; - return this.pushStack( jQuery.unique( ret ), name, selector ); +// Create mouseenter and mouseleave events +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + setup: function( data ) { + jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); + }, + teardown: function( data ) { + jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + } }; }); -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function(name, original){ - jQuery.fn[ name ] = function( selector ) { - var ret = [], insert = jQuery( selector ); +// submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; - for ( var i = 0, l = insert.length; i < l; i++ ) { - var elems = (i > 0 ? this.clone(true) : this).get(); - jQuery.fn[ original ].apply( jQuery(insert[i]), elems ); - ret = ret.concat( elems ); + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + return trigger( "submit", this, arguments ); + } + }); + + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); + } + }; + +} + +// change delegation, happens here so we have bind. +if ( !jQuery.support.changeBubbles ) { + + var formElems = /textarea|input|select/i, + + changeFilters, + + getVal = function( elem ) { + var type = elem.type, val = elem.value; + + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; + + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; + + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; } - return this.pushStack( ret, name, selector ); + return val; + }, + + testChange = function testChange( e ) { + var elem = e.target, data, val; + + if ( !formElems.test( elem.nodeName ) || elem.readOnly ) { + return; + } + + data = jQuery.data( elem, "_change_data" ); + val = getVal(elem); + + // the current data will be also retrieved by beforeactivate + if ( e.type !== "focusout" || elem.type !== "radio" ) { + jQuery.data( elem, "_change_data", val ); + } + + if ( data === undefined || val === data ) { + return; + } + + if ( data != null || val ) { + e.type = "change"; + return jQuery.event.trigger( e, arguments[1], elem ); + } + }; + + jQuery.event.special.change = { + filters: { + focusout: testChange, + + click: function( e ) { + var elem = e.target, type = elem.type; + + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, + + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; + + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, + + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information/focus[in] is not needed anymore + beforeactivate: function( e ) { + var elem = e.target; + jQuery.data( elem, "_change_data", getVal(elem) ); + } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return formElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return formElems.test( this.nodeName ); + } + }; + + changeFilters = jQuery.event.special.change.filters; +} + +function trigger( type, elem, args ) { + args[0].type = type; + return jQuery.event.handle.apply( elem, args ); +} + +// Create "bubbling" focus and blur events +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + this.addEventListener( orig, handler, true ); + }, + teardown: function() { + this.removeEventListener( orig, handler, true ); + } + }; + + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.handle.call( this, e ); + } + }); +} + +jQuery.each(["bind", "one"], function( i, name ) { + jQuery.fn[ name ] = function( type, data, fn ) { + // Handle object literals + if ( typeof type === "object" ) { + for ( var key in type ) { + this[ name ](key, data, type[key], fn); + } + return this; + } + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }) : fn; + + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; }; }); -jQuery.each({ - removeAttr: function( name ) { - jQuery.attr( this, name, "" ); - if (this.nodeType == 1) - this.removeAttribute( name ); - }, +jQuery.fn.extend({ + unbind: function( type, fn ) { + // Handle object literals + if ( typeof type === "object" && !type.preventDefault ) { + for ( var key in type ) { + this.unbind(key, type[key]); + } - addClass: function( classNames ) { - jQuery.className.add( this, classNames ); - }, + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } + } - removeClass: function( classNames ) { - jQuery.className.remove( this, classNames ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); }, - toggleClass: function( classNames, state ) { - if( typeof state !== "boolean" ) - state = !jQuery.className.has( this, classNames ); - jQuery.className[ state ? "add" : "remove" ]( this, classNames ); + triggerHandler: function( type, data ) { + if ( this[0] ) { + var event = jQuery.Event( type ); + event.preventDefault(); + event.stopPropagation(); + jQuery.event.trigger( event, data, this[0] ); + return event.result; + } }, - remove: function( selector ) { - if ( !selector || jQuery.filter( selector, [ this ] ).length ) { - // Prevent memory leaks - jQuery( "*", this ).add([this]).each(function(){ - jQuery.event.remove(this); - jQuery.removeData(this); - }); - if (this.parentNode) - this.parentNode.removeChild( this ); + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, i = 1; + + // link all the functions, so any of them can unbind this click handler + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); } + + return this.click( jQuery.proxy( fn, function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + })); }, - empty: function() { - // Remove element nodes and prevent memory leaks - jQuery(this).children().remove(); + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + +jQuery.each(["live", "die"], function( i, name ) { + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + types = (types || "").split(" "); + + while ( (type = types[ i++ ]) != null ) { + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + + if ( name === "live" ) { + // bind live handler + context.each(function(){ + jQuery.event.add( this, liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + }); - // Remove any remaining nodes - while ( this.firstChild ) - this.removeChild( this.firstChild ); + } else { + // unbind live handler + context.unbind( liveConvert( type, selector ), fn ); + } + } + + return this; } -}, function(name, fn){ - jQuery.fn[ name ] = function(){ - return this.each( fn, arguments ); +}); + +function liveHandler( event ) { + var stop, elems = [], selectors = [], args = arguments, + related, match, handleObj, elem, j, i, l, data, + events = jQuery.data( this, "events" ); + + // Make sure we avoid non-left-click bubbling in Firefox (#3861) + if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { + return; + } + + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); + + } else { + live.splice( j--, 1 ); + } + } + + match = jQuery( event.target ).closest( selectors, event.currentTarget ); + + for ( i = 0, l = match.length; i < l; i++ ) { + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( match[i].selector === handleObj.selector ) { + elem = match[i].elem; + related = null; + + // Those two events require additional checking + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; + } + + if ( !related || related !== elem ) { + elems.push({ elem: elem, handleObj: handleObj }); + } + } + } + } + + for ( i = 0, l = elems.length; i < l; i++ ) { + match = elems[i]; + event.currentTarget = match.elem; + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) { + stop = false; + break; + } + } + + return stop; +} + +function liveConvert( type, selector ) { + return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&"); +} + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( fn ) { + return fn ? this.bind( name, fn ) : this.trigger( name ); }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } }); -// Helper function used by the dimensions and offset modules -function num(elem, prop) { - return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0; +// Prevent memory leaks in IE +// Window isn't included so as not to unbind existing unload events +// More info: +// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ +if ( window.attachEvent && !window.addEventListener ) { + window.attachEvent("onunload", function() { + for ( var id in jQuery.cache ) { + if ( jQuery.cache[ id ].handle ) { + // Try/Catch is to handle iframes being unloaded, see #4280 + try { + jQuery.event.remove( jQuery.cache[ id ].handle.elem ); + } catch(e) {} + } + } + }); } -var expando = "jQuery" + now(), uuid = 0, windowData = {};
-
-jQuery.extend({
- cache: {},
-
- data: function( elem, name, data ) {
- elem = elem == window ?
- windowData :
- elem;
-
- var id = elem[ expando ];
-
- // Compute a unique ID for the element
- if ( !id )
- id = elem[ expando ] = ++uuid;
-
- // Only generate the data cache if we're
- // trying to access or manipulate it
- if ( name && !jQuery.cache[ id ] )
- jQuery.cache[ id ] = {};
-
- // Prevent overriding the named cache with undefined values
- if ( data !== undefined )
- jQuery.cache[ id ][ name ] = data;
-
- // Return the named cache data, or the ID for the element
- return name ?
- jQuery.cache[ id ][ name ] :
- id;
- },
-
- removeData: function( elem, name ) {
- elem = elem == window ?
- windowData :
- elem;
-
- var id = elem[ expando ];
-
- // If we want to remove a specific section of the element's data
- if ( name ) {
- if ( jQuery.cache[ id ] ) {
- // Remove the section of cache data
- delete jQuery.cache[ id ][ name ];
-
- // If we've removed all the data, remove the element's cache
- name = "";
-
- for ( name in jQuery.cache[ id ] )
- break;
-
- if ( !name )
- jQuery.removeData( elem );
- }
-
- // Otherwise, we want to remove all of the element's data
- } else {
- // Clean up the element expando
- try {
- delete elem[ expando ];
- } catch(e){
- // IE has trouble directly removing the expando
- // but it's ok with using removeAttribute
- if ( elem.removeAttribute )
- elem.removeAttribute( expando );
- }
-
- // Completely remove the data cache
- delete jQuery.cache[ id ];
- }
- },
- queue: function( elem, type, data ) {
- if ( elem ){
-
- type = (type || "fx") + "queue";
-
- var q = jQuery.data( elem, type );
-
- if ( !q || jQuery.isArray(data) )
- q = jQuery.data( elem, type, jQuery.makeArray(data) );
- else if( data )
- q.push( data );
-
- }
- return q;
- },
-
- dequeue: function( elem, type ){
- var queue = jQuery.queue( elem, type ),
- fn = queue.shift();
-
- if( !type || type === "fx" )
- fn = queue[0];
-
- if( fn !== undefined )
- fn.call(elem);
- }
-});
-
-jQuery.fn.extend({
- data: function( key, value ){
- var parts = key.split(".");
- parts[1] = parts[1] ? "." + parts[1] : "";
-
- if ( value === undefined ) {
- var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
-
- if ( data === undefined && this.length )
- data = jQuery.data( this[0], key );
-
- return data === undefined && parts[1] ?
- this.data( parts[0] ) :
- data;
- } else
- return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
- jQuery.data( this, key, value );
- });
- },
-
- removeData: function( key ){
- return this.each(function(){
- jQuery.removeData( this, key );
- });
- },
- queue: function(type, data){
- if ( typeof type !== "string" ) {
- data = type;
- type = "fx";
- }
-
- if ( data === undefined )
- return jQuery.queue( this[0], type );
-
- return this.each(function(){
- var queue = jQuery.queue( this, type, data );
-
- if( type == "fx" && queue.length == 1 )
- queue[0].call(this);
- });
- },
- dequeue: function(type){
- return this.each(function(){
- jQuery.dequeue( this, type );
- });
- }
-});/*! - * Sizzle CSS Selector Engine - v0.9.3 +/*! + * Sizzle CSS Selector Engine - v1.0 * Copyright 2009, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ (function(){ -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g, +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, done = 0, - toString = Object.prototype.toString; + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function(){ + baseHasDuplicate = false; + return 0; +}); var Sizzle = function(selector, context, results, seed) { results = results || []; - context = context || document; + var origContext = context = context || document; - if ( context.nodeType !== 1 && context.nodeType !== 9 ) + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { return []; + } if ( !selector || typeof selector !== "string" ) { return results; } - var parts = [], m, set, checkSet, check, mode, extra, prune = true; + var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context), + soFar = selector; // Reset the position of the chunker regexp (start from head) - chunker.lastIndex = 0; - - while ( (m = chunker.exec(selector)) !== null ) { + while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { + soFar = m[3]; + parts.push( m[1] ); if ( m[2] ) { - extra = RegExp.rightContext; + extra = m[3]; break; } } @@ -1458,38 +2682,51 @@ var Sizzle = function(selector, context, results, seed) { while ( parts.length ) { selector = parts.shift(); - if ( Expr.relative[ selector ] ) + if ( Expr.relative[ selector ] ) { selector += parts.shift(); - + } + set = posProcess( selector, set ); } } } else { - var ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context, isXML(context) ); - set = Sizzle.filter( ret.expr, ret.set ); - - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + var ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; } - while ( parts.length ) { - var cur = parts.pop(), pop = cur; + if ( context ) { + var ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; - if ( !Expr.relative[ cur ] ) { - cur = ""; + if ( parts.length > 0 ) { + checkSet = makeArray(set); } else { - pop = parts.pop(); + prune = false; } - if ( pop == null ) { - pop = context; - } + while ( parts.length ) { + var cur = parts.pop(), pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } - Expr.relative[ cur ]( checkSet, pop, isXML(context) ); + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + } else { + checkSet = parts = []; } } @@ -1498,13 +2735,13 @@ var Sizzle = function(selector, context, results, seed) { } if ( !checkSet ) { - throw "Syntax error, unrecognized expression: " + (cur || selector); + Sizzle.error( cur || selector ); } if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); - } else if ( context.nodeType === 1 ) { + } else if ( context && context.nodeType === 1 ) { for ( var i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { results.push( set[i] ); @@ -1522,17 +2759,22 @@ var Sizzle = function(selector, context, results, seed) { } if ( extra ) { - Sizzle( extra, context, results, seed ); + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } - if ( sortOrder ) { - hasDuplicate = false; - results.sort(sortOrder); + return results; +}; - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); - } +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); } } } @@ -1555,8 +2797,9 @@ Sizzle.find = function(expr, context, isXML){ for ( var i = 0, l = Expr.order.length; i < l; i++ ) { var type = Expr.order[i], match; - if ( (match = Expr.match[ type ].exec( expr )) ) { - var left = RegExp.leftContext; + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice(1,1); if ( left.substr( left.length - 1 ) !== "\\" ) { match[1] = (match[1] || "").replace(/\\/g, ""); @@ -1582,11 +2825,17 @@ Sizzle.filter = function(expr, set, inplace, not){ while ( expr && set.length ) { for ( var type in Expr.filter ) { - if ( (match = Expr.match[ type ].exec( expr )) != null ) { - var filter = Expr.filter[ type ], found, item; + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var filter = Expr.filter[ type ], found, item, left = match[1]; anyFound = false; - if ( curLoop == result ) { + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { result = []; } @@ -1637,9 +2886,9 @@ Sizzle.filter = function(expr, set, inplace, not){ } // Improper expression - if ( expr == old ) { + if ( expr === old ) { if ( anyFound == null ) { - throw "Syntax error, unrecognized expression: " + expr; + Sizzle.error( expr ); } else { break; } @@ -1651,18 +2900,23 @@ Sizzle.filter = function(expr, set, inplace, not){ return curLoop; }; +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; + var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], match: { - ID: /#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/, + ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/ + PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ }, + leftMatch: {}, attrMap: { "class": "className", "for": "htmlFor" @@ -1673,20 +2927,20 @@ var Expr = Sizzle.selectors = { } }, relative: { - "+": function(checkSet, part, isXML){ + "+": function(checkSet, part){ var isPartStr = typeof part === "string", isTag = isPartStr && !/\W/.test(part), isPartStrNotTag = isPartStr && !isTag; - if ( isTag && !isXML ) { - part = part.toUpperCase(); + if ( isTag ) { + part = part.toLowerCase(); } for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { if ( (elem = checkSet[i]) ) { while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ? + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? elem || false : elem === part; } @@ -1696,17 +2950,17 @@ var Expr = Sizzle.selectors = { Sizzle.filter( part, checkSet, true ); } }, - ">": function(checkSet, part, isXML){ + ">": function(checkSet, part){ var isPartStr = typeof part === "string"; if ( isPartStr && !/\W/.test(part) ) { - part = isXML ? part : part.toUpperCase(); + part = part.toLowerCase(); for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var parent = elem.parentNode; - checkSet[i] = parent.nodeName === part ? parent : false; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; } } } else { @@ -1727,8 +2981,8 @@ var Expr = Sizzle.selectors = { "": function(checkSet, part, isXML){ var doneName = done++, checkFn = dirCheck; - if ( !part.match(/\W/) ) { - var nodeCheck = part = isXML ? part : part.toUpperCase(); + if ( typeof part === "string" && !/\W/.test(part) ) { + var nodeCheck = part = part.toLowerCase(); checkFn = dirNodeCheck; } @@ -1737,8 +2991,8 @@ var Expr = Sizzle.selectors = { "~": function(checkSet, part, isXML){ var doneName = done++, checkFn = dirCheck; - if ( typeof part === "string" && !part.match(/\W/) ) { - var nodeCheck = part = isXML ? part : part.toUpperCase(); + if ( typeof part === "string" && !/\W/.test(part) ) { + var nodeCheck = part = part.toLowerCase(); checkFn = dirNodeCheck; } @@ -1752,7 +3006,7 @@ var Expr = Sizzle.selectors = { return m ? [m] : []; } }, - NAME: function(match, context, isXML){ + NAME: function(match, context){ if ( typeof context.getElementsByName !== "undefined" ) { var ret = [], results = context.getElementsByName(match[1]); @@ -1779,9 +3033,10 @@ var Expr = Sizzle.selectors = { for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) { - if ( !inplace ) + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { result.push( elem ); + } } else if ( inplace ) { curLoop[i] = false; } @@ -1794,14 +3049,13 @@ var Expr = Sizzle.selectors = { return match[1].replace(/\\/g, ""); }, TAG: function(match, curLoop){ - for ( var i = 0; curLoop[i] === false; i++ ){} - return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase(); + return match[1].toLowerCase(); }, CHILD: function(match){ - if ( match[1] == "nth" ) { + if ( match[1] === "nth" ) { // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( - match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" || + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); // calculate the numbers (first)n+(last) including if they are negative @@ -1830,7 +3084,7 @@ var Expr = Sizzle.selectors = { PSEUDO: function(match, curLoop, inplace, result, not){ if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one - if ( match[3].match(chunker).length > 1 || /^\w/.test(match[3]) ) { + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); @@ -1903,7 +3157,7 @@ var Expr = Sizzle.selectors = { return "reset" === elem.type; }, button: function(elem){ - return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON"; + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; }, input: function(elem){ return /input|select|textarea|button/i.test(elem.nodeName); @@ -1929,10 +3183,10 @@ var Expr = Sizzle.selectors = { return i > match[3] - 0; }, nth: function(elem, i, match){ - return match[3] - 0 == i; + return match[3] - 0 === i; }, eq: function(elem, i, match){ - return match[3] - 0 == i; + return match[3] - 0 === i; } }, filter: { @@ -1942,7 +3196,7 @@ var Expr = Sizzle.selectors = { if ( filter ) { return filter( elem, i, match, array ); } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0; + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; @@ -1953,6 +3207,8 @@ var Expr = Sizzle.selectors = { } return true; + } else { + Sizzle.error( "Syntax error, unrecognized expression: " + name ); } }, CHILD: function(elem, match){ @@ -1960,20 +3216,26 @@ var Expr = Sizzle.selectors = { switch (type) { case 'only': case 'first': - while (node = node.previousSibling) { - if ( node.nodeType === 1 ) return false; + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + if ( type === "first" ) { + return true; } - if ( type == 'first') return true; node = elem; case 'last': - while (node = node.nextSibling) { - if ( node.nodeType === 1 ) return false; + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } } return true; case 'nth': var first = match[2], last = match[3]; - if ( first == 1 && last == 0 ) { + if ( first === 1 && last === 0 ) { return true; } @@ -1991,10 +3253,10 @@ var Expr = Sizzle.selectors = { } var diff = elem.nodeIndex - last; - if ( first == 0 ) { - return diff == 0; + if ( first === 0 ) { + return diff === 0; } else { - return ( diff % first == 0 && diff / first >= 0 ); + return ( diff % first === 0 && diff / first >= 0 ); } } }, @@ -2002,7 +3264,7 @@ var Expr = Sizzle.selectors = { return elem.nodeType === 1 && elem.getAttribute("id") === match; }, TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName === match; + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; }, CLASS: function(elem, match){ return (" " + (elem.className || elem.getAttribute("class")) + " ") @@ -2030,7 +3292,7 @@ var Expr = Sizzle.selectors = { !check ? value && result !== false : type === "!=" ? - value != check : + value !== check : type === "^=" ? value.indexOf(check) === 0 : type === "$=" ? @@ -2052,11 +3314,14 @@ var Expr = Sizzle.selectors = { var origPOS = Expr.match.POS; for ( var type in Expr.match ) { - Expr.match[ type ] = RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){ + return "\\" + (num - 0 + 1); + })); } var makeArray = function(array, results) { - array = Array.prototype.slice.call( array ); + array = Array.prototype.slice.call( array, 0 ); if ( results ) { results.push.apply( results, array ); @@ -2068,8 +3333,10 @@ var makeArray = function(array, results) { // Perform a simple check to determine if the browser is capable of // converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) try { - Array.prototype.slice.call( document.documentElement.childNodes ); + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work } catch(e){ @@ -2098,6 +3365,13 @@ var sortOrder; if ( document.documentElement.compareDocumentPosition ) { sortOrder = function( a, b ) { + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.compareDocumentPosition ? -1 : 1; + } + var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; if ( ret === 0 ) { hasDuplicate = true; @@ -2106,6 +3380,13 @@ if ( document.documentElement.compareDocumentPosition ) { }; } else if ( "sourceIndex" in document.documentElement ) { sortOrder = function( a, b ) { + if ( !a.sourceIndex || !b.sourceIndex ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.sourceIndex ? -1 : 1; + } + var ret = a.sourceIndex - b.sourceIndex; if ( ret === 0 ) { hasDuplicate = true; @@ -2114,11 +3395,18 @@ if ( document.documentElement.compareDocumentPosition ) { }; } else if ( document.createRange ) { sortOrder = function( a, b ) { + if ( !a.ownerDocument || !b.ownerDocument ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.ownerDocument ? -1 : 1; + } + var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); - aRange.selectNode(a); - aRange.collapse(true); - bRange.selectNode(b); - bRange.collapse(true); + aRange.setStart(a, 0); + aRange.setEnd(a, 0); + bRange.setStart(b, 0); + bRange.setEnd(b, 0); var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); if ( ret === 0 ) { hasDuplicate = true; @@ -2127,13 +3415,33 @@ if ( document.documentElement.compareDocumentPosition ) { }; } +// Utility function for retreiving the text value of an array of DOM nodes +function getText( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += getText( elem.childNodes ); + } + } + + return ret; +} + // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ // We're going to inject a fake input element with a specified name - var form = document.createElement("form"), + var form = document.createElement("div"), id = "script" + (new Date).getTime(); - form.innerHTML = "<input name='" + id + "'/>"; + form.innerHTML = "<a name='" + id + "'/>"; // Inject it into the root element, check its status, and remove it quickly var root = document.documentElement; @@ -2141,7 +3449,7 @@ if ( document.documentElement.compareDocumentPosition ) { // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) - if ( !!document.getElementById( id ) ) { + if ( document.getElementById( id ) ) { Expr.find.ID = function(match, context, isXML){ if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); @@ -2156,6 +3464,7 @@ if ( document.documentElement.compareDocumentPosition ) { } root.removeChild( form ); + root = form = null; // release memory in IE })(); (function(){ @@ -2196,69 +3505,75 @@ if ( document.documentElement.compareDocumentPosition ) { return elem.getAttribute("href", 2); }; } + + div = null; // release memory in IE })(); -if ( document.querySelectorAll ) (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); - div.innerHTML = "<p class='TEST'></p>"; +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "<p class='TEST'></p>"; - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } - Sizzle = function(query, context, extra, seed){ - context = context || document; + Sizzle = function(query, context, extra, seed){ + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && context.nodeType === 9 && !isXML(context) ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(e){} + } + + return oldSizzle(query, context, extra, seed); + }; - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && context.nodeType === 9 && !isXML(context) ) { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(e){} + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; } - - return oldSizzle(query, context, extra, seed); - }; - Sizzle.find = oldSizzle.find; - Sizzle.filter = oldSizzle.filter; - Sizzle.selectors = oldSizzle.selectors; - Sizzle.matches = oldSizzle.matches; -})(); + div = null; // release memory in IE + })(); +} -if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){ +(function(){ var div = document.createElement("div"); + div.innerHTML = "<div class='test e'></div><div class='test'></div>"; // Opera can't find a second classname (in 9.6) - if ( div.getElementsByClassName("e").length === 0 ) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { return; + } // Safari caches class attributes, doesn't catch changes (in 3.2) div.lastChild.className = "e"; - if ( div.getElementsByClassName("e").length === 1 ) + if ( div.getElementsByClassName("e").length === 1 ) { return; - + } + Expr.order.splice(1, 0, "CLASS"); Expr.find.CLASS = function(match, context, isXML) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; + + div = null; // release memory in IE })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - var sibDir = dir == "previousSibling" && !isXML; for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { - if ( sibDir && elem.nodeType === 1 ){ - elem.sizcache = doneName; - elem.sizset = i; - } elem = elem[dir]; var match = false; @@ -2273,7 +3588,7 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { elem.sizset = i; } - if ( elem.nodeName === cur ) { + if ( elem.nodeName.toLowerCase() === cur ) { match = elem; break; } @@ -2287,14 +3602,9 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { } function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - var sibDir = dir == "previousSibling" && !isXML; for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { - if ( sibDir && elem.nodeType === 1 ) { - elem.sizcache = doneName; - elem.sizset = i; - } elem = elem[dir]; var match = false; @@ -2329,15 +3639,17 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { } } -var contains = document.compareDocumentPosition ? function(a, b){ - return a.compareDocumentPosition(b) & 16; +var contains = document.compareDocumentPosition ? function(a, b){ + return !!(a.compareDocumentPosition(b) & 16); } : function(a, b){ return a !== b && (a.contains ? a.contains(b) : true); }; var isXML = function(elem){ - return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || - !!elem.ownerDocument && isXML( elem.ownerDocument ); + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; }; var posProcess = function(selector, context){ @@ -2362,872 +3674,1122 @@ var posProcess = function(selector, context){ // EXPOSE jQuery.find = Sizzle; -jQuery.filter = Sizzle.filter; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = getText; +jQuery.isXMLDoc = isXML; +jQuery.contains = contains; -Sizzle.selectors.filters.hidden = function(elem){ - return elem.offsetWidth === 0 || elem.offsetHeight === 0; -}; +return; -Sizzle.selectors.filters.visible = function(elem){ - return elem.offsetWidth > 0 || elem.offsetHeight > 0; -}; +window.Sizzle = Sizzle; -Sizzle.selectors.filters.animated = function(elem){ - return jQuery.grep(jQuery.timers, function(fn){ - return elem === fn.elem; - }).length; -}; +})(); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + slice = Array.prototype.slice; + +// Implement the identical functionality for filter and not +var winnow = function( elements, qualifier, keep ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) === keep; + }); -jQuery.multiFilter = function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return (elem === qualifier) === keep; + }); - return Sizzle.matches(expr, elems); -}; + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); -jQuery.dir = function( elem, dir ){ - var matched = [], cur = elem[dir]; - while ( cur && cur != document ) { - if ( cur.nodeType == 1 ) - matched.push( cur ); - cur = cur[dir]; + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } } - return matched; + + return jQuery.grep(elements, function( elem, i ) { + return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + }); }; -jQuery.nth = function(cur, result, dir, elem){ - result = result || 1; - var num = 0; +jQuery.fn.extend({ + find: function( selector ) { + var ret = this.pushStack( "", "find", selector ), length = 0; + + for ( var i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( var n = length; n < ret.length; n++ ) { + for ( var r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } - for ( ; cur; cur = cur[dir] ) - if ( cur.nodeType == 1 && ++num == result ) - break; + return ret; + }, - return cur; -}; + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, -jQuery.sibling = function(n, elem){ - var r = []; + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType == 1 && n != elem ) - r.push( n ); - } + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && jQuery.filter( selector, this ).length > 0; + }, - return r; -}; + closest: function( selectors, context ) { + if ( jQuery.isArray( selectors ) ) { + var ret = [], cur = this[0], match, matches = {}, selector; -return; + if ( cur && selectors.length ) { + for ( var i = 0, l = selectors.length; i < l; i++ ) { + selector = selectors[i]; -window.Sizzle = Sizzle; + if ( !matches[selector] ) { + matches[selector] = jQuery.expr.match.POS.test( selector ) ? + jQuery( selector, context || this.context ) : + selector; + } + } -})(); -/* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. - */ -jQuery.event = { + while ( cur && cur.ownerDocument && cur !== context ) { + for ( selector in matches ) { + match = matches[selector]; - // Bind an event to an element - // Original by Dean Edwards - add: function(elem, types, handler, data) { - if ( elem.nodeType == 3 || elem.nodeType == 8 ) - return; + if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { + ret.push({ selector: selector, elem: cur }); + delete matches[selector]; + } + } + cur = cur.parentNode; + } + } - // For whatever reason, IE has trouble passing the window object - // around, causing it to be cloned in the process - if ( elem.setInterval && elem != window ) - elem = window; + return ret; + } - // Make sure that the function being executed has a unique ID - if ( !handler.guid ) - handler.guid = this.guid++; + var pos = jQuery.expr.match.POS.test( selectors ) ? + jQuery( selectors, context || this.context ) : null; - // if data is passed, bind to handler - if ( data !== undefined ) { - // Create temporary function pointer to original handler - var fn = handler; + return this.map(function( i, cur ) { + while ( cur && cur.ownerDocument && cur !== context ) { + if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) { + return cur; + } + cur = cur.parentNode; + } + return null; + }); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + if ( !elem || typeof elem === "string" ) { + return jQuery.inArray( this[0], + // If it receives a string, the selector is used + // If it receives nothing, the siblings are used + elem ? jQuery( elem ) : this.parent().children() ); + } + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context || this.context ) : + jQuery.makeArray( selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); - // Create unique handler function, wrapped around original handler - handler = this.proxy( fn ); +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} - // Store data in unique handler - handler.data = data; +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; } - // Init the element's event structure - var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}), - handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){ - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - return typeof jQuery !== "undefined" && !jQuery.event.triggered ? - jQuery.event.handle.apply(arguments.callee.elem, arguments) : - undefined; - }); - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native - // event in IE. - handle.elem = elem; + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - jQuery.each(types.split(/\s+/), function(index, type) { - // Namespaced event handlers - var namespaces = type.split("."); - type = namespaces.shift(); - handler.type = namespaces.slice().sort().join("."); + ret = this.length > 1 ? jQuery.unique( ret ) : ret; - // Get the current list of functions bound to this event - var handlers = events[type]; - - if ( jQuery.event.specialAll[type] ) - jQuery.event.specialAll[type].setup.call(elem, data, namespaces); + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } - // Init the event handler queue - if (!handlers) { - handlers = events[type] = {}; + return this.pushStack( ret, name, slice.call(arguments).join(",") ); + }; +}); - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false - if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) { - // Bind the global event handler to the element - if (elem.addEventListener) - elem.addEventListener(type, handle, false); - else if (elem.attachEvent) - elem.attachEvent("on" + type, handle); - } +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], cur = elem[dir]; + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); } + cur = cur[dir]; + } + return matched; + }, - // Add the function to the element's handler list - handlers[handler.guid] = handler; + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; - // Keep track of which events have been used, for global triggering - jQuery.event.global[type] = true; - }); + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } - // Nullify elem to prevent memory leaks in IE - elem = null; + return cur; }, - guid: 1, - global: {}, + sibling: function( n, elem ) { + var r = []; - // Detach an event or set of events from an element - remove: function(elem, types, handler) { - // don't do events on text and comment nodes - if ( elem.nodeType == 3 || elem.nodeType == 8 ) - return; + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } - var events = jQuery.data(elem, "events"), ret, index; + return r; + } +}); +var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g, + rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i, + rtagName = /<([\w:]+)/, + rtbody = /<tbody/i, + rhtml = /<|&#?\w+;/, + rnocache = /<script|<object|<embed|<option|<style/i, + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, // checked="checked" or checked (html5) + fcloseTag = function( all, front, tag ) { + return rselfClosing.test( tag ) ? + all : + front + "></" + tag + ">"; + }, + wrapMap = { + option: [ 1, "<select multiple='multiple'>", "</select>" ], + legend: [ 1, "<fieldset>", "</fieldset>" ], + thead: [ 1, "<table>", "</table>" ], + tr: [ 2, "<table><tbody>", "</tbody></table>" ], + td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], + col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], + area: [ 1, "<map>", "</map>" ], + _default: [ 0, "", "" ] + }; - if ( events ) { - // Unbind all events for the element - if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") ) - for ( var type in events ) - this.remove( elem, type + (types || "") ); - else { - // types is actually an event object here - if ( types.type ) { - handler = types.handler; - types = types.type; - } +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; - // Handle multiple events seperated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - jQuery.each(types.split(/\s+/), function(index, type){ - // Namespaced event handlers - var namespaces = type.split("."); - type = namespaces.shift(); - var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)"); - - if ( events[type] ) { - // remove the given handler for the given type - if ( handler ) - delete events[type][handler.guid]; - - // remove all handlers for the given type - else - for ( var handle in events[type] ) - // Handle the removal of namespaced events - if ( namespace.test(events[type][handle].type) ) - delete events[type][handle]; - - if ( jQuery.event.specialAll[type] ) - jQuery.event.specialAll[type].teardown.call(elem, namespaces); - - // remove generic event handler if no more handlers exist - for ( ret in events[type] ) break; - if ( !ret ) { - if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false ) { - if (elem.removeEventListener) - elem.removeEventListener(type, jQuery.data(elem, "handle"), false); - else if (elem.detachEvent) - elem.detachEvent("on" + type, jQuery.data(elem, "handle")); - } - ret = null; - delete events[type]; - } - } - }); - } +// IE can't serialize <link> and <script> tags normally +if ( !jQuery.support.htmlSerialize ) { + wrapMap._default = [ 1, "div<div>", "</div>" ]; +} - // Remove the expando if it's no longer used - for ( ret in events ) break; - if ( !ret ) { - var handle = jQuery.data( elem, "handle" ); - if ( handle ) handle.elem = null; - jQuery.removeData( elem, "events" ); - jQuery.removeData( elem, "handle" ); - } +jQuery.fn.extend({ + text: function( text ) { + if ( jQuery.isFunction(text) ) { + return this.each(function(i) { + var self = jQuery(this); + self.text( text.call(this, i, self.text()) ); + }); } + + if ( typeof text !== "object" && text !== undefined ) { + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + } + + return jQuery.text( this ); }, - // bubbling is internal - trigger: function( event, data, elem, bubbling ) { - // Event object or event type - var type = event.type || event; + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } - if( !bubbling ){ - event = typeof event === "object" ? - // jQuery.Event object - event[expando] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); } - // Handle a global trigger - if ( !elem ) { - // Don't bubble custom events when global (to avoid too much overhead) - event.stopPropagation(); - // Only trigger if we've ever bound an event for it - if ( this.global[type] ) - jQuery.each( jQuery.cache, function(){ - if ( this.events && this.events[type] ) - jQuery.event.trigger( event, data, this.handle.elem ); - }); - } + wrap.map(function() { + var elem = this; - // Handle triggering a single element + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } - // don't do events on text and comment nodes - if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 ) - return undefined; - - // Clean up in case it is reused - event.result = undefined; - event.target = elem; - - // Clone the incoming data, if any - data = jQuery.makeArray(data); - data.unshift( event ); + return elem; + }).append(this); } - event.currentTarget = elem; + return this; + }, - // Trigger the event, it is assumed that "handle" is a function - var handle = jQuery.data(elem, "handle"); - if ( handle ) - handle.apply( elem, data ); + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } - // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links) - if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false ) - event.result = false; + return this.each(function() { + var self = jQuery( this ), contents = self.contents(); - // Trigger the native events (except for clicks on links) - if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) { - this.triggered = true; - try { - elem[ type ](); - // prevent IE from throwing an error for some hidden elements - } catch (e) {} - } + if ( contents.length ) { + contents.wrapAll( html ); - this.triggered = false; + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + return this.each(function() { + jQuery( this ).wrapAll( html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 ) { + this.appendChild( elem ); + } + }); + }, - if ( !event.isPropagationStopped() ) { - var parent = elem.parentNode || elem.ownerDocument; - if ( parent ) - jQuery.event.trigger(event, data, parent, true); + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + if ( this[0] && this[0].parentNode ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this ); + }); + } else if ( arguments.length ) { + var set = jQuery(arguments[0]); + set.push.apply( set, this.toArray() ); + return this.pushStack( set, "before", arguments ); } }, - handle: function(event) { - // returned undefined or false - var all, handlers; + after: function() { + if ( this[0] && this[0].parentNode ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + } else if ( arguments.length ) { + var set = this.pushStack( this, "after", arguments ); + set.push.apply( set, jQuery(arguments[0]).toArray() ); + return set; + } + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + jQuery.cleanData( [ elem ] ); + } - event = arguments[0] = jQuery.event.fix( event || window.event ); - event.currentTarget = this; + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + } + } - // Namespaced event handlers - var namespaces = event.type.split("."); - event.type = namespaces.shift(); + return this; + }, + + empty: function() { + for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + } - // Cache this now, all = true means, any handler - all = !namespaces.length && !event.exclusive; + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + } - var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)"); + return this; + }, - handlers = ( jQuery.data(this, "events") || {} )[event.type]; + clone: function( events ) { + // Do the clone + var ret = this.map(function() { + if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var html = this.outerHTML, ownerDocument = this.ownerDocument; + if ( !html ) { + var div = ownerDocument.createElement("div"); + div.appendChild( this.cloneNode(true) ); + html = div.innerHTML; + } - for ( var j in handlers ) { - var handler = handlers[j]; + return jQuery.clean([html.replace(rinlinejQuery, "") + // Handle the case in IE 8 where action=/test/> self-closes a tag + .replace(/=([^="'>\s]+\/)>/g, '="$1">') + .replace(rleadingWhitespace, "")], ownerDocument)[0]; + } else { + return this.cloneNode(true); + } + }); - // Filter the functions by class - if ( all || namespace.test(handler.type) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handler; - event.data = handler.data; + // Copy the events from the original to the clone + if ( events === true ) { + cloneCopyEvent( this, ret ); + cloneCopyEvent( this.find("*"), ret.find("*") ); + } - var ret = handler.apply(this, arguments); + // Return the cloned set + return ret; + }, + + html: function( value ) { + if ( value === undefined ) { + return this[0] && this[0].nodeType === 1 ? + this[0].innerHTML.replace(rinlinejQuery, "") : + null; - if( ret !== undefined ){ - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); + // See if we can take a shortcut and just use innerHTML + } else if ( typeof value === "string" && !rnocache.test( value ) && + (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && + !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { + + value = value.replace(rxhtmlTag, fcloseTag); + + try { + for ( var i = 0, l = this.length; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + if ( this[i].nodeType === 1 ) { + jQuery.cleanData( this[i].getElementsByTagName("*") ); + this[i].innerHTML = value; } } - if( event.isImmediatePropagationStopped() ) - break; - + // If using innerHTML throws an exception, use the fallback method + } catch(e) { + this.empty().append( value ); } + + } else if ( jQuery.isFunction( value ) ) { + this.each(function(i){ + var self = jQuery(this), old = self.html(); + self.empty().append(function(){ + return value.call( this, i, old ); + }); + }); + + } else { + this.empty().append( value ); } + + return this; }, - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + replaceWith: function( value ) { + if ( this[0] && this[0].parentNode ) { + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this), old = self.html(); + self.replaceWith( value.call( this, i, old ) ); + }); + } - fix: function(event) { - if ( event[expando] ) - return event; + if ( typeof value !== "string" ) { + value = jQuery(value).detach(); + } - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; - event = jQuery.Event( originalEvent ); + return this.each(function() { + var next = this.nextSibling, parent = this.parentNode; - for ( var i = this.props.length, prop; i; ){ - prop = this.props[ --i ]; - event[ prop ] = originalEvent[ prop ]; + jQuery(this).remove(); + + if ( next ) { + jQuery(next).before( value ); + } else { + jQuery(parent).append( value ); + } + }); + } else { + return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ); } + }, - // Fix target property, if necessary - if ( !event.target ) - event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + detach: function( selector ) { + return this.remove( selector, true ); + }, - // check if target is a textnode (safari) - if ( event.target.nodeType == 3 ) - event.target = event.target.parentNode; + domManip: function( args, table, callback ) { + var results, first, value = args[0], scripts = [], fragment, parent; - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) - event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement; + // We can't cloneNode fragments that contain checked, in WebKit + if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { + return this.each(function() { + jQuery(this).domManip( args, table, callback, true ); + }); + } - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, body = document.body; - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0); + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + args[0] = value.call(this, i, table ? self.html() : undefined); + self.domManip( args, table, callback ); + }); } - // Add which for key events - if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) - event.which = event.charCode || event.keyCode; + if ( this[0] ) { + parent = value && value.parentNode; - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) - event.metaKey = event.ctrlKey; + // If we're in a fragment, just use that instead of building a new one + if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { + results = { fragment: parent }; - // Add which for click: 1 == left; 2 == middle; 3 == right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button ) - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } else { + results = buildFragment( args, this, scripts ); + } + + fragment = results.fragment; + + if ( fragment.childNodes.length === 1 ) { + first = fragment = fragment.firstChild; + } else { + first = fragment.firstChild; + } - return event; - }, + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + callback.call( + table ? + root(this[i], first) : + this[i], + i > 0 || results.cacheable || this.length > 1 ? + fragment.cloneNode(true) : + fragment + ); + } + } - proxy: function( fn, proxy ){ - proxy = proxy || function(){ return fn.apply(this, arguments); }; - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++; - // So proxy can be declared as an argument - return proxy; - }, + if ( scripts.length ) { + jQuery.each( scripts, evalScript ); + } + } - special: { - ready: { - // Make sure the ready event is setup - setup: bindReady, - teardown: function() {} + return this; + + function root( elem, cur ) { + return jQuery.nodeName(elem, "table") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; } - }, - - specialAll: { - live: { - setup: function( selector, namespaces ){ - jQuery.event.add( this, namespaces[0], liveHandler ); - }, - teardown: function( namespaces ){ - if ( namespaces.length ) { - var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)"); - - jQuery.each( (jQuery.data(this, "events").live || {}), function(){ - if ( name.test(this.type) ) - remove++; - }); - - if ( remove < 1 ) - jQuery.event.remove( this, namespaces[0], liveHandler ); + } +}); + +function cloneCopyEvent(orig, ret) { + var i = 0; + + ret.each(function() { + if ( this.nodeName !== (orig[i] && orig[i].nodeName) ) { + return; + } + + var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( var type in events ) { + for ( var handler in events[ type ] ) { + jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); } } } + }); +} + +function buildFragment( args, nodes, scripts ) { + var fragment, cacheable, cacheresults, + doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document); + + // Only cache "small" (1/2 KB) strings that are associated with the main document + // Cloning options loses the selected state, so don't cache them + // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment + // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache + if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document && + !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) { + + cacheable = true; + cacheresults = jQuery.fragments[ args[0] ]; + if ( cacheresults ) { + if ( cacheresults !== 1 ) { + fragment = cacheresults; + } + } } -}; -jQuery.Event = function( src ){ - // Allow instantiation without the 'new' keyword - if( !this.preventDefault ) - return new jQuery.Event(src); - - // Event object - if( src && src.type ){ - this.originalEvent = src; - this.type = src.type; - // Event type - }else - this.type = src; + if ( !fragment ) { + fragment = doc.createDocumentFragment(); + jQuery.clean( args, doc, fragment, scripts ); + } - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = now(); - - // Mark it as fixed - this[expando] = true; -}; + if ( cacheable ) { + jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1; + } -function returnFalse(){ - return false; + return { fragment: fragment, cacheable: cacheable }; } -function returnTrue(){ - return true; -} - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; - var e = this.originalEvent; - if( !e ) - return; - // if preventDefault exists run it on the original event - if (e.preventDefault) - e.preventDefault(); - // otherwise set the returnValue property of the original event to false (IE) - e.returnValue = false; - }, - stopPropagation: function() { - this.isPropagationStopped = returnTrue; +jQuery.fragments = {}; - var e = this.originalEvent; - if( !e ) - return; - // if stopPropagation exists run it on the original event - if (e.stopPropagation) - e.stopPropagation(); - // otherwise set the cancelBubble property of the original event to true (IE) - e.cancelBubble = true; - }, - stopImmediatePropagation:function(){ - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse -}; -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function(event) { - // Check if mouse(over|out) are still within the same parent element - var parent = event.relatedTarget; - // Traverse up the tree - while ( parent && parent != this ) - try { parent = parent.parentNode; } - catch(e) { parent = this; } - - if( parent != this ){ - // set the correct event type - event.type = event.data; - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } -}; - -jQuery.each({ - mouseover: 'mouseenter', - mouseout: 'mouseleave' -}, function( orig, fix ){ - jQuery.event.special[ fix ] = { - setup: function(){ - jQuery.event.add( this, orig, withinElement, fix ); - }, - teardown: function(){ - jQuery.event.remove( this, orig, withinElement ); +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var ret = [], insert = jQuery( selector ), + parent = this.length === 1 && this[0].parentNode; + + if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { + insert[ original ]( this[0] ); + return this; + + } else { + for ( var i = 0, l = insert.length; i < l; i++ ) { + var elems = (i > 0 ? this.clone(true) : this).get(); + jQuery.fn[ original ].apply( jQuery(insert[i]), elems ); + ret = ret.concat( elems ); + } + + return this.pushStack( ret, name, insert.selector ); } - }; + }; }); -jQuery.fn.extend({ - bind: function( type, data, fn ) { - return type == "unload" ? this.one(type, data, fn) : this.each(function(){ - jQuery.event.add( this, type, fn || data, fn && data ); - }); - }, +jQuery.extend({ + clean: function( elems, context, fragment, scripts ) { + context = context || document; - one: function( type, data, fn ) { - var one = jQuery.event.proxy( fn || data, function(event) { - jQuery(this).unbind(event, one); - return (fn || data).apply( this, arguments ); - }); - return this.each(function(){ - jQuery.event.add( this, type, one, fn && data); - }); - }, + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) { + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + } - unbind: function( type, fn ) { - return this.each(function(){ - jQuery.event.remove( this, type, fn ); - }); - }, + var ret = []; - trigger: function( type, data ) { - return this.each(function(){ - jQuery.event.trigger( type, data, this ); - }); - }, + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + if ( typeof elem === "number" ) { + elem += ""; + } - triggerHandler: function( type, data ) { - if( this[0] ){ - var event = jQuery.Event(type); - event.preventDefault(); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return event.result; - } - }, + if ( !elem ) { + continue; + } - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, i = 1; + // Convert html string into DOM nodes + if ( typeof elem === "string" && !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); - // link all the functions, so any of them can unbind this click handler - while( i < args.length ) - jQuery.event.proxy( fn, args[i++] ); + } else if ( typeof elem === "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, fcloseTag); - return this.click( jQuery.event.proxy( fn, function(event) { - // Figure out which function to execute - this.lastToggle = ( this.lastToggle || 0 ) % i; + // Trim whitespace, otherwise indexOf won't work as expected + var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(), + wrap = wrapMap[ tag ] || wrapMap._default, + depth = wrap[0], + div = context.createElement("div"); - // Make sure that clicks stop - event.preventDefault(); + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + elem + wrap[2]; - // and execute the function - return args[ this.lastToggle++ ].apply( this, arguments ) || false; - })); - }, + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } - hover: function(fnOver, fnOut) { - return this.mouseenter(fnOver).mouseleave(fnOut); - }, + // Remove IE's autoinserted <tbody> from table fragments + if ( !jQuery.support.tbody ) { - ready: function(fn) { - // Attach the listeners - bindReady(); + // String was a <table>, *may* have spurious <tbody> + var hasBody = rtbody.test(elem), + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : - // If the DOM is already ready - if ( jQuery.isReady ) - // Execute the function immediately - fn.call( document, jQuery ); + // String was a bare <thead> or <tfoot> + wrap[1] === "<table>" && !hasBody ? + div.childNodes : + []; - // Otherwise, remember the function for later - else - // Add the function to the wait list - jQuery.readyList.push( fn ); + for ( var j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } + } - return this; - }, - - live: function( type, fn ){ - var proxy = jQuery.event.proxy( fn ); - proxy.guid += this.selector + type; + } - jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy ); + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } - return this; + elem = div.childNodes; + } + + if ( elem.nodeType ) { + ret.push( elem ); + } else { + ret = jQuery.merge( ret, elem ); + } + } + + if ( fragment ) { + for ( var i = 0; ret[i]; i++ ) { + if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { + scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); + + } else { + if ( ret[i].nodeType === 1 ) { + ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) ); + } + fragment.appendChild( ret[i] ); + } + } + } + + return ret; }, - die: function( type, fn ){ - jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null ); - return this; + cleanData: function( elems ) { + var data, id, cache = jQuery.cache, + special = jQuery.event.special, + deleteExpando = jQuery.support.deleteExpando; + + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + id = elem[ jQuery.expando ]; + + if ( id ) { + data = cache[ id ]; + + if ( data.events ) { + for ( var type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + } else { + removeEvent( elem, type, data.handle ); + } + } + } + + if ( deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } + + delete cache[ id ]; + } + } } }); +// exclude the following css properties to add px +var rexclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + ralpha = /alpha\([^)]*\)/, + ropacity = /opacity=([^)]*)/, + rfloat = /float/i, + rdashAlpha = /-([a-z])/ig, + rupper = /([A-Z])/g, + rnumpx = /^-?\d+(?:px)?$/i, + rnum = /^-?\d/, + + cssShow = { position: "absolute", visibility: "hidden", display:"block" }, + cssWidth = [ "Left", "Right" ], + cssHeight = [ "Top", "Bottom" ], + + // cache check for defaultView.getComputedStyle + getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, + // normalize float css property + styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat", + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; -function liveHandler( event ){ - var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"), - stop = true, - elems = []; - - jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){ - if ( check.test(fn.type) ) { - var elem = jQuery(event.target).closest(fn.data)[0]; - if ( elem ) - elems.push({ elem: elem, fn: fn }); +jQuery.fn.css = function( name, value ) { + return access( this, name, value, true, function( elem, name, value ) { + if ( value === undefined ) { + return jQuery.curCSS( elem, name ); + } + + if ( typeof value === "number" && !rexclude.test(name) ) { + value += "px"; } - }); - elems.sort(function(a,b) { - return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest"); - }); - - jQuery.each(elems, function(){ - if ( this.fn.call(this.elem, event, this.fn.data) === false ) - return (stop = false); + jQuery.style( elem, name, value ); }); +}; - return stop; -} +jQuery.extend({ + style: function( elem, name, value ) { + // don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } -function liveConvert(type, selector){ - return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join("."); -} + // ignore negative width and height values #1599 + if ( (name === "width" || name === "height") && parseFloat(value) < 0 ) { + value = undefined; + } -jQuery.extend({ - isReady: false, - readyList: [], - // Handle when the DOM is ready - ready: function() { - // Make sure that the DOM is not already loaded - if ( !jQuery.isReady ) { - // Remember that the DOM is ready - jQuery.isReady = true; + var style = elem.style || elem, set = value !== undefined; - // If there are functions bound, to execute - if ( jQuery.readyList ) { - // Execute all of them - jQuery.each( jQuery.readyList, function(){ - this.call( document, jQuery ); - }); + // IE uses filters for opacity + if ( !jQuery.support.opacity && name === "opacity" ) { + if ( set ) { + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + style.zoom = 1; - // Reset the list of functions - jQuery.readyList = null; + // Set the alpha filter to set the opacity + var opacity = parseInt( value, 10 ) + "" === "NaN" ? "" : "alpha(opacity=" + value * 100 + ")"; + var filter = style.filter || jQuery.curCSS( elem, "filter" ) || ""; + style.filter = ralpha.test(filter) ? filter.replace(ralpha, opacity) : opacity; } - // Trigger any bound ready events - jQuery(document).triggerHandler("ready"); + return style.filter && style.filter.indexOf("opacity=") >= 0 ? + (parseFloat( ropacity.exec(style.filter)[1] ) / 100) + "": + ""; } - } -}); -var readyBound = false; + // Make sure we're using the right name for getting the float value + if ( rfloat.test( name ) ) { + name = styleFloat; + } -function bindReady(){ - if ( readyBound ) return; - readyBound = true; + name = name.replace(rdashAlpha, fcamelCase); - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", function(){ - document.removeEventListener( "DOMContentLoaded", arguments.callee, false ); - jQuery.ready(); - }, false ); + if ( set ) { + style[ name ] = value; + } + + return style[ name ]; + }, - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent("onreadystatechange", function(){ - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", arguments.callee ); - jQuery.ready(); + css: function( elem, name, force, extra ) { + if ( name === "width" || name === "height" ) { + var val, props = cssShow, which = name === "width" ? cssWidth : cssHeight; + + function getWH() { + val = name === "width" ? elem.offsetWidth : elem.offsetHeight; + + if ( extra === "border" ) { + return; + } + + jQuery.each( which, function() { + if ( !extra ) { + val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + } + + if ( extra === "margin" ) { + val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; + } else { + val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + } + }); } - }); - // If IE and not an iframe - // continually check to see if the document is ready - if ( document.documentElement.doScroll && window == window.top ) (function(){ - if ( jQuery.isReady ) return; + if ( elem.offsetWidth !== 0 ) { + getWH(); + } else { + jQuery.swap( elem, props, getWH ); + } - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch( error ) { - setTimeout( arguments.callee, 0 ); - return; + return Math.max(0, Math.round(val)); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style, filter; + + // IE uses filters for opacity + if ( !jQuery.support.opacity && name === "opacity" && elem.currentStyle ) { + ret = ropacity.test(elem.currentStyle.filter || "") ? + (parseFloat(RegExp.$1) / 100) + "" : + ""; + + return ret === "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if ( rfloat.test( name ) ) { + name = styleFloat; + } + + if ( !force && style && style[ name ] ) { + ret = style[ name ]; + + } else if ( getComputedStyle ) { + + // Only "float" is needed here + if ( rfloat.test( name ) ) { + name = "float"; } - // and execute any waiting functions - jQuery.ready(); - })(); - } + name = name.replace( rupper, "-$1" ).toLowerCase(); - // A fallback to window.onload, that will always work - jQuery.event.add( window, "load", jQuery.ready ); -} + var defaultView = elem.ownerDocument.defaultView; -jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," + - "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," + - "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){ + if ( !defaultView ) { + return null; + } - // Handle event binding - jQuery.fn[name] = function(fn){ - return fn ? this.bind(name, fn) : this.trigger(name); - }; -}); + var computedStyle = defaultView.getComputedStyle( elem, null ); -// Prevent memory leaks in IE -// And prevent errors on refresh with events like mouseover in other browsers -// Window isn't included so as not to unbind existing unload events -jQuery( window ).bind( 'unload', function(){ - for ( var id in jQuery.cache ) - // Skip the window - if ( id != 1 && jQuery.cache[ id ].handle ) - jQuery.event.remove( jQuery.cache[ id ].handle.elem ); -}); -(function(){ + if ( computedStyle ) { + ret = computedStyle.getPropertyValue( name ); + } - jQuery.support = {}; + // We should always get a number back from opacity + if ( name === "opacity" && ret === "" ) { + ret = "1"; + } - var root = document.documentElement, - script = document.createElement("script"), - div = document.createElement("div"), - id = "script" + (new Date).getTime(); + } else if ( elem.currentStyle ) { + var camelCase = name.replace(rdashAlpha, fcamelCase); - div.style.display = "none"; - div.innerHTML = ' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>'; + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; - var all = div.getElementsByTagName("*"), - a = div.getElementsByTagName("a")[0]; + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - // Can't get basic test support - if ( !all || !all.length || !a ) { - return; - } + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; - jQuery.support = { - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType == 3, - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that you can get all elements in an <object> element - // IE 7 always returns no results - objectAll: !!div.getElementsByTagName("object")[0] - .getElementsByTagName("*").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText insted) - style: /red/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", - - // Make sure that element opacity exists - // (IE uses filter instead) - opacity: a.style.opacity === "0.5", - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = camelCase === "fontSize" ? "1em" : (ret || 0); + ret = style.pixelLeft + "px"; - // Will be defined later - scriptEval: false, - noCloneEvent: true, - boxModel: null - }; - - script.type = "text/javascript"; - try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e){} + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } - root.insertBefore( script, root.firstChild ); - - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support.scriptEval = true; - delete window[ id ]; - } + return ret; + }, - root.removeChild( script ); + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; - if ( div.attachEvent && div.fireEvent ) { - div.attachEvent("onclick", function(){ - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - jQuery.support.noCloneEvent = false; - div.detachEvent("onclick", arguments.callee); - }); - div.cloneNode(true).fireEvent("onclick"); + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) { + elem.style[ name ] = old[ name ]; + } } +}); - // Figure out if the W3C box model works as expected - // document.body must exist before we can do this - jQuery(function(){ - var div = document.createElement("div"); - div.style.width = div.style.paddingLeft = "1px"; +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.hidden = function( elem ) { + var width = elem.offsetWidth, height = elem.offsetHeight, + skip = elem.nodeName.toLowerCase() === "tr"; - document.body.appendChild( div ); - jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; - document.body.removeChild( div ).style.display = 'none'; - }); -})(); + return width === 0 && height === 0 && !skip ? + true : + width > 0 && height > 0 && !skip ? + false : + jQuery.curCSS(elem, "display") === "none"; + }; -var styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat"; + jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); + }; +} +var jsc = now(), + rscript = /<script(.|\s)*?\/script>/gi, + rselectTextarea = /select|textarea/i, + rinput = /color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i, + jsre = /=\?(&|$)/, + rquery = /\?/, + rts = /(\?|&)_=.*?(&|$)/, + rurl = /^(\w+:)?\/\/([^\/?#]+)/, + r20 = /%20/g, + + // Keep a copy of the old load method + _load = jQuery.fn.load; -jQuery.props = { - "for": "htmlFor", - "class": "className", - "float": styleFloat, - cssFloat: styleFloat, - styleFloat: styleFloat, - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - tabindex: "tabIndex" -}; jQuery.fn.extend({ - // Keep a copy of the old load - _load: jQuery.fn.load, - load: function( url, params, callback ) { - if ( typeof url !== "string" ) - return this._load( url ); + if ( typeof url !== "string" ) { + return _load.call( this, url ); + + // Don't do a request if no elements are being requested + } else if ( !this.length ) { + return this; + } var off = url.indexOf(" "); if ( off >= 0 ) { @@ -3239,7 +4801,7 @@ jQuery.fn.extend({ var type = "GET"; // If the second parameter was provided - if ( params ) + if ( params ) { // If it's a function if ( jQuery.isFunction( params ) ) { // We assume that it's the callback @@ -3247,10 +4809,11 @@ jQuery.fn.extend({ params = null; // Otherwise, build a param string - } else if( typeof params === "object" ) { - params = jQuery.param( params ); + } else if ( typeof params === "object" ) { + params = jQuery.param( params, jQuery.ajaxSettings.traditional ); type = "POST"; } + } var self = this; @@ -3260,27 +4823,30 @@ jQuery.fn.extend({ type: type, dataType: "html", data: params, - complete: function(res, status){ + complete: function( res, status ) { // If successful, inject the HTML into all the matched elements - if ( status == "success" || status == "notmodified" ) + if ( status === "success" || status === "notmodified" ) { // See if a selector was specified self.html( selector ? // Create a dummy div to hold the results - jQuery("<div/>") + jQuery("<div />") // inject the contents of the document in, removing the scripts // to avoid any 'Permission Denied' errors in IE - .append(res.responseText.replace(/<script(.|\s)*?\/script>/g, "")) + .append(res.responseText.replace(rscript, "")) // Locate the specified elements .find(selector) : // If not, just inject the full result res.responseText ); + } - if( callback ) + if ( callback ) { self.each( callback, [res.responseText, status, res] ); + } } }); + return this; }, @@ -3288,40 +4854,41 @@ jQuery.fn.extend({ return jQuery.param(this.serializeArray()); }, serializeArray: function() { - return this.map(function(){ + return this.map(function() { return this.elements ? jQuery.makeArray(this.elements) : this; }) - .filter(function(){ + .filter(function() { return this.name && !this.disabled && - (this.checked || /select|textarea/i.test(this.nodeName) || - /text|hidden|password|search/i.test(this.type)); + (this.checked || rselectTextarea.test(this.nodeName) || + rinput.test(this.type)); }) - .map(function(i, elem){ + .map(function( i, elem ) { var val = jQuery(this).val(); - return val == null ? null : + + return val == null ? + null : jQuery.isArray(val) ? - jQuery.map( val, function(val, i){ - return {name: elem.name, value: val}; + jQuery.map( val, function( val, i ) { + return { name: elem.name, value: val }; }) : - {name: elem.name, value: val}; + { name: elem.name, value: val }; }).get(); } }); // Attach a bunch of functions for handling common AJAX events -jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){ - jQuery.fn[o] = function(f){ +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) { + jQuery.fn[o] = function( f ) { return this.bind(o, f); }; }); -var jsc = now(); - jQuery.extend({ - + get: function( url, data, callback, type ) { - // shift arguments if data argument was ommited + // shift arguments if data argument was omited if ( jQuery.isFunction( data ) ) { + type = type || callback; callback = data; data = null; } @@ -3344,7 +4911,9 @@ jQuery.extend({ }, post: function( url, data, callback, type ) { + // shift arguments if data argument was omited if ( jQuery.isFunction( data ) ) { + type = type || callback; callback = data; data = {}; } @@ -3374,13 +4943,21 @@ jQuery.extend({ data: null, username: null, password: null, + traditional: false, */ // Create the request object; Microsoft failed to properly - // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available + // implement the XMLHttpRequest in IE7 (can't request local files), + // so we use the ActiveXObject when it is available // This function can be overriden by calling jQuery.ajaxSetup - xhr:function(){ - return window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest(); - }, + xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ? + function() { + return new window.XMLHttpRequest(); + } : + function() { + try { + return new window.ActiveXObject("Microsoft.XMLHTTP"); + } catch(e) {} + }, accepts: { xml: "application/xml, text/xml", html: "text/html", @@ -3393,36 +4970,41 @@ jQuery.extend({ // Last-Modified header cache for next request lastModified: {}, + etag: {}, - ajax: function( s ) { - // Extend the settings, but re-extend 's' so that it can be - // checked again later (in the test suite, specifically) - s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s)); - - var jsonp, jsre = /=\?(&|$)/g, status, data, + ajax: function( origSettings ) { + var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings); + + var jsonp, status, data, + callbackContext = origSettings && origSettings.context || s, type = s.type.toUpperCase(); // convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) - s.data = jQuery.param(s.data); + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } // Handle JSONP Parameter Callbacks - if ( s.dataType == "jsonp" ) { - if ( type == "GET" ) { - if ( !s.url.match(jsre) ) - s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?"; - } else if ( !s.data || !s.data.match(jsre) ) + if ( s.dataType === "jsonp" ) { + if ( type === "GET" ) { + if ( !jsre.test( s.url ) ) { + s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?"; + } + } else if ( !s.data || !jsre.test(s.data) ) { s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?"; + } s.dataType = "json"; } // Build temporary JSONP function - if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) { - jsonp = "jsonp" + jsc++; + if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) { + jsonp = s.jsonpCallback || ("jsonp" + jsc++); // Replace the =? sequence both in the query string and the data - if ( s.data ) + if ( s.data ) { s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1"); + } + s.url = s.url.replace(jsre, "=" + jsonp + "$1"); // We need to make sure @@ -3430,75 +5012,85 @@ jQuery.extend({ s.dataType = "script"; // Handle JSONP-style loading - window[ jsonp ] = function(tmp){ + window[ jsonp ] = window[ jsonp ] || function( tmp ) { data = tmp; success(); complete(); // Garbage collect window[ jsonp ] = undefined; - try{ delete window[ jsonp ]; } catch(e){} - if ( head ) + + try { + delete window[ jsonp ]; + } catch(e) {} + + if ( head ) { head.removeChild( script ); + } }; } - if ( s.dataType == "script" && s.cache == null ) + if ( s.dataType === "script" && s.cache === null ) { s.cache = false; + } - if ( s.cache === false && type == "GET" ) { + if ( s.cache === false && type === "GET" ) { var ts = now(); + // try replacing _= if it is there - var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2"); + var ret = s.url.replace(rts, "$1_=" + ts + "$2"); + // if nothing was replaced, add timestamp to the end - s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : ""); + s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : ""); } // If data is available, append data to url for get requests - if ( s.data && type == "GET" ) { - s.url += (s.url.match(/\?/) ? "&" : "?") + s.data; - - // IE likes to send both get and post data, prevent this - s.data = null; + if ( s.data && type === "GET" ) { + s.url += (rquery.test(s.url) ? "&" : "?") + s.data; } // Watch for a new set of requests - if ( s.global && ! jQuery.active++ ) + if ( s.global && ! jQuery.active++ ) { jQuery.event.trigger( "ajaxStart" ); + } // Matches an absolute URL, and saves the domain - var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url ); + var parts = rurl.exec( s.url ), + remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); // If we're requesting a remote document // and trying to load JSON or Script with a GET - if ( s.dataType == "script" && type == "GET" && parts - && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){ - - var head = document.getElementsByTagName("head")[0]; + if ( s.dataType === "script" && type === "GET" && remote ) { + var head = document.getElementsByTagName("head")[0] || document.documentElement; var script = document.createElement("script"); script.src = s.url; - if (s.scriptCharset) + if ( s.scriptCharset ) { script.charset = s.scriptCharset; + } // Handle Script loading if ( !jsonp ) { var done = false; // Attach handlers for all browsers - script.onload = script.onreadystatechange = function(){ + script.onload = script.onreadystatechange = function() { if ( !done && (!this.readyState || - this.readyState == "loaded" || this.readyState == "complete") ) { + this.readyState === "loaded" || this.readyState === "complete") ) { done = true; success(); complete(); // Handle memory leak in IE script.onload = script.onreadystatechange = null; - head.removeChild( script ); + if ( head && script.parentNode ) { + head.removeChild( script ); + } } }; } - head.appendChild(script); + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); // We handle everything using the script element injection return undefined; @@ -3509,158 +5101,197 @@ jQuery.extend({ // Create the request object var xhr = s.xhr(); + if ( !xhr ) { + return; + } + // Open the socket // Passing null username, generates a login popup on Opera (#2865) - if( s.username ) + if ( s.username ) { xhr.open(type, s.url, s.async, s.username, s.password); - else + } else { xhr.open(type, s.url, s.async); + } // Need an extra try/catch for cross domain requests in Firefox 3 try { // Set the correct header, if data is being sent - if ( s.data ) + if ( s.data || origSettings && origSettings.contentType ) { xhr.setRequestHeader("Content-Type", s.contentType); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[s.url] ) { + xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]); + } - // Set the If-Modified-Since header, if ifModified mode. - if ( s.ifModified ) - xhr.setRequestHeader("If-Modified-Since", - jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" ); + if ( jQuery.etag[s.url] ) { + xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]); + } + } // Set header so the called script knows that it's an XMLHttpRequest - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + // Only send the header if it's not a remote XHR + if ( !remote ) { + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + } // Set the Accepts header for the server, depending on the dataType xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ? s.accepts[ s.dataType ] + ", */*" : s.accepts._default ); - } catch(e){} + } catch(e) {} // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && s.beforeSend(xhr, s) === false ) { + if ( s.beforeSend && s.beforeSend.call(callbackContext, xhr, s) === false ) { // Handle the global AJAX counter - if ( s.global && ! --jQuery.active ) + if ( s.global && ! --jQuery.active ) { jQuery.event.trigger( "ajaxStop" ); + } + // close opended socket xhr.abort(); return false; } - if ( s.global ) - jQuery.event.trigger("ajaxSend", [xhr, s]); + if ( s.global ) { + trigger("ajaxSend", [xhr, s]); + } // Wait for a response to come back - var onreadystatechange = function(isTimeout){ - // The request was aborted, clear the interval and decrement jQuery.active - if (xhr.readyState == 0) { - if (ival) { - // clear poll interval - clearInterval(ival); - ival = null; - // Handle the global AJAX counter - if ( s.global && ! --jQuery.active ) - jQuery.event.trigger( "ajaxStop" ); + var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) { + // The request was aborted + if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) { + // Opera doesn't call onreadystatechange before this point + // so we simulate the call + if ( !requestDone ) { + complete(); + } + + requestDone = true; + if ( xhr ) { + xhr.onreadystatechange = jQuery.noop; } + // The transfer is complete and the data is available, or the request timed out - } else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) { + } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) { requestDone = true; + xhr.onreadystatechange = jQuery.noop; - // clear poll interval - if (ival) { - clearInterval(ival); - ival = null; - } + status = isTimeout === "timeout" ? + "timeout" : + !jQuery.httpSuccess( xhr ) ? + "error" : + s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? + "notmodified" : + "success"; - status = isTimeout == "timeout" ? "timeout" : - !jQuery.httpSuccess( xhr ) ? "error" : - s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" : - "success"; + var errMsg; - if ( status == "success" ) { + if ( status === "success" ) { // Watch for, and catch, XML document parse errors try { // process the data (runs the xml through httpData regardless of callback) data = jQuery.httpData( xhr, s.dataType, s ); - } catch(e) { + } catch(err) { status = "parsererror"; + errMsg = err; } } // Make sure that the request was successful or notmodified - if ( status == "success" ) { - // Cache Last-Modified header, if ifModified mode. - var modRes; - try { - modRes = xhr.getResponseHeader("Last-Modified"); - } catch(e) {} // swallow exception thrown by FF if header is not available - - if ( s.ifModified && modRes ) - jQuery.lastModified[s.url] = modRes; - + if ( status === "success" || status === "notmodified" ) { // JSONP handles its own success callback - if ( !jsonp ) + if ( !jsonp ) { success(); - } else - jQuery.handleError(s, xhr, status); + } + } else { + jQuery.handleError(s, xhr, status, errMsg); + } // Fire the complete handlers complete(); - if ( isTimeout ) + if ( isTimeout === "timeout" ) { xhr.abort(); + } // Stop memory leaks - if ( s.async ) + if ( s.async ) { xhr = null; + } } }; - if ( s.async ) { - // don't attach the handler to the request, just poll it instead - var ival = setInterval(onreadystatechange, 13); + // Override the abort handler, if we can (IE doesn't allow it, but that's OK) + // Opera doesn't fire onreadystatechange at all on abort + try { + var oldAbort = xhr.abort; + xhr.abort = function() { + if ( xhr ) { + oldAbort.call( xhr ); + } - // Timeout checker - if ( s.timeout > 0 ) - setTimeout(function(){ - // Check to see if the request is still happening - if ( xhr && !requestDone ) - onreadystatechange( "timeout" ); - }, s.timeout); + onreadystatechange( "abort" ); + }; + } catch(e) { } + + // Timeout checker + if ( s.async && s.timeout > 0 ) { + setTimeout(function() { + // Check to see if the request is still happening + if ( xhr && !requestDone ) { + onreadystatechange( "timeout" ); + } + }, s.timeout); } // Send the data try { - xhr.send(s.data); + xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null ); } catch(e) { jQuery.handleError(s, xhr, null, e); + // Fire the complete handlers + complete(); } // firefox 1.5 doesn't fire statechange for sync requests - if ( !s.async ) + if ( !s.async ) { onreadystatechange(); + } - function success(){ + function success() { // If a local callback was specified, fire it and pass it the data - if ( s.success ) - s.success( data, status ); + if ( s.success ) { + s.success.call( callbackContext, data, status, xhr ); + } // Fire the global callback - if ( s.global ) - jQuery.event.trigger( "ajaxSuccess", [xhr, s] ); + if ( s.global ) { + trigger( "ajaxSuccess", [xhr, s] ); + } } - function complete(){ + function complete() { // Process result - if ( s.complete ) - s.complete(xhr, status); + if ( s.complete ) { + s.complete.call( callbackContext, xhr, status); + } // The request was completed - if ( s.global ) - jQuery.event.trigger( "ajaxComplete", [xhr, s] ); + if ( s.global ) { + trigger( "ajaxComplete", [xhr, s] ); + } // Handle the global AJAX counter - if ( s.global && ! --jQuery.active ) + if ( s.global && ! --jQuery.active ) { jQuery.event.trigger( "ajaxStop" ); + } + } + + function trigger(type, args) { + (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args); } // return XMLHttpRequest to allow aborting the request etc. @@ -3669,11 +5300,14 @@ jQuery.extend({ handleError: function( s, xhr, status, e ) { // If a local callback was specified, fire it - if ( s.error ) s.error( xhr, status, e ); + if ( s.error ) { + s.error.call( s.context || s, xhr, status, e ); + } // Fire the global callback - if ( s.global ) - jQuery.event.trigger( "ajaxError", [xhr, s, e] ); + if ( s.global ) { + (s.context ? jQuery(s.context) : jQuery.event).trigger( "ajaxError", [xhr, s, e] ); + } }, // Counter for holding the number of active queries @@ -3683,86 +5317,131 @@ jQuery.extend({ httpSuccess: function( xhr ) { try { // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450 - return !xhr.status && location.protocol == "file:" || - ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223; - } catch(e){} + return !xhr.status && location.protocol === "file:" || + // Opera returns 0 when status is 304 + ( xhr.status >= 200 && xhr.status < 300 ) || + xhr.status === 304 || xhr.status === 1223 || xhr.status === 0; + } catch(e) {} + return false; }, // Determines if an XMLHttpRequest returns NotModified httpNotModified: function( xhr, url ) { - try { - var xhrRes = xhr.getResponseHeader("Last-Modified"); + var lastModified = xhr.getResponseHeader("Last-Modified"), + etag = xhr.getResponseHeader("Etag"); - // Firefox always returns 200. check Last-Modified date - return xhr.status == 304 || xhrRes == jQuery.lastModified[url]; - } catch(e){} - return false; + if ( lastModified ) { + jQuery.lastModified[url] = lastModified; + } + + if ( etag ) { + jQuery.etag[url] = etag; + } + + // Opera returns 0 when status is 304 + return xhr.status === 304 || xhr.status === 0; }, httpData: function( xhr, type, s ) { - var ct = xhr.getResponseHeader("content-type"), - xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0, + var ct = xhr.getResponseHeader("content-type") || "", + xml = type === "xml" || !type && ct.indexOf("xml") >= 0, data = xml ? xhr.responseXML : xhr.responseText; - if ( xml && data.documentElement.tagName == "parsererror" ) - throw "parsererror"; - + if ( xml && data.documentElement.nodeName === "parsererror" ) { + jQuery.error( "parsererror" ); + } + // Allow a pre-filtering function to sanitize the response - // s != null is checked to keep backwards compatibility - if( s && s.dataFilter ) + // s is checked to keep backwards compatibility + if ( s && s.dataFilter ) { data = s.dataFilter( data, type ); + } // The filter can actually parse the response - if( typeof data === "string" ){ + if ( typeof data === "string" ) { + // Get the JavaScript object, if JSON is used. + if ( type === "json" || !type && ct.indexOf("json") >= 0 ) { + data = jQuery.parseJSON( data ); // If the type is "script", eval it in global context - if ( type == "script" ) + } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) { jQuery.globalEval( data ); - - // Get the JavaScript object, if JSON is used. - if ( type == "json" ) - data = window["eval"]("(" + data + ")"); + } } - + return data; }, // Serialize an array of form elements or a set of // key/values into a query string - param: function( a ) { - var s = [ ]; - - function add( key, value ){ - s[ s.length ] = encodeURIComponent(key) + '=' + encodeURIComponent(value); - }; - - // If an array was passed in, assume that it is an array - // of form elements - if ( jQuery.isArray(a) || a.jquery ) + param: function( a, traditional ) { + var s = []; + + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings.traditional; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray(a) || a.jquery ) { // Serialize the form elements - jQuery.each( a, function(){ + jQuery.each( a, function() { add( this.name, this.value ); }); - - // Otherwise, assume that it's an object of key/value pairs - else - // Serialize the key/values - for ( var j in a ) - // If the value is an array then the key names need to be repeated - if ( jQuery.isArray(a[j]) ) - jQuery.each( a[j], function(){ - add( j, this ); - }); - else - add( j, jQuery.isFunction(a[j]) ? a[j]() : a[j] ); + + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( var prefix in a ) { + buildParams( prefix, a[prefix] ); + } + } // Return the resulting serialization - return s.join("&").replace(/%20/g, "+"); - } + return s.join("&").replace(r20, "+"); + + function buildParams( prefix, obj ) { + if ( jQuery.isArray(obj) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || /\[\]$/.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + } else { + // If array item is non-scalar (array or object), encode its + // numeric index to resolve deserialization ambiguity issues. + // Note that rack (as of 1.0.0) can't currently deserialize + // nested arrays properly, and attempting to do so may cause + // a server error. Possible fixes are to modify rack's + // deserialization algorithm or to provide an option or flag + // to force array serialization to be shallow. + buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v ); + } + }); + + } else if ( !traditional && obj != null && typeof obj === "object" ) { + // Serialize object item. + jQuery.each( obj, function( k, v ) { + buildParams( prefix + "[" + k + "]", v ); + }); + + } else { + // Serialize scalar item. + add( prefix, obj ); + } + } + function add( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction(value) ? value() : value; + s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value); + } + } }); var elemdisplay = {}, + rfxtypes = /toggle|show|hide/, + rfxnum = /^([+-]=)?([\d+-.]+)(.*)$/, timerId, fxAttrs = [ // height animations @@ -3773,69 +5452,67 @@ var elemdisplay = {}, [ "opacity" ] ]; -function genFx( type, num ){ - var obj = {}; - jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function(){ - obj[ this ] = type; - }); - return obj; -} - jQuery.fn.extend({ - show: function(speed,callback){ - if ( speed ) { + show: function( speed, callback ) { + if ( speed || speed === 0) { return this.animate( genFx("show", 3), speed, callback); + } else { - for ( var i = 0, l = this.length; i < l; i++ ){ + for ( var i = 0, l = this.length; i < l; i++ ) { var old = jQuery.data(this[i], "olddisplay"); - + this[i].style.display = old || ""; - + if ( jQuery.css(this[i], "display") === "none" ) { - var tagName = this[i].tagName, display; - - if ( elemdisplay[ tagName ] ) { - display = elemdisplay[ tagName ]; + var nodeName = this[i].nodeName, display; + + if ( elemdisplay[ nodeName ] ) { + display = elemdisplay[ nodeName ]; + } else { - var elem = jQuery("<" + tagName + " />").appendTo("body"); - + var elem = jQuery("<" + nodeName + " />").appendTo("body"); + display = elem.css("display"); - if ( display === "none" ) + + if ( display === "none" ) { display = "block"; - + } + elem.remove(); - - elemdisplay[ tagName ] = display; + + elemdisplay[ nodeName ] = display; } - + jQuery.data(this[i], "olddisplay", display); } } // Set the display of the elements in a second loop // to avoid the constant reflow - for ( var i = 0, l = this.length; i < l; i++ ){ - this[i].style.display = jQuery.data(this[i], "olddisplay") || ""; + for ( var j = 0, k = this.length; j < k; j++ ) { + this[j].style.display = jQuery.data(this[j], "olddisplay") || ""; } - + return this; } }, - hide: function(speed,callback){ - if ( speed ) { + hide: function( speed, callback ) { + if ( speed || speed === 0 ) { return this.animate( genFx("hide", 3), speed, callback); + } else { - for ( var i = 0, l = this.length; i < l; i++ ){ + for ( var i = 0, l = this.length; i < l; i++ ) { var old = jQuery.data(this[i], "olddisplay"); - if ( !old && old !== "none" ) + if ( !old && old !== "none" ) { jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display")); + } } // Set the display of the elements in a second loop // to avoid the constant reflow - for ( var i = 0, l = this.length; i < l; i++ ){ - this[i].style.display = "none"; + for ( var j = 0, k = this.length; j < k; j++ ) { + this[j].style.display = "none"; } return this; @@ -3845,77 +5522,107 @@ jQuery.fn.extend({ // Save the old toggle function _toggle: jQuery.fn.toggle, - toggle: function( fn, fn2 ){ + toggle: function( fn, fn2 ) { var bool = typeof fn === "boolean"; - return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ? - this._toggle.apply( this, arguments ) : - fn == null || bool ? - this.each(function(){ - var state = bool ? fn : jQuery(this).is(":hidden"); - jQuery(this)[ state ? "show" : "hide" ](); - }) : - this.animate(genFx("toggle", 3), fn, fn2); + if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) { + this._toggle.apply( this, arguments ); + + } else if ( fn == null || bool ) { + this.each(function() { + var state = bool ? fn : jQuery(this).is(":hidden"); + jQuery(this)[ state ? "show" : "hide" ](); + }); + + } else { + this.animate(genFx("toggle", 3), fn, fn2); + } + + return this; }, - fadeTo: function(speed,to,callback){ - return this.animate({opacity: to}, speed, callback); + fadeTo: function( speed, to, callback ) { + return this.filter(":hidden").css("opacity", 0).show().end() + .animate({opacity: to}, speed, callback); }, animate: function( prop, speed, easing, callback ) { var optall = jQuery.speed(speed, easing, callback); - return this[ optall.queue === false ? "each" : "queue" ](function(){ - + if ( jQuery.isEmptyObject( prop ) ) { + return this.each( optall.complete ); + } + + return this[ optall.queue === false ? "each" : "queue" ](function() { var opt = jQuery.extend({}, optall), p, - hidden = this.nodeType == 1 && jQuery(this).is(":hidden"), + hidden = this.nodeType === 1 && jQuery(this).is(":hidden"), self = this; - + for ( p in prop ) { - if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden ) + var name = p.replace(rdashAlpha, fcamelCase); + + if ( p !== name ) { + prop[ name ] = prop[ p ]; + delete prop[ p ]; + p = name; + } + + if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) { return opt.complete.call(this); + } - if ( ( p == "height" || p == "width" ) && this.style ) { + if ( ( p === "height" || p === "width" ) && this.style ) { // Store display property opt.display = jQuery.css(this, "display"); // Make sure that nothing sneaks out opt.overflow = this.style.overflow; } + + if ( jQuery.isArray( prop[p] ) ) { + // Create (if needed) and add to specialEasing + (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1]; + prop[p] = prop[p][0]; + } } - if ( opt.overflow != null ) + if ( opt.overflow != null ) { this.style.overflow = "hidden"; + } opt.curAnim = jQuery.extend({}, prop); - jQuery.each( prop, function(name, val){ + jQuery.each( prop, function( name, val ) { var e = new jQuery.fx( self, opt, name ); - if ( /toggle|show|hide/.test(val) ) - e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop ); - else { - var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/), + if ( rfxtypes.test(val) ) { + e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop ); + + } else { + var parts = rfxnum.exec(val), start = e.cur(true) || 0; if ( parts ) { - var end = parseFloat(parts[2]), + var end = parseFloat( parts[2] ), unit = parts[3] || "px"; // We need to compute starting value - if ( unit != "px" ) { + if ( unit !== "px" ) { self.style[ name ] = (end || 1) + unit; start = ((end || 1) / e.cur(true)) * start; self.style[ name ] = start + unit; } // If a +=/-= token was provided, we're doing a relative animation - if ( parts[1] ) - end = ((parts[1] == "-=" ? -1 : 1) * end) + start; + if ( parts[1] ) { + end = ((parts[1] === "-=" ? -1 : 1) * end) + start; + } e.custom( start, end, unit ); - } else + + } else { e.custom( start, val, "" ); + } } }); @@ -3924,26 +5631,31 @@ jQuery.fn.extend({ }); }, - stop: function(clearQueue, gotoEnd){ + stop: function( clearQueue, gotoEnd ) { var timers = jQuery.timers; - if (clearQueue) + if ( clearQueue ) { this.queue([]); + } - this.each(function(){ + this.each(function() { // go in reverse order so anything added to the queue during the loop is ignored - for ( var i = timers.length - 1; i >= 0; i-- ) - if ( timers[i].elem == this ) { - if (gotoEnd) + for ( var i = timers.length - 1; i >= 0; i-- ) { + if ( timers[i].elem === this ) { + if (gotoEnd) { // force the next step to be the last timers[i](true); + } + timers.splice(i, 1); } + } }); // start the next in the queue if the last step wasn't forced - if (!gotoEnd) + if ( !gotoEnd ) { this.dequeue(); + } return this; } @@ -3957,16 +5669,15 @@ jQuery.each({ slideToggle: genFx("toggle", 1), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" } -}, function( name, props ){ - jQuery.fn[ name ] = function( speed, callback ){ +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, callback ) { return this.animate( props, speed, callback ); }; }); jQuery.extend({ - - speed: function(speed, easing, fn) { - var opt = typeof speed === "object" ? speed : { + speed: function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? speed : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, @@ -3978,11 +5689,13 @@ jQuery.extend({ // Queueing opt.old = opt.complete; - opt.complete = function(){ - if ( opt.queue !== false ) + opt.complete = function() { + if ( opt.queue !== false ) { jQuery(this).dequeue(); - if ( jQuery.isFunction( opt.old ) ) + } + if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); + } }; return opt; @@ -3999,42 +5712,45 @@ jQuery.extend({ timers: [], - fx: function( elem, options, prop ){ + fx: function( elem, options, prop ) { this.options = options; this.elem = elem; this.prop = prop; - if ( !options.orig ) + if ( !options.orig ) { options.orig = {}; + } } }); jQuery.fx.prototype = { - // Simple function for setting a style value - update: function(){ - if ( this.options.step ) + update: function() { + if ( this.options.step ) { this.options.step.call( this.elem, this.now, this ); + } (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); // Set display property to block for height/width animations - if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style ) + if ( ( this.prop === "height" || this.prop === "width" ) && this.elem.style ) { this.elem.style.display = "block"; + } }, // Get the current size - cur: function(force){ - if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) + cur: function( force ) { + if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) { return this.elem[ this.prop ]; + } var r = parseFloat(jQuery.css(this.elem, this.prop, force)); return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0; }, // Start an animation from one number to another - custom: function(from, to, unit){ + custom: function( from, to, unit ) { this.startTime = now(); this.start = from; this.end = to; @@ -4043,47 +5759,36 @@ jQuery.fx.prototype = { this.pos = this.state = 0; var self = this; - function t(gotoEnd){ + function t( gotoEnd ) { return self.step(gotoEnd); } t.elem = this.elem; if ( t() && jQuery.timers.push(t) && !timerId ) { - timerId = setInterval(function(){ - var timers = jQuery.timers; - - for ( var i = 0; i < timers.length; i++ ) - if ( !timers[i]() ) - timers.splice(i--, 1); - - if ( !timers.length ) { - clearInterval( timerId ); - timerId = undefined; - } - }, 13); + timerId = setInterval(jQuery.fx.tick, 13); } }, // Simple 'show' function - show: function(){ + show: function() { // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop ); + this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); this.options.show = true; // Begin the animation // Make sure that we start at a small width/height to avoid any // flash of content - this.custom(this.prop == "width" || this.prop == "height" ? 1 : 0, this.cur()); + this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur()); // Start by showing the element - jQuery(this.elem).show(); + jQuery( this.elem ).show(); }, // Simple 'hide' function - hide: function(){ + hide: function() { // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop ); + this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); this.options.hide = true; // Begin the animation @@ -4091,8 +5796,8 @@ jQuery.fx.prototype = { }, // Each step of an animation - step: function(gotoEnd){ - var t = now(); + step: function( gotoEnd ) { + var t = now(), done = true; if ( gotoEnd || t >= this.options.duration + this.startTime ) { this.now = this.end; @@ -4101,10 +5806,11 @@ jQuery.fx.prototype = { this.options.curAnim[ this.prop ] = true; - var done = true; - for ( var i in this.options.curAnim ) - if ( this.options.curAnim[i] !== true ) + for ( var i in this.options.curAnim ) { + if ( this.options.curAnim[i] !== true ) { done = false; + } + } if ( done ) { if ( this.options.display != null ) { @@ -4112,31 +5818,40 @@ jQuery.fx.prototype = { this.elem.style.overflow = this.options.overflow; // Reset the display - this.elem.style.display = this.options.display; - if ( jQuery.css(this.elem, "display") == "none" ) + var old = jQuery.data(this.elem, "olddisplay"); + this.elem.style.display = old ? old : this.options.display; + + if ( jQuery.css(this.elem, "display") === "none" ) { this.elem.style.display = "block"; + } } // Hide the element if the "hide" operation was done - if ( this.options.hide ) + if ( this.options.hide ) { jQuery(this.elem).hide(); + } // Reset the properties, if the item has been hidden or shown - if ( this.options.hide || this.options.show ) - for ( var p in this.options.curAnim ) - jQuery.attr(this.elem.style, p, this.options.orig[p]); - + if ( this.options.hide || this.options.show ) { + for ( var p in this.options.curAnim ) { + jQuery.style(this.elem, p, this.options.orig[p]); + } + } + // Execute the complete function this.options.complete.call( this.elem ); } return false; + } else { var n = t - this.startTime; this.state = n / this.options.duration; // Perform the easing function, defaults to swing - this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration); + var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop]; + var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear"); + this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration); this.now = this.start + ((this.end - this.start) * this.pos); // Perform the next step of the animation @@ -4145,232 +5860,381 @@ jQuery.fx.prototype = { return true; } - }; jQuery.extend( jQuery.fx, { - speeds:{ + tick: function() { + var timers = jQuery.timers; + + for ( var i = 0; i < timers.length; i++ ) { + if ( !timers[i]() ) { + timers.splice(i--, 1); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + }, + + stop: function() { + clearInterval( timerId ); + timerId = null; + }, + + speeds: { slow: 600, fast: 200, // Default speed _default: 400 }, - step: { - opacity: function(fx){ - jQuery.attr(fx.elem.style, "opacity", fx.now); + step: { + opacity: function( fx ) { + jQuery.style(fx.elem, "opacity", fx.now); }, - _default: function(fx){ - if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) - fx.elem.style[ fx.prop ] = fx.now + fx.unit; - else + _default: function( fx ) { + if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { + fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit; + } else { fx.elem[ fx.prop ] = fx.now; + } } } }); -if ( document.documentElement["getBoundingClientRect"] ) - jQuery.fn.offset = function() { - if ( !this[0] ) return { top: 0, left: 0 }; - if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] ); - var box = this[0].getBoundingClientRect(), doc = this[0].ownerDocument, body = doc.body, docElem = doc.documentElement, + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; + }; +} + +function genFx( type, num ) { + var obj = {}; + + jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() { + obj[ this ] = type; + }); + + return obj; +} +if ( "getBoundingClientRect" in document.documentElement ) { + jQuery.fn.offset = function( options ) { + var elem = this[0]; + + if ( options ) { + return this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + if ( !elem || !elem.ownerDocument ) { + return null; + } + + if ( elem === elem.ownerDocument.body ) { + return jQuery.offset.bodyOffset( elem ); + } + + var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement, clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, - top = box.top + (self.pageYOffset || jQuery.boxModel && docElem.scrollTop || body.scrollTop ) - clientTop, - left = box.left + (self.pageXOffset || jQuery.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft; + top = box.top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop ) - clientTop, + left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft; + return { top: top, left: left }; }; -else - jQuery.fn.offset = function() { - if ( !this[0] ) return { top: 0, left: 0 }; - if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] ); - jQuery.offset.initialized || jQuery.offset.initialize(); - var elem = this[0], offsetParent = elem.offsetParent, prevOffsetParent = elem, +} else { + jQuery.fn.offset = function( options ) { + var elem = this[0]; + + if ( options ) { + return this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + if ( !elem || !elem.ownerDocument ) { + return null; + } + + if ( elem === elem.ownerDocument.body ) { + return jQuery.offset.bodyOffset( elem ); + } + + jQuery.offset.initialize(); + + var offsetParent = elem.offsetParent, prevOffsetParent = elem, doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement, body = doc.body, defaultView = doc.defaultView, - prevComputedStyle = defaultView.getComputedStyle(elem, null), + prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, top = elem.offsetTop, left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { - computedStyle = defaultView.getComputedStyle(elem, null); - top -= elem.scrollTop, left -= elem.scrollLeft; + if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + break; + } + + computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle; + top -= elem.scrollTop; + left -= elem.scrollLeft; + if ( elem === offsetParent ) { - top += elem.offsetTop, left += elem.offsetLeft; - if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName)) ) - top += parseInt( computedStyle.borderTopWidth, 10) || 0, - left += parseInt( computedStyle.borderLeftWidth, 10) || 0; + top += elem.offsetTop; + left += elem.offsetLeft; + + if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) { + top += parseFloat( computedStyle.borderTopWidth ) || 0; + left += parseFloat( computedStyle.borderLeftWidth ) || 0; + } + prevOffsetParent = offsetParent, offsetParent = elem.offsetParent; } - if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) - top += parseInt( computedStyle.borderTopWidth, 10) || 0, - left += parseInt( computedStyle.borderLeftWidth, 10) || 0; + + if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { + top += parseFloat( computedStyle.borderTopWidth ) || 0; + left += parseFloat( computedStyle.borderLeftWidth ) || 0; + } + prevComputedStyle = computedStyle; } - if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) - top += body.offsetTop, + if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) { + top += body.offsetTop; left += body.offsetLeft; + } - if ( prevComputedStyle.position === "fixed" ) - top += Math.max(docElem.scrollTop, body.scrollTop), - left += Math.max(docElem.scrollLeft, body.scrollLeft); + if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + top += Math.max( docElem.scrollTop, body.scrollTop ); + left += Math.max( docElem.scrollLeft, body.scrollLeft ); + } return { top: top, left: left }; }; +} jQuery.offset = { initialize: function() { - if ( this.initialized ) return; - var body = document.body, container = document.createElement('div'), innerDiv, checkDiv, table, td, rules, prop, bodyMarginTop = body.style.marginTop, - html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>'; + var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0, + html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>"; - rules = { position: 'absolute', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px', visibility: 'hidden' }; - for ( prop in rules ) container.style[prop] = rules[prop]; + jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } ); container.innerHTML = html; - body.insertBefore(container, body.firstChild); - innerDiv = container.firstChild, checkDiv = innerDiv.firstChild, td = innerDiv.nextSibling.firstChild.firstChild; + body.insertBefore( container, body.firstChild ); + innerDiv = container.firstChild; + checkDiv = innerDiv.firstChild; + td = innerDiv.nextSibling.firstChild.firstChild; this.doesNotAddBorder = (checkDiv.offsetTop !== 5); this.doesAddBorderForTableAndCells = (td.offsetTop === 5); - innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative'; + checkDiv.style.position = "fixed", checkDiv.style.top = "20px"; + // safari subtracts parent border width here which is 5px + this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15); + checkDiv.style.position = checkDiv.style.top = ""; + + innerDiv.style.overflow = "hidden", innerDiv.style.position = "relative"; this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5); - body.style.marginTop = '1px'; - this.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0); - body.style.marginTop = bodyMarginTop; + this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop); - body.removeChild(container); - this.initialized = true; + body.removeChild( container ); + body = container = innerDiv = checkDiv = table = td = null; + jQuery.offset.initialize = jQuery.noop; }, - bodyOffset: function(body) { - jQuery.offset.initialized || jQuery.offset.initialize(); + bodyOffset: function( body ) { var top = body.offsetTop, left = body.offsetLeft; - if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) - top += parseInt( jQuery.curCSS(body, 'marginTop', true), 10 ) || 0, - left += parseInt( jQuery.curCSS(body, 'marginLeft', true), 10 ) || 0; + + jQuery.offset.initialize(); + + if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) { + top += parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0; + left += parseFloat( jQuery.curCSS(body, "marginLeft", true) ) || 0; + } + return { top: top, left: left }; + }, + + setOffset: function( elem, options, i ) { + // set position first, in-case top/left are set even on static elem + if ( /static/.test( jQuery.curCSS( elem, "position" ) ) ) { + elem.style.position = "relative"; + } + var curElem = jQuery( elem ), + curOffset = curElem.offset(), + curTop = parseInt( jQuery.curCSS( elem, "top", true ), 10 ) || 0, + curLeft = parseInt( jQuery.curCSS( elem, "left", true ), 10 ) || 0; + + if ( jQuery.isFunction( options ) ) { + options = options.call( elem, i, curOffset ); + } + + var props = { + top: (options.top - curOffset.top) + curTop, + left: (options.left - curOffset.left) + curLeft + }; + + if ( "using" in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } } }; jQuery.fn.extend({ position: function() { - var left = 0, top = 0, results; - - if ( this[0] ) { - // Get *real* offsetParent - var offsetParent = this.offsetParent(), - - // Get correct offsets - offset = this.offset(), - parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset(); - - // Subtract element margins - // note: when an element has margin: auto the offsetLeft and marginLeft - // are the same in Safari causing offset.left to incorrectly be 0 - offset.top -= num( this, 'marginTop' ); - offset.left -= num( this, 'marginLeft' ); - - // Add offsetParent borders - parentOffset.top += num( offsetParent, 'borderTopWidth' ); - parentOffset.left += num( offsetParent, 'borderLeftWidth' ); - - // Subtract the two offsets - results = { - top: offset.top - parentOffset.top, - left: offset.left - parentOffset.left - }; + if ( !this[0] ) { + return null; } - return results; + var elem = this[0], + + // Get *real* offsetParent + offsetParent = this.offsetParent(), + + // Get correct offsets + offset = this.offset(), + parentOffset = /^body|html$/i.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); + + // Subtract element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + offset.top -= parseFloat( jQuery.curCSS(elem, "marginTop", true) ) || 0; + offset.left -= parseFloat( jQuery.curCSS(elem, "marginLeft", true) ) || 0; + + // Add offsetParent borders + parentOffset.top += parseFloat( jQuery.curCSS(offsetParent[0], "borderTopWidth", true) ) || 0; + parentOffset.left += parseFloat( jQuery.curCSS(offsetParent[0], "borderLeftWidth", true) ) || 0; + + // Subtract the two offsets + return { + top: offset.top - parentOffset.top, + left: offset.left - parentOffset.left + }; }, offsetParent: function() { - var offsetParent = this[0].offsetParent || document.body; - while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') ) - offsetParent = offsetParent.offsetParent; - return jQuery(offsetParent); + return this.map(function() { + var offsetParent = this.offsetParent || document.body; + while ( offsetParent && (!/^body|html$/i.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent; + }); } }); // Create scrollLeft and scrollTop methods -jQuery.each( ['Left', 'Top'], function(i, name) { - var method = 'scroll' + name; - - jQuery.fn[ method ] = function(val) { - if (!this[0]) return null; +jQuery.each( ["Left", "Top"], function( i, name ) { + var method = "scroll" + name; - return val !== undefined ? + jQuery.fn[ method ] = function(val) { + var elem = this[0], win; + + if ( !elem ) { + return null; + } + if ( val !== undefined ) { // Set the scroll offset - this.each(function() { - this == window || this == document ? - window.scrollTo( - !i ? val : jQuery(window).scrollLeft(), - i ? val : jQuery(window).scrollTop() - ) : + return this.each(function() { + win = getWindow( this ); + + if ( win ) { + win.scrollTo( + !i ? val : jQuery(win).scrollLeft(), + i ? val : jQuery(win).scrollTop() + ); + + } else { this[ method ] = val; - }) : + } + }); + } else { + win = getWindow( elem ); // Return the scroll offset - this[0] == window || this[0] == document ? - self[ i ? 'pageYOffset' : 'pageXOffset' ] || - jQuery.boxModel && document.documentElement[ method ] || - document.body[ method ] : - this[0][ method ]; + return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] : + jQuery.support.boxModel && win.document.documentElement[ method ] || + win.document.body[ method ] : + elem[ method ]; + } }; }); + +function getWindow( elem ) { + return ("scrollTo" in elem && elem.document) ? + elem : + elem.nodeType === 9 ? + elem.defaultView || elem.parentWindow : + false; +} // Create innerHeight, innerWidth, outerHeight and outerWidth methods -jQuery.each([ "Height", "Width" ], function(i, name){ +jQuery.each([ "Height", "Width" ], function( i, name ) { - var tl = i ? "Left" : "Top", // top or left - br = i ? "Right" : "Bottom", // bottom or right - lower = name.toLowerCase(); + var type = name.toLowerCase(); // innerHeight and innerWidth - jQuery.fn["inner" + name] = function(){ + jQuery.fn["inner" + name] = function() { return this[0] ? - jQuery.css( this[0], lower, false, "padding" ) : + jQuery.css( this[0], type, false, "padding" ) : null; }; // outerHeight and outerWidth - jQuery.fn["outer" + name] = function(margin) { + jQuery.fn["outer" + name] = function( margin ) { return this[0] ? - jQuery.css( this[0], lower, false, margin ? "margin" : "border" ) : + jQuery.css( this[0], type, false, margin ? "margin" : "border" ) : null; }; - - var type = name.toLowerCase(); jQuery.fn[ type ] = function( size ) { // Get window width or height - return this[0] == window ? + var elem = this[0]; + if ( !elem ) { + return size == null ? null : this; + } + + if ( jQuery.isFunction( size ) ) { + return this.each(function( i ) { + var self = jQuery( this ); + self[ type ]( size.call( this, i, self[ type ]() ) ); + }); + } + + return ("scrollTo" in elem && elem.document) ? // does it walk and quack like a window? // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode - document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] || - document.body[ "client" + name ] : + elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] || + elem.document.body[ "client" + name ] : // Get document width or height - this[0] == document ? + (elem.nodeType === 9) ? // is it a document // Either scroll[Width/Height] or offset[Width/Height], whichever is greater Math.max( - document.documentElement["client" + name], - document.body["scroll" + name], document.documentElement["scroll" + name], - document.body["offset" + name], document.documentElement["offset" + name] + elem.documentElement["client" + name], + elem.body["scroll" + name], elem.documentElement["scroll" + name], + elem.body["offset" + name], elem.documentElement["offset" + name] ) : // Get or set width or height on the element size === undefined ? // Get width or height on the element - (this.length ? jQuery.css( this[0], type ) : null) : + jQuery.css( elem, type ) : // Set the width or height on the element (default to pixels if value is unitless) this.css( type, typeof size === "string" ? size : size + "px" ); }; }); -})(); +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +})(window); diff --git a/share/www/script/json2.js b/share/www/script/json2.js index 48c55361..a1a3b170 100644 --- a/share/www/script/json2.js +++ b/share/www/script/json2.js @@ -1,6 +1,6 @@ /* http://www.JSON.org/json2.js - 2009-08-17 + 2010-03-20 Public Domain. @@ -8,6 +8,14 @@ See http://www.JSON.org/js.html + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + This file creates a global JSON object containing two methods: stringify and parse. @@ -136,15 +144,9 @@ This is a reference implementation. You are free to copy, modify, or redistribute. - - This code should be minified before deployment. - See http://javascript.crockford.com/jsmin.html - - USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO - NOT CONTROL. */ -/*jslint evil: true */ +/*jslint evil: true, strict: false */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, @@ -153,7 +155,6 @@ test, toJSON, toString, valueOf */ -"use strict"; // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. @@ -432,6 +433,7 @@ if (!this.JSON) { // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. + text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { diff --git a/share/www/script/jspec/jspec.css b/share/www/script/jspec/jspec.css new file mode 100644 index 00000000..629d41c5 --- /dev/null +++ b/share/www/script/jspec/jspec.css @@ -0,0 +1,149 @@ +body.jspec { + margin: 45px 0; + font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; + background: #efefef url(images/bg.png) top left repeat-x; + text-align: center; +} +#jspec { + margin: 0 auto; + padding-top: 30px; + width: 1008px; + background: url(images/vr.png) top left repeat-y; + text-align: left; +} +#jspec-top { + position: relative; + margin: 0 auto; + width: 1008px; + height: 40px; + background: url(images/sprites.bg.png) top left no-repeat; +} +#jspec-bottom { + margin: 0 auto; + width: 1008px; + height: 15px; + background: url(images/sprites.bg.png) bottom left no-repeat; +} +#jspec .loading { + margin-top: -45px; + width: 1008px; + height: 80px; + background: url(images/loading.gif) 50% 50% no-repeat; +} +#jspec-title { + position: absolute; + top: 15px; + left: 20px; + width: 160px; + font-size: 22px; + font-weight: normal; + background: url(images/sprites.png) 0 -126px no-repeat; + text-align: center; +} +#jspec-title em { + font-size: 10px; + font-style: normal; + color: #BCC8D1; +} +#jspec-report * { + margin: 0; + padding: 0; + background: none; + border: none; +} +#jspec-report { + padding: 15px 40px; + font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; + color: #7B8D9B; +} +#jspec-report.has-failures { + padding-bottom: 30px; +} +#jspec-report .hidden { + display: none; +} +#jspec-report .heading { + margin-bottom: 15px; +} +#jspec-report .heading span { + padding-right: 10px; +} +#jspec-report .heading .passes em { + color: #0ea0eb; +} +#jspec-report .heading .failures em { + color: #FA1616; +} +#jspec-report table { + font-size: 11px; + border-collapse: collapse; +} +#jspec-report td { + padding: 8px; + text-indent: 30px; + color: #7B8D9B; +} +#jspec-report tr.body { + display: none; +} +#jspec-report tr.body pre { + margin: 0; + padding: 0 0 5px 25px; +} +#jspec-report tr.even:hover + tr.body, +#jspec-report tr.odd:hover + tr.body { + display: block; +} +#jspec-report tr td:first-child em { + display: block; + clear: both; + font-style: normal; + font-weight: normal; + color: #7B8D9B; +} +#jspec-report tr.even:hover, +#jspec-report tr.odd:hover { + text-shadow: 1px 1px 1px #fff; + background: #F2F5F7; +} +#jspec-report td + td { + padding-right: 0; + width: 15px; +} +#jspec-report td.pass { + background: url(images/sprites.png) 3px -7px no-repeat; +} +#jspec-report td.fail { + background: url(images/sprites.png) 3px -158px no-repeat; + font-weight: bold; + color: #FC0D0D; +} +#jspec-report td.requires-implementation { + background: url(images/sprites.png) 3px -333px no-repeat; +} +#jspec-report tr.description td { + margin-top: 25px; + padding-top: 25px; + font-size: 12px; + font-weight: bold; + text-indent: 0; + color: #1a1a1a; +} +#jspec-report tr.description:first-child td { + border-top: none; +} +#jspec-report .assertion { + display: block; + float: left; + margin: 0 0 0 1px; + padding: 0; + width: 1px; + height: 5px; + background: #7B8D9B; +} +#jspec-report .assertion.failed { + background: red; +} +.jspec-sandbox { + display: none; +}
\ No newline at end of file diff --git a/share/www/script/jspec/jspec.jquery.js b/share/www/script/jspec/jspec.jquery.js new file mode 100644 index 00000000..fcad7ab9 --- /dev/null +++ b/share/www/script/jspec/jspec.jquery.js @@ -0,0 +1,72 @@ + +// JSpec - jQuery - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed) + +JSpec +.requires('jQuery', 'when using jspec.jquery.js') +.include({ + name: 'jQuery', + + // --- Initialize + + init : function() { + jQuery.ajaxSetup({ async: false }) + }, + + // --- Utilities + + utilities : { + element: jQuery, + elements: jQuery, + sandbox : function() { + return jQuery('<div class="sandbox"></div>') + } + }, + + // --- Matchers + + matchers : { + have_tag : "jQuery(expected, actual).length === 1", + have_one : "alias have_tag", + have_tags : "jQuery(expected, actual).length > 1", + have_many : "alias have_tags", + have_any : "alias have_tags", + have_child : "jQuery(actual).children(expected).length === 1", + have_children : "jQuery(actual).children(expected).length > 1", + have_text : "jQuery(actual).text() === expected", + have_value : "jQuery(actual).val() === expected", + be_enabled : "!jQuery(actual).attr('disabled')", + have_class : "jQuery(actual).hasClass(expected)", + + be_visible : function(actual) { + return jQuery(actual).css('display') != 'none' && + jQuery(actual).css('visibility') != 'hidden' && + jQuery(actual).attr('type') != 'hidden' + }, + + be_hidden : function(actual) { + return !JSpec.does(actual, 'be_visible') + }, + + have_classes : function(actual) { + return !JSpec.any(JSpec.toArray(arguments, 1), function(arg){ + return !JSpec.does(actual, 'have_class', arg) + }) + }, + + have_attr : function(actual, attr, value) { + return value ? jQuery(actual).attr(attr) == value: + jQuery(actual).attr(attr) + }, + + 'be disabled selected checked' : function(attr) { + return 'jQuery(actual).attr("' + attr + '")' + }, + + 'have type id title alt href src sel rev name target' : function(attr) { + return function(actual, value) { + return JSpec.does(actual, 'have_attr', attr, value) + } + } + } +}) + diff --git a/share/www/script/jspec/jspec.js b/share/www/script/jspec/jspec.js new file mode 100644 index 00000000..b2ea4768 --- /dev/null +++ b/share/www/script/jspec/jspec.js @@ -0,0 +1,1756 @@ + +// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed) + +;(function(){ + + JSpec = { + version : '3.3.2', + assert : true, + cache : {}, + suites : [], + modules : [], + allSuites : [], + matchers : {}, + stubbed : [], + options : {}, + request : 'XMLHttpRequest' in this ? XMLHttpRequest : null, + stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 }, + + /** + * Default context in which bodies are evaluated. + * + * Replace context simply by setting JSpec.context + * to your own like below: + * + * JSpec.context = { foo : 'bar' } + * + * Contexts can be changed within any body, this can be useful + * in order to provide specific helper methods to specific suites. + * + * To reset (usually in after hook) simply set to null like below: + * + * JSpec.context = null + * + */ + + defaultContext : { + + /** + * Return an object used for proxy assertions. + * This object is used to indicate that an object + * should be an instance of _object_, not the constructor + * itself. + * + * @param {function} constructor + * @return {hash} + * @api public + */ + + an_instance_of : function(constructor) { + return { an_instance_of : constructor } + }, + + /** + * Load fixture at _path_. + * + * Fixtures are resolved as: + * + * - <path> + * - <path>.html + * + * @param {string} path + * @return {string} + * @api public + */ + + fixture : function(path) { + if (JSpec.cache[path]) return JSpec.cache[path] + return JSpec.cache[path] = + JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) || + JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html') + } + }, + + // --- Objects + + reporters : { + + /** + * Report to server. + * + * Options: + * - uri specific uri to report to. + * - verbose weither or not to output messages + * - failuresOnly output failure messages only + * + * @api public + */ + + Server : function(results, options) { + var uri = options.uri || window.location.protocol + "//" + window.location.host + '/results' + JSpec.post(uri, { + stats: JSpec.stats, + options: options, + results: map(results.allSuites, function(suite) { + if (suite.hasSpecs()) + return { + description: suite.description, + specs: map(suite.specs, function(spec) { + return { + description: spec.description, + message: !spec.passed() ? spec.failure().message : null, + status: spec.requiresImplementation() ? 'pending' : + spec.passed() ? 'pass' : + 'fail', + assertions: map(spec.assertions, function(assertion){ + return { + passed: assertion.passed + } + }) + } + }) + } + }) + }) + if ('close' in main) main.close() + }, + + /** + * Default reporter, outputting to the DOM. + * + * Options: + * - reportToId id of element to output reports to, defaults to 'jspec' + * - failuresOnly displays only suites with failing specs + * + * @api public + */ + + DOM : function(results, options) { + var id = option('reportToId') || 'jspec', + report = document.getElementById(id), + failuresOnly = option('failuresOnly'), + classes = results.stats.failures ? 'has-failures' : '' + if (!report) throw 'JSpec requires the element #' + id + ' to output its reports' + + function bodyContents(body) { + return JSpec. + escape(JSpec.contentsOf(body)). + replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }). + replace(/\r\n|\r|\n/gm, '<br/>') + } + + report.innerHTML = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \ + <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \ + <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \ + <span class="passes">Duration: <em>' + results.duration + '</em> ms</span> \ + </div><table class="suites">' + map(results.allSuites, function(suite) { + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran + if (displaySuite && suite.hasSpecs()) + return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' + + map(suite.specs, function(i, spec) { + return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' + + (spec.requiresImplementation() ? + '<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' : + (spec.passed() && !failuresOnly) ? + '<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' : + !spec.passed() ? + '<td class="fail">' + escape(spec.description) + + map(spec.failures(), function(a){ return '<em>' + escape(a.message) + '</em>' }).join('') + + '</td><td>' + spec.assertionsGraph() + '</td>' : + '') + + '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>' + }).join('') + '</tr>' + }).join('') + '</table></div>' + }, + + /** + * Terminal reporter. + * + * @api public + */ + + Terminal : function(results, options) { + var failuresOnly = option('failuresOnly') + print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') + + color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + + color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n") + + function indent(string) { + return string.replace(/^(.)/gm, ' $1') + } + + each(results.allSuites, function(suite) { + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran + if (displaySuite && suite.hasSpecs()) { + print(color(' ' + suite.description, 'bold')) + each(suite.specs, function(spec){ + var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){ + return graph + color('.', assertion.passed ? 'green' : 'red') + }) + if (spec.requiresImplementation()) + print(color(' ' + spec.description, 'blue') + assertionsGraph) + else if (spec.passed() && !failuresOnly) + print(color(' ' + spec.description, 'green') + assertionsGraph) + else if (!spec.passed()) + print(color(' ' + spec.description, 'red') + assertionsGraph + + "\n" + indent(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n") + }) + print("") + } + }) + + quit(results.stats.failures) + } + }, + + Assertion : function(matcher, actual, expected, negate) { + extend(this, { + message: '', + passed: false, + actual: actual, + negate: negate, + matcher: matcher, + expected: expected, + + // Report assertion results + + report : function() { + if (JSpec.assert) + this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++ + return this + }, + + // Run the assertion + + run : function() { + // TODO: remove unshifting + expected.unshift(actual) + this.result = matcher.match.apply(this, expected) + this.passed = negate ? !this.result : this.result + if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name) + return this + } + }) + }, + + ProxyAssertion : function(object, method, times, negate) { + var self = this + var old = object[method] + + // Proxy + + object[method] = function(){ + args = toArray(arguments) + result = old.apply(object, args) + self.calls.push({ args : args, result : result }) + return result + } + + // Times + + this.times = { + once : 1, + twice : 2 + }[times] || times || 1 + + extend(this, { + calls: [], + message: '', + defer: true, + passed: false, + negate: negate, + object: object, + method: method, + + // Proxy return value + + and_return : function(result) { + this.expectedResult = result + return this + }, + + // Proxy arguments passed + + with_args : function() { + this.expectedArgs = toArray(arguments) + return this + }, + + // Check if any calls have failing results + + anyResultsFail : function() { + return any(this.calls, function(call){ + return self.expectedResult.an_instance_of ? + call.result.constructor != self.expectedResult.an_instance_of: + !equal(self.expectedResult, call.result) + }) + }, + + // Check if any calls have passing results + + anyResultsPass : function() { + return any(this.calls, function(call){ + return self.expectedResult.an_instance_of ? + call.result.constructor == self.expectedResult.an_instance_of: + equal(self.expectedResult, call.result) + }) + }, + + // Return the passing result + + passingResult : function() { + return this.anyResultsPass().result + }, + + // Return the failing result + + failingResult : function() { + return this.anyResultsFail().result + }, + + // Check if any arguments fail + + anyArgsFail : function() { + return any(this.calls, function(call){ + return any(self.expectedArgs, function(i, arg){ + if (arg == null) return call.args[i] == null + return arg.an_instance_of ? + call.args[i].constructor != arg.an_instance_of: + !equal(arg, call.args[i]) + + }) + }) + }, + + // Check if any arguments pass + + anyArgsPass : function() { + return any(this.calls, function(call){ + return any(self.expectedArgs, function(i, arg){ + return arg.an_instance_of ? + call.args[i].constructor == arg.an_instance_of: + equal(arg, call.args[i]) + + }) + }) + }, + + // Return the passing args + + passingArgs : function() { + return this.anyArgsPass().args + }, + + // Return the failing args + + failingArgs : function() { + return this.anyArgsFail().args + }, + + // Report assertion results + + report : function() { + if (JSpec.assert) + this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures + return this + }, + + // Run the assertion + + run : function() { + var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' ) + + function times(n) { + return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n] + } + + if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail())) + this.message = methodString + ' to return ' + puts(this.expectedResult) + + ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult())) + + if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail())) + this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) + + ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs())) + + if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length >= this.times : this.calls.length != this.times) + this.message = methodString + ' to be called ' + times(this.times) + + ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length)) + + if (!this.message.length) + this.passed = true + + return this + } + }) + }, + + /** + * Specification Suite block object. + * + * @param {string} description + * @param {function} body + * @api private + */ + + Suite : function(description, body) { + var self = this + extend(this, { + body: body, + description: description, + suites: [], + specs: [], + ran: false, + hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }, + + // Add a spec to the suite + + addSpec : function(description, body) { + var spec = new JSpec.Spec(description, body) + this.specs.push(spec) + JSpec.stats.specs++ // TODO: abstract + spec.suite = this + }, + + // Add a hook to the suite + + addHook : function(hook, body) { + this.hooks[hook].push(body) + }, + + // Add a nested suite + + addSuite : function(description, body) { + var suite = new JSpec.Suite(description, body) + JSpec.allSuites.push(suite) + suite.name = suite.description + suite.description = this.description + ' ' + suite.description + this.suites.push(suite) + suite.suite = this + }, + + // Invoke a hook in context to this suite + + hook : function(hook) { + if (this.suite) this.suite.hook(hook) + each(this.hooks[hook], function(body) { + JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ") + }) + }, + + // Check if nested suites are present + + hasSuites : function() { + return this.suites.length + }, + + // Check if this suite has specs + + hasSpecs : function() { + return this.specs.length + }, + + // Check if the entire suite passed + + passed : function() { + return !any(this.specs, function(spec){ + return !spec.passed() + }) + } + }) + }, + + /** + * Specification block object. + * + * @param {string} description + * @param {function} body + * @api private + */ + + Spec : function(description, body) { + extend(this, { + body: body, + description: description, + assertions: [], + + // Add passing assertion + + pass : function(message) { + this.assertions.push({ passed: true, message: message }) + if (JSpec.assert) ++JSpec.stats.passes + }, + + // Add failing assertion + + fail : function(message) { + this.assertions.push({ passed: false, message: message }) + if (JSpec.assert) ++JSpec.stats.failures + }, + + // Run deferred assertions + + runDeferredAssertions : function() { + each(this.assertions, function(assertion){ + if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion) + }) + }, + + // Find first failing assertion + + failure : function() { + return find(this.assertions, function(assertion){ + return !assertion.passed + }) + }, + + // Find all failing assertions + + failures : function() { + return select(this.assertions, function(assertion){ + return !assertion.passed + }) + }, + + // Weither or not the spec passed + + passed : function() { + return !this.failure() + }, + + // Weither or not the spec requires implementation (no assertions) + + requiresImplementation : function() { + return this.assertions.length == 0 + }, + + // Sprite based assertions graph + + assertionsGraph : function() { + return map(this.assertions, function(assertion){ + return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>' + }).join('') + } + }) + }, + + Module : function(methods) { + extend(this, methods) + }, + + JSON : { + + /** + * Generic sequences. + */ + + meta : { + '\b' : '\\b', + '\t' : '\\t', + '\n' : '\\n', + '\f' : '\\f', + '\r' : '\\r', + '"' : '\\"', + '\\' : '\\\\' + }, + + /** + * Escapable sequences. + */ + + escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + + /** + * JSON encode _object_. + * + * @param {mixed} object + * @return {string} + * @api private + */ + + encode : function(object) { + var self = this + if (object == undefined || object == null) return 'null' + if (object === true) return 'true' + if (object === false) return 'false' + switch (typeof object) { + case 'number': return object + case 'string': return this.escapable.test(object) ? + '"' + object.replace(this.escapable, function (a) { + return typeof self.meta[a] === 'string' ? self.meta[a] : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4) + }) + '"' : + '"' + object + '"' + case 'object': + if (object.constructor == Array) + return '[' + map(object, function(val){ + return self.encode(val) + }).join(', ') + ']' + else if (object) + return '{' + map(object, function(key, val){ + return self.encode(key) + ':' + self.encode(val) + }).join(', ') + '}' + } + return 'null' + } + }, + + // --- DSLs + + DSLs : { + snake : { + expect : function(actual){ + return JSpec.expect(actual) + }, + + describe : function(description, body) { + return JSpec.currentSuite.addSuite(description, body) + }, + + it : function(description, body) { + return JSpec.currentSuite.addSpec(description, body) + }, + + before : function(body) { + return JSpec.currentSuite.addHook('before', body) + }, + + after : function(body) { + return JSpec.currentSuite.addHook('after', body) + }, + + before_each : function(body) { + return JSpec.currentSuite.addHook('before_each', body) + }, + + after_each : function(body) { + return JSpec.currentSuite.addHook('after_each', body) + }, + + should_behave_like : function(description) { + return JSpec.shareBehaviorsOf(description) + } + } + }, + + // --- Methods + + /** + * Check if _value_ is 'stop'. For use as a + * utility callback function. + * + * @param {mixed} value + * @return {bool} + * @api public + */ + + haveStopped : function(value) { + return value === 'stop' + }, + + /** + * Include _object_ which may be a hash or Module instance. + * + * @param {hash, Module} object + * @return {JSpec} + * @api public + */ + + include : function(object) { + var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object) + this.modules.push(module) + if ('init' in module) module.init() + if ('utilities' in module) extend(this.defaultContext, module.utilities) + if ('matchers' in module) this.addMatchers(module.matchers) + if ('reporters' in module) extend(this.reporters, module.reporters) + if ('DSLs' in module) + each(module.DSLs, function(name, methods){ + JSpec.DSLs[name] = JSpec.DSLs[name] || {} + extend(JSpec.DSLs[name], methods) + }) + return this + }, + + /** + * Add a module hook _name_, which is immediately + * called per module with the _args_ given. An array of + * hook return values is returned. + * + * @param {name} string + * @param {...} args + * @return {array} + * @api private + */ + + hook : function(name, args) { + args = toArray(arguments, 1) + return inject(JSpec.modules, [], function(results, module){ + if (typeof module[name] == 'function') + results.push(JSpec.evalHook(module, name, args)) + }) + }, + + /** + * Eval _module_ hook _name_ with _args_. Evaluates in context + * to the module itself, JSpec, and JSpec.context. + * + * @param {Module} module + * @param {string} name + * @param {array} args + * @return {mixed} + * @api private + */ + + evalHook : function(module, name, args) { + hook('evaluatingHookBody', module, name) + try { return module[name].apply(module, args) } + catch(e) { error('Error in hook ' + module.name + '.' + name + ': ', e) } + }, + + /** + * Same as hook() however accepts only one _arg_ which is + * considered immutable. This function passes the arg + * to the first module, then passes the return value of the last + * module called, to the following module. + * + * @param {string} name + * @param {mixed} arg + * @return {mixed} + * @api private + */ + + hookImmutable : function(name, arg) { + return inject(JSpec.modules, arg, function(result, module){ + if (typeof module[name] == 'function') + return JSpec.evalHook(module, name, [result]) + }) + }, + + /** + * Find a suite by its description or name. + * + * @param {string} description + * @return {Suite} + * @api private + */ + + findSuite : function(description) { + return find(this.allSuites, function(suite){ + return suite.name == description || suite.description == description + }) + }, + + /** + * Share behaviors (specs) of the given suite with + * the current suite. + * + * @param {string} description + * @api public + */ + + shareBehaviorsOf : function(description) { + if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite) + else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name' + }, + + /** + * Copy specs from one suite to another. + * + * @param {Suite} fromSuite + * @param {Suite} toSuite + * @api public + */ + + copySpecs : function(fromSuite, toSuite) { + each(fromSuite.specs, function(spec){ + var newSpec = new Object(); + extend(newSpec, spec); + newSpec.assertions = []; + toSuite.specs.push(newSpec); + }) + }, + + /** + * Convert arguments to an array. + * + * @param {object} arguments + * @param {int} offset + * @return {array} + * @api public + */ + + toArray : function(arguments, offset) { + return Array.prototype.slice.call(arguments, offset || 0) + }, + + /** + * Return ANSI-escaped colored string. + * + * @param {string} string + * @param {string} color + * @return {string} + * @api public + */ + + color : function(string, color) { + return "\u001B[" + { + bold : 1, + black : 30, + red : 31, + green : 32, + yellow : 33, + blue : 34, + magenta : 35, + cyan : 36, + white : 37 + }[color] + 'm' + string + "\u001B[0m" + }, + + /** + * Default matcher message callback. + * + * @api private + */ + + defaultMatcherMessage : function(actual, expected, negate, name) { + return 'expected ' + puts(actual) + ' to ' + + (negate ? 'not ' : '') + + name.replace(/_/g, ' ') + + ' ' + (expected.length > 1 ? + puts.apply(this, expected.slice(1)) : + '') + }, + + /** + * Normalize a matcher message. + * + * When no messge callback is present the defaultMatcherMessage + * will be assigned, will suffice for most matchers. + * + * @param {hash} matcher + * @return {hash} + * @api public + */ + + normalizeMatcherMessage : function(matcher) { + if (typeof matcher.message != 'function') + matcher.message = this.defaultMatcherMessage + return matcher + }, + + /** + * Normalize a matcher body + * + * This process allows the following conversions until + * the matcher is in its final normalized hash state. + * + * - '==' becomes 'actual == expected' + * - 'actual == expected' becomes 'return actual == expected' + * - function(actual, expected) { return actual == expected } becomes + * { match : function(actual, expected) { return actual == expected }} + * + * @param {mixed} body + * @return {hash} + * @api public + */ + + normalizeMatcherBody : function(body) { + switch (body.constructor) { + case String: + if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)] + if (body.length < 4) body = 'actual ' + body + ' expected' + return { match: function(actual, expected) { return eval(body) }} + + case Function: + return { match: body } + + default: + return body + } + }, + + /** + * Get option value. This method first checks if + * the option key has been set via the query string, + * otherwise returning the options hash value. + * + * @param {string} key + * @return {mixed} + * @api public + */ + + option : function(key) { + return (value = query(key)) !== null ? value : + JSpec.options[key] || null + }, + + /** + * Check if object _a_, is equal to object _b_. + * + * @param {object} a + * @param {object} b + * @return {bool} + * @api private + */ + + equal: function(a, b) { + if (typeof a != typeof b) return + if (a === b) return true + if (a instanceof RegExp) + return a.toString() === b.toString() + if (a instanceof Date) + return Number(a) === Number(b) + if (typeof a != 'object') return + if (a.length !== undefined) + if (a.length !== b.length) return + else + for (var i = 0, len = a.length; i < len; ++i) + if (!equal(a[i], b[i])) + return + for (var key in a) + if (!equal(a[key], b[key])) + return + return true + }, + + /** + * Return last element of an array. + * + * @param {array} array + * @return {object} + * @api public + */ + + last : function(array) { + return array[array.length - 1] + }, + + /** + * Convert object(s) to a print-friend string. + * + * @param {...} object + * @return {string} + * @api public + */ + + puts : function(object) { + if (arguments.length > 1) + return map(toArray(arguments), function(arg){ + return puts(arg) + }).join(', ') + if (object === undefined) return 'undefined' + if (object === null) return 'null' + if (object === true) return 'true' + if (object === false) return 'false' + if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name + if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector) + if (object.jquery) return object.get(0).outerHTML + if (object.nodeName) return object.outerHTML + switch (object.constructor) { + case Function: return object.name || object + case String: + return '"' + object + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + '"' + case Array: + return inject(object, '[', function(b, v){ + return b + ', ' + puts(v) + }).replace('[,', '[') + ' ]' + case Object: + object.__hit__ = true + return inject(object, '{', function(b, k, v) { + if (k == '__hit__') return b + return b + ', ' + k + ': ' + (v && v.__hit__ ? '<circular reference>' : puts(v)) + }).replace('{,', '{') + ' }' + default: + return object.toString() + } + }, + + /** + * Escape HTML. + * + * @param {string} html + * @return {string} + * @api public + */ + + escape : function(html) { + return html.toString() + .replace(/&/gmi, '&') + .replace(/"/gmi, '"') + .replace(/>/gmi, '>') + .replace(/</gmi, '<') + }, + + /** + * Perform an assertion without reporting. + * + * This method is primarily used for internal + * matchers in order retain DRYness. May be invoked + * like below: + * + * does('foo', 'eql', 'foo') + * does([1,2], 'include', 1, 2) + * + * External hooks are not run for internal assertions + * performed by does(). + * + * @param {mixed} actual + * @param {string} matcher + * @param {...} expected + * @return {mixed} + * @api private + */ + + does : function(actual, matcher, expected) { + var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, toArray(arguments, 2)) + return assertion.run().result + }, + + /** + * Perform an assertion. + * + * expect(true).to('be', true) + * expect('foo').not_to('include', 'bar') + * expect([1, [2]]).to('include', 1, [2]) + * + * @param {mixed} actual + * @return {hash} + * @api public + */ + + expect : function(actual) { + function assert(matcher, args, negate) { + var expected = toArray(args, 1) + matcher.negate = negate + assertion = new JSpec.Assertion(matcher, actual, expected, negate) + hook('beforeAssertion', assertion) + if (matcher.defer) assertion.run() + else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion) + return assertion.result + } + + function to(matcher) { + return assert(matcher, arguments, false) + } + + function not_to(matcher) { + return assert(matcher, arguments, true) + } + + return { + to : to, + should : to, + not_to: not_to, + should_not : not_to + } + }, + + /** + * Strim whitespace or chars. + * + * @param {string} string + * @param {string} chars + * @return {string} + * @api public + */ + + strip : function(string, chars) { + return string. + replace(new RegExp('[' + (chars || '\\s') + ']*$'), ''). + replace(new RegExp('^[' + (chars || '\\s') + ']*'), '') + }, + + /** + * Call an iterator callback with arguments a, or b + * depending on the arity of the callback. + * + * @param {function} callback + * @param {mixed} a + * @param {mixed} b + * @return {mixed} + * @api private + */ + + callIterator : function(callback, a, b) { + return callback.length == 1 ? callback(b) : callback(a, b) + }, + + /** + * Extend an object with another. + * + * @param {object} object + * @param {object} other + * @api public + */ + + extend : function(object, other) { + each(other, function(property, value){ + object[property] = value + }) + }, + + /** + * Iterate an object, invoking the given callback. + * + * @param {hash, array} object + * @param {function} callback + * @return {JSpec} + * @api public + */ + + each : function(object, callback) { + if (object.constructor == Array) + for (var i = 0, len = object.length; i < len; ++i) + callIterator(callback, i, object[i]) + else + for (var key in object) + if (object.hasOwnProperty(key)) + callIterator(callback, key, object[key]) + }, + + /** + * Iterate with memo. + * + * @param {hash, array} object + * @param {object} memo + * @param {function} callback + * @return {object} + * @api public + */ + + inject : function(object, memo, callback) { + each(object, function(key, value){ + memo = (callback.length == 2 ? + callback(memo, value): + callback(memo, key, value)) || + memo + }) + return memo + }, + + /** + * Destub _object_'s _method_. When no _method_ is passed + * all stubbed methods are destubbed. When no arguments + * are passed every object found in JSpec.stubbed will be + * destubbed. + * + * @param {mixed} object + * @param {string} method + * @api public + */ + + destub : function(object, method) { + if (method) { + if (object['__prototype__' + method]) + delete object[method] + else + object[method] = object['__original__' + method] + delete object['__prototype__' + method] + delete object['__original____' + method] + } + else if (object) { + for (var key in object) + if (captures = key.match(/^(?:__prototype__|__original__)(.*)/)) + destub(object, captures[1]) + } + else + while (JSpec.stubbed.length) + destub(JSpec.stubbed.shift()) + }, + + /** + * Stub _object_'s _method_. + * + * stub(foo, 'toString').and_return('bar') + * + * @param {mixed} object + * @param {string} method + * @return {hash} + * @api public + */ + + stub : function(object, method) { + hook('stubbing', object, method) + JSpec.stubbed.push(object) + var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__' + object[type + method] = object[method] + object[method] = function(){} + return { + and_return : function(value) { + if (typeof value == 'function') object[method] = value + else object[method] = function(){ return value } + } + } + }, + + /** + * Map callback return values. + * + * @param {hash, array} object + * @param {function} callback + * @return {array} + * @api public + */ + + map : function(object, callback) { + return inject(object, [], function(memo, key, value){ + memo.push(callIterator(callback, key, value)) + }) + }, + + /** + * Returns the first matching expression or null. + * + * @param {hash, array} object + * @param {function} callback + * @return {mixed} + * @api public + */ + + any : function(object, callback) { + return inject(object, null, function(state, key, value){ + if (state == undefined) + return callIterator(callback, key, value) ? value : state + }) + }, + + /** + * Returns an array of values collected when the callback + * given evaluates to true. + * + * @param {hash, array} object + * @return {function} callback + * @return {array} + * @api public + */ + + select : function(object, callback) { + return inject(object, [], function(selected, key, value){ + if (callIterator(callback, key, value)) + selected.push(value) + }) + }, + + /** + * Define matchers. + * + * @param {hash} matchers + * @api public + */ + + addMatchers : function(matchers) { + each(matchers, function(name, body){ + JSpec.addMatcher(name, body) + }) + }, + + /** + * Define a matcher. + * + * @param {string} name + * @param {hash, function, string} body + * @api public + */ + + addMatcher : function(name, body) { + hook('addingMatcher', name, body) + if (name.indexOf(' ') != -1) { + var matchers = name.split(/\s+/) + var prefix = matchers.shift() + each(matchers, function(name) { + JSpec.addMatcher(prefix + '_' + name, body(name)) + }) + } + this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body)) + this.matchers[name].name = name + }, + + /** + * Add a root suite to JSpec. + * + * @param {string} description + * @param {body} function + * @api public + */ + + describe : function(description, body) { + var suite = new JSpec.Suite(description, body) + hook('addingSuite', suite) + this.allSuites.push(suite) + this.suites.push(suite) + }, + + /** + * Return the contents of a function body. + * + * @param {function} body + * @return {string} + * @api public + */ + + contentsOf : function(body) { + return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1] + }, + + /** + * Evaluate a JSpec capture body. + * + * @param {function} body + * @param {string} errorMessage (optional) + * @return {Type} + * @api private + */ + + evalBody : function(body, errorMessage) { + var dsl = this.DSL || this.DSLs.snake + var matchers = this.matchers + var context = this.context || this.defaultContext + var contents = this.contentsOf(body) + hook('evaluatingBody', dsl, matchers, context, contents) + try { with (dsl){ with (context) { with (matchers) { eval(contents) }}} } + catch(e) { error(errorMessage, e) } + }, + + /** + * Pre-process a string of JSpec. + * + * @param {string} input + * @return {string} + * @api private + */ + + preprocess : function(input) { + if (typeof input != 'string') return + input = hookImmutable('preprocessing', input) + return input. + replace(/\t/g, ' '). + replace(/\r\n|\n|\r/g, '\n'). + split('__END__')[0]. + replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)'). + replace(/describe\s+(.*?)$/gm, 'describe($1, function(){'). + replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){'). + replace(/^ *(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){'). + replace(/^\s*end(?=\s|$)/gm, '});'). + replace(/-\{/g, 'function(){'). + replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }). + replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)'). + replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)'). + replace(/, \)/g, ')'). + replace(/should\.not/g, 'should_not') + }, + + /** + * Create a range string which can be evaluated to a native array. + * + * @param {int} start + * @param {int} end + * @return {string} + * @api public + */ + + range : function(start, end) { + var current = parseInt(start), end = parseInt(end), values = [current] + if (end > current) while (++current <= end) values.push(current) + else while (--current >= end) values.push(current) + return '[' + values + ']' + }, + + /** + * Report on the results. + * + * @api public + */ + + report : function() { + this.duration = Number(new Date) - this.start + hook('reporting', JSpec.options) + new (JSpec.options.reporter || JSpec.reporters.DOM)(JSpec, JSpec.options) + }, + + /** + * Run the spec suites. Options are merged + * with JSpec options when present. + * + * @param {hash} options + * @return {JSpec} + * @api public + */ + + run : function(options) { + if (any(hook('running'), haveStopped)) return this + if (options) extend(this.options, options) + this.start = Number(new Date) + each(this.suites, function(suite) { JSpec.runSuite(suite) }) + return this + }, + + /** + * Run a suite. + * + * @param {Suite} suite + * @api public + */ + + runSuite : function(suite) { + this.currentSuite = suite + this.evalBody(suite.body) + suite.ran = true + hook('beforeSuite', suite), suite.hook('before') + each(suite.specs, function(spec) { + hook('beforeSpec', spec) + suite.hook('before_each') + JSpec.runSpec(spec) + hook('afterSpec', spec) + suite.hook('after_each') + }) + if (suite.hasSuites()) { + each(suite.suites, function(suite) { + JSpec.runSuite(suite) + }) + } + hook('afterSuite', suite), suite.hook('after') + this.stats.suitesFinished++ + }, + + /** + * Report a failure for the current spec. + * + * @param {string} message + * @api public + */ + + fail : function(message) { + JSpec.currentSpec.fail(message) + }, + + /** + * Report a passing assertion for the current spec. + * + * @param {string} message + * @api public + */ + + pass : function(message) { + JSpec.currentSpec.pass(message) + }, + + /** + * Run a spec. + * + * @param {Spec} spec + * @api public + */ + + runSpec : function(spec) { + this.currentSpec = spec + try { this.evalBody(spec.body) } + catch (e) { fail(e) } + spec.runDeferredAssertions() + destub() + this.stats.specsFinished++ + this.stats.assertions += spec.assertions.length + }, + + /** + * Require a dependency, with optional message. + * + * @param {string} dependency + * @param {string} message (optional) + * @return {JSpec} + * @api public + */ + + requires : function(dependency, message) { + hook('requiring', dependency, message) + try { eval(dependency) } + catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message } + return this + }, + + /** + * Query against the current query strings keys + * or the queryString specified. + * + * @param {string} key + * @param {string} queryString + * @return {string, null} + * @api private + */ + + query : function(key, queryString) { + var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1) + return inject(queryString.split('&'), null, function(value, pair){ + parts = pair.split('=') + return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value + }) + }, + + /** + * Throw a JSpec related error. + * + * @param {string} message + * @param {Exception} e + * @api public + */ + + error : function(message, e) { + throw (message ? message : '') + e.toString() + + (e.line ? ' near line ' + e.line : '') + }, + + /** + * Ad-hoc POST request for JSpec server usage. + * + * @param {string} uri + * @param {string} data + * @api private + */ + + post : function(uri, data) { + if (any(hook('posting', uri, data), haveStopped)) return + var request = this.xhr() + request.open('POST', uri, false) + request.setRequestHeader('Content-Type', 'application/json') + request.send(JSpec.JSON.encode(data)) + }, + + /** + * Instantiate an XMLHttpRequest. + * + * Here we utilize IE's lame ActiveXObjects first which + * allow IE access serve files via the file: protocol, otherwise + * we then default to XMLHttpRequest. + * + * @return {XMLHttpRequest, ActiveXObject} + * @api private + */ + + xhr : function() { + return this.ieXhr() || new JSpec.request + }, + + /** + * Return Microsoft piece of crap ActiveXObject. + * + * @return {ActiveXObject} + * @api public + */ + + ieXhr : function() { + function object(str) { + try { return new ActiveXObject(str) } catch(e) {} + } + return object('Msxml2.XMLHTTP.6.0') || + object('Msxml2.XMLHTTP.3.0') || + object('Msxml2.XMLHTTP') || + object('Microsoft.XMLHTTP') + }, + + /** + * Check for HTTP request support. + * + * @return {bool} + * @api private + */ + + hasXhr : function() { + return JSpec.request || 'ActiveXObject' in main + }, + + /** + * Try loading _file_ returning the contents + * string or null. Chain to locate / read a file. + * + * @param {string} file + * @return {string} + * @api public + */ + + tryLoading : function(file) { + try { return JSpec.load(file) } catch (e) {} + }, + + /** + * Load a _file_'s contents. + * + * @param {string} file + * @param {function} callback + * @return {string} + * @api public + */ + + load : function(file, callback) { + if (any(hook('loading', file), haveStopped)) return + if ('readFile' in main) + return readFile(file) + else if (this.hasXhr()) { + var request = this.xhr() + request.open('GET', file, false) + request.send(null) + if (request.readyState == 4 && + (request.status == 0 || + request.status.toString().charAt(0) == 2)) + return request.responseText + } + else + error("failed to load `" + file + "'") + }, + + /** + * Load, pre-process, and evaluate a file. + * + * @param {string} file + * @param {JSpec} + * @api public + */ + + exec : function(file) { + if (any(hook('executing', file), haveStopped)) return this + eval('with (JSpec){' + this.preprocess(this.load(file)) + '}') + return this + } + } + + // --- Node.js support + + if (typeof GLOBAL === 'object' && typeof exports === 'object') + quit = process.exit, + print = require('sys').puts, + readFile = require('fs').readFileSync + + // --- Utility functions + + var main = this, + find = JSpec.any, + utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \ + error escape extend puts query strip color does addMatchers callIterator toArray equal'.split(/\s+/) + while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift()) + if (!main.setTimeout) main.setTimeout = function(callback){ callback() } + + // --- Matchers + + addMatchers({ + equal : "===", + eql : "equal(actual, expected)", + be : "alias equal", + be_greater_than : ">", + be_less_than : "<", + be_at_least : ">=", + be_at_most : "<=", + be_a : "actual.constructor == expected", + be_an : "alias be_a", + be_an_instance_of : "actual instanceof expected", + be_null : "actual == null", + be_true : "actual == true", + be_false : "actual == false", + be_undefined : "typeof actual == 'undefined'", + be_type : "typeof actual == expected", + match : "typeof actual == 'string' ? actual.match(expected) : false", + respond_to : "typeof actual[expected] == 'function'", + have_length : "actual.length == expected", + be_within : "actual >= expected[0] && actual <= last(expected)", + have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)", + + receive : { defer : true, match : function(actual, method, times) { + proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate) + JSpec.currentSpec.assertions.push(proxy) + return proxy + }}, + + be_empty : function(actual) { + if (actual.constructor == Object && actual.length == undefined) + for (var key in actual) + return false; + return !actual.length + }, + + include : function(actual) { + for (state = true, i = 1; i < arguments.length; i++) { + arg = arguments[i] + switch (actual.constructor) { + case String: + case Number: + case RegExp: + case Function: + state = actual.toString().indexOf(arg) !== -1 + break + + case Object: + state = arg in actual + break + + case Array: + state = any(actual, function(value){ return equal(value, arg) }) + break + } + if (!state) return false + } + return true + }, + + throw_error : { match : function(actual, expected, message) { + try { actual() } + catch (e) { + this.e = e + var assert = function(arg) { + switch (arg.constructor) { + case RegExp : return arg.test(e.message || e.toString()) + case String : return arg == (e.message || e.toString()) + case Function : return e instanceof arg || e.name == arg.name + } + } + return message ? assert(expected) && assert(message) : + expected ? assert(expected) : + true + } + }, message : function(actual, expected, negate) { + // TODO: refactor when actual is not in expected [0] + var message_for = function(i) { + if (expected[i] == undefined) return 'exception' + switch (expected[i].constructor) { + case RegExp : return 'exception matching ' + puts(expected[i]) + case String : return 'exception of ' + puts(expected[i]) + case Function : return expected[i].name || 'Error' + } + } + exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '') + return 'expected ' + exception + (negate ? ' not ' : '' ) + + ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was') + }}, + + have : function(actual, length, property) { + return actual[property].length == length + }, + + have_at_least : function(actual, length, property) { + return actual[property].length >= length + }, + + have_at_most :function(actual, length, property) { + return actual[property].length <= length + }, + + have_within : function(actual, range, property) { + length = actual[property].length + return length >= range.shift() && length <= range.pop() + }, + + have_prop : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + does(actual[property], 'eql', value) + }, + + have_property : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + value === actual[property] + } + }) + +})() diff --git a/share/www/script/jspec/jspec.xhr.js b/share/www/script/jspec/jspec.xhr.js new file mode 100644 index 00000000..61648795 --- /dev/null +++ b/share/www/script/jspec/jspec.xhr.js @@ -0,0 +1,195 @@ + +// JSpec - XHR - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed) + +(function(){ + + var lastRequest + + // --- Original XMLHttpRequest + + var OriginalXMLHttpRequest = 'XMLHttpRequest' in this ? + XMLHttpRequest : + function(){} + var OriginalActiveXObject = 'ActiveXObject' in this ? + ActiveXObject : + undefined + + // --- MockXMLHttpRequest + + var MockXMLHttpRequest = function() { + this.requestHeaders = {} + } + + MockXMLHttpRequest.prototype = { + status: 0, + async: true, + readyState: 0, + responseText: '', + abort: function(){}, + onreadystatechange: function(){}, + + /** + * Return response headers hash. + */ + + getAllResponseHeaders : function(){ + return this.responseHeaders + }, + + /** + * Return case-insensitive value for header _name_. + */ + + getResponseHeader : function(name) { + return this.responseHeaders[name.toLowerCase()] + }, + + /** + * Set case-insensitive _value_ for header _name_. + */ + + setRequestHeader : function(name, value) { + this.requestHeaders[name.toLowerCase()] = value + }, + + /** + * Open mock request. + */ + + open : function(method, url, async, user, password) { + this.user = user + this.password = password + this.url = url + this.readyState = 1 + this.method = method.toUpperCase() + if (async != undefined) this.async = async + if (this.async) this.onreadystatechange() + }, + + /** + * Send request _data_. + */ + + send : function(data) { + var self = this + this.data = data + this.readyState = 4 + if (this.method == 'HEAD') this.responseText = null + this.responseHeaders['content-length'] = (this.responseText || '').length + if(this.async) this.onreadystatechange() + lastRequest = function(){ + return self + } + } + } + + // --- Response status codes + + JSpec.statusCodes = { + 100: 'Continue', + 101: 'Switching Protocols', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 300: 'Multiple Choice', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported' + } + + /** + * Mock XMLHttpRequest requests. + * + * mockRequest().and_return('some data', 'text/plain', 200, { 'X-SomeHeader' : 'somevalue' }) + * + * @return {hash} + * @api public + */ + + function mockRequest() { + return { and_return : function(body, type, status, headers) { + XMLHttpRequest = MockXMLHttpRequest + ActiveXObject = false + status = status || 200 + headers = headers || {} + headers['content-type'] = type + JSpec.extend(XMLHttpRequest.prototype, { + responseText: body, + responseHeaders: headers, + status: status, + statusText: JSpec.statusCodes[status] + }) + }} + } + + /** + * Unmock XMLHttpRequest requests. + * + * @api public + */ + + function unmockRequest() { + XMLHttpRequest = OriginalXMLHttpRequest + ActiveXObject = OriginalActiveXObject + } + + JSpec.include({ + name: 'Mock XHR', + + // --- Utilities + + utilities : { + mockRequest: mockRequest, + unmockRequest: unmockRequest + }, + + // --- Hooks + + afterSpec : function() { + unmockRequest() + }, + + // --- DSLs + + DSLs : { + snake : { + mock_request: mockRequest, + unmock_request: unmockRequest, + last_request: function(){ return lastRequest() } + } + } + + }) +})()
\ No newline at end of file diff --git a/share/www/script/oauth.js b/share/www/script/oauth.js index aa0019d5..ada00a27 100644 --- a/share/www/script/oauth.js +++ b/share/www/script/oauth.js @@ -78,7 +78,7 @@ OAuth.setProperties(OAuth, // utility functions } if (s instanceof Array) { var e = ""; - for (var i = 0; i < s.length; ++s) { + for (var i = 0; i < s.length; ++i) { if (e != "") e += '&'; e += percentEncode(s[i]); } diff --git a/share/www/script/test/attachment_names.js b/share/www/script/test/attachment_names.js index d90c24c4..988dd2d2 100644 --- a/share/www/script/test/attachment_names.js +++ b/share/www/script/test/attachment_names.js @@ -24,7 +24,7 @@ couchTests.attachment_names = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; // inline attachments try { @@ -72,7 +72,7 @@ couchTests.attachment_names = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; try { db.save(binAttDoc); diff --git a/share/www/script/test/attachment_paths.js b/share/www/script/test/attachment_paths.js index a2a0f69c..3f6ffb7c 100644 --- a/share/www/script/test/attachment_paths.js +++ b/share/www/script/test/attachment_paths.js @@ -33,7 +33,7 @@ couchTests.attachment_paths = function(debug) { data: "V2UgbGlrZSBwZXJjZW50IHR3byBGLg==" } } - } + }; T(db.save(binAttDoc).ok); @@ -73,7 +73,10 @@ couchTests.attachment_paths = function(debug) { T(binAttDoc._attachments["foo/bar.txt"] !== undefined); T(binAttDoc._attachments["foo%2Fbaz.txt"] !== undefined); T(binAttDoc._attachments["foo/bar2.txt"] !== undefined); - T(binAttDoc._attachments["foo/bar2.txt"].content_type == "text/plain;charset=utf-8"); + TEquals("text/plain;charset=utf-8", // thank you Safari + binAttDoc._attachments["foo/bar2.txt"].content_type.toLowerCase(), + "correct content-type" + ); T(binAttDoc._attachments["foo/bar2.txt"].length == 30); //// now repeat the while thing with a design doc @@ -92,7 +95,7 @@ couchTests.attachment_paths = function(debug) { data: "V2UgbGlrZSBwZXJjZW50IHR3byBGLg==" } } - } + }; T(db.save(binAttDoc).ok); @@ -141,7 +144,10 @@ couchTests.attachment_paths = function(debug) { T(binAttDoc._attachments["foo/bar.txt"] !== undefined); T(binAttDoc._attachments["foo/bar2.txt"] !== undefined); - T(binAttDoc._attachments["foo/bar2.txt"].content_type == "text/plain;charset=utf-8"); + TEquals("text/plain;charset=utf-8", // thank you Safari + binAttDoc._attachments["foo/bar2.txt"].content_type.toLowerCase(), + "correct content-type" + ); T(binAttDoc._attachments["foo/bar2.txt"].length == 30); } }; diff --git a/share/www/script/test/attachment_ranges.js b/share/www/script/test/attachment_ranges.js new file mode 100644 index 00000000..e1d40eae --- /dev/null +++ b/share/www/script/test/attachment_ranges.js @@ -0,0 +1,134 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +couchTests.attachment_ranges = function(debug) { + var db = new CouchDB("test_suite_db", { + "X-Couch-Full-Commit": "false" + }); + db.deleteDb(); + db.createDb(); + + if (debug) debugger; + + var binAttDoc = { + _id: "bin_doc", + _attachments: { + "foo.txt": { + content_type: "application/octet-stream", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + + var save_response = db.save(binAttDoc); + T(save_response.ok); + + // Fetching the whole entity is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-28" + } + }); + TEquals(206, xhr.status, "fetch 0-28"); + TEquals("This is a base64 encoded text", xhr.responseText); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Fetch the whole entity without an end offset is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-" + } + }); + TEquals(206, xhr.status, "fetch 0-"); + TEquals("This is a base64 encoded text", xhr.responseText); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Badly formed range header is a 200. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes:0-" + } + }); + TEquals(200, xhr.status, "fetch with bad range header"); + + // Fetch the end of an entity without an end offset is a 206. + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=2-" + } + }); + TEquals(206, xhr.status, "fetch 2-"); + TEquals("is is a base64 encoded text", xhr.responseText); + TEquals("bytes 2-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("27", xhr.getResponseHeader("Content-Length")); + + // Fetch past the end of the entity is a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-29" + } + }); + TEquals(206, xhr.status, "fetch 0-29"); + TEquals("bytes 0-28/29", xhr.getResponseHeader("Content-Range")); + TEquals("29", xhr.getResponseHeader("Content-Length")); + + // Fetch first part of entity is a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=0-3" + } + }); + TEquals(206, xhr.status, "fetch 0-3"); + TEquals("This", xhr.responseText); + TEquals("4", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 0-3/29", xhr.getResponseHeader("Content-Range")); + + // Fetch middle of entity is also a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=10-15" + } + }); + TEquals(206, xhr.status, "fetch 10-15"); + TEquals("base64", xhr.responseText); + TEquals("6", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 10-15/29", xhr.getResponseHeader("Content-Range")); + + // Fetch end of entity is also a 206 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=-3" + } + }); + TEquals(206, xhr.status, "fetch -3"); + TEquals("ext", xhr.responseText); + TEquals("3", xhr.getResponseHeader("Content-Length")); + TEquals("bytes 26-28/29", xhr.getResponseHeader("Content-Range")); + + // backward range is 416 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=5-3" + } + }); + TEquals(416, xhr.status, "fetch 5-3"); + + // range completely outside of entity is 416 + var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt", { + headers: { + "Range": "bytes=300-310" + } + }); + TEquals(416, xhr.status, "fetch 300-310"); + +}; diff --git a/share/www/script/test/attachment_views.js b/share/www/script/test/attachment_views.js index fd30dcfc..a92a8ad0 100644 --- a/share/www/script/test/attachment_views.js +++ b/share/www/script/test/attachment_views.js @@ -68,11 +68,11 @@ couchTests.attachment_views= function(debug) { } emit(parseInt(doc._id), count); - } + }; var reduceFunction = function(key, values) { return sum(values); - } + }; var result = db.query(mapFunction, reduceFunction); diff --git a/share/www/script/test/attachments.js b/share/www/script/test/attachments.js index 36f5a5ad..b0cfd2c5 100644 --- a/share/www/script/test/attachments.js +++ b/share/www/script/test/attachments.js @@ -24,7 +24,7 @@ couchTests.attachments= function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; var save_response = db.save(binAttDoc); T(save_response.ok); @@ -68,12 +68,12 @@ couchTests.attachments= function(debug) { T(binAttDoc2._attachments["foo.txt"] !== undefined); T(binAttDoc2._attachments["foo2.txt"] !== undefined); - T(binAttDoc2._attachments["foo2.txt"].content_type == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", binAttDoc2._attachments["foo2.txt"].content_type); T(binAttDoc2._attachments["foo2.txt"].length == 30); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo2.txt"); T(xhr.responseText == "This is no base64 encoded text"); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); // test without rev, should fail var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt"); @@ -93,10 +93,11 @@ couchTests.attachments= function(debug) { }); T(xhr.status == 201); var rev = JSON.parse(xhr.responseText).rev; + TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", { headers:{"Content-Type":"text/plain;charset=utf-8"}, @@ -110,14 +111,15 @@ couchTests.attachments= function(debug) { }); T(xhr.status == 201); var rev = JSON.parse(xhr.responseText).rev; + TEquals('"' + rev + '"', xhr.getResponseHeader("Etag")); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt"); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); T(xhr.status == 200); @@ -129,7 +131,7 @@ couchTests.attachments= function(debug) { var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev); T(xhr.status == 200); T(xhr.responseText == bin_data); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); // empty attachments var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt", { @@ -156,7 +158,7 @@ couchTests.attachments= function(debug) { // Attachment sparseness COUCHDB-220 - var docs = [] + var docs = []; for (var i = 0; i < 5; i++) { var doc = { _id: (i).toString(), @@ -166,8 +168,8 @@ couchTests.attachments= function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } - docs.push(doc) + }; + docs.push(doc); } var saved = db.bulkSave(docs); @@ -210,7 +212,7 @@ couchTests.attachments= function(debug) { var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc5/lorem.txt"); T(xhr.responseText == lorem); - T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8"); + TEqualsIgnoreCase("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); // test large inline attachment too var lorem_b64 = CouchDB.request("GET", "/_utils/script/test/lorem_b64.txt").responseText; @@ -244,4 +246,30 @@ couchTests.attachments= function(debug) { body: "THIS IS AN ATTACHMENT. BOOYA!" }); TEquals(400, xhr.status, "should return error code 400 Bad Request"); + + // test COUCHDB-809 - stubs should only require the 'stub' field + var bin_doc6 = { + _id: "bin_doc6", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + } + }; + T(db.save(bin_doc6).ok); + // stub out the attachment + bin_doc6._attachments["foo.txt"] = { stub: true }; + T(db.save(bin_doc6).ok == true); + + // wrong rev pos specified + + // stub out the attachment with the wrong revpos + bin_doc6._attachments["foo.txt"] = { stub: true, revpos: 10}; + try { + T(db.save(bin_doc6).ok == true); + T(false && "Shouldn't get here!"); + } catch (e) { + T(e.error == "missing_stub"); + } }; diff --git a/share/www/script/test/attachments_multipart.js b/share/www/script/test/attachments_multipart.js index 2b79e559..96fe344f 100644 --- a/share/www/script/test/attachments_multipart.js +++ b/share/www/script/test/attachments_multipart.js @@ -58,7 +58,7 @@ couchTests.attachments_multipart= function(debug) { var result = JSON.parse(xhr.responseText); - T(result.ok) + T(result.ok); @@ -115,8 +115,11 @@ couchTests.attachments_multipart= function(debug) { // now test receiving multipart docs function getBoundary(xhr) { - var ctype = xhr.getResponseHeader("Content-Type"); - + if (xhr instanceof XMLHttpRequest) { + var ctype = xhr.getResponseHeader("Content-Type"); + } else { + var ctype = xhr.headers['Content-Type']; + } var ctypeArgs = ctype.split("; ").slice(1); var boundary = null; for(var i=0; i<ctypeArgs.length; i++) { @@ -134,7 +137,11 @@ couchTests.attachments_multipart= function(debug) { function parseMultipart(xhr) { var boundary = getBoundary(xhr); - var mimetext = xhr.responseText; + if (xhr instanceof XMLHttpRequest) { + var mimetext = xhr.responseText; + } else { + var mimetext = xhr.body; + } // strip off leading boundary var leading = "--" + boundary + "\r\n"; var last = "\r\n--" + boundary + "--"; @@ -193,7 +200,7 @@ couchTests.attachments_multipart= function(debug) { // a certain rev). xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"" + firstrev + "\"]", - {headers:{"accept": "multipart/related,*/*;"}}); + {headers:{"accept": "multipart/related, */*"}}); T(xhr.status == 200); @@ -207,7 +214,34 @@ couchTests.attachments_multipart= function(debug) { T(doc._attachments['bar.txt'].follows == true); T(sections[1].body == "this is 18 chars l"); - + + // try the atts_since parameter together with the open_revs parameter + xhr = CouchDB.request( + "GET", + '/test_suite_db/multipart?open_revs=["' + + doc._rev + '"]&atts_since=["' + firstrev + '"]', + {headers: {"accept": "multipart/mixed"}} + ); + + T(xhr.status === 200); + + sections = parseMultipart(xhr); + // 1 section, with a multipart/related Content-Type + T(sections.length === 1); + T(sections[0].headers['Content-Type'].indexOf('multipart/related;') === 0); + + var innerSections = parseMultipart(sections[0]); + // 2 inner sections: a document body section plus an attachment data section + T(innerSections.length === 2); + T(innerSections[0].headers['content-type'] === 'application/json'); + + doc = JSON.parse(innerSections[0].body); + + T(doc._attachments['foo.txt'].stub === true); + T(doc._attachments['bar.txt'].follows === true); + + T(innerSections[1].body === "this is 18 chars l"); + // try it with a rev that doesn't exist (should get all attachments) xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"1-2897589\"]", diff --git a/share/www/script/test/auth_cache.js b/share/www/script/test/auth_cache.js new file mode 100644 index 00000000..e48f7370 --- /dev/null +++ b/share/www/script/test/auth_cache.js @@ -0,0 +1,280 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.auth_cache = function(debug) { + + if (debug) debugger; + + // Simple secret key generator + function generateSecret(length) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789+/"; + var secret = ''; + for (var i = 0; i < length; i++) { + secret += tab.charAt(Math.floor(Math.random() * 64)); + } + return secret; + } + + var authDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"}); + var server_config = [ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: authDb.name + }, + { + section: "couch_httpd_auth", + key: "auth_cache_size", + value: "3" + }, + { + section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, default_authentication_handler}" + }, + { + section: "couch_httpd_auth", + key: "secret", + value: generateSecret(64) + } + ]; + + + function hits() { + var hits = CouchDB.requestStats("couchdb", "auth_cache_hits", true); + return hits.current || 0; + } + + + function misses() { + var misses = CouchDB.requestStats("couchdb", "auth_cache_misses", true); + return misses.current || 0; + } + + + function testFun() { + var hits_before, + misses_before, + hits_after, + misses_after; + + var fdmanana = CouchDB.prepareUserDoc({ + name: "fdmanana", + roles: ["dev"] + }, "qwerty"); + + T(authDb.save(fdmanana).ok); + + var chris = CouchDB.prepareUserDoc({ + name: "chris", + roles: ["dev", "mafia", "white_costume"] + }, "the_god_father"); + + T(authDb.save(chris).ok); + + var joe = CouchDB.prepareUserDoc({ + name: "joe", + roles: ["erlnager"] + }, "functional"); + + T(authDb.save(joe).ok); + + var johndoe = CouchDB.prepareUserDoc({ + name: "johndoe", + roles: ["user"] + }, "123456"); + + T(authDb.save(johndoe).ok); + + hits_before = hits(); + misses_before = misses(); + + T(CouchDB.login("fdmanana", "qwerty").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("fdmanana", "qwerty").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("chris", "the_god_father").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("joe", "functional").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("johndoe", "123456").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("joe", "functional").ok); + + hits_after = hits(); + misses_after = misses(); + + // it's an MRU cache, joe was removed from cache to add johndoe + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.logout().ok); + T(CouchDB.login("fdmanana", "qwerty").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + hits_before = hits_after; + misses_before = misses_after; + + var new_salt = CouchDB.newUuids(1)[0]; + var new_passwd = hex_sha1("foobar" + new_salt); + fdmanana.salt = new_salt; + fdmanana.password_sha = new_passwd; + + T(authDb.save(fdmanana).ok); + T(CouchDB.logout().ok); + + // cache was refreshed + T(CouchDB.login("fdmanana", "qwerty").error === "unauthorized"); + T(CouchDB.login("fdmanana", "foobar").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 2)); + + T(CouchDB.logout().ok); + + hits_before = hits_after; + misses_before = misses_after; + + // and yet another update + new_salt = CouchDB.newUuids(1)[0]; + new_passwd = hex_sha1("javascript" + new_salt); + fdmanana.salt = new_salt; + fdmanana.password_sha = new_passwd; + + T(authDb.save(fdmanana).ok); + T(CouchDB.logout().ok); + + // cache was refreshed + T(CouchDB.login("fdmanana", "foobar").error === "unauthorized"); + T(CouchDB.login("fdmanana", "javascript").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 2)); + + T(authDb.deleteDoc(fdmanana).ok); + T(CouchDB.logout().ok); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.login("fdmanana", "javascript").error === "unauthorized"); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + // login, compact authentication DB, login again and verify that + // there was a cache hit + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.login("johndoe", "123456").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === (misses_before + 1)); + T(hits_after === hits_before); + + T(CouchDB.logout().ok); + T(authDb.compact().ok); + + while (authDb.info().compact_running); + + hits_before = hits_after; + misses_before = misses_after; + + T(CouchDB.login("johndoe", "123456").ok); + + hits_after = hits(); + misses_after = misses(); + + T(misses_after === misses_before); + T(hits_after === (hits_before + 1)); + + T(CouchDB.logout().ok); + } + + + authDb.deleteDb(); + run_on_modified_server(server_config, testFun); + + // cleanup + authDb.deleteDb(); +}
\ No newline at end of file diff --git a/share/www/script/test/basics.js b/share/www/script/test/basics.js index 0f9ac44f..30c27c11 100644 --- a/share/www/script/test/basics.js +++ b/share/www/script/test/basics.js @@ -37,15 +37,14 @@ couchTests.basics = function(debug) { TEquals(dbname, xhr.getResponseHeader("Location").substr(-dbname.length), "should return Location header to newly created document"); - - TEquals("http://", - xhr.getResponseHeader("Location").substr(0, 7), + TEquals(CouchDB.protocol, + xhr.getResponseHeader("Location").substr(0, CouchDB.protocol.length), "should return absolute Location header to newly created document"); }); // Get the database info, check the db_name T(db.info().db_name == "test_suite_db"); - T(CouchDB.allDbs().indexOf("test_suite_db") != -1) + T(CouchDB.allDbs().indexOf("test_suite_db") != -1); // Get the database info, check the doc_count T(db.info().doc_count == 0); @@ -91,13 +90,13 @@ couchTests.basics = function(debug) { emit(null, doc.b); }; - results = db.query(mapFunction); + var results = db.query(mapFunction); // verify only one document found and the result value (doc.b). T(results.total_rows == 1 && results.rows[0].value == 16); // reopen document we saved earlier - existingDoc = db.open(id); + var existingDoc = db.open(id); T(existingDoc.a==1); @@ -152,18 +151,20 @@ couchTests.basics = function(debug) { // test that the POST response has a Location header var xhr = CouchDB.request("POST", "/test_suite_db", { - body: JSON.stringify({"foo":"bar"}) + body: JSON.stringify({"foo":"bar"}), + headers: {"Content-Type": "application/json"} }); var resp = JSON.parse(xhr.responseText); T(resp.ok); var loc = xhr.getResponseHeader("Location"); T(loc, "should have a Location header"); var locs = loc.split('/'); - T(locs[4] == resp.id); - T(locs[3] == "test_suite_db"); + T(locs[locs.length-1] == resp.id); + T(locs[locs.length-2] == "test_suite_db"); // test that that POST's with an _id aren't overriden with a UUID. var xhr = CouchDB.request("POST", "/test_suite_db", { + headers: {"Content-Type": "application/json"}, body: JSON.stringify({"_id": "oppossum", "yar": "matey"}) }); var resp = JSON.parse(xhr.responseText); @@ -179,9 +180,8 @@ couchTests.basics = function(debug) { TEquals("/test_suite_db/newdoc", xhr.getResponseHeader("Location").substr(-21), "should return Location header to newly created document"); - - TEquals("http://", - xhr.getResponseHeader("Location").substr(0, 7), + TEquals(CouchDB.protocol, + xhr.getResponseHeader("Location").substr(0, CouchDB.protocol.length), "should return absolute Location header to newly created document"); // deleting a non-existent doc should be 404 @@ -189,12 +189,12 @@ couchTests.basics = function(debug) { T(xhr.status == 404); // Check for invalid document members - bad_docs = [ + var bad_docs = [ ["goldfish", {"_zing": 4}], ["zebrafish", {"_zoom": "hello"}], ["mudfish", {"zane": "goldfish", "_fan": "something smells delicious"}], ["tastyfish", {"_bing": {"wha?": "soda can"}}] - ] + ]; var test_doc = function(info) { var data = JSON.stringify(info[1]); xhr = CouchDB.request("PUT", "/test_suite_db/" + info[0], {body: data}); @@ -202,7 +202,10 @@ couchTests.basics = function(debug) { result = JSON.parse(xhr.responseText); T(result.error == "doc_validation"); - xhr = CouchDB.request("POST", "/test_suite_db/", {body: data}); + xhr = CouchDB.request("POST", "/test_suite_db/", { + headers: {"Content-Type": "application/json"}, + body: data + }); T(xhr.status == 500); result = JSON.parse(xhr.responseText); T(result.error == "doc_validation"); diff --git a/share/www/script/test/batch_save.js b/share/www/script/test/batch_save.js index 1c8a2be9..a1b00192 100644 --- a/share/www/script/test/batch_save.js +++ b/share/www/script/test/batch_save.js @@ -36,7 +36,10 @@ couchTests.batch_save = function(debug) { // repeat the tests for POST for(i=0; i < 100; i++) { - var resp = db.request("POST", db.uri + "?batch=ok", {body: JSON.stringify({a:1})}); + var resp = db.request("POST", db.uri + "?batch=ok", { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({a:1}) + }); T(JSON.parse(resp.responseText).ok); } diff --git a/share/www/script/test/bulk_docs.js b/share/www/script/test/bulk_docs.js index 346aea83..9095e6b3 100644 --- a/share/www/script/test/bulk_docs.js +++ b/share/www/script/test/bulk_docs.js @@ -51,12 +51,12 @@ couchTests.bulk_docs = function(debug) { T(results.length == 5); T(results[0].id == "0"); T(results[0].error == "conflict"); - T(results[0].rev === undefined); // no rev member when a conflict + T(typeof results[0].rev === "undefined"); // no rev member when a conflict // but the rest are not for (i = 1; i < 5; i++) { T(results[i].id == i.toString()); - T(results[i].rev) + T(results[i].rev); T(db.open(docs[i]._id) == null); } @@ -64,7 +64,7 @@ couchTests.bulk_docs = function(debug) { // save doc 0, this will cause a conflict when we save docs[0] var doc = db.open("0"); - docs[0] = db.open("0") + docs[0] = db.open("0"); db.save(doc); docs[0].shooby = "dooby"; @@ -93,8 +93,8 @@ couchTests.bulk_docs = function(debug) { // Regression test for failure on update/delete var newdoc = {"_id": "foobar", "body": "baz"}; T(db.save(newdoc).ok); - update = {"_id": newdoc._id, "_rev": newdoc._rev, "body": "blam"}; - torem = {"_id": newdoc._id, "_rev": newdoc._rev, "_deleted": true}; + var update = {"_id": newdoc._id, "_rev": newdoc._rev, "body": "blam"}; + var torem = {"_id": newdoc._id, "_rev": newdoc._rev, "_deleted": true}; results = db.bulkSave([update, torem]); T(results[0].error == "conflict" || results[1].error == "conflict"); }; diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 0cbf3bd6..7ce3baaa 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -12,12 +12,12 @@ function jsonp(obj) { T(jsonp_flag == 0); - T(obj.results.length == 1 && obj.last_seq==1, "jsonp") + T(obj.results.length == 1 && obj.last_seq == 1, "jsonp"); jsonp_flag = 1; } couchTests.changes = function(debug) { - var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"}); db.deleteDb(); db.createDb(); if (debug) debugger; @@ -25,24 +25,32 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/test_suite_db/_changes"); var resp = JSON.parse(req.responseText); - T(resp.results.length == 0 && resp.last_seq==0, "empty db") + T(resp.results.length == 0 && resp.last_seq == 0, "empty db"); var docFoo = {_id:"foo", bar:1}; T(db.save(docFoo).ok); T(db.ensureFullCommit().ok); + T(db.open(docFoo._id)._id == docFoo._id); req = CouchDB.request("GET", "/test_suite_db/_changes"); var resp = JSON.parse(req.responseText); - T(resp.results.length == 1 && resp.last_seq==1, "one doc db") - T(resp.results[0].changes[0].rev == docFoo._rev) + T(resp.last_seq == 1); + T(resp.results.length == 1, "one doc db"); + T(resp.results[0].changes[0].rev == docFoo._rev); // test with callback - var xhr = CouchDB.request("GET", "/test_suite_db/_changes?callback=jsonp"); - T(xhr.status == 200); - jsonp_flag = 0; - eval(xhr.responseText); - T(jsonp_flag == 1); - + + run_on_modified_server( + [{section: "httpd", + key: "allow_jsonp", + value: "true"}], + function() { + var xhr = CouchDB.request("GET", "/test_suite_db/_changes?callback=jsonp"); + T(xhr.status == 200); + jsonp_flag = 0; + eval(xhr.responseText); + T(jsonp_flag == 1); + }); req = CouchDB.request("GET", "/test_suite_db/_changes?feed=continuous&timeout=10"); var lines = req.responseText.split("\n"); @@ -70,98 +78,111 @@ couchTests.changes = function(debug) { // WebKit (last checked on nightly #47686) does fail on processing // the async-request properly while javascript is executed. - var sleep = function(msecs) { - // by making a slow sync request, we allow the waiting XHR request data - // to be received. - var req = CouchDB.request("GET", "/_sleep?time=" + msecs); - T(JSON.parse(req.responseText).ok == true); - } - - xhr.open("GET", "/test_suite_db/_changes?feed=continuous", true); + xhr.open("GET", "/test_suite_db/_changes?feed=continuous&timeout=500", true); xhr.send(""); var docBar = {_id:"bar", bar:1}; db.save(docBar); + + var lines, change1, change2; + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + change1 = JSON.parse(lines[0]); + change2 = JSON.parse(lines[1]); + if (change2.seq != 2) { + throw "bad seq, try again"; + } + }, "bar-only"); - sleep(100); - var lines = xhr.responseText.split("\n"); - - var change = JSON.parse(lines[0]); - - T(change.seq == 1) - T(change.id == "foo") - - change = JSON.parse(lines[1]); - - T(change.seq == 2) - T(change.id == "bar") - T(change.changes[0].rev == docBar._rev) - + T(change1.seq == 1); + T(change1.id == "foo"); + + T(change2.seq == 2); + T(change2.id == "bar"); + T(change2.changes[0].rev == docBar._rev); + + var docBaz = {_id:"baz", baz:1}; db.save(docBaz); - sleep(100); - var lines = xhr.responseText.split("\n"); - - change = JSON.parse(lines[2]); - - T(change.seq == 3); - T(change.id == "baz"); - T(change.changes[0].rev == docBaz._rev); + var change3; + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + change3 = JSON.parse(lines[2]); + if (change3.seq != 3) { + throw "bad seq, try again"; + } + }); + + T(change3.seq == 3); + T(change3.id == "baz"); + T(change3.changes[0].rev == docBaz._rev); xhr = CouchDB.newXhr(); //verify the hearbeat newlines are sent - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&heartbeat=10", true); + xhr.open("GET", "/test_suite_db/_changes?feed=continuous&heartbeat=10&timeout=500", true); xhr.send(""); + + var str; + waitForSuccess(function() { + str = xhr.responseText; + if (str.charAt(str.length - 1) != "\n" || str.charAt(str.length - 2) != "\n") { + throw("keep waiting"); + } + }, "heartbeat"); - sleep(100); - - var str = xhr.responseText; - - T(str.charAt(str.length - 1) == "\n") - T(str.charAt(str.length - 2) == "\n") + T(str.charAt(str.length - 1) == "\n"); + T(str.charAt(str.length - 2) == "\n"); + // otherwise we'll continue to receive heartbeats forever + xhr.abort(); // test longpolling xhr = CouchDB.newXhr(); xhr.open("GET", "/test_suite_db/_changes?feed=longpoll", true); xhr.send(""); - - sleep(100); - var lines = xhr.responseText.split("\n"); - T(lines[5]=='"last_seq":3}'); - + + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + if (lines[5] != '"last_seq":3}') { + throw("still waiting"); + } + }, "last_seq"); + xhr = CouchDB.newXhr(); xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=3", true); xhr.send(""); - sleep(100); - var docBarz = {_id:"barz", bar:1}; db.save(docBarz); - - sleep(100); - - var lines = xhr.responseText.split("\n"); - + var parse_changes_line = function(line) { if (line.charAt(line.length-1) == ",") { - line = line.substring(0, line.length-1); + var linetrimmed = line.substring(0, line.length-1); + } else { + var linetrimmed = line; } - return JSON.parse(line); - } - - change = parse_changes_line(lines[1]); - + return JSON.parse(linetrimmed); + }; + + waitForSuccess(function() { + lines = xhr.responseText.split("\n"); + if (lines[3] != '"last_seq":4}') { + throw("still waiting"); + } + }, "change_lines"); + + var change = parse_changes_line(lines[1]); T(change.seq == 4); T(change.id == "barz"); T(change.changes[0].rev == docBarz._rev); T(lines[3]=='"last_seq":4}'); - + + } // test the filtered changes @@ -175,9 +196,18 @@ couchTests.changes = function(debug) { }), "userCtx" : stringFun(function(doc, req) { return doc.user && (doc.user == req.userCtx.name); - }) + }), + "conflicted" : "function(doc, req) { return (doc._conflicts);}" + }, + options : { + local_seq : true + }, + views : { + local_seq : { + map : "function(doc) {emit(doc._local_seq, null)}" + } } - } + }; db.save(ddoc); @@ -190,7 +220,7 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop"); var resp = JSON.parse(req.responseText); - T(resp.results.length == 1); + T(resp.results.length == 1, "filtered/bop"); req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=woox"); resp = JSON.parse(req.responseText); @@ -198,15 +228,14 @@ couchTests.changes = function(debug) { req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/dynamic&field=bop"); resp = JSON.parse(req.responseText); - T(resp.results.length == 1); + T(resp.results.length == 1, "changes_filter/dynamic&field=bop"); if (!is_safari && xhr) { // full test requires parallel connections // filter with longpoll // longpoll filters full history when run without a since seq xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop", true); + xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&filter=changes_filter/bop", false); xhr.send(""); - sleep(100); var resp = JSON.parse(xhr.responseText); T(resp.last_seq == 7); // longpoll waits until a matching change before returning @@ -215,22 +244,53 @@ couchTests.changes = function(debug) { xhr.send(""); db.save({"_id":"falsy", "bop" : ""}); // empty string is falsy db.save({"_id":"bingo","bop" : "bingo"}); - sleep(100); - var resp = JSON.parse(xhr.responseText); + + waitForSuccess(function() { + resp = JSON.parse(xhr.responseText); + }, "longpoll-since"); + T(resp.last_seq == 9); T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update"); - - // filter with continuous - xhr = CouchDB.newXhr(); - xhr.open("GET", "/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout=200", true); - xhr.send(""); - db.save({"_id":"rusty", "bop" : "plankton"}); - T(db.ensureFullCommit().ok); - sleep(300); - var lines = xhr.responseText.split("\n"); - T(JSON.parse(lines[1]).id == "bingo", lines[1]); - T(JSON.parse(lines[2]).id == "rusty", lines[2]); - T(JSON.parse(lines[3]).last_seq == 10, lines[3]); + xhr.abort(); + + var timeout = 500; + var last_seq = 10; + while (true) { + + // filter with continuous + xhr = CouchDB.newXhr(); + xhr.open("GET", "/test_suite_db/_changes?feed=continuous&filter=changes_filter/bop&timeout="+timeout, true); + xhr.send(""); + + db.save({"_id":"rusty", "bop" : "plankton"}); + T(xhr.readyState != 4, "test client too slow"); + var rusty = db.open("rusty", {cache_bust : new Date()}); + T(rusty._id == "rusty"); + + waitForSuccess(function() { // throws an error after 5 seconds + if (xhr.readyState != 4) { + throw("still waiting"); + } + }, "continuous-rusty"); + lines = xhr.responseText.split("\n"); + var good = false; + try { + JSON.parse(lines[3]); + good = true; + } catch(e) { + } + if (good) { + T(JSON.parse(lines[1]).id == "bingo", lines[1]); + T(JSON.parse(lines[2]).id == "rusty", lines[2]); + T(JSON.parse(lines[3]).last_seq == last_seq, lines[3]); + break; + } else { + xhr.abort(); + db.deleteDoc(rusty); + timeout = timeout * 2; + last_seq = last_seq + 2; + } + } } // error conditions @@ -257,7 +317,8 @@ couchTests.changes = function(debug) { var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/bop&style=all_docs"); var resp = JSON.parse(req.responseText); - TEquals(3, resp.results.length, "should return matching rows"); + var expect = (!is_safari && xhr) ? 3: 1; + TEquals(expect, resp.results.length, "should return matching rows"); // test for userCtx run_on_modified_server( @@ -286,9 +347,119 @@ couchTests.changes = function(debug) { resp = JSON.parse(req.responseText); T(resp.results.length == 1, "userCtx"); T(resp.results[0].id == docResp.id); - }); - - req = CouchDB.request("GET", "/test_suite_db/_changes?limit=1"); - resp = JSON.parse(req.responseText); - TEquals(1, resp.results.length) + } + ); + + req = CouchDB.request("GET", "/test_suite_db/_changes?limit=1"); + resp = JSON.parse(req.responseText); + TEquals(1, resp.results.length); + + //filter includes _conflicts + var id = db.save({'food' : 'pizza'}).id; + db.bulkSave([{_id: id, 'food' : 'pasta'}], {all_or_nothing:true}); + + req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/conflicted"); + resp = JSON.parse(req.responseText); + T(resp.results.length == 1, "filter=changes_filter/conflicted"); + + // test with erlang filter function + run_on_modified_server([{ + section: "native_query_servers", + key: "erlang", + value: "{couch_native_process, start_link, []}" + }], function() { + var erl_ddoc = { + _id: "_design/erlang", + language: "erlang", + filters: { + foo: + 'fun({Doc}, Req) -> ' + + ' Value = couch_util:get_value(<<"value">>, Doc),' + + ' (Value rem 2) =:= 0' + + 'end.' + } + }; + + db.deleteDb(); + db.createDb(); + T(db.save(erl_ddoc).ok); + + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=erlang/foo"); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 0); + + T(db.save({_id: "doc1", value : 1}).ok); + T(db.save({_id: "doc2", value : 2}).ok); + T(db.save({_id: "doc3", value : 3}).ok); + T(db.save({_id: "doc4", value : 4}).ok); + + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=erlang/foo"); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 2); + T(resp.results[0].id === "doc2"); + T(resp.results[1].id === "doc4"); + + // test filtering on docids + // + + var options = { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({"doc_ids": ["something", "anotherthing", "andmore"]}) + }; + + var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 0); + + T(db.save({"_id":"something", "bop" : "plankton"}).ok); + var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 1); + T(resp.results[0].id === "something"); + + T(db.save({"_id":"anotherthing", "bop" : "plankton"}).ok); + var req = CouchDB.request("POST", "/test_suite_db/_changes?filter=_doc_ids", options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 2); + T(resp.results[0].id === "something"); + T(resp.results[1].id === "anotherthing"); + + var docids = JSON.stringify(["something", "anotherthing", "andmore"]), + req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_doc_ids&doc_ids="+docids, options); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 2); + T(resp.results[0].id === "something"); + T(resp.results[1].id === "anotherthing"); + + var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=_design"); + var resp = JSON.parse(req.responseText); + T(resp.results.length === 1); + T(resp.results[0].id === "_design/erlang"); + + + if (!is_safari && xhr) { + // filter docids with continuous + xhr = CouchDB.newXhr(); + xhr.open("POST", "/test_suite_db/_changes?feed=continuous&timeout=500&since=7&filter=_doc_ids", true); + xhr.setRequestHeader("Content-Type", "application/json"); + + xhr.send(options.body); + + T(db.save({"_id":"andmore", "bop" : "plankton"}).ok); + + + waitForSuccess(function() { + if (xhr.readyState != 4) { + throw("still waiting"); + } + }, "andmore-only"); + + var line = JSON.parse(xhr.responseText.split("\n")[0]); + T(line.seq == 8); + T(line.id == "andmore"); + } + + }); + }; + diff --git a/share/www/script/test/compact.js b/share/www/script/test/compact.js index f63bfc57..805a3b08 100644 --- a/share/www/script/test/compact.js +++ b/share/www/script/test/compact.js @@ -26,11 +26,12 @@ couchTests.compact = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; T(db.save(binAttDoc).ok); var originalsize = db.info().disk_size; + var start_time = db.info().instance_start_time; for(var i in docs) { db.deleteDoc(docs[i]); @@ -38,17 +39,20 @@ couchTests.compact = function(debug) { T(db.ensureFullCommit().ok); var deletesize = db.info().disk_size; T(deletesize > originalsize); + T(db.setDbProperty("_revs_limit", 666).ok); T(db.compact().ok); T(db.last_req.status == 202); // compaction isn't instantaneous, loop until done while (db.info().compact_running) {}; + T(db.info().instance_start_time == start_time); + T(db.getDbProperty("_revs_limit") === 666); T(db.ensureFullCommit().ok); restartServer(); var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt"); - T(xhr.responseText == "This is a base64 encoded text") - T(xhr.getResponseHeader("Content-Type") == "text/plain") + T(xhr.responseText == "This is a base64 encoded text"); + T(xhr.getResponseHeader("Content-Type") == "text/plain"); T(db.info().doc_count == 1); T(db.info().disk_size < deletesize); diff --git a/share/www/script/test/config.js b/share/www/script/test/config.js index ef74934b..e83ecfd9 100644 --- a/share/www/script/test/config.js +++ b/share/www/script/test/config.js @@ -29,19 +29,25 @@ couchTests.config = function(debug) { */ var server_port = CouchDB.host.split(':'); if(server_port.length == 1 && CouchDB.inBrowser) { - var proto = window.location.protocol; - if(proto == "http:") { + if(CouchDB.protocol == "http://") { port = 80; } - if(proto == "https:") { + if(CouchDB.protocol == "https://") { port = 443; } } else { port = server_port.pop(); } + if(CouchDB.protocol == "http://") { + config_port = config.httpd.port; + } + if(CouchDB.protocol == "https://") { + config_port = config.ssl.port; + } + if(port) { - T(config.httpd.port == port); + TEquals(config_port, port, "ports should match"); } T(config.couchdb.database_dir); @@ -50,7 +56,8 @@ couchTests.config = function(debug) { T(config.log.level); T(config.query_servers.javascript); - // test that settings can be altered + // test that settings can be altered, and that an undefined whitelist allows any change + TEquals(undefined, config.httpd.config_whitelist, "Default whitelist is empty"); xhr = CouchDB.request("PUT", "/_config/test/foo",{ body : JSON.stringify("bar"), headers: {"X-Couch-Persist": "false"} @@ -64,4 +71,93 @@ couchTests.config = function(debug) { xhr = CouchDB.request("GET", "/_config/test/foo"); config = JSON.parse(xhr.responseText); T(config == "bar"); + + // Non-term whitelist values allow further modification of the whitelist. + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("!This is an invalid Erlang term!"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to an invalid Erlang term"); + xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{ + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Modify whitelist despite it being invalid syntax"); + + // Non-list whitelist values allow further modification of the whitelist. + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("{[yes, a_valid_erlang_term, but_unfortunately, not_a_list]}"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to an non-list term"); + xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{ + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Modify whitelist despite it not being a list"); + + // Keys not in the whitelist may not be modified. + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("[{httpd,config_whitelist}, {test,foo}]"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to something valid"); + + ["PUT", "DELETE"].forEach(function(method) { + ["test/not_foo", "not_test/foo", "neither_test/nor_foo"].forEach(function(pair) { + var path = "/_config/" + pair; + var test_name = method + " to " + path + " disallowed: not whitelisted"; + + xhr = CouchDB.request(method, path, { + body : JSON.stringify("Bummer! " + test_name), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(400, xhr.status, test_name); + }); + }); + + // Keys in the whitelist may be modified. + ["PUT", "DELETE"].forEach(function(method) { + xhr = CouchDB.request(method, "/_config/test/foo",{ + body : JSON.stringify(method + " to whitelisted config variable"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Keys in the whitelist may be modified"); + }); + + // Non-2-tuples in the whitelist are ignored + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("[{httpd,config_whitelist}, these, {are}, {nOt, 2, tuples}," + + " [so], [they, will], [all, become, noops], {test,foo}]"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist with some inert values"); + ["PUT", "DELETE"].forEach(function(method) { + xhr = CouchDB.request(method, "/_config/test/foo",{ + body : JSON.stringify(method + " to whitelisted config variable"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Update whitelisted variable despite invalid entries"); + }); + + // Atoms, binaries, and strings suffice as whitelist sections and keys. + ["{test,foo}", '{"test","foo"}', '{<<"test">>,<<"foo">>}'].forEach(function(pair) { + xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{ + body : JSON.stringify("[{httpd,config_whitelist}, " + pair + "]"), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Set config whitelist to include " + pair); + + var pair_format = {"t":"tuple", '"':"string", "<":"binary"}[pair[1]]; + ["PUT", "DELETE"].forEach(function(method) { + xhr = CouchDB.request(method, "/_config/test/foo",{ + body : JSON.stringify(method + " with " + pair_format), + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Whitelist works with " + pair_format); + }); + }); + + xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{ + headers: {"X-Couch-Persist": "false"} + }); + TEquals(200, xhr.status, "Reset config whitelist to undefined"); }; diff --git a/share/www/script/test/conflicts.js b/share/www/script/test/conflicts.js index b8b93946..7258bc31 100644 --- a/share/www/script/test/conflicts.js +++ b/share/www/script/test/conflicts.js @@ -44,7 +44,7 @@ couchTests.conflicts = function(debug) { var changes = db.changes(); - T( changes.results.length == 1) + T(changes.results.length == 1); // Now clear out the _rev member and save. This indicates this document is // new, not based on an existing revision. diff --git a/share/www/script/test/content_negotiation.js b/share/www/script/test/content_negotiation.js index 171dbb3d..c79df948 100644 --- a/share/www/script/test/content_negotiation.js +++ b/share/www/script/test/content_negotiation.js @@ -17,11 +17,14 @@ couchTests.content_negotiation = function(debug) { if (debug) debugger; var xhr; - xhr = CouchDB.request("GET", "/test_suite_db/"); - TEquals("text/plain;charset=utf-8", xhr.getResponseHeader("Content-Type")); + // with no accept header + var req = CouchDB.newXhr(); + req.open("GET", "/test_suite_db/", false); + req.send(""); + TEquals("text/plain;charset=utf-8", req.getResponseHeader("Content-Type")); // make sure JSON responses end in a newline - var text = xhr.responseText; + var text = req.responseText; TEquals("\n", text[text.length-1]); xhr = CouchDB.request("GET", "/test_suite_db/", { diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js index 9eadfee0..8ad993cc 100644 --- a/share/www/script/test/cookie_auth.js +++ b/share/www/script/test/cookie_auth.js @@ -46,39 +46,39 @@ couchTests.cookie_auth = function(debug) { // Create a user var jasonUserDoc = CouchDB.prepareUserDoc({ - username: "Jason Davies", + name: "Jason Davies", roles: ["dev"] }, password); T(usersDb.save(jasonUserDoc).ok); var checkDoc = usersDb.open(jasonUserDoc._id); - T(checkDoc.username == "Jason Davies"); + T(checkDoc.name == "Jason Davies"); var jchrisUserDoc = CouchDB.prepareUserDoc({ - username: "jchris@apache.org" + name: "jchris@apache.org" }, "funnybone"); T(usersDb.save(jchrisUserDoc).ok); // make sure we cant create duplicate users var duplicateJchrisDoc = CouchDB.prepareUserDoc({ - username: "jchris@apache.org" + name: "jchris@apache.org" }, "eh, Boo-Boo?"); try { - usersDb.save(duplicateJchrisDoc) + usersDb.save(duplicateJchrisDoc); T(false && "Can't create duplicate user names. Should have thrown an error."); } catch (e) { T(e.error == "conflict"); T(usersDb.last_req.status == 409); } - // we can't create _usernames + // we can't create _names var underscoreUserDoc = CouchDB.prepareUserDoc({ - username: "_why" + name: "_why" }, "copperfield"); try { - usersDb.save(underscoreUserDoc) + usersDb.save(underscoreUserDoc); T(false && "Can't create underscore user names. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -87,46 +87,59 @@ couchTests.cookie_auth = function(debug) { // we can't create docs with malformed ids var badIdDoc = CouchDB.prepareUserDoc({ - username: "foo" + name: "foo" }, "bar"); badIdDoc._id = "org.apache.couchdb:w00x"; try { - usersDb.save(badIdDoc) + usersDb.save(badIdDoc); T(false && "Can't create malformed docids. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); T(usersDb.last_req.status == 403); } - - try { - usersDb.save(underscoreUserDoc) - T(false && "Can't create underscore user names. Should have thrown an error."); - } catch (e) { - T(e.error == "forbidden"); - T(usersDb.last_req.status == 403); - } // login works T(CouchDB.login('Jason Davies', password).ok); - T(CouchDB.session().name == 'Jason Davies'); + T(CouchDB.session().userCtx.name == 'Jason Davies'); + // JSON login works + var xhr = CouchDB.request("POST", "/_session", { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + name: 'Jason Davies', + password: password + }) + }); + + T(JSON.parse(xhr.responseText).ok); + T(CouchDB.session().userCtx.name == 'Jason Davies'); + // update one's own credentials document jasonUserDoc.foo=2; T(usersDb.save(jasonUserDoc).ok); + T(CouchDB.session().userCtx.roles.indexOf("_admin") == -1); + // can't delete another users doc unless you are admin + try { + usersDb.deleteDoc(jchrisUserDoc); + T(false && "Can't delete other users docs. Should have thrown an error."); + } catch (e) { + T(e.error == "forbidden"); + T(usersDb.last_req.status == 403); + } // TODO should login() throw an exception here? T(!CouchDB.login('Jason Davies', "2.71828").ok); T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok); // a failed login attempt should log you out - T(CouchDB.session().name != 'Jason Davies'); + T(CouchDB.session().userCtx.name != 'Jason Davies'); // test redirect xhr = CouchDB.request("POST", "/_session?next=/", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, - body: "username=Jason%20Davies&password="+encodeURIComponent(password) + body: "name=Jason%20Davies&password="+encodeURIComponent(password) }); // should this be a redirect code instead of 200? // The cURL adapter is returning the expected 302 here. @@ -134,23 +147,23 @@ couchTests.cookie_auth = function(debug) { // to follow the redirect, ie, the browser follows and does a // GET on the returned Location if (xhr.status == 200) { - T(/Welcome/.test(xhr.responseText)) + T(/Welcome/.test(xhr.responseText)); } else { - T(xhr.status == 302) - T(xhr.getResponseHeader("Location")) + T(xhr.status == 302); + T(xhr.getResponseHeader("Location")); } // test users db validations // // test that you can't update docs unless you are logged in as the user (or are admin) T(CouchDB.login("jchris@apache.org", "funnybone").ok); - T(CouchDB.session().name == "jchris@apache.org"); - T(CouchDB.session().roles.length == 0); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + T(CouchDB.session().userCtx.roles.length == 0); jasonUserDoc.foo=3; try { - usersDb.save(jasonUserDoc) + usersDb.save(jasonUserDoc); T(false && "Can't update someone else's user doc. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -161,7 +174,7 @@ couchTests.cookie_auth = function(debug) { jchrisUserDoc.roles = ["foo"]; try { - usersDb.save(jchrisUserDoc) + usersDb.save(jchrisUserDoc); T(false && "Can't set roles unless you are admin. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -169,7 +182,7 @@ couchTests.cookie_auth = function(debug) { } T(CouchDB.logout().ok); - T(CouchDB.session().roles[0] == "_admin"); + T(CouchDB.session().userCtx.roles[0] == "_admin"); jchrisUserDoc.foo = ["foo"]; T(usersDb.save(jchrisUserDoc).ok); @@ -178,7 +191,7 @@ couchTests.cookie_auth = function(debug) { jchrisUserDoc.roles = ["_bar"]; try { - usersDb.save(jchrisUserDoc) + usersDb.save(jchrisUserDoc); T(false && "Can't add system roles to user's db. Should have thrown an error."); } catch (e) { T(e.error == "forbidden"); @@ -187,24 +200,24 @@ couchTests.cookie_auth = function(debug) { // make sure the foo role has been applied T(CouchDB.login("jchris@apache.org", "funnybone").ok); - T(CouchDB.session().name == "jchris@apache.org"); - T(CouchDB.session().roles.indexOf("_admin") == -1); - T(CouchDB.session().roles.indexOf("foo") != -1); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") == -1); + T(CouchDB.session().userCtx.roles.indexOf("foo") != -1); // now let's make jchris a server admin T(CouchDB.logout().ok); - T(CouchDB.session().roles[0] == "_admin"); - T(CouchDB.session().name == null); + T(CouchDB.session().userCtx.roles[0] == "_admin"); + T(CouchDB.session().userCtx.name == null); // set the -hashed- password so the salt matches // todo ask on the ML about this run_on_modified_server([{section: "admins", key: "jchris@apache.org", value: "funnybone"}], function() { T(CouchDB.login("jchris@apache.org", "funnybone").ok); - T(CouchDB.session().name == "jchris@apache.org"); - T(CouchDB.session().roles.indexOf("_admin") != -1); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") != -1); // test that jchris still has the foo role - T(CouchDB.session().roles.indexOf("foo") != -1); + T(CouchDB.session().userCtx.roles.indexOf("foo") != -1); // should work even when user doc has no password jchrisUserDoc = usersDb.open(jchrisUserDoc._id); @@ -214,13 +227,13 @@ couchTests.cookie_auth = function(debug) { T(CouchDB.logout().ok); T(CouchDB.login("jchris@apache.org", "funnybone").ok); var s = CouchDB.session(); - T(s.name == "jchris@apache.org"); - T(s.roles.indexOf("_admin") != -1); + T(s.userCtx.name == "jchris@apache.org"); + T(s.userCtx.roles.indexOf("_admin") != -1); // test session info - T(s.info.authenticated == "{couch_httpd_auth, cookie_authentication_handler}"); - T(s.info.user_db == "test_suite_users"); + T(s.info.authenticated == "cookie"); + T(s.info.authentication_db == "test_suite_users"); // test that jchris still has the foo role - T(CouchDB.session().roles.indexOf("foo") != -1); + T(CouchDB.session().userCtx.roles.indexOf("foo") != -1); }); } finally { diff --git a/share/www/script/test/copy_doc.js b/share/www/script/test/copy_doc.js index a6de1892..99e3c7fe 100644 --- a/share/www/script/test/copy_doc.js +++ b/share/www/script/test/copy_doc.js @@ -36,6 +36,9 @@ couchTests.copy_doc = function(debug) { }); T(xhr.status == 409); // conflict + var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied2"); + T(xhr.status == 400); // bad request (no Destination header) + var rev = db.open("doc_to_be_overwritten")._rev; var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied2", { headers: {"Destination":"doc_to_be_overwritten?rev=" + rev} diff --git a/share/www/script/test/delayed_commits.js b/share/www/script/test/delayed_commits.js index d0c87182..dbb072fb 100644 --- a/share/www/script/test/delayed_commits.js +++ b/share/www/script/test/delayed_commits.js @@ -122,4 +122,33 @@ couchTests.delayed_commits = function(debug) { } }); + + // Test that a conflict can't cause delayed commits to fail + run_on_modified_server( + [{section: "couchdb", + key: "delayed_commits", + value: "true"}], + + function() { + //First save a document and commit it + T(db.save({_id:"6",a:2,b:4}).ok); + T(db.ensureFullCommit().ok); + //Generate a conflict + try { + db.save({_id:"6",a:2,b:4}); + } catch( e) { + T(e.error == "conflict"); + } + //Wait for the delayed commit interval to pass + var time = new Date(); + while(new Date() - time < 2000); + //Save a new doc + T(db.save({_id:"7",a:2,b:4}).ok); + //Wait for the delayed commit interval to pass + var time = new Date(); + while(new Date() - time < 2000); + //Crash the server and make sure the last doc was written + restartServer(); + T(db.open("7") != null); + }); }; diff --git a/share/www/script/test/design_docs.js b/share/www/script/test/design_docs.js index 9318d2bc..f2f63841 100644 --- a/share/www/script/test/design_docs.js +++ b/share/www/script/test/design_docs.js @@ -13,139 +13,309 @@ couchTests.design_docs = function(debug) { var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); var db2 = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); + + if (debug) debugger; + db.deleteDb(); db.createDb(); db2.deleteDb(); db2.createDb(); - if (debug) debugger; - run_on_modified_server( - [{section: "query_server_config", + var server_config = [ + { + section: "query_server_config", key: "reduce_limit", - value: "false"}], -function() { + value: "false" + } + ]; - var numDocs = 500; + var testFun = function() { + var numDocs = 500; - function makebigstring(power) { - var str = "a"; - while(power-- > 0) { - str = str + str; + function makebigstring(power) { + var str = "a"; + while(power-- > 0) { + str = str + str; + } + return str; } - return str; - } - - var designDoc = { - _id:"_design/test", // turn off couch.js id escaping? - language: "javascript", - views: { - all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"}, - no_docs: {map: "function(doc) {}"}, - single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}, - summate: {map:"function (doc) {emit(doc.integer, doc.integer)};", - reduce:"function (keys, values) { return sum(values); };"}, - summate2: {map:"function (doc) {emit(doc.integer, doc.integer)};", - reduce:"function (keys, values) { return sum(values); };"}, - huge_src_and_results: {map: "function(doc) { if (doc._id == \"1\") { emit(\"" + makebigstring(16) + "\", null) }}", - reduce:"function (keys, values) { return \"" + makebigstring(16) + "\"; };"} - }, - shows: { - simple: "function() {return 'ok'};" - } - } - var xhr = CouchDB.request("PUT", "/test_suite_db_a/_design/test", {body: JSON.stringify(designDoc)}); - var resp = JSON.parse(xhr.responseText); - - TEquals(resp.rev, db.save(designDoc).rev); - - // test that editing a show fun on the ddoc results in a change in output - var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); - T(xhr.status == 200); - TEquals(xhr.responseText, "ok"); - - designDoc.shows.simple = "function() {return 'ko'};" - T(db.save(designDoc).ok); - - var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); - T(xhr.status == 200); - TEquals(xhr.responseText, "ko"); - - var xhr = CouchDB.request("GET", "/test_suite_db_a/_design/test/_show/simple?cache=buster"); - T(xhr.status == 200); - TEquals("ok", xhr.responseText, 'query server used wrong ddoc'); - - // test that we get design doc info back - var dinfo = db.designInfo("_design/test"); - TEquals("test", dinfo.name); - var vinfo = dinfo.view_index; - TEquals(51, vinfo.disk_size); - TEquals(false, vinfo.compact_running); - TEquals("3f88e53b303e2342e49a66c538c30679", vinfo.signature); - - db.bulkSave(makeDocs(1, numDocs + 1)); - - // test that the _all_docs view returns correctly with keys - var results = db.allDocs({startkey:"_design", endkey:"_design0"}); - T(results.rows.length == 1); - - for (var loop = 0; loop < 2; loop++) { - var rows = db.view("test/all_docs_twice").rows; - for (var i = 0; i < numDocs; i++) { - T(rows[2*i].key == i+1); - T(rows[(2*i)+1].key == i+1); - } - T(db.view("test/no_docs").total_rows == 0) - T(db.view("test/single_doc").total_rows == 1) + + var designDoc = { + _id: "_design/test", + language: "javascript", + whatever : { + stringzone : "exports.string = 'plankton';", + commonjs : { + whynot : "exports.test = require('../stringzone'); " + + "exports.foo = require('whatever/stringzone');", + upper : "exports.testing = require('./whynot').test.string.toUpperCase()+" + + "module.id+require('./whynot').foo.string" + } + }, + views: { + all_docs_twice: { + map: + (function(doc) { + emit(doc.integer, null); + emit(doc.integer, null); + }).toString() + }, + no_docs: { + map: + (function(doc) { + }).toString() + }, + single_doc: { + map: + (function(doc) { + if (doc._id === "1") { + emit(1, null); + } + }).toString() + }, + summate: { + map: + (function(doc) { + emit(doc.integer, doc.integer); + }).toString(), + reduce: + (function(keys, values) { + return sum(values); + }).toString() + }, + summate2: { + map: + (function(doc) { + emit(doc.integer, doc.integer); + }).toString(), + reduce: + (function(keys, values) { + return sum(values); + }).toString() + }, + huge_src_and_results: { + map: + (function(doc) { + if (doc._id === "1") { + emit(makebigstring(16), null); + } + }).toString(), + reduce: + (function(keys, values) { + return makebigstring(16); + }).toString() + }, + lib : { + baz : "exports.baz = 'bam';", + foo : { + foo : "exports.foo = 'bar';", + boom : "exports.boom = 'ok';", + zoom : "exports.zoom = 'yeah';" + } + }, + commonjs : { + map : + (function(doc) { + emit(null, require('views/lib/foo/boom').boom); + }).toString() + } + }, + shows: { + simple: + (function() { + return 'ok'; + }).toString(), + requirey: + (function() { + var lib = require('whatever/commonjs/upper'); + return lib.testing; + }).toString(), + circular: + (function() { + var lib = require('whatever/commonjs/upper'); + return JSON.stringify(this); + }).toString() + } + }; // designDoc + + var xhr = CouchDB.request( + "PUT", "/test_suite_db_a/_design/test", {body: JSON.stringify(designDoc)} + ); + var resp = JSON.parse(xhr.responseText); + + TEquals(resp.rev, db.save(designDoc).rev); + + // test that editing a show fun on the ddoc results in a change in output + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ok"); + + designDoc.shows.simple = (function() { + return 'ko'; + }).toString(); + T(db.save(designDoc).ok); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ko"); + + xhr = CouchDB.request( + "GET", "/test_suite_db_a/_design/test/_show/simple?cache=buster" + ); + T(xhr.status == 200); + TEquals("ok", xhr.responseText, 'query server used wrong ddoc'); + + // test commonjs require + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/requirey"); + T(xhr.status == 200); + TEquals("PLANKTONwhatever/commonjs/upperplankton", xhr.responseText); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/circular"); + T(xhr.status == 200); + TEquals("javascript", JSON.parse(xhr.responseText).language); + + var prev_view_sig = db.designInfo("_design/test").view_index.signature; + var prev_view_size = db.designInfo("_design/test").view_index.disk_size; + + db.bulkSave(makeDocs(1, numDocs + 1)); T(db.ensureFullCommit().ok); - restartServer(); - }; - - // test when language not specified, Javascript is implied - var designDoc2 = { - _id:"_design/test2", - // language: "javascript", - views: { - single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"} - } - }; - T(db.save(designDoc2).ok); - T(db.view("test2/single_doc").total_rows == 1); + // test that we get correct design doc info back, + // and also that GET /db/_design/test/_info + // hasn't triggered an update of the views + db.view("test/summate", {stale: "ok"}); // make sure view group's open + for (var i = 0; i < 2; i++) { + var dinfo = db.designInfo("_design/test"); + TEquals("test", dinfo.name); + var vinfo = dinfo.view_index; + TEquals(prev_view_size, vinfo.disk_size, "view group disk size didn't change"); + TEquals(false, vinfo.compact_running); + TEquals(prev_view_sig, vinfo.signature, 'ddoc sig'); + // wait some time (there were issues where an update + // of the views had been triggered in the background) + var start = new Date().getTime(); + while (new Date().getTime() < start + 2000); + TEquals(0, db.view("test/all_docs_twice", {stale: "ok"}).total_rows, 'view info'); + TEquals(0, db.view("test/single_doc", {stale: "ok"}).total_rows, 'view info'); + TEquals(0, db.view("test/summate", {stale: "ok"}).rows.length, 'view info'); + T(db.ensureFullCommit().ok); + restartServer(); + }; + + db.bulkSave(makeDocs(numDocs + 1, numDocs * 2 + 1)); + T(db.ensureFullCommit().ok); + + // open view group + db.view("test/summate", {stale: "ok"}); + // wait so the views can get initialized + var start = new Date().getTime(); + while (new Date().getTime() < start + 2000); + + // test that POST /db/_view_cleanup + // doesn't trigger an update of the views + var len1 = db.view("test/all_docs_twice", {stale: "ok"}).total_rows; + var len2 = db.view("test/single_doc", {stale: "ok"}).total_rows; + var len3 = db.view("test/summate", {stale: "ok"}).rows.length; + for (i = 0; i < 2; i++) { + T(db.viewCleanup().ok); + // wait some time (there were issues where an update + // of the views had been triggered in the background) + start = new Date().getTime(); + while (new Date().getTime() < start + 2000); + TEquals(len1, db.view("test/all_docs_twice", {stale: "ok"}).total_rows, 'view cleanup'); + TEquals(len2, db.view("test/single_doc", {stale: "ok"}).total_rows, 'view cleanup'); + TEquals(len3, db.view("test/summate", {stale: "ok"}).rows.length, 'view cleanup'); + T(db.ensureFullCommit().ok); + restartServer(); + // we'll test whether the view group stays closed + // and the views stay uninitialized (they should!) + len1 = len2 = len3 = 0; + }; + + // test commonjs in map functions + resp = db.view("test/commonjs", {limit:1}); + T(resp.rows[0].value == 'ok'); + + // test that the _all_docs view returns correctly with keys + var results = db.allDocs({startkey:"_design", endkey:"_design0"}); + T(results.rows.length == 1); + + for (i = 0; i < 2; i++) { + var rows = db.view("test/all_docs_twice").rows; + for (var j = 0; j < numDocs; j++) { + T(rows[2 * j].key == (j + 1)); + T(rows[(2 * j) + 1].key == (j + 1)); + }; + T(db.view("test/no_docs").total_rows == 0); + T(db.view("test/single_doc").total_rows == 1); + T(db.ensureFullCommit().ok); + restartServer(); + }; - var summate = function(N) {return (N+1)*N/2;}; - var result = db.view("test/summate"); - T(result.rows[0].value == summate(numDocs)); + // test when language not specified, Javascript is implied + var designDoc2 = { + _id: "_design/test2", + // language: "javascript", + views: { + single_doc: { + map: + (function(doc) { + if (doc._id === "1") { + emit(1, null); + } + }).toString() + } + } + }; - result = db.view("test/summate", {startkey:4,endkey:4}); - T(result.rows[0].value == 4); + T(db.save(designDoc2).ok); + T(db.view("test2/single_doc").total_rows == 1); - result = db.view("test/summate", {startkey:4,endkey:5}); - T(result.rows[0].value == 9); + var summate = function(N) { + return (N + 1) * (N / 2); + }; + var result = db.view("test/summate"); + T(result.rows[0].value == summate(numDocs * 2)); - result = db.view("test/summate", {startkey:4,endkey:6}); - T(result.rows[0].value == 15); + result = db.view("test/summate", {startkey: 4, endkey: 4}); + T(result.rows[0].value == 4); - // Verify that a shared index (view def is an exact copy of "summate") - // does not confuse the reduce stage - result = db.view("test/summate2", {startkey:4,endkey:6}); - T(result.rows[0].value == 15); + result = db.view("test/summate", {startkey: 4, endkey: 5}); + T(result.rows[0].value == 9); - for(var i=1; i<numDocs/2; i+=30) { - result = db.view("test/summate", {startkey:i,endkey:numDocs-i}); - T(result.rows[0].value == summate(numDocs-i) - summate(i-1)); - } + result = db.view("test/summate", {startkey: 4, endkey: 6}); + T(result.rows[0].value == 15); - T(db.deleteDoc(designDoc).ok); - T(db.open(designDoc._id) == null); - T(db.view("test/no_docs") == null); + // test start_key and end_key aliases + result = db.view("test/summate", {start_key: 4, end_key: 6}); + T(result.rows[0].value == 15); - T(db.ensureFullCommit().ok); - restartServer(); - T(db.open(designDoc._id) == null); - T(db.view("test/no_docs") == null); + // Verify that a shared index (view def is an exact copy of "summate") + // does not confuse the reduce stage + result = db.view("test/summate2", {startkey: 4, endkey: 6}); + T(result.rows[0].value == 15); - // trigger ddoc cleanup - T(db.viewCleanup().ok); + for(i = 1; i < (numDocs / 2); i += 30) { + result = db.view("test/summate", {startkey: i, endkey: (numDocs - i)}); + T(result.rows[0].value == summate(numDocs - i) - summate(i - 1)); + } + + T(db.deleteDoc(designDoc).ok); + T(db.open(designDoc._id) == null); + T(db.view("test/no_docs") == null); + + T(db.ensureFullCommit().ok); + restartServer(); + T(db.open(designDoc._id) == null); + T(db.view("test/no_docs") == null); -}); + // trigger ddoc cleanup + T(db.viewCleanup().ok); + }; // enf of testFun + + run_on_modified_server(server_config, testFun); + + // cleanup + db.deleteDb(); + db2.deleteDb(); }; diff --git a/share/www/script/test/erlang_views.js b/share/www/script/test/erlang_views.js index e9e0363f..7eddab40 100644 --- a/share/www/script/test/erlang_views.js +++ b/share/www/script/test/erlang_views.js @@ -29,8 +29,8 @@ couchTests.erlang_views = function(debug) { T(db.save(doc).ok); var mfun = 'fun({Doc}) -> ' + - ' K = proplists:get_value(<<"integer">>, Doc, null), ' + - ' V = proplists:get_value(<<"string">>, Doc, null), ' + + ' K = couch_util:get_value(<<"integer">>, Doc, null), ' + + ' V = couch_util:get_value(<<"string">>, Doc, null), ' + ' Emit(K, V) ' + 'end.'; @@ -44,7 +44,7 @@ couchTests.erlang_views = function(debug) { // check simple reduction - another doc with same key. var doc = {_id: "2", integer: 1, string: "str2"}; T(db.save(doc).ok); - rfun = "fun(Keys, Values, ReReduce) -> length(Values) end." + rfun = "fun(Keys, Values, ReReduce) -> length(Values) end."; results = db.query(mfun, rfun, null, null, "erlang"); T(results.rows[0].value == 2); @@ -55,9 +55,9 @@ couchTests.erlang_views = function(debug) { shows: { simple: 'fun(Doc, {Req}) -> ' + - ' {Info} = proplists:get_value(<<"info">>, Req, {[]}), ' + - ' Purged = proplists:get_value(<<"purge_seq">>, Info, -1), ' + - ' Verb = proplists:get_value(<<"method">>, Req, <<"not_get">>), ' + + ' {Info} = couch_util:get_value(<<"info">>, Req, {[]}), ' + + ' Purged = couch_util:get_value(<<"purge_seq">>, Info, -1), ' + + ' Verb = couch_util:get_value(<<"method">>, Req, <<"not_get">>), ' + ' R = list_to_binary(io_lib:format("~b - ~s", [Purged, Verb])), ' + ' {[{<<"code">>, 200}, {<<"headers">>, {[]}}, {<<"body">>, R}]} ' + 'end.' @@ -67,7 +67,7 @@ couchTests.erlang_views = function(debug) { 'fun(Head, {Req}) -> ' + ' Send(<<"head">>), ' + ' Fun = fun({Row}, _) -> ' + - ' Val = proplists:get_value(<<"value">>, Row, -1), ' + + ' Val = couch_util:get_value(<<"value">>, Row, -1), ' + ' Send(list_to_binary(integer_to_list(Val))), ' + ' {ok, nil} ' + ' end, ' + @@ -117,10 +117,10 @@ couchTests.erlang_views = function(debug) { T(db.bulkSave(docs).length, 250, "Saved big doc set."); var mfun = 'fun({Doc}) -> ' + - 'Words = proplists:get_value(<<"words">>, Doc), ' + + 'Words = couch_util:get_value(<<"words">>, Doc), ' + 'lists:foreach(fun({Word}) -> ' + - 'WordString = proplists:get_value(<<"word">>, Word), ' + - 'Count = proplists:get_value(<<"count">>, Word), ' + + 'WordString = couch_util:get_value(<<"word">>, Word), ' + + 'Count = couch_util:get_value(<<"count">>, Word), ' + 'Emit(WordString , Count) ' + 'end, Words) ' + 'end.'; diff --git a/share/www/script/test/etags_views.js b/share/www/script/test/etags_views.js index a12734f8..f556d6ac 100644 --- a/share/www/script/test/etags_views.js +++ b/share/www/script/test/etags_views.js @@ -11,23 +11,34 @@ // the License. couchTests.etags_views = function(debug) { - var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"}); db.deleteDb(); db.createDb(); if (debug) debugger; var designDoc = { - _id:"_design/etags", + _id: "_design/etags", language: "javascript", views : { + fooView: { + map: stringFun(function(doc) { + if (doc.foo) { + emit("bar", 1); + } + }), + }, basicView : { map : stringFun(function(doc) { - emit(doc.integer, doc.string); + if(doc.integer && doc.string) { + emit(doc.integer, doc.string); + } }) }, withReduce : { map : stringFun(function(doc) { - emit(doc.integer, doc.string); + if(doc.integer && doc.string) { + emit(doc.integer, doc.string); + } }), reduce : stringFun(function(keys, values, rereduce) { if (rereduce) { @@ -38,11 +49,11 @@ couchTests.etags_views = function(debug) { }) } } - } + }; T(db.save(designDoc).ok); + db.bulkSave(makeDocs(0, 10)); + var xhr; - var docs = makeDocs(0, 10); - db.bulkSave(docs); // verify get w/Etag on map view xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); @@ -52,17 +63,92 @@ couchTests.etags_views = function(debug) { headers: {"if-none-match": etag} }); T(xhr.status == 304); - // TODO GET with keys (when that is available) + + // verify ETag doesn't change when an update + // doesn't change the view group's index + T(db.save({"_id":"doc1", "foo":"bar"}).ok); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + + // Verify that purges affect etags + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var foo_etag = xhr.getResponseHeader("etag"); + var doc1 = db.open("doc1"); + xhr = CouchDB.request("POST", "/test_suite_db/_purge", { + body: JSON.stringify({"doc1":[doc1._rev]}) + }); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 != foo_etag); + + // Test that _purge didn't affect the other view etags. + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + + // verify different views in the same view group may have different ETags + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var etag1 = xhr.getResponseHeader("etag"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 != etag2); + + // verify ETag changes when an update changes the view group's index. + db.bulkSave(makeDocs(10, 20)); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 != etag); + + // verify ETag is the same after a restart + restartServer(); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/basicView"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 == etag2); // reduce view xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); T(xhr.status == 200); var etag = xhr.getResponseHeader("etag"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce", { + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce",{ headers: {"if-none-match": etag} }); T(xhr.status == 304); + // verify ETag doesn't change when an update + // doesn't change the view group's index + T(db.save({"_id":"doc3", "foo":"bar"}).ok); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + // purge + var doc3 = db.open("doc3"); + xhr = CouchDB.request("POST", "/test_suite_db/_purge", { + body: JSON.stringify({"doc3":[doc3._rev]}) + }); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 == etag); + + // verify different views in the same view group may have different ETags + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/fooView"); + var etag1 = xhr.getResponseHeader("etag"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 != etag2); + + // verify ETag changes when an update changes the view group's index + db.bulkSave(makeDocs(20, 30)); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag1 = xhr.getResponseHeader("etag"); + T(etag1 != etag); + + // verify ETag is the same after a restart + restartServer(); + xhr = CouchDB.request("GET", "/test_suite_db/_design/etags/_view/withReduce"); + var etag2 = xhr.getResponseHeader("etag"); + T(etag1 == etag2); + // confirm ETag changes with different POST bodies xhr = CouchDB.request("POST", "/test_suite_db/_design/etags/_view/basicView", {body: JSON.stringify({keys:[1]})} diff --git a/share/www/script/test/http.js b/share/www/script/test/http.js index 8a2e09b8..5f46af52 100644 --- a/share/www/script/test/http.js +++ b/share/www/script/test/http.js @@ -25,7 +25,7 @@ couchTests.http = function(debug) { var xhr = CouchDB.request("PUT", "/test_suite_db/test", {body: "{}"}); var host = CouchDB.host; - TEquals("http://" + host + "/test_suite_db/test", + TEquals(CouchDB.protocol + host + "/test_suite_db/test", xhr.getResponseHeader("Location"), "should include ip address"); @@ -34,7 +34,7 @@ couchTests.http = function(debug) { headers: {"X-Forwarded-Host": "mysite.com"} }); - TEquals("http://mysite.com/test_suite_db/test2", + TEquals(CouchDB.protocol + "mysite.com/test_suite_db/test2", xhr.getResponseHeader("Location"), "should include X-Forwarded-Host"); @@ -47,7 +47,7 @@ couchTests.http = function(debug) { body: "{}", headers: {"X-Host": "mysite2.com"} }); - TEquals("http://mysite2.com/test_suite_db/test3", + TEquals(CouchDB.protocol + "mysite2.com/test_suite_db/test3", xhr.getResponseHeader("Location"), "should include X-Host"); }); diff --git a/share/www/script/test/jsonp.js b/share/www/script/test/jsonp.js index dfd6a0df..9aba7189 100644 --- a/share/www/script/test/jsonp.js +++ b/share/www/script/test/jsonp.js @@ -32,38 +32,51 @@ couchTests.jsonp = function(debug) { db.deleteDb(); db.createDb(); if (debug) debugger; - + var doc = {_id:"0",a:0,b:0}; T(db.save(doc).ok); + + // callback param is ignored unless jsonp is configured + var xhr = CouchDB.request("GET", "/test_suite_db/0?callback=jsonp_not_configured"); + JSON.parse(xhr.responseText); - // Test unchunked callbacks. - var xhr = CouchDB.request("GET", "/test_suite_db/0?callback=jsonp_no_chunk"); - T(xhr.status == 200); - jsonp_flag = 0; - eval(xhr.responseText); - T(jsonp_flag == 1); - xhr = CouchDB.request("GET", "/test_suite_db/0?callback=foo\""); - T(xhr.status == 400); + run_on_modified_server( + [{section: "httpd", + key: "allow_jsonp", + value: "true"}], + function() { - // Test chunked responses - var doc = {_id:"1",a:1,b:1}; - T(db.save(doc).ok); + // Test unchunked callbacks. + var xhr = CouchDB.request("GET", "/test_suite_db/0?callback=jsonp_no_chunk"); + T(xhr.status == 200); + jsonp_flag = 0; + eval(xhr.responseText); + T(jsonp_flag == 1); + xhr = CouchDB.request("GET", "/test_suite_db/0?callback=foo\""); + T(xhr.status == 400); + + // Test chunked responses + var doc = {_id:"1",a:1,b:1}; + T(db.save(doc).ok); + + var designDoc = { + _id:"_design/test", + language: "javascript", + views: { + all_docs: {map: "function(doc) {if(doc.a) emit(null, doc.a);}"} + } + }; + T(db.save(designDoc).ok); + + var url = "/test_suite_db/_design/test/_view/all_docs?callback=jsonp_chunk"; + xhr = CouchDB.request("GET", url); + T(xhr.status == 200); + jsonp_flag = 0; + eval(xhr.responseText); + T(jsonp_flag == 1); + xhr = CouchDB.request("GET", url + "\'"); + T(xhr.status == 400); + }); - var designDoc = { - _id:"_design/test", - language: "javascript", - views: { - all_docs: {map: "function(doc) {if(doc.a) emit(null, doc.a);}"} - } - } - T(db.save(designDoc).ok); - var url = "/test_suite_db/_design/test/_view/all_docs?callback=jsonp_chunk"; - xhr = CouchDB.request("GET", url); - T(xhr.status == 200); - jsonp_flag = 0; - eval(xhr.responseText); - T(jsonp_flag == 1); - xhr = CouchDB.request("GET", url + "\'"); - T(xhr.status == 400); }; diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index 54214c2c..e98a212e 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -156,6 +156,9 @@ couchTests.list_views = function(debug) { var row = getRow(); send(row.doc.integer); return "tail"; + }), + secObj: stringFun(function(head, req) { + return toJSON(req.secObj); }) } }; @@ -178,7 +181,7 @@ couchTests.list_views = function(debug) { 'fun(Head, {Req}) -> ' + ' Send(<<"[">>), ' + ' Fun = fun({Row}, Sep) -> ' + - ' Val = proplists:get_value(<<"key">>, Row, 23), ' + + ' Val = couch_util:get_value(<<"key">>, Row, 23), ' + ' Send(list_to_binary(Sep ++ integer_to_list(Val))), ' + ' {ok, ","} ' + ' end, ' + @@ -201,6 +204,7 @@ couchTests.list_views = function(debug) { T(xhr.status == 200, "standard get should be 200"); T(/head0123456789tail/.test(xhr.responseText)); + // test that etags are available var etag = xhr.getResponseHeader("etag"); xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView", { @@ -220,10 +224,13 @@ couchTests.list_views = function(debug) { T(etag1 != etag2, "POST to map _list generates key-depdendent ETags"); // test the richness of the arguments - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView?update_seq=true"); T(xhr.status == 200, "standard get should be 200"); var resp = JSON.parse(xhr.responseText); - TEquals(resp.head, {total_rows:10, offset:0}); + TEquals(10, resp.head.total_rows); + TEquals(0, resp.head.offset); + TEquals(11, resp.head.update_seq); + T(resp.rows.length == 10); TEquals(resp.rows[0], {"id": "0","key": 0,"value": "0"}); @@ -320,6 +327,16 @@ couchTests.list_views = function(debug) { T(/FirstKey: 2/.test(xhr.responseText)); T(/LastKey: 7/.test(xhr.responseText)); + // multi-key fetch with GET + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView" + + "?keys=[2,4,5,7]"); + + T(xhr.status == 200, "multi key"); + T(!(/Key: 1 /.test(xhr.responseText))); + T(/Key: 2/.test(xhr.responseText)); + T(/FirstKey: 2/.test(xhr.responseText)); + T(/LastKey: 7/.test(xhr.responseText)); + // no multi-key fetch allowed when group=false xhr = CouchDB.request("POST", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?group=false", { body: '{"keys":[2,4,5,7]}' @@ -343,13 +360,21 @@ couchTests.list_views = function(debug) { // T(xhr.getResponseHeader("Content-Type") == "text/plain"); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "basic stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/basicView", { + headers : { + "Accept" : "text/html" + } + }); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "stop 2"); // aborting iteration with reduce var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter/withReduce?group=true"); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop"); - xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/stopIter2/withReduce?group=true", { + headers : { + "Accept" : "text/html" + } + }); T(xhr.responseText.match(/^head 0 1 2 tail$/) && "reduce stop 2"); // with accept headers for HTML @@ -383,7 +408,7 @@ couchTests.list_views = function(debug) { T(/LastKey: 0/.test(xhr.responseText)); // Test we do multi-key requests on lists and views in separate docs. - var url = "/test_suite_db/_design/lists/_list/simpleForm/views/basicView" + var url = "/test_suite_db/_design/lists/_list/simpleForm/views/basicView"; xhr = CouchDB.request("POST", url, { body: '{"keys":[-2,-4,-5,-7]}' }); @@ -394,6 +419,12 @@ couchTests.list_views = function(debug) { T(/FirstKey: -2/.test(xhr.responseText)); T(/LastKey: -7/.test(xhr.responseText)); + // Test if secObj is available + var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/secObj/basicView"); + T(xhr.status == 200, "standard get should be 200"); + var resp = JSON.parse(xhr.responseText); + T(typeof(resp) == "object"); + var erlViewTest = function() { T(db.save(erlListDoc).ok); var url = "/test_suite_db/_design/erlang/_list/simple/views/basicView" + @@ -408,6 +439,8 @@ couchTests.list_views = function(debug) { } }; + + run_on_modified_server([{ section: "native_query_servers", key: "erlang", diff --git a/share/www/script/test/method_override.js b/share/www/script/test/method_override.js new file mode 100644 index 00000000..0bb4c61f --- /dev/null +++ b/share/www/script/test/method_override.js @@ -0,0 +1,40 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header +couchTests.method_override = function(debug) { + var result = JSON.parse(CouchDB.request("GET", "/").responseText); + T(result.couchdb == "Welcome"); + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + + db.createDb(); + + var doc = {bob : "connie"}; + xhr = CouchDB.request("POST", "/test_suite_db/fnord", {body: JSON.stringify(doc), headers:{"X-HTTP-Method-Override" : "PUT"}}); + T(xhr.status == 201); + + doc = db.open("fnord"); + T(doc.bob == "connie"); + + xhr = CouchDB.request("POST", "/test_suite_db/fnord?rev=" + doc._rev, {headers:{"X-HTTP-Method-Override" : "DELETE"}}); + T(xhr.status == 200); + + xhr = CouchDB.request("GET", "/test_suite_db/fnord2", {body: JSON.stringify(doc), headers:{"X-HTTP-Method-Override" : "PUT"}}); + // Method Override is ignored when original Method isn't POST + T(xhr.status == 404); + + doc = db.open("fnord"); + T(doc == null); + +}; diff --git a/share/www/script/test/oauth.js b/share/www/script/test/oauth.js index d55d13e8..82ebe8a4 100644 --- a/share/www/script/test/oauth.js +++ b/share/www/script/test/oauth.js @@ -71,7 +71,7 @@ couchTests.oauth = function(debug) { var host = CouchDB.host; var dbPair = { source: { - url: "http://" + host + "/test_suite_db_a", + url: CouchDB.protocol + host + "/test_suite_db_a", auth: { oauth: { consumer_key: "key", @@ -82,7 +82,7 @@ couchTests.oauth = function(debug) { } }, target: { - url: "http://" + host + "/test_suite_db_b", + url: CouchDB.protocol + host + "/test_suite_db_b", headers: {"Authorization": adminBasicAuthHeaderValue()} } }; @@ -90,16 +90,26 @@ couchTests.oauth = function(debug) { // this function will be called on the modified server var testFun = function () { try { - CouchDB.request("PUT", "http://" + host + "/_config/admins/testadmin", { + CouchDB.request("PUT", CouchDB.protocol + host + "/_config/admins/testadmin", { headers: {"X-Couch-Persist": "false"}, body: JSON.stringify(testadminPassword) }); - - CouchDB.request("GET", "/_sleep?time=50"); + var i = 0; + waitForSuccess(function() { + //loop until the couch server has processed the password + i += 1; + var xhr = CouchDB.request("GET", CouchDB.protocol + host + "/_config/admins/testadmin?foo="+i,{ + headers: { + "Authorization": adminBasicAuthHeaderValue() + }}); + if (xhr.responseText.indexOf("\"-hashed-") != 0) { + throw("still waiting"); + } + }, "wait-for-admin"); CouchDB.newUuids(2); // so we have one to make the salt - CouchDB.request("PUT", "http://" + host + "/_config/couch_httpd_auth/require_valid_user", { + CouchDB.request("PUT", CouchDB.protocol + host + "/_config/couch_httpd_auth/require_valid_user", { headers: { "X-Couch-Persist": "false", "Authorization": adminBasicAuthHeaderValue() @@ -116,7 +126,7 @@ couchTests.oauth = function(debug) { // Create a user var jasonUserDoc = CouchDB.prepareUserDoc({ - username: "jason", + name: "jason", roles: ["test"] }, "testpassword"); T(usersDb.save(jasonUserDoc).ok); @@ -147,11 +157,11 @@ couchTests.oauth = function(debug) { }; // Get request token via Authorization header - xhr = oauthRequest("GET", "http://" + host + "/_oauth/request_token", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_oauth/request_token", message, accessor); T(xhr.status == expectedCode); // GET request token via query parameters - xhr = oauthRequest("GET", "http://" + host + "/_oauth/request_token", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_oauth/request_token", message, accessor); T(xhr.status == expectedCode); responseMessage = OAuth.decodeForm(xhr.responseText); @@ -161,7 +171,7 @@ couchTests.oauth = function(debug) { //xhr = CouchDB.request("GET", authorization_url + '?oauth_token=' + responseMessage.oauth_token); //T(xhr.status == expectedCode); - xhr = oauthRequest("GET", "http://" + host + "/_session", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session", message, accessor); T(xhr.status == expectedCode); if (xhr.status == expectedCode == 200) { data = JSON.parse(xhr.responseText); @@ -169,11 +179,11 @@ couchTests.oauth = function(debug) { T(data.roles[0] == "test"); } - xhr = oauthRequest("GET", "http://" + host + "/_session?foo=bar", message, accessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session?foo=bar", message, accessor); T(xhr.status == expectedCode); // Test HEAD method - xhr = oauthRequest("HEAD", "http://" + host + "/_session?foo=bar", message, accessor); + xhr = oauthRequest("HEAD", CouchDB.protocol + host + "/_session?foo=bar", message, accessor); T(xhr.status == expectedCode); // Replication @@ -197,7 +207,7 @@ couchTests.oauth = function(debug) { oauth_version: "1.0" } }; - xhr = oauthRequest("GET", "http://" + host + "/_session?foo=bar", message, adminAccessor); + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session?foo=bar", message, adminAccessor); if (xhr.status == expectedCode == 200) { data = JSON.parse(xhr.responseText); T(data.name == "testadmin"); @@ -206,13 +216,13 @@ couchTests.oauth = function(debug) { // Test when the user's token doesn't exist. message.parameters.oauth_token = "not a token!"; - xhr = oauthRequest("GET", "http://" + host + "/_session?foo=bar", + xhr = oauthRequest("GET", CouchDB.protocol + host + "/_session?foo=bar", message, adminAccessor); T(xhr.status == 400, "Request should be invalid."); } } } finally { - var xhr = CouchDB.request("PUT", "http://" + host + "/_config/couch_httpd_auth/require_valid_user", { + var xhr = CouchDB.request("PUT", CouchDB.protocol + host + "/_config/couch_httpd_auth/require_valid_user", { headers: { "Authorization": adminBasicAuthHeaderValue(), "X-Couch-Persist": "false" @@ -221,7 +231,7 @@ couchTests.oauth = function(debug) { }); T(xhr.status == 200); - var xhr = CouchDB.request("DELETE", "http://" + host + "/_config/admins/testadmin", { + var xhr = CouchDB.request("DELETE", CouchDB.protocol + host + "/_config/admins/testadmin", { headers: { "Authorization": adminBasicAuthHeaderValue(), "X-Couch-Persist": "false" diff --git a/share/www/script/test/proxyauth.js b/share/www/script/test/proxyauth.js new file mode 100644 index 00000000..40af0089 --- /dev/null +++ b/share/www/script/test/proxyauth.js @@ -0,0 +1,130 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + + + +couchTests.proxyauth = function(debug) { + // this test proxy authentification handler + + var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"}); + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + + if (debug) debugger; + + // Simple secret key generator + function generateSecret(length) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var secret = ''; + for (var i=0; i<length; i++) { + secret += tab.charAt(Math.floor(Math.random() * 64)); + } + return secret; + } + + var secret = generateSecret(64); + + function TestFun() { + usersDb.deleteDb(); + usersDb.createDb(); + db.deleteDb(); + db.createDb(); + + var benoitcUserDoc = CouchDB.prepareUserDoc({ + name: "benoitc@apache.org" + }, "test"); + T(usersDb.save(benoitcUserDoc).ok); + + T(CouchDB.session().userCtx.name == null); + + // test that you can use basic auth aginst the users db + var s = CouchDB.session({ + headers : { + "Authorization" : "Basic YmVub2l0Y0BhcGFjaGUub3JnOnRlc3Q=" + } + }); + T(s.userCtx.name == "benoitc@apache.org"); + T(s.info.authenticated == "default"); + + CouchDB.logout(); + + var headers = { + "X-Auth-CouchDB-UserName": "benoitc@apache.org", + "X-Auth-CouchDB-Roles": "test", + "X-Auth-CouchDB-Token": hex_hmac_sha1(secret, "benoitc@apache.org") + }; + + var designDoc = { + _id:"_design/test", + language: "javascript", + + shows: { + "welcome": stringFun(function(doc,req) { + return "Welcome " + req.userCtx["name"]; + }), + "role": stringFun(function(doc, req) { + return req.userCtx['roles'][0]; + }) + } + }; + + db.save(designDoc); + + var req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/welcome", + {headers: headers}); + T(req.responseText == "Welcome benoitc@apache.org"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/role", + {headers: headers}); + T(req.responseText == "test"); + + var xhr = CouchDB.request("PUT", "/_config/couch_httpd_auth/proxy_use_secret",{ + body : JSON.stringify("true"), + headers: {"X-Couch-Persist": "false"} + }); + T(xhr.status == 200); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/welcome", + {headers: headers}); + T(req.responseText == "Welcome benoitc@apache.org"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_show/role", + {headers: headers}); + T(req.responseText == "test"); + + } + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value:"{couch_httpd_auth, proxy_authentification_handler}, {couch_httpd_auth, default_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "authentication_db", + value: "test_suite_users"}, + {section: "couch_httpd_auth", + key: "secret", + value: secret}, + {section: "couch_httpd_auth", + key: "x_auth_username", + value: "X-Auth-CouchDB-UserName"}, + {section: "couch_httpd_auth", + key: "x_auth_roles", + value: "X-Auth-CouchDB-Roles"}, + {section: "couch_httpd_auth", + key: "x_auth_token", + value: "X-Auth-CouchDB-Token"}, + {section: "couch_httpd_auth", + key: "proxy_use_secret", + value: "false"}], + TestFun + ); + +};
\ No newline at end of file diff --git a/share/www/script/test/purge.js b/share/www/script/test/purge.js index ca68b363..af72ea4f 100644 --- a/share/www/script/test/purge.js +++ b/share/www/script/test/purge.js @@ -30,7 +30,7 @@ couchTests.purge = function(debug) { all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"}, single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"} } - } + }; T(db.save(designDoc).ok); @@ -50,17 +50,19 @@ couchTests.purge = function(debug) { // purge the documents var xhr = CouchDB.request("POST", "/test_suite_db/_purge", { - body: JSON.stringify({"1":[doc1._rev], "2":[doc2._rev]}), + body: JSON.stringify({"1":[doc1._rev], "2":[doc2._rev]}) }); T(xhr.status == 200); + var result = JSON.parse(xhr.responseText); var newInfo = db.info(); + // purging increments the update sequence T(info.update_seq+1 == newInfo.update_seq); // and it increments the purge_seq T(info.purge_seq+1 == newInfo.purge_seq); + T(result.purge_seq == newInfo.purge_seq); - var result = JSON.parse(xhr.responseText); T(result.purged["1"][0] == doc1._rev); T(result.purged["2"][0] == doc2._rev); @@ -81,16 +83,18 @@ couchTests.purge = function(debug) { var doc4 = db.open("4"); xhr = CouchDB.request("POST", "/test_suite_db/_purge", { - body: JSON.stringify({"3":[doc3._rev]}), + body: JSON.stringify({"3":[doc3._rev]}) }); T(xhr.status == 200); xhr = CouchDB.request("POST", "/test_suite_db/_purge", { - body: JSON.stringify({"4":[doc4._rev]}), + body: JSON.stringify({"4":[doc4._rev]}) }); T(xhr.status == 200); + result = JSON.parse(xhr.responseText); + T(result.purge_seq == db.info().purge_seq); var rows = db.view("test/all_docs_twice").rows; for (var i = 4; i < numDocs; i++) { diff --git a/share/www/script/test/reader_acl.js b/share/www/script/test/reader_acl.js new file mode 100644 index 00000000..cc249ea4 --- /dev/null +++ b/share/www/script/test/reader_acl.js @@ -0,0 +1,198 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.reader_acl = function(debug) { + // this tests read access control + + var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"}); + var secretDb = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + function testFun() { + try { + usersDb.deleteDb(); + usersDb.createDb(); + secretDb.deleteDb(); + secretDb.createDb(); + + // create a user with top-secret-clearance + var jchrisUserDoc = CouchDB.prepareUserDoc({ + name: "jchris@apache.org", + roles : ["top-secret"] + }, "funnybone"); + T(usersDb.save(jchrisUserDoc).ok); + usersDb.ensureFullCommit(); + + T(CouchDB.session().userCtx.name == null); + + // set secret db to be read controlled + T(secretDb.save({_id:"baz",foo:"bar"}).ok); + T(secretDb.open("baz").foo == "bar"); + + T(secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club"], + names : ["joe","barb"] + } + }).ok); + } finally { + CouchDB.logout(); + } + } + + // split into 2 funs so we can test restart behavior + function testFun2() { + try { + // can't read it as jchris b/c he's missing the needed role + T(CouchDB.login("jchris@apache.org", "funnybone").ok); + T(CouchDB.session().userCtx.name == "jchris@apache.org"); + + try { + secretDb.open("baz"); + T(false && "can't open a doc from a secret db") ; + } catch(e) { + T(true) + } + + CouchDB.logout(); + + // make anyone with the top-secret role an admin + // db admins are automatically readers + T(secretDb.setSecObj({ + "admins" : { + roles : ["top-secret"], + names : [] + }, + "readers" : { + roles : ["super-secret-club"], + names : ["joe","barb"] + } + }).ok); + + + T(CouchDB.login("jchris@apache.org", "funnybone").ok); + + // db admin can read + T(secretDb.open("baz").foo == "bar"); + + // and run temp views + TEquals(secretDb.query(function(doc) { + emit(null, null) + }).total_rows, 1); + + CouchDB.logout(); + T(CouchDB.session().userCtx.roles.indexOf("_admin") != -1); + + // admin now adds the top-secret role to the db's readers + // and removes db-admins + T(secretDb.setSecObj({ + "admins" : { + roles : [], + names : [] + }, + "readers" : { + roles : ["super-secret-club", "top-secret"], + names : ["joe","barb"] + } + }).ok); + + // server _admin can always read + T(secretDb.open("baz").foo == "bar"); + + // and run temp views + TEquals(secretDb.query(function(doc) { + emit(null, null) + }).total_rows, 1); + + T(secretDb.save({ + "_id" : "_design/foo", + views : { + bar : { + map : "function(doc){emit(null, null)}" + } + } + }).ok) + + // now top-secret users can read too + T(CouchDB.login("jchris@apache.org", "funnybone").ok); + T(CouchDB.session().userCtx.roles.indexOf("_admin") == -1); + T(secretDb.open("baz").foo == "bar"); + // readers can query stored views + T(secretDb.view("foo/bar").total_rows == 1); + + // readers can't do temp views + try { + var results = secretDb.query(function(doc) { + emit(null, null); + }); + T(false && "temp view should be admin only"); + } catch (e) { + T(true && "temp view is admin only"); + } + + + CouchDB.logout(); + + // can't set non string reader names or roles + try { + secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club", {"top-secret":"awesome"}], + names : ["joe","barb"] + } + }) + T(false && "only string roles"); + } catch (e) {} + + try { + secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club", {"top-secret":"awesome"}], + names : ["joe",22] + } + }); + T(false && "only string names"); + } catch (e) {} + + try { + secretDb.setSecObj({ + "readers" : { + roles : ["super-secret-club", {"top-secret":"awesome"}], + names : "joe" + } + }); + T(false && "only lists of names"); + } catch (e) {} + } finally { + CouchDB.logout(); + } + }; + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "authentication_db", value: "test_suite_users"}], + testFun + ); + + // security changes will always commit synchronously + restartServer(); + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "authentication_db", value: "test_suite_users"}], + testFun2 + ); +} diff --git a/share/www/script/test/recreate_doc.js b/share/www/script/test/recreate_doc.js index a6a64ac0..05843558 100644 --- a/share/www/script/test/recreate_doc.js +++ b/share/www/script/test/recreate_doc.js @@ -51,7 +51,7 @@ couchTests.recreate_doc = function(debug) { data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" } } - } + }; try { // same as before, but with binary db.save(binAttDoc); diff --git a/share/www/script/test/reduce.js b/share/www/script/test/reduce.js index 9c80fa7f..979a0292 100644 --- a/share/www/script/test/reduce.js +++ b/share/www/script/test/reduce.js @@ -15,14 +15,15 @@ couchTests.reduce = function(debug) { db.deleteDb(); db.createDb(); if (debug) debugger; - var numDocs = 500 + var numDocs = 500; var docs = makeDocs(1,numDocs + 1); db.bulkSave(docs); var summate = function(N) {return (N+1)*N/2;}; var map = function (doc) { emit(doc.integer, doc.integer); - emit(doc.integer, doc.integer)}; + emit(doc.integer, doc.integer); + }; var reduce = function (keys, values) { return sum(values); }; var result = db.query(map, reduce); T(result.rows[0].value == 2*summate(numDocs)); @@ -69,7 +70,7 @@ couchTests.reduce = function(debug) { T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11)); } - map = function (doc) {emit(doc.keys, 1)}; + map = function (doc) { emit(doc.keys, 1); }; reduce = function (keys, values) { return sum(values); }; var results = db.query(map, reduce, {group:true}); @@ -107,7 +108,7 @@ couchTests.reduce = function(debug) { db.createDb(); - var map = function (doc) {emit(doc.val, doc.val)}; + var map = function (doc) { emit(doc.val, doc.val); }; var reduceCombine = function (keys, values, rereduce) { // This computes the standard deviation of the mapped results var stdDeviation=0.0; diff --git a/share/www/script/test/reduce_builtin.js b/share/www/script/test/reduce_builtin.js index 7938a0cf..b3cc3cc7 100644 --- a/share/www/script/test/reduce_builtin.js +++ b/share/www/script/test/reduce_builtin.js @@ -16,22 +16,37 @@ couchTests.reduce_builtin = function(debug) { db.createDb(); if (debug) debugger; - var numDocs = 500 + var numDocs = 500; var docs = makeDocs(1,numDocs + 1); db.bulkSave(docs); var summate = function(N) {return (N+1)*N/2;}; + var sumsqr = function(N) { + var acc = 0; + for (var i=1; i<=N; ++i) { + acc += i*i; + } + return acc; + }; + // this is the same test as the reduce.js test // only we'll let CouchDB run reduce in Erlang var map = function (doc) { emit(doc.integer, doc.integer); - emit(doc.integer, doc.integer)}; + emit(doc.integer, doc.integer); + }; var result = db.query(map, "_sum"); T(result.rows[0].value == 2*summate(numDocs)); result = db.query(map, "_count"); T(result.rows[0].value == 1000); + result = db.query(map, "_stats"); + T(result.rows[0].value.sum == 2*summate(numDocs)); + T(result.rows[0].value.count == 1000); + T(result.rows[0].value.min == 1); + T(result.rows[0].value.max == 500); + T(result.rows[0].value.sumsqr == 2*sumsqr(numDocs)); result = db.query(map, "_sum", {startkey: 4, endkey: 4}); T(result.rows[0].value == 8); @@ -58,6 +73,26 @@ couchTests.reduce_builtin = function(debug) { T(result.rows[0].value == 2*(summate(numDocs-i) - summate(i-1))); } + // test for trailing characters after builtin functions, desired behaviour + // is to disregard any trailing characters + // I think the behavior should be a prefix test, so that even "_statsorama" + // or "_stats\nare\awesome" should work just as "_stats" does. - JChris + + var trailing = ["\u000a", "orama", "\nare\nawesome", " ", " \n "]; + + for(var i=0; i < trailing.length; i++) { + result = db.query(map, "_sum" + trailing[i]); + T(result.rows[0].value == 2*summate(numDocs)); + result = db.query(map, "_count" + trailing[i]); + T(result.rows[0].value == 1000); + result = db.query(map, "_stats" + trailing[i]); + T(result.rows[0].value.sum == 2*summate(numDocs)); + T(result.rows[0].value.count == 1000); + T(result.rows[0].value.min == 1); + T(result.rows[0].value.max == 500); + T(result.rows[0].value.sumsqr == 2*sumsqr(numDocs)); + } + db.deleteDb(); db.createDb(); @@ -81,7 +116,7 @@ couchTests.reduce_builtin = function(debug) { T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11)); } - map = function (doc) {emit(doc.keys, 1)}; + map = function (doc) { emit(doc.keys, 1); }; // with emitted values being 1, count should be the same as sum var builtins = ["_sum", "_count"]; @@ -115,5 +150,30 @@ couchTests.reduce_builtin = function(debug) { T(equals(results.rows[5], {key:["d","b"],value:10*i})); T(equals(results.rows[6], {key:["d","c"],value:10*i})); }; + + map = function (doc) { emit(doc.keys, [1, 1]); }; + + var results = db.query(map, "_sum", {group:true}); + T(equals(results.rows[0], {key:["a"],value:[20*i,20*i]})); + T(equals(results.rows[1], {key:["a","b"],value:[20*i,20*i]})); + T(equals(results.rows[2], {key:["a", "b", "c"],value:[10*i,10*i]})); + T(equals(results.rows[3], {key:["a", "b", "d"],value:[10*i,10*i]})); + + var results = db.query(map, "_sum", {group: true, limit: 2}); + T(equals(results.rows[0], {key: ["a"], value: [20*i,20*i]})); + T(equals(results.rows.length, 2)); + + var results = db.query(map, "_sum", {group_level:1}); + T(equals(results.rows[0], {key:["a"],value:[70*i,70*i]})); + T(equals(results.rows[1], {key:["d"],value:[40*i,40*i]})); + + var results = db.query(map, "_sum", {group_level:2}); + T(equals(results.rows[0], {key:["a"],value:[20*i,20*i]})); + T(equals(results.rows[1], {key:["a","b"],value:[40*i,40*i]})); + T(equals(results.rows[2], {key:["a","c"],value:[10*i,10*i]})); + T(equals(results.rows[3], {key:["d"],value:[10*i,10*i]})); + T(equals(results.rows[4], {key:["d","a"],value:[10*i,10*i]})); + T(equals(results.rows[5], {key:["d","b"],value:[10*i,10*i]})); + T(equals(results.rows[6], {key:["d","c"],value:[10*i,10*i]})); } } diff --git a/share/www/script/test/replication.js b/share/www/script/test/replication.js index 78678937..5acff4ab 100644 --- a/share/www/script/test/replication.js +++ b/share/www/script/test/replication.js @@ -17,19 +17,19 @@ couchTests.replication = function(debug) { {source:"test_suite_db_a", target:"test_suite_db_b"}, {source:"test_suite_db_a", - target:"http://" + host + "/test_suite_db_b"}, - {source:"http://" + host + "/test_suite_db_a", + target:CouchDB.protocol + host + "/test_suite_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_db_a", target:"test_suite_db_b"}, - {source:"http://" + host + "/test_suite_db_a", - target:"http://" + host + "/test_suite_db_b"} - ] + {source:CouchDB.protocol + host + "/test_suite_db_a", + target:CouchDB.protocol + host + "/test_suite_db_b"} + ]; var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"}); var numDocs = 10; var xhr; for (var testPair = 0; testPair < dbPairs.length; testPair++) { - var A = dbPairs[testPair].source - var B = dbPairs[testPair].target + var A = dbPairs[testPair].source; + var B = dbPairs[testPair].target; dbA.deleteDb(); dbA.createDb(); @@ -41,7 +41,7 @@ couchTests.replication = function(debug) { test_template: new function () { this.init = function(dbA, dbB) { // before anything has happened - } + }; this.afterAB1 = function(dbA, dbB) { // called after replicating src=A tgt=B first time. }; @@ -165,20 +165,20 @@ couchTests.replication = function(debug) { this.afterAB1 = function(dbA, dbB) { var xhr = CouchDB.request("GET", "/test_suite_db_a/bin_doc/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); xhr = CouchDB.request("GET", "/test_suite_db_b/bin_doc/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); // and the design-doc xhr = CouchDB.request("GET", "/test_suite_db_a/_design/with_bin/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); xhr = CouchDB.request("GET", "/test_suite_db_b/_design/with_bin/foo%2Bbar.txt"); - T(xhr.responseText == "This is a base64 encoded text") + T(xhr.responseText == "This is a base64 encoded text"); }; }, @@ -209,8 +209,8 @@ couchTests.replication = function(debug) { var docB = dbB.open("foo", {conflicts: true, deleted_conflicts: true}); // We should have no conflicts this time - T(docA._conflicts === undefined) - T(docB._conflicts === undefined); + T(typeof docA._conflicts === "undefined"); + T(typeof docB._conflicts === "undefined"); // They show up as deleted conflicts instead T(docA._deleted_conflicts[0] == docB._deleted_conflicts[0]); @@ -229,7 +229,7 @@ couchTests.replication = function(debug) { var seqA = result.source_last_seq; T(0 == result.history[0].start_last_seq); - T(result.history[1] === undefined) + T(typeof result.history[1] === "undefined"); for(test in repTests) { if(repTests[test].afterAB1) repTests[test].afterAB1(dbA, dbB); @@ -239,7 +239,7 @@ couchTests.replication = function(debug) { var seqB = result.source_last_seq; T(0 == result.history[0].start_last_seq); - T(result.history[1] === undefined) + T(typeof result.history[1] === "undefined"); for(test in repTests) { if(repTests[test].afterBA1) repTests[test].afterBA1(dbA, dbB); @@ -252,7 +252,7 @@ couchTests.replication = function(debug) { T(seqA < result2.source_last_seq); T(seqA == result2.history[0].start_last_seq); - T(result2.history[1].end_last_seq == seqA) + T(result2.history[1].end_last_seq == seqA); seqA = result2.source_last_seq; @@ -260,11 +260,11 @@ couchTests.replication = function(debug) { if(repTests[test].afterAB2) repTests[test].afterAB2(dbA, dbB); } - result = CouchDB.replicate(B, A) + result = CouchDB.replicate(B, A); T(seqB < result.source_last_seq); T(seqB == result.history[0].start_last_seq); - T(result.history[1].end_last_seq == seqB) + T(result.history[1].end_last_seq == seqB); seqB = result.source_last_seq; @@ -296,9 +296,436 @@ couchTests.replication = function(debug) { // remote dbB.deleteDb(); - CouchDB.replicate(dbA.name, "http://" + CouchDB.host + "/test_suite_db_b", { + CouchDB.replicate(dbA.name, CouchDB.protocol + CouchDB.host + "/test_suite_db_b", { body: {"create_target": true} }); TEquals("test_suite_db_b", dbB.info().db_name, "Target database should exist"); + + // continuous + var continuousResult = CouchDB.replicate(dbA.name, "test_suite_db_b", { + body: {"continuous": true} + }); + T(continuousResult.ok); + T(continuousResult._local_id); + + var cancelResult = CouchDB.replicate(dbA.name, "test_suite_db_b", { + body: {"cancel": true} + }); + T(cancelResult.ok); + T(continuousResult._local_id == cancelResult._local_id); + + try { + var cancelResult2 = CouchDB.replicate(dbA.name, "test_suite_db_b", { + body: {"cancel": true} + }); + } catch (e) { + T(e.error == "not_found"); + } + // test replication object option doc_ids + + var dbA = new CouchDB("test_suite_rep_docs_db_a", {"X-Couch-Full-Commit":"false"}); + var dbB = new CouchDB("test_suite_rep_docs_db_b", {"X-Couch-Full-Commit":"false"}); + + dbA.deleteDb(); + dbA.createDb(); + + var all_docs = [ + { + _id: "foo1", + value: "a" + }, + { + _id: "foo2", + value: "b" + }, + { + _id: "foo3", + value: "c" + }, + { + _id: "slashed/foo", + value: "s" + }, + { + _id: "_design/foobar", + language: "javascript", + value: "I am a design doc", + filters: { + idfilter: (function(doc, req) { + return doc.value == Number(req.filter_value); + }).toString() + }, + views: { + countview: (function(doc) { + emit(doc.value, 1); + }).toString() + } + } + ]; + + for (var i = 0; i < all_docs.length; i++) { + T(dbA.save(all_docs[i]).ok); + } + + var dbPairs = [ + {source:"test_suite_rep_docs_db_a", + target:"test_suite_rep_docs_db_b"}, + {source:"test_suite_rep_docs_db_a", + target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a", + target:"test_suite_rep_docs_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a", + target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"} + ]; + + var target_doc_ids = [ + ["foo1", "foo3", "foo666"], + ["foo1", "foo666"], + ["foo666", "foo2"], + ["foo2", "foo9999", "foo1"], + ["foo3", "slashed/foo"], + ["foo3", "slashed%2Ffoo"], + ["foo1", "_design/foobar"], + ["foo1", "foo1001", "_design%2Ffoobar"] + ]; + + for (var i = 0; i < dbPairs.length; i++) { + var src_db = dbPairs[i].source; + var tgt_db = dbPairs[i].target; + + for (var j = 0; j < target_doc_ids.length; j++) { + var doc_ids = target_doc_ids[j]; + var valid_doc_ids = []; + var invalid_doc_ids = []; + + for (var p = 0; p < doc_ids.length; p++) { + var id = doc_ids[p]; + var found = false; + + for (var k = 0; k < all_docs.length; k++) { + var doc = all_docs[k]; + + if (id === doc._id) { + found = true; + break; + } + } + + if (found) { + valid_doc_ids.push(id); + } else { + invalid_doc_ids.push(id); + } + }; + + dbB.deleteDb(); + dbB.createDb(); + + var repResult = CouchDB.replicate(src_db, tgt_db, { + body: {"doc_ids": doc_ids} + }); + + T(repResult.ok); + T(repResult.docs_written === valid_doc_ids.length); + T(repResult.docs_read === valid_doc_ids.length); + T(repResult.doc_write_failures === 0); + + for (var k = 0; k < all_docs.length; k++) { + var doc = all_docs[k]; + var tgt_doc = dbB.open(doc._id); + + if (doc_ids.indexOf(doc._id) >= 0) { + T(tgt_doc !== null); + T(tgt_doc.value === doc.value); + } else { + T(tgt_doc === null); + } + } + + for (var k = 0; k < invalid_doc_ids.length; k++) { + var tgt_doc = dbB.open(invalid_doc_ids[k]); + + T(tgt_doc === null); + } + } + } + + // test filtered replication + var filterFun1 = (function(doc, req) { + if (doc.value < Number(req.query.maxvalue)) { + return true; + } else { + return false; + } + }).toString(); + + var filterFun2 = (function(doc, req) { + return true; + }).toString(); + + var dbPairs = [ + {source:"test_suite_filtered_rep_db_a", + target:"test_suite_filtered_rep_db_b"}, + {source:"test_suite_filtered_rep_db_a", + target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a", + target:"test_suite_filtered_rep_db_b"}, + {source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a", + target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"} + ]; + var sourceDb = new CouchDB("test_suite_filtered_rep_db_a"); + var targetDb = new CouchDB("test_suite_filtered_rep_db_b"); + + for (var i = 0; i < dbPairs.length; i++) { + sourceDb.deleteDb(); + sourceDb.createDb(); + + T(sourceDb.save({_id: "foo1", value: 1}).ok); + T(sourceDb.save({_id: "foo2", value: 2}).ok); + T(sourceDb.save({_id: "foo3", value: 3}).ok); + T(sourceDb.save({_id: "foo4", value: 4}).ok); + + var ddoc = { + "_id": "_design/mydesign", + "language": "javascript", + "filters": { + "myfilter": filterFun1 + } + }; + + T(sourceDb.save(ddoc).ok); + + targetDb.deleteDb(); + targetDb.createDb(); + + var dbA = dbPairs[i].source; + var dbB = dbPairs[i].target; + + var repResult = CouchDB.replicate(dbA, dbB, { + body: { + "filter" : "mydesign/myfilter", + "query_params" : { + "maxvalue": "3" + } + } + }); + + T(repResult.ok); + T(repResult.history instanceof Array); + T(repResult.history.length === 1); + T(repResult.history[0].docs_written === 2); + T(repResult.history[0].docs_read === 2); + T(repResult.history[0].doc_write_failures === 0); + + var docFoo1 = targetDb.open("foo1"); + T(docFoo1 !== null); + T(docFoo1.value === 1); + + var docFoo2 = targetDb.open("foo2"); + T(docFoo2 !== null); + T(docFoo2.value === 2); + + var docFoo3 = targetDb.open("foo3"); + T(docFoo3 === null); + + var docFoo4 = targetDb.open("foo4"); + T(docFoo4 === null); + + // replication should start from scratch after the filter's code changed + + ddoc.filters.myfilter = filterFun2; + T(sourceDb.save(ddoc).ok); + + repResult = CouchDB.replicate(dbA, dbB, { + body: { + "filter" : "mydesign/myfilter", + "query_params" : { + "maxvalue": "3" + } + } + }); + + T(repResult.ok); + T(repResult.history instanceof Array); + T(repResult.history.length === 1); + T(repResult.history[0].docs_written === 3); + T(repResult.history[0].docs_read === 3); + T(repResult.history[0].doc_write_failures === 0); + + docFoo1 = targetDb.open("foo1"); + T(docFoo1 !== null); + T(docFoo1.value === 1); + + docFoo2 = targetDb.open("foo2"); + T(docFoo2 !== null); + T(docFoo2.value === 2); + + docFoo3 = targetDb.open("foo3"); + T(docFoo3 !== null); + T(docFoo3.value === 3); + + docFoo4 = targetDb.open("foo4"); + T(docFoo4 !== null); + T(docFoo4.value === 4); + + T(targetDb.open("_design/mydesign") !== null); + } + + // test for COUCHDB-868 - design docs' attachments not getting replicated + // when doing a pull replication with HTTP basic auth + dbA = new CouchDB("test_suite_db_a"); + dbB = new CouchDB("test_suite_db_b"); + var usersDb = new CouchDB("test_suite_auth"); + var lorem = CouchDB.request( + "GET", "/_utils/script/test/lorem.txt").responseText; + var lorem_b64 = CouchDB.request( + "GET", "/_utils/script/test/lorem_b64.txt").responseText; + + usersDb.deleteDb(); + usersDb.createDb(); + dbA.deleteDb(); + dbA.createDb(); + dbB.deleteDb(); + dbB.createDb(); + + var atts_ddoc = { + _id: "_design/i_have_atts", + language: "javascript" + }; + T(dbA.save(atts_ddoc).ok); + + var rev = atts_ddoc._rev; + var att_1_name = "lorem.txt"; + var att_2_name = "lorem.dat"; + var xhr = CouchDB.request( + "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_1_name + "?rev=" + rev, { + headers: {"Content-Type": "text/plain;charset=utf-8"}, + body: lorem + }); + rev = JSON.parse(xhr.responseText).rev; + T(xhr.status === 201); + xhr = CouchDB.request( + "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_2_name + "?rev=" + rev, { + headers: {"Content-Type": "application/data"}, + body: lorem_b64 + }); + T(xhr.status === 201); + + var fdmananaUserDoc = CouchDB.prepareUserDoc({ + name: "fdmanana", + roles: ["reader"] + }, "qwerty"); + T(usersDb.save(fdmananaUserDoc).ok); + + T(dbA.setSecObj({ + admins: { + names: [], + roles: ["admin"] + }, + readers: { + names: [], + roles: ["reader"] + } + }).ok); + T(dbB.setSecObj({ + admins: { + names: ["fdmanana"], + roles: [] + } + }).ok); + + var server_config = [ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: usersDb.name + }, + // to prevent admin party mode + { + section: "admins", + key: "joe", + value: "erlang" + } + ]; + + var test_fun = function() { + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1); + + var repResult = CouchDB.replicate( + CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name, + dbB.name + ); + T(repResult.ok === true); + T(repResult.history instanceof Array); + T(repResult.history.length === 1); + T(repResult.history[0].docs_written === 1); + T(repResult.history[0].docs_read === 1); + T(repResult.history[0].doc_write_failures === 0); + + var atts_ddoc_copy = dbB.open(atts_ddoc._id); + T(atts_ddoc_copy !== null); + T(typeof atts_ddoc_copy._attachments === "object"); + T(atts_ddoc_copy._attachments !== null); + T(att_1_name in atts_ddoc_copy._attachments); + T(att_2_name in atts_ddoc_copy._attachments); + + var xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_1_name); + T(xhr.status === 200); + T(xhr.responseText === lorem); + + xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_2_name); + T(xhr.status === 200); + T(xhr.responseText === lorem_b64); + + CouchDB.logout(); + T(CouchDB.login("joe", "erlang").ok); + T(dbA.setSecObj({ + admins: { + names: [], + roles: ["bar"] + }, + readers: { + names: [], + roles: ["foo"] + } + }).ok); + T(dbB.deleteDb().ok === true); + T(dbB.createDb().ok === true); + T(dbB.setSecObj({ + admins: { + names: ["fdmanana"], + roles: [] + } + }).ok); + CouchDB.logout(); + + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1); + try { + repResult = CouchDB.replicate( + CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name, + dbB.name + ); + T(false, "replication should have failed"); + } catch(x) { + T(x.error === "unauthorized"); + } + + atts_ddoc_copy = dbB.open(atts_ddoc._id); + T(atts_ddoc_copy === null); + + CouchDB.logout(); + T(CouchDB.login("joe", "erlang").ok); + }; + + run_on_modified_server(server_config, test_fun); + + // cleanup + dbA.deleteDb(); + dbB.deleteDb(); + usersDb.deleteDb(); }; diff --git a/share/www/script/test/replicator_db.js b/share/www/script/test/replicator_db.js new file mode 100644 index 00000000..3c6a5d8e --- /dev/null +++ b/share/www/script/test/replicator_db.js @@ -0,0 +1,818 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.replicator_db = function(debug) { + + if (debug) debugger; + + var wait_rep_doc = 500; // number of millisecs to wait after saving a Rep Doc + var host = CouchDB.host; + var dbA = new CouchDB("test_suite_rep_db_a", {"X-Couch-Full-Commit":"false"}); + var dbB = new CouchDB("test_suite_rep_db_b", {"X-Couch-Full-Commit":"false"}); + var repDb = new CouchDB("test_suite_rep_db", {"X-Couch-Full-Commit":"false"}); + var usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"}); + + var docs1 = [ + { + _id: "foo1", + value: 11 + }, + { + _id: "foo2", + value: 22 + }, + { + _id: "foo3", + value: 33 + } + ]; + + function waitForRep(repDb, repDoc, state) { + var newRep, + t0 = new Date(), + t1, + ms = 1000; + + do { + newRep = repDb.open(repDoc._id); + t1 = new Date(); + } while (((t1 - t0) <= ms) && newRep._replication_state !== state); + } + + function waitForSeq(sourceDb, targetDb) { + var targetSeq, + sourceSeq = sourceDb.info().update_seq, + t0 = new Date(), + t1, + ms = 1000; + + do { + targetSeq = targetDb.info().update_seq; + t1 = new Date(); + } while (((t1 - t0) <= ms) && targetSeq < sourceSeq); + } + + function wait(ms) { + var t0 = new Date(), t1; + do { + CouchDB.request("GET", "/"); + t1 = new Date(); + } while ((t1 - t0) <= ms); + } + + + function populate_db(db, docs) { + db.deleteDb(); + db.createDb(); + for (var i = 0; i < docs.length; i++) { + var d = docs[i]; + delete d._rev; + T(db.save(d).ok); + } + } + + function simple_replication() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_simple_rep", + source: dbA.name, + target: dbB.name + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "completed", "simple"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + } + + + function filtered_replication() { + var docs2 = docs1.concat([ + { + _id: "_design/mydesign", + language : "javascript", + filters : { + myfilter : (function(doc, req) { + return (doc.value % 2) !== Number(req.query.myparam); + }).toString() + } + } + ]); + + populate_db(dbA, docs2); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_filt_rep_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + filter: "mydesign/myfilter", + query_params: { + myparam: 1 + } + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs2.length; i++) { + var doc = docs2[i]; + var copy = dbB.open(doc._id); + + if (typeof doc.value === "number") { + if ((doc.value % 2) !== 1) { + T(copy !== null); + T(copy.value === doc.value); + } else { + T(copy === null); + } + } + } + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "completed", "filtered"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + } + + + function continuous_replication() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_cont_rep_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + + T(repDb.save(repDoc).ok); + + waitForSeq(dbA, dbB); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + // add another doc to source, it will be replicated to target + var docX = { + _id: "foo1000", + value: 1001 + }; + + T(dbA.save(docX).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo1000"); + T(copy !== null); + T(copy.value === 1001); + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "triggered"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + + // add a design doc to source, it will be replicated to target + // when the "user_ctx" property is not defined in the replication doc, + // the replication will be done under an _admin context, therefore + // design docs will be replicated + var ddoc = { + _id: "_design/foobar", + language: "javascript" + }; + + T(dbA.save(ddoc).ok); + + waitForSeq(dbA, dbB); + var ddoc_copy = dbB.open("_design/foobar"); + T(ddoc_copy !== null); + T(ddoc.language === "javascript"); + + // update the design doc on source, test that the new revision is replicated + ddoc.language = "erlang"; + T(dbA.save(ddoc).ok); + T(ddoc._rev.indexOf("2-") === 0); + + waitForSeq(dbA, dbB); + ddoc_copy = dbB.open("_design/foobar"); + T(ddoc_copy !== null); + T(ddoc_copy._rev === ddoc._rev); + T(ddoc.language === "erlang"); + + // stop replication by deleting the replication document + T(repDb.deleteDoc(repDoc1).ok); + + // add another doc to source, it will NOT be replicated to target + var docY = { + _id: "foo666", + value: 999 + }; + + T(dbA.save(docY).ok); + + wait(200); // is there a way to avoid wait here? + var copy = dbB.open("foo666"); + T(copy === null); + } + + + function by_doc_ids_replication() { + // to test that we can replicate docs with slashes in their IDs + var docs2 = docs1.concat([ + { + _id: "_design/mydesign", + language : "javascript" + } + ]); + + populate_db(dbA, docs2); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_cont_rep_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + doc_ids: ["foo666", "foo3", "_design/mydesign", "foo999", "foo1"] + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + var copy = dbB.open("foo1"); + T(copy !== null); + T(copy.value === 11); + + copy = dbB.open("foo2"); + T(copy === null); + + copy = dbB.open("foo3"); + T(copy !== null); + T(copy.value === 33); + + copy = dbB.open("foo666"); + T(copy === null); + + copy = dbB.open("foo999"); + T(copy === null); + + copy = dbB.open("_design/mydesign"); + T(copy !== null); + T(copy.language === "javascript"); + } + + + function successive_identical_replications() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc1 = { + _id: "foo_ident_rep_1", + source: dbA.name, + target: dbB.name + }; + T(repDb.save(repDoc1).ok); + + waitForRep(repDb, repDoc1, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + var repDoc1_copy = repDb.open(repDoc1._id); + T(repDoc1_copy !== null); + T(repDoc1_copy.source === repDoc1.source); + T(repDoc1_copy.target === repDoc1.target); + T(repDoc1_copy._replication_state === "completed"); + T(typeof repDoc1_copy._replication_state_time === "number"); + T(typeof repDoc1_copy._replication_id === "string"); + + var newDoc = { + _id: "doc666", + value: 666 + }; + T(dbA.save(newDoc).ok); + + wait(200); + var newDoc_copy = dbB.open(newDoc._id); + // not replicated because first replication is complete (not continuous) + T(newDoc_copy === null); + + var repDoc2 = { + _id: "foo_ident_rep_2", + source: dbA.name, + target: dbB.name + }; + T(repDb.save(repDoc2).ok); + + waitForRep(repDb, repDoc2, "completed"); + var newDoc_copy = dbB.open(newDoc._id); + T(newDoc_copy !== null); + T(newDoc_copy.value === newDoc.value); + + var repDoc2_copy = repDb.open(repDoc2._id); + T(repDoc2_copy !== null); + T(repDoc2_copy.source === repDoc1.source); + T(repDoc2_copy.target === repDoc1.target); + T(repDoc2_copy._replication_state === "completed"); + T(typeof repDoc2_copy._replication_state_time === "number"); + T(typeof repDoc2_copy._replication_id === "string"); + T(repDoc2_copy._replication_id === repDoc1_copy._replication_id); + } + + + // test the case where multiple replication docs (different IDs) + // describe in fact the same replication (source, target, etc) + function identical_rep_docs() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc1 = { + _id: "foo_dup_rep_doc_1", + source: "http://" + host + "/" + dbA.name, + target: dbB.name + }; + var repDoc2 = { + _id: "foo_dup_rep_doc_2", + source: "http://" + host + "/" + dbA.name, + target: dbB.name + }; + + T(repDb.save(repDoc1).ok); + T(repDb.save(repDoc2).ok); + + waitForRep(repDb, repDoc1, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + repDoc1 = repDb.open("foo_dup_rep_doc_1"); + T(repDoc1 !== null); + T(repDoc1._replication_state === "completed", "identical"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + + repDoc2 = repDb.open("foo_dup_rep_doc_2"); + T(repDoc2 !== null); + T(typeof repDoc2._replication_state === "undefined"); + T(typeof repDoc2._replication_state_time === "undefined"); + T(repDoc2._replication_id === repDoc1._replication_id); + } + + + // test the case where multiple replication docs (different IDs) + // describe in fact the same continuous replication (source, target, etc) + function identical_continuous_rep_docs() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc1 = { + _id: "foo_dup_cont_rep_doc_1", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + var repDoc2 = { + _id: "foo_dup_cont_rep_doc_2", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + + T(repDb.save(repDoc1).ok); + T(repDb.save(repDoc2).ok); + + waitForSeq(dbA, dbB); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + repDoc1 = repDb.open("foo_dup_cont_rep_doc_1"); + T(repDoc1 !== null); + T(repDoc1._replication_state === "triggered"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + + repDoc2 = repDb.open("foo_dup_cont_rep_doc_2"); + T(repDoc2 !== null); + T(typeof repDoc2._replication_state === "undefined"); + T(typeof repDoc2._replication_state_time === "undefined"); + T(repDoc2._replication_id === repDoc1._replication_id); + + var newDoc = { + _id: "foo666", + value: 999 + }; + T(dbA.save(newDoc).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo666"); + T(copy !== null); + T(copy.value === 999); + + // deleting second replication doc, doesn't affect the 1st one and + // neither it stops the replication + T(repDb.deleteDoc(repDoc2).ok); + repDoc1 = repDb.open("foo_dup_cont_rep_doc_1"); + T(repDoc1 !== null); + T(repDoc1._replication_state === "triggered"); + T(typeof repDoc1._replication_state_time === "number"); + + var newDoc2 = { + _id: "foo5000", + value: 5000 + }; + T(dbA.save(newDoc2).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo5000"); + T(copy !== null); + T(copy.value === 5000); + + // deleting the 1st replication document stops the replication + T(repDb.deleteDoc(repDoc1).ok); + var newDoc3 = { + _id: "foo1983", + value: 1983 + }; + T(dbA.save(newDoc3).ok); + + wait(wait_rep_doc); //how to remove wait? + var copy = dbB.open("foo1983"); + T(copy === null); + } + + + function test_replication_credentials_delegation() { + populate_db(usersDb, []); + + var joeUserDoc = CouchDB.prepareUserDoc({ + name: "joe", + roles: ["god", "erlanger"] + }, "erly"); + T(usersDb.save(joeUserDoc).ok); + + var ddoc = { + _id: "_design/beer", + language: "javascript" + }; + populate_db(dbA, docs1.concat([ddoc])); + populate_db(dbB, []); + + T(dbB.setSecObj({ + admins: { + names: [], + roles: ["god"] + } + }).ok); + + var server_admins_config = [ + { + section: "admins", + key: "fdmanana", + value: "qwerty" + } + ]; + + run_on_modified_server(server_admins_config, function() { + + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") !== -1); + + var repDoc = { + _id: "foo_rep_del_doc_1", + source: dbA.name, + target: dbB.name, + user_ctx: { + name: "joe", + roles: ["erlanger"] + } + }; + + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + // design doc was not replicated, because joe is not an admin of db B + var doc = dbB.open(ddoc._id); + T(doc === null); + + // now test the same replication but putting the role "god" in the + // delegation user context property + var repDoc2 = { + _id: "foo_rep_del_doc_2", + source: dbA.name, + target: dbB.name, + user_ctx: { + name: "joe", + roles: ["erlanger", "god"] + } + }; + T(repDb.save(repDoc2).ok); + + waitForRep(repDb, repDoc2, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + // because anyone with a 'god' role is an admin of db B, a replication + // that is delegated to a 'god' role can write design docs to db B + doc = dbB.open(ddoc._id); + T(doc !== null); + T(doc.language === ddoc.language); + }); + } + + + function continuous_replication_survives_restart() { + var origRepDbName = CouchDB.request( + "GET", "/_config/replicator/db").responseText; + + repDb.deleteDb(); + + var xhr = CouchDB.request("PUT", "/_config/replicator/db", { + body : JSON.stringify(repDb.name), + headers: {"X-Couch-Persist": "false"} + }); + T(xhr.status === 200); + + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_cont_rep_survives_doc", + source: "http://" + host + "/" + dbA.name, + target: dbB.name, + continuous: true + }; + + T(repDb.save(repDoc).ok); + + waitForSeq(dbA, dbB); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + repDb.ensureFullCommit(); + dbA.ensureFullCommit(); + + restartServer(); + + xhr = CouchDB.request("PUT", "/_config/replicator/db", { + body : JSON.stringify(repDb.name), + headers: {"X-Couch-Persist": "false"} + }); + + T(xhr.status === 200); + + // add another doc to source, it will be replicated to target + var docX = { + _id: "foo1000", + value: 1001 + }; + + T(dbA.save(docX).ok); + + waitForSeq(dbA, dbB); + var copy = dbB.open("foo1000"); + T(copy !== null); + T(copy.value === 1001); + + repDoc = repDb.open("foo_cont_rep_survives_doc"); + T(repDoc !== null); + T(repDoc.continuous === true); + + // stop replication + T(repDb.deleteDoc(repDoc).ok); + + xhr = CouchDB.request("PUT", "/_config/replicator/db", { + body : origRepDbName, + headers: {"X-Couch-Persist": "false"} + }); + T(xhr.status === 200); + } + + + function rep_db_write_authorization() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var server_admins_config = [ + { + section: "admins", + key: "fdmanana", + value: "qwerty" + } + ]; + + run_on_modified_server(server_admins_config, function() { + var repDoc = { + _id: "foo_rep_doc", + source: dbA.name, + target: dbB.name + }; + + T(CouchDB.login("fdmanana", "qwerty").ok); + T(CouchDB.session().userCtx.name === "fdmanana"); + T(CouchDB.session().userCtx.roles.indexOf("_admin") !== -1); + + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + + T(copy !== null); + T(copy.value === doc.value); + } + + repDoc = repDb.open("foo_rep_doc"); + T(repDoc !== null); + repDoc.target = "test_suite_foo_db"; + repDoc.create_target = true; + + // Only the replicator can update replication documents. + // Admins can only add and delete replication documents. + try { + repDb.save(repDoc); + T(false && "Should have thrown an exception"); + } catch (x) { + T(x["error"] === "forbidden"); + } + }); + } + + + function rep_doc_with_bad_rep_id() { + populate_db(dbA, docs1); + populate_db(dbB, []); + + var repDoc = { + _id: "foo_rep", + source: dbA.name, + target: dbB.name, + replication_id: "1234abc" + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "completed"); + for (var i = 0; i < docs1.length; i++) { + var doc = docs1[i]; + var copy = dbB.open(doc._id); + T(copy !== null); + T(copy.value === doc.value); + } + + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1.source === repDoc.source); + T(repDoc1.target === repDoc.target); + T(repDoc1._replication_state === "completed", + "replication document with bad replication id failed"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + T(repDoc1._replication_id !== "1234abc"); + } + + + function error_state_replication() { + populate_db(dbA, docs1); + + var repDoc = { + _id: "foo_error_rep", + source: dbA.name, + target: "nonexistent_test_db" + }; + T(repDb.save(repDoc).ok); + + waitForRep(repDb, repDoc, "error"); + var repDoc1 = repDb.open(repDoc._id); + T(repDoc1 !== null); + T(repDoc1._replication_state === "error"); + T(typeof repDoc1._replication_state_time === "number"); + T(typeof repDoc1._replication_id === "string"); + } + + + // run all the tests + var server_config = [ + { + section: "replicator", + key: "db", + value: repDb.name + } + ]; + + repDb.deleteDb(); + run_on_modified_server(server_config, simple_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, filtered_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, continuous_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, by_doc_ids_replication); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, successive_identical_replications); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, identical_rep_docs); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, identical_continuous_rep_docs); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, rep_db_write_authorization); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, rep_doc_with_bad_rep_id); + + var server_config_2 = server_config.concat([ + { + section: "couch_httpd_auth", + key: "authentication_db", + value: usersDb.name + } + ]); + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config_2, test_replication_credentials_delegation); + + repDb.deleteDb(); + restartServer(); + continuous_replication_survives_restart(); + + repDb.deleteDb(); + restartServer(); + run_on_modified_server(server_config, error_state_replication); + + + // cleanup + repDb.deleteDb(); + usersDb.deleteDb(); + dbA.deleteDb(); + dbB.deleteDb(); +}; diff --git a/share/www/script/test/rewrite.js b/share/www/script/test/rewrite.js new file mode 100644 index 00000000..474abb5e --- /dev/null +++ b/share/www/script/test/rewrite.js @@ -0,0 +1,443 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + + + +couchTests.rewrite = function(debug) { + // this test _rewrite handler + + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + db.deleteDb(); + db.createDb(); + + + if (debug) debugger; + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, + {section:"httpd", + key: "WWW-Authenticate", + value: "X-Couch-Test-Auth"}], + + function(){ + var designDoc = { + _id:"_design/test", + language: "javascript", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, + rewrites: [ + { + "from": "foo", + "to": "foo.txt" + }, + { + "from": "foo2", + "to": "foo.txt", + "method": "GET" + }, + { + "from": "hello/:id", + "to": "_update/hello/:id", + "method": "PUT" + }, + { + "from": "/welcome", + "to": "_show/welcome" + }, + { + "from": "/welcome/:name", + "to": "_show/welcome", + "query": { + "name": ":name" + } + }, + { + "from": "/welcome2", + "to": "_show/welcome", + "query": { + "name": "user" + } + }, + { + "from": "/welcome3/:name", + "to": "_update/welcome2/:name", + "method": "PUT" + }, + { + "from": "/welcome3/:name", + "to": "_show/welcome2/:name", + "method": "GET" + }, + { + "from": "/welcome4/*", + "to" : "_show/welcome3", + "query": { + "name": "*" + } + }, + { + "from": "/type/<type>.json", + "to": "_show/type/:type", + "query": { + "format": "json" + } + }, + { + "from": "/type/<type>.xml", + "to": "_show/type/:type", + "query": { + "format": "xml" + } + }, + { + "from": "/type/<type>", + "to": "_show/type/:type", + "query": { + "format": "html" + } + }, + { + "from": "/welcome5/*", + "to" : "_show/*", + "query": { + "name": "*" + } + }, + { + "from": "basicView", + "to": "_view/basicView", + }, + { + "from": "simpleForm/basicView", + "to": "_list/simpleForm/basicView", + }, + { + "from": "simpleForm/basicViewFixed", + "to": "_list/simpleForm/basicView", + "query": { + "startkey": 3, + "endkey": 8 + } + }, + { + "from": "simpleForm/basicViewPath/:start/:end", + "to": "_list/simpleForm/basicView", + "query": { + "startkey": ":start", + "endkey": ":end" + } + }, + { + "from": "simpleForm/complexView", + "to": "_list/simpleForm/complexView", + "query": { + "key": [1, 2] + } + }, + { + "from": "simpleForm/complexView2", + "to": "_list/simpleForm/complexView", + "query": { + "key": ["test", {}] + } + }, + { + "from": "simpleForm/complexView3", + "to": "_list/simpleForm/complexView", + "query": { + "key": ["test", ["test", "essai"]] + } + }, + { + "from": "simpleForm/complexView4", + "to": "_list/simpleForm/complexView2", + "query": { + "key": {"c": 1} + } + }, + { + "from": "simpleForm/complexView5/:a/:b", + "to": "_list/simpleForm/complexView3", + "query": { + "key": [":a", ":b"] + } + }, + { + "from": "simpleForm/complexView6", + "to": "_list/simpleForm/complexView3", + "query": { + "key": [":a", ":b"] + } + }, + { + "from": "/", + "to": "_view/basicView", + } + ], + lists: { + simpleForm: stringFun(function(head, req) { + log("simpleForm"); + send('<ul>'); + var row, row_number = 0, prevKey, firstKey = null; + while (row = getRow()) { + row_number += 1; + if (!firstKey) firstKey = row.key; + prevKey = row.key; + send('\n<li>Key: '+row.key + +' Value: '+row.value + +' LineNo: '+row_number+'</li>'); + } + return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>'; + }), + }, + shows: { + "welcome": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }), + "welcome2": stringFun(function(doc, req) { + return "Welcome " + doc.name; + }), + "welcome3": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }), + "type": stringFun(function(doc, req) { + return req.id + " as " + req.query.format; + }) + }, + updates: { + "hello" : stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id : req.id + }, "New World"] + } + return [null, "Empty World"]; + } + doc.world = "hello"; + doc.edited_by = req.userCtx; + return [doc, "hello doc"]; + }), + "welcome2": stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id: req.id, + name: req.id + }, "New World"] + } + return [null, "Empty World"]; + } + return [doc, "hello doc"]; + }) + }, + views : { + basicView : { + map : stringFun(function(doc) { + if (doc.integer) { + emit(doc.integer, doc.string); + } + + }) + }, + complexView: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit([doc.a, doc.b], doc.string); + } + }) + }, + complexView2: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit(doc.a, doc.string); + } + }) + }, + complexView3: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit(doc.b, doc.string); + } + }) + } + } + } + + db.save(designDoc); + + var docs = makeDocs(0, 10); + db.bulkSave(docs); + + var docs2 = [ + {"a": 1, "b": 1, "string": "doc 1", "type": "complex"}, + {"a": 1, "b": 2, "string": "doc 2", "type": "complex"}, + {"a": "test", "b": {}, "string": "doc 3", "type": "complex"}, + {"a": "test", "b": ["test", "essai"], "string": "doc 4", "type": "complex"}, + {"a": {"c": 1}, "b": "", "string": "doc 5", "type": "complex"} + ]; + + db.bulkSave(docs2); + + // test simple rewriting + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo2"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + + // test POST + // hello update world + + var doc = {"word":"plankton", "name":"Rusty"} + var resp = db.save(doc); + T(resp.ok); + var docid = resp.id; + + xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/hello/"+docid); + T(xhr.status == 201); + T(xhr.responseText == "hello doc"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) + + doc = db.open(docid); + T(doc.world == "hello"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome?name=user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome/user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome2"); + T(req.responseText == "Welcome user"); + + xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/welcome3/test"); + T(xhr.status == 201); + T(xhr.responseText == "New World"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome3/test"); + T(xhr.responseText == "Welcome test"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome4/user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome5/welcome3"); + T(req.responseText == "Welcome welcome3"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/basicView"); + T(xhr.status == 200, "view call"); + T(/{"total_rows":9/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/"); + T(xhr.status == 200, "view call"); + T(/{"total_rows":9/.test(xhr.responseText)); + + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewPath/3/8"); + T(xhr.status == 200, "with query params"); + T(!(/Key: 1/.test(xhr.responseText))); + T(/FirstKey: 3/.test(xhr.responseText)); + T(/LastKey: 8/.test(xhr.responseText)); + + // get with query params + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView"); + T(xhr.status == 200, "with query params"); + T(/FirstKey: [1, 2]/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView2"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 3/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView3"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView4"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 5/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView5/test/essai"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/complexView6?a=test&b=essai"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/type/test.json"); + T(req.responseText == "test as json"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/type/test.xml"); + T(req.responseText == "test as xml"); + + req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/type/test"); + T(req.responseText == "test as html"); + + // test path relative to server + designDoc.rewrites.push({ + "from": "uuids", + "to": "../../../_uuids" + }); + T(db.save(designDoc).ok); + + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/uuids"); + T(xhr.status == 500); + var result = JSON.parse(xhr.responseText); + T(result.error == "insecure_rewrite_rule"); + + run_on_modified_server( + [{section: "httpd", + key: "secure_rewrites", + value: "false"}], + function() { + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/uuids?cache=bust"); + T(xhr.status == 200); + var result = JSON.parse(xhr.responseText); + T(result.uuids.length == 1); + var first = result.uuids[0]; + }); + + }); + +} diff --git a/share/www/script/test/security_validation.js b/share/www/script/test/security_validation.js index d07195e1..42aa11c9 100644 --- a/share/www/script/test/security_validation.js +++ b/share/www/script/test/security_validation.js @@ -13,7 +13,7 @@ couchTests.security_validation = function(debug) { // This tests couchdb's security and validation features. This does // not test authentication, except to use test authentication code made - // specifically for this testing. It is a WWWW-Authenticate scheme named + // specifically for this testing. It is a WWW-Authenticate scheme named // X-Couch-Test-Auth, and the user names and passwords are hard coded // on the server-side. // @@ -21,7 +21,7 @@ couchTests.security_validation = function(debug) { // implementation for Firefox and Safari, and probably other browsers are // broken (Firefox always prompts the user on 401 failures, Safari gives // odd security errors when using different name/passwords, perhaps due - // to cross site scripting prevention). These problems essentially make Basic + // to cross site scripting prevention). These problems essentially make Basic // authentication testing in the browser impossible. But while hard to // test automated in the browser, Basic auth may still useful for real // world use where these bugs/behaviors don't matter. @@ -69,7 +69,13 @@ couchTests.security_validation = function(debug) { var designDoc = { _id:"_design/test", language: "javascript", - validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) { + validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx, secObj) { + if (secObj.admin_override) { + if (userCtx.roles.indexOf('_admin') != -1) { + // user is admin, they can do anything + return true; + } + } // docs should have an author field. if (!newDoc._deleted && !newDoc.author) { throw {forbidden: @@ -99,13 +105,27 @@ couchTests.security_validation = function(debug) { } // set user as the admin - T(db.setDbProperty("_admins", ["Damien Katz"]).ok); + T(db.setSecObj({ + admins : {names : ["Damien Katz"]} + }).ok); T(userDb.save(designDoc).ok); - // test the _whoami endpoint + var user2Db = new CouchDB("test_suite_db", + {"WWW-Authenticate": "X-Couch-Test-Auth Jan Lehnardt:apple"} + ); + // Attempt to save the design as a non-admin (in replication scenario) + try { + user2Db.save(designDoc, {new_edits : false}); + T(false && "Can't get here. Should have thrown an error on design doc"); + } catch (e) { + T(e.error == "unauthorized"); + T(user2Db.last_req.status == 401); + } + + // test the _session API var resp = userDb.request("GET", "/_session"); - var user = JSON.parse(resp.responseText) + var user = JSON.parse(resp.responseText).userCtx; T(user.name == "Damien Katz"); // test that the roles are listed properly TEquals(user.roles, []); @@ -116,20 +136,23 @@ couchTests.security_validation = function(debug) { doc.foo=2; T(userDb.save(doc).ok); - // Save a document that's missing an author field. - try { - userDb.save({foo:1}); - T(false && "Can't get here. Should have thrown an error 2"); - } catch (e) { - T(e.error == "forbidden"); - T(userDb.last_req.status == 403); + // Save a document that's missing an author field (before and after compaction) + for (var i=0; i<2; i++) { + try { + userDb.save({foo:1}); + T(false && "Can't get here. Should have thrown an error 2"); + } catch (e) { + T(e.error == "forbidden"); + T(userDb.last_req.status == 403); + } + // compact. + T(db.compact().ok); + T(db.last_req.status == 202); + // compaction isn't instantaneous, loop until done + while (db.info().compact_running) {}; } // Now attempt to update the document as a different user, Jan - var user2Db = new CouchDB("test_suite_db", - {"WWW-Authenticate": "X-Couch-Test-Auth Jan Lehnardt:apple"} - ); - var doc = user2Db.open("testdoc"); doc.foo=3; try { @@ -158,6 +181,37 @@ couchTests.security_validation = function(debug) { T(e.error == "unauthorized"); T(userDb.last_req.status == 401); } + + // admin must save with author field unless admin override + var resp = db.request("GET", "/_session"); + var user = JSON.parse(resp.responseText).userCtx; + T(user.name == null); + // test that we are admin + TEquals(user.roles, ["_admin"]); + + // can't save the doc even though we are admin + var doc = db.open("testdoc"); + doc.foo=3; + try { + db.save(doc); + T(false && "Can't get here. Should have thrown an error 3"); + } catch (e) { + T(e.error == "unauthorized"); + T(db.last_req.status == 401); + } + + // now turn on admin override + T(db.setDbProperty("_security", {admin_override : true}).ok); + T(db.save(doc).ok); + + // try to do something lame + try { + db.setDbProperty("_security", ["foo"]); + T(false && "can't do this"); + } catch(e) {} + + // go back to normal + T(db.setDbProperty("_security", {admin_override : false}).ok); // Now delete document T(user2Db.deleteDoc(doc).ok); @@ -188,7 +242,6 @@ couchTests.security_validation = function(debug) { T(db.open("booboo") == null); T(db.open("foofoo") == null); - // Now test replication var AuthHeaders = {"WWW-Authenticate": "X-Couch-Test-Auth Christopher Lenz:dog food"}; var host = CouchDB.host; @@ -197,16 +250,16 @@ couchTests.security_validation = function(debug) { target:"test_suite_db_b"}, {source:"test_suite_db_a", - target:{url: "http://" + host + "/test_suite_db_b", + target:{url: CouchDB.protocol + host + "/test_suite_db_b", headers: AuthHeaders}}, - {source:{url:"http://" + host + "/test_suite_db_a", + {source:{url:CouchDB.protocol + host + "/test_suite_db_a", headers: AuthHeaders}, target:"test_suite_db_b"}, - {source:{url:"http://" + host + "/test_suite_db_a", + {source:{url:CouchDB.protocol + host + "/test_suite_db_a", headers: AuthHeaders}, - target:{url:"http://" + host + "/test_suite_db_b", + target:{url:CouchDB.protocol + host + "/test_suite_db_b", headers: AuthHeaders}}, ] var adminDbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index f484e029..55ed9698 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -25,7 +25,11 @@ couchTests.show_documents = function(debug) { if (doc) { return "Hello World"; } else { - return "Empty World"; + if(req.id) { + return "New World"; + } else { + return "Empty World"; + } } }), "just-name" : stringFun(function(doc, req) { @@ -50,18 +54,28 @@ couchTests.show_documents = function(debug) { json : req } }), + "show-deleted" : stringFun(function(doc, req) { + if(doc) { + return doc._id; + } else { + return "No doc " + req.id; + } + }), "render-error" : stringFun(function(doc, req) { return noSuchVariable; }), "empty" : stringFun(function(doc, req) { return ""; }), + "fail" : stringFun(function(doc, req) { + return doc._id; + }), "xml-type" : stringFun(function(doc, req) { return { "headers" : { "Content-Type" : "application/xml" }, - "body" : new XML('<xml><node foo="bar"/></xml>') + "body" : new XML('<xml><node foo="bar"/></xml>').toXMLString() } }), "no-set-etag" : stringFun(function(doc, req) { @@ -72,6 +86,25 @@ couchTests.show_documents = function(debug) { "body" : "something" } }), + "list-api" : stringFun(function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey"); + }), + "list-api-mix" : stringFun(function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey "); + return "Dude"; + }), + "list-api-mix-with-header" : stringFun(function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey "); + return { + headers: { + "X-Couch-Test-Header-Awesome": "Oh Yeah!" + }, + body: "Dude" + }; + }), "accept-switch" : stringFun(function(doc, req) { if (req.headers["Accept"].match(/image/)) { return { @@ -114,12 +147,19 @@ couchTests.show_documents = function(debug) { // E4X outside of a string. Outside of tests you // can just use E4X literals. eval('xml.node.@foo = doc.word'); - return xml; + log('xml: '+xml.toSource()); + return xml.toXMLString(); }); provides("foo", function() { return "foofoo"; }); + }), + "withSlash": stringFun(function(doc, req) { + return { json: doc } + }), + "secObj": stringFun(function(doc, req) { + return { json: req.secObj }; }) } }; @@ -157,7 +197,7 @@ couchTests.show_documents = function(debug) { T(xhr.responseText == ""); // // hello template world (non-existing docid) - xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/nonExistingDoc"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/fail/nonExistingDoc"); T(xhr.status == 404); var resp = JSON.parse(xhr.responseText); T(resp.error == "not_found"); @@ -169,8 +209,7 @@ couchTests.show_documents = function(debug) { // show with missing doc xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/missingdoc"); T(xhr.status == 404); - var resp = JSON.parse(xhr.responseText); - T(resp.error == "not_found"); + TEquals("No such doc", xhr.responseText); // show with missing func xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/missing/"+docid); @@ -342,4 +381,56 @@ couchTests.show_documents = function(debug) { xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/json/foo"); TEquals(1, JSON.parse(xhr.responseText)._conflicts.length); + var doc3 = {_id:"a/b/c", a:1}; + db.save(doc3); + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/withSlash/a/b/c"); + T(xhr.status == 200); + + // hello template world (non-existing docid) + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/nonExistingDoc"); + T(xhr.responseText == "New World"); + + // test list() compatible API + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api/foo"); + T(xhr.responseText == "Hey"); + TEquals("Yeah", xhr.getResponseHeader("X-Couch-Test-Header"), "header should be cool"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api-mix/foo"); + T(xhr.responseText == "Hey Dude"); + TEquals("Yeah", xhr.getResponseHeader("X-Couch-Test-Header"), "header should be cool"); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/list-api-mix-with-header/foo"); + T(xhr.responseText == "Hey Dude"); + TEquals("Yeah", xhr.getResponseHeader("X-Couch-Test-Header"), "header should be cool"); + TEquals("Oh Yeah!", xhr.getResponseHeader("X-Couch-Test-Header-Awesome"), "header should be cool"); + + // test deleted docs + var doc = {_id:"testdoc",foo:1}; + db.save(doc); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/show-deleted/testdoc"); + TEquals("testdoc", xhr.responseText, "should return 'testdoc'"); + + db.deleteDoc(doc); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/show-deleted/testdoc"); + TEquals("No doc testdoc", xhr.responseText, "should return 'no doc testdoc'"); + + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, + {section:"httpd", + key: "WWW-Authenticate", + value: "X-Couch-Test-Auth"}], + + function() { + T(db.setDbProperty("_security", {foo: true}).ok); + T(db.save(doc).ok); + + xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/secObj"); + var resp = JSON.parse(xhr.responseText); + T(resp.foo == true); + } + ); + }; diff --git a/share/www/script/test/stats.js b/share/www/script/test/stats.js index 793b390d..6fb0fbba 100644 --- a/share/www/script/test/stats.js +++ b/share/www/script/test/stats.js @@ -30,7 +30,7 @@ couchTests.stats = function(debug) { _id:"_design/test", // turn off couch.js id escaping? language: "javascript", views: { - all_docs: {map: "function(doc) {emit(doc.integer, null);}"}, + all_docs: {map: "function(doc) {emit(doc.integer, null);}"} } }; db.save(designDoc); @@ -81,19 +81,17 @@ couchTests.stats = function(debug) { var pre_dbs = getStat("couchdb", "open_databases").current || 0; var pre_files = getStat("couchdb", "open_os_files").current || 0; - // We have to make sure that as we open the max'th database - // that we've waited for more than 1 second since opening - // the first database so that any delayed commits will be - // flushed. var triggered = false; var db = null; for(var i = 0; i < max*2; i++) { - try { - db = newDb("test_suite_db_" + i, true); - } catch(e) { - triggered = true; - CouchDB.request("GET", "/_sleep?time=1500"); - db = newDb("test_suite_db_" + i, true); + while (true) { + try { + db = newDb("test_suite_db_" + i, true); + break; + } catch(e) { + // all_dbs_active error! + triggered = true; + } } // Trigger a delayed commit @@ -162,12 +160,15 @@ couchTests.stats = function(debug) { runTest("couchdb", "database_writes", { run: function(db) { - CouchDB.request("POST", "/test_suite_db", {body: '{"a": "1"}'}) + CouchDB.request("POST", "/test_suite_db", { + headers: {"Content-Type": "application/json"}, + body: '{"a": "1"}' + }); }, test: function(before, after) { TEquals(before+1, after, "POST'ing new docs increments doc writes."); } - }) + }); runTest("couchdb", "database_writes", { setup: function(db) {db.save({"_id": "test"});}, @@ -246,7 +247,7 @@ couchTests.stats = function(debug) { }); runTest("httpd", "temporary_view_reads", { - run: function(db) {db.query(function(doc) {emit(doc._id)})}, + run: function(db) { db.query(function(doc) { emit(doc._id); }); }, test: function(before, after) { TEquals(before+1, after, "Temporary views have their own counter."); } @@ -260,7 +261,7 @@ couchTests.stats = function(debug) { }); runTest("httpd", "view_reads", { - run: function(db) {db.query(function(doc) {emit(doc._id)});}, + run: function(db) { db.query(function(doc) { emit(doc._id); }); }, test: function(before, after) { TEquals(before, after, "Temporary views don't affect permanent views."); } diff --git a/share/www/script/test/update_documents.js b/share/www/script/test/update_documents.js index 87fc7352..49d3b68a 100644 --- a/share/www/script/test/update_documents.js +++ b/share/www/script/test/update_documents.js @@ -68,10 +68,13 @@ couchTests.update_documents = function(debug) { "headers" : { "Content-Type" : "application/xml" }, - "body" : xml + "body" : xml.toXMLString() }; return [doc, resp]; + }), + "get-uuid" : stringFun(function(doc, req) { + return [null, req.uuid]; }) } }; @@ -110,9 +113,13 @@ couchTests.update_documents = function(debug) { T(JSON.parse(xhr.responseText).error == "method_not_allowed"); // // hello update world (non-existing docid) + xhr = CouchDB.request("GET", "/test_suite_db/nonExistingDoc"); + T(xhr.status == 404); xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/hello/nonExistingDoc"); T(xhr.status == 201); T(xhr.responseText == "<p>New World</p>"); + xhr = CouchDB.request("GET", "/test_suite_db/nonExistingDoc"); + T(xhr.status == 200); // in place update xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/in-place/"+docid+'?field=title&value=test'); @@ -123,7 +130,7 @@ couchTests.update_documents = function(debug) { // bump counter xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/bump-counter/"+docid, { - headers : {"X-Couch-Full-Commit":"false"} + headers : {"X-Couch-Full-Commit":"true"} }); T(xhr.status == 201); T(xhr.responseText == "<h1>bumped it!</h1>"); @@ -135,12 +142,16 @@ couchTests.update_documents = function(debug) { headers : {"X-Couch-Full-Commit":"true"} }); + var NewRev = xhr.getResponseHeader("X-Couch-Update-NewRev"); doc = db.open(docid); + T(doc['_rev'] == NewRev); + + T(doc.counter == 2); // parse xml xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/xml/"+docid, { - headers : {"X-Couch-Full-Commit":"false"}, + headers : {"X-Couch-Full-Commit":"true"}, "body" : '<xml><foo>bar</foo></xml>' }); T(xhr.status == 201); @@ -148,5 +159,10 @@ couchTests.update_documents = function(debug) { doc = db.open(docid); T(doc.via_xml == "bar"); + + // Server provides UUID when POSTing without an ID in the URL + xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/get-uuid/"); + T(xhr.status == 200); + T(xhr.responseText.length == 32); }; diff --git a/share/www/script/test/users_db.js b/share/www/script/test/users_db.js index 2cf63fcf..1e13e5d7 100644 --- a/share/www/script/test/users_db.js +++ b/share/www/script/test/users_db.js @@ -24,44 +24,101 @@ couchTests.users_db = function(debug) { // to determine the actual users db name. function testFun() { - usersDb.deleteDb(); - // test that the validation function is installed var ddoc = usersDb.open("_design/_auth"); T(ddoc.validate_doc_update); // test that you can login as a user using basic auth var jchrisUserDoc = CouchDB.prepareUserDoc({ - username: "jchris@apache.org" + name: "jchris@apache.org" }, "funnybone"); T(usersDb.save(jchrisUserDoc).ok); - T(CouchDB.session().name == null); + T(CouchDB.session().userCtx.name == null); + + // test that you can use basic auth aginst the users db var s = CouchDB.session({ headers : { + // base64_encode("jchris@apache.org:funnybone") "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l" } }); - T(s.name == "jchris@apache.org"); - T(s.user_doc._id == "org.couchdb.user:jchris@apache.org") - T(s.info.authenticated == "{couch_httpd_auth, default_authentication_handler}"); - T(s.info.user_db == "test_suite_users"); - TEquals(["{couch_httpd_oauth, oauth_authentication_handler}", - "{couch_httpd_auth, cookie_authentication_handler}", - "{couch_httpd_auth, default_authentication_handler}"], s.info.handlers); + T(s.userCtx.name == "jchris@apache.org"); + T(s.info.authenticated == "default"); + T(s.info.authentication_db == "test_suite_users"); + TEquals(["oauth", "cookie", "default"], s.info.authentication_handlers); var s = CouchDB.session({ headers : { - "Authorization" : "Basic Xzpf" // username and pass of _:_ + "Authorization" : "Basic Xzpf" // name and pass of _:_ } }); T(s.name == null); - T(s.info.authenticated == "{couch_httpd_auth, default_authentication_handler}"); + T(s.info.authenticated == "default"); + + + // ok, now create a conflicting edit on the jchris doc, and make sure there's no login. + var jchrisUser2 = JSON.parse(JSON.stringify(jchrisUserDoc)); + jchrisUser2.foo = "bar"; + T(usersDb.save(jchrisUser2).ok); + try { + usersDb.save(jchrisUserDoc); + T(false && "should be an update conflict") + } catch(e) { + T(true); + } + // save as bulk with new_edits=false to force conflict save + var resp = usersDb.bulkSave([jchrisUserDoc],{all_or_nothing : true}); + + var jchrisWithConflict = usersDb.open(jchrisUserDoc._id, {conflicts : true}); + T(jchrisWithConflict._conflicts.length == 1) + + // no login with conflicted user doc + try { + var s = CouchDB.session({ + headers : { + "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l" + } + }); + T(false && "this will throw") + } catch(e) { + T(e.error == "unauthorized") + T(/conflict/.test(e.reason)) + } + + // you can delete a user doc + s = CouchDB.session().userCtx; + T(s.name == null); + T(s.roles.indexOf("_admin") !== -1); + T(usersDb.deleteDoc(jchrisWithConflict).ok); + + // you can't change doc from type "user" + jchrisUserDoc = usersDb.open(jchrisUserDoc._id); + jchrisUserDoc.type = "not user"; + try { + usersDb.save(jchrisUserDoc); + T(false && "should only allow us to save doc when type == 'user'"); + } catch(e) { + T(e.reason == "doc.type must be user"); + } + jchrisUserDoc.type = "user"; + + // "roles" must be an array + jchrisUserDoc.roles = "not an array"; + try { + usersDb.save(jchrisUserDoc); + T(false && "should only allow us to save doc when roles is an array"); + } catch(e) { + T(e.reason == "doc.roles must be an array"); + } + jchrisUserDoc.roles = []; }; - + + usersDb.deleteDb(); run_on_modified_server( [{section: "couch_httpd_auth", - key: "authentication_db", value: "test_suite_users"}], + key: "authentication_db", value: usersDb.name}], testFun ); + usersDb.deleteDb(); // cleanup -}
\ No newline at end of file +} diff --git a/share/www/script/test/uuids.js b/share/www/script/test/uuids.js index 4de7ce90..fc33a105 100644 --- a/share/www/script/test/uuids.js +++ b/share/www/script/test/uuids.js @@ -93,12 +93,20 @@ couchTests.uuids = function(debug) { xhr = CouchDB.request("GET", "/_uuids?count=1000"); T(xhr.status == 200); result = JSON.parse(xhr.responseText); - for(var i = 1; i < result.uuids.length; i++) { - T(result.uuids[i].length == 32); - var u1 = result.uuids[i-1].substr(0, 13); - var u2 = result.uuids[i].substr(0, 13); - T(u1 < u2, "UTC uuids are roughly ordered."); + T(result.uuids[1].length == 32); + + // no collisions + var seen = {}; + for(var i in result.uuids) { + var id = result.uuids[i]; + T(seen[id] === undefined); + seen[id] = 1; } + + // roughly ordered + var u1 = result.uuids[1].substr(0, 13); + var u2 = result.uuids[result.uuids.length-1].substr(0, 13); + T(u1 < u2, "UTC uuids are only roughly ordered, so this assertion may fail occasionally. Don't sweat it."); }; run_on_modified_server([{ diff --git a/share/www/script/test/view_collation.js b/share/www/script/test/view_collation.js index f4ae4a15..b01a5c50 100644 --- a/share/www/script/test/view_collation.js +++ b/share/www/script/test/view_collation.js @@ -90,27 +90,27 @@ couchTests.view_collation = function(debug) { // the inclusive_end=true functionality is limited to endkey currently // if you need inclusive_start=false for startkey, please do implement. ;) var rows = db.query(queryFun, null, {endkey : "b", inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // descending=true var rows = db.query(queryFun, null, {endkey : "b", descending:true, inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // test inclusive_end=false var rows = db.query(queryFun, null, {endkey : "b", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); // descending=true var rows = db.query(queryFun, null, {endkey : "b", descending:true, inclusive_end:false}).rows; - T(rows[rows.length-1].key == "B") + T(rows[rows.length-1].key == "B"); var rows = db.query(queryFun, null, { endkey : "b", endkey_docid: "10", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); var rows = db.query(queryFun, null, { endkey : "b", endkey_docid: "11", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); }; diff --git a/share/www/script/test/view_collation_raw.js b/share/www/script/test/view_collation_raw.js index 08f37fae..31624cdb 100644 --- a/share/www/script/test/view_collation_raw.js +++ b/share/www/script/test/view_collation_raw.js @@ -97,27 +97,27 @@ couchTests.view_collation_raw = function(debug) { // the inclusive_end=true functionality is limited to endkey currently // if you need inclusive_start=false for startkey, please do implement. ;) var rows = db.view("test/test", {endkey : "b", inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // descending=true var rows = db.view("test/test", {endkey : "b", descending:true, inclusive_end:true}).rows; - T(rows[rows.length-1].key == "b") + T(rows[rows.length-1].key == "b"); // test inclusive_end=false var rows = db.view("test/test", {endkey : "b", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); // descending=true var rows = db.view("test/test", {endkey : "b", descending:true, inclusive_end:false}).rows; - T(rows[rows.length-1].key == "ba") + T(rows[rows.length-1].key == "ba"); var rows = db.view("test/test", { endkey : "b", endkey_docid: "10", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); var rows = db.view("test/test", { endkey : "b", endkey_docid: "11", inclusive_end:false}).rows; - T(rows[rows.length-1].key == "aa") + T(rows[rows.length-1].key == "aa"); }; diff --git a/share/www/script/test/view_compaction.js b/share/www/script/test/view_compaction.js new file mode 100644 index 00000000..a11fb7bd --- /dev/null +++ b/share/www/script/test/view_compaction.js @@ -0,0 +1,104 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.view_compaction = function(debug) { + + if (debug) debugger; + + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit": "true"}); + + db.deleteDb(); + db.createDb(); + + var ddoc = { + _id: "_design/foo", + language: "javascript", + views: { + view1: { + map: "function(doc) { emit(doc._id, doc.value) }" + }, + view2: { + map: "function(doc) { emit(doc._id, doc.value); }", + reduce: "function(keys, values, rereduce) { return sum(values); }" + } + } + }; + T(db.save(ddoc).ok); + + var docs = makeDocs(0, 1000); + db.bulkSave(docs); + + var resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 1001); + + + // update docs + for (var i = 0; i < docs.length; i++) { + docs[i].integer = docs[i].integer + 1; + } + db.bulkSave(docs); + + + resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 2001); + + + // update docs again... + for (var i = 0; i < docs.length; i++) { + docs[i].integer = docs[i].integer + 2; + } + db.bulkSave(docs); + + + resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 3001); + + var disk_size_before_compact = resp.view_index.disk_size; + + // compact view group + var xhr = CouchDB.request("POST", "/" + db.name + "/_compact" + "/foo"); + T(JSON.parse(xhr.responseText).ok === true); + + resp = db.designInfo("_design/foo"); + while (resp.view_index.compact_running === true) { + resp = db.designInfo("_design/foo"); + } + + + resp = db.view('foo/view1', {}); + T(resp.rows.length === 1000); + + resp = db.view('foo/view2', {}); + T(resp.rows.length === 1); + + resp = db.designInfo("_design/foo"); + T(resp.view_index.update_seq === 3001); + T(resp.view_index.disk_size < disk_size_before_compact); +};
\ No newline at end of file diff --git a/share/www/script/test/view_errors.js b/share/www/script/test/view_errors.js index 0f90c46f..e8bd08e4 100644 --- a/share/www/script/test/view_errors.js +++ b/share/www/script/test/view_errors.js @@ -16,8 +16,6 @@ couchTests.view_errors = function(debug) { db.createDb(); if (debug) debugger; - - run_on_modified_server( [{section: "couchdb", key: "os_process_timeout", @@ -26,12 +24,13 @@ couchTests.view_errors = function(debug) { var doc = {integer: 1, string: "1", array: [1, 2, 3]}; T(db.save(doc).ok); - // emitting a key value that is undefined should result in that row not - // being included in the view results + // emitting a key value that is undefined should result in that row + // being included in the view results as null var results = db.query(function(doc) { emit(doc.undef, null); }); - T(results.total_rows == 0); + T(results.total_rows == 1); + T(results.rows[0].key == null); // if a view function throws an exception, its results are not included in // the view index, but the view does not itself raise an error @@ -41,13 +40,13 @@ couchTests.view_errors = function(debug) { T(results.total_rows == 0); // if a view function includes an undefined value in the emitted key or - // value, an error is logged and the result is not included in the view - // index, and the view itself does not raise an error + // value, it is treated as null var results = db.query(function(doc) { emit([doc._id, doc.undef], null); }); - T(results.total_rows == 0); - + T(results.total_rows == 1); + T(results.rows[0].key[1] == null); + // querying a view with invalid params should give a resonable error message var xhr = CouchDB.request("POST", "/test_suite_db/_temp_view?startkey=foo", { headers: {"Content-Type": "application/json"}, @@ -57,14 +56,14 @@ couchTests.view_errors = function(debug) { }); T(JSON.parse(xhr.responseText).error == "bad_request"); - // views should ignore Content-Type, like the rest of CouchDB + // content type must be json var xhr = CouchDB.request("POST", "/test_suite_db/_temp_view", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, body: JSON.stringify({language: "javascript", map : "function(doc){}" }) }); - T(xhr.status == 200); + T(xhr.status == 415); var map = function (doc) {emit(doc.integer, doc.integer);}; @@ -75,9 +74,6 @@ couchTests.view_errors = function(debug) { T(e.error == "query_parse_error"); } - // reduce=false on map views doesn't work, so group=true will - // never throw for temp reduce views. - var designDoc = { _id:"_design/test", language: "javascript", @@ -105,6 +101,15 @@ couchTests.view_errors = function(debug) { db.view("test/no_reduce", {group: true}); T(0 == 1); } catch(e) { + T(db.last_req.status == 400); + T(e.error == "query_parse_error"); + } + + try { + db.view("test/no_reduce", {group_level: 1}); + T(0 == 1); + } catch(e) { + T(db.last_req.status == 400); T(e.error == "query_parse_error"); } @@ -116,10 +121,23 @@ couchTests.view_errors = function(debug) { T(e.error == "query_parse_error"); } + db.view("test/no_reduce", {reduce: false}); + TEquals(200, db.last_req.status, "reduce=false for map views (without" + + " group or group_level) is allowed"); + try { db.view("test/with_reduce", {group: true, reduce: false}); T(0 == 1); } catch(e) { + T(db.last_req.status == 400); + T(e.error == "query_parse_error"); + } + + try { + db.view("test/with_reduce", {group_level: 1, reduce: false}); + T(0 == 1); + } catch(e) { + T(db.last_req.status == 400); T(e.error == "query_parse_error"); } @@ -159,5 +177,13 @@ couchTests.view_errors = function(debug) { T(xhr.status == 500); result = JSON.parse(xhr.responseText); T(result.error == "reduce_overflow_error"); + + try { + db.query(function() {emit(null, null)}, null, {startkey: 2, endkey:1}); + T(0 == 1); + } catch(e) { + T(e.error == "query_parse_error"); + T(e.reason.match(/no rows can match/i)); + } }); }; diff --git a/share/www/script/test/view_multi_key_all_docs.js b/share/www/script/test/view_multi_key_all_docs.js index 62e49665..1113be4d 100644 --- a/share/www/script/test/view_multi_key_all_docs.js +++ b/share/www/script/test/view_multi_key_all_docs.js @@ -25,24 +25,52 @@ couchTests.view_multi_key_all_docs = function(debug) { for(var i=0; i<rows.length; i++) T(rows[i].id == keys[i]); + // keys in GET parameters + rows = db.allDocs({keys:keys}, null).rows; + T(rows.length == keys.length); + for(var i=0; i<rows.length; i++) + T(rows[i].id == keys[i]); + rows = db.allDocs({limit: 1}, keys).rows; T(rows.length == 1); T(rows[0].id == keys[0]); + // keys in GET parameters + rows = db.allDocs({limit: 1, keys: keys}, null).rows; + T(rows.length == 1); + T(rows[0].id == keys[0]); + rows = db.allDocs({skip: 2}, keys).rows; T(rows.length == 3); for(var i=0; i<rows.length; i++) T(rows[i].id == keys[i+2]); + // keys in GET parameters + rows = db.allDocs({skip: 2, keys: keys}, null).rows; + T(rows.length == 3); + for(var i=0; i<rows.length; i++) + T(rows[i].id == keys[i+2]); + rows = db.allDocs({descending: "true"}, keys).rows; T(rows.length == keys.length); for(var i=0; i<rows.length; i++) T(rows[i].id == keys[keys.length-i-1]); + // keys in GET parameters + rows = db.allDocs({descending: "true", keys: keys}, null).rows; + T(rows.length == keys.length); + for(var i=0; i<rows.length; i++) + T(rows[i].id == keys[keys.length-i-1]); + rows = db.allDocs({descending: "true", skip: 3, limit:1}, keys).rows; T(rows.length == 1); T(rows[0].id == keys[1]); + // keys in GET parameters + rows = db.allDocs({descending: "true", skip: 3, limit:1, keys: keys}, null).rows; + T(rows.length == 1); + T(rows[0].id == keys[1]); + // Check we get invalid rows when the key doesn't exist rows = db.allDocs({}, [1, "i_dont_exist", "0"]).rows; T(rows.length == 3); @@ -51,4 +79,13 @@ couchTests.view_multi_key_all_docs = function(debug) { T(rows[1].error == "not_found"); T(!rows[1].id); T(rows[2].id == rows[2].key && rows[2].key == "0"); + + // keys in GET parameters + rows = db.allDocs({keys: [1, "i_dont_exist", "0"]}, null).rows; + T(rows.length == 3); + T(rows[0].error == "not_found"); + T(!rows[0].id); + T(rows[1].error == "not_found"); + T(!rows[1].id); + T(rows[2].id == rows[2].key && rows[2].key == "0"); }; diff --git a/share/www/script/test/view_multi_key_design.js b/share/www/script/test/view_multi_key_design.js index 5a2f645d..38396955 100644 --- a/share/www/script/test/view_multi_key_design.js +++ b/share/www/script/test/view_multi_key_design.js @@ -34,11 +34,11 @@ couchTests.view_multi_key_design = function(debug) { reduce:"function (keys, values) { return sum(values); };" } } - } + }; T(db.save(designDoc).ok); // Test that missing keys work too - var keys = [101,30,15,37,50] + var keys = [101,30,15,37,50]; var reduce = db.view("test/summate",{group:true},keys).rows; T(reduce.length == keys.length-1); // 101 is missing for(var i=0; i<reduce.length; i++) { @@ -54,6 +54,13 @@ couchTests.view_multi_key_design = function(debug) { T(rows[i].key == rows[i].value); } + // with GET keys + rows = db.view("test/all_docs",{keys:keys},null).rows; + for(var i=0;i<rows.length; i++) { + T(keys.indexOf(rows[i].key) != -1); + T(rows[i].key == rows[i].value); + } + var reduce = db.view("test/summate",{group:true},keys).rows; T(reduce.length == keys.length); for(var i=0; i<reduce.length; i++) { @@ -61,8 +68,18 @@ couchTests.view_multi_key_design = function(debug) { T(reduce[i].key == reduce[i].value); } + // with GET keys + reduce = db.view("test/summate",{group:true,keys:keys},null).rows; + T(reduce.length == keys.length); + for(var i=0; i<reduce.length; i++) { + T(keys.indexOf(reduce[i].key) != -1); + T(reduce[i].key == reduce[i].value); + } + // Test that invalid parameter combinations get rejected var badargs = [{startkey:0}, {endkey:0}, {key: 0}, {group_level: 2}]; + var getbadargs = [{startkey:0, keys:keys}, {endkey:0, keys:keys}, + {key:0, keys:keys}, {group_level: 2, keys:keys}]; for(var i in badargs) { try { @@ -71,6 +88,13 @@ couchTests.view_multi_key_design = function(debug) { } catch (e) { T(e.error == "query_parse_error"); } + + try { + db.view("test/all_docs",getbadargs[i],null); + T(0==1); + } catch (e) { + T(e.error = "query_parse_error"); + } } try { @@ -80,8 +104,18 @@ couchTests.view_multi_key_design = function(debug) { T(e.error == "query_parse_error"); } + try { + db.view("test/summate",{keys:keys},null); + T(0==1); + } catch (e) { + T(e.error == "query_parse_error"); + } + // Test that a map & reduce containing func support keys when reduce=false - resp = db.view("test/summate", {reduce: false}, keys); + var resp = db.view("test/summate", {reduce: false}, keys); + T(resp.rows.length == 5); + + resp = db.view("test/summate", {reduce: false, keys: keys}, null); T(resp.rows.length == 5); // Check that limiting by startkey_docid and endkey_docid get applied @@ -96,34 +130,66 @@ couchTests.view_multi_key_design = function(debug) { T(curr[i].value == exp_val[i]); } + curr = db.view("test/multi_emit", {startkey_docid: 21, endkey_docid: 23, keys: [0, 2]}, null).rows; + T(curr.length == 6); + for( var i = 0 ; i < 6 ; i++) + { + T(curr[i].key == exp_key[i]); + T(curr[i].value == exp_val[i]); + } + // Check limit works curr = db.view("test/all_docs", {limit: 1}, keys).rows; T(curr.length == 1); T(curr[0].key == 10); + curr = db.view("test/all_docs", {limit: 1, keys: keys}, null).rows; + T(curr.length == 1); + T(curr[0].key == 10); + // Check offset works curr = db.view("test/multi_emit", {skip: 1}, [0]).rows; T(curr.length == 99); T(curr[0].value == 1); + curr = db.view("test/multi_emit", {skip: 1, keys: [0]}, null).rows; + T(curr.length == 99); + T(curr[0].value == 1); + // Check that dir works curr = db.view("test/multi_emit", {descending: "true"}, [1]).rows; T(curr.length == 100); T(curr[0].value == 99); T(curr[99].value == 0); + curr = db.view("test/multi_emit", {descending: "true", keys: [1]}, null).rows; + T(curr.length == 100); + T(curr[0].value == 99); + T(curr[99].value == 0); + // Check a couple combinations curr = db.view("test/multi_emit", {descending: "true", skip: 3, limit: 2}, [2]).rows; T(curr.length, 2); T(curr[0].value == 96); T(curr[1].value == 95); + curr = db.view("test/multi_emit", {descending: "true", skip: 3, limit: 2, keys: [2]}, null).rows; + T(curr.length, 2); + T(curr[0].value == 96); + T(curr[1].value == 95); + curr = db.view("test/multi_emit", {skip: 2, limit: 3, startkey_docid: "13"}, [0]).rows; T(curr.length == 3); T(curr[0].value == 15); T(curr[1].value == 16); T(curr[2].value == 17); + curr = db.view("test/multi_emit", {skip: 2, limit: 3, startkey_docid: "13", keys: [0]}, null).rows; + T(curr.length == 3); + T(curr[0].value == 15); + T(curr[1].value == 16); + T(curr[2].value == 17); + curr = db.view("test/multi_emit", {skip: 1, limit: 5, startkey_docid: "25", endkey_docid: "27"}, [1]).rows; T(curr.length == 2); @@ -131,8 +197,20 @@ couchTests.view_multi_key_design = function(debug) { T(curr[1].value == 27); curr = db.view("test/multi_emit", + {skip: 1, limit: 5, startkey_docid: "25", endkey_docid: "27", keys: [1]}, null).rows; + T(curr.length == 2); + T(curr[0].value == 26); + T(curr[1].value == 27); + + curr = db.view("test/multi_emit", {skip: 1, limit: 5, startkey_docid: "28", endkey_docid: "26", descending: "true"}, [1]).rows; T(curr.length == 2); T(curr[0].value == 27); T(curr[1].value == 26); + + curr = db.view("test/multi_emit", + {skip: 1, limit: 5, startkey_docid: "28", endkey_docid: "26", descending: "true", keys: [1]}, null).rows; + T(curr.length == 2); + T(curr[0].value == 27); + T(curr[1].value == 26); }; diff --git a/share/www/script/test/view_pagination.js b/share/www/script/test/view_pagination.js index 1af2df35..ed3a7ee1 100644 --- a/share/www/script/test/view_pagination.js +++ b/share/www/script/test/view_pagination.js @@ -19,7 +19,7 @@ couchTests.view_pagination = function(debug) { var docs = makeDocs(0, 100); db.bulkSave(docs); - var queryFun = function(doc) { emit(doc.integer, null) }; + var queryFun = function(doc) { emit(doc.integer, null); }; var i; // page through the view ascending @@ -29,13 +29,26 @@ couchTests.view_pagination = function(debug) { startkey_docid: i, limit: 10 }); - T(queryResults.rows.length == 10) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == i) + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == i); var j; for (j = 0; j < 10;j++) { T(queryResults.rows[j].key == i + j); } + + // test aliases start_key and start_key_doc_id + queryResults = db.query(queryFun, null, { + start_key: i, + start_key_doc_id: i, + limit: 10 + }); + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == i); + for (j = 0; j < 10;j++) { + T(queryResults.rows[j].key == i + j); + } } // page through the view descending @@ -46,9 +59,9 @@ couchTests.view_pagination = function(debug) { descending: true, limit: 10 }); - T(queryResults.rows.length == 10) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == docs.length - i - 1) + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == docs.length - i - 1); var j; for (j = 0; j < 10; j++) { T(queryResults.rows[j].key == i - j); @@ -63,60 +76,72 @@ couchTests.view_pagination = function(debug) { descending: false, limit: 10 }); - T(queryResults.rows.length == 10) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == i) + T(queryResults.rows.length == 10); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == i); var j; for (j = 0; j < 10;j++) { T(queryResults.rows[j].key == i + j); } } + function testEndkeyDocId(queryResults) { + T(queryResults.rows.length == 35); + T(queryResults.total_rows == docs.length); + T(queryResults.offset == 1); + T(queryResults.rows[0].id == "1"); + T(queryResults.rows[1].id == "10"); + T(queryResults.rows[2].id == "11"); + T(queryResults.rows[3].id == "12"); + T(queryResults.rows[4].id == "13"); + T(queryResults.rows[5].id == "14"); + T(queryResults.rows[6].id == "15"); + T(queryResults.rows[7].id == "16"); + T(queryResults.rows[8].id == "17"); + T(queryResults.rows[9].id == "18"); + T(queryResults.rows[10].id == "19"); + T(queryResults.rows[11].id == "2"); + T(queryResults.rows[12].id == "20"); + T(queryResults.rows[13].id == "21"); + T(queryResults.rows[14].id == "22"); + T(queryResults.rows[15].id == "23"); + T(queryResults.rows[16].id == "24"); + T(queryResults.rows[17].id == "25"); + T(queryResults.rows[18].id == "26"); + T(queryResults.rows[19].id == "27"); + T(queryResults.rows[20].id == "28"); + T(queryResults.rows[21].id == "29"); + T(queryResults.rows[22].id == "3"); + T(queryResults.rows[23].id == "30"); + T(queryResults.rows[24].id == "31"); + T(queryResults.rows[25].id == "32"); + T(queryResults.rows[26].id == "33"); + T(queryResults.rows[27].id == "34"); + T(queryResults.rows[28].id == "35"); + T(queryResults.rows[29].id == "36"); + T(queryResults.rows[30].id == "37"); + T(queryResults.rows[31].id == "38"); + T(queryResults.rows[32].id == "39"); + T(queryResults.rows[33].id == "4"); + T(queryResults.rows[34].id == "40"); + } + // test endkey_docid - var queryResults = db.query(function(doc) { emit(null, null);}, null, { + var queryResults = db.query(function(doc) { emit(null, null); }, null, { startkey: null, startkey_docid: 1, endkey: null, endkey_docid: 40 }); + testEndkeyDocId(queryResults); - T(queryResults.rows.length == 35) - T(queryResults.total_rows == docs.length) - T(queryResults.offset == 1) - T(queryResults.rows[0].id == "1"); - T(queryResults.rows[1].id == "10"); - T(queryResults.rows[2].id == "11"); - T(queryResults.rows[3].id == "12"); - T(queryResults.rows[4].id == "13"); - T(queryResults.rows[5].id == "14"); - T(queryResults.rows[6].id == "15"); - T(queryResults.rows[7].id == "16"); - T(queryResults.rows[8].id == "17"); - T(queryResults.rows[9].id == "18"); - T(queryResults.rows[10].id == "19"); - T(queryResults.rows[11].id == "2"); - T(queryResults.rows[12].id == "20"); - T(queryResults.rows[13].id == "21"); - T(queryResults.rows[14].id == "22"); - T(queryResults.rows[15].id == "23"); - T(queryResults.rows[16].id == "24"); - T(queryResults.rows[17].id == "25"); - T(queryResults.rows[18].id == "26"); - T(queryResults.rows[19].id == "27"); - T(queryResults.rows[20].id == "28"); - T(queryResults.rows[21].id == "29"); - T(queryResults.rows[22].id == "3"); - T(queryResults.rows[23].id == "30"); - T(queryResults.rows[24].id == "31"); - T(queryResults.rows[25].id == "32"); - T(queryResults.rows[26].id == "33"); - T(queryResults.rows[27].id == "34"); - T(queryResults.rows[28].id == "35"); - T(queryResults.rows[29].id == "36"); - T(queryResults.rows[30].id == "37"); - T(queryResults.rows[31].id == "38"); - T(queryResults.rows[32].id == "39"); - T(queryResults.rows[33].id == "4"); - T(queryResults.rows[34].id == "40"); + // test aliases end_key_doc_id and end_key + queryResults = db.query(function(doc) { emit(null, null); }, null, { + start_key: null, + start_key_doc_id: 1, + end_key: null, + end_key_doc_id: 40 + }); + testEndkeyDocId(queryResults); }; diff --git a/share/www/script/test/view_sandboxing.js b/share/www/script/test/view_sandboxing.js index 9f893b28..02951d9f 100644 --- a/share/www/script/test/view_sandboxing.js +++ b/share/www/script/test/view_sandboxing.js @@ -42,11 +42,99 @@ couchTests.view_sandboxing = function(debug) { // make sure that a view cannot access the map_funs array defined used by // the view server - var results = db.query(function(doc) { map_funs.push(1); emit(null, doc) }); + var results = db.query(function(doc) { map_funs.push(1); emit(null, doc); }); T(results.total_rows == 0); // make sure that a view cannot access the map_results array defined used by // the view server - var results = db.query(function(doc) { map_results.push(1); emit(null, doc) }); + var results = db.query(function(doc) { map_results.push(1); emit(null, doc); }); T(results.total_rows == 0); + + // test for COUCHDB-925 + // altering 'doc' variable in map function affects other map functions + var ddoc = { + _id: "_design/foobar", + language: "javascript", + views: { + view1: { + map: + (function(doc) { + if (doc.values) { + doc.values = [666]; + } + if (doc.tags) { + doc.tags.push("qwerty"); + } + if (doc.tokens) { + doc.tokens["c"] = 3; + } + }).toString() + }, + view2: { + map: + (function(doc) { + if (doc.values) { + emit(doc._id, doc.values); + } + if (doc.tags) { + emit(doc._id, doc.tags); + } + if (doc.tokens) { + emit(doc._id, doc.tokens); + } + }).toString() + } + } + }; + var doc1 = { + _id: "doc1", + values: [1, 2, 3] + }; + var doc2 = { + _id: "doc2", + tags: ["foo", "bar"], + tokens: {a: 1, b: 2} + }; + + db.deleteDb(); + db.createDb(); + T(db.save(ddoc).ok); + T(db.save(doc1).ok); + T(db.save(doc2).ok); + + var view1Results = db.view( + "foobar/view1", {bypass_cache: Math.round(Math.random() * 1000)}); + var view2Results = db.view( + "foobar/view2", {bypass_cache: Math.round(Math.random() * 1000)}); + + TEquals(0, view1Results.rows.length, "view1 has 0 rows"); + TEquals(3, view2Results.rows.length, "view2 has 3 rows"); + + TEquals(doc1._id, view2Results.rows[0].key); + TEquals(doc2._id, view2Results.rows[1].key); + TEquals(doc2._id, view2Results.rows[2].key); + + // https://bugzilla.mozilla.org/show_bug.cgi?id=449657 + TEquals(3, view2Results.rows[0].value.length, + "Warning: installed SpiderMonkey version doesn't allow sealing of arrays"); + if (view2Results.rows[0].value.length === 3) { + TEquals(1, view2Results.rows[0].value[0]); + TEquals(2, view2Results.rows[0].value[1]); + TEquals(3, view2Results.rows[0].value[2]); + } + + TEquals(1, view2Results.rows[1].value["a"]); + TEquals(2, view2Results.rows[1].value["b"]); + TEquals('undefined', typeof view2Results.rows[1].value["c"], + "doc2.tokens object was not sealed"); + + TEquals(2, view2Results.rows[2].value.length, + "Warning: installed SpiderMonkey version doesn't allow sealing of arrays"); + if (view2Results.rows[2].value.length === 2) { + TEquals("foo", view2Results.rows[2].value[0]); + TEquals("bar", view2Results.rows[2].value[1]); + } + + // cleanup + db.deleteDb(); }; diff --git a/share/www/script/test/view_update_seq.js b/share/www/script/test/view_update_seq.js new file mode 100644 index 00000000..69b8c42d --- /dev/null +++ b/share/www/script/test/view_update_seq.js @@ -0,0 +1,106 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.view_update_seq = function(debug) { + var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"true"}); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + T(db.info().update_seq == 0); + + var resp = db.allDocs({update_seq:true}); + + T(resp.rows.length == 0); + T(resp.update_seq == 0); + + var designDoc = { + _id:"_design/test", + language: "javascript", + views: { + all_docs: { + map: "function(doc) { emit(doc.integer, doc.string) }" + }, + summate: { + map:"function (doc) {emit(doc.integer, doc.integer)};", + reduce:"function (keys, values) { return sum(values); };" + } + } + }; + T(db.save(designDoc).ok); + + T(db.info().update_seq == 1); + + resp = db.allDocs({update_seq:true}); + + T(resp.rows.length == 1); + T(resp.update_seq == 1); + + var docs = makeDocs(0, 100); + db.bulkSave(docs); + + resp = db.allDocs({limit: 1}); + T(resp.rows.length == 1); + T(!resp.update_seq, "all docs"); + + resp = db.allDocs({limit: 1, update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + resp = db.view('test/all_docs', {limit: 1, update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + resp = db.view('test/all_docs', {limit: 1, update_seq:false}); + T(resp.rows.length == 1); + T(!resp.update_seq, "view"); + + resp = db.view('test/summate', {update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + db.save({"id":"0"}); + resp = db.view('test/all_docs', {limit: 1,stale: "ok", update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + db.save({"id":"00"}); + resp = db.view('test/all_docs', + {limit: 1, stale: "update_after", update_seq: true}); + T(resp.rows.length == 1); + T(resp.update_seq == 101); + + // wait 5 seconds for the next assertions to pass in very slow machines + var t0 = new Date(), t1; + do { + CouchDB.request("GET", "/"); + t1 = new Date(); + } while ((t1 - t0) < 5000); + + resp = db.view('test/all_docs', {limit: 1, stale: "ok", update_seq: true}); + T(resp.rows.length == 1); + T(resp.update_seq == 103); + + resp = db.view('test/all_docs', {limit: 1, update_seq:true}); + T(resp.rows.length == 1); + T(resp.update_seq == 103); + + resp = db.view('test/all_docs',{update_seq:true},["0","1"]); + T(resp.update_seq == 103); + + resp = db.view('test/all_docs',{update_seq:true},["0","1"]); + T(resp.update_seq == 103); + + resp = db.view('test/summate',{group:true, update_seq:true},["0","1"]); + T(resp.update_seq == 103); + +}; diff --git a/share/www/script/test/view_xml.js b/share/www/script/test/view_xml.js index 451fb6a8..3403b47c 100644 --- a/share/www/script/test/view_xml.js +++ b/share/www/script/test/view_xml.js @@ -22,7 +22,7 @@ couchTests.view_xml = function(debug) { var results = db.query( "function(doc) {\n" + " var xml = new XML(doc.content);\n" + - " emit(xml.title.text(), null);\n" + + " emit(xml.title.text().toXMLString(), null);\n" + "}"); T(results.total_rows == 2); T(results.rows[0].key == "Testing E4X"); @@ -31,7 +31,7 @@ couchTests.view_xml = function(debug) { var results = db.query( "function(doc) {\n" + " var xml = new XML(doc.content);\n" + - " emit(xml.title.@id, null);\n" + + " emit(xml.title.@id.toXMLString(), null);\n" + "}"); T(results.total_rows == 2); T(results.rows[0].key == "e4x"); diff --git a/share/www/session.html b/share/www/session.html new file mode 100644 index 00000000..0ebd943d --- /dev/null +++ b/share/www/session.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<!-- + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +--> +<html lang="en"> + <head> + <title>Session</title> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> + <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> + <script src="script/json2.js"></script> + <script src="script/sha1.js"></script> + <script src="script/jquery.js?1.4.2"></script> + <script src="script/jquery.couch.js?0.11.0"></script> + <script src="script/jquery.dialog.js?0.11.0"></script> + <script src="script/futon.js?0.11.0"></script> + <script src="script/futon.browse.js?0.11.0"></script> + <script src="script/futon.format.js?0.11.0"></script> + <script> + $(function() { + var ret, reason, q = window.location.search, qps = q.split("&"); + $.map(qps, function(qp) { + var m = qp.match(/return=(.*)/); + if (m) { + ret = decodeURIComponent(m[1]); + } + m = qp.match(/reason=(.*)/); + if (m) { + reason = $.futon.escape(decodeURIComponent(m[1])); + } + }); + if (reason) { + $("#aboutSession").append('<p>The application says: <em>'+reason+'</em></p>'); + } + if (ret) { + $("#aboutSession").append($('<p>Once you are logged in, click this link to return to your application: </p>').append($("<a></a>").attr("href", ret).text(ret))); + // todo this needs to look different if you are already logged in + // a note about you are logged in but you can't access this + } + // do the sidebar but in the middle without the sidebar + $.futon.storage.set("sidebar", "hidden"); + setTimeout(function() { + var ctx = $$("#userCtx").userCtx; + $.futon.storage.set("sidebar", "show"); + if (ctx && ctx.name) { + $("#aboutSession").append("<p>It looks like you are logged in, maybe you don't have access to that url.</p>"); + } + },100); + }); + </script> + </head> + <body> + <div id="wrap"> + <h1><a href="index.html">Overview</a> + <strong>Session</strong></h1> + <div id="content"> + <h2>Establish or Modify Your Session</h2> + <div id="loginSignup"> + <div id="aboutSession"></div> + <span id="userCtx"> + <span class="loggedout"> + <a href="#" class="signup">Signup</a> or <a href="#" class="login">Login</a> + </span> + <span class="loggedin"> + Welcome <a class="name">?</a>! + <br/> + <a href="#" class="logout">Logout</a> + </span> + <span class="loggedinadmin"> + Welcome <a class="name">?</a>! + <br/> + <a href="#" class="createadmin">Setup more admins</a> or + <a href="#" class="logout">Logout</a> + </span> + <span class="adminparty"> + Welcome to Admin Party! + <br/> + Everyone is admin. <a href="#" class="createadmin">Fix this</a> + </span> + </span> + </div> + </div> + + </div> + </body> +</html> diff --git a/share/www/spec/couch_js_class_methods_spec.js b/share/www/spec/couch_js_class_methods_spec.js new file mode 100644 index 00000000..7eac2348 --- /dev/null +++ b/share/www/spec/couch_js_class_methods_spec.js @@ -0,0 +1,401 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for couch.js lines 313-470 + +describe 'CouchDB class' + describe 'session stuff' + before + useTestUserDb(); + end + + after + useOldUserDb(); + end + + before_each + userDoc = users_db.save(CouchDB.prepareUserDoc({name: "Gaius Baltar", roles: ["president"]}, "secretpass")); + end + + after_each + users_db.deleteDoc({_id : userDoc.id, _rev : userDoc.rev}) + end + + describe '.login' + it 'should return ok true' + CouchDB.login("Gaius Baltar", "secretpass").ok.should.be_true + end + + it 'should return the name of the logged in user' + CouchDB.login("Gaius Baltar", "secretpass").name.should.eql "Gaius Baltar" + end + + it 'should return the roles of the logged in user' + CouchDB.login("Gaius Baltar", "secretpass").roles.should.eql ["president"] + end + + it 'should post _session' + CouchDB.should.receive("request", "once").with_args("POST", "/_session") + CouchDB.login("Gaius Baltar", "secretpass"); + end + + it 'should create a session' + CouchDB.login("Gaius Baltar", "secretpass"); + CouchDB.session().userCtx.name.should.eql "Gaius Baltar" + end + end + + describe '.logout' + before_each + CouchDB.login("Gaius Baltar", "secretpass"); + end + + it 'should return ok true' + CouchDB.logout().ok.should.be_true + end + + it 'should delete _session' + CouchDB.should.receive("request", "once").with_args("DELETE", "/_session") + CouchDB.logout(); + end + + it 'should result in an invalid session' + CouchDB.logout(); + CouchDB.session().name.should.be_null + end + end + + describe '.session' + before_each + CouchDB.login("Gaius Baltar", "secretpass"); + end + + it 'should return ok true' + CouchDB.session().ok.should.be_true + end + + it 'should return the users name' + CouchDB.session().userCtx.name.should.eql "Gaius Baltar" + end + + it 'should return the users roles' + CouchDB.session().userCtx.roles.should.eql ["president"] + end + + it 'should return the name of the authentication db' + CouchDB.session().info.authentication_db.should.eql "spec_users_db" + end + + it 'should return the active authentication handler' + CouchDB.session().info.authenticated.should.eql "cookie" + end + end + end + + describe 'db stuff' + before_each + db = new CouchDB("spec_db", {"X-Couch-Full-Commit":"false"}); + db.createDb(); + end + + after_each + db.deleteDb(); + end + + describe '.prepareUserDoc' + before_each + userDoc = CouchDB.prepareUserDoc({name: "Laura Roslin"}, "secretpass"); + end + + it 'should return the users name' + userDoc.name.should.eql "Laura Roslin" + end + + it 'should prefix the id with the CouchDB user_prefix' + userDoc._id.should.eql "org.couchdb.user:Laura Roslin" + end + + it 'should return the users roles' + var userDocWithRoles = CouchDB.prepareUserDoc({name: "William Adama", roles: ["admiral", "commander"]}, "secretpass") + userDocWithRoles.roles.should.eql ["admiral", "commander"] + end + + it 'should return the hashed password' + userDoc.password_sha.length.should.be_at_least 30 + userDoc.password_sha.should.be_a String + end + end + + describe '.allDbs' + it 'should get _all_dbs' + CouchDB.should.receive("request", "once").with_args("GET", "/_all_dbs"); + CouchDB.allDbs(); + end + + it 'should return an array that includes a created database' + temp_db = new CouchDB("temp_spec_db", {"X-Couch-Full-Commit":"false"}); + temp_db.createDb(); + CouchDB.allDbs().should.include("temp_spec_db"); + temp_db.deleteDb(); + end + + it 'should return an array that does not include a database that does not exist' + CouchDB.allDbs().should.not.include("not_existing_temp_spec_db"); + end + end + + describe '.allDesignDocs' + it 'should return the total number of documents' + CouchDB.allDesignDocs().spec_db.total_rows.should.eql 0 + db.save({'type':'battlestar', 'name':'galactica'}); + CouchDB.allDesignDocs().spec_db.total_rows.should.eql 1 + end + + it 'should return undefined when the db does not exist' + CouchDB.allDesignDocs().non_existing_db.should.be_undefined + end + + it 'should return no documents when there are no design documents' + CouchDB.allDesignDocs().spec_db.rows.should.eql [] + end + + it 'should return all design documents' + var designDoc = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc); }" + } + }, + "_id" : "_design/spec_db" + }; + db.save(designDoc); + + var allDesignDocs = CouchDB.allDesignDocs(); + allDesignDocs.spec_db.rows[0].id.should.eql "_design/spec_db" + allDesignDocs.spec_db.rows[0].key.should.eql "_design/spec_db" + allDesignDocs.spec_db.rows[0].value.rev.length.should.be_at_least 30 + end + end + + describe '.getVersion' + it 'should get the CouchDB version' + CouchDB.should.receive("request", "once").with_args("GET", "/") + CouchDB.getVersion(); + end + + it 'should return the CouchDB version' + CouchDB.getVersion().should_match /^\d\d?\.\d\d?\.\d\d?.*/ + end + end + + describe '.replicate' + before_each + db2 = new CouchDB("spec_db_2", {"X-Couch-Full-Commit":"false"}); + db2.createDb(); + host = window.location.protocol + "//" + window.location.host ; + end + + after_each + db2.deleteDb(); + end + + it 'should return no_changes true when there are no changes between the dbs' + CouchDB.replicate(host + db.uri, host + db2.uri).no_changes.should.be_true + end + + it 'should return the session ID' + db.save({'type':'battlestar', 'name':'galactica'}); + CouchDB.replicate(host + db.uri, host + db2.uri).session_id.length.should.be_at_least 30 + end + + it 'should return source_last_seq' + db.save({'type':'battlestar', 'name':'galactica'}); + db.save({'type':'battlestar', 'name':'pegasus'}); + + CouchDB.replicate(host + db.uri, host + db2.uri).source_last_seq.should.eql 2 + end + + it 'should return the replication history' + db.save({'type':'battlestar', 'name':'galactica'}); + db.save({'type':'battlestar', 'name':'pegasus'}); + + var result = CouchDB.replicate(host + db.uri, host + db2.uri); + result.history[0].docs_written.should.eql 2 + result.history[0].start_last_seq.should.eql 0 + end + + it 'should pass through replication options' + db.save({'type':'battlestar', 'name':'galactica'}); + db2.deleteDb(); + -{CouchDB.replicate(host + db.uri, host + db2.uri)}.should.throw_error + var result = CouchDB.replicate(host + db.uri, host + db2.uri, {"body" : {"create_target":true}}); + + result.ok.should.eql true + result.history[0].docs_written.should.eql 1 + db2.info().db_name.should.eql "spec_db_2" + end + end + + describe '.newXhr' + it 'should return a XMLHTTPRequest' + CouchDB.newXhr().should.have_prop 'readyState' + CouchDB.newXhr().should.have_prop 'responseText' + CouchDB.newXhr().should.have_prop 'status' + end + end + + describe '.request' + it 'should return a XMLHttpRequest' + var req = CouchDB.request("GET", '/'); + req.should.include "readyState" + req.should.include "responseText" + req.should.include "statusText" + end + + it 'should pass through the options headers' + var xhr = CouchDB.newXhr(); + stub(CouchDB, 'newXhr').and_return(xhr); + + xhr.should.receive("setRequestHeader", "once").with_args("X-Couch-Full-Commit", "true") + CouchDB.request("GET", "/", {'headers': {"X-Couch-Full-Commit":"true"}}); + end + + it 'should pass through the options body' + var xhr = CouchDB.newXhr(); + stub(CouchDB, 'newXhr').and_return(xhr); + + xhr.should.receive("send", "once").with_args({"body_key":"body_value"}) + CouchDB.request("GET", "/", {'body': {"body_key":"body_value"}}); + end + + it 'should prepend the urlPrefix to the uri' + var oldPrefix = CouchDB.urlPrefix; + CouchDB.urlPrefix = "/_utils"; + + var xhr = CouchDB.newXhr(); + stub(CouchDB, 'newXhr').and_return(xhr); + + xhr.should.receive("open", "once").with_args("GET", "/_utils/", false) + CouchDB.request("GET", "/", {'headers': {"X-Couch-Full-Commit":"true"}}); + + CouchDB.urlPrefix = oldPrefix; + end + end + + describe '.requestStats' + it 'should get the stats for specified module and key' + var stats = CouchDB.requestStats('couchdb', 'open_databases', null); + stats.description.should.eql 'number of open databases' + stats.current.should.be_a Number + end + + it 'should add flush true to the request when there is a test argument' + CouchDB.should.receive("request", "once").with_args("GET", "/_stats/httpd/requests?flush=true") + CouchDB.requestStats('httpd', 'requests', 'test'); + end + + it 'should still work when there is a test argument' + var stats = CouchDB.requestStats('httpd_status_codes', '200', 'test'); + stats.description.should.eql 'number of HTTP 200 OK responses' + stats.sum.should.be_a Number + end + end + + describe '.newUuids' + after_each + CouchDB.uuids_cache = []; + end + + it 'should return the specified amount of uuids' + var uuids = CouchDB.newUuids(45); + uuids.should.have_length 45 + end + + it 'should return an array with uuids' + var uuids = CouchDB.newUuids(1); + uuids[0].should.be_a String + uuids[0].should.have_length 32 + end + + it 'should leave the uuids_cache with 100 uuids when theres no buffer size specified' + CouchDB.newUuids(23); + CouchDB.uuids_cache.should.have_length 100 + end + + it 'should leave the uuids_cache with the specified buffer size' + CouchDB.newUuids(23, 150); + CouchDB.uuids_cache.should.have_length 150 + end + + it 'should get the uuids from the uuids_cache when there are enough uuids in there' + CouchDB.newUuids(10); + CouchDB.newUuids(25); + CouchDB.uuids_cache.should.have_length 75 + end + + it 'should create new uuids and add as many as specified to the uuids_cache when there are not enough uuids in the cache' + CouchDB.newUuids(10); + CouchDB.newUuids(125, 60); + CouchDB.uuids_cache.should.have_length 160 + end + end + + describe '.maybeThrowError' + it 'should throw an error when the request has status 404' + var req = CouchDB.request("GET", "/nonexisting_db"); + -{CouchDB.maybeThrowError(req)}.should.throw_error + end + + it 'should throw an error when the request has status 412' + var req = CouchDB.request("PUT", "/spec_db"); + -{CouchDB.maybeThrowError(req)}.should.throw_error + end + + it 'should throw an error when the request has status 405' + var req = CouchDB.request("DELETE", "/_utils"); + -{CouchDB.maybeThrowError(req)}.should.throw_error + end + + it 'should throw the responseText of the request' + var req = CouchDB.request("GET", "/nonexisting_db"); + try { + CouchDB.maybeThrowError(req) + } catch(e) { + e.error.should.eql JSON.parse(req.responseText).error + e.reason.should.eql JSON.parse(req.responseText).reason + } + end + + it 'should throw an unknown error when the responseText is invalid json' + mock_request().and_return("invalid json...", "application/json", 404, {}) + try { + CouchDB.maybeThrowError(CouchDB.newXhr()) + } catch(e) { + e.error.should.eql "unknown" + e.reason.should.eql "invalid json..." + } + end + end + + describe '.params' + it 'should turn a json object into a http params string' + var params = CouchDB.params({"president":"laura", "cag":"lee"}) + params.should.eql "president=laura&cag=lee" + end + + it 'should return a blank string when the object is empty' + var params = CouchDB.params({}) + params.should.eql "" + end + end + end +end
\ No newline at end of file diff --git a/share/www/spec/couch_js_instance_methods_1_spec.js b/share/www/spec/couch_js_instance_methods_1_spec.js new file mode 100644 index 00000000..7f23bd2c --- /dev/null +++ b/share/www/spec/couch_js_instance_methods_1_spec.js @@ -0,0 +1,311 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for couch.js lines 1-130 + +describe 'CouchDB instance' + before_each + db = new CouchDB("spec_db", {"X-Couch-Full-Commit":"false"}); + end + + describe '.request' + before_each + db.createDb(); + end + + after_each + db.deleteDb(); + end + + it 'should return a XMLHttpRequest' + var req = db.request("GET", "/spec_db"); + req.should.include "readyState" + req.should.include "responseText" + req.should.include "statusText" + // in Safari a XMLHttpRequest is actually a XMLHttpRequestConstructor, + // otherwise we could just do: + // req.should.be_a XMLHttpRequest + end + + it 'should set the options the CouchDB instance has got as httpHeaders' + CouchDB.should.receive("request", "once").with_args("GET", "/spec_db", {headers: {"X-Couch-Full-Commit": "false"}}) + db.request("GET", "/spec_db"); + end + + it 'should pass through the options' + CouchDB.should.receive("request", "once").with_args("GET", "/spec_db", {"X-Couch-Persist": "true", headers: {"X-Couch-Full-Commit": "false"}}) + db.request("GET", "/spec_db", {"X-Couch-Persist":"true"}); + end + end + + describe '.createDb' + after_each + db.deleteDb(); + end + + it 'should create the db' + db.createDb(); + db.last_req.status.should.eql 201 + end + + it 'should return the ok true' + db.createDb().should.eql {"ok" : true} + end + + it 'should result in a created db' + db.createDb(); + try{ + db.createDb(); + } catch(e) { + e.error.should.eql "file_exists" + } + end + + it 'should have create a db with update sequence 0' + db.createDb(); + db.info().update_seq.should.eql 0 + end + end + + describe '.deleteDb' + before_each + db.createDb(); + end + + it 'should delete the db' + db.deleteDb(); + db.last_req.status.should.eql 200 + end + + it 'should return the responseText of the request' + db.deleteDb().should.eql {"ok" : true} + end + + it 'should result in a deleted db' + db.deleteDb(); + db.deleteDb(); + db.last_req.status.should.eql 404 + end + end + + describe 'document methods' + before_each + doc = {"Name" : "Kara Thrace", "Callsign" : "Starbuck"}; + db.createDb(); + end + + after_each + db.deleteDb(); + end + + describe '.save' + it 'should save the document' + db.save(doc); + db.last_req.status.should.eql 201 + end + + it 'should return ok true' + db.save(doc).ok.should.be_true + end + + it 'should return ID and revision of the document' + var response = db.save(doc); + response.id.should.be_a String + response.id.should.have_length 32 + response.rev.should.be_a String + response.rev.length.should.be_at_least 30 + end + + it 'should result in a saved document with generated ID' + var response = db.save(doc); + var saved_doc = db.open(response.id); + saved_doc.Name.should.eql "Kara Thrace" + saved_doc.Callsign.should.eql "Starbuck" + end + + it 'should save the document with the specified ID' + doc._id = "123"; + var response = db.save(doc); + response.id.should.eql "123" + end + + it 'should pass through the options' + doc._id = "123"; + CouchDB.should.receive("request", "once").with_args("PUT", "/spec_db/123?batch=ok") + db.save(doc, {"batch" : "ok"}); + end + end + + describe '.open' + before_each + doc._id = "123"; + db.save(doc); + end + + it 'should open the document' + db.open("123").should.eql doc + end + + it 'should return null when there is no document with the given ID' + db.open("non_existing").should.be_null + end + + it 'should pass through the options' + CouchDB.should.receive("request", "once").with_args("GET", "/spec_db/123?revs=true") + db.open("123", {"revs" : "true"}); + end + end + + describe '.deleteDoc' + before_each + doc._id = "123"; + saved_doc = db.save(doc); + delete_response = db.deleteDoc({_id : "123", _rev : saved_doc.rev}); + delete_last_req = db.last_req; + db.open("123"); + end + + it 'should send a successful request' + delete_last_req.status.should.eql 200 + end + + it 'should result in a deleted document' + db.open("123").should.be_null + end + + it 'should return ok true, the ID and the revision of the deleted document' + delete_response.ok.should.be_true + delete_response.id.should.eql "123" + delete_response.rev.should.be_a String + delete_response.rev.length.should.be_at_least 30 + end + + it 'should mark the document as deleted' + var responseText = db.request("GET", "/spec_db/123").responseText; + JSON.parse(responseText).should.eql {"error":"not_found","reason":"deleted"} + end + + it 'should record the revision in the deleted document' + var responseText = db.request("GET", "/spec_db/123?rev=" + delete_response.rev).responseText; + var deleted_doc = JSON.parse(responseText); + deleted_doc._rev.should.eql delete_response.rev + deleted_doc._id.should.eql delete_response.id + deleted_doc._deleted.should.be_true + end + end + + describe '.deleteDocAttachment' + before_each + doc._id = "123"; + doc._attachments = { + "friend.txt" : { + "content_type": "text\/plain", + // base64 encoded + "data": "TGVlIEFkYW1hIGlzIGEgZm9ybWVyIENvbG9uaWFsIEZsZWV0IFJlc2VydmUgb2ZmaWNlci4=" + } + }; + saved_doc = db.save(doc); + end + + it 'should be executed on a document with attachment' + db.open("123")._attachments.should.include "friend.txt" + db.open("123")._attachments["friend.txt"].stub.should.be_true + end + + describe 'after delete' + before_each + delete_response = db.deleteDocAttachment({_id : "123", _rev : saved_doc.rev}, "friend.txt"); + db.open("123"); + end + + it 'should send a successful request' + db.last_req.status.should.eql 200 + end + + it 'should leave the document untouched' + db.open("123").Callsign.should.eql "Starbuck" + end + + it 'should result in a deleted document attachment' + db.open("123").should.not.include "_attachments" + end + + it 'should record the revision in the document whose attachment has been deleted' + var responseText = db.request("GET", "/spec_db/123?rev=" + delete_response.rev).responseText; + var deleted_doc = JSON.parse(responseText); + deleted_doc._rev.should.eql delete_response.rev + deleted_doc._id.should.eql delete_response.id + end + + it 'should return ok true, the ID and the revision of the document whose attachment has been deleted' + delete_response.ok.should.be_true + delete_response.id.should.eql "123" + delete_response.should.have_property 'rev' + end + end + end + + describe '.bulkSave' + before_each + doc = {"Name" : "Kara Thrace", "Callsign" : "Starbuck"}; + doc2 = {"Name" : "Karl C. Agathon", "Callsign" : "Helo"}; + doc3 = {"Name" : "Sharon Valerii", "Callsign" : "Boomer"}; + docs = [doc, doc2, doc3]; + end + + it 'should save the documents' + db.bulkSave(docs); + db.last_req.status.should.eql 201 + end + + it 'should return ID and revision of the documents' + var response = db.bulkSave(docs); + response[0].id.should.be_a String + response[0].id.should.have_length 32 + response[0].rev.should.be_a String + response[0].rev.length.should.be_at_least 30 + response[1].id.should.be_a String + response[1].id.should.have_length 32 + response[1].rev.should.be_a String + response[1].rev.length.should.be_at_least 30 + response[2].id.should.be_a String + response[2].id.should.have_length 32 + response[2].rev.should.be_a String + response[2].rev.length.should.be_at_least 30 + end + + it 'should result in saved documents' + var response = db.bulkSave(docs); + db.open(response[0].id).Name.should.eql "Kara Thrace" + db.open(response[1].id).Name.should.eql "Karl C. Agathon" + db.open(response[2].id).Name.should.eql "Sharon Valerii" + end + + it 'should save the document with specified IDs' + doc._id = "123"; + doc2._id = "456"; + docs = [doc, doc2, doc3]; + var response = db.bulkSave(docs); + response[0].id.should.eql "123" + response[1].id.should.eql "456" + response[2].id.should.have_length 32 + end + + it 'should pass through the options' + doc._id = "123"; + docs = [doc]; + CouchDB.should.receive("request", "once").with_args("POST", "/spec_db/_bulk_docs", {body: '{"docs":[{"Name":"Kara Thrace","Callsign":"Starbuck","_id":"123"}],"batch":"ok"}'}) + db.bulkSave(docs, {"batch" : "ok"}); + end + end + end +end
\ No newline at end of file diff --git a/share/www/spec/couch_js_instance_methods_2_spec.js b/share/www/spec/couch_js_instance_methods_2_spec.js new file mode 100644 index 00000000..76df6368 --- /dev/null +++ b/share/www/spec/couch_js_instance_methods_2_spec.js @@ -0,0 +1,246 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for couch.js lines 132-199 + +describe 'CouchDB instance' + before_each + db = new CouchDB("spec_db", {"X-Couch-Full-Commit":"false"}); + db.createDb(); + end + + after_each + db.deleteDb(); + end + + describe '.ensureFullCommit' + it 'should return ok true' + db.ensureFullCommit().ok.should.be_true + end + + it 'should return the instance start time' + db.ensureFullCommit().instance_start_time.should.have_length 16 + end + + it 'should post _ensure_full_commit to the db' + db.should.receive("request", "once").with_args("POST", "/spec_db/_ensure_full_commit") + db.ensureFullCommit(); + end + end + + describe '.query' + before_each + db.save({"Name" : "Cally Tyrol", "job" : "deckhand", "_id" : "789"}); + db.save({"Name" : "Felix Gaeta", "job" : "officer", "_id" : "123"}); + db.save({"Name" : "Samuel T. Anders", "job" : "pilot", "_id" : "456"}); + map_function = "function(doc) { emit(doc._id, 1); }"; + reduce_function = "function(key, values, rereduce) { return sum(values); }"; + end + + it 'should apply the map function' + var result = db.query(map_function); + + result.rows.should.have_length 3 + result.rows[0].id.should.eql "123" + result.rows[0].key.should.eql "123" + result.rows[0].value.should.eql 1 + result.rows[1].id.should.eql "456" + result.rows[1].key.should.eql "456" + result.rows[1].value.should.eql 1 + result.rows[2].id.should.eql "789" + result.rows[2].key.should.eql "789" + result.rows[2].value.should.eql 1 + end + + it 'should apply the reduce function' + var result = db.query(map_function, reduce_function); + + result.rows.should.have_length 1 + result.rows[0].key.should.be_null + result.rows[0].value.should_eql 3 + end + + it 'should pass through the options' + var result = db.query(map_function, null, {"startkey":"456"}); + + result.rows.should.have_length 2 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.should.eql 1 + result.rows[1].id.should.eql "789" + result.rows[1].key.should.eql "789" + result.rows[1].value.should.eql 1 + end + + it 'should pass through the keys' + var result = db.query(map_function, null, {}, ["456", "123"]); + + result.rows.should.have_length 2 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.should.eql 1 + result.rows[1].id.should.eql "123" + result.rows[1].key.should.eql "123" + result.rows[1].value.should.eql 1 + end + + it 'should pass through the options and the keys' + var result = db.query(map_function, null, {"include_docs":"true"}, ["456"]); + + result.rows.should.have_length 1 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.should.eql 1 + result.rows[0].doc["job"].should.eql "pilot" + result.rows[0].doc["_rev"].length.should.be_at_least 30 + end + + it 'should apply a view in erlang also' + // when this test fails, read this: http://wiki.apache.org/couchdb/EnableErlangViews + var erlang_map = 'fun({Doc}) -> ' + + 'ID = proplists:get_value(<<"_id">>, Doc, null), ' + + 'Emit(ID, 1) ' + + 'end.'; + var result = db.query(erlang_map, null, null, null, "erlang"); + + result.rows.should.have_length 3 + result.rows[0].id.should.eql "123" + result.rows[0].key.should.eql "123" + result.rows[0].value.should.eql 1 + result.rows[1].id.should.eql "456" + result.rows[1].key.should.eql "456" + result.rows[1].value.should.eql 1 + result.rows[2].id.should.eql "789" + result.rows[2].key.should.eql "789" + result.rows[2].value.should.eql 1 + end + end + + describe '.view' + before_each + db.save({"Name" : "Cally Tyrol", "job" : "deckhand", "_id" : "789"}); + db.save({"Name" : "Felix Gaeta", "job" : "officer", "_id" : "123"}); + db.save({"Name" : "Samuel T. Anders", "job" : "pilot", "_id" : "456"}); + view = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc.Name); }" + } + }, + "_id" : "_design/spec_db" + }; + db.save(view); + end + + it 'should apply the view' + var result = db.view('spec_db/people'); + + result.rows.should.have_length 3 + result.rows[0].id.should.eql "123" + result.rows[0].key.should.eql "123" + result.rows[0].value.should.eql "Felix Gaeta" + result.rows[1].id.should.eql "456" + result.rows[1].key.should.eql "456" + result.rows[1].value.should.eql "Samuel T. Anders" + result.rows[2].id.should.eql "789" + result.rows[2].key.should.eql "789" + result.rows[2].value.should.eql "Cally Tyrol" + end + + it 'should pass through the options' + var result = db.view('spec_db/people', {"skip":"2"}); + + result.rows.should.have_length 1 + result.rows[0].id.should.eql "789" + result.rows[0].key.should.eql "789" + result.rows[0].value.should.eql "Cally Tyrol" + end + + it 'should pass through the keys' + var result = db.view('spec_db/people', {}, ["456", "123"]); + + result.rows.should.have_length 2 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.should.eql "Samuel T. Anders" + result.rows[1].id.should.eql "123" + result.rows[1].key.should.eql "123" + result.rows[1].value.should.eql "Felix Gaeta" + end + + it 'should pass through the options and the keys' + var result = db.view('spec_db/people', {"include_docs":"true"}, ["456"]); + + result.rows.should.have_length 1 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.should.eql "Samuel T. Anders" + result.rows[0].doc["job"].should.eql "pilot" + result.rows[0].doc["_rev"].length.should.be_at_least 30 + end + + it 'should return null when the view doesnt exist' + var result = db.view('spec_db/non_existing_view'); + + result.should.be_null + end + end + + describe '.info' + before_each + result = db.info(); + end + + it 'should return the name of the database' + result.db_name.should.eql "spec_db" + end + + it 'should return the number of documents' + result.doc_count.should.eql 0 + end + + it 'should return the start time of the db instance' + result.instance_start_time.should.have_length 16 + end + end + + describe '.designInfo' + before_each + designDoc = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc); }" + } + }, + "_id" : "_design/spec_db" + }; + db.save(designDoc); + result = db.designInfo("_design/spec_db"); + end + + it 'should return the database name' + result.name.should.eql "spec_db" + end + + it 'should return a views language' + result.view_index.language.should.eql "javascript" + end + + it 'should return a views update sequence' + result.view_index.update_seq.should.eql 0 + end + + it 'should return a views signature' + result.view_index.signature.should.have_length 32 + end + end +end
\ No newline at end of file diff --git a/share/www/spec/couch_js_instance_methods_3_spec.js b/share/www/spec/couch_js_instance_methods_3_spec.js new file mode 100644 index 00000000..b7464c01 --- /dev/null +++ b/share/www/spec/couch_js_instance_methods_3_spec.js @@ -0,0 +1,215 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for couch.js lines 201-265 + +describe 'CouchDB instance' + before_each + db = new CouchDB("spec_db", {"X-Couch-Full-Commit":"false"}); + db.createDb(); + end + + after_each + db.deleteDb(); + end + + describe '.allDocs' + it 'should return no docs when there arent any' + db.allDocs().total_rows.should.eql 0 + db.allDocs().rows.should.eql [] + end + + describe 'with docs' + before_each + db.save({"Name" : "Felix Gaeta", "_id" : "123"}); + db.save({"Name" : "Samuel T. Anders", "_id" : "456"}); + end + + it 'should return all docs' + var result = db.allDocs(); + + result.total_rows.should.eql 2 + result.rows.should.have_length 2 + result.rows[0].id.should.eql "123" + result.rows[0].key.should.eql "123" + result.rows[0].value.rev.length.should.be_at_least 30 + result.rows[1].id.should.eql "456" + end + + it 'should pass through the options' + var result = db.allDocs({"startkey": "123", "limit": "1"}); + + result.rows.should.have_length 1 + result.rows[0].id.should.eql "123" + end + + it 'should pass through the keys' + var result = db.allDocs({}, ["456"]); + + result.rows.should.have_length 1 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.rev.length.should.be_at_least 30 + end + + it 'should pass through the options and the keys' + var result = db.allDocs({"include_docs":"true"}, ["456"]); + + result.rows.should.have_length 1 + result.rows[0].id.should.eql "456" + result.rows[0].key.should.eql "456" + result.rows[0].value.rev.length.should.be_at_least 30 + result.rows[0].doc["Name"].should.eql "Samuel T. Anders" + result.rows[0].doc["_rev"].length.should.be_at_least 30 + end + + end + end + + describe '.designDocs' + it 'should return nothing when there arent any design docs' + db.save({"Name" : "Felix Gaeta", "_id" : "123"}); + db.designDocs().rows.should.eql [] + end + + it 'should return all design docs' + var designDoc = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc); }" + } + }, + "_id" : "_design/spec_db" + }; + db.save(designDoc); + db.save({"Name" : "Felix Gaeta", "_id" : "123"}); + + var result = db.designDocs(); + + result.total_rows.should.eql 2 + result.rows.should.have_length 1 + result.rows[0].id.should.eql "_design/spec_db" + result.rows[0].key.should.eql "_design/spec_db" + result.rows[0].value.rev.length.should.be_at_least 30 + end + end + + describe '.changes' + it 'should return no changes when there arent any' + db.changes().last_seq.should.eql 0 + db.changes().results.should.eql [] + end + + describe 'with changes' + before_each + db.save({"Name" : "Felix Gaeta", "_id" : "123"}); + db.save({"Name" : "Samuel T. Anders", "_id" : "456"}); + end + + it 'should return changes' + var result = db.changes(); + + result.last_seq.should.eql 2 + result.results[0].id.should.eql "123" + result.results[0].seq.should.eql 1 + result.results[0].changes[0].rev.length.should.be_at_least 30 + result.results[1].id.should.eql "456" + result.results[1].seq.should.eql 2 + result.results[1].changes[0].rev.length.should.be_at_least 30 + end + + it 'should pass through the options' + var result = db.changes({"since":"1"}); + + result.last_seq.should.eql 2 + result.results[0].id.should.eql "456" + end + end + end + + describe '.compact' + it 'should return ok true' + db.compact().ok.should.be_true + end + + it 'should post _compact to the db' + db.should.receive("request", "once").with_args("POST", "/spec_db/_compact") + db.compact(); + end + end + + describe '.viewCleanup' + it 'should return ok true' + db.viewCleanup().ok.should.be_true + end + + it 'should post _view_cleanup to the db' + db.should.receive("request", "once").with_args("POST", "/spec_db/_view_cleanup") + db.viewCleanup(); + end + end + + describe '.setDbProperty' + it 'should return ok true' + db.setDbProperty("_revs_limit", 1500).ok.should.be_true + end + + it 'should set a db property' + db.setDbProperty("_revs_limit", 1500); + db.getDbProperty("_revs_limit").should.eql 1500 + db.setDbProperty("_revs_limit", 1200); + db.getDbProperty("_revs_limit").should.eql 1200 + end + end + + describe '.getDbProperty' + it 'should get a db property' + db.setDbProperty("_revs_limit", 1200); + db.getDbProperty("_revs_limit").should.eql 1200 + end + + it 'should throw an error when the property doesnt exist' + function(){ db.getDbProperty("_doesnt_exist")}.should.throw_error + end + end + + describe '.setSecObj' + it 'should return ok true' + db.setSecObj({"readers":{"names":["laura"],"roles":["president"]}}).ok.should.be_true + end + + it 'should save a well formed object into the _security object ' + db.should.receive("request", "once").with_args("PUT", "/spec_db/_security", {body: '{"readers":{"names":["laura"],"roles":["president"]}}'}) + db.setSecObj({"readers": {"names" : ["laura"], "roles" : ["president"]}}) + end + + it 'should throw an error when the readers or admins object is malformed' + -{ db.setSecObj({"admins":["cylon"]}) }.should.throw_error + end + + it 'should save any other object into the _security object' + db.setSecObj({"something" : "anything"}) + db.getSecObj().should.eql {"something" : "anything"} + end + end + + describe '.getSecObj' + it 'should get the security object' + db.setSecObj({"admins" : {"names" : ["bill"], "roles" : ["admiral"]}}) + db.getSecObj().should.eql {"admins" : {"names": ["bill"], "roles": ["admiral"]}} + end + + it 'should return an empty object when there is no security object' + db.getSecObj().should.eql {} + end + end +end
\ No newline at end of file diff --git a/share/www/spec/custom_helpers.js b/share/www/spec/custom_helpers.js new file mode 100644 index 00000000..d29ee87b --- /dev/null +++ b/share/www/spec/custom_helpers.js @@ -0,0 +1,51 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +function stubAlert(){ + if(typeof(old_alert) == 'undefined'){ + old_alert = alert; + } + alert = function(msg){ + alert_msg = msg; + }; +} + +function destubAlert(){ + alert = old_alert; +} + +function errorCallback(status, error, reason){ + console.log("Unexpected " + status + " error: " + error + " - " + reason) + throw("Unexpected " + status + " error: " + error + " - " + reason); +} + +function successCallback(resp){ + console.log("No error message here unexpectedly, successful response instead.") + throw("No error message here unexpectedly, successful response instead."); +} + +function useTestUserDb(){ + users_db = new CouchDB("spec_users_db"); + var xhr = CouchDB.request("PUT", "/_config/couch_httpd_auth/authentication_db", { + body: JSON.stringify("spec_users_db") + }); + if(typeof(old_auth_db) == 'undefined'){ + old_auth_db = xhr.responseText.replace(/\n/,'').replace(/"/g,''); + } +} + +function useOldUserDb(){ + CouchDB.request("PUT", "/_config/couch_httpd_auth/authentication_db", { + body: JSON.stringify(old_auth_db) + }); + users_db.deleteDb(); +}
\ No newline at end of file diff --git a/share/www/spec/jquery_couch_js_class_methods_spec.js b/share/www/spec/jquery_couch_js_class_methods_spec.js new file mode 100644 index 00000000..f2df81b3 --- /dev/null +++ b/share/www/spec/jquery_couch_js_class_methods_spec.js @@ -0,0 +1,523 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for jquery_couch.js lines 48-156 and 415-448 + +describe 'jQuery couchdb' + before + stubAlert(); + end + + after + destubAlert(); + end + + describe 'activeTasks' + before_each + db = $.couch.db("spec_db"); + db.create(); + end + + after_each + db.drop(); + end + + it 'should return an empty array when there are no active tasks' + $.couch.activeTasks({ + success: function(resp){ + resp.should.eql [] + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return an active task' + // doing a bit of stuff here so compaction has something to do and takes a while + var battlestar, civillian; + db.saveDoc({"type":"Battlestar", "name":"Galactica"}, { + success: function(resp){ + db.openDoc(resp.id, { + success: function(resp2){ + battlestar = resp2; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + battlestar.name = "Pegasus"; + db.saveDoc(battlestar); + + db.saveDoc({"type":"Civillian", "name":"Cloud 9"}, { + success: function(resp){ + db.openDoc(resp.id, { + success: function(resp2){ + civillian = resp2; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + civillian.name = "Olympic Carrier"; + db.saveDoc(civillian); + db.removeDoc(civillian); + + db.compact({ + ajaxStart: function(resp){ + $.couch.activeTasks({ + success: function(resp2){ + resp2[0].type.should.eql "Database Compaction" + resp2[0].task.should.eql "spec_db" + resp2[0].should.have_prop "status" + resp2[0].should.include "pid" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + } + }); + end + end + + describe 'allDbs' + it 'should return an array that includes a created database' + temp_db = new CouchDB("temp_spec_db", {"X-Couch-Full-Commit":"false"}); + temp_db.createDb(); + $.couch.allDbs({ + success: function(resp){ + resp.should.include "temp_spec_db" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + temp_db.deleteDb(); + end + + it 'should return an array that does not include a database that does not exist' + $.couch.allDbs({ + success: function(resp){ + resp.should.not.include("not_existing_temp_spec_db"); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'config' + it 'should get the config settings' + $.couch.config({ + success: function(resp){ + resp.httpd.port.should.eql window.location.port + resp.stats.samples.should.match /\[.*\]/ + resp.native_query_servers.should.have_prop "erlang" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should get a specific config setting' + $.couch.config({ + success: function(resp){ + parseInt(resp.max_document_size).should.be_a Number + resp.delayed_commits.should.be_a String + resp.database_dir.should.be_a String + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, "couchdb"); + end + + it 'should update a config setting' + $.couch.config({ + success: function(resp){ + resp.should.eql "" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, "test", "colony", "Caprica"); + + $.couch.config({ + success: function(resp){ + resp.colony.should.eql "Caprica" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, "test"); + + $.couch.config({}, "test", "colony", null); + end + + it 'should delete a config setting' + $.couch.config({}, "test", "colony", "Caprica"); + + $.couch.config({ + success: function(resp){ + resp.should.eql "Caprica" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, "test", "colony", null); + + $.couch.config({ + success: function(resp){ + resp.should.eql {} + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, "test"); + end + + it 'should alert with an error message prefix' + $.couch.config("asdf", "asdf", "asdf"); + alert_msg.should.match /An error occurred retrieving\/updating the server configuration/ + end + end + + describe 'session' + it 'should return information about the session' + $.couch.session({ + success: function(resp){ + resp.info.should.have_prop 'authentication_db' + resp.userCtx.should.include 'name' + resp.userCtx.roles.should.be_an Array + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'userDb' + it 'should return the userDb' + var authentication_db; + $.couch.session({ + success: function(resp){ + authentication_db = resp.info.authentication_db; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + + $.couch.userDb(function(resp){ + resp.name.should.eql authentication_db + }); + end + + it 'should return a db instance' + $.couch.userDb(function(resp){ + resp.should.respond_to 'allDocs' + resp.should.respond_to 'bulkSave' + }); + end + end + + describe 'user_db stuff' + before + useTestUserDb(); + end + + after + useOldUserDb(); + end + + describe 'signup' + it 'should return a saved user' + $.couch.signup( + {name: "Tom Zarek"}, "secretpass", { + success: function(resp){ + resp.id.should.eql "org.couchdb.user:Tom Zarek" + resp.rev.length.should.be_at_least 30 + resp.ok.should.be_true + users_db.deleteDoc({_id : resp.id, _rev : resp.rev}) + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should create a userDoc in the user db' + $.couch.signup( + {name: "Tom Zarek"}, "secretpass", { + success: function(resp){ + var user = users_db.open(resp.id); + user.name.should.eql "Tom Zarek" + user._id.should.eql "org.couchdb.user:Tom Zarek" + user.roles.should.eql [] + user.password_sha.length.should.be_at_least 30 + user.password_sha.should.be_a String + users_db.deleteDoc({_id : resp.id, _rev : resp.rev}) + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should create a userDoc with roles when specified' + $.couch.signup( + {name: "Tom Zarek", roles: ["vice_president", "activist"]}, "secretpass", { + success: function(resp){ + var user = users_db.open(resp.id); + user.roles.should.eql ["vice_president", "activist"] + users_db.deleteDoc({_id : resp.id, _rev : resp.rev}) + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'login' + before_each + user = {}; + $.couch.signup({name: "Tom Zarek", roles: ["vice_president", "activist"]}, "secretpass", { + success: function(resp){ + user.id = resp.id; + user.rev = resp.rev; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + after_each + users_db.deleteDoc({_id : user.id, _rev : user.rev}) + end + + it 'should return the logged in user' + $.couch.login({ + name: "Tom Zarek", + password: "secretpass", + success: function(resp){ + resp.name.should.eql "Tom Zarek" + resp.ok.should.be_true + resp.roles.should.eql ["vice_president", "activist"] + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in a session for the logged in user' + $.couch.login({ + name: "Tom Zarek", + password: "secretpass" + }); + $.couch.session({ + success: function(resp){ + resp.info.authentication_db.should.eql "spec_users_db" + resp.userCtx.name.should.eql "Tom Zarek" + resp.userCtx.roles.should.eql ["vice_president", "activist"] + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return a 404 when password is wrong' + $.couch.login({ + name: "Tom Zarek", + password: "wrongpass", + error: function(status, error, reason){ + status.should.eql 401 + error.should.eql "unauthorized" + reason.should.eql "Name or password is incorrect." + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should return a 404 when the user doesnt exist in the users db' + $.couch.login({ + name: "Number Three", + password: "secretpass", + error: function(status, error, reason){ + status.should.eql 401 + error.should.eql "unauthorized" + reason.should.eql "Name or password is incorrect." + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should alert with an error message prefix' + $.couch.login("asdf"); + alert_msg.should.match /An error occurred logging in/ + end + end + + describe 'logout' + before_each + user = {}; + $.couch.signup({name: "Tom Zarek", roles: ["vice_president", "activist"]}, "secretpass", { + success: function(resp){ + user.id = resp.id; + user.rev = resp.rev; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + $.couch.login({name: "Tom Zarek", password: "secretpass"}); + end + + after_each + users_db.deleteDoc({_id : user.id, _rev : user.rev}) + end + + it 'should return ok true' + $.couch.logout({ + success: function(resp){ + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in an empty session' + $.couch.logout(); + $.couch.session({ + success: function(resp){ + resp.userCtx.name.should.be_null + resp.userCtx.roles.should.not.include ["vice_president"] + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + end + + describe 'encodeDocId' + it 'should return the encoded docID when it is not a design document' + $.couch.encodeDocId("viper").should.eql(encodeURIComponent("viper")) + end + + it 'should encode only the name of the design document' + $.couch.encodeDocId("_design/raptor").should.eql("_design/" + encodeURIComponent("raptor")) + end + + it 'should also work when the name of the des' + $.couch.encodeDocId("_design/battlestar/_view/crew").should.eql("_design/" + encodeURIComponent("battlestar/_view/crew")) + end + end + + describe 'info' + it 'should return the CouchDB version' + $.couch.info({ + success: function(resp){ + resp.couchdb.should.eql "Welcome" + resp.version.should_match /^\d\d?\.\d\d?\.\d\d?.*/ + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'replicate' + before_each + db = $.couch.db("spec_db"); + db.create(); + db2 = $.couch.db("spec_db_2"); + db2.create(); + host = window.location.protocol + "//" + window.location.host ; + end + + after_each + db.drop(); + db2.drop(); + end + + it 'should return no_changes true when there are no changes between the dbs' + $.couch.replicate(host + db.uri, host + db2.uri, { + success: function(resp){ + resp.ok.should.be_true + resp.no_changes.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return the session ID' + db.saveDoc({'type':'battlestar', 'name':'galactica'}); + $.couch.replicate(host + db.uri, host + db2.uri, { + success: function(resp){ + resp.session_id.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return source_last_seq' + db.saveDoc({'type':'battlestar', 'name':'galactica'}); + db.saveDoc({'type':'battlestar', 'name':'pegasus'}); + + $.couch.replicate(host + db.uri, host + db2.uri, { + success: function(resp){ + resp.source_last_seq.should.eql 2 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return the replication history' + db.saveDoc({'type':'battlestar', 'name':'galactica'}); + db.saveDoc({'type':'battlestar', 'name':'pegasus'}); + + $.couch.replicate(host + db.uri, host + db2.uri, { + success: function(resp){ + resp.history[0].docs_written.should.eql 2 + resp.history[0].start_last_seq.should.eql 0 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through replication options' + db.saveDoc({'type':'battlestar', 'name':'galactica'}); + db2.drop(); + $.couch.replicate(host + db.uri, host + db2.uri, { + error: function(status, error, reason){ + status.should.eql 500 + reason.should.match /db_not_found/ + }, + success: function(resp){successCallback(resp)} + }); + + $.couch.replicate(host + db.uri, host + db2.uri, { + success: function(resp){ + resp.ok.should.eql true + resp.history[0].docs_written.should.eql 1 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, { + "create_target":true + }); + + db2.info({ + success: function(resp){ + resp.db_name.should.eql "spec_db_2" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + $.couch.replicate("asdf"); + alert_msg.should.match /Replication failed/ + end + end + + describe 'newUUID' + it 'should return a new UUID' + var new_uuid = $.couch.newUUID(1); + new_uuid.should.be_a String + new_uuid.should.have_length 32 + end + + it 'should fill the uuidCache with the specified number minus 1' + // we can't reach the uuidCache from here, so we mock the next request + // to test that the next uuid is not coming from the request, but from the cache. + $.couch.newUUID(2); + mock_request().and_return({'uuids':['a_sample_uuid']}) + $.couch.newUUID(1).should.not.eql 'a_sample_uuid' + $.couch.newUUID(1).should.eql 'a_sample_uuid' + end + + it 'should alert with an error message prefix' + $.couch.newUUID("asdf"); + alert_msg.should.match /Failed to retrieve UUID batch/ + end + end +end
\ No newline at end of file diff --git a/share/www/spec/jquery_couch_js_instance_methods_1_spec.js b/share/www/spec/jquery_couch_js_instance_methods_1_spec.js new file mode 100644 index 00000000..8538c856 --- /dev/null +++ b/share/www/spec/jquery_couch_js_instance_methods_1_spec.js @@ -0,0 +1,202 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for jquery_couch.js lines 163-209 + +describe 'jQuery couchdb db' + before + stubAlert(); + end + + after + destubAlert(); + end + + before_each + db = $.couch.db('spec_db'); + end + + describe 'constructor' + it 'should set the name' + db.name.should.eql 'spec_db' + end + + it 'should set the uri' + db.uri.should.eql '/spec_db/' + end + end + + describe 'triggering db functions' + before_each + db.create(); + end + + after_each + db.drop(); + end + + describe 'compact' + it 'should return ok true' + db.compact({ + success: function(resp) { + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should trigger _compact' + db.compact({ + success: function(resp, obj) { + obj.url.should.eql "/spec_db/_compact" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'viewCleanup' + it 'should return ok true' + db.viewCleanup({ + success: function(resp) { + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should trigger _view_cleanup' + db.viewCleanup({ + success: function(resp, obj) { + obj.url.should.eql "/spec_db/_view_cleanup" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'compactView' + before_each + var designDoc = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc); }" + } + }, + "_id" : "_design/myview" + }; + db.saveDoc(designDoc); + db.saveDoc({"Name" : "Felix Gaeta", "_id" : "123"}); + end + + it 'should return ok true' + db.compactView("myview", { + success: function(resp) { + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should trigger _compact_view with the groupname' + db.compactView("myview", { + success: function(resp, obj) { + obj.url.should.eql "/spec_db/_compact/myview" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return raise a 404 error when the design name doesnt exist' + db.compactView("non_existing_design_name", { + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "missing" + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should alert with an error message prefix' + db.compactView("asdf"); + alert_msg.should.match /The view could not be compacted/ + end + end + end + + describe 'create' + after_each + db.drop(); + end + + it 'should return ok true' + db.create({ + success: function(resp) { + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in a created db' + db.create(); + db.create({ + error: function(status, error, reason){ + status.should.eql 412 + error.should.eql "file_exists" + reason.should.eql "The database could not be created, the file already exists." + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should alert with an error message prefix' + db.create(); + db.create(); + alert_msg.should.match /The database could not be created/ + end + end + + describe 'drop' + before_each + db.create(); + end + + it 'should return ok true' + db.drop({ + success: function(resp) { + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in a deleted db' + db.drop(); + db.drop({ + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "missing" + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should alert with an error message prefix' + db.drop(); + db.drop(); + alert_msg.should.match /The database could not be deleted/ + end + end +end
\ No newline at end of file diff --git a/share/www/spec/jquery_couch_js_instance_methods_2_spec.js b/share/www/spec/jquery_couch_js_instance_methods_2_spec.js new file mode 100644 index 00000000..8f35affa --- /dev/null +++ b/share/www/spec/jquery_couch_js_instance_methods_2_spec.js @@ -0,0 +1,433 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for jquery_couch.js lines 210-299 + +describe 'jQuery couchdb db' + before + stubAlert(); + end + + after + destubAlert(); + end + + before_each + db = $.couch.db('spec_db'); + db.create(); + end + + after_each + db.drop(); + end + + describe 'info' + before_each + result = {}; + db.info({ + success: function(resp) { result = resp; }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return the name of the database' + result.db_name.should.eql "spec_db" + end + + it 'should return the number of documents' + result.doc_count.should.eql 0 + end + + it 'should return the start time of the db instance' + result.instance_start_time.should.have_length 16 + end + end + + describe 'allDocs' + it 'should return no docs when there arent any' + db.allDocs({ + success: function(resp) { + resp.total_rows.should.eql 0 + resp.rows.should.eql [] + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + describe 'with docs' + before_each + db.saveDoc({"Name" : "Felix Gaeta", "_id" : "123"}); + db.saveDoc({"Name" : "Samuel T. Anders", "_id" : "456"}); + end + + it 'should return all docs' + db.allDocs({ + success: function(resp) { + resp.total_rows.should.eql 2 + resp.rows.should.have_length 2 + resp.rows[0].id.should.eql "123" + resp.rows[0].key.should.eql "123" + resp.rows[0].value.rev.length.should.be_at_least 30 + resp.rows[1].id.should.eql "456" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options' + db.allDocs({ + "startkey": "123", + "limit": "1", + success: function(resp) { + resp.rows.should.have_length 1 + resp.rows[0].id.should.eql "123" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + end + + describe 'allDesignDocs' + it 'should return nothing when there arent any design docs' + db.saveDoc({"Name" : "Felix Gaeta", "_id" : "123"}); + db.allDesignDocs({ + success: function(resp) { + resp.rows.should.eql [] + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return all design docs' + var designDoc = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc); }" + } + }, + "_id" : "_design/spec_db" + }; + db.saveDoc(designDoc); + db.saveDoc({"Name" : "Felix Gaeta", "_id" : "123"}); + + db.allDesignDocs({ + success: function(resp) { + resp.total_rows.should.eql 2 + resp.rows.should.have_length 1 + resp.rows[0].id.should.eql "_design/spec_db" + resp.rows[0].key.should.eql "_design/spec_db" + resp.rows[0].value.rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + end + + describe 'allApps' + it 'should provide a custom function with appName, appPath and design document when there is an attachment with index.html' + var designDoc = {"_id" : "_design/with_attachments"}; + + designDoc._attachments = { + "index.html" : { + "content_type": "text\/html", + // this is "<html><p>Hi, here is index!</p></html>", base64 encoded + "data": "PGh0bWw+PHA+SGksIGhlcmUgaXMgaW5kZXghPC9wPjwvaHRtbD4=" + } + }; + db.saveDoc(designDoc); + + db.allApps({ + eachApp: function(appName, appPath, ddoc) { + appName.should.eql "with_attachments" + appPath.should.eql "/spec_db/_design/with_attachments/index.html" + ddoc._id.should.eql "_design/with_attachments" + ddoc._attachments["index.html"].content_type.should.eql "text/html" + ddoc._attachments["index.html"].length.should.eql "<html><p>Hi, here is index!</p></html>".length + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should provide a custom function with appName, appPath and design document when there is a couchapp with index file' + var designDoc = {"_id" : "_design/with_index"}; + designDoc.couchapp = { + "index" : "cylon" + }; + db.saveDoc(designDoc); + + db.allApps({ + eachApp: function(appName, appPath, ddoc) { + appName.should.eql "with_index" + appPath.should.eql "/spec_db/_design/with_index/cylon" + ddoc._id.should.eql "_design/with_index" + ddoc.couchapp.index.should.eql "cylon" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should not call the eachApp function when there is neither index.html in _attachments nor a couchapp index file' + var designDoc = {"_id" : "_design/nothing"}; + db.saveDoc(designDoc); + + var eachApp_called = false; + db.allApps({ + eachApp: function(appName, appPath, ddoc) { + eachApp_called = true; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + + eachApp_called.should.be_false + end + + it 'should alert with an error message prefix' + db.allApps(); + alert_msg.should.match /Please provide an eachApp function for allApps()/ + end + end + + describe 'openDoc' + before_each + doc = {"Name" : "Louanne Katraine", "Callsign" : "Kat", "_id" : "123"}; + db.saveDoc(doc); + end + + it 'should open the document' + db.openDoc("123", { + success: function(resp){ + resp.should.eql doc + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should raise a 404 error when there is no document with the given ID' + db.openDoc("non_existing", { + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "missing" + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should pass through the options' + doc.Name = "Sasha"; + db.saveDoc(doc); + db.openDoc("123", { + revs: true, + success: function(resp){ + resp._revisions.start.should.eql 2 + resp._revisions.ids.should.have_length 2 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.openDoc("asdf"); + alert_msg.should.match /The document could not be retrieved/ + end + end + + describe 'saveDoc' + before_each + doc = {"Name" : "Kara Thrace", "Callsign" : "Starbuck"}; + end + + it 'should save the document' + db.saveDoc(doc, { + success: function(resp, status){ + status.should.eql 201 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return ok true' + db.saveDoc(doc, { + success: function(resp, status){ + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return ID and revision of the document' + db.saveDoc(doc, { + success: function(resp, status){ + resp.id.should.be_a String + resp.id.should.have_length 32 + resp.rev.should.be_a String + resp.rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in a saved document with generated ID' + db.saveDoc(doc, { + success: function(resp, status){ + db.openDoc(resp.id, { + success: function(resp2){ + resp2.Name.should.eql "Kara Thrace" + resp2.Callsign.should.eql "Starbuck" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should save the document with the specified ID' + doc._id = "123"; + db.saveDoc(doc, { + success: function(resp, status){ + resp.id.should.eql "123" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options' + db.saveDoc(doc, { + "batch" : "ok", + success: function(resp, status){ + // when using batch ok, couch sends a 202 status immediately + status.should.eql 202 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.saveDoc("asdf"); + alert_msg.should.match /The document could not be saved/ + end + end + + describe 'bulkSave' + before_each + doc = {"Name" : "Kara Thrace", "Callsign" : "Starbuck"}; + doc2 = {"Name" : "Karl C. Agathon", "Callsign" : "Helo"}; + doc3 = {"Name" : "Sharon Valerii", "Callsign" : "Boomer"}; + docs = [doc, doc2, doc3]; + end + + it 'should save all documents' + db.bulkSave({"docs": docs}); + db.allDocs({ + success: function(resp) { + resp.total_rows.should.eql 3 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in saved documents' + doc3._id = "789"; + db.bulkSave({"docs": [doc3]}); + + db.openDoc("789", { + success: function(resp){ + resp.Name.should.eql "Sharon Valerii" + resp.Callsign.should.eql "Boomer" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return ID and revision of the documents' + db.bulkSave({"docs": docs},{ + success: function(resp){ + resp[0].id.should.be_a String + resp[0].id.should.have_length 32 + resp[0].rev.should.be_a String + resp[0].rev.length.should.be_at_least 30 + resp[1].id.should.be_a String + resp[1].id.should.have_length 32 + resp[1].rev.should.be_a String + resp[1].rev.length.should.be_at_least 30 + resp[2].id.should.be_a String + resp[2].id.should.have_length 32 + resp[2].rev.should.be_a String + resp[2].rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should save the document with specified IDs' + doc._id = "123"; + doc2._id = "456"; + docs = [doc, doc2, doc3]; + + db.bulkSave({"docs": docs},{ + success: function(resp){ + resp[0].id.should.eql "123" + resp[1].id.should.eql "456" + resp[2].id.should.have_length 32 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options' + // a lengthy way to test that a conflict can't be created with the + // all_or_nothing option set to false, but can be when it's true. + + var old_doc = {"Name" : "Louanne Katraine", "Callsign" : "Kat", "_id" : "123"}; + db.saveDoc(old_doc, { + success: function(resp){ + old_doc._rev = resp.rev; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + + var new_doc = {"Name" : "Sasha", "Callsign" : "Kat", "_id" : "123"}; + + db.bulkSave({"docs": [new_doc], "all_or_nothing": false}, { + success: function(resp){ + resp[0].id.should.eql "123" + resp[0].error.should.eql "conflict" + resp[0].reason.should.eql "Document update conflict." + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + + db.bulkSave({"docs": [new_doc], "all_or_nothing": true}, { + success: function(resp){ + resp[0].id.should.eql "123" + resp[0].rev.should.not.eql old_doc._rev + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + + db.openDoc("123", { + "conflicts": true, + success: function(resp){ + resp._conflicts[0].should.eql old_doc._rev + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.bulkSave("asdf"); + alert_msg.should.match /The documents could not be saved/ + end + end +end
\ No newline at end of file diff --git a/share/www/spec/jquery_couch_js_instance_methods_3_spec.js b/share/www/spec/jquery_couch_js_instance_methods_3_spec.js new file mode 100644 index 00000000..5d27d817 --- /dev/null +++ b/share/www/spec/jquery_couch_js_instance_methods_3_spec.js @@ -0,0 +1,540 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Specs for jquery_couch.js lines 300-411 + +describe 'jQuery couchdb db' + before + stubAlert(); + end + + after + destubAlert(); + end + + before_each + db = $.couch.db('spec_db'); + db.create(); + end + + after_each + db.drop(); + end + + describe 'removeDoc' + before_each + doc = {"Name" : "Louanne Katraine", "Callsign" : "Kat", "_id" : "345"}; + saved_doc = {}; + db.saveDoc(doc, { + success: function(resp){ + saved_doc = resp; + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in a deleted document' + db.removeDoc({_id : "345", _rev : saved_doc.rev}, { + success: function(resp){ + db.openDoc("345", { + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "deleted" + }, + success: function(resp){successCallback(resp)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return ok true, the ID and the revision of the deleted document' + db.removeDoc({_id : "345", _rev : saved_doc.rev}, { + success: function(resp){ + resp.ok.should.be_true + resp.id.should.eql "345" + resp.rev.should.be_a String + resp.rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should record the revision in the deleted document' + db.removeDoc({_id : "345", _rev : saved_doc.rev}, { + success: function(resp){ + db.openDoc("345", { + rev: resp.rev, + success: function(resp2){ + resp2._rev.should.eql resp.rev + resp2._id.should.eql resp.id + resp2._deleted.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.removeDoc({_id: "asdf"}); + alert_msg.should.match /The document could not be deleted/ + end + end + + describe 'bulkRemove' + before_each + doc = {"Name" : "Kara Thrace", "Callsign" : "Starbuck", "_id" : "123"}; + doc2 = {"Name" : "Karl C. Agathon", "Callsign" : "Helo", "_id" : "456"}; + doc3 = {"Name" : "Sharon Valerii", "Callsign" : "Boomer", "_id" : "789"}; + docs = [doc, doc2, doc3]; + + db.bulkSave({"docs": docs}, { + success: function(resp){ + for (var i = 0; i < docs.length; i++) { + docs[i]._rev = resp[i].rev; + } + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should remove all documents specified' + db.bulkRemove({"docs": docs}); + db.allDocs({ + success: function(resp) { + resp.total_rows.should.eql 0 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should not remove documents that should not have been deleted' + db.bulkRemove({"docs": [doc3]}); + db.allDocs({ + success: function(resp) { + resp.total_rows.should.eql 2 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should result in deleted documents' + db.bulkRemove({"docs": docs}, { + success: function(resp){ + db.openDoc("123", { + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "deleted" + }, + success: function(resp){successCallback(resp)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should return the ID and the revision of the deleted documents' + db.bulkRemove({"docs": docs}, { + success: function(resp){ + resp[0].id.should.eql "123" + resp[0].rev.should.be_a String + resp[0].rev.length.should.be_at_least 30 + resp[1].id.should.eql "456" + resp[1].rev.should.be_a String + resp[1].rev.length.should.be_at_least 30 + resp[2].id.should.eql "789" + resp[2].rev.should.be_a String + resp[2].rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should record the revision in the deleted documents' + db.bulkRemove({"docs": docs}, { + success: function(resp){ + db.openDoc("123", { + rev: resp[0].rev, + success: function(resp2){ + resp2._rev.should.eql resp[0].rev + resp2._id.should.eql resp[0].id + resp2._deleted.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.bulkRemove({docs: ["asdf"]}); + alert_msg.should.match /The documents could not be deleted/ + end + end + + describe 'copyDoc' + before_each + doc = {"Name" : "Sharon Agathon", "Callsign" : "Athena", "_id" : "123"}; + db.saveDoc(doc); + end + + it 'should result in another document with same data and new id' + db.copyDoc("123", { + success: function(resp){ + resp.id.should.eql "456" + resp.rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, { + headers: {"Destination":"456"} + }); + + db.openDoc("456", { + success: function(resp){ + resp.Name.should.eql "Sharon Agathon" + resp.Callsign.should.eql "Athena" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should throw an error when trying to overwrite a document without providing a revision' + doc2 = {"Name" : "Louanne Katraine", "Callsign" : "Kat", "_id" : "456"}; + db.saveDoc(doc2); + + db.copyDoc("123", { + error: function(status, error, reason){ + status.should.eql 409 + error.should.eql "conflict" + reason.should.eql "Document update conflict." + }, + success: function(resp){successCallback(resp)} + }, { + headers: {"Destination":"456"} + }); + end + + it 'should overwrite a document with the correct revision' + doc2 = {"Name" : "Louanne Katraine", "Callsign" : "Kat", "_id" : "456"}; + var doc2_rev; + db.saveDoc(doc2, { + success: function(resp){ + doc2_rev = resp.rev; + } + }); + + db.copyDoc("123", { + success: function(resp){ + resp.id.should.eql "456" + resp.rev.length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }, { + headers: {"Destination":"456?rev=" + doc2_rev} + }); + + db.openDoc("456", { + success: function(resp){ + resp.Name.should.eql "Sharon Agathon" + resp.Callsign.should.eql "Athena" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.copyDoc("asdf", {}, {}); + alert_msg.should.match /The document could not be copied/ + end + end + + describe 'query' + before_each + db.saveDoc({"Name" : "Cally Tyrol", "job" : "deckhand", "_id" : "789"}); + db.saveDoc({"Name" : "Felix Gaeta", "job" : "officer", "_id" : "123"}); + db.saveDoc({"Name" : "Samuel T. Anders", "job" : "pilot", "_id" : "456"}); + map_function = "function(doc) { emit(doc._id, 1); }"; + reduce_function = "function(key, values, rereduce) { return sum(values); }"; + end + + it 'should apply the map function' + db.query(map_function, null, null, { + success: function(resp){ + resp.rows.should.have_length 3 + resp.rows[0].id.should.eql "123" + resp.rows[0].key.should.eql "123" + resp.rows[0].value.should.eql 1 + resp.rows[1].id.should.eql "456" + resp.rows[1].key.should.eql "456" + resp.rows[1].value.should.eql 1 + resp.rows[2].id.should.eql "789" + resp.rows[2].key.should.eql "789" + resp.rows[2].value.should.eql 1 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should apply the reduce function' + db.query(map_function, reduce_function, null, { + success: function(resp){ + resp.rows.should.have_length 1 + resp.rows[0].key.should.be_null + resp.rows[0].value.should_eql 3 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options' + db.query(map_function, null, null, { + "startkey": "456", + success: function(resp){ + resp.rows.should.have_length 2 + resp.rows[0].id.should.eql "456" + resp.rows[0].key.should.eql "456" + resp.rows[0].value.should.eql 1 + resp.rows[1].id.should.eql "789" + resp.rows[1].key.should.eql "789" + resp.rows[1].value.should.eql 1 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the keys' + //shouldn't this better work? TODO: implement in jquery.couch.js + console.log("shouldn't this better work? TODO: implement in jquery.couch.js") + db.query(map_function, null, null, { + "keys": ["456", "123"], + success: function(resp){ + resp.rows.should.have_length 2 + resp.rows[0].id.should.eql "456" + resp.rows[0].key.should.eql "456" + resp.rows[0].value.should.eql 1 + resp.rows[1].id.should.eql "123" + resp.rows[1].key.should.eql "123" + resp.rows[1].value.should.eql 1 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options and the keys' + //shouldn't this better work? TODO: implement in jquery.couch.js + console.log("shouldn't this better work? TODO: implement in jquery.couch.js") + db.query(map_function, null, null, { + "include_docs":"true", + "keys": ["456"], + success: function(resp){ + resp.rows.should.have_length 1 + resp.rows[0].id.should.eql "456" + resp.rows[0].key.should.eql "456" + resp.rows[0].value.should.eql 1 + resp.rows[0].doc["job"].should.eql "pilot" + resp.rows[0].doc["_rev"].length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should apply a query in erlang also' + // when this test fails, read this: http://wiki.apache.org/couchdb/EnableErlangViews + var erlang_map = 'fun({Doc}) -> ' + + 'ID = proplists:get_value(<<"_id">>, Doc, null), ' + + 'Emit(ID, 1) ' + + 'end.'; + db.query(erlang_map, null, "erlang", { + success: function(resp){ + resp.rows.should.have_length 3 + resp.rows[0].id.should.eql "123" + resp.rows[0].key.should.eql "123" + resp.rows[0].value.should.eql 1 + resp.rows[1].id.should.eql "456" + resp.rows[1].key.should.eql "456" + resp.rows[1].value.should.eql 1 + resp.rows[2].id.should.eql "789" + resp.rows[2].key.should.eql "789" + resp.rows[2].value.should.eql 1 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.query("asdf"); + alert_msg.should.match /An error occurred querying the database/ + end + end + + describe 'view' + before_each + db.saveDoc({"Name" : "Cally Tyrol", "job" : "deckhand", "_id" : "789"}); + db.saveDoc({"Name" : "Felix Gaeta", "job" : "officer", "_id" : "123"}); + db.saveDoc({"Name" : "Samuel T. Anders", "job" : "pilot", "_id" : "456"}); + view = { + "views" : { + "people" : { + "map" : "function(doc) { emit(doc._id, doc.Name); }" + } + }, + "_id" : "_design/spec_db" + }; + db.saveDoc(view); + end + + it 'should apply the view' + db.view('spec_db/people', { + success: function(resp){ + resp.rows.should.have_length 3 + resp.rows[0].id.should.eql "123" + resp.rows[0].key.should.eql "123" + resp.rows[0].value.should.eql "Felix Gaeta" + resp.rows[1].id.should.eql "456" + resp.rows[1].key.should.eql "456" + resp.rows[1].value.should.eql "Samuel T. Anders" + resp.rows[2].id.should.eql "789" + resp.rows[2].key.should.eql "789" + resp.rows[2].value.should.eql "Cally Tyrol" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options' + db.view('spec_db/people', { + "skip":"2", + success: function(resp){ + resp.rows.should.have_length 1 + resp.rows[0].id.should.eql "789" + resp.rows[0].key.should.eql "789" + resp.rows[0].value.should.eql "Cally Tyrol" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the keys' + db.view('spec_db/people', { + "keys":["456", "123"], + success: function(resp){ + resp.rows.should.have_length 2 + resp.rows[0].id.should.eql "456" + resp.rows[0].key.should.eql "456" + resp.rows[0].value.should.eql "Samuel T. Anders" + resp.rows[1].id.should.eql "123" + resp.rows[1].key.should.eql "123" + resp.rows[1].value.should.eql "Felix Gaeta" + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should pass through the options and the keys' + db.view('spec_db/people', { + "include_docs":"true", + "keys":["456"], + success: function(resp){ + resp.rows.should.have_length 1 + resp.rows[0].id.should.eql "456" + resp.rows[0].key.should.eql "456" + resp.rows[0].value.should.eql "Samuel T. Anders" + resp.rows[0].doc["job"].should.eql "pilot" + resp.rows[0].doc["_rev"].length.should.be_at_least 30 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should throw a 404 when the view doesnt exist' + db.view('spec_db/non_existing_view', { + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "missing_named_view" + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should alert with an error message prefix' + db.view("asdf"); + alert_msg.should.match /An error occurred accessing the view/ + end + end + + describe 'setDbProperty' + it 'should return ok true' + db.setDbProperty("_revs_limit", 1500, { + success: function(resp){ + resp.ok.should.be_true + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should set a db property' + db.setDbProperty("_revs_limit", 1500); + db.getDbProperty("_revs_limit", { + success: function(resp){ + resp.should.eql 1500 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + db.setDbProperty("_revs_limit", 1200); + db.getDbProperty("_revs_limit", { + success: function(resp){ + resp.should.eql 1200 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should alert with an error message prefix' + db.setDbProperty("asdf"); + alert_msg.should.match /The property could not be updated/ + end + end + + describe 'getDbProperty' + it 'should get a db property' + db.setDbProperty("_revs_limit", 1200); + db.getDbProperty("_revs_limit", { + success: function(resp){ + resp.should.eql 1200 + }, + error: function(status, error, reason){errorCallback(status, error, reason)} + }); + end + + it 'should throw a 404 when the property doesnt exist' + db.getDbProperty("_doesnt_exist", { + error: function(status, error, reason){ + status.should.eql 404 + error.should.eql "not_found" + reason.should.eql "missing" + }, + success: function(resp){successCallback(resp)} + }); + end + + it 'should alert with an error message prefix' + db.getDbProperty("asdf"); + alert_msg.should.match /The property could not be retrieved/ + end + end +end
\ No newline at end of file diff --git a/share/www/spec/run.html b/share/www/spec/run.html new file mode 100644 index 00000000..e438333d --- /dev/null +++ b/share/www/spec/run.html @@ -0,0 +1,46 @@ +<!-- +Licensed under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. +--> +<html> + <head> + <link type="text/css" rel="stylesheet" href="../script/jspec/jspec.css" /> + <script src="../script/jquery.js"></script> + <script src="../script/sha1.js"></script> + <script src="../script/jspec/jspec.js"></script> + <script src="../script/jspec/jspec.jquery.js"></script> + <script src="../script/jspec/jspec.xhr.js"></script> + <script src="./custom_helpers.js"></script> + <script src="../script/couch.js"></script> + <script src="../script/jquery.couch.js"></script> + <script> + function runSuites() { + JSpec + .exec('couch_js_class_methods_spec.js') + .exec('couch_js_instance_methods_1_spec.js') + .exec('couch_js_instance_methods_2_spec.js') + .exec('couch_js_instance_methods_3_spec.js') + .exec('jquery_couch_js_class_methods_spec.js') + .exec('jquery_couch_js_instance_methods_1_spec.js') + .exec('jquery_couch_js_instance_methods_2_spec.js') + .exec('jquery_couch_js_instance_methods_3_spec.js') + .run({failuresOnly: true}) + .report() + } + </script> + </head> + <body class="jspec" onLoad="runSuites();"> + <div id="jspec-top"><h2 id="jspec-title">JSpec <em><script>document.write(JSpec.version)</script></em></h2></div> + <div id="jspec"></div> + <div id="jspec-bottom"></div> + </body> +</html> diff --git a/share/www/status.html b/share/www/status.html index 3e23f85d..2067ab9b 100644 --- a/share/www/status.html +++ b/share/www/status.html @@ -19,8 +19,10 @@ specific language governing permissions and limitations under the License. <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css"> <script src="script/json2.js"></script> - <script src="script/jquery.js?1.3.1"></script> + <script src="script/sha1.js"></script> + <script src="script/jquery.js?1.4.2"></script> <script src="script/jquery.couch.js?0.11.0"></script> + <script src="script/jquery.dialog.js?0.11.0"></script> <script src="script/futon.js?0.11.0"></script> </head> <body><div id="wrap"> diff --git a/share/www/style/layout.css b/share/www/style/layout.css index b07a59e5..4fff4980 100644 --- a/share/www/style/layout.css +++ b/share/www/style/layout.css @@ -55,7 +55,7 @@ code.null { color: #666; } button { font-size: 100%; -webkit-appearance: square-button; } button[disabled] { color: #999; } input, select, textarea { background: #fff; border: 1px solid; - border-color: #999 #ddd #ddd #999; margin: 0; padding: 1px; + border-color: #999 #ddd #ddd #999; color: #000; margin: 0; padding: 1px; } input.placeholder { color: #999; } textarea { @@ -219,8 +219,8 @@ body.fullwidth #sidebar { border-bottom: 1px solid #333; right: 0; height: 100%; margin-right: 210px; position: relative; } body.fullwidth #wrap { margin-right: 0; } -#content { padding: 1em 16px 3em 10px; overflow: auto; position: absolute; - top: 33px; bottom: 0; left: 0; right: 0; +#content { padding: 1em 16px 3em 10px; overflow: auto; overflow-y: scroll; + position: absolute; top: 33px; bottom: 0; left: 0; right: 0; } /* Toolbar */ @@ -229,6 +229,7 @@ body.fullwidth #wrap { margin-right: 0; } margin: 0 0 .5em; padding: 5px 5px 5px 3px; } #toolbar li { display: inline; } +#toolbar li.current {float:right;} #toolbar button { background: transparent 2px 2px no-repeat; border: none; color: #666; margin: 0; padding: 2px 1em 2px 22px; cursor: pointer; font-size: 95%; line-height: 16px; @@ -236,13 +237,13 @@ body.fullwidth #wrap { margin-right: 0; } #toolbar button:hover { background-position: 2px -30px; color: #000; } #toolbar button:active { background-position: 2px -62px; color: #000; } #toolbar button.add { background-image: url(../image/add.png); } +#toolbar button.security { background-image: url(../image/key.png); } #toolbar button.compact { background-image: url(../image/compact.png); } -#toolbar button.viewcleanup { background-image: url(../image/compact.png); } -#toolbar button.compactview { background-image: url(../image/compact.png); } #toolbar button.delete { background-image: url(../image/delete.png); } #toolbar button.load { background-image: url(../image/load.png); } #toolbar button.run { background-image: url(../image/run.png); } #toolbar button.save { background-image: url(../image/save.png); } +#toolbar button.share { background-image: url(../image/compact.png); } /* Dialogs */ @@ -260,18 +261,17 @@ body.fullwidth #wrap { margin-right: 0; } -webkit-box-shadow: 4px 4px 6px #333; } *html #dialog { width: 33em; } -body.loading #dialog h2 { - background: url(../image/spinner.gif) center no-repeat; -} #dialog.loading { width: 220px; height: 80px; } #dialog.loaded { background-image: none; } -#dialog h2 { background: #666; border-top: 1px solid #555; - border-bottom: 1px solid #777; color: #ccc; font-size: 110%; - font-weight: bold; margin: 0 -2em; padding: .35em 2em; +#dialog h2 { background: #666 98% 50% no-repeat; + border-top: 1px solid #555; border-bottom: 1px solid #777; color: #ccc; + font-size: 110%; font-weight: bold; margin: 0 -1em; padding: .35em 1em; } -#dialog h3 { - color: #ccc; font-size: 110%; - font-weight: bold; margin: 0 -2em; padding: .35em 2em; +body.loading #dialog h2 { + background-image: url(../image/spinner.gif); +} +#dialog h3 { color: #ccc; font-size: 100%; font-weight: bold; margin: 0 -2em; + padding: .35em 2em 0; } #dialog fieldset { background: #222; border-top: 1px solid #111; margin: 0 0 1em; padding: .5em 1em 1em; @@ -290,6 +290,11 @@ body.loading #dialog h2 { #dialog fieldset input { background-color: #e9e9e9; vertical-align: middle; } #dialog fieldset input.error { background-color: #f9e4e4; } #dialog fieldset div.error { padding-top: .3em; color: #b33; } +#dialog fieldset.radiogroup { padding-top: 1em; } +#dialog fieldset.radiogroup label { position: relative; padding-left: 25px; } +#dialog fieldset.radiogroup input { position: absolute; left: 5px; top: 2px; } +#dialog fieldset.radiogroup p.help { margin-top: .5em; margin-left: 25px; } +#dialog fieldset.radiogroup hr { border-color: #333; margin-left: 25px; } #dialog .buttons { padding: 0 .5em .5em; text-align: right; } #dialog .buttons button { background: #444; border: 1px solid #aaa; color: #ddd; cursor: pointer; font-size: 90%; font-weight: normal; @@ -319,6 +324,13 @@ body.loading #dialog h2 { } #switch select { font-size: 90%; } +/* Stale views checkbox */ + +#staleviews { + color: #666; float: right; font-size: 90%; + font-weight: bold; line-height: 16px; padding: 5px; +} + /* View function editing */ #viewcode { background: #fff; border: 1px solid; @@ -351,8 +363,8 @@ body.loading #dialog h2 { white-space: nowrap; } #viewcode .code textarea { border: none; border-top: 1px solid #ccc; - color: #333; font-size: 11px; margin: 0; min-height: 50px; padding: .4em 0 0; - resize: none; width: 100%; overflow: auto; + color: #333; font-size: 11px; margin: 0; min-height: 50px; overflow: auto; + padding: .4em 0 0; resize: none; width: 100%; } #viewcode .code textarea:focus { background: #e9f4ff; } #viewcode .bottom { border-bottom: none; clear: left; padding: 1px 3px; } @@ -491,7 +503,7 @@ body.loading #dialog h2 { #fields tbody.content td ul.attachments li { margin-bottom: .3em; min-height: 20px; padding-left: 20px; } -#fields tbody.content td ul.attachments tt { font-size: 95%; } +#fields tbody.content td ul.attachments tt { font-size: 11px; } #fields tbody.content td ul.attachments li span.info { color: #666; display: block; font-size: 95%; } @@ -598,3 +610,9 @@ form#replicator p.actions { padding: 1px; clear: left; margin: 0; font-family: "DejaVu Sans Mono",Menlo,Courier,monospace; font-size: 11px; } + + +/* Session */ +#loginSignup { + font-size:200%; +}
\ No newline at end of file |