From 219c08db48348683ad31ac4927ed05374840df81 Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Mon, 17 Aug 2009 11:11:14 -0500 Subject: [PATCH] August 17, 2009. Commit #252042 by webchick at 08:10 --- COPYRIGHT.txt | 6 +- CVS/Entries | 4 +- includes/CVS/Entries | 10 +- includes/ajax.inc | 716 +++++++++++ includes/bootstrap.inc | 188 +-- includes/browser.inc | 1139 ++++++++++++++++++ includes/common.inc | 3 +- includes/database/mysql/CVS/Entries | 2 +- includes/database/mysql/schema.inc | 28 +- includes/database/pgsql/CVS/Entries | 2 +- includes/database/pgsql/schema.inc | 16 +- includes/database/sqlite/CVS/Entries | 2 +- includes/database/sqlite/schema.inc | 6 +- includes/file.inc | 7 +- includes/form.inc | 137 +-- misc/CVS/Entries | 8 +- misc/ahah.js | 259 ---- misc/ajax.js | 383 ++++++ misc/autocomplete.js | 4 +- misc/drupal.js | 6 +- misc/progress.js | 4 +- modules/book/CVS/Entries | 6 +- modules/book/book.css | 4 +- modules/book/book.module | 7 +- modules/book/book.pages.inc | 44 +- modules/comment/CVS/Entries | 4 +- modules/comment/comment.api.php | 15 +- modules/comment/comment.module | 5 +- modules/dblog/CVS/Entries | 4 +- modules/dblog/dblog.admin.inc | 6 +- modules/dblog/dblog.test | 175 ++- modules/field/CVS/Entries | 4 +- modules/field/field.form.inc | 28 +- modules/field/field.test | 17 +- modules/help/CVS/Entries | 2 +- modules/node/CVS/Entries | 2 +- modules/poll/CVS/Entries | 4 +- modules/poll/poll.module | 12 +- modules/poll/poll.test | 21 +- modules/simpletest/CVS/Entries | 4 +- modules/simpletest/simpletest.info | 3 +- modules/simpletest/simpletest.module | 25 +- modules/simpletest/tests/CVS/Entries | 11 +- modules/simpletest/tests/bootstrap.test | 10 +- modules/simpletest/tests/browser.test | 123 ++ modules/simpletest/tests/browser_test.info | 8 + modules/simpletest/tests/browser_test.module | 79 ++ modules/simpletest/tests/file.test | 5 +- modules/simpletest/tests/form_test.module | 4 +- modules/system/CVS/Entries | 10 +- modules/system/system-rtl.css | 6 +- modules/system/system.admin.inc | 20 +- modules/system/system.css | 10 +- modules/system/system.install | 13 +- modules/system/system.module | 30 +- modules/taxonomy/CVS/Entries | 4 +- modules/upload/CVS/Entries | 2 +- modules/upload/upload.module | 20 +- sites/default/CVS/Entries | 2 +- sites/default/default.settings.php | 26 +- 60 files changed, 3006 insertions(+), 699 deletions(-) create mode 100644 includes/ajax.inc create mode 100644 includes/browser.inc delete mode 100644 misc/ahah.js create mode 100644 misc/ajax.js create mode 100644 modules/simpletest/tests/browser.test create mode 100644 modules/simpletest/tests/browser_test.info create mode 100644 modules/simpletest/tests/browser_test.module diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index 4c0fef2..d88b703 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -1,6 +1,6 @@ -// $Id: COPYRIGHT.txt,v 1.4 2008/12/20 18:24:32 dries Exp $ +// $Id: COPYRIGHT.txt,v 1.5 2009/08/16 19:01:05 dries Exp $ -All Drupal code is Copyright 2001 - 2008 by the original authors. +All Drupal code is Copyright 2001 - 2009 by the original authors. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,5 +21,5 @@ Drupal includes works under other copyright notices and distributed according to the terms of the GNU General Public License or a compatible license, including: - jQuery - Copyright (c) 2008 John Resig + jQuery - Copyright (c) 2008 - 2009 John Resig diff --git a/CVS/Entries b/CVS/Entries index 59d16c1..5351683 100644 --- a/CVS/Entries +++ b/CVS/Entries @@ -5,7 +5,6 @@ D/profiles//// D/scripts//// D/sites//// D/themes//// -/COPYRIGHT.txt/1.4/Fri Jun 12 07:15:24 2009// /INSTALL.mysql.txt/1.11/Fri Jun 12 07:15:24 2009// /INSTALL.pgsql.txt/1.8/Fri Aug 7 05:58:20 2009// /INSTALL.sqlite.txt/1.1/Fri Jun 12 07:15:24 2009// @@ -20,4 +19,5 @@ D/themes//// /UPGRADE.txt/1.15/Fri Aug 14 05:08:40 2009// /install.php/1.192/Fri Aug 14 05:08:40 2009// /update.php/1.296/Fri Aug 14 05:08:40 2009// -/.htaccess/1.104/Sun Aug 16 17:51:57 2009// +/.htaccess/1.104/Sun Aug 16 17:59:33 2009// +/COPYRIGHT.txt/1.5/Mon Aug 17 16:10:24 2009// diff --git a/includes/CVS/Entries b/includes/CVS/Entries index 2ead158..e1b5219 100644 --- a/includes/CVS/Entries +++ b/includes/CVS/Entries @@ -2,7 +2,6 @@ D/database//// D/filetransfer//// /actions.inc/1.29/Fri Aug 7 05:58:21 2009// /batch.inc/1.36/Sat Jun 13 03:09:33 2009// -/bootstrap.inc/1.293/Fri Aug 7 05:58:21 2009// /cache-install.inc/1.3/Fri Aug 7 05:58:21 2009// /cache.inc/1.38/Fri Jul 3 03:59:24 2009// /file.mimetypes.inc/1.3/Fri Aug 7 05:58:21 2009// @@ -23,12 +22,15 @@ D/filetransfer//// /update.inc/1.2/Fri Aug 7 05:58:21 2009// /xmlrpc.inc/1.58/Tue Jun 16 00:53:18 2009// /xmlrpcs.inc/1.28/Sat Jun 13 03:10:00 2009// -/file.inc/1.180/Fri Aug 14 05:08:40 2009// -/form.inc/1.356/Fri Aug 14 05:08:40 2009// /install.inc/1.104/Fri Aug 14 05:08:40 2009// /locale.inc/1.223/Fri Aug 14 05:08:40 2009// /menu.inc/1.333/Fri Aug 14 05:08:40 2009// /module.inc/1.152/Fri Aug 14 05:08:40 2009// /pager.inc/1.69/Fri Aug 14 05:08:40 2009// /theme.inc/1.504/Fri Aug 14 05:08:40 2009// -/common.inc/1.961/Sat Aug 15 19:33:18 2009// +/ajax.inc/1.1/Mon Aug 17 07:29:32 2009// +/bootstrap.inc/1.295/Mon Aug 17 16:10:24 2009// +/browser.inc/1.1/Mon Aug 17 06:08:46 2009// +/common.inc/1.962/Mon Aug 17 16:10:24 2009// +/file.inc/1.181/Mon Aug 17 16:10:24 2009// +/form.inc/1.357/Mon Aug 17 16:10:24 2009// diff --git a/includes/ajax.inc b/includes/ajax.inc new file mode 100644 index 0000000..28934d2 --- /dev/null +++ b/includes/ajax.inc @@ -0,0 +1,716 @@ +command is the type of command and will + * be used to find the method (it will correlate directly to a method in + * the Drupal.ajax[command] space). The object may contain any other data that + * the command needs to process. + * + * Commands are usually created with a couple of helper functions, so they + * look like this: + * + * @code + * $commands = array(); + * // Replace the content of '#object-1' on the page with 'some html here'. + * $commands[] = ajax_command_replace('#object-1', 'some html here'); + * // Add a visual "changed" marker to the '#object-1' element. + * $commands[] = ajax_command_changed('#object-1'); + * // Output new markup to the browser and end the request. + * ajax_render($commands); + * @endcode + */ + +/** + * Render a commands array into JSON and exit. + * + * Commands are immediately handed back to the AJAX requester. This function + * will render and immediately exit. + * + * @param $commands + * A list of macro commands generated by the use of ajax_command_*() + * functions. + * @param $header + * If set to FALSE the 'text/javascript' header used by drupal_json() will + * not be used, which is necessary when using an IFRAME. If set to + * 'multipart' the output will be wrapped in a textarea, which can also be + * used as an alternative method when uploading files. + */ +function ajax_render($commands = array(), $header = TRUE) { + // Automatically extract any 'settings' added via drupal_add_js() and make + // them the first command. + $scripts = drupal_add_js(NULL, NULL); + if (!empty($scripts['settings'])) { + array_unshift($commands, ajax_command_settings($scripts['settings']['data'])); + } + + // Allow modules to alter any AJAX response. + drupal_alter('ajax_render', $commands); + + // Use === here so that bool TRUE doesn't match 'multipart'. + if ($header === 'multipart') { + // We do not use drupal_json() here because the header is not true. We are + // not really returning JSON, strictly-speaking, but rather JSON content + // wrapped in a textarea as per the "file uploads" example here: + // http://malsup.com/jquery/form/#code-samples + print ''; + } + else if ($header) { + drupal_json($commands); + } + else { + print drupal_to_js($commands); + } + exit; +} + +/** + * Send an error response back via AJAX and immediately exit. + * + * This function can be used to quickly create a command array with an error + * string and send it, short-circuiting the error handling process. + * + * @param $error + * A string to display in an alert. + */ +function ajax_render_error($error = '') { + $commands = array(); + $commands[] = ajax_command_error(empty($error) ? t('An error occurred while handling the request: The server received invalid input.') : $error); + ajax_render($commands); +} + +/** + * Get a form submitted via #ajax during an AJAX callback. + * + * This will load a form from the form cache used during AJAX operations. It + * pulls the form info from $_POST. + * + * @return + * An array containing the $form and $form_state. Use the list() function + * to break these apart: + * @code + * list($form, $form_state, $form_id, $form_build_id) = ajax_get_form(); + * @endcode + */ +function ajax_get_form() { + $form_state = form_state_defaults(); + + $form_build_id = $_POST['form_build_id']; + + // Get the form from the cache. + $form = form_get_cache($form_build_id, $form_state); + if (!$form) { + // If $form cannot be loaded from the cache, the form_build_id in $_POST + // must be invalid, which means that someone performed a POST request onto + // system/ajax without actually viewing the concerned form in the browser. + // This is likely a hacking attempt as it never happens under normal + // circumstances, so we just do nothing. + exit; + } + + // Since some of the submit handlers are run, redirects need to be disabled. + $form['#redirect'] = FALSE; + + // The form needs to be processed; prepare for that by setting a few internal + // variables. + $form_state['input'] = $_POST; + $form_state['args'] = $form['#args']; + $form_id = $form['#form_id']; + + return array($form, $form_state, $form_id, $form_build_id); +} + +/** + * Menu callback for AJAX callbacks through the #ajax['callback'] Form API property. + */ +function ajax_form_callback() { + list($form, $form_state, $form_id, $form_build_id) = ajax_get_form(); + + // Build, validate and if possible, submit the form. + drupal_process_form($form_id, $form, $form_state); + + // This call recreates the form relying solely on the $form_state that + // drupal_process_form() set up. + $form = drupal_rebuild_form($form_id, $form_state, $form_build_id); + + // Get the callback function from the clicked button. + $ajax = $form_state['clicked_button']['#ajax']; + $callback = $ajax['callback']; + if (drupal_function_exists($callback)) { + $html = $callback($form, $form_state); + + // If the returned value is a string, assume it is HTML and create + // a command object to return automatically. + if (is_string($html)) { + $commands = array(ajax_command_replace(NULL, $html)); + } + // Otherwise, $html is supposed to be an array of commands, suitable for + // Drupal.ajax, so we pass it on as is. + else { + $commands = $html; + } + + ajax_render($commands); + } + + // Return a 'do nothing' command if there was no callback. + ajax_render(array()); +} + +/** + * Add AJAX information about a form element to the page to communicate with JavaScript. + * + * If #ajax['path'] is set on an element, this additional JavaScript is added + * to the page header to attach the AJAX behaviors. See ajax.js for more + * information. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: + * - #ajax['event'] + * - #ajax['path'] + * - #ajax['wrapper'] + * - #ajax['parameters'] + * - #ajax['effect'] + * + * @return + * None. Additional code is added to the header of the page using + * drupal_add_js(). + */ +function ajax_process_form($element) { + $js_added = &drupal_static(__FUNCTION__, array()); + + // Add a reasonable default event handler if none was specified. + if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) { + switch ($element['#type']) { + case 'submit': + case 'button': + case 'image_button': + // Use the mousedown instead of the click event because form + // submission via pressing the enter key triggers a click event on + // submit inputs, inappropriately triggering AJAX behaviors. + $element['#ajax']['event'] = 'mousedown'; + // Attach an additional event handler so that AJAX behaviors + // can be triggered still via keyboard input. + $element['#ajax']['keypress'] = TRUE; + break; + + case 'password': + case 'textfield': + case 'textarea': + $element['#ajax']['event'] = 'blur'; + break; + + case 'radio': + case 'checkbox': + case 'select': + $element['#ajax']['event'] = 'change'; + break; + + default: + return $element; + } + } + + // Adding the same JavaScript settings twice will cause a recursion error, + // we avoid the problem by checking if the JavaScript has already been added. + if (!isset($js_added[$element['#id']]) && (isset($element['#ajax']['callback']) || isset($element['#ajax']['path'])) && isset($element['#ajax']['event'])) { + drupal_add_library('system', 'form'); + $element['#attached_js'] = array('misc/ajax.js'); + + $ajax_binding = array( + 'url' => isset($element['#ajax']['callback']) ? url('system/ajax') : url($element['#ajax']['path']), + 'event' => $element['#ajax']['event'], + 'keypress' => empty($element['#ajax']['keypress']) ? NULL : $element['#ajax']['keypress'], + 'wrapper' => empty($element['#ajax']['wrapper']) ? NULL : $element['#ajax']['wrapper'], + 'selector' => empty($element['#ajax']['selector']) ? '#' . $element['#id'] : $element['#ajax']['selector'], + 'effect' => empty($element['#ajax']['effect']) ? 'none' : $element['#ajax']['effect'], + 'speed ' => empty($element['#ajax']['effect']) ? 'none' : $element['#ajax']['effect'], + 'method' => empty($element['#ajax']['method']) ? 'replace' : $element['#ajax']['method'], + 'progress' => empty($element['#ajax']['progress']) ? array('type' => 'throbber') : $element['#ajax']['progress'], + 'button' => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE, + ); + + // Convert a simple #ajax['progress'] type string into an array. + if (is_string($ajax_binding['progress'])) { + $ajax_binding['progress'] = array('type' => $ajax_binding['progress']); + } + // Change progress path to a full URL. + if (isset($ajax_binding['progress']['path'])) { + $ajax_binding['progress']['url'] = url($ajax_binding['progress']['path']); + } + // Add progress.js if we're doing a bar display. + if ($ajax_binding['progress']['type'] == 'bar') { + drupal_add_js('misc/progress.js', array('cache' => FALSE)); + } + + drupal_add_js(array('ajax' => array($element['#id'] => $ajax_binding)), 'setting'); + + $js_added[$element['#id']] = TRUE; + $element['#cache'] = TRUE; + } + return $element; +} + +/** + * @} End of "defgroup ajax". + */ + +/** + * @defgroup ajax_commands AJAX framework commands + * @{ + */ + +/** + * Creates a Drupal AJAX 'alert' command. + * + * The 'alert' command instructs the client to display a JavaScript alert + * dialog box. + * + * This command is implemented by Drupal.ajax.prototype.commands.alert() + * defined in misc/ajax.js. + * + * @param $text + * The message string to dipslay to the user. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_alert($text) { + return array( + 'command' => 'alert', + 'text' => $text, + ); +} + +/** + * Creates a Drupal AJAX 'insert/replaceWith' command. + * + * The 'insert/replace' command instructs the client to use jQuery's + * replaceWith() method to replace each element matched matched by the given + * selector with the given HTML. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $html + * The data to use with the jQuery replaceWith() method. + * @param $settings + * An optional array of settings that will be used for this command only. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Manipulation/replaceWith#content + */ +function ajax_command_replace($selector, $html, $settings = NULL) { + return array( + 'command' => 'insert', + 'method' => 'replaceWith', + 'selector' => $selector, + 'data' => $html, + 'settings' => $settings, + ); +} + +/** + * Creates a Drupal AJAX 'insert/html' command. + * + * The 'insert/html' command instructs the client to use jQuery's html() + * method to set the HTML content of each element matched by the given + * selector while leaving the outer tags intact. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $html + * The data to use with the jQuery html() method. + * @param $settings + * An optional array of settings that will be used for this command only. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Attributes/html#val + */ +function ajax_command_html($selector, $html, $settings = NULL) { + return array( + 'command' => 'insert', + 'method' => 'html', + 'selector' => $selector, + 'data' => $html, + 'settings' => $settings, + ); +} + +/** + * Creates a Drupal AJAX 'insert/prepend' command. + * + * The 'insert/prepend' command instructs the client to use jQuery's prepend() + * method to prepend the given HTML content to the inside each element matched + * by the given selector. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $html + * The data to use with the jQuery prepend() method. + * @param $settings + * An optional array of settings that will be used for this command only. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Manipulation/prepend#content + */ +function ajax_command_prepend($selector, $html, $settings = NULL) { + return array( + 'command' => 'insert', + 'method' => 'prepend', + 'selector' => $selector, + 'data' => $html, + 'settings' => $settings, + ); +} + +/** + * Creates a Drupal AJAX 'insert/append' command. + * + * The 'insert/append' command instructs the client to use jQuery's append() + * method to append the given HTML content to the inside each element matched + * by the given selector. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $html + * The data to use with the jQuery append() method. + * @param $settings + * An optional array of settings that will be used for this command only. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Manipulation/append#content + */ +function ajax_command_append($selector, $html, $settings = NULL) { + return array( + 'command' => 'insert', + 'method' => 'append', + 'selector' => $selector, + 'data' => $html, + 'settings' => $settings, + ); +} + +/** + * Creates a Drupal AJAX 'insert/after' command. + * + * The 'insert/after' command instructs the client to use jQuery's after() + * method to insert the given HTML content after each element matched by + * the given selector. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $html + * The data to use with the jQuery after() method. + * @param $settings + * An optional array of settings that will be used for this command only. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Manipulation/after#content + */ +function ajax_command_after($selector, $html, $settings = NULL) { + return array( + 'command' => 'insert', + 'method' => 'after', + 'selector' => $selector, + 'data' => $html, + 'settings' => $settings, + ); +} + +/** + * Creates a Drupal AJAX 'insert/before' command. + * + * The 'insert/before' command instructs the client to use jQuery's before() + * method to insert the given HTML content before each of elements matched by + * the given selector. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $html + * The data to use with the jQuery before() method. + * @param $settings + * An optional array of settings that will be used for this command only. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Manipulation/before#content + */ +function ajax_command_before($selector, $html, $settings = NULL) { + return array( + 'command' => 'insert', + 'method' => 'before', + 'selector' => $selector, + 'data' => $html, + 'settings' => $settings, + ); +} + +/** + * Creates a Drupal AJAX 'remove' command. + * + * The 'remove' command instructs the client to use jQuery's remove() method + * to remove each of elements matched by the given selector, and everything + * within them. + * + * This command is implemented by Drupal.ajax.prototype.commands.remove() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Manipulation/remove#expr + */ +function ajax_command_remove($selector) { + return array( + 'command' => 'remove', + 'selector' => $selector, + ); +} + +/** + * Creates a Drupal AJAX 'changed' command. + * + * This command instructs the client to mark each of the elements matched by the + * given selector as 'ajax-changed'. + * + * This command is implemented by Drupal.ajax.prototype.commands.changed() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $asterisk + * An optional CSS selector which must be inside $selector. If specified, + * an asterisk will be appended to the HTML inside the $asterisk selector. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_changed($selector, $asterisk = '') { + return array( + 'command' => 'changed', + 'selector' => $selector, + 'star' => $asterisk, + ); +} + +/** + * Creates a Drupal AJAX 'css' command. + * + * The 'css' command will instruct the client to use the jQuery css() method + * to apply the CSS arguments to elements matched by the given selector. + * + * This command is implemented by Drupal.ajax.prototype.commands.insert() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $argument + * An array of key/value pairs to set in the CSS for the selector. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/CSS/css#properties + */ +function ajax_command_css($selector, $argument) { + return array( + 'command' => 'css', + 'selector' => $selector, + 'argument' => $argument, + ); +} + +/** + * Creates a Drupal AJAX 'settings' command. + * + * The 'settings' command instructs the client to extend Drupal.settings with + * the given array. + * + * This command is implemented by Drupal.ajax.prototype.commands.settings() + * defined in misc/ajax.js. + * + * @param $argument + * An array of key/value pairs to add to the settings. This will be utilized + * for all commands after this if they do not include their own settings + * array. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_settings($argument) { + return array( + 'command' => 'settings', + 'argument' => $argument, + ); +} + +/** + * Creates a Drupal AJAX 'data' command. + * + * The 'data' command instructs the client to attach the name=value pair of + * data to the selector via jQuery's data cache. + * + * This command is implemented by Drupal.ajax.prototype.commands.data() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. If the command is a response to a request from + * an #ajax form element then this value can be NULL. + * @param $name + * The name or key (in the key value pair) of the data attached to this + * selector. + * @param $value + * The value of the data. Not just limited to strings can be any format. + * + * @return + * An array suitable for use with the ajax_render() function. + * + * @see http://docs.jquery.com/Core/data#namevalue + */ +function ajax_command_data($selector, $name, $value) { + return array( + 'command' => 'data', + 'selector' => $selector, + 'name' => $name, + 'value' => $value, + ); +} + +/** + * Creates a Drupal AJAX 'restripe' command. + * + * The 'restripe' command instructs the client to restripe a table. This is + * usually used after a table has been modifed by a replace or append command. + * + * This command is implemented by Drupal.ajax.prototype.commands.restripe() + * defined in misc/ajax.js. + * + * @param $selector + * A jQuery selector string. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_restripe($selector) { + return array( + 'command' => 'restripe', + 'selector' => $selector, + ); +} + diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 2cf5e8e..d448a4a 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1,5 +1,5 @@ $ip))->fetchField(); + $denied = in_array($ip, $blocked_ips); + } + // Only check if database.inc is loaded already. If + // $conf['page_cache_without_database'] = TRUE; is set in settings.php, + // then the database won't be loaded here so the IPs in the database + // won't be denied. However the user asked explicitly not to use the + // database and also in this case it's quite likely that the user relies + // on higher performance solutions like a firewall. + elseif (function_exists('db_is_active')) { + $denied = (bool)db_query("SELECT 1 FROM {blocked_ips} WHERE ip = :ip", array(':ip' => $ip))->fetchField(); + } + return $denied; +} + +/** + * Handle denied users. + * + * @param $ip + * IP address to check. Prints a message and exits if access is denied. + */ +function drupal_block_denied($ip) { + // Deny access to blocked IP addresses - t() is not yet available. + if (drupal_is_denied($ip)) { + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + print 'Sorry, ' . check_plain(ip_address()) . ' has been banned.'; + exit(); } } @@ -1358,16 +1372,24 @@ function drupal_anonymous_user($session = '') { * DRUPAL_BOOTSTRAP_LANGUAGE: identify the language used on the page. * DRUPAL_BOOTSTRAP_PATH: set $_GET['q'] to Drupal path of request. * DRUPAL_BOOTSTRAP_FULL: Drupal is fully loaded, validate and fix input data. + * @param $new_phase + * A boolean, set to FALSE if calling drupal_bootstrap from inside a + * function called from drupal_bootstrap (recursion). */ -function drupal_bootstrap($phase = NULL) { +function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { + $final_phase = &drupal_static(__FUNCTION__ . '_final_phase'); + // When not recursing, store the phase name so it's not forgotten while + // recursing. + if ($new_phase) { + $final_phase = $phase; + } $phases = &drupal_static(__FUNCTION__ . '_phases', array( DRUPAL_BOOTSTRAP_CONFIGURATION, - DRUPAL_BOOTSTRAP_EARLY_PAGE_CACHE, + DRUPAL_BOOTSTRAP_PAGE_CACHE, DRUPAL_BOOTSTRAP_DATABASE, - DRUPAL_BOOTSTRAP_ACCESS, - DRUPAL_BOOTSTRAP_SESSION, DRUPAL_BOOTSTRAP_VARIABLES, - DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE, + DRUPAL_BOOTSTRAP_SESSION, + DRUPAL_BOOTSTRAP_PAGE_HEADER, DRUPAL_BOOTSTRAP_LANGUAGE, DRUPAL_BOOTSTRAP_PATH, DRUPAL_BOOTSTRAP_FULL, @@ -1375,10 +1397,16 @@ function drupal_bootstrap($phase = NULL) { $completed_phase = &drupal_static(__FUNCTION__ . '_completed_phase', -1); if (isset($phase)) { - while ($phases && $phase > $completed_phase) { + // Call a phase if it has not been called before and is below the requested + // phase. + while ($phases && $phase > $completed_phase && $final_phase > $completed_phase) { $current_phase = array_shift($phases); _drupal_bootstrap($current_phase); - $completed_phase = $current_phase; + // This function is reentrant. Only update the completed phase when the + // current call actually resulted in a progress in the bootstrap process. + if ($current_phase > $completed_phase) { + $completed_phase = $current_phase; + } } } return $completed_phase; @@ -1397,6 +1425,7 @@ function drupal_get_bootstrap_phase() { function _drupal_bootstrap($phase) { global $conf, $user; + static $cache; switch ($phase) { @@ -1408,16 +1437,45 @@ function _drupal_bootstrap($phase) { drupal_settings_initialize(); break; - case DRUPAL_BOOTSTRAP_EARLY_PAGE_CACHE: + case DRUPAL_BOOTSTRAP_PAGE_CACHE: // Allow specifying special cache handlers in settings.php, like // using memcached or files for storing cache information. require_once DRUPAL_ROOT . '/' . variable_get('cache_inc', 'includes/cache.inc'); - // If the page_cache_fastpath is set to TRUE in settings.php and - // page_cache_fastpath (implemented in the special implementation of - // cache.inc) printed the page and indicated this with a returned TRUE - // then we are done. - if (variable_get('page_cache_fastpath', FALSE) && page_cache_fastpath()) { - exit; + // Check for a cache mode force from settings.php. + if (variable_get('page_cache_without_database')) { + $cache_mode = CACHE_NORMAL; + } + else { + drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE); + $cache_mode = variable_get('cache'); + } + drupal_block_denied(ip_address()); + // If there is no session cookie and cache is enabled (or forced), try + // to serve a cached page. + if (!isset($_COOKIE[session_name()]) && $cache_mode == CACHE_NORMAL) { + // Make sure there is a user object because it's timestamp will be + // checked, hook_boot might check for anonymous user etc. + $user = drupal_anonymous_user(); + // Get the page from the cache. + $cache = drupal_page_get_cache(); + // If there is a cached page, display it. + if (is_object($cache)) { + // If the skipping of the bootstrap hooks is not enforced, call + // hook_boot. + if (variable_get('page_cache_invoke_hooks', TRUE)) { + require_once DRUPAL_ROOT . '/includes/module.inc'; + module_invoke_all('boot'); + } + header('X-Drupal-Cache: HIT'); + drupal_serve_page_from_cache($cache); + // If the skipping of the bootstrap hooks is not enforced, call + // hook_exit. + if (variable_get('page_cache_invoke_hooks', TRUE)) { + module_invoke_all('exit'); + } + // We are done. + exit; + } } break; @@ -1437,13 +1495,9 @@ function _drupal_bootstrap($phase) { spl_autoload_register('drupal_autoload_interface'); break; - case DRUPAL_BOOTSTRAP_ACCESS: - // Deny access to blocked IP addresses - t() is not yet available. - if (drupal_is_denied(ip_address())) { - header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); - print 'Sorry, ' . check_plain(ip_address()) . ' has been banned.'; - exit(); - } + case DRUPAL_BOOTSTRAP_VARIABLES: + // Load variables from the database, but do not overwrite variables set in settings.php. + $conf = variable_initialize(isset($conf) ? $conf : array()); break; case DRUPAL_BOOTSTRAP_SESSION: @@ -1451,41 +1505,9 @@ function _drupal_bootstrap($phase) { drupal_session_initialize(); break; - case DRUPAL_BOOTSTRAP_VARIABLES: - // Load variables from the database, but do not overwrite variables set in settings.php. - $conf = variable_initialize(isset($conf) ? $conf : array()); - break; - - case DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE: - $cache_mode = variable_get('cache', CACHE_DISABLED); - - // Get the page from the cache. - if ($cache_mode != CACHE_DISABLED) { - $cache = drupal_page_get_cache(); - } - else { - $cache = FALSE; - } - - // If the skipping of the bootstrap hooks is not enforced, call hook_boot. - if (!$cache || $cache_mode != CACHE_AGGRESSIVE) { - // Load module handling. - require_once DRUPAL_ROOT . '/includes/module.inc'; - module_invoke_all('boot'); - } - - // If there is a cached page, display it. - if ($cache) { - header('X-Drupal-Cache: HIT'); - drupal_serve_page_from_cache($cache); - // If the skipping of the bootstrap hooks is not enforced, call hook_exit. - if ($cache_mode != CACHE_AGGRESSIVE) { - module_invoke_all('exit'); - } - // We are done. - exit; - } - + case DRUPAL_BOOTSTRAP_PAGE_HEADER: + require_once DRUPAL_ROOT . '/includes/module.inc'; + module_invoke_all('boot'); if (!$cache && drupal_page_is_cacheable()) { header('X-Drupal-Cache: MISS'); } diff --git a/includes/browser.inc b/includes/browser.inc new file mode 100644 index 0000000..7cc7ac6 --- /dev/null +++ b/includes/browser.inc @@ -0,0 +1,1139 @@ +get('http://example.com'); + * @endcode + * The result of the GET request can be accessed in two ways: 1) the get() + * method returns an array defining the result of the request, or 2) the + * individual properties can be accessed from the browser instance via their + * respective access methods. The following demonstrates the properties that + * are avaialable and how to access them. + * @code + * $browser->getUrl(); + * $browser->getResponseHeaders(); + * $browser->getContent(); + * @endcode + * + * When performing a POST request the following format is used. + * @code + * $browser = new Browser(); + * $post = array( + * 'field_name1' => 'foo', + * 'checkbox1' => TRUE, + * 'multipleselect1[]' => array( + * 'value1', + * 'value2', + * ), + * ); + * $browser->post('http://example.com/form', $post, 'Submit button text'); + * @endcode + * To submit a multi-step form or to post to the current page the URL passed to + * post() may be set to NULL. If there were two steps on the form shown in the + * example above with the mutliple select field on the second page and a submit + * button with the title "Next" on the first page the code be as follows. + * @code + * $browser = new Browser(); + * $post = array( + * 'field_name1' => 'foo', + * 'checkbox1' => TRUE, + * ); + * $browser->post('http://example.com/form', $post, 'Next'); + * + * $post = array( + * 'multipleselect1[]' => array( + * 'value1', + * 'value2', + * ), + * ); + * $browser->post(NULL, $post, 'Final'); + * @endcode + */ + +/** + * Browser API class. + * + * All browser functionality is provided by this main class which manages the + * various aspects of the browser. + */ +class Browser { + + /** + * Flag indicating if curl is available. + * + * @var boolean + */ + protected $curl; + + /** + * The handle of the current curl connection. + * + * @var resource + */ + protected $handle; + + /** + * The current cookie file used by curl. + * + * Cookies are not reused so they can be stored in memory instead of a file. + * + * @var mixed + */ + protected $cookieFile = NULL; + + /** + * The request headers. + * + * @var array + */ + protected $requestHeaders = array(); + + /** + * The URL of the current page. + * + * @var string + */ + protected $url; + + /** + * The response headers of the current page. + * + * @var Array + */ + protected $headers = array(); + + /** + * The raw content of the current page. + * + * @var string + */ + protected $content; + + /** + * The BrowserPage class representing to the current page. + * + * @var BrowserPage + */ + protected $page; + + /** + * Initialize the browser. + * + * @param $force_stream + * Force the use of the PHP stream wrappers insead of CURL. This is used + * during testing to force the use of the stream wrapper so it can be + * tested. + */ + public function __construct($force_stream = FALSE) { + $this->curl = $force_stream ? FALSE : function_exists('curl_init'); + $this->setUserAgent('Drupal (+http://drupal.org/)'); + + if ($this->curl) { + $this->handle = curl_init(); + curl_setopt_array($this->handle, $this->curlOptions()); + } + else { + $this->handle = stream_context_create(); + } + } + + /** + * Check the the method is supported by the backend. + * + * @param $method + * The method string identifier. + */ + public function isMethodSupported($method) { + return $method == 'GET' || $method == 'POST'; + } + + /** + * Get the request headers. + * + * The request headers are sent in every request made by the browser with a + * few changes made the the individual request methods. + * + * @return + * Associative array of request headers. + */ + public function getRequestHeaders() { + return $this->requestHeaders; + } + + /** + * Set the request headers. + * + * @param $headers + * Associative array of request headers. + */ + public function setRequestHeaders(array $headers) { + $this->requestHeaders = $headers; + } + + /** + * Get the user-agent that the browser is identifying itself as. + * + * @return + * Browser user-agent. + */ + public function getUserAgent() { + return $this->requestHeaders['User-Agent']; + } + + /** + * Set the user-agent that the browser will identify itself as. + * + * @param $agent + * User-agent to to identify as. + */ + public function setUserAgent($agent) { + $this->requestHeaders['User-Agent'] = $agent; + } + + /** + * Get the URL of the current page. + * + * @return + * The URL of the current page. + */ + public function getUrl() { + return $this->url; + } + + /** + * Get the response headers of the current page. + * + * @return + * The response headers of the current page. + */ + public function getResponseHeaders() { + return $this->headers; + } + + /** + * Get the raw content of the current page. + * + * @return + * The raw content for the current page. + */ + public function getContent() { + return $this->content; + } + + /** + * Get the BrowserPage instance for the current page. + * + * If the raw content is new and the page has not yet been parsed then parse + * the content and ensure that it is valid. + * + * @return + * BrowserPage instance for the current page. + */ + public function getPage() { + if (!isset($this->page)) { + $this->page = new BrowserPage($this->url, $this->headers, $this->content); + } + return $this->page; + } + + /** + * Get the current state of the browser. + * + * @return + * An associative array containing state information, including: 1) url, 2) + * headers, 3) content. + * @see getUrl() + * @see getResponseHeaders() + * @see getContent() + */ + public function getState() { + return array( + 'url' => $this->url, + 'headers' => $this->headers, + 'content' => $this->content, + ); + } + + /** + * Set the state of the browser. + * + * @param $url + * The URL of the current page. + * @param $headers + * The response headers of the current page. + * @param $content + * The raw content of the current page. + */ + public function setState($url, $headers, $content) { + $this->url = $url; + $this->headers = $headers; + $this->content = $content; + + // Clear the page variable since the content has change. + unset($this->page); + + $this->checkForRefresh(); + } + + /** + * Perform a GET request. + * + * @param $url + * Absolute URL to request. + * @return + * Associative array of state information, as returned by getState(). + * @see getState(). + */ + public function get($url) { + if ($this->curl) { + $this->curlExecute(array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + )); + } + else { + $this->streamExecute($url, array( + 'method' => 'GET', + 'header' => array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + )); + } + + $this->refreshCheck(); + + return $this->getState(); + } + + /** + * Perform a POST request. + * + * @param $url + * Absolute URL to request, or NULL to submit the current page. + * @param $fields + * Associative array of fields to submit as POST variables. + * @param $submit + * Text contained in 'value' properly of submit button of which to press. + * @return + * Associative array of state information, as returned by + * browser_state_get(). + * @see browser_state_get() + */ + public function post($url, array $fields, $submit) { + // If URL is set then request the page, otherwise use the current page. + if ($url) { + $this->get($url); + } + else { + $url = $this->url; + } + + if (($page = $this->getPage()) === FALSE) { + return FALSE; + } + + if (($form = $this->findForm($fields, $submit)) === FALSE) { + return FALSE; + } + + // If form specified action then use that for the post url. + if ($form['action']) { + $url = $page->getAbsoluteUrl($form['action']); + } + + if ($this->curl) { + $this->curlExecute(array( + CURLOPT_POST => TRUE, + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => http_build_query($form['post'], NULL, '&'), + )); + } + else { + $this->streamExecute($url, array( + 'method' => 'POST', + 'header' => array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + 'content' => http_build_query($form['post'], NULL, '&'), + )); + } + + $this->refreshCheck(); + + return $this->getState(); + } + + /** + * Find the the form that patches the conditions. + * + * @param $fields + * Associative array of fields to submit as POST variables. + * @param $submit + * Text contained in 'value' properly of submit button of which to press. + * @return + * Form action and the complete post array containing default values if not + * overridden, or FALSE if no form matching the conditions was found. + */ + protected function findForm(array $fields, $submit) { + $page = $this->getPage(); + + $forms = $page->getForms(); + foreach ($forms as $form) { + if (($post = $this->processForm($form, $fields, $submit)) !== FALSE) { + $action = (isset($form['action']) ? (string) $form['action'] : FALSE); + return array( + 'action' => $action, + 'post' => $post, + ); + } + } + return FALSE; + } + + /** + * Check the conditions against the specified form and process values. + * + * @param $form + * Form SimpleXMLElement object. + * @param $fields + * Associative array of fields to submit as POST variables. + * @param $submit + * Text contained in 'value' properly of submit button of which to press. + * @return + * The complete post array containing default values if not overridden, or + * FALSE if no form matching the conditions was found. + */ + protected function processForm($form, $fields, $submit) { + $page = $this->getPage(); + + $post = array(); + $submit_found = FALSE; + $inputs = $page->getInputs($form); + foreach ($inputs as $input) { + $name = (string) $input['name']; + $html_value = isset($input['value']) ? (string) $input['value'] : ''; + + // Get type from input vs textarea and select. + $type = isset($input['type']) ? (string) $input['type'] : $input->getName(); + + if (isset($fields[$name])) { + if ($type == 'file') { + // Make sure the file path is the absolute path. + $file = realpath($fields[$name]); + if ($file && is_file($file)) { + // Signify that the post field is a file in case backend needs to + // perform additional processing. + $post[$name] = '@' . $file; + } + // Known type, field processed. + unset($fields[$name]); + } + elseif (($processed_value = $this->processField($input, $type, $fields[$name], $html_value)) !== NULL) { + // Value may be ommitted (checkbox). + if ($processed_value !== FALSE) { + if (is_array($processed_value)) { + $post += $processed_value; + } + else { + $post[$name] = $processed_value; + } + } + // Known type, field processed. + unset($fields[$name]); + } + } + + // No post value for the field means that: no post field value specified, + // the value does not match the field (checkbox, radio, select), or the + // field is of an unknown type. + if (!isset($post[$name])) { + // No value specified so use default value (value in HTML). + if (($default_value = $this->getDefaultFieldValue($input, $type, $html_value)) !== NULL) { + $post[$name] = $default_value; + unset($fields[$name]); + } + } + + // Check if the + if (($type == 'submit' || $type == 'image') && $submit == $html_value) { + $post[$name] = $html_value; + $submit_found = TRUE; + } + } + + if ($submit_found) { + return $post; + } + return FALSE; + } + + /** + * Get the value to be sent for the specified field. + * + * @param $input + * Input SimpleXMLElement object. + * @param $type + * Input type: text, textarea, password, radio, checkbox, or select. + * @param $new_value + * The new value to be assigned to the input. + * @param $html_value + * The cleaned default value for the input from the HTML value. + */ + protected function processField($input, $type, $new_value, $html_value) { + switch ($type) { + case 'text': + case 'textarea': + case 'password': + return $new_value; + case 'radio': + if ($new_value == $html_value) { + return $new_value; + } + return NULL; + case 'checkbox': + // If $new_value is set to FALSE then ommit checkbox value, otherwise + // pass original value. + if ($new_value === FALSE) { + return FALSE; + } + return $html_value; + case 'select': + // Remove the ending [] from multi-select element name. + $key = preg_replace('/\[\]$/', '', (string) $input['name']); + + $options = $page->getSelectOptions($input); + $index = 0; + $out = array(); + foreach ($options as $value => $text) { + if (is_array($value)) { + if (in_array($value, $new_value)) { + $out[$key . '[' . $index++ . ']'] = $value; + } + } + elseif ($new_value == $value) { + return $new_value; + } + } + return ($out ? $out : NULL); + default: + return NULL; + } + } + + /** + * Get the cleaned default value for the input from the HTML value. + * + * @param $input + * Input SimpleXMLElement object. + * @param $type + * Input type: text, textarea, password, radio, checkbox, or select. + * @param $html_value + * The default value for the input, as specified in the HTML. + */ + protected function getDefaultFieldValue($input, $type, $html_value) { + switch ($type) { + case 'textarea': + return (string) $input; + case 'select': + // Remove the ending [] from multi-select element name. + $key = preg_replace('/\[\]$/', '', (string) $input['name']); + $single = empty($input['multiple']); + + $options = $page->getSelectOptionElements($input); + $first = TRUE; + $index = 0; + $out = array(); + foreach ($options as $option) { + // For single select, we load the first option, if there is a + // selected option that will overwrite it later. + if ($option['selected'] || ($first && $single)) { + $first = FALSE; + if ($single) { + $out[$key] = (string) $option['value']; + } + else { + $out[$key . '[' . $index++ . ']'] = (string) $option['value']; + } + } + return ($single ? $out[$key] : $out); + } + break; + case 'file': + return NULL; + case 'radio': + case 'checkbox': + if (!isset($input['checked'])) { + return NULL; + } + // Deliberately no break. + default: + return $html_value; + } + } + + /** + * Perform a request of arbitrary type. + * + * Please use get() and post() for GET and POST requests respectively. + * + * @param $method + * The method string identifier. + * @param $url + * Absolute URL to request. + * @param $additional + * Additional parameters related to the particular request method. + * @return + * Associative array of state information, as returned by getState(). + * @see getState(). + */ + public function request($method, $url, array $additional) { + if (!$this->isMethodSupported($method)) { + return FALSE; + } + + // TODO + } + + /** + * Perform the request using the PHP stream wrapper. + * + * @param $url + * The url to request. + * @param $options + * The HTTP stream context options to be passed to + * stream_context_set_params(). + */ + protected function streamExecute($url, array $options) { + // Global variable provided by PHP stream wapper. + global $http_response_header; + + if (!isset($options['header'])) { + $options['header'] = array(); + } + + // Merge default request headers with the passed headers and generate + // header string to be sent in http request. + $headers = $this->requestHeaders + $options['header']; + $options['header'] = $this->headerString($headers); + + // Update the handler options. + stream_context_set_params($this->handle, array( + 'options' => array( + 'http' => $options, + ) + )); + + // Make the request. + $this->content = file_get_contents($url, FALSE, $this->handle); + $this->url = $url; + $this->headers = $this->headerParseAll($http_response_header); + unset($this->page); + } + + + /** + * Perform curl_exec() with the specified option changes. + * + * @param $options + * Curl options to set, any options not set will maintain their previous + * value. + */ + function curlExecute(array $options) { + // Headers need to be reset since callback appends. + $this->headers = array(); + + // Ensure that request headers are up to date. + curl_setopt($this->handle, CURLOPT_USERAGENT, $this->requestHeaders['User-Agent']); + curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->requestHeaders); + + curl_setopt_array($this->handle, $options); + $this->content = curl_exec($this->handle); + $this->url = curl_getinfo($this->handle, CURLINFO_EFFECTIVE_URL); + + // $this->headers should be filled by $this->curlHeaderCallback(). + unset($this->page); + } + + /** + * Get the default curl options to be used with each request. + * + * @return + * Default curl options. + */ + protected function curlOptions() { + return array( + CURLOPT_COOKIEJAR => $this->cookieFile, + CURLOPT_FOLLOWLOCATION => TRUE, + CURLOPT_HEADERFUNCTION => array($this, 'curlHeaderCallback'), + CURLOPT_HTTPHEADER => $this->requestHeaders, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => FALSE, + CURLOPT_SSL_VERIFYHOST => FALSE, + CURLOPT_URL => '/', + CURLOPT_USERAGENT => $this->requestHeaders['User-Agent'], + ); + } + + /** + * Reads reponse headers and stores in $headers array. + * + * @param $curlHandler + * The curl handler. + * @param $header + * An header. + * @return + * The string length of the header. (required by curl) + */ + protected function curlHeaderCallback($handler, $header) { + // Ignore blank header lines. + $clean_header = trim($header); + if ($clean_header) { + $this->headers += $this->headerParse($clean_header); + } + + // Curl requires strlen() to be returned. + return strlen($header); + } + + /** + * Generate a header string given he associative array of headers. + * + * @param $headers + * Associative array of headers. + * @return + * Header string to be used with stream. + */ + protected function headerString(array $headers) { + $string = ''; + foreach ($headers as $key => $header) { + $string .= "$key: $header\r\n"; + } + return $string; + } + + /** + * Parse the response header array to create an associative array. + * + * @param $headers + * Array of headers. + * @return + * An associative array of headers. + */ + protected function headerParseAll(array $headers) { + $out = array(); + foreach ($headers as $header) { + $out += $this->headerParse($header); + } + return $out; + } + + /** + * Parse an individual header into name and value. + * + * @param $header + * A string header string. + * @return + * Parsed header as array($name => $value), or array() if parse failed. + */ + protected function headerParse($header) { + $parts = explode(':', $header, 2); + + // Ensure header line is valid. + if (count($parts) == 2) { + $name = $this->headerName(trim($parts[0])); + return array($name => trim($parts[1])); + } + return array(); + } + + /** + * Ensure that header name is formatted with all lowercase letters. + * + * @param $name + * Header name to format. + * @return + * Formatted header name. + */ + protected function headerName($name) { + return strtolower($name); + } + + /** + * Check for a refresh signifier. + * + * A refresh signifier can either be the 'Location' HTTP header or the meta + * tag 'http-equiv="Refresh"'. + */ + function refreshCheck() { + // If not handled by backend wrapper then go ahead and handle. + if (isset($this->headers['Location'])) { + // Expect absolute URL. + $this->get($this->headers['Location']); + } + + if (($page = $this->getPage()) !== FALSE && ($tag = $page->getMetaTag('Refresh', 'http-equiv'))) { + // Parse the content attribute of the meta tag for the format: + // "[delay]: URL=[path_to_redirect_to]". + if (preg_match('/\d+;\s*URL=(?P.*)/i', $tag['content'], $match)) { + $this->get($page->getAbsoluteUrl(decode_entities($match['url']))); + } + } + } + + /** + * Check for a refresh signifier. + * + * A refresh signifier can either be the 'Location' HTTP header or the meta + * tag 'http-equiv="Refresh"'. + */ + protected function checkForRefresh() { + // If not handled by backend wrapper then go ahead and handle. + if (isset($this->headers['Location'])) { + // Expect absolute URL. + $this->get($this->headers['Location']); + } + + if (($page = $this->getPage()) !== FALSE && ($tag = $page->getMetaTag('Refresh', 'http-equiv'))) { + // Parse the content attribute of the meta tag for the format: + // "[delay]: URL=[path_to_redirect_to]". + if (preg_match('/\d+;\s*URL=(?P.*)/i', $tag['content'], $match)) { + $this->get($page->getAbsoluteUrl(decode_entities($match['url']))); + } + } + } + + /** + * Close the wrapper connection. + */ + function __destruct() { + if (isset($this->handle)) { + if ($this->curl) { + curl_close($this->handle); + } + unset($this->handle); + } + } +} + + +/** + * Represents a page of content that has been fetched by the Browser. The class + * provides a number of convenience methods that relate to page content. + */ +class BrowserPage { + + /** + * The URL of the page. + * + * @var string + */ + protected $url; + + /** + * The response headers of the page. + * + * @var Array + */ + protected $headers; + + /** + * The root element of the page. + * + * @var SimpleXMLElement + */ + protected $root; + + /** + * Initialize the BrowserPage with the page state information. + * + * @param $url + * The URL of the page. + * @param $headers + * The response headers of the page. + * @param $content + * The raw content of the page. + */ + public function BrowserPage($url, $headers, $content) { + $this->url = $url; + $this->headers = $headers; + $this->root = $this->load($content); + } + + /** + * Attempt to parse the raw content using DOM and import it into SimpleXML. + * + * @param $content + * The raw content of the page. + * @return + * The root element of the page, or FALSE. + */ + protected function load($content) { + // Use DOM to load HTML soup, and hide warnings. + $document = @DOMDocument::loadHTML($content); + if ($document) { + return simplexml_import_dom($document); + } + return FALSE; + } + + /** + * Check if the raw content is valid and could be parse. + * + * @return + * TRUE if content is valid, otherwise FALSE. + */ + public function isValid() { + return ($this->root !== FALSE); + } + + /** + * Perform an xpath search on the contents of the page. + * + * The search is relative to the root element, usually the HTML tag, of the + * page. To perform a search using a different root element follow the + * example below. + * @code + * $parent = $page->xpath('.//parent'); + * $parent[0]->xpath('//children'); + * @endcode + * + * @param $xpath + * The xpath string. + * @return + * An array of SimpleXMLElement objects or FALSE in case of an error. + * @link http://us.php.net/manual/function.simplexml-element-xpath.php + */ + public function xpath($xpath) { + if ($this->isValid()) { + return $this->root->xpath($xpath); + } + return FALSE; + } + + /** + * Get all the meta tags. + * + * @return + * An array of SimpleXMLElement objects representing meta tags. + */ + public function getMetaTags() { + return $this->xpath('//meta'); + } + + /** + * Get a specific meta tag. + * + * @param $key + * The meta tag key. + * @param $type + * The type of meta tag, either: 'name' or 'http-equiv'. + * @return + * A SimpleXMLElement object representing the meta tag, or FALSE if not + * found. + */ + public function getMetaTag($key, $type = 'name') { + if ($tags = $this->getMetaTags()) { + foreach ($tags as $tag) { + if ($tag[$type] == $key) { + return $tag; + } + } + } + return FALSE; + } + + /** + * Get all the form elements. + * + * @return + * An array of SimpleXMLElement objects representing form elements. + */ + public function getForms() { + return $this->xpath('//form'); + } + + /** + * Get all the input elements, or only those nested within a parent element. + * + * @param $parent + * SimpleXMLElement representing the parent to search within. + * @return + * An array of SimpleXMLElement objects representing form elements. + */ + public function getInputs($parent = NULL) { + if ($parent) { + return $parent->xpath('.//input|.//textarea|.//select'); + } + return $this->xpath('.//input|.//textarea|.//select'); + } + + /** + * Get all the options contained by a select, including nested options. + * + * @param $select + * SimpleXMLElement representing the select to extract option from. + * @return + * Associative array where the keys represent each option value and the + * value is the text contained within the option tag. For example: + * @code + * array( + * 'option1' => 'Option 1', + * 'option2' => 'Option 2', + * ) + * @endcode + */ + public function getSelectOptions(SimpleXMLElement $select) { + $elements = $this->getSelectOptionElements($select); + + $options = array(); + foreach ($elements as $element) { + $options[(string) $element['value']] = $this->asText($element); + } + return $options; + } + + /** + * Get all selected options contained by a select, including nested options. + * + * @param $select + * SimpleXMLElement representing the select to extract option from. + * @return + * Associative array of selected items in the format described by + * BrowserPage->getSelectOptions(). + * @see BrowserPage->getSelectOptions() + */ + public function getSelectedOptions(SimpleXMLElement $select) { + $elements = getSelectOptionElements($select); + + $options = array(); + foreach ($elements as $element) { + if (isset($elements['selected'])) { + $options[(string) $element['value']] = asText($element); + } + } + return $options; + } + + /** + * Get all the options contained by a select, including nested options. + * + * @param $element + * SimpleXMLElement representing the select to extract option from. + * @return + * An array of SimpleXMLElement objects representing option elements. + */ + public function getSelectOptionElements(SimpleXMLElement $element) { + $options = array(); + + // Add all options items. + foreach ($element->option as $option) { + $options[] = $option; + } + + // Search option group children. + if (isset($element->optgroup)) { + foreach ($element->optgroup as $group) { + $options = array_merge($options, $this->getSelectOptionElements($group)); + } + } + return $options; + } + + /** + * Get the absolute URL for a given path, relative to the page. + * + * @param + * A path relative to the page or absolute. + * @return + * An absolute path. + */ + public function getAbsoluteUrl($path) { + $parts = @parse_url($path); + if (isset($parts['scheme'])) { + return $path; + } + + $base = $this->getBaseUrl(); + if ($path[0] == '/') { + // Lead / then use host as base. + $parts = parse_url($base); + $base = $parts['scheme'] . '://' . $parts['host']; + } + return $base . $path; + } + + /** + * Get the base URL of the page. + * + * If a 'base' HTML element is defined then the URL it defines is used as the + * base URL for the page, otherwise the page URL is used to determine the + * base URL. + * + * @return + * The base URL of the page. + */ + public function getBaseUrl() { + // Check for base element. + $elements = $this->xpath('.//base'); + if ($elements) { + // More than one may be specified. + foreach ($elements as $element) { + if (isset($element['href'])) { + $base = (string) $element['href']; + break; + } + } + } + else { + $base = $this->url; + if ($pos = strpos($base, '?')) { + // Remove query string. + $base = substr($base, 0, $pos); + } + + // Ignore everything after the last forward slash. + $base = substr($base, 0, strrpos($base, '/')); + } + + // Ensure that the last character is a forward slash. + if ($base[strlen($base) - 1] != '/') { + $base .= '/'; + } + return $base; + } + + /** + * Extract the text contained by the element. + * + * Strips all XML/HTML tags, decodes HTML entities, and trims the result. + * + * @param $element + * SimpleXMLElement to extract text from. + * @return + * Extracted text. + */ + public function asText(SimpleXMLElement $element) { + return trim(html_entity_decode(strip_tags($element->asXML()))); + } +} + +/** + * @} End of "defgroup browser". + */ diff --git a/includes/common.inc b/includes/common.inc index 66b250e..ec04b95 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -1,5 +1,5 @@ $fields) { - $keys[] = 'UNIQUE KEY ' . $key . ' (' . $this->createKeysSqlHelper($fields) . ')'; + $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeysSqlHelper($fields) . ')'; } } if (!empty($spec['indexes'])) { foreach ($spec['indexes'] as $index => $fields) { - $keys[] = 'INDEX ' . $index . ' (' . $this->createKeysSqlHelper($fields) . ')'; + $keys[] = 'INDEX `' . $index . '` (' . $this->createKeysSqlHelper($fields) . ')'; } } @@ -242,10 +242,10 @@ class DatabaseSchema_mysql extends DatabaseSchema { $ret = array(); foreach ($fields as $field) { if (is_array($field)) { - $ret[] = $field[0] . '(' . $field[1] . ')'; + $ret[] = '`' . $field[0] . '`(' . $field[1] . ')'; } else { - $ret[] = $field; + $ret[] = '`' . $field . '`'; } } return implode(', ', $ret); @@ -255,10 +255,10 @@ class DatabaseSchema_mysql extends DatabaseSchema { $ret = array(); foreach ($fields as $field) { if (is_array($field)) { - $ret[] = $field[0] . '(' . $field[1] . ')'; + $ret[] = '`' . $field[0] . '`(' . $field[1] . ')'; } else { - $ret[] = $field; + $ret[] = '`' . $field . '`'; } } return implode(', ', $ret); @@ -297,7 +297,7 @@ class DatabaseSchema_mysql extends DatabaseSchema { } public function dropField(&$ret, $table, $field) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP ' . $field); + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP `' . $field . '`'); } public function fieldSetDefault(&$ret, $table, $field, $default) { @@ -308,11 +308,11 @@ class DatabaseSchema_mysql extends DatabaseSchema { $default = is_string($default) ? "'$default'" : $default; } - $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' SET DEFAULT ' . $default); + $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $default); } public function fieldSetNoDefault(&$ret, $table, $field) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' DROP DEFAULT'); + $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT'); } public function addPrimaryKey(&$ret, $table, $fields) { @@ -324,20 +324,20 @@ class DatabaseSchema_mysql extends DatabaseSchema { } public function addUniqueKey(&$ret, $table, $name, $fields) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD UNIQUE KEY ' . $name . ' (' . $this->createKeySql($fields) . ')'); + $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')'); } public function dropUniqueKey(&$ret, $table, $name) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP KEY ' . $name); + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`'); } public function addIndex(&$ret, $table, $name, $fields) { - $query = 'ALTER TABLE {' . $table . '} ADD INDEX ' . $name . ' (' . $this->createKeySql($fields) . ')'; + $query = 'ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')'; $ret[] = update_sql($query); } public function dropIndex(&$ret, $table, $name) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP INDEX ' . $name); + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`'); } public function changeField(&$ret, $table, $field, $field_new, $spec, $keys_new = array()) { diff --git a/includes/database/pgsql/CVS/Entries b/includes/database/pgsql/CVS/Entries index 5d217c4..a55dabd 100644 --- a/includes/database/pgsql/CVS/Entries +++ b/includes/database/pgsql/CVS/Entries @@ -1,5 +1,5 @@ /database.inc/1.26/Sat Jun 13 03:10:06 2009// /install.inc/1.4/Fri Aug 7 05:58:21 2009// /query.inc/1.14/Fri Aug 7 05:58:21 2009// -/schema.inc/1.18/Tue Aug 11 21:17:14 2009// +/schema.inc/1.19/Mon Aug 17 16:10:24 2009// D diff --git a/includes/database/pgsql/schema.inc b/includes/database/pgsql/schema.inc index 3599580..31b5975 100644 --- a/includes/database/pgsql/schema.inc +++ b/includes/database/pgsql/schema.inc @@ -1,5 +1,5 @@ _createKeySql($fields) . ')'; return $query; } diff --git a/includes/database/sqlite/CVS/Entries b/includes/database/sqlite/CVS/Entries index 7aa2d98..2b3c272 100644 --- a/includes/database/sqlite/CVS/Entries +++ b/includes/database/sqlite/CVS/Entries @@ -1,5 +1,5 @@ /database.inc/1.19/Tue Jun 30 03:30:38 2009// /install.inc/1.2/Fri Aug 7 05:58:21 2009// /query.inc/1.7/Fri Aug 7 05:58:21 2009// -/schema.inc/1.7/Thu Jul 23 09:28:47 2009// +/schema.inc/1.8/Mon Aug 17 16:10:24 2009// D diff --git a/includes/database/sqlite/schema.inc b/includes/database/sqlite/schema.inc index eed7a30..9123c42 100644 --- a/includes/database/sqlite/schema.inc +++ b/includes/database/sqlite/schema.inc @@ -1,5 +1,5 @@ $fields) { - $sql[] = 'CREATE UNIQUE INDEX {' . $tablename . '}_' . $key . ' ON {' . $tablename . '} (' . $this->createKeySql($fields) . "); \n"; + $sql[] = 'CREATE UNIQUE INDEX "{' . $tablename . '}_' . $key . '" ON {' . $tablename . '} (' . $this->createKeySql($fields) . "); \n"; } } if (!empty($schema['indexes'])) { foreach ($schema['indexes'] as $index => $fields) { - $sql[] = 'CREATE INDEX {' . $tablename . '}_' . $index . ' ON {' . $tablename . '} (' . $this->createKeySql($fields) . "); \n"; + $sql[] = 'CREATE INDEX "{' . $tablename . '}_' . $index . '" ON {' . $tablename . '} (' . $this->createKeySql($fields) . "); \n"; } } return $sql; diff --git a/includes/file.inc b/includes/file.inc index ef15107..ebc76a7 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -1,5 +1,5 @@ fields('f'); // If the $fids array is populated, add those to the query. diff --git a/includes/form.inc b/includes/form.inc index d334dab..a546602 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -1,5 +1,5 @@ $element['#attributes'], '#parents' => $element['#parents'], '#id' => form_clean_id('edit-' . implode('-', $parents_for_id)), - '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL, + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, ); } } @@ -1970,91 +1928,6 @@ function theme_text_format_wrapper($element) { return $output; } -/** - * Add AHAH information about a form element to the page to communicate with - * javascript. If #ahah[path] is set on an element, this additional javascript is - * added to the page header to attach the AHAH behaviors. See ahah.js for more - * information. - * - * @param $element - * An associative array containing the properties of the element. - * Properties used: ahah_event, ahah_path, ahah_wrapper, ahah_parameters, - * ahah_effect. - * @return - * None. Additional code is added to the header of the page using - * drupal_add_js. - */ -function form_process_ahah($element) { - $js_added = &drupal_static(__FUNCTION__, array()); - // Add a reasonable default event handler if none specified. - if (isset($element['#ahah']) && !isset($element['#ahah']['event'])) { - switch ($element['#type']) { - case 'submit': - case 'button': - case 'image_button': - // Use the mousedown instead of the click event because form - // submission via pressing the enter key triggers a click event on - // submit inputs, inappropriately triggering AHAH behaviors. - $element['#ahah']['event'] = 'mousedown'; - // Attach an additional event handler so that AHAH behaviors - // can be triggered still via keyboard input. - $element['#ahah']['keypress'] = TRUE; - break; - case 'password': - case 'textfield': - case 'textarea': - $element['#ahah']['event'] = 'blur'; - break; - case 'radio': - case 'checkbox': - case 'select': - $element['#ahah']['event'] = 'change'; - break; - default: - return $element; - } - } - - // Adding the same javascript settings twice will cause a recursion error, - // we avoid the problem by checking if the javascript has already been added. - if ((isset($element['#ahah']['callback']) || isset($element['#ahah']['path'])) && isset($element['#ahah']['event']) && !isset($js_added[$element['#id']])) { - drupal_add_library('system', 'form'); - drupal_add_js('misc/ahah.js'); - - $ahah_binding = array( - 'url' => isset($element['#ahah']['callback']) ? url('system/ahah') : url($element['#ahah']['path']), - 'event' => $element['#ahah']['event'], - 'keypress' => empty($element['#ahah']['keypress']) ? NULL : $element['#ahah']['keypress'], - 'wrapper' => empty($element['#ahah']['wrapper']) ? NULL : $element['#ahah']['wrapper'], - 'selector' => empty($element['#ahah']['selector']) ? '#' . $element['#id'] : $element['#ahah']['selector'], - 'effect' => empty($element['#ahah']['effect']) ? 'none' : $element['#ahah']['effect'], - 'method' => empty($element['#ahah']['method']) ? 'replace' : $element['#ahah']['method'], - 'progress' => empty($element['#ahah']['progress']) ? array('type' => 'throbber') : $element['#ahah']['progress'], - 'button' => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE, - ); - - // Convert a simple #ahah[progress] type string into an array. - if (is_string($ahah_binding['progress'])) { - $ahah_binding['progress'] = array('type' => $ahah_binding['progress']); - } - // Change progress path to a full url. - if (isset($ahah_binding['progress']['path'])) { - $ahah_binding['progress']['url'] = url($ahah_binding['progress']['path']); - } - - // Add progress.js if we're doing a bar display. - if ($ahah_binding['progress']['type'] == 'bar') { - drupal_add_js('misc/progress.js', array('cache' => FALSE)); - } - - drupal_add_js(array('ahah' => array($element['#id'] => $ahah_binding)), 'setting'); - - $js_added[$element['#id']] = TRUE; - $element['#cache'] = TRUE; - } - return $element; -} - /** * Format a checkbox. * @@ -2132,7 +2005,7 @@ function form_process_checkboxes($element) { '#return_value' => $key, '#default_value' => isset($value[$key]), '#attributes' => $element['#attributes'], - '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL, + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, ); } } @@ -2245,7 +2118,7 @@ function form_process_tableselect($element) { '#return_value' => $key, '#default_value' => isset($value[$key]), '#attributes' => $element['#attributes'], - '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL, + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, ); } else { @@ -2260,7 +2133,7 @@ function form_process_tableselect($element) { '#attributes' => $element['#attributes'], '#parents' => $element['#parents'], '#id' => form_clean_id('edit-' . implode('-', $parents_for_id)), - '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL, + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, ); } } diff --git a/misc/CVS/Entries b/misc/CVS/Entries index cb603bd..580f5f6 100644 --- a/misc/CVS/Entries +++ b/misc/CVS/Entries @@ -1,13 +1,10 @@ D/farbtastic//// D/ui//// -/ahah.js/1.17/Fri Aug 7 05:58:21 2009// /arrow-asc.png/1.1/Fri Jun 12 07:15:24 2009/-kb/ /arrow-desc.png/1.1/Fri Jun 12 07:15:24 2009/-kb/ -/autocomplete.js/1.31/Thu Jul 23 09:28:47 2009// /batch.js/1.9/Fri Jun 12 07:15:24 2009// /collapse.js/1.23/Fri Jun 12 07:15:24 2009// /draggable.png/1.1/Fri Jun 12 07:15:24 2009/-kb/ -/drupal.js/1.56/Thu Jul 23 09:28:47 2009// /druplicon.png/1.4/Fri Jun 12 07:15:24 2009/-kb/ /favicon.ico/1.3/Fri Jun 12 07:15:24 2009/-kb/ /feed.png/1.2/Fri Aug 7 05:58:21 2009/-kb/ @@ -39,7 +36,6 @@ D/ui//// /print-rtl.css/1.1/Fri Jun 12 07:15:25 2009// /print.css/1.6/Fri Jun 12 07:15:25 2009// /progress.gif/1.3/Fri Jun 12 07:15:25 2009/-kb/ -/progress.js/1.24/Fri Jun 12 07:15:25 2009// /tabledrag.js/1.29/Sat Jun 13 03:10:09 2009// /tableheader.js/1.24/Fri Jun 12 07:15:25 2009// /tableselect.js/1.12/Fri Jun 12 07:15:25 2009// @@ -55,3 +51,7 @@ D/ui//// /watchdog-ok.png/1.1/Fri Jun 12 07:15:25 2009/-kb/ /watchdog-warning.png/1.1/Fri Jun 12 07:15:25 2009/-kb/ /xml.png/1.1/Fri Jun 12 07:15:25 2009/-kb/ +/ajax.js/1.1/Mon Aug 17 07:12:15 2009// +/autocomplete.js/1.32/Mon Aug 17 16:10:24 2009// +/drupal.js/1.57/Mon Aug 17 16:10:24 2009// +/progress.js/1.25/Mon Aug 17 16:10:24 2009// diff --git a/misc/ahah.js b/misc/ahah.js deleted file mode 100644 index f7037df..0000000 --- a/misc/ahah.js +++ /dev/null @@ -1,259 +0,0 @@ -// $Id: ahah.js,v 1.17 2009/07/16 02:46:13 dries Exp $ -(function ($) { - -/** - * Provides AJAX-like page updating via AHAH (Asynchronous HTML and HTTP). - * - * AHAH is a method of making a request via Javascript while viewing an HTML - * page. The request returns a small chunk of HTML, which is then directly - * injected into the page. - * - * Drupal uses this file to enhance form elements with #ahah[path] and - * #ahah[wrapper] properties. If set, this file will automatically be included - * to provide AHAH capabilities. - */ - -Drupal.ahah = Drupal.ahah || {}; - -/** - * Attaches the ahah behavior to each ahah form element. - */ -Drupal.behaviors.ahah = { - attach: function (context, settings) { - for (var base in settings.ahah) { - if (!$('#' + base + '.ahah-processed').size()) { - var element_settings = settings.ahah[base]; - - $(element_settings.selector).each(function () { - element_settings.element = this; - Drupal.ahah[base] = new Drupal.ahah(base, element_settings); - }); - - $('#' + base).addClass('ahah-processed'); - } - } - } -}; - -/** - * AHAH object. - * - * All AHAH objects on a page are accessible through the global Drupal.ahah object - * and are keyed by the submit button's ID. You can access them from your module's - * JavaScript file to override properties or functions. - * For example, if your AHAH enabled button has the ID 'edit-submit', you can - * redefine the function that is called to insert the new content like this - * (inside a Drupal.behaviors attach block): - * @code - * Drupal.behaviors.myCustomAhahStuff = { - * attach: function(context, settings) { - * Drupal.ahah['edit-submit'].insertNewContent = function(response, status) { - * new_content = $(response.data); - * $('#my-wrapper').append(new_content); - * alert('New content was appended to #my-wrapper'); - * } - * } - * }; - * @endcode - */ -Drupal.ahah = function (base, element_settings) { - // Set the properties for this object. - this.element = element_settings.element; - this.selector = element_settings.selector; - this.event = element_settings.event; - this.keypress = element_settings.keypress; - this.url = element_settings.url; - this.wrapper = '#' + element_settings.wrapper; - this.effect = element_settings.effect; - this.method = element_settings.method; - this.progress = element_settings.progress; - this.button = element_settings.button || { }; - - if (this.effect == 'none') { - this.showEffect = 'show'; - this.hideEffect = 'hide'; - this.showSpeed = ''; - } - else if (this.effect == 'fade') { - this.showEffect = 'fadeIn'; - this.hideEffect = 'fadeOut'; - this.showSpeed = 'slow'; - } - else { - this.showEffect = this.effect + 'Toggle'; - this.hideEffect = this.effect + 'Toggle'; - this.showSpeed = 'slow'; - } - - // Record the form action and target, needed for iFrame file uploads. - var form = $(this.element).parents('form'); - this.form_action = form.attr('action'); - this.form_target = form.attr('target'); - this.form_encattr = form.attr('encattr'); - - // Set the options for the ajaxSubmit function. - // The 'this' variable will not persist inside of the options object. - var ahah = this; - var options = { - url: ahah.url, - data: ahah.button, - beforeSubmit: function (form_values, element_settings, options) { - return ahah.beforeSubmit(form_values, element_settings, options); - }, - success: function (response, status) { - // Sanity check for browser support (object expected). - // When using iFrame uploads, responses must be returned as a string. - if (typeof response == 'string') { - response = Drupal.parseJson(response); - } - return ahah.success(response, status); - }, - complete: function (response, status) { - if (status == 'error' || status == 'parsererror') { - return ahah.error(response, ahah.url); - } - }, - dataType: 'json', - type: 'POST' - }; - - // Bind the ajaxSubmit function to the element event. - $(element_settings.element).bind(element_settings.event, function () { - $(element_settings.element).parents('form').ajaxSubmit(options); - return false; - }); - // If necessary, enable keyboard submission so that AHAH behaviors - // can be triggered through keyboard input as well as e.g. a mousedown - // action. - if (element_settings.keypress) { - $(element_settings.element).keypress(function (event) { - // Detect enter key. - if (event.keyCode == 13) { - $(element_settings.element).trigger(element_settings.event); - return false; - } - }); - } -}; - -/** - * Handler for the form redirection submission. - */ -Drupal.ahah.prototype.beforeSubmit = function (form_values, element, options) { - // Disable the element that received the change. - $(this.element).addClass('progress-disabled').attr('disabled', true); - - // Insert progressbar or throbber. - if (this.progress.type == 'bar') { - var progressBar = new Drupal.progressBar('ahah-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback)); - if (this.progress.message) { - progressBar.setProgress(-1, this.progress.message); - } - if (this.progress.url) { - progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500); - } - this.progress.element = $(progressBar.element).addClass('ahah-progress ahah-progress-bar'); - this.progress.object = progressBar; - $(this.element).after(this.progress.element); - } - else if (this.progress.type == 'throbber') { - this.progress.element = $('
 
'); - if (this.progress.message) { - $('.throbber', this.progress.element).after('
' + this.progress.message + '
'); - } - $(this.element).after(this.progress.element); - } -}; - -/** - * Handler for the form redirection completion. - */ -Drupal.ahah.prototype.success = function (response, status) { - var form = $(this.element).parents('form'); - - // Restore the previous action and target to the form. - form.attr('action', this.form_action); - this.form_target ? form.attr('target', this.form_target) : form.removeAttr('target'); - this.form_encattr ? form.attr('target', this.form_encattr) : form.removeAttr('encattr'); - - // Remove the progress element. - if (this.progress.element) { - $(this.progress.element).remove(); - } - if (this.progress.object) { - this.progress.object.stopMonitoring(); - } - $(this.element).removeClass('progress-disabled').attr('disabled', false); - - Drupal.freezeHeight(); - - // Call the insertNewContent handler to insert the new content into the page. - this.insertNewContent(response, status); - - Drupal.unfreezeHeight(); -}; - -/** - * Handler to insert the new content into the page. - */ -Drupal.ahah.prototype.insertNewContent = function (response, status) { - var wrapper = $(this.wrapper); - - // Manually insert HTML into the jQuery object, using $() directly crashes - // Safari with long string lengths. http://dev.jquery.com/ticket/1152 - var new_content = $('
').html(response.data); - - // Add the new content to the page. - if (this.method == 'replace') { - wrapper.empty().append(new_content); - } - else { - wrapper[this.method](new_content); - } - - // Immediately hide the new content if we're using any effects. - if (this.showEffect != 'show') { - new_content.hide(); - } - - // Determine what effect use and what content will receive the effect, then - // show the new content. - if ($('.ahah-new-content', new_content).size() > 0) { - $('.ahah-new-content', new_content).hide(); - new_content.show(); - $('.ahah-new-content', new_content)[this.showEffect](this.showSpeed); - } - else if (this.showEffect != 'show') { - new_content[this.showEffect](this.showSpeed); - } - - // Attach all javascript behaviors to the new content, if it was successfully - // added to the page, this if statement allows #ahah[wrapper] to be optional. - if (new_content.parents('html').length > 0) { - // Apply any settings from the returned JSON if available. - var settings = response.settings || Drupal.settings; - Drupal.attachBehaviors(new_content, settings); - } -}; - -/** - * Handler for the form redirection error. - */ -Drupal.ahah.prototype.error = function (response, uri) { - alert(Drupal.ahahError(response, uri)); - // Resore the previous action and target to the form. - $(this.element).parent('form').attr({ action: this.form_action, target: this.form_target }); - // Remove the progress element. - if (this.progress.element) { - $(this.progress.element).remove(); - } - if (this.progress.object) { - this.progress.object.stopMonitoring(); - } - // Undo hide. - $(this.wrapper).show(); - // Re-enable the element. - $(this.element).removeClass('progress-disabled').attr('disabled', false); -}; - -})(jQuery); diff --git a/misc/ajax.js b/misc/ajax.js new file mode 100644 index 0000000..8a74467 --- /dev/null +++ b/misc/ajax.js @@ -0,0 +1,383 @@ +// $Id: ajax.js,v 1.1 2009/08/17 07:12:15 webchick Exp $ +(function ($) { + +/** + * Provides AJAX page updating via jQuery $.ajax (Asynchronous JavaScript and XML). + * + * AJAX is a method of making a request via Javascript while viewing an HTML + * page. The request returns an array of commands encoded in JSON, which is + * then executed to make any changes that are necessary to the page. + * + * Drupal uses this file to enhance form elements with #ajax['path'] and + * #ajax['wrapper'] properties. If set, this file will automatically be included + * to provide AJAX capabilities. + */ + +Drupal.ajax = Drupal.ajax || {}; + +/** + * Attaches the AJAX behavior to each AJAX form element. + */ +Drupal.behaviors.AJAX = { + attach: function (context, settings) { + // Load all AJAX behaviors specified in the settings. + for (var base in settings.ajax) { + if (!$('#' + base + '.ajax-processed').length) { + var element_settings = settings.ajax[base]; + + $(element_settings.selector).each(function () { + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + + $('#' + base).addClass('ajax-processed'); + } + } + + // Bind AJAX behaviors to all items showing the class. + $('.use-ajax:not(.ajax-processed)').addClass('ajax-processed').each(function () { + var element_settings = {}; + + // For anchor tags, these will go to the target of the anchor rather + // than the usual location. + if ($(this).attr('href')) { + element_settings.url = $(this).attr('href'); + } + var base = $(this).attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + + // This class means to submit the form to the action using AJAX. + $('.use-ajax-submit:not(.ajax-processed)').addClass('ajax-processed').each(function () { + var element_settings = {}; + + // AJAX submits specified in this manner automatically submit to the + // normal form action. + element_settings.url = $(this.form).attr('action'); + element_settings.set_click = TRUE; + + var base = $(this).attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + } +}; + +/** + * AJAX object. + * + * All AJAX objects on a page are accessible through the global Drupal.ajax + * object and are keyed by the submit button's ID. You can access them from + * your module's JavaScript file to override properties or functions. + * + * For example, if your AJAX enabled button has the ID 'edit-submit', you can + * redefine the function that is called to insert the new content like this + * (inside a Drupal.behaviors attach block): + * @code + * Drupal.behaviors.myCustomAJAXStuff = { + * attach: function (context, settings) { + * Drupal.ajax['edit-submit'].commands.insert = function (ajax, response, status) { + * new_content = $(response.data); + * $('#my-wrapper').append(new_content); + * alert('New content was appended to #my-wrapper'); + * } + * } + * }; + * @endcode + */ +Drupal.ajax = function (base, element, element_settings) { + var defaults = { + url: 'system/ajax', + event: 'mousedown', + keypress: true, + selector: '#' + base, + effect: 'none', + speed: 'slow', + method: 'replace', + progress: { + type: 'bar', + message: 'Please wait...' + }, + button: {}, + }; + + $.extend(this, defaults, element_settings); + + this.element = element; + + // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let + // the server detect when it needs to degrade gracefully. + this.url = element_settings.url.replace('/nojs/', '/ajax/'); + this.wrapper = '#' + element_settings.wrapper; + + // If there isn't a form, jQuery.ajax() will be used instead, allowing us to + // bind AJAX to links as well. + if (this.element.form) { + this.form = $(this.element.form); + } + + // Set the options for the ajaxSubmit function. + // The 'this' variable will not persist inside of the options object. + var ajax = this; + var options = { + url: ajax.url, + data: ajax.button, + beforeSubmit: function (form_values, element_settings, options) { + return ajax.beforeSubmit(form_values, element_settings, options); + }, + success: function (response, status) { + // Sanity check for browser support (object expected). + // When using iFrame uploads, responses must be returned as a string. + if (typeof response == 'string') { + response = Drupal.parseJson(response); + } + return ajax.success(response, status); + }, + complete: function (response, status) { + if (status == 'error' || status == 'parsererror') { + return ajax.error(response, ajax.url); + } + }, + dataType: 'json', + type: 'POST' + }; + + // Bind the ajaxSubmit function to the element event. + $(this.element).bind(element_settings.event, function () { + if (ajax.form) { + // If setClick is set, we must set this to ensure that the button's + // value is passed. + if (ajax.setClick) { + // Mark the clicked button. 'form.clk' is a special variable for + // ajaxSubmit that tells the system which element got clicked to + // trigger the submit. Without it there would be no 'op' or + // equivalent. + ajax.form.clk = this.element; + } + + ajax.form.ajaxSubmit(options); + } + else { + $.ajax(options); + } + + return false; + }); + + // If necessary, enable keyboard submission so that AJAX behaviors + // can be triggered through keyboard input as well as e.g. a mousedown + // action. + if (element_settings.keypress) { + $(element_settings.element).keypress(function (event) { + // Detect enter key. + if (event.keyCode == 13) { + $(element_settings.element).trigger(element_settings.event); + return false; + } + }); + } +}; + +/** + * Handler for the form redirection submission. + */ +Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) { + // Disable the element that received the change. + $(this.element).addClass('progress-disabled').attr('disabled', true); + + // Insert progressbar or throbber. + if (this.progress.type == 'bar') { + var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback)); + if (this.progress.message) { + progressBar.setProgress(-1, this.progress.message); + } + if (this.progress.url) { + progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500); + } + this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar'); + this.progress.object = progressBar; + $(this.element).after(this.progress.element); + } + else if (this.progress.type == 'throbber') { + this.progress.element = $('
 
'); + if (this.progress.message) { + $('.throbber', this.progress.element).after('
' + this.progress.message + '
'); + } + $(this.element).after(this.progress.element); + } +}; + +/** + * Handler for the form redirection completion. + */ +Drupal.ajax.prototype.success = function (response, status) { + // Remove the progress element. + if (this.progress.element) { + $(this.progress.element).remove(); + } + if (this.progress.object) { + this.progress.object.stopMonitoring(); + } + $(this.element).removeClass('progress-disabled').attr('disabled', false); + + Drupal.freezeHeight(); + + for (i in response) { + if (response[i]['command'] && this.commands[response[i]['command']]) { + this.commands[response[i]['command']](this, response[i], status); + } + } + + Drupal.unfreezeHeight(); + + // Remove any response-specific settings so they don't get used on the next + // call by mistake. + this.settings = {}; +}; + +/** + * Build an effect object which tells us how to apply the effect when adding new HTML. + */ +Drupal.ajax.prototype.getEffect = function (response) { + var type = response.effect || this.effect; + var speed = response.speed || this.speed; + + var effect = {}; + if (type == 'none') { + effect.showEffect = 'show'; + effect.hideEffect = 'hide'; + effect.showSpeed = ''; + } + else if (type == 'fade') { + effect.showEffect = 'fadeIn'; + effect.hideEffect = 'fadeOut'; + effect.showSpeed = speed; + } + else { + effect.showEffect = type + 'Toggle'; + effect.hideEffect = type + 'Toggle'; + effect.showSpeed = speed; + } + + return effect; +} + +/** + * Handler for the form redirection error. + */ +Drupal.ajax.prototype.error = function (response, uri) { + alert(Drupal.ajaxError(response, uri)); + // Remove the progress element. + if (this.progress.element) { + $(this.progress.element).remove(); + } + if (this.progress.object) { + this.progress.object.stopMonitoring(); + } + // Undo hide. + $(this.wrapper).show(); + // Re-enable the element. + $(this.element).removeClass('progress-disabled').attr('disabled', false); +}; + +/** + * Provide a series of commands that the server can request the client perform. + */ +Drupal.ajax.prototype.commands = { + /** + * Command to insert new content into the DOM. + */ + insert: function (ajax, response, status) { + // Get information from the response. If it is not there, default to + // our presets. + var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); + var method = response.method || ajax.method; + var effect = ajax.getEffect(response); + + // Manually insert HTML into the jQuery object, using $() directly crashes + // Safari with long string lengths. http://dev.jquery.com/ticket/3178 + var new_content = $('
').html(response.data); + + // Add the new content to the page. + wrapper[method](new_content); + + // Immediately hide the new content if we're using any effects. + if (effect.showEffect != 'show') { + new_content.hide(); + } + + // Determine which effect to use and what content will receive the + // effect, then show the new content. + if ($('.ajax-new-content', new_content).length > 0) { + $('.ajax-new-content', new_content).hide(); + new_content.show(); + $('.ajax-new-content', new_content)[effect.showEffect](effect.showSpeed); + } + else if (effect.showEffect != 'show') { + new_content[effect.showEffect](effect.showSpeed); + } + + // Attach all JavaScript behaviors to the new content, if it was successfully + // added to the page, this if statement allows #ajax['wrapper'] to be + // optional. + if (new_content.parents('html').length > 0) { + // Apply any settings from the returned JSON if available. + var settings = response.settings || ajax.settings || Drupal.settings; + Drupal.attachBehaviors(new_content, settings); + } + }, + + /** + * Command to remove a chunk from the page. + */ + remove: function (ajax, response, status) { + $(response.selector).remove(); + }, + + /** + * Command to mark a chunk changed. + */ + changed: function (ajax, response, status) { + if (!$(response.selector).hasClass('ajax-changed')) { + $(response.selector).addClass('ajax-changed'); + if (response.asterisk) { + $(response.selector).find(response.asterisk).append(' * '); + } + } + }, + + /** + * Command to provide an alert. + */ + alert: function (ajax, response, status) { + alert(response.text, response.title); + }, + + /** + * Command to set the settings that will be used for other commands in this response. + */ + settings: function (ajax, response, status) { + ajax.settings = response.settings; + }, + + /** + * Command to attach data using jQuery's data API. + */ + data: function (ajax, response, status) { + $(response.selector).data(response.name, response.value); + }, + + /** + * Command to restripe a table. + */ + restripe: function (ajax, response, status) { + // :even and :odd are reversed because jQuery counts from 0 and + // we count from 1, so we're out of sync. + $('tbody tr:not(:hidden)', $(response.selector)) + .removeClass('even').removeClass('odd') + .filter(':even') + .addClass('odd').end() + .filter(':odd') + .addClass('even'); + } +}; + +})(jQuery); diff --git a/misc/autocomplete.js b/misc/autocomplete.js index f1b1327..6e82efb 100644 --- a/misc/autocomplete.js +++ b/misc/autocomplete.js @@ -1,4 +1,4 @@ -// $Id: autocomplete.js,v 1.31 2009/07/03 19:21:54 dries Exp $ +// $Id: autocomplete.js,v 1.32 2009/08/17 07:12:15 webchick Exp $ (function ($) { /** @@ -289,7 +289,7 @@ Drupal.ACDB.prototype.search = function (searchString) { } }, error: function (xmlhttp) { - alert(Drupal.ahahError(xmlhttp, db.uri)); + alert(Drupal.ajaxError(xmlhttp, db.uri)); } }); }, this.delay); diff --git a/misc/drupal.js b/misc/drupal.js index cbac648..1ea8b8f 100644 --- a/misc/drupal.js +++ b/misc/drupal.js @@ -1,4 +1,4 @@ -// $Id: drupal.js,v 1.56 2009/07/03 19:21:54 dries Exp $ +// $Id: drupal.js,v 1.57 2009/08/17 07:12:15 webchick Exp $ var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'locale': {} }; @@ -294,9 +294,9 @@ Drupal.getSelection = function (element) { }; /** - * Build an error message from ahah response. + * Build an error message from an AJAX response. */ -Drupal.ahahError = function (xmlhttp, uri) { +Drupal.ajaxError = function (xmlhttp, uri) { if (xmlhttp.status == 200 || (xmlhttp.status == 500 && xmlhttp.statusText == 'Service unavailable (with message)')) { if ($.trim(xmlhttp.responseText)) { var message = Drupal.t("An error occurred. \nPath: @uri\nMessage: !text", { '@uri': uri, '!text': xmlhttp.responseText }); diff --git a/misc/progress.js b/misc/progress.js index 053e3da..19263a8 100644 --- a/misc/progress.js +++ b/misc/progress.js @@ -1,4 +1,4 @@ -// $Id: progress.js,v 1.24 2009/04/27 20:19:35 webchick Exp $ +// $Id: progress.js,v 1.25 2009/08/17 07:12:15 webchick Exp $ (function ($) { /** @@ -84,7 +84,7 @@ Drupal.progressBar.prototype.sendPing = function () { pb.timer = setTimeout(function () { pb.sendPing(); }, pb.delay); }, error: function (xmlhttp) { - pb.displayError(Drupal.ahahError(xmlhttp, pb.uri)); + pb.displayError(Drupal.ajaxError(xmlhttp, pb.uri)); } }); } diff --git a/modules/book/CVS/Entries b/modules/book/CVS/Entries index f804247..22ace65 100644 --- a/modules/book/CVS/Entries +++ b/modules/book/CVS/Entries @@ -4,11 +4,11 @@ /book-node-export-html.tpl.php/1.2/Fri Jun 12 07:15:25 2009// /book-rtl.css/1.2/Fri Jun 12 07:15:25 2009// /book.admin.inc/1.20/Sat Jun 13 03:10:40 2009// -/book.css/1.10/Thu Jul 23 09:28:47 2009// /book.info/1.12/Sat Jun 13 03:10:41 2009// /book.install/1.32/Sun Jun 28 14:49:16 2009// /book.js/1.6/Fri Jun 12 07:15:25 2009// -/book.pages.inc/1.14/Fri Jun 12 07:15:25 2009// /book.test/1.13/Fri Aug 7 05:58:21 2009// -/book.module/1.503/Fri Aug 14 05:08:40 2009// +/book.css/1.11/Mon Aug 17 16:10:24 2009// +/book.module/1.504/Mon Aug 17 16:10:24 2009// +/book.pages.inc/1.15/Mon Aug 17 16:10:24 2009// D diff --git a/modules/book/book.css b/modules/book/book.css index 127be93..8bb0b7f 100644 --- a/modules/book/book.css +++ b/modules/book/book.css @@ -1,4 +1,4 @@ -/* $Id: book.css,v 1.10 2009/07/04 14:57:22 dries Exp $ */ +/* $Id: book.css,v 1.11 2009/08/17 07:12:15 webchick Exp $ */ .book-navigation .menu { border-top: 1px solid #888; @@ -47,7 +47,7 @@ html.js #edit-book-pick-book { #book-admin-edit select.progress-disabled { margin-right: 0; } -#book-admin-edit tr.ahah-new-content { +#book-admin-edit tr.ajax-new-content { background-color: #ffd; } #book-admin-edit .form-item { diff --git a/modules/book/book.module b/modules/book/book.module index 9223e8a..bd5e60c 100644 --- a/modules/book/book.module +++ b/modules/book/book.module @@ -1,5 +1,5 @@ t('Your page will be a part of the selected book.'), '#weight' => -5, '#attributes' => array('class' => 'book-title-select'), - '#ahah' => array( + '#ajax' => array( 'path' => 'book/js/form', 'wrapper' => 'edit-book-plid-wrapper', - 'effect' => 'slide', + 'effect' => 'fade', + 'speed' => 'fast', ), ); } diff --git a/modules/book/book.pages.inc b/modules/book/book.pages.inc index bcd0c36..5b63be7 100644 --- a/modules/book/book.pages.inc +++ b/modules/book/book.pages.inc @@ -1,5 +1,5 @@ book; - $book_link['bid'] = $bid; - // Get the new options and update the cache. - $form['book']['plid'] = _book_parent_select($book_link); - form_set_cache($_POST['form_build_id'], $form, $cached_form_state); - // Build and render the new select element, then return it in JSON format. - $form_state = array(); - $form = form_builder($form['form_id']['#value'] , $form, $form_state); - $output = drupal_render($form['book']['plid']); - drupal_json(array('status' => TRUE, 'data' => $output)); - } - else { - drupal_json(array('status' => FALSE, 'data' => '')); - } - } - else { - drupal_json(array('status' => FALSE, 'data' => '')); + + // Validate the bid. + if (isset($form['book']['bid']['#options'][$bid])) { + $book_link = $form['#node']->book; + $book_link['bid'] = $bid; + // Get the new options and update the cache. + $form['book']['plid'] = _book_parent_select($book_link); + form_set_cache($form['values']['form_build_id'], $form, $form_state); + + // Build and render the new select element, then return it in JSON format. + $form_state = array(); + $form = form_builder($form['form_id']['#value'], $form, $form_state); + + $commands[] = ajax_command_replace(NULL, drupal_render($form['book']['plid'])); } - exit(); + + ajax_render($commands); } diff --git a/modules/comment/CVS/Entries b/modules/comment/CVS/Entries index dfd9929..27019d2 100644 --- a/modules/comment/CVS/Entries +++ b/modules/comment/CVS/Entries @@ -2,7 +2,6 @@ /comment-rtl.css/1.2/Fri Jun 12 07:15:25 2009// /comment-wrapper.tpl.php/1.7/Fri Aug 7 05:58:21 2009// /comment.admin.inc/1.30/Fri Aug 7 05:58:21 2009// -/comment.api.php/1.10/Thu Jul 23 09:28:47 2009// /comment.css/1.5/Fri Jun 12 07:15:25 2009// /comment.info/1.10/Sat Jun 13 03:10:53 2009// /comment.install/1.44/Tue Aug 11 21:17:14 2009// @@ -10,5 +9,6 @@ /comment.pages.inc/1.24/Fri Aug 7 05:58:21 2009// /comment.test/1.43/Tue Aug 11 21:17:14 2009// /comment.tpl.php/1.10/Fri Aug 7 05:58:21 2009// -/comment.module/1.752/Fri Aug 14 05:08:40 2009// +/comment.api.php/1.11/Mon Aug 17 16:10:25 2009// +/comment.module/1.753/Mon Aug 17 16:10:25 2009// D diff --git a/modules/comment/comment.api.php b/modules/comment/comment.api.php index d6dacbb..0e262be 100644 --- a/modules/comment/comment.api.php +++ b/modules/comment/comment.api.php @@ -1,5 +1,5 @@ subject = trim($comment->subject); +} + /** * The comment is being inserted. * diff --git a/modules/comment/comment.module b/modules/comment/comment.module index 24d3def..27ccaa6 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -1,5 +1,5 @@ cid) { // Update the comment in the database. db_update('comment') diff --git a/modules/dblog/CVS/Entries b/modules/dblog/CVS/Entries index 8b49b5b..f30cfae 100644 --- a/modules/dblog/CVS/Entries +++ b/modules/dblog/CVS/Entries @@ -1,8 +1,8 @@ /dblog-rtl.css/1.4/Thu Jul 23 09:28:47 2009// -/dblog.admin.inc/1.23/Fri Aug 7 05:58:21 2009// /dblog.css/1.6/Thu Jul 23 09:28:47 2009// /dblog.info/1.7/Sat Jun 13 03:10:58 2009// /dblog.install/1.16/Sat Jun 13 03:10:58 2009// /dblog.module/1.38/Sat Jun 13 03:10:58 2009// -/dblog.test/1.26/Fri Aug 14 17:36:51 2009// +/dblog.admin.inc/1.24/Mon Aug 17 16:10:25 2009// +/dblog.test/1.27/Mon Aug 17 16:10:25 2009// D diff --git a/modules/dblog/dblog.admin.inc b/modules/dblog/dblog.admin.inc index f6e37c2..cbd3774 100644 --- a/modules/dblog/dblog.admin.inc +++ b/modules/dblog/dblog.admin.inc @@ -1,5 +1,5 @@ t('Type'), - 'where' => "w.type = ':s'", + 'where' => "w.type = ?", 'options' => $types, ); } $filters['severity'] = array( 'title' => t('Severity'), - 'where' => 'w.severity = :d', + 'where' => 'w.severity = ?', 'options' => watchdog_severity_levels(), ); diff --git a/modules/dblog/dblog.test b/modules/dblog/dblog.test index 92c4732..9fa33f3 100644 --- a/modules/dblog/dblog.test +++ b/modules/dblog/dblog.test @@ -1,5 +1,5 @@ 'custom', + 'type' => $type, 'message' => 'Log entry added to test the dblog row limit.', 'variables' => array(), - 'severity' => WATCHDOG_NOTICE, + 'severity' => $severity, 'link' => NULL, 'user' => $this->big_user, 'request_uri' => $base_root . request_uri(), @@ -105,7 +110,7 @@ class DBLogTestCase extends DrupalWebTestCase { ); $message = 'Log entry added to test the dblog row limit.'; for ($i = 0; $i < $count; $i++) { - $log['message'] = $i . ' => ' . $message; + $log['message'] = $this->randomString(); dblog_watchdog($log); } } @@ -389,4 +394,162 @@ class DBLogTestCase extends DrupalWebTestCase { $count = db_query('SELECT COUNT(*) FROM {watchdog}')->fetchField(); $this->assertEqual($count, 0, t('DBLog contains :count records after a clear.', array(':count' => $count))); } + + /** + * Test the dblog filter on admin/reports/dblog. + */ + protected function testFilter() { + $this->drupalLogin($this->big_user); + + // Clear log to ensure that only generated entries are found. + db_delete('watchdog')->execute(); + + // Generate watchdog entries. + $type_names = array(); + $types = array(); + for ($i = 0; $i < 3; $i++) { + $type_names[] = $type_name = $this->randomName(); + $severity = WATCHDOG_EMERG; + for ($j = 0; $j < 3; $j++) { + $types[] = $type = array( + 'count' => mt_rand(1, 5), + 'type' => $type_name, + 'severity' => $severity++, + ); + $this->generateLogEntries($type['count'], $type['type'], $type['severity']); + } + } + + // View the dblog. + $this->drupalGet('admin/reports/dblog'); + + // Confirm all the entries are displayed. + $count = $this->getTypeCount($types); + foreach ($types as $key => $type) { + $this->assertEqual($count[$key], $type['count'], 'Count matched'); + } + + // Filter by each type and confirm that entries with various severities are + // displayed. + foreach ($type_names as $type_name) { + $edit = array( + 'type[]' => array($type_name), + ); + $this->drupalPost(NULL, $edit, t('Filter')); + + // Count the number of entries of this type. + $type_count = 0; + foreach ($types as $type) { + if ($type['type'] == $type_name) { + $type_count += $type['count']; + } + } + + $count = $this->getTypeCount($types); + $this->assertEqual(array_sum($count), $type_count, 'Count matched'); + } + + // Set filter to match each of the three type attributes and confirm the + // number of entries displayed. + foreach ($types as $key => $type) { + $edit = array( + 'type[]' => array($type['type']), + 'severity[]' => array($type['severity']), + ); + $this->drupalPost(NULL, $edit, t('Filter')); + + $count = $this->getTypeCount($types); + $this->assertEqual(array_sum($count), $type['count'], 'Count matched'); + } + } + + /** + * Get the log entry information form the page. + * + * @return + * List of entries and their information. + */ + protected function getLogEntries() { + $entries = array(); + if ($table = $this->xpath('.//table[@id="admin-dblog"]')) { + $table = array_shift($table); + foreach ($table->tbody->tr as $row) { + $entries[] = array( + 'severity' => $this->getSeverityConstant($row['class']), + 'type' => $this->asText($row->td[1]), + 'message' => $this->asText($row->td[3]), + 'user' => $this->asText($row->td[4]), + ); + } + } + return $entries; + } + + /** + * Get the count of entries per type. + * + * @param $types + * The type information to compare against. + * @return + * The count of each type keyed by the key of the $types array. + */ + protected function getTypeCount(array $types) { + $entries = $this->getLogEntries(); + $count = array_fill(0, count($types), 0); + foreach ($entries as $entry) { + foreach ($types as $key => $type) { + if ($entry['type'] == $type['type'] && $entry['severity'] == $type['severity']) { + $count[$key]++; + break; + } + } + } + return $count; + } + + /** + * Get the watchdog severity constant corresponding to the CSS class. + * + * @param $class + * CSS class attribute. + * @return + * The watchdog severity constant or NULL if not found. + */ + protected function getSeverityConstant($class) { + // Reversed array from dblog_overview(). + $map = array( + 'dblog-debug' => WATCHDOG_DEBUG, + 'dblog-info' => WATCHDOG_INFO, + 'dblog-notice' => WATCHDOG_NOTICE, + 'dblog-warning' => WATCHDOG_WARNING, + 'dblog-error' => WATCHDOG_ERROR, + 'dblog-critical' => WATCHDOG_CRITICAL, + 'dblog-alert' => WATCHDOG_ALERT, + 'dblog-emerg' => WATCHDOG_EMERG, + ); + + // Find the class that contains the severity. + $classes = explode(' ', $class); + foreach ($classes as $class) { + if (isset($map[$class])) { + return $map[$class]; + } + } + return NULL; + } + + /** + * Extract the text contained by the element. + * + * @param $element + * Element to extract text from. + * @return + * Extracted text. + */ + protected function asText(SimpleXMLElement $element) { + if (!is_object($element)) { + return $this->fail('The element is not an element.'); + } + return trim(html_entity_decode(strip_tags($element->asXML()))); + } } diff --git a/modules/field/CVS/Entries b/modules/field/CVS/Entries index 8a4335a..27c6081 100644 --- a/modules/field/CVS/Entries +++ b/modules/field/CVS/Entries @@ -1,12 +1,12 @@ D/modules//// D/theme//// /field.install/1.11/Thu Aug 13 01:50:00 2009// -/field.test/1.40/Thu Aug 13 01:50:00 2009// /field.api.php/1.26/Fri Aug 14 17:52:22 2009// /field.attach.inc/1.36/Fri Aug 14 17:52:22 2009// /field.crud.inc/1.25/Fri Aug 14 17:52:22 2009// /field.default.inc/1.14/Fri Aug 14 17:52:22 2009// -/field.form.inc/1.13/Fri Aug 14 17:52:22 2009// /field.info/1.4/Fri Aug 14 17:52:22 2009// /field.info.inc/1.12/Fri Aug 14 17:52:22 2009// /field.module/1.22/Fri Aug 14 17:52:22 2009// +/field.form.inc/1.14/Mon Aug 17 16:10:25 2009// +/field.test/1.41/Mon Aug 17 16:10:25 2009// diff --git a/modules/field/field.form.inc b/modules/field/field.form.inc index a60c4d1..9f7d0bb 100644 --- a/modules/field/field.form.inc +++ b/modules/field/field.form.inc @@ -1,5 +1,5 @@ t('Add another item'), // Submit callback for disabled JavaScript. '#submit' => array('field_add_more_submit'), - '#ahah' => array( + '#ajax' => array( 'path' => 'field/js_add_more/' . $bundle_name_url_css . '/' . $field_name_url_css, 'wrapper' => $field_name_url_css . '-wrapper', 'method' => 'replace', @@ -362,13 +362,13 @@ function field_add_more_js($bundle_name, $field_name) { } if ($invalid) { - drupal_json(array('data' => '')); - exit; + ajax_render(array()); } // We don't simply return a new empty widget row to append to existing ones, // because: // - ahah.js won't simply let us add a new row to a table + // @todo ajax.js lets you. :) // - attaching the 'draggable' behavior won't be easy // So we resort to rebuilding the whole table of widgets including the // existing ones, which makes us jump through a few hoops. @@ -428,21 +428,15 @@ function field_add_more_js($bundle_name, $field_name) { foreach ($form_path as $key) { $field_form = $field_form[$key]; } - // Add a div around the new field to receive the ahah effect. - $field_form[$delta]['#prefix'] = '
' . (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : ''); + // Add a DIV around the new field to receive the AJAX effect. + $field_form[$delta]['#prefix'] = '
' . (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : ''); $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') . '
'; // Prevent duplicate wrapper. unset($field_form['#prefix'], $field_form['#suffix']); - // If a newly inserted widget contains AHAH behaviors, they normally won't - // work because AHAH doesn't know about those - it just attaches to the exact - // form elements that were initially specified in the Drupal.settings object. - // The new ones didn't exist then, so we need to update Drupal.settings - // by ourselves in order to let AHAH know about those new form elements. - $javascript = drupal_add_js(NULL, NULL); - $output_js = isset($javascript['setting']) ? '' : ''; - - $output = theme('status_messages') . drupal_render($field_form) . $output_js; - drupal_json(array('status' => TRUE, 'data' => $output)); - exit; + $output = theme('status_messages') . drupal_render($field_form); + + $commands = array(); + $commands[] = ajax_command_replace(NULL, $output); + ajax_render($commands); } diff --git a/modules/field/field.test b/modules/field/field.test index 62a32a2..b7f3bee 100644 --- a/modules/field/field.test +++ b/modules/field/field.test @@ -1,5 +1,5 @@ additionalCurlOptions[CURLOPT_URL]); // The response is drupal_json, so we need to undo some escaping. - $response = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent())); - $this->assertTrue(is_object($response), t('The response is an object')); - $this->assertIdentical($response->status, TRUE, t('Response status is true')); + $commands = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent())); + + // The JSON response will be two AJAX commands. The first is a settings + // command and the second is the replace command. + $settings = reset($commands); + $replace = next($commands); + + $this->assertTrue(is_object($settings), t('The response settings command is an object')); + $this->assertTrue(is_object($replace), t('The response replace command is an object')); + // This response data is valid HTML so we will can reuse everything we have // for HTML pages. - $this->content = $response->data; + $this->content = $replace->data; // Needs to be emptied out so the new content will be parsed. $this->elements = ''; diff --git a/modules/help/CVS/Entries b/modules/help/CVS/Entries index f29685d..76169a5 100644 --- a/modules/help/CVS/Entries +++ b/modules/help/CVS/Entries @@ -4,5 +4,5 @@ /help.css/1.2/Fri Jun 12 07:15:25 2009// /help.info/1.8/Sat Jun 13 03:11:11 2009// /help.module/1.87/Fri Aug 14 05:08:40 2009// -/help.test/1.12/Sun Aug 16 17:51:57 2009// +/help.test/1.12/Sun Aug 16 17:59:34 2009// D diff --git a/modules/node/CVS/Entries b/modules/node/CVS/Entries index d10e1bb..035a1f0 100644 --- a/modules/node/CVS/Entries +++ b/modules/node/CVS/Entries @@ -11,4 +11,4 @@ D/tests//// /node.test/1.38/Tue Jul 28 19:18:06 2009// /node.tpl.php/1.19/Thu Aug 6 05:05:59 2009// /node.module/1.1099/Fri Aug 14 17:52:22 2009// -/node.pages.inc/1.73/Sun Aug 16 17:50:50 2009// +/node.pages.inc/1.73/Mon Aug 17 16:10:12 2009// diff --git a/modules/poll/CVS/Entries b/modules/poll/CVS/Entries index 0e30d09..b006c1d 100644 --- a/modules/poll/CVS/Entries +++ b/modules/poll/CVS/Entries @@ -8,6 +8,6 @@ /poll.info/1.9/Fri Aug 14 17:33:48 2009// /poll.install/1.24/Fri Aug 14 17:33:48 2009// /poll.pages.inc/1.20/Fri Aug 14 17:33:48 2009// -/poll.test/1.21/Fri Aug 14 17:33:48 2009// -/poll.module/1.302/Fri Aug 14 17:52:22 2009// +/poll.module/1.303/Mon Aug 17 16:10:25 2009// +/poll.test/1.22/Mon Aug 17 16:10:25 2009// D diff --git a/modules/poll/poll.module b/modules/poll/poll.module index e85909a..b680d51 100644 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -1,5 +1,5 @@ 'submit', '#value' => t('More choices'), '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."), '#weight' => 1, '#submit' => array('poll_more_choices_submit'), // If no javascript action. - '#ahah' => array( + '#ajax' => array( 'callback' => 'poll_choice_js', 'wrapper' => 'poll-choices', 'method' => 'replace', @@ -320,7 +320,7 @@ function poll_more_choices_submit($form, &$form_state) { // Make the changes we want to the form state. if ($form_state['values']['poll_more']) { - $n = $_GET['q'] == 'system/ahah' ? 1 : 5; + $n = $_GET['q'] == 'system/ajax' ? 1 : 5; $form_state['choice_count'] = count($form_state['values']['choice']) + $n; } } @@ -373,9 +373,7 @@ function poll_choice_js($form, $form_state) { // Prevent duplicate wrappers. unset($choice_form['#prefix'], $choice_form['#suffix']); - $output = theme('status_messages') . drupal_render($choice_form); - - drupal_json(array('status' => TRUE, 'data' => $output)); + return theme('status_messages') . drupal_render($choice_form); } /** diff --git a/modules/poll/poll.test b/modules/poll/poll.test index ca43366..0a2942d 100644 --- a/modules/poll/poll.test +++ b/modules/poll/poll.test @@ -1,5 +1,5 @@ additionalCurlOptions[CURLOPT_URL] = url('system/ahah', array('absolute' => TRUE)); + $this->additionalCurlOptions[CURLOPT_URL] = url('system/ajax', array('absolute' => TRUE)); $this->drupalPost(NULL, $edit, t('More choices')); unset($this->additionalCurlOptions[CURLOPT_URL]); // The response is drupal_json, so we need to undo some escaping. - $response = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent())); - $this->assertTrue(is_object($response), t('The response is an object')); - $this->assertIdentical($response->status, TRUE, t('Response status is true')); - // This response data is valid HTML so we will can reuse everything we have + $commands = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent())); + + // The JSON response will be two AJAX commands. The first is a settings + // command and the second is the replace command. + $settings = reset($commands); + $replace = next($commands); + + $this->assertTrue(is_object($settings), t('The response settings command is an object')); + $this->assertTrue(is_object($replace), t('The response replace command is an object')); + + // This replace data is valid HTML so we will can reuse everything we have // for HTML pages. - $this->content = $response->data; + $this->content = $replace->data; // Needs to be emptied out so the new content will be parsed. $this->elements = ''; diff --git a/modules/simpletest/CVS/Entries b/modules/simpletest/CVS/Entries index 4278096..74d6cf7 100644 --- a/modules/simpletest/CVS/Entries +++ b/modules/simpletest/CVS/Entries @@ -2,10 +2,10 @@ D/files//// D/tests//// /simpletest.api.php/1.3/Thu Jul 23 09:28:47 2009// /simpletest.css/1.6/Fri Aug 7 05:58:21 2009// -/simpletest.info/1.7/Fri Jul 3 03:59:26 2009// /simpletest.install/1.25/Fri Aug 7 05:58:21 2009// /simpletest.js/1.11/Fri Jun 12 07:15:26 2009// /drupal_web_test_case.php/1.136/Sat Aug 15 19:33:20 2009// -/simpletest.module/1.66/Sat Aug 15 19:33:20 2009// /simpletest.pages.inc/1.14/Sat Aug 15 19:33:20 2009// /simpletest.test/1.29/Sat Aug 15 19:33:20 2009// +/simpletest.info/1.8/Mon Aug 17 16:10:25 2009// +/simpletest.module/1.67/Mon Aug 17 16:10:25 2009// diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index 608eeae..dac8f86 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -1,4 +1,4 @@ -; $Id: simpletest.info,v 1.7 2009/07/01 13:44:53 dries Exp $ +; $Id: simpletest.info,v 1.8 2009/08/17 06:08:47 dries Exp $ name = "SimpleTest" description = "Provides a framework for unit and functional testing." package = Core @@ -14,6 +14,7 @@ files[] = drupal_web_test_case.php files[] = tests/actions.test files[] = tests/batch.test files[] = tests/bootstrap.test +files[] = tests/browser.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index 0f941f7..163268b 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -1,5 +1,5 @@ useDefaults(array('test_id')) ->execute(); - + // Clear out the previous verbose files. file_unmanaged_delete_recursive(file_directory_path() . '/simpletest/verbose'); @@ -382,8 +382,13 @@ function simpletest_registry_files_alter(&$files, $modules) { function simpletest_clean_environment() { simpletest_clean_database(); simpletest_clean_temporary_directories(); - $count = simpletest_clean_results_table(); - drupal_set_message(format_plural($count, 'Removed 1 test result.', 'Removed @count test results.')); + if (variable_get('simpletest_clear_results', TRUE)) { + $count = simpletest_clean_results_table(); + drupal_set_message(format_plural($count, 'Removed 1 test result.', 'Removed @count test results.')); + } + else { + drupal_set_message(t('Clear results is disabled and the test results table will not be cleared.'), 'warning'); + } } /** @@ -438,17 +443,17 @@ function simpletest_clean_temporary_directories() { * @param $test_id * Test ID to remove results for, or NULL to remove all results. * @return - * The number of results removed or FALSE. + * The number of results removed. */ function simpletest_clean_results_table($test_id = NULL) { if (variable_get('simpletest_clear_results', TRUE)) { if ($test_id) { $count = db_query('SELECT COUNT(test_id) FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id))->fetchField(); - db_delete("simpletest") + db_delete('simpletest') ->condition('test_id', $test_id) ->execute(); - db_delete("simpletest_test_id") + db_delete('simpletest_test_id') ->condition('test_id', $test_id) ->execute(); } @@ -456,11 +461,11 @@ function simpletest_clean_results_table($test_id = NULL) { $count = db_query('SELECT COUNT(test_id) FROM {simpletest_test_id}')->fetchField(); // Clear test results. - db_delete("simpletest")->execute(); - db_delete("simpletest_test_id")->execute(); + db_delete('simpletest')->execute(); + db_delete('simpletest_test_id')->execute(); } return $count; } - return FALSE; + return 0; } diff --git a/modules/simpletest/tests/CVS/Entries b/modules/simpletest/tests/CVS/Entries index 643141a..d7a6795 100644 --- a/modules/simpletest/tests/CVS/Entries +++ b/modules/simpletest/tests/CVS/Entries @@ -1,6 +1,5 @@ /actions.test/1.5/Thu Jul 23 09:28:47 2009// /batch.test/1.2/Thu Jul 23 09:28:47 2009// -/bootstrap.test/1.19/Fri Aug 7 05:58:21 2009// /cache.test/1.9/Thu Jul 23 09:28:47 2009// /common_test.info/1.1/Thu Jul 2 04:27:23 2009// /common_test.module/1.2/Thu Jul 23 09:28:47 2009// @@ -11,13 +10,11 @@ /error_test.info/1.1/Fri May 22 15:03:46 2009// /field_test.info/1.2/Fri Jun 12 07:15:26 2009// /field_test.install/1.4/Sat May 30 11:17:32 2009// -/file.test/1.37/Fri Aug 7 05:58:21 2009// /file_test.info/1.1/Fri Jun 12 07:15:26 2009// /file_test.module/1.11/Fri Aug 7 05:58:21 2009// /filetransfer.test/1.3/Thu Jul 23 09:28:47 2009// /form.test/1.14/Thu Jul 23 09:28:47 2009// /form_test.info/1.1/Fri Jun 12 07:15:26 2009// -/form_test.module/1.7/Sat Jun 13 03:11:38 2009// /graph.test/1.6/Thu Jul 23 09:28:47 2009// /image.test/1.6/Thu Jul 23 09:28:47 2009// /image_test.info/1.1/Fri Jun 12 07:15:26 2009// @@ -44,7 +41,13 @@ /xmlrpc_test.module/1.4/Sat Jun 13 03:11:42 2009// /field_test.module/1.14/Fri Aug 14 05:08:40 2009// /theme.test/1.5/Fri Aug 14 05:08:40 2009// -/common.test/1.63/Sun Aug 16 17:51:57 2009// /error.test/1.5/Sat Aug 15 19:33:20 2009// /error_test.module/1.4/Sat Aug 15 19:33:20 2009// +/bootstrap.test/1.20/Mon Aug 17 16:10:25 2009// +/browser.test/1.1/Mon Aug 17 06:08:47 2009// +/browser_test.info/1.1/Mon Aug 17 06:08:47 2009// +/browser_test.module/1.1/Mon Aug 17 06:08:47 2009// +/common.test/1.63/Sun Aug 16 17:59:34 2009// +/file.test/1.38/Mon Aug 17 16:10:25 2009// +/form_test.module/1.8/Mon Aug 17 16:10:25 2009// D diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index 3cc553a..d003d06 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -1,5 +1,5 @@ assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot called with normal cache.')); $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit called with normal cache.')); - // Test with aggressive cache. Boot and exit should not fire since the - // page is cached. - variable_set('cache', CACHE_AGGRESSIVE); + // Boot and exit should not fire since the page is cached. + variable_set('page_cache_invoke_hooks', FALSE); $this->assertTrue(cache_get(url('', array('absolute' => TRUE)), 'cache_page'), t('Page has been cached.')); $this->drupalGet(''); $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot not called with agressive cache and a cached page.')); $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit not called with agressive cache and a cached page.')); - // Test with aggressive cache and page cache cleared. Boot and exit should - // be called. + // Test with page cache cleared, boot and exit should be called. $this->assertTrue(db_delete('cache_page')->execute(), t('Page cache cleared.')); $this->drupalGet(''); $calls++; diff --git a/modules/simpletest/tests/browser.test b/modules/simpletest/tests/browser.test new file mode 100644 index 0000000..00f2871 --- /dev/null +++ b/modules/simpletest/tests/browser.test @@ -0,0 +1,123 @@ + t('Browser'), + 'description' => t('Test general browser functionality.'), + 'group' => t('Browser'), + ); + } + + public function setUp() { + parent::setUp('browser_test'); + } + + /** + * Test general browser functionality. + */ + public function testBrowserBackend() { + global $db_prefix; + + $browser = new Browser(); + $browser->setUserAgent(drupal_generate_test_ua($db_prefix)); + + // Check browser refresh, both meta tag and HTTP header. + $request = $browser->get(url('browser_test/refresh/meta', array('absolute' => TRUE))); + $this->assertEqual($request['content'], 'Refresh successful', 'Meta refresh successful ($request)'); + $this->assertEqual($browser->getContent(), 'Refresh successful', 'Meta refresh successful ($browser)'); + + $request = $browser->get(url('browser_test/refresh/header', array('absolute' => TRUE))); + $this->assertEqual($request['content'], 'Refresh successful', 'Meta refresh successful ($request)'); + $this->assertEqual($browser->getContent(), 'Refresh successful', 'Meta refresh successful ($browser)'); + } +} + +/** + * Test browser backend wrappers. + */ +class BrowserBackendTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => t('Browser - wrapper backends'), + 'description' => t('Test stream and curl backends execution of GET and POST requests.'), + 'group' => t('Browser'), + ); + } + + public function setUp() { + parent::setUp('browser_test'); + } + + /** + * Test stream and curl backends execution of GET and POST requests. + */ + public function testBrowserBackend() { + global $db_prefix; + + foreach (array('stream', 'curl') as $wrapper) { + $browser = new Browser($wrapper == 'stream'); + $browser->setUserAgent(drupal_generate_test_ua($db_prefix)); + + $string = $this->randomName(); + $edit = array( + 'foo' => $string, + ); + + // Test GET method. + $request = $browser->get(url('browser_test/print/get', array('absolute' => TRUE, 'query' => $edit))); + $this->assertEqual($string, $request['content'], t('String found during GET request ($request)'), $wrapper); + $this->assertEqual($string, $browser->getContent(), t('String found during GET request ($browser)'), $wrapper); + + // Test POST method. + $request = $browser->post(url('browser_test/print/post', array('absolute' => TRUE)), $edit, t('Submit')); + $this->assertEqual($string, $request['content'], t('String found during POST request ($request)'), $wrapper); + $this->assertEqual($string, $browser->getContent(), t('String found during POST request ($browser)'), $wrapper); + } + } +} + +/** + * Test browser page manipulation functionality. + */ +class BrowserPageTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => t('Browser - page'), + 'description' => t('Check "BrowserPage" class functionality.'), + 'group' => t('Browser'), + ); + } + + public function setUp() { + parent::setUp('browser_test'); + } + + /** + * Check "BrowserPage" class functionality. + */ + public function testBrowserPage() { + global $db_prefix; + + $browser = new Browser(); + $browser->setUserAgent(drupal_generate_test_ua($db_prefix)); + + $browser->get(url('browser_test/print/post', array('absolute' => TRUE))); + $page = $browser->getPage(); + $input = $page->xpath('//input[@name="foo"]'); + $input = $input[0]; + $this->assertEqual('foo', $input['name'], t('Field "foo" found')); + } +} diff --git a/modules/simpletest/tests/browser_test.info b/modules/simpletest/tests/browser_test.info new file mode 100644 index 0000000..1269b9f --- /dev/null +++ b/modules/simpletest/tests/browser_test.info @@ -0,0 +1,8 @@ +; $Id: browser_test.info,v 1.1 2009/08/17 06:08:47 dries Exp $ +name = Browser test +description = Provide various pages for testing the browser. +package = Testing +version = VERSION +core = 7.x +files[] = browser_test.module +hidden = TRUE diff --git a/modules/simpletest/tests/browser_test.module b/modules/simpletest/tests/browser_test.module new file mode 100644 index 0000000..1ab112a --- /dev/null +++ b/modules/simpletest/tests/browser_test.module @@ -0,0 +1,79 @@ + 'browser_test_print_get', + 'access arguments' => array('access content'), + ); + $items['browser_test/print/post'] = array( + 'page callback' => 'drupal_get_form', + 'page arguments' => array('browser_test_print_post_form'), + 'access arguments' => array('access content'), + ); + + $items['browser_test/refresh/meta'] = array( + 'page callback' => 'browser_test_refresh_meta', + 'access arguments' => array('access content'), + ); + $items['browser_test/refresh/header'] = array( + 'page callback' => 'browser_test_refresh_header', + 'access arguments' => array('access content'), + ); + + return $items; +} + +function browser_test_print_get() { + echo $_GET['foo']; + exit; +} + +function browser_test_print_post_form(&$form_state) { + $form = array(); + + $form['foo'] = array( + '#type' => 'textfield', + ); + $form['op'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} + +function browser_test_print_post_form_submit($form, &$form_state) { + echo $form_state['values']['foo']; + exit; +} + +function browser_test_refresh_meta() { + if (!isset($_GET['refresh'])) { + $url = url('browser_test/refresh/meta', array('absolute' => TRUE, 'query' => 'refresh=true')); + drupal_add_html_head(''); + return ''; + } + echo 'Refresh successful'; + exit; +} + +function browser_test_refresh_header() { + if (!isset($_GET['refresh'])) { + $url = url('browser_test/refresh/header', array('absolute' => TRUE, 'query' => 'refresh=true')); + drupal_set_header('Location', $url); + return ''; + } + echo 'Refresh successful'; + exit; +} diff --git a/modules/simpletest/tests/file.test b/modules/simpletest/tests/file.test index d7a0446..3504158 100644 --- a/modules/simpletest/tests/file.test +++ b/modules/simpletest/tests/file.test @@ -1,5 +1,5 @@ fid, $file2->fid)); $this->assertTrue(isset($files[$file1->fid]), t('File was loaded successfully')); $this->assertTrue(isset($files[$file2->fid]), t('File was loaded successfully')); + + // Check that file_load_multiple() with no arguments returns FALSE. + $this->assertFalse(file_load_multiple(), t('No files were loaded.')); } diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index dfe97a7..f080850 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -1,5 +1,5 @@ 0) { - $module_names = array(); - foreach ($problem_modules as $module) { - $info = drupal_parse_info_file(drupal_get_path('module', $module) . "/$module.info"); - $module_names[] = $info['name']; - } - sort($module_names); - $message = format_plural(count($module_names), '%module may cause issues when using aggressive caching.', '%modules may cause issues when using aggressive caching.', array('%module' => $module_names[0], '%modules' => implode(', ', $module_names))); - $description = ' ' . $message . ''; - } - else { - $description = ' ' . t('Note: if you use aggressive page caching and enable new modules, you will need to check this setting again to ensure compatibility.') . ''; - } $form['caching']['cache'] = array( '#type' => 'radios', '#title' => t('Page cache for anonymous users'), '#default_value' => $cache, - '#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_NORMAL => t('Normal (recommended)'), CACHE_AGGRESSIVE => t('Aggressive (possible issues)')), - '#description' => t('Aggressive caching prevents modules from being loaded if the requested page can be retrieved from the cache. This may cause issues if you use modules that need to be loaded every request i.e. for writing data like statistics.') . $description, + '#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_NORMAL => t('Normal (recommended)')), ); $period = drupal_map_assoc(array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400), 'format_interval'); $period[0] = '<' . t('none') . '>'; diff --git a/modules/system/system.css b/modules/system/system.css index 7997e61..c5f0359 100644 --- a/modules/system/system.css +++ b/modules/system/system.css @@ -1,4 +1,4 @@ -/* $Id: system.css,v 1.59 2009/08/12 23:51:19 webchick Exp $ */ +/* $Id: system.css,v 1.60 2009/08/17 07:12:16 webchick Exp $ */ /* ** HTML elements @@ -450,20 +450,20 @@ html.js .no-js { .progress-disabled { float: left; /* LTR */ } -.ahah-progress { +.ajax-progress { float: left; /* LTR */ } -.ahah-progress .throbber { +.ajax-progress .throbber { width: 15px; height: 15px; margin: 2px; background: transparent url(../../misc/throbber.gif) no-repeat 0px -18px; float: left; /* LTR */ } -tr .ahah-progress .throbber { +tr .ajax-progress .throbber { margin: 0 2px; } -.ahah-progress-bar { +.ajax-progress-bar { width: 16em; } diff --git a/modules/system/system.install b/modules/system/system.install index f377f37..f446fb8 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -1,5 +1,5 @@ TRUE, 'query' => "Aggressive caching was disabled and replaced with normal caching, please read the page caching section in default.settings.php for more information on how to enable similar functionality."); + } + return array(); +} + /** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. diff --git a/modules/system/system.module b/modules/system/system.module index 3a490e0..68c4bee 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -1,5 +1,5 @@ 'op', '#button_type' => 'submit', '#executes_submit_callback' => TRUE, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme_wrappers' => array('button'), ); @@ -295,7 +295,7 @@ function system_elements() { '#name' => 'op', '#button_type' => 'submit', '#executes_submit_callback' => FALSE, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme_wrappers' => array('button'), ); @@ -303,7 +303,7 @@ function system_elements() { '#input' => TRUE, '#button_type' => 'submit', '#executes_submit_callback' => TRUE, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#return_value' => TRUE, '#has_garbage_value' => TRUE, '#src' => NULL, @@ -315,7 +315,7 @@ function system_elements() { '#size' => 60, '#maxlength' => 128, '#autocomplete_path' => FALSE, - '#process' => array('form_process_text_format', 'form_process_ahah'), + '#process' => array('form_process_text_format', 'ajax_process_form'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), ); @@ -324,7 +324,7 @@ function system_elements() { '#input' => TRUE, '#size' => 60, '#maxlength' => 128, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme' => 'password', '#theme_wrappers' => array('form_element'), ); @@ -340,7 +340,7 @@ function system_elements() { '#cols' => 60, '#rows' => 5, '#resizable' => TRUE, - '#process' => array('form_process_text_format', 'form_process_ahah'), + '#process' => array('form_process_text_format', 'ajax_process_form'), '#theme' => 'textarea', '#theme_wrappers' => array('form_element'), ); @@ -355,7 +355,7 @@ function system_elements() { $type['radio'] = array( '#input' => TRUE, '#default_value' => NULL, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme' => 'radio', '#theme_wrappers' => array('form_element'), '#form_element_skip_title' => TRUE, @@ -372,7 +372,7 @@ function system_elements() { $type['checkbox'] = array( '#input' => TRUE, '#return_value' => 1, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme' => 'checkbox', '#theme_wrappers' => array('form_element'), '#form_element_skip_title' => TRUE, @@ -382,7 +382,7 @@ function system_elements() { '#input' => TRUE, '#size' => 0, '#multiple' => FALSE, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme' => 'select', '#theme_wrappers' => array('form_element'), ); @@ -391,7 +391,7 @@ function system_elements() { '#input' => TRUE, '#delta' => 10, '#default_value' => 0, - '#process' => array('form_process_weight', 'form_process_ahah'), + '#process' => array('form_process_weight', 'ajax_process_form'), ); $type['date'] = array( @@ -430,7 +430,7 @@ function system_elements() { $type['hidden'] = array( '#input' => TRUE, - '#process' => array('form_process_ahah'), + '#process' => array('ajax_process_form'), '#theme' => 'hidden', ); @@ -447,7 +447,7 @@ function system_elements() { '#collapsible' => FALSE, '#collapsed' => FALSE, '#value' => NULL, - '#process' => array('form_process_fieldset', 'form_process_ahah'), + '#process' => array('form_process_fieldset', 'ajax_process_form'), '#pre_render' => array('form_pre_render_fieldset'), '#theme_wrappers' => array('fieldset'), ); @@ -476,9 +476,9 @@ function system_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); - $items['system/ahah'] = array( + $items['system/ajax'] = array( 'title' => 'AHAH callback', - 'page callback' => 'form_ahah_callback', + 'page callback' => 'ajax_form_callback', 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); diff --git a/modules/taxonomy/CVS/Entries b/modules/taxonomy/CVS/Entries index fc02037..8c93372 100644 --- a/modules/taxonomy/CVS/Entries +++ b/modules/taxonomy/CVS/Entries @@ -6,6 +6,6 @@ /taxonomy.js/1.6/Fri Jun 12 07:15:26 2009// /taxonomy.test/1.43/Fri Aug 7 05:58:22 2009// /vocabulary.js/1.1/Fri Jun 12 19:29:04 2009// -/taxonomy.module/1.495/Sun Aug 16 17:50:50 2009// -/taxonomy.pages.inc/1.31/Sun Aug 16 17:50:50 2009// +/taxonomy.module/1.495/Mon Aug 17 16:10:12 2009// +/taxonomy.pages.inc/1.31/Mon Aug 17 16:10:12 2009// D diff --git a/modules/upload/CVS/Entries b/modules/upload/CVS/Entries index 3ae2c58..15b3219 100644 --- a/modules/upload/CVS/Entries +++ b/modules/upload/CVS/Entries @@ -2,6 +2,6 @@ /upload.info/1.9/Sat Jun 13 03:12:01 2009// /upload.install/1.12/Sat Jun 13 03:12:01 2009// /upload.js/1.3/Fri Jun 12 07:15:26 2009// -/upload.module/1.245/Fri Aug 7 05:58:22 2009// /upload.test/1.22/Fri Aug 7 05:58:22 2009// +/upload.module/1.246/Mon Aug 17 16:10:25 2009// D diff --git a/modules/upload/upload.module b/modules/upload/upload.module index 70e883f..ca1e550 100644 --- a/modules/upload/upload.module +++ b/modules/upload/upload.module @@ -1,5 +1,5 @@ files as $file) { @@ -236,7 +236,7 @@ function upload_form_alter(&$form, $form_state, $form_id) { '#weight' => 30, ); - // Wrapper for fieldset contents (used by ahah.js). + // Wrapper for fieldset contents (used by ajax.js). $form['attachments']['wrapper'] = array( '#prefix' => '
', '#suffix' => '
', @@ -582,7 +582,7 @@ function _upload_form($node) { '#type' => 'submit', '#value' => t('Attach'), '#name' => 'attach', - '#ahah' => array( + '#ajax' => array( 'path' => 'upload/js', 'wrapper' => 'attach-wrapper', 'progress' => array('type' => 'bar', 'message' => t('Please wait...')), @@ -692,9 +692,11 @@ function upload_js() { $form = form_builder('upload_js', $form, $form_state); $output = theme('status_messages') . drupal_render($form); - // We send the updated file attachments form. - // Don't call drupal_json(). ahah.js uses an iframe and - // the header output by drupal_json() causes problems in some browsers. - print drupal_to_js(array('status' => TRUE, 'data' => $output)); - exit; + $commands = array(); + $commands[] = ajax_command_replace(NULL, $output); + + // AJAX uploads use an