Index: modules/filter/filter.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.admin.inc,v retrieving revision 1.17 diff -u -r1.17 filter.admin.inc --- modules/filter/filter.admin.inc 15 Nov 2008 08:23:07 -0000 1.17 +++ modules/filter/filter.admin.inc 1 Dec 2008 05:55:58 -0000 @@ -286,7 +286,10 @@ $default = variable_get('filter_default_format', 1); // Replace existing instances of the deleted format with the default format. - db_query("UPDATE {node_revisions} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); + db_query("UPDATE {node} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); + if (db_table_exists('node_revisions')) { + db_query("UPDATE {node_revisions} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); + } db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); db_query("UPDATE {box} SET format = %d WHERE format = %d", $default, $form_state['values']['format']); Index: modules/node/node.css =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.css,v retrieving revision 1.5 diff -u -r1.5 node.css --- modules/node/node.css 25 Jan 2008 21:21:44 -0000 1.5 +++ modules/node/node.css 1 Dec 2008 05:55:58 -0000 @@ -17,9 +17,6 @@ margin-left: 0.5em; /* LTR */ clear: right; /* LTR */ } -td.revision-current { - background: #ffc; -} .node-form .form-text { display: block; width: 95%; Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.998 diff -u -r1.998 node.module --- modules/node/node.module 22 Nov 2008 14:09:41 -0000 1.998 +++ modules/node/node.module 1 Dec 2008 05:55:59 -0000 @@ -65,7 +65,7 @@ switch ($path) { case 'admin/help#node': - $output = '
' . t('The node module manages content on your site, and stores all posts (regardless of type) as a "node" . In addition to basic publishing settings, including whether the post has been published, promoted to the site front page, or should remain present (or sticky) at the top of lists, the node module also records basic information about the author of a post. Optional revision control over edits is available. For additional functionality, the node module is often extended by other modules.') . '
'; + $output = '' . t('The node module manages content on your site, and stores all posts (regardless of type) as a "node" . In addition to basic publishing settings, including whether the post has been published, promoted to the site front page, or should remain present (or sticky) at the top of lists, the node module also records basic information about the author of a post. Optional revision control over edits is available via the node_revisions module. For additional functionality, the node module is often extended by other modules.') . '
'; $output .= '' . t('Though each post on your site is a node, each post is also of a particular content type. Content types are used to define the characteristics of a post, including the title and description of the fields displayed on its add and edit pages. Each content type may have different default settings for Publishing options and other workflow controls. By default, the two content types in a standard Drupal installation are Page and Story. Use the content types page to add new or edit existing content types. Additional content types also become available as you enable additional core, contributed and custom modules.', array('@content-type' => url('admin/build/types'))) . '
'; $output .= '' . t('The administrative content page allows you to review and manage your site content. The post settings page sets certain options for the display of posts. The node module makes a number of permissions available for each content type, which may be set by role on the permissions page.', array('@content' => url('admin/content/node'), '@post-settings' => url('admin/content/node-settings'), '@permissions' => url('admin/user/permissions'))) . '
'; $output .= '' . t('For more information, see the online handbook entry for Node module.', array('@node' => 'http://drupal.org/handbook/modules/node/')) . '
'; @@ -76,8 +76,6 @@ return '' . t('Below is a list of all the content types on your site. All posts that exist on your site are instances of one of these content types.') . '
'; case 'admin/build/types/add': return '' . t('To create a new content type, enter the human-readable name, the machine-readable name, and all other relevant fields that are on this page. Once created, users of your site will be able to create posts that are instances of this content type.') . '
'; - case 'node/%/revisions': - return '' . t('The revisions let you track differences between multiple versions of a post.') . '
'; case 'node/%/edit': $node = node_load($arg[1]); $type = node_get_types('type', $node->type); @@ -129,9 +127,6 @@ 'arguments' => array('node' => NULL), 'file' => 'node.pages.inc', ), - 'node_log_message' => array( - 'arguments' => array('log' => NULL), - ), 'node_submitted' => array( 'arguments' => array('node' => NULL), ), @@ -734,29 +729,24 @@ * * @param $param * Either the nid of the node or an array of conditions to match against in the database query - * @param $revision - * Which numbered revision to load. Defaults to the current version. * @param $reset * Whether to reset the internal node_load cache. * * @return * A fully-populated node object. */ -function node_load($param = array(), $revision = NULL, $reset = NULL) { +function node_load($param = array(), $reset = NULL) { static $nodes = array(); if ($reset) { $nodes = array(); } - $cachable = ($revision == NULL); $arguments = array(); if (is_numeric($param)) { - if ($cachable) { - // Is the node statically cached? - if (isset($nodes[$param])) { - return is_object($nodes[$param]) ? clone $nodes[$param] : $nodes[$param]; - } + // Is the node statically cached? + if (isset($nodes[$param])) { + return is_object($nodes[$param]) ? clone $nodes[$param] : $nodes[$param]; } $cond = 'n.nid = %d'; $arguments[] = $param; @@ -775,27 +765,12 @@ // Retrieve a field list based on the site's schema. $fields = drupal_schema_fields_sql('node', 'n'); - $fields = array_merge($fields, drupal_schema_fields_sql('node_revisions', 'r')); $fields = array_merge($fields, array('u.name', 'u.picture', 'u.data')); - // Remove fields not needed in the query: n.vid and r.nid are redundant, - // n.title is unnecessary because the node title comes from the - // node_revisions table. We'll keep r.vid, r.title, and n.nid. - $fields = array_diff($fields, array('n.vid', 'n.title', 'r.nid')); $fields = implode(', ', $fields); - // Rename timestamp field for clarity. - $fields = str_replace('r.timestamp', 'r.timestamp AS revision_timestamp', $fields); - // Change name of revision uid so it doesn't conflict with n.uid. - $fields = str_replace('r.uid', 'r.uid AS revision_uid', $fields); // Retrieve the node. // No db_rewrite_sql is applied so as to get complete indexing for search. - if ($revision) { - array_unshift($arguments, $revision); - $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.nid = n.nid AND r.vid = %d WHERE ' . $cond, $arguments)); - } - else { - $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.vid = n.vid WHERE ' . $cond, $arguments)); - } + $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid WHERE ' . $cond, $arguments)); if ($node && $node->nid) { // Call the node specific callback (if any) and piggy-back the @@ -811,9 +786,7 @@ $node->$key = $value; } } - if ($cachable) { - $nodes[$node->nid] = is_object($node) ? clone $node : $node; - } + $nodes[$node->nid] = is_object($node) ? clone $node : $node; } return $node; @@ -914,17 +887,9 @@ // Insert a new node. $node->is_new = TRUE; - // When inserting a node, $node->log must be set because - // {node_revisions}.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 = ''; - } - - // For the same reasons, make sure we have $node->teaser and - // $node->body. We should consider making these fields nullable + // When inserting a node, $node->teaser and $node->body must be set + // because {node}.teaser and {node}.body do not (and cannot) have a + // default value. We should consider making these fields nullable // in a future version since node types are not required to use them. if (!isset($node->teaser)) { $node->teaser = ''; @@ -933,15 +898,6 @@ $node->body = ''; } } - elseif (!empty($node->revision)) { - $node->old_vid = $node->vid; - } - else { - // When updating a node, avoid clobberring an existing log entry with an empty one. - if (empty($node->log)) { - unset($node->log); - } - } // Set some required fields: if (empty($node->created)) { @@ -951,28 +907,16 @@ $node->changed = REQUEST_TIME; $node->timestamp = REQUEST_TIME; - $update_node = TRUE; - - // Generate the node table query and the node_revisions table query. + print_r($node); + // Generate the node 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)) { - _node_save_revision($node, $user->uid); - } - else { - _node_save_revision($node, $user->uid, 'vid'); - $update_node = FALSE; - } $op = 'update'; } - if ($update_node) { - db_query('UPDATE {node} SET vid = %d WHERE nid = %d', $node->vid, $node->nid); - } // Call the node specific callback (if any). This can be // node_invoke($node, 'insert') or @@ -988,24 +932,6 @@ } /** - * Helper function to save a revision with the uid of the current user. - * - * Node is taken by reference, becuse drupal_write_record() updates the - * $node with the revision id, and we need to pass that back to the caller. - */ -function _node_save_revision(&$node, $uid, $update = NULL) { - $temp_uid = $node->uid; - $node->uid = $uid; - if (isset($update)) { - drupal_write_record('node_revisions', $node, $update); - } - else { - drupal_write_record('node_revisions', $node); - } - $node->uid = $temp_uid; -} - -/** * Delete a node. */ function node_delete($nid) { @@ -1014,7 +940,6 @@ if (node_access('delete', $node)) { db_query('DELETE FROM {node} WHERE nid = %d', $node->nid); - db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid); // Call the node-specific callback (if any): node_invoke($node, 'delete'); @@ -1144,10 +1069,7 @@ /** * Generate a page displaying a single node, along with its comments. */ -function node_show($node, $cid, $message = FALSE) { - if ($message) { - drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))), PASS_THROUGH); - } +function node_show($node, $cid) { $output = node_view($node, FALSE, TRUE); if (function_exists('comment_render') && $node->comment) { @@ -1161,15 +1083,6 @@ } /** - * Theme a log message. - * - * @ingroup themeable - */ -function theme_node_log_message($log) { - return '' . filter_xss($revision->log) . '
' : ''), - 'class' => 'revision-current'); - $operations[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => 2); - } - else { - $row[] = t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '!username' => theme('username', $revision))) - . (($revision->log != '') ? '' . filter_xss($revision->log) . '
' : ''); - if ($revert_permission) { - $operations[] = l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert"); - } - if ($delete_permission) { - $operations[] = l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete"); - } - } - $rows[] = array_merge($row, $operations); - } - - return theme('table', $header, $rows); -} - -/** - * Ask for confirmation of the reversion to prevent against CSRF attacks. - */ -function node_revision_revert_confirm($form_state, $node_revision) { - $form['#node_revision'] = $node_revision; - return confirm_form($form, t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', '', t('Revert'), t('Cancel')); -} - -function node_revision_revert_confirm_submit($form, &$form_state) { - $node_revision = $form['#node_revision']; - $node_revision->revision = 1; - $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($node_revision->revision_timestamp))); - if (module_exists('taxonomy')) { - $node_revision->taxonomy = array_keys($node_revision->taxonomy); - } - - node_save($node_revision); - - watchdog('content', '@type: reverted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->title, '%revision' => $node_revision->vid)); - drupal_set_message(t('@type %title has been reverted back to the revision from %revision-date.', array('@type' => node_get_types('name', $node_revision), '%title' => $node_revision->title, '%revision-date' => format_date($node_revision->revision_timestamp)))); - $form_state['redirect'] = 'node/' . $node_revision->nid . '/revisions'; -} - -function node_revision_delete_confirm($form_state, $node_revision) { - $form['#node_revision'] = $node_revision; - return confirm_form($form, t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', t('This action cannot be undone.'), t('Delete'), t('Cancel')); -} - -function node_revision_delete_confirm_submit($form, &$form_state) { - $node_revision = $form['#node_revision']; - db_query("DELETE FROM {node_revisions} WHERE nid = %d AND vid = %d", $node_revision->nid, $node_revision->vid); - node_invoke_nodeapi($node_revision, 'delete_revision'); - watchdog('content', '@type: deleted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->title, '%revision' => $node_revision->vid)); - drupal_set_message(t('Revision from %revision-date of @type %title has been deleted.', array('%revision-date' => format_date($node_revision->revision_timestamp), '@type' => node_get_types('name', $node_revision), '%title' => $node_revision->title))); - $form_state['redirect'] = 'node/' . $node_revision->nid; - if (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $node_revision->nid)) > 1) { - $form_state['redirect'] .= '/revisions'; - } -} Index: modules/node/node.test =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.test,v retrieving revision 1.8 diff -u -r1.8 node.test --- modules/node/node.test 25 Nov 2008 13:14:27 -0000 1.8 +++ modules/node/node.test 1 Dec 2008 05:55:59 -0000 @@ -1,91 +1,6 @@ t('Node revisions'), - 'description' => t('Create a node with revisions and test viewing, reverting, and deleting revisions.'), - 'group' => t('Node'), - ); - } - - function setUp() { - parent::setUp(); - - // Create and login user. - $web_user = $this->drupalCreateUser(array('view revisions', 'revert revisions', 'edit any page content', - 'delete revisions', 'delete any page content')); - $this->drupalLogin($web_user); - - // Create initial node. - $node = $this->drupalCreateNode(); - $settings = get_object_vars($node); - $settings['revision'] = 1; - - $nodes = array(); - $logs = array(); - - // Get original node. - $nodes[] = $node; - - // Create three revisions. - $revision_count = 3; - for ($i = 0; $i < $revision_count; $i++) { - $logs[] = $settings['log'] = $this->randomName(32); - - // Create revision with random title and body and update variables. - $this->drupalCreateNode($settings); - $node = node_load($node->nid); // Make sure we get revision information. - $settings = get_object_vars($node); - - $nodes[] = $node; - } - - $this->nodes = $nodes; - $this->logs = $logs; - } - - /** - * Check node revision related opperations. - */ - function testRevisions() { - $nodes = $this->nodes; - $logs = $this->logs; - - // Get last node for simple checks. - $node = $nodes[3]; - - // Confirm the correct revision text appears on "view revisions" page. - $this->drupalGet("node/$node->nid/revisions/$node->vid/view"); - $this->assertText($node->body, t('Correct text displays for version.')); - - // Confirm the correct log message appears on "revisions overview" page. - $this->drupalGet("node/$node->nid/revisions"); - foreach ($logs as $log) { - $this->assertText($log, t('Log message found.')); - } - - // Confirm that revisions revert properly. - $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/revert", array(), t('Revert')); - $this->assertRaw(t('@type %title has been reverted back to the revision from %revision-date.', - array('@type' => 'Page', '%title' => $nodes[1]->title, - '%revision-date' => format_date($nodes[1]->revision_timestamp))), t('Revision reverted.')); - $reverted_node = node_load($node->nid); - $this->assertTrue(($nodes[1]->body == $reverted_node->body), t('Node reverted correctly.')); - - // Confirm revisions delete properly. - $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/delete", array(), t('Delete')); - $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', - array('%revision-date' => format_date($nodes[1]->revision_timestamp), - '@type' => 'Page', '%title' => $nodes[1]->title)), t('Revision deleted.')); - $this->assertTrue(db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d and vid = %d', $node->nid, $nodes[1]->vid)) == 0, t('Revision not found.')); - } -} - class NodeTeaserTestCase extends DrupalWebTestCase { function getInfo() { return array( Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.8 diff -u -r1.8 node.install --- modules/node/node.install 15 Nov 2008 13:01:08 -0000 1.8 +++ modules/node/node.install 1 Dec 2008 05:55:58 -0000 @@ -42,6 +42,24 @@ 'not null' => TRUE, 'default' => '', ), + 'body' => array( + 'description' => 'The body of this version.', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + ), + 'teaser' => array( + 'description' => 'The teaser of this version.', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + ), + 'format' => array( + 'description' => "The input format used by this version's body.", + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), 'uid' => array( 'description' => 'The {users}.uid that owns this node; initially, this is the user that created it.', 'type' => 'int', @@ -210,73 +228,6 @@ 'primary key' => array('nid'), ); - $schema['node_revisions'] = array( - 'description' => 'Stores information about each saved version of a {node}.', - 'fields' => array( - 'nid' => array( - 'description' => 'The {node} this version belongs to.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'vid' => array( - 'description' => 'The primary identifier for this version.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'uid' => array( - 'description' => 'The {users}.uid that created this version.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'title' => array( - 'description' => 'The title of this version.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'body' => array( - 'description' => 'The body of this version.', - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - ), - 'teaser' => array( - 'description' => 'The teaser of this version.', - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - ), - 'log' => array( - 'description' => 'The log entry explaining the changes in this version.', - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - ), - 'timestamp' => array( - 'description' => 'A Unix timestamp indicating when this version was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'format' => array( - 'description' => "The input format used by this version's body.", - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'indexes' => array( - 'nid' => array('nid'), - 'uid' => array('uid'), - ), - 'primary key' => array('vid'), - ); - $schema['node_type'] = array( 'description' => 'Stores information about all defined {node} types.', 'fields' => array( @@ -326,7 +277,7 @@ 'default' => '', ), 'has_body' => array( - 'description' => 'Boolean indicating whether this type uses the {node_revisions}.body field.', + 'description' => 'Boolean indicating whether this type uses the {node}.body field.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, @@ -398,5 +349,40 @@ } /** + * Split off Node Revisions module from Node module. + */ +function node_update_7001() { + $ret = array(); + + // Migrate over information. We create a new node table because the update + // would be prohibitively slow. + $ret[] = update_sql('CREATE TABLE {node_temp} AS + SELECT n.nid, n.vid, n.type, n.language, n.title, r.body, r.teaser, r.format, n.uid, n.status, n.created, + n.changed, n.comment, n.promote, n.moderate, n.sticky, n.tnid, n.translate + FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid'); + + // Check to make sure that the node tables match, before proceeding. + $new_count = db_result(db_query('SELECT COUNT(1) FROM {node_temp}')); + $old_count = db_result(db_query('SELECT COUNT(1) FROM {node}')); + + if ($new_count == $old_count) { + db_rename_table($ret, 'node', 'node_old'); + db_rename_table($ret, 'node_temp', 'node'); + db_drop_table($ret, 'node_old'); + + if (db_table_exists('node_revisions')) { + // node table is self-sufficient now, so we can safely remove + // single revisions from the node_revisions table. + $ret[] = update_sql('CREATE TABLE {node_revisions_temp} AS SELECT * FROM {node_revisions} GROUP BY nid HAVING COUNT(nid) > 1'); + db_rename_table($ret, 'node_revisions', 'node_revisions_old'); + db_rename_table($ret, 'node_revisions_temp', 'node_revisions'); + db_drop_table($ret, 'node_revisions_old'); + } + } + + return $ret; +} + +/** * End of 6.x to 7.x updates */ Index: modules/search/search.module =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.module,v retrieving revision 1.275 diff -u -r1.275 search.module --- modules/search/search.module 15 Nov 2008 11:45:04 -0000 1.275 +++ modules/search/search.module 1 Dec 2008 05:56:01 -0000 @@ -508,7 +508,7 @@ $linknid = $match[1]; if ($linknid > 0) { // Note: ignore links to uncachable nodes to avoid redirect bugs. - $node = db_fetch_object(db_query('SELECT n.title, n.nid, n.vid, r.format FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid WHERE n.nid = %d', $linknid)); + $node = db_fetch_object(db_query('SELECT title, nid, vid, format FROM {node} WHERE nid = %d', $linknid)); if (filter_format_allowcache($node->format)) { $link = TRUE; $linktitle = $node->title; Index: modules/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.133 diff -u -r1.133 blogapi.module --- modules/blogapi/blogapi.module 11 Nov 2008 16:49:37 -0000 1.133 +++ modules/blogapi/blogapi.module 1 Dec 2008 05:55:58 -0000 @@ -201,7 +201,6 @@ $edit['name'] = $user->name; $edit['promote'] = in_array('promote', $node_type_default); $edit['comment'] = variable_get('comment_' . $edit['type'], 2); - $edit['revision'] = in_array('revision', $node_type_default); $edit['format'] = FILTER_FORMAT_DEFAULT; $edit['status'] = $publish; @@ -379,7 +378,7 @@ } if ($bodies) { - $result = db_query_range("SELECT n.nid, n.title, r.body, r.format, n.comment, n.created, u.name FROM {node} n, {node_revisions} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC", array( + $result = db_query_range("SELECT n.nid, n.title, n.body, n.format, n.comment, n.created, u.name FROM {node} n, {users} u WHERE n.uid = u.uid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC", array( ':type' => $blogid, ':uid' => $user->uid ), 0, $number_of_posts); Index: modules/node_revisions/node_revisions.pages.inc =================================================================== RCS file: modules/node_revisions/node_revisions.pages.inc diff -N modules/node_revisions/node_revisions.pages.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/node_revisions/node_revisions.pages.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,92 @@ + $node->title)), PASS_THROUGH); + + $header = array(t('Revision'), array('data' => t('Operations'), 'colspan' => 2)); + + $revisions = node_revisions_list($node); + + $rows = array(); + $revert_permission = FALSE; + if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) { + $revert_permission = TRUE; + } + $delete_permission = FALSE; + if ((user_access('delete revisions') || user_access('administer nodes')) && node_access('delete', $node)) { + $delete_permission = TRUE; + } + foreach ($revisions as $revision) { + $row = array(); + $operations = array(); + + if ($revision->current_vid > 0) { + $row[] = array('data' => t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"), '!username' => theme('username', $revision))) + . (($revision->log != '') ? '' . filter_xss($revision->log) . '
' : ''), + 'class' => 'revision-current'); + $operations[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => 2); + } + else { + $row[] = t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '!username' => theme('username', $revision))) + . (($revision->log != '') ? '' . filter_xss($revision->log) . '
' : ''); + if ($revert_permission) { + $operations[] = l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert"); + } + if ($delete_permission) { + $operations[] = l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete"); + } + } + $rows[] = array_merge($row, $operations); + } + + return theme('table', $header, $rows); +} + +/** + * Ask for confirmation of the reversion to prevent against CSRF attacks. + */ +function node_revisions_revert_confirm($form_state, $node_revision) { + $form['#node_revision'] = $node_revision; + return confirm_form($form, t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', '', t('Revert'), t('Cancel')); +} + +function node_revisions_revert_confirm_submit($form, &$form_state) { + $node_revision = $form['#node_revision']; + $node_revision->revision = 1; + $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($node_revision->revision_timestamp))); + if (module_exists('taxonomy')) { + $node_revision->taxonomy = array_keys($node_revision->taxonomy); + } + + node_save($node_revision); + + watchdog('content', '@type: reverted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->title, '%revision' => $node_revision->vid)); + drupal_set_message(t('@type %title has been reverted back to the revision from %revision-date.', array('@type' => node_get_types('name', $node_revision), '%title' => $node_revision->title, '%revision-date' => format_date($node_revision->revision_timestamp)))); + $form_state['redirect'] = 'node/' . $node_revision->nid . '/revisions'; +} + +function node_revisions_delete_confirm($form_state, $node_revision) { + $form['#node_revision'] = $node_revision; + return confirm_form($form, t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', t('This action cannot be undone.'), t('Delete'), t('Cancel')); +} + +function node_revisions_delete_confirm_submit($form, &$form_state) { + $node_revision = $form['#node_revision']; + db_query("DELETE FROM {node_revisions} WHERE nid = %d AND vid = %d", $node_revision->nid, $node_revision->vid); + node_invoke_nodeapi($node_revision, 'delete_revision'); + watchdog('content', '@type: deleted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->title, '%revision' => $node_revision->vid)); + drupal_set_message(t('Revision from %revision-date of @type %title has been deleted.', array('%revision-date' => format_date($node_revision->revision_timestamp), '@type' => node_get_types('name', $node_revision), '%title' => $node_revision->title))); + $form_state['redirect'] = 'node/' . $node_revision->nid; + if (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $node_revision->nid)) > 1) { + $form_state['redirect'] .= '/revisions'; + } +} Index: modules/node_revisions/node_revisions.install =================================================================== RCS file: modules/node_revisions/node_revisions.install diff -N modules/node_revisions/node_revisions.install --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/node_revisions/node_revisions.install 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,93 @@ + 'Stores information about each saved version of a {node}.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node} this version belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'vid' => array( + 'description' => 'The primary identifier for this version.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'uid' => array( + 'description' => 'The {users}.uid that created this version.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'title' => array( + 'description' => 'The title of this version.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'body' => array( + 'description' => 'The body of this version.', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + ), + 'teaser' => array( + 'description' => 'The teaser of this version.', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + ), + 'format' => array( + 'description' => "The input format used by this version's body.", + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'log' => array( + 'description' => 'The log entry explaining the changes in this version.', + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + ), + 'timestamp' => array( + 'description' => 'A Unix timestamp indicating when this version was created.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'nid' => array('nid'), + 'uid' => array('uid'), + ), + 'primary key' => array('vid'), + ); + + return $schema; +} + Index: modules/node_revisions/node_revisions.test =================================================================== RCS file: modules/node_revisions/node_revisions.test diff -N modules/node_revisions/node_revisions.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/node_revisions/node_revisions.test 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,87 @@ + t('Node revisions'), + 'description' => t('Create a node with revisions and test viewing, reverting, and deleting revisions.'), + 'group' => t('Node'), + ); + } + + function setUp() { + parent::setUp(); + + // Create and login user. + $web_user = $this->drupalCreateUser(array('view revisions', 'revert revisions', 'edit any page content', + 'delete revisions', 'delete any page content')); + $this->drupalLogin($web_user); + + // Create initial node. + $node = $this->drupalCreateNode(); + $settings = get_object_vars($node); + $settings['revision'] = 1; + + $nodes = array(); + $logs = array(); + + // Get original node. + $nodes[] = $node; + + // Create three revisions. + $revision_count = 3; + for ($i = 0; $i < $revision_count; $i++) { + $logs[] = $settings['log'] = $this->randomName(32); + + // Create revision with random title and body and update variables. + $this->drupalCreateNode($settings); + $node = node_load($node->nid); // Make sure we get revision information. + $settings = get_object_vars($node); + + $nodes[] = $node; + } + + $this->nodes = $nodes; + $this->logs = $logs; + } + + /** + * Check node revision related opperations. + */ + function testRevisions() { + $nodes = $this->nodes; + $logs = $this->logs; + + // Get last node for simple checks. + $node = $nodes[3]; + + // Confirm the correct revision text appears on "view revisions" page. + $this->drupalGet("node/$node->nid/revisions/$node->vid/view"); + $this->assertText($node->body, t('Correct text displays for version.')); + + // Confirm the correct log message appears on "revisions overview" page. + $this->drupalGet("node/$node->nid/revisions"); + foreach ($logs as $log) { + $this->assertText($log, t('Log message found.')); + } + + // Confirm that revisions revert properly. + $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/revert", array(), t('Revert')); + $this->assertRaw(t('@type %title has been reverted back to the revision from %revision-date.', + array('@type' => 'Page', '%title' => $nodes[1]->title, + '%revision-date' => format_date($nodes[1]->revision_timestamp))), t('Revision reverted.')); + $reverted_node = node_load($node->nid); + $this->assertTrue(($nodes[1]->body == $reverted_node->body), t('Node reverted correctly.')); + + // Confirm revisions delete properly. + $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/delete", array(), t('Delete')); + $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', + array('%revision-date' => format_date($nodes[1]->revision_timestamp), + '@type' => 'Page', '%title' => $nodes[1]->title)), t('Revision deleted.')); + $this->assertTrue(db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d and vid = %d', $node->nid, $nodes[1]->vid)) == 0, t('Revision not found.')); + } +} Index: modules/node_revisions/node_revisions.module =================================================================== RCS file: modules/node_revisions/node_revisions.module diff -N modules/node_revisions/node_revisions.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/node_revisions/node_revisions.module 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,419 @@ +' . t('The node_revisions module allows content on your site to be revisioned.') . ''; + $output .= '' . t('For more information, see the online handbook entry for Node_revisions module.', array('@node' => 'http://drupal.org/handbook/modules/node_revisions/')) . '
'; + return $output; + case 'node/%/revisions': + return '' . t('The revisions let you track differences between multiple versions of a post.') . '
'; + } +} + +/** + * Implementation of hook_theme(). + */ +function node_revisions_theme() { + return array( + 'node_revisions_log_message' => array( + 'arguments' => array('log' => NULL), + ), + ); +} + +/** + * Implementation of hook_perm(). + */ +function node_revisions_perm() { + $perms = array( + 'view revisions' => array( + 'title' => t('View revisions'), + 'description' => t('View content revisions.'), + ), + 'revert revisions' => array( + 'title' => t('Revert revisions'), + 'description' => t('Replace content with an older revision.'), + ), + 'delete revisions' => array( + 'title' => t('Delete revisions'), + 'description' => t('Delete content revisions.'), + ), + ); + + foreach (node_get_types() as $type) { + if ($type->base == 'node_content') { + $perms += node_list_permissions($type); + } + } + + return $perms; +} + +/** + * Implementation of hook_menu(). + */ +function node_revisions_menu() { + $items['node/%node_revisions/revisions'] = array( + 'title' => 'Revisions', + 'page callback' => 'node_revisions_overview', + 'page arguments' => array(1), + 'access callback' => '_node_revisions_access', + 'access arguments' => array(1), + 'weight' => 2, + 'type' => MENU_LOCAL_TASK, + ); + $items['node/%node_revisions/revisions/%/view'] = array( + 'title' => 'Revisions', + 'load arguments' => array(3), + 'page callback' => 'node_revisions_show', + 'page arguments' => array(1, TRUE), + 'access callback' => '_node_revisions_access', + 'access arguments' => array(1), + 'type' => MENU_CALLBACK, + ); + $items['node/%node_revisions/revisions/%/revert'] = array( + 'title' => 'Revert to earlier revision', + 'load arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('node_revisions_revert_confirm', 1), + 'access callback' => '_node_revisions_access', + 'access arguments' => array(1, 'update'), + 'type' => MENU_CALLBACK, + ); + $items['node/%node_revisions/revisions/%/delete'] = array( + 'title' => 'Delete earlier revision', + 'load arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('node_revisions_delete_confirm', 1), + 'access callback' => '_node_revisions_access', + 'access arguments' => array(1, 'delete'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Menu item access callback - determine if revisioning is accessible. + */ +function _node_revisions_access($node, $op = 'view') { + static $access = array(); + if (!isset($access[$node->vid])) { + $node_current_revision = node_load($node->nid); + $is_current_revision = $node_current_revision->vid == $node->vid; + // There should be at least two revisions. If the vid of the given node + // and the vid of the current revision differs, then we already have two + // different revisions so there is no need for a separate database check. + // Also, if you try to revert to or delete the current revision, that's + // not good. + if ($is_current_revision && (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $node->nid)) == 1 || $op == 'update' || $op == 'delete')) { + $access[$node->vid] = FALSE; + } + elseif (user_access('administer nodes')) { + $access[$node->vid] = TRUE; + } + else { + $map = array('view' => 'view revisions', 'update' => 'revert revisions', 'delete' => 'delete revisions'); + // First check the user permission, second check the access to the + // current revision and finally, if the node passed in is not the current + // revision then access to that, too. + $access[$node->vid] = isset($map[$op]) && user_access($map[$op]) && node_access($op, $node_current_revision) && ($is_current_revision || node_access($op, $node)); + } + } + return $access[$node->vid]; +} + + +/** + * Implementation of hook_nodeapi_prepare(). + */ +function node_revisions_nodeapi_load(&$node, $teaser, $page) { + // Always use the default revision setting. + $node->revision = in_array('revision', $node_options); +} + +/** + * Implementation of hook_nodeapi_load(). + */ +function node_revisions_nodeapi_load(&$node, $teaser, $page) { + // Nodes should always be preset with the default revision setting. + $node->revision = in_array('revision', variable_get('node_options_'. $node->type, array('status', 'promote'))); +} + +/** + * Implementation of hook_nodeapi_presave(). + */ +function node_revisions_nodeapi_presave(&$node, $teaser, $page) { + // Apply filters to some default node fields: + if (empty($node->nid)) { + // When inserting a node, $node->log must be set because + // {node_revisions}.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; + } + elseif (empty($node->log)) { + // When updating a node, avoid clobbering an existing log entry with an empty one. + unset($node->log); + } +} + +/** + * Implementation of hook_nodeapi_insert(). + */ +function node_revisions_nodeapi_insert(&$node, $teaser, $page) { + // Generate the node_revisions table query. + node_revisions_save($node, $user->uid); + db_query('UPDATE {node} SET vid = %d WHERE nid = %d', $node->vid, $node->nid); +} + +/** + * Implementation of hook_nodeapi_update(). + */ +function node_revisions_nodeapi_update(&$node, $teaser, $page) { + // Generate the node_revisions table query. + if (!empty($node->revision)) { + node_revisions_save($node, $user->uid); + } + else { + node_revisions_save($node, $user->uid, 'vid'); + db_query('UPDATE {node} SET vid = %d WHERE nid = %d', $node->vid, $node->nid); + } +} + +/** + * Implementation of hook_nodeapi_delete(). + */ +function node_revisions_nodeapi_delete(&$node, $teaser, $page) { + db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid); +} + +/** + * Implementation of hook_nodeapi_blogapi_new(). + */ +function node_revisions_nodeapi_blogapi_new(&$node, $teaser, $page) { + $node['revision'] = in_array('revision', variable_get('node_options_' . $node['type'], array('status', 'promote'))); +} + + + +/** + * Implementation of hook_user_delete(). + */ +function node_revisions_user_delete(&$edit, &$user) { + db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid); +} + +/** + * Load a node_revisions object from the database. + * + * @param $param + * Either the nid of the node or an array of conditions to match against in the database query + * @param $revision + * Which numbered revision to load. Defaults to the current version. + * @param $reset + * Whether to reset the internal node_revisions_load cache. + * + * @return + * A fully-populated node object. + */ +function node_revisions_load($param = array(), $revision = NULL, $reset = NULL) { + static $nodes = array(); + + if ($reset) { + $nodes = array(); + } + + $cachable = ($revision == NULL); + $arguments = array(); + if (is_numeric($param)) { + if ($cachable) { + // Is the node statically cached? + if (isset($nodes[$param])) { + return is_object($nodes[$param]) ? clone $nodes[$param] : $nodes[$param]; + } + } + $cond = 'n.nid = %d'; + $arguments[] = $param; + } + elseif (is_array($param)) { + // Turn the conditions into a query. + foreach ($param as $key => $value) { + $cond[] = 'n.' . db_escape_table($key) . " = '%s'"; + $arguments[] = $value; + } + $cond = implode(' AND ', $cond); + } + else { + return FALSE; + } + + // Retrieve a field list based on the site's schema. + $fields = drupal_schema_fields_sql('node', 'n'); + $fields = array_merge($fields, drupal_schema_fields_sql('node_revisions', 'r')); + $fields = array_merge($fields, array('u.name', 'u.picture', 'u.data')); + // Remove unneeded fields from the query: + // n.vid, n.title, n.body, n.teaser, n.format and r.nid are redundant. + $fields = array_diff($fields, array('n.vid', 'n.title', 'n.body', 'n.teaser', 'n.format', 'r.nid')); + $fields = implode(', ', $fields); + // Rename timestamp field for clarity. + $fields = str_replace('r.timestamp', 'r.timestamp AS revision_timestamp', $fields); + // Change name of revision uid so it doesn't conflict with n.uid. + $fields = str_replace('r.uid', 'r.uid AS revision_uid', $fields); + + // Retrieve the node. + // No db_rewrite_sql is applied so as to get complete indexing for search. + if ($revision) { + array_unshift($arguments, $revision); + $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.nid = n.nid AND r.vid = %d WHERE ' . $cond, $arguments)); + } + else { + $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.vid = n.vid WHERE ' . $cond, $arguments)); + } + + if ($node && $node->nid) { + // Call the node specific callback (if any) and piggy-back the + // results to the node or overwrite some values. + if ($extra = node_invoke($node, 'load')) { + foreach ($extra as $key => $value) { + $node->$key = $value; + } + } + + if ($extra = node_invoke_nodeapi($node, 'load')) { + foreach ($extra as $key => $value) { + $node->$key = $value; + } + } + if ($cachable) { + $nodes[$node->nid] = is_object($node) ? clone $node : $node; + } + } + + return $node; +} + +/** + * Save a revision with the uid of the current user. + * + * Node is taken by reference, becuse drupal_write_record() updates the + * $node with the revision id, and we need to pass that back to the caller. + */ +function node_revisions_save(&$node, $uid, $update = NULL) { + $temp_uid = $node->uid; + $node->uid = $uid; + if (isset($update)) { + drupal_write_record('node_revisions', $node, $update); + } + else { + drupal_write_record('node_revisions', $node); + } + $node->uid = $temp_uid; +} + +/** + * Generate a page displaying a single node revision. + * + * Optionally change the title to show revision information + * and pass through to node_view(). Don't show comments and + * don't update the history table as node_show() does. + */ +function node_revisions_show($node, $message = FALSE) { + if ($message) { + drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))), PASS_THROUGH); + } + return node_view($node, FALSE, TRUE); +} + +/** + * Return a list of all the existing revision numbers. + */ +function node_revisions_list($node) { + $revisions = array(); + $result = db_query('SELECT r.vid, r.title, r.log, r.uid, n.vid AS current_vid, r.timestamp, u.name FROM {node_revisions} r LEFT JOIN {node} n ON n.vid = r.vid INNER JOIN {users} u ON u.uid = r.uid WHERE r.nid = %d ORDER BY r.timestamp DESC', $node->nid); + while ($revision = db_fetch_object($result)) { + $revisions[$revision->vid] = $revision; + } + return $revisions; +} + +/** + * Theme a log message. + * + * @ingroup themeable + */ +function theme_node_revisions_log_message($log) { + return '