Commit cb98091e authored by webchick's avatar webchick

#108818 by David Strauss, chx, Crell: Add transactions to key X_save() routines.

parent bf703452
......@@ -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');
......
......@@ -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 @@ abstract class Database {
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);
}
/**
......
......@@ -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'],
......
......@@ -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);
}
}
/**
......
......@@ -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