diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index bc13133ca1f0fd822d41fcd197ec6c2d0d6e291b..803212c14606097cfe2c7ebc56a05d45cf99504c 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1276,6 +1276,8 @@ function request_uri() { * * @see watchdog_severity_levels() * @see hook_watchdog() + * @see DatabaseConnection::rollback() + * @see DatabaseTransaction::rollback() */ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) { global $user, $base_root; @@ -1602,6 +1604,10 @@ function _drupal_bootstrap_database() { // Initialize the database system. Note that the connection // won't be initialized until it is actually requested. require_once DRUPAL_ROOT . '/includes/database/database.inc'; + + // Set Drupal's watchdog as the logging callback. + Database::setLoggingCallback('watchdog', WATCHDOG_NOTICE, WATCHDOG_ERROR); + // Register autoload functions so that we can access classes and interfaces. spl_autoload_register('drupal_autoload_class'); spl_autoload_register('drupal_autoload_interface'); diff --git a/includes/database/database.inc b/includes/database/database.inc index 81f04fb2e02d8e24719239e8b090565b1f47eab3..074911c75c61045b0f930c05d490b282e4cb3771 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -228,6 +228,13 @@ abstract class DatabaseConnection extends PDO { */ protected $willRollback; + /** + * Array of argument arrays for logging post-rollback. + * + * @var array + */ + protected $rollbackLogs = array(); + /** * The name of the Select class for this connection. * @@ -849,12 +856,53 @@ public function startTransaction($required = FALSE) { * Schedule the current transaction for rollback. * * This method throws an exception if no transaction is active. - */ - public function rollback() { + * + * @param $type + * The category to which the rollback message belongs. + * @param $message + * The message to store in the log. Keep $message translatable + * by not concatenating dynamic values into it! Variables in the + * message should be added by using placeholder strings alongside + * the variables argument to declare the value of the placeholders. + * @param $variables + * Array of variables to replace in the message on display or + * NULL if message is already translated or not possible to + * translate. + * @param $severity + * The severity of the message, as per RFC 3164. + * @param $link + * A link to associate with the message. + * + * @see DatabaseTransaction::rollback() + * @see watchdog() + */ + public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) { if ($this->transactionLayers == 0) { throw new NoActiveTransactionException(); } + // Set the severity to the configured default if not specified. + if (!isset($severity)) { + $logging = Database::getLoggingCallback(); + if (is_array($logging)) { + $severity = $logging['default_severity']; + } + } + + // Record in an array to send to the log after transaction rollback. Messages written + // directly to a log (with a database back-end) will roll back during the following + // transaction rollback. This is an array because rollback could be requested multiple + // times during a transaction, and all such errors ought to be logged. + if (isset($message)) { + $this->rollbackLogs[] = array( + 'type' => $type, + 'message' => $message, + 'variables' => $variables, + 'severity' => $severity, + 'link' => $link, + ); + } + $this->willRollback = TRUE; } @@ -890,9 +938,6 @@ public function pushTransaction() { if ($this->supportsTransactions()) { parent::beginTransaction(); } - - // Reset any scheduled rollback - $this->willRollback = FALSE; } } @@ -912,11 +957,41 @@ public function popTransaction() { --$this->transactionLayers; - if ($this->transactionLayers == 0 && $this->supportsTransactions()) { + if ($this->transactionLayers == 0) { if ($this->willRollback) { - parent::rollBack(); + $logging = Database::getLoggingCallback(); + $logging_callback = NULL; + if (is_array($logging)) { + $logging_callback = $logging['callback']; + } + + if ($this->supportsTransactions()) { + parent::rollBack(); + } + else { + if (isset($logging_callback)) { + // Log the failed rollback. + $logging_callback('database', 'Explicit rollback failed: not supported on active connection.', array(), $logging['error_severity']); + } + + // It would be nice to throw an exception here if logging failed, + // but throwing exceptions in destructors is not supported. + } + + if (isset($logging_callback)) { + // Play back the logged errors to the specified logging callback post-rollback. + foreach ($this->rollbackLogs as $log_item) { + $logging_callback($log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']); + } + } + + // Reset any scheduled rollback. + $this->willRollback = FALSE; + + // Reset the error logs. + $this->rollbackLogs = array(); } - else { + elseif ($this->supportsTransactions()) { parent::commit(); } } @@ -1163,6 +1238,17 @@ abstract class Database { */ static protected $logs = array(); + /** + * A logging function callback array. + * + * The function must accept the same function signature as Drupal's watchdog(). + * The array containst key/value pairs for callback (string), default_severity (int), + * and error_severity (int). + * + * @var string + */ + static protected $logging_callback = NULL; + /** * Start logging a given logging key on the specified connection. * @@ -1193,6 +1279,37 @@ final public static function startLog($logging_key, $key = 'default') { return self::$logs[$key]; } + /** + * Set a logging callback for notices and errors. + * + * @see watchdog() + * @param $logging_callback + * The function to use as the logging callback. + * @param $logging_default_severity + * The default severity level to use for logged messages. + * @param $logging_error_severity + * The severity level to use for logging error messages. + */ + final public static function setLoggingCallback($callback, $default_severity, $error_severity) { + self::$logging_callback = array( + 'callback' => $callback, + 'default_severity' => $default_severity, + 'error_severity' => $error_severity, + ); + } + + /** + * Get the logging callback for notices and errors. + * + * @return + * An array with the logging callback and severity levels. + * + * @see watchdog() + */ + final public static function getLoggingCallback() { + return self::$logging_callback; + } + /** * Retrieve the queries logged on for given logging key. * @@ -1542,9 +1659,34 @@ public function __destruct() { * * This is just a wrapper method to rollback whatever transaction stack we * are currently in, which is managed by the connection object itself. - */ - public function rollback() { - $this->connection->rollback(); + * + * @param $type + * The category to which the rollback message belongs. + * @param $message + * The message to store in the log. Keep $message translatable + * by not concatenating dynamic values into it! Variables in the + * message should be added by using placeholder strings alongside + * the variables argument to declare the value of the placeholders. + * @param $variables + * Array of variables to replace in the message on display or + * NULL if message is already translated or not possible to + * translate. + * @param $severity + * The severity of the message, as per RFC 3164. + * @param $link + * A link to associate with the message. + * + * @see DatabaseConnection::rollback() + * @see watchdog() + */ + public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) { + if (!isset($severity)) { + $logging = Database::getLoggingCallback(); + if (is_array($logging)) { + $severity = $logging['default_severity']; + } + } + $this->connection->rollback($type, $message, $variables, $severity, $link); } /** diff --git a/modules/block/block.admin.inc b/modules/block/block.admin.inc index c605424a86311a2e0a96c532cdebb80946ee0dad..a747a5b4e93b79710633360651bf327b85a742b7 100644 --- a/modules/block/block.admin.inc +++ b/modules/block/block.admin.inc @@ -114,6 +114,8 @@ function block_admin_display_form($form, &$form_state, $blocks, $theme) { * Process main blocks administration form submissions. */ function block_admin_display_form_submit($form, &$form_state) { + $txn = db_transaction(); + foreach ($form_state['values'] as $block) { $block['status'] = (int) ($block['region'] != BLOCK_REGION_NONE); $block['region'] = $block['status'] ? $block['region'] : ''; @@ -365,6 +367,8 @@ function block_admin_configure_validate($form, &$form_state) { function block_admin_configure_submit($form, &$form_state) { if (!form_get_errors()) { + $txn = db_transaction(); + db_update('block') ->fields(array( 'visibility' => (int) $form_state['values']['visibility'], diff --git a/modules/comment/comment.module b/modules/comment/comment.module index 0d9f9c7eddd6fa6e8605e26da7f761fbf9b25004..5b10d35be3cd03afcd681ea69b9bb392c33d5872 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -1245,144 +1245,151 @@ function comment_access($op, $comment) { function comment_save($comment) { global $user; - $defaults = array( - 'mail' => '', - 'homepage' => '', - 'name' => '', - 'status' => user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, - ); - foreach ($defaults as $key => $default) { - if (!isset($comment->$key)) { - $comment->$key = $default; + $transaction = db_transaction(); + try { + $defaults = array( + 'mail' => '', + 'homepage' => '', + 'name' => '', + 'status' => user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, + ); + foreach ($defaults as $key => $default) { + if (!isset($comment->$key)) { + $comment->$key = $default; + } + } + // Make sure we have a bundle name. + if (!isset($comment->node_type)) { + $node = node_load($comment->nid); + $comment->node_type = 'comment_node_' . $node->type; } - } - // Make sure we have a bundle name. - if (!isset($comment->node_type)) { - $node = node_load($comment->nid); - $comment->node_type = 'comment_node_' . $node->type; - } - field_attach_presave('comment', $comment); + field_attach_presave('comment', $comment); - // Allow modules to alter the comment before saving. - module_invoke_all('comment_presave', $comment); + // Allow modules to alter the comment before saving. + module_invoke_all('comment_presave', $comment); - if ($comment->cid) { - // Update the comment in the database. - db_update('comment') - ->fields(array( - 'status' => $comment->status, - 'created' => $comment->created, - 'changed' => $comment->changed, - 'subject' => $comment->subject, - 'comment' => $comment->comment, - 'format' => $comment->comment_format, - 'uid' => $comment->uid, - 'name' => $comment->name, - 'mail' => $comment->mail, - 'homepage' => $comment->homepage, - 'language' => $comment->language, - )) - ->condition('cid', $comment->cid) - ->execute(); - field_attach_update('comment', $comment); - // Allow modules to respond to the updating of a comment. - module_invoke_all('comment_update', $comment); - // Add an entry to the watchdog log. - watchdog('content', 'Comment: updated %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); - } - else { - // Add the comment to database. This next section builds the thread field. - // Also see the documentation for comment_build(). - if ($comment->pid == 0) { - // This is a comment with no parent comment (depth 0): we start - // by retrieving the maximum thread level. - $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); - // Strip the "/" from the end of the thread. - $max = rtrim($max, '/'); - // Finally, build the thread field for this new comment. - $thread = int2vancode(vancode2int($max) + 1) . '/'; + if ($comment->cid) { + // Update the comment in the database. + db_update('comment') + ->fields(array( + 'status' => $comment->status, + 'created' => $comment->created, + 'changed' => $comment->changed, + 'subject' => $comment->subject, + 'comment' => $comment->comment, + 'format' => $comment->comment_format, + 'uid' => $comment->uid, + 'name' => $comment->name, + 'mail' => $comment->mail, + 'homepage' => $comment->homepage, + 'language' => $comment->language, + )) + ->condition('cid', $comment->cid) + ->execute(); + field_attach_update('comment', $comment); + // Allow modules to respond to the updating of a comment. + module_invoke_all('comment_update', $comment); + // Add an entry to the watchdog log. + watchdog('content', 'Comment: updated %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); } else { - // This is a comment with a parent comment, so increase the part of the - // thread value at the proper depth. - - // Get the parent comment: - $parent = comment_load($comment->pid); - // Strip the "/" from the end of the parent thread. - $parent->thread = (string) rtrim((string) $parent->thread, '/'); - // Get the max value in *this* thread. - $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( - ':thread' => $parent->thread . '.%', - ':nid' => $comment->nid, - ))->fetchField(); - - if ($max == '') { - // First child of this parent. - $thread = $parent->thread . '.' . int2vancode(0) . '/'; - } - else { - // Strip the "/" at the end of the thread. + // Add the comment to database. This next section builds the thread field. + // Also see the documentation for comment_build(). + if ($comment->pid == 0) { + // This is a comment with no parent comment (depth 0): we start + // by retrieving the maximum thread level. + $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); + // Strip the "/" from the end of the thread. $max = rtrim($max, '/'); - // Get the value at the correct depth. - $parts = explode('.', $max); - $parent_depth = count(explode('.', $parent->thread)); - $last = $parts[$parent_depth]; // Finally, build the thread field for this new comment. - $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/'; + $thread = int2vancode(vancode2int($max) + 1) . '/'; + } + else { + // This is a comment with a parent comment, so increase the part of the + // thread value at the proper depth. + + // Get the parent comment: + $parent = comment_load($comment->pid); + // Strip the "/" from the end of the parent thread. + $parent->thread = (string) rtrim((string) $parent->thread, '/'); + // Get the max value in *this* thread. + $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( + ':thread' => $parent->thread . '.%', + ':nid' => $comment->nid, + ))->fetchField(); + + if ($max == '') { + // First child of this parent. + $thread = $parent->thread . '.' . int2vancode(0) . '/'; + } + else { + // Strip the "/" at the end of the thread. + $max = rtrim($max, '/'); + // Get the value at the correct depth. + $parts = explode('.', $max); + $parent_depth = count(explode('.', $parent->thread)); + $last = $parts[$parent_depth]; + // Finally, build the thread field for this new comment. + $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/'; + } } - } - if (empty($comment->created)) { - $comment->created = REQUEST_TIME; - } + if (empty($comment->created)) { + $comment->created = REQUEST_TIME; + } - if (empty($comment->changed)) { - $comment->changed = $comment->created; - } + if (empty($comment->changed)) { + $comment->changed = $comment->created; + } - if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. - $comment->name = $user->name; - } + if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. + $comment->name = $user->name; + } - $comment->cid = db_insert('comment') - ->fields(array( - 'nid' => $comment->nid, - 'pid' => empty($comment->pid) ? 0 : $comment->pid, - 'uid' => $comment->uid, - 'subject' => $comment->subject, - 'comment' => $comment->comment, - 'format' => $comment->comment_format, - 'hostname' => ip_address(), - 'created' => $comment->created, - 'changed' => $comment->changed, - 'status' => $comment->status, - 'thread' => $thread, - 'name' => $comment->name, - 'mail' => $comment->mail, - 'homepage' => $comment->homepage, - 'language' => $comment->language, - )) - ->execute(); + $comment->cid = db_insert('comment') + ->fields(array( + 'nid' => $comment->nid, + 'pid' => empty($comment->pid) ? 0 : $comment->pid, + 'uid' => $comment->uid, + 'subject' => $comment->subject, + 'comment' => $comment->comment, + 'format' => $comment->comment_format, + 'hostname' => ip_address(), + 'created' => $comment->created, + 'changed' => $comment->changed, + 'status' => $comment->status, + 'thread' => $thread, + 'name' => $comment->name, + 'mail' => $comment->mail, + 'homepage' => $comment->homepage, + 'language' => $comment->language, + )) + ->execute(); - // Ignore slave server temporarily to give time for the - // saved node to be propagated to the slave. - db_ignore_slave(); + // Ignore slave server temporarily to give time for the + // saved node to be propagated to the slave. + db_ignore_slave(); - field_attach_insert('comment', $comment); + field_attach_insert('comment', $comment); - // Tell the other modules a new comment has been submitted. - module_invoke_all('comment_insert', $comment); - // Add an entry to the watchdog log. - watchdog('content', 'Comment: added %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); - } - _comment_update_node_statistics($comment->nid); - // Clear the cache so an anonymous user can see his comment being added. - cache_clear_all(); + // Tell the other modules a new comment has been submitted. + module_invoke_all('comment_insert', $comment); + // Add an entry to the watchdog log. + watchdog('content', 'Comment: added %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); + } + _comment_update_node_statistics($comment->nid); + // Clear the cache so an anonymous user can see his comment being added. + cache_clear_all(); - if ($comment->status == COMMENT_PUBLISHED) { - module_invoke_all('comment_publish', $comment); + if ($comment->status == COMMENT_PUBLISHED) { + module_invoke_all('comment_publish', $comment); + } + } + catch (Exception $e) { + $transaction->rollback('comment', $e->getMessage(), array(), WATCHDOG_ERROR); } + } /** diff --git a/modules/node/node.module b/modules/node/node.module index 4b036225bbdc55afa9e756557881c5cf2f20e89f..e3775ca64dcb06ab2af667e340685d6f6e0e0471 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -933,108 +933,115 @@ function node_submit($node) { * omitted (or $node->is_new is TRUE), a new node will be added. */ function node_save($node) { - field_attach_presave('node', $node); - // Let modules modify the node before it is saved to the database. - module_invoke_all('node_presave', $node); - global $user; + $transaction = db_transaction(); - if (!isset($node->is_new)) { - $node->is_new = empty($node->nid); - } + try { + field_attach_presave('node', $node); + // Let modules modify the node before it is saved to the database. + module_invoke_all('node_presave', $node); + global $user; - // Apply filters to some default node fields: - if ($node->is_new) { - // Insert a new node. - $node->is_new = TRUE; + if (!isset($node->is_new)) { + $node->is_new = empty($node->nid); + } - // When inserting a node, $node->log must be set because - // {node_revision}.log does not (and cannot) have a default - // value. If the user does not have permission to create - // revisions, however, the form will not contain an element for - // log so $node->log will be unset at this point. - if (!isset($node->log)) { - $node->log = ''; + // Apply filters to some default node fields: + if ($node->is_new) { + // Insert a new node. + $node->is_new = TRUE; + + // When inserting a node, $node->log must be set because + // {node_revision}.log does not (and cannot) have a default + // value. If the user does not have permission to create + // revisions, however, the form will not contain an element for + // log so $node->log will be unset at this point. + if (!isset($node->log)) { + $node->log = ''; + } } - } - elseif (!empty($node->revision)) { - $node->old_vid = $node->vid; - unset($node->vid); - } - else { - // When updating a node, avoid clobbering an existing log entry with an empty one. - if (empty($node->log)) { - unset($node->log); + elseif (!empty($node->revision)) { + $node->old_vid = $node->vid; + unset($node->vid); + } + else { + // When updating a node, avoid clobbering an existing log entry with an empty one. + if (empty($node->log)) { + unset($node->log); + } } - } - - // Set some required fields: - if (empty($node->created)) { - $node->created = REQUEST_TIME; - } - // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...) - $node->changed = REQUEST_TIME; - - $node->timestamp = REQUEST_TIME; - $update_node = TRUE; - - // When converting the title property to fields we preserved the {node}.title - // db column for performance, setting the default language value into this - // column. After this we restore the field data structure to the previous node - // title field. - $title_field = $node->title; - $langcode = FIELD_LANGUAGE_NONE; - $node->title = $title_field[$langcode][0]['value']; - // Generate the node table query and the node_revisions table query. - if ($node->is_new) { - drupal_write_record('node', $node); - _node_save_revision($node, $user->uid); - $op = 'insert'; - } - else { - drupal_write_record('node', $node, 'nid'); - if (!empty($node->revision)) { + // Set some required fields: + if (empty($node->created)) { + $node->created = REQUEST_TIME; + } + // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...) + $node->changed = REQUEST_TIME; + + $node->timestamp = REQUEST_TIME; + $update_node = TRUE; + + // When converting the title property to fields we preserved the {node}.title + // db column for performance, setting the default language value into this + // column. After this we restore the field data structure to the previous node + // title field. + $title_field = $node->title; + $langcode = FIELD_LANGUAGE_NONE; + $node->title = $title_field[$langcode][0]['value']; + + // Generate the node table query and the node_revisions table query. + if ($node->is_new) { + drupal_write_record('node', $node); _node_save_revision($node, $user->uid); + $op = 'insert'; } else { - _node_save_revision($node, $user->uid, 'vid'); - $update_node = FALSE; + drupal_write_record('node', $node, 'nid'); + if (!empty($node->revision)) { + _node_save_revision($node, $user->uid); + } + else { + _node_save_revision($node, $user->uid, 'vid'); + $update_node = FALSE; + } + $op = 'update'; } - $op = 'update'; + if ($update_node) { + db_update('node') + ->fields(array('vid' => $node->vid)) + ->condition('nid', $node->nid) + ->execute(); + } + + // Restore the title field data structure after db storage. + $node->title = $title_field; + + // Call the node specific callback (if any). This can be + // node_invoke($node, 'insert') or + // node_invoke($node, 'update'). + node_invoke($node, $op); + + // Save fields. + $function = "field_attach_$op"; + $function('node', $node); + + module_invoke_all('node_' . $op, $node); + + // Update the node access table for this node. + node_access_acquire_grants($node); + + // Clear internal properties. + unset($node->is_new); + + // Clear the page and block caches. + cache_clear_all(); + + // Ignore slave server temporarily to give time for the + // saved node to be propagated to the slave. + db_ignore_slave(); } - if ($update_node) { - db_update('node') - ->fields(array('vid' => $node->vid)) - ->condition('nid', $node->nid) - ->execute(); + catch (Exception $e) { + $transaction->rollback('node', $e->getMessage(), array(), WATCHDOG_ERROR); } - - // Restore the title field data structure after db storage. - $node->title = $title_field; - - // Call the node specific callback (if any). This can be - // node_invoke($node, 'insert') or - // node_invoke($node, 'update'). - node_invoke($node, $op); - - // Save fields. - $function = "field_attach_$op"; - $function('node', $node); - - module_invoke_all('node_' . $op, $node); - - // Update the node access table for this node. - node_access_acquire_grants($node); - - // Clear internal properties. - unset($node->is_new); - - // Clear the page and block caches. - cache_clear_all(); - - // Ignore slave server temporarily to give time for the - // saved node to be propagated to the slave. - db_ignore_slave(); } /** diff --git a/modules/node/node.test b/modules/node/node.test index 807dc5f652d94aa237e857d6a5290d73f4491f1d..2fd36a97c11d73c36e2e3f57dca6f06db449fd90 100644 --- a/modules/node/node.test +++ b/modules/node/node.test @@ -329,7 +329,8 @@ class PageCreationTestCase extends DrupalWebTestCase { } function setUp() { - parent::setUp(); + // Enable dummy module that implements hook_node_post_save for exceptions. + parent::setUp('node_test_exception'); $web_user = $this->drupalCreateUser(array('create page content', 'edit own page content')); $this->drupalLogin($web_user); @@ -353,6 +354,37 @@ class PageCreationTestCase extends DrupalWebTestCase { $node = $this->drupalGetNodeByTitle($edit["title[$langcode][0][value]"]); $this->assertTrue($node, t('Node found in database.')); } + + /** + * Create a page node and verify that a transaction rolls back the failed creation + */ + function testFailedPageCreation() { + // Create a node. + $edit = array(); + $langcode = FIELD_LANGUAGE_NONE; + $edit["title[$langcode][0][value]"] = 'testing_transaction_exception'; + $edit["body[$langcode][0][value]"] = $this->randomName(16); + $this->drupalPost('node/add/page', $edit, t('Save')); + + if (Database::getConnection()->supportsTransactions()) { + // Check that the node does not exist in the database. + $node = $this->drupalGetNodeByTitle($edit["title[$langcode][0][value]"]); + $this->assertFalse($node, t('Transactions supported, and node not found in database.')); + } + else { + // Check that the node exists in the database. + $node = $this->drupalGetNodeByTitle($edit["title[$langcode][0][value]"]); + $this->assertTrue($node, t('Transactions not supported, and node found in database.')); + + // Check that the failed rollback was logged. + $records = db_query("SELECT wid FROM {watchdog} WHERE message LIKE 'Explicit rollback failed%'")->fetchAll(); + $this->assertTrue(count($records) > 0, t('Transactions not supported, and rollback error logged to watchdog.')); + } + + // Check that the rollback error was logged. + $records = db_query("SELECT wid FROM {watchdog} WHERE message LIKE 'Test exception for rollback.'")->fetchAll(); + $this->assertTrue(count($records) > 0, t('Rollback explanatory error logged to watchdog.')); + } } class PageViewTestCase extends DrupalWebTestCase { diff --git a/modules/node/tests/node_test_exception.info b/modules/node/tests/node_test_exception.info new file mode 100644 index 0000000000000000000000000000000000000000..afe5b37196537eaf83a69bd1c0cb6b2d758e1806 --- /dev/null +++ b/modules/node/tests/node_test_exception.info @@ -0,0 +1,8 @@ +; $Id$ +name = "Node module exception tests" +description = "Support module for node related exception testing." +package = Testing +version = VERSION +core = 7.x +files[] = node_test_exception.module +hidden = TRUE diff --git a/modules/node/tests/node_test_exception.module b/modules/node/tests/node_test_exception.module new file mode 100644 index 0000000000000000000000000000000000000000..7b09c77c7dd0cc18360aa04a8bf0da26f9fb373e --- /dev/null +++ b/modules/node/tests/node_test_exception.module @@ -0,0 +1,17 @@ +<?php +// $Id$ + +/** + * @file + * Dummy module implementing node related hooks to test API interaction with + * the Node module. + */ + +/** + * Implement hook_node_insert(). + */ +function node_test_exception_node_insert($node) { + if ($node->title['zxx'][0]['value'] == 'testing_transaction_exception') { + throw new Exception('Test exception for rollback.'); + } +} diff --git a/modules/user/user.module b/modules/user/user.module index a9cca984909282102f1df899b6ecbb1e1eab0032..2772b5ffb83675572bbc3aef76b954e9a58cb59a 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -304,219 +304,225 @@ function user_load_by_name($name) { * A fully-loaded $user object upon successful save or FALSE if the save failed. */ function user_save($account, $edit = array(), $category = 'account') { - $table = drupal_get_schema('users'); - $user_fields = $table['fields']; - - if (!empty($edit['pass'])) { - // Allow alternate password hashing schemes. - require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); - $edit['pass'] = user_hash_password(trim($edit['pass'])); - // Abort if the hashing failed and returned FALSE. - if (!$edit['pass']) { - return FALSE; + $transaction = db_transaction(); + try { + $table = drupal_get_schema('users'); + $user_fields = $table['fields']; + + if (!empty($edit['pass'])) { + // Allow alternate password hashing schemes. + require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); + $edit['pass'] = user_hash_password(trim($edit['pass'])); + // Abort if the hashing failed and returned FALSE. + if (!$edit['pass']) { + return FALSE; + } + } + else { + // Avoid overwriting an existing password with a blank password. + unset($edit['pass']); } - } - else { - // Avoid overwriting an existing password with a blank password. - unset($edit['pass']); - } - // Get the fields form so we can recognize the fields in the $edit - // form that should not go into the serialized data array. - $field_form = array(); - $field_form_state = array(); - $edit = (object) $edit; - field_attach_form('user', $edit, $field_form, $field_form_state); + // Get the fields form so we can recognize the fields in the $edit + // form that should not go into the serialized data array. + $field_form = array(); + $field_form_state = array(); + $edit = (object) $edit; + field_attach_form('user', $edit, $field_form, $field_form_state); - // Presave fields. - field_attach_presave('user', $edit); + // Presave fields. + field_attach_presave('user', $edit); - $edit = (array) $edit; + $edit = (array) $edit; - if (!isset($account->is_new)) { - $account->is_new = empty($account->uid); - } + if (!isset($account->is_new)) { + $account->is_new = empty($account->uid); + } - user_module_invoke('presave', $edit, $account, $category); + user_module_invoke('presave', $edit, $account, $category); - if (is_object($account) && !$account->is_new) { - $data = unserialize(db_query('SELECT data FROM {users} WHERE uid = :uid', array(':uid' => $account->uid))->fetchField()); - // Consider users edited by an administrator as logged in, if they haven't - // already, so anonymous users can view the profile (if allowed). - if (empty($edit['access']) && empty($account->access) && user_access('administer users')) { - $edit['access'] = REQUEST_TIME; - } - foreach ($edit as $key => $value) { - // Form fields that don't pertain to the users, user_roles, or - // Field API are automatically serialized into the users.data - // column. - if (!in_array($key, array('roles', 'is_new')) && empty($user_fields[$key]) && empty($field_form[$key])) { - if ($value === NULL) { - unset($data[$key]); - } - else { - $data[$key] = $value; + if (is_object($account) && !$account->is_new) { + $data = unserialize(db_query('SELECT data FROM {users} WHERE uid = :uid', array(':uid' => $account->uid))->fetchField()); + // Consider users edited by an administrator as logged in, if they haven't + // already, so anonymous users can view the profile (if allowed). + if (empty($edit['access']) && empty($account->access) && user_access('administer users')) { + $edit['access'] = REQUEST_TIME; + } + foreach ($edit as $key => $value) { + // Form fields that don't pertain to the users, user_roles, or + // Field API are automatically serialized into the users.data + // column. + if (!in_array($key, array('roles', 'is_new')) && empty($user_fields[$key]) && empty($field_form[$key])) { + if ($value === NULL) { + unset($data[$key]); + } + else { + $data[$key] = $value; + } } } - } - // Process picture uploads. - if (!empty($edit['picture']->fid)) { - $picture = $edit['picture']; - // If the picture is a temporary file move it to its final location and - // make it permanent. - if (($picture->status & FILE_STATUS_PERMANENT) == 0) { - $info = image_get_info($picture->uri); - $picture_directory = variable_get('file_default_scheme', 'public') . '://' . variable_get('user_picture_path', 'pictures'); - - // Prepare the pictures directory. - file_prepare_directory($picture_directory, FILE_CREATE_DIRECTORY); - $destination = file_stream_wrapper_uri_normalize($picture_directory . '/picture-' . $account->uid . '.' . $info['extension']); - - if ($picture = file_move($picture, $destination, FILE_EXISTS_REPLACE)) { - $picture->status |= FILE_STATUS_PERMANENT; - $edit['picture'] = file_save($picture); + // Process picture uploads. + if (!empty($edit['picture']->fid)) { + $picture = $edit['picture']; + // If the picture is a temporary file move it to its final location and + // make it permanent. + if (($picture->status & FILE_STATUS_PERMANENT) == 0) { + $info = image_get_info($picture->uri); + $picture_directory = variable_get('file_default_scheme', 'public') . '://' . variable_get('user_picture_path', 'pictures'); + + // Prepare the pictures directory. + file_prepare_directory($picture_directory, FILE_CREATE_DIRECTORY); + $destination = file_stream_wrapper_uri_normalize($picture_directory . '/picture-' . $account->uid . '.' . $info['extension']); + + if ($picture = file_move($picture, $destination, FILE_EXISTS_REPLACE)) { + $picture->status |= FILE_STATUS_PERMANENT; + $edit['picture'] = file_save($picture); + } } } - } - $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid; - - $edit['data'] = $data; - // Do not allow 'uid' to be changed. - $edit['uid'] = $account->uid; - // Save changes to the user table. - $success = drupal_write_record('users', $edit, 'uid'); - if ($success === FALSE) { - // The query failed - better to abort the save than risk further - // data loss. - return FALSE; - } - - // If the picture changed or was unset, remove the old one. This step needs - // to occur after updating the {users} record so that user_file_references() - // doesn't report it in use and block the deletion. - if (!empty($account->picture->fid) && ($edit['picture'] != $account->picture->fid)) { - file_delete($account->picture); - } + $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid; + + $edit['data'] = $data; + // Do not allow 'uid' to be changed. + $edit['uid'] = $account->uid; + // Save changes to the user table. + $success = drupal_write_record('users', $edit, 'uid'); + if ($success === FALSE) { + // The query failed - better to abort the save than risk further + // data loss. + return FALSE; + } - // Reload user roles if provided. - if (isset($edit['roles']) && is_array($edit['roles'])) { - db_delete('users_roles') - ->condition('uid', $account->uid) - ->execute(); + // If the picture changed or was unset, remove the old one. This step needs + // to occur after updating the {users} record so that user_file_references() + // doesn't report it in use and block the deletion. + if (!empty($account->picture->fid) && ($edit['picture'] != $account->picture->fid)) { + file_delete($account->picture); + } - $query = db_insert('users_roles')->fields(array('uid', 'rid')); - foreach (array_keys($edit['roles']) as $rid) { - if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) { - $query->values(array( - 'uid' => $account->uid, - 'rid' => $rid, - )); + // Reload user roles if provided. + if (isset($edit['roles']) && is_array($edit['roles'])) { + db_delete('users_roles') + ->condition('uid', $account->uid) + ->execute(); + + $query = db_insert('users_roles')->fields(array('uid', 'rid')); + foreach (array_keys($edit['roles']) as $rid) { + if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) { + $query->values(array( + 'uid' => $account->uid, + 'rid' => $rid, + )); + } } + $query->execute(); } - $query->execute(); - } - // Delete a blocked user's sessions to kick them if they are online. - if (isset($edit['status']) && $edit['status'] == 0) { - drupal_session_destroy_uid($account->uid); - } + // Delete a blocked user's sessions to kick them if they are online. + if (isset($edit['status']) && $edit['status'] == 0) { + drupal_session_destroy_uid($account->uid); + } - // If the password changed, delete all open sessions and recreate - // the current one. - if (!empty($edit['pass'])) { - drupal_session_destroy_uid($account->uid); - if ($account->uid == $GLOBALS['user']->uid) { - drupal_session_regenerate(); + // If the password changed, delete all open sessions and recreate + // the current one. + if (!empty($edit['pass'])) { + drupal_session_destroy_uid($account->uid); + if ($account->uid == $GLOBALS['user']->uid) { + drupal_session_regenerate(); + } } - } - // Save Field data. - $object = (object) $edit; - field_attach_update('user', $object); + // Save Field data. + $object = (object) $edit; + field_attach_update('user', $object); - // Refresh user object. - $user = user_load($account->uid, TRUE); + // Refresh user object. + $user = user_load($account->uid, TRUE); - // Send emails after we have the new user object. - if (isset($edit['status']) && $edit['status'] != $account->status) { - // The user's status is changing; conditionally send notification email. - $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked'; - _user_mail_notify($op, $user); - } + // Send emails after we have the new user object. + if (isset($edit['status']) && $edit['status'] != $account->status) { + // The user's status is changing; conditionally send notification email. + $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked'; + _user_mail_notify($op, $user); + } - user_module_invoke('update', $edit, $user, $category); - } - else { - // Allow 'uid' to be set by the caller. There is no danger of writing an - // existing user as drupal_write_record will do an INSERT. - if (empty($edit['uid'])) { - $edit['uid'] = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField()); - } - // Allow 'created' to be set by the caller. - if (!isset($edit['created'])) { - $edit['created'] = REQUEST_TIME; - } - // Consider users created by an administrator as already logged in, so - // anonymous users can view the profile (if allowed). - if (empty($edit['access']) && user_access('administer users')) { - $edit['access'] = REQUEST_TIME; + user_module_invoke('update', $edit, $user, $category); } + else { + // Allow 'uid' to be set by the caller. There is no danger of writing an + // existing user as drupal_write_record will do an INSERT. + if (empty($edit['uid'])) { + $edit['uid'] = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField()); + } + // Allow 'created' to be set by the caller. + if (!isset($edit['created'])) { + $edit['created'] = REQUEST_TIME; + } + // Consider users created by an administrator as already logged in, so + // anonymous users can view the profile (if allowed). + if (empty($edit['access']) && user_access('administer users')) { + $edit['access'] = REQUEST_TIME; + } - $edit['mail'] = trim($edit['mail']); - $success = drupal_write_record('users', $edit); - if ($success === FALSE) { - // On a failed INSERT some other existing user's uid may be returned. - // We must abort to avoid overwriting their account. - return FALSE; - } + $edit['mail'] = trim($edit['mail']); + $success = drupal_write_record('users', $edit); + if ($success === FALSE) { + // On a failed INSERT some other existing user's uid may be returned. + // We must abort to avoid overwriting their account. + return FALSE; + } - // Build the initial user object. - $user = user_load($edit['uid'], TRUE); + // Build the initial user object. + $user = user_load($edit['uid'], TRUE); - $object = (object) $edit; - field_attach_insert('user', $object); + $object = (object) $edit; + field_attach_insert('user', $object); - user_module_invoke('insert', $edit, $user, $category); + user_module_invoke('insert', $edit, $user, $category); - // Note, we wait with saving the data column to prevent module-handled - // fields from being saved there. - $data = array(); - foreach ($edit as $key => $value) { - // Form fields that don't pertain to the users, user_roles, or - // Field API are automatically serialized into the user.data - // column. - if ((!in_array($key, array('roles', 'is_new'))) && (empty($user_fields[$key]) && empty($field_form[$key])) && ($value !== NULL)) { - $data[$key] = $value; + // Note, we wait with saving the data column to prevent module-handled + // fields from being saved there. + $data = array(); + foreach ($edit as $key => $value) { + // Form fields that don't pertain to the users, user_roles, or + // Field API are automatically serialized into the user.data + // column. + if ((!in_array($key, array('roles', 'is_new'))) && (empty($user_fields[$key]) && empty($field_form[$key])) && ($value !== NULL)) { + $data[$key] = $value; + } + } + if (!empty($data)) { + $data_array = array('uid' => $user->uid, 'data' => $data); + drupal_write_record('users', $data_array, 'uid'); } - } - if (!empty($data)) { - $data_array = array('uid' => $user->uid, 'data' => $data); - drupal_write_record('users', $data_array, 'uid'); - } - // Save user roles (delete just to be safe). - if (isset($edit['roles']) && is_array($edit['roles'])) { - db_delete('users_roles') - ->condition('uid', $edit['uid']) - ->execute(); - $query = db_insert('users_roles')->fields(array('uid', 'rid')); - foreach (array_keys($edit['roles']) as $rid) { - if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) { - $query->values(array( - 'uid' => $edit['uid'], - 'rid' => $rid, - )); + // Save user roles (delete just to be safe). + if (isset($edit['roles']) && is_array($edit['roles'])) { + db_delete('users_roles') + ->condition('uid', $edit['uid']) + ->execute(); + $query = db_insert('users_roles')->fields(array('uid', 'rid')); + foreach (array_keys($edit['roles']) as $rid) { + if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) { + $query->values(array( + 'uid' => $edit['uid'], + 'rid' => $rid, + )); + } } + $query->execute(); } - $query->execute(); + + // Build the finished user object. + $user = user_load($edit['uid'], TRUE); } - // Build the finished user object. - $user = user_load($edit['uid'], TRUE); + return $user; + } + catch (Exception $e) { + $transaction->rollback('user', $e->getMessage(), array(), WATCHDOG_ERROR); } - - return $user; } /**