current_vid) of ANY node
* OR single revision of UNPUBLISHED node
* Current, published:
* - revision with (vid == current_vid) of PUBLISHED node
* Archived:
* - all other revisions, i.e.
* revision with (vid < current_vid) of ANY node
* OR revision with (vid == current_vid) of UNPUBLISHED node
*
* Note: these will change when Revisioning is going to store revision states
* independently from vid number (e.g. in different table).
*/
/**
* Return a single or all possible revision state names.
*
* @param $state
* optional state id, as defined in revisioning_api.inc
* @return
* if $state is provided, state name. Otherwise, state names array keyed by state id.
*/
function revisioning_revision_states($state = NULL) {
static $states;
$states = array(
REVISION_ARCHIVED => t('Archived'),
REVISION_CURRENT => t('Current, published'),
REVISION_PENDING => t('Pending'),
);
return $state === NULL ? $states : $states[$state];
}
/**
* Return TRUE when either of the following is true:
* o the supplied node has at least one revision more recent than the current
* o the node is not yet published and consists of a single revision
*
* Relies on vid, current_revision_id and num_revisions set on the node object,
* see function node_tools_nodeapi()
*
* @param $node
* @return TRUE, if node is pending according to the above definition
*/
function _revisioning_node_is_pending($node) {
return ($node->vid > $node->current_revision_id) || (!$node->status && $node->num_revisions == 1);
}
/**
* Implementation of hook_revisionapi().
*
* Act on various revision events.
*
* @param $op
* Operation
* @param $node
* Node of current operation (loaded with vid of the operation).
*
* "Pre" operations can be useful to get values before they are lost or changed,
* for example, to save a backup of revision before it's deleted.
* Also, for "pre" operations vetoing mechanics could be implemented, so it
* would be possible to veto an operation via hook_revisionapi(). For example,
* when the hook is returning FALSE, operation will be vetoed.
*
* @TODO: Add more operations if needed.
*/
function revisioning_revisionapi($op, $node) {
switch ($op) {
case 'pre revert':
// Invoke corresponding Rules event
if (module_exists('rules')) {
rules_invoke_event('revisioning_pre_revert', $node);
}
break;
case 'post revert':
// Invoke the revisioning trigger passing 'revert' as the operation
if (module_exists('trigger')) {
module_invoke_all('revisioning', 'revert', $node, $node->vid);
}
// Invoke corresponding Rules event
if (module_exists('rules')) {
rules_invoke_event('revisioning_post_revert', $node);
}
break;
case 'pre publish':
// Invoke corresponding Rules event
if (module_exists('rules')) {
rules_invoke_event('revisioning_pre_publish', $node);
}
break;
case 'post publish':
// Invoke the revisioning trigger passing 'publish' as the operation
if (module_exists('trigger')) {
module_invoke_all('revisioning', 'publish', $node);
}
// Invoke corresponding Rules event
if (module_exists('rules')) {
rules_invoke_event('revisioning_post_publish', $node);
}
break;
//case 'pre unpublish':
// Not implemented: do we really need it ?
case 'post unpublish':
// Invoke the revisioning trigger passing 'unpublish' as the operation
if (module_exists('trigger')) {
module_invoke_all('revisioning', 'unpublish', $node);
}
// Invoke corresponding Rules event
if (module_exists('rules')) {
rules_invoke_event('revisioning_post_unpublish', $node);
}
break;
case 'pre delete':
// Invoke corresponding Rules event
if (module_exists('rules')) {
rules_invoke_event('revisioning_pre_delete', $node);
}
break;
case 'post delete':
break;
}
}
/**
* Get the id of the latest revision belonging to a node.
* @param
* $nid, id of the node
* @return
* ID of the latest revision.
*/
function revisioning_get_latest_revision_id($nid) {
return db_result(db_query('SELECT MAX(vid) FROM {node_revisions} WHERE nid=%d', $nid));
}
/**
* Get the id of the user who last edited the supplied node, ie. the author
* of the latest revision.
* This is irrespective of whether this latest revision is pending or not,
* unless TRUE is specified for the second argument, in which case the uid
* of the creator of the current revision (published or not) is returned.
*
* @param $nid
* The id of the node whose most recent editor id is to be returned.
* @param $current
* Whether the uid of the current or very latest revision should be returned.
* @return
* A single number being the user id (uid).
*/
function revisioning_get_last_editor($nid, $current = FALSE) {
$sql = ($current)
? "SELECT vid FROM {node} WHERE nid = %d"
: "SELECT MAX(vid) FROM {node_revisions} WHERE nid = %d";
$vid = db_result(db_query($sql, $nid));
return db_result(db_query("SELECT uid FROM {node_revisions} WHERE vid = %d", $vid));
}
/**
* Revert node to selected revision without changing its publication status.
*
* @param $node
* Target $node object (loaded with target revision) or nid of target node
* @param $vid
* Optional vid of revision to revert to, if provided $node must not be an object.
*/
function _revisioning_revertpublish_revision(&$node, $vid = NULL) {
$node_revision = is_object($node) ? $node : node_load($node, $vid);
$return = module_invoke_all('revisionapi', 'pre revert', $node_revision);
if (in_array(FALSE, $return)) {
drupal_goto('node/'. $node_revision->nid .'/revisions/'. $node_revision->vid .'/view');
die;
}
_revisioning_revert_revision($node_revision);
module_invoke_all('revisionapi', 'post revert', $node_revision);
}
/**
* Revert node to selected revision without publishing it.
*
* This is same as node_revision_revert_confirm_submit() in node_pages.inc,
* except it doesn't put any messages on screen.
*
* @param $node
* Target $node object (loaded with target revision) or nid of target node
* @param $vid
* optional vid of revision to revert to, if provided $node is not an object.
*/
function _revisioning_revert_revision(&$node, $vid = NULL) {
$node_revision = is_object($node) ? $node : node_load($node, $vid);
$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));
}
/**
* Publish node, without calling node_save().
* @obsolete
* This function is no longer used. Use _revisioning_publish_revision().
*
* @param $node
* Target $node object or nid of target node
* @param $clear_cache
* Whether to clear the cache afterwards or not. Clearing the cache on every
* node during bulk operations can be time-consuming.
*
function _revisioning_publish_node($node, $clear_cache = TRUE) {
if (is_numeric($node)) {
$node = node_load($node);
}
db_query("UPDATE {node} SET status=1 WHERE nid=%d", $node->nid);
// Let other modules know there was an update on the node, just like
// node_save() does.
$node->status = 1;
node_invoke_nodeapi($node, 'update');
// Update the node access table for this node.
node_access_acquire_grants($node);
if ($clear_cache) {
cache_clear_all();
}
}
*/
/**
* Unpublish node, without calling node_save().
*
* @param $node
* Target $node object or nid.
* @param $clear_cache
* Whether to clear the cache afterwards or not. Clearing the cache on every
* node during bulk operations can be time-consuming.
*/
function _revisioning_unpublish_node($node, $clear_cache = TRUE) {
if (is_numeric($node)) {
$node = node_load($node);
}
db_query("UPDATE {node} SET status=0 WHERE nid=%d", $node->nid);
// Let other modules know there was an update on the node, just like
// node_save() does.
$node->status = 0;
$node->bypass_nodeapi = TRUE; // avoid revisioning_nodeapi() doing stuff
$node->pathauto_perform_alias = FALSE; // avoid pathauto_nodeapi() doing stuff
node_invoke_nodeapi($node, 'update');
// Update the node access table for this node.
node_access_acquire_grants($node);
if ($clear_cache) {
cache_clear_all();
}
}
/**
* Delete selected revision of node, provided it's not current.
*
* This is same as node_revision_delete_confirm_submit() in node_pages.inc,
* except it doesn't put any messages on the screen. This way it becomes
* reusable (eg. in actions).
* Since we are calling nodeapi as in node_revision_delete_confirm_submit(), we
* invoke our "post delete" revisionapi hook in nodeapi. This way revisionapi
* hooks work the same way both with "delete revision" submit handler and when
* this function is called, and we don't invoke revisionapi "post delete" hook
* twice.
*
* @param $node
* Target $node object (loaded with target revision) or nid of target node
* @param $vid
* optional vid of revision to delete, if provided $node is not object.
*
* @TODO: Insert check to prevent deletion of current revision of node.
*/
function _revisioning_delete_revision(&$node, $vid = NULL) {
$node_revision = is_object($node) ? $node : node_load($node, $vid);
module_invoke_all('revisionapi', 'pre delete', $node_revision);
db_query("DELETE FROM {node_revisions} WHERE nid = %d AND vid = %d", $node_revision->nid, $node_revision->vid);
db_query("DELETE FROM {term_node} 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));
}
/**
* Unpublish revision (i.e. the node).
*
* Note that no check is made as to whether the initiating user has permission
* to unpublish this node.
*
* @param $node
* Target $node object or nid of target node
*/
function _revisioning_unpublish_revision(&$node) {
$node_revision = is_object($node) ? $node : node_load($node);
module_invoke_all('revisionapi', 'pre unpublish', $node_revision);
_revisioning_unpublish_node($node_revision);
watchdog('content', 'Unpublished @type %title', array('@type' => $node_revision->type, '%title' => $node_revision->title), WATCHDOG_NOTICE, l(t('view'), "node/$node_revision->nid"));
module_invoke_all('revisionapi', 'post unpublish', $node_revision);
}
/**
* Make the supplied revision of the node current and publish it.
* It is the caller's responsibility to provide proper revision.
* Note that no check is made as to whether the initiating user has permission
* to publish this revision.
*
* @param $node
* Target $node object (loaded with target revision) or nid of target node
* @param $vid
* optional vid of revision to make current, if provided $node is not object.
* @param $clear_cache
* Whether to clear the cache afterwards or not. Clearing the cache on every
* node during bulk operations can be time-consuming.
*/
function _revisioning_publish_revision(&$node, $vid = NULL, $clear_cache = TRUE) {
$node_revision = is_object($node) ? $node : node_load($node, $vid);
$return = module_invoke_all('revisionapi', 'pre publish', $node_revision);
if (in_array(FALSE, $return)) {
drupal_goto('node/'. $node_revision->nid .'/revisions/'. $node_revision->vid .'/view');
die;
}
// Update node table, making sure the "published" (ie. status) flag is set
db_query("UPDATE {node} SET vid=%d, title='%s', status=1 WHERE nid=%d", $node_revision->vid, $node_revision->title, $node_revision->nid);
if ($clear_cache) {
cache_clear_all();
}
$node_revision->status = 1;
$node_revision->bypass_nodeapi = TRUE; // avoid revisioning_nodeapi() doing stuff
$node_revision->nodewords = FALSE; // avoid nodewords_nodeapi() doing stuff
// On the first save of a node we should allow pathauto_nodeapi() to create the
// alias as normal. Otherwise, the alias will not be created if a node is set
// to be immediately published during creation. See [#1635542].
if (empty($node->is_new)) {
$node_revision->pathauto_perform_alias = FALSE; // avoid pathauto_nodeapi() doing stuff
}
node_invoke_nodeapi($node_revision, 'update');
// Update the node access table for this node.
node_access_acquire_grants($node_revision);
watchdog('content', 'Published rev #%revision of @type %title', array('@type' => $node_revision->type, '%title' => $node_revision->title, '%revision' => $node_revision->vid), WATCHDOG_NOTICE, l(t('view'), "node/$node_revision->nid/revisions/$node_revision->vid/view"));
module_invoke_all('revisionapi', 'post publish', $node_revision);
}
/**
* Find the most recent pending revision, make it current, unless it already is
* and publish node.
* Note that no check is made as to whether the initiating user has permission
* to publish this node.
*
* @param $node
* The node object whose latest pending revision is to be published
* @return
* TRUE if operation was successful, FALSE if there is no pending revision to
* publish
*/
function _revisioning_publish_latest_revision(&$node) {
// Get latest pending revision or take the current provided it's UNpublished
$latest_pending = array_shift(_revisioning_get_pending_revisions($node->nid));
if (!$latest_pending) {
if (!$node->status && $node->is_current) {
_revisioning_publish_revision($node);
return TRUE;
}
}
else {
_revisioning_publish_revision($node->nid, $latest_pending->vid);
return TRUE;
}
return FALSE;
}
/**
* Return a count of the number of revisions newer than the supplied vid.
*
* @param $vid
* The reference vid.
* @param $nid
* The id of the node.
* @return
* integer
*/
function _revisioning_get_number_of_revisions_newer_than($vid, $nid) {
return db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {node_revisions} r ON n.nid=r.nid WHERE (r.vid>%d AND n.nid=%d)", $vid, $nid));
}
/**
* Return a count of the number of revisions newer than the current revision.
*
* @param $nid
* The id of the node.
* @return
* integer
*/
function _revisioning_get_number_of_pending_revisions($nid) {
return db_result(db_query("SELECT COUNT(*) FROM {node} n INNER JOIN {node_revisions} r ON n.nid=r.nid WHERE (r.vid>n.vid AND n.nid=%d)", $nid));
}
/**
* Get the number of archived revisions belonging to a node.
* @param
* $nid, id of the node
* @return
* A count representing the number of archived revisions for the node
* Returns zero if there is only one (i.e. the current) revision.
*/
function revisioning_get_number_of_archived_revisions($node) {
return db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d AND vid < %d', $node->nid, $node->current_revision_id));
}
/**
* Delete all revisions with a vid less than the current.
*/
function revisioning_delete_archived_revisions($node) {
db_query('DELETE FROM {term_node} WHERE nid = %d AND vid < %d', $node->nid, $node->current_revision_id);
return db_query('DELETE FROM {node_revisions} WHERE nid = %d AND vid < %d', $node->nid, $node->current_revision_id);
}
/**
* Retrieve a list of revisions with a vid greater than the current.
*
* @param $nid
* The node id to retrieve.
* @return
* An array of revisions (latest first), each containing vid, title and
* content type.
*/
function _revisioning_get_pending_revisions($nid) {
$sql = "SELECT r.vid, r.title, n.type FROM {node} n INNER JOIN {node_revisions} r ON n.nid=r.nid WHERE (r.vid>n.vid AND n.nid=%d) ORDER BY r.vid DESC";
$result = db_query($sql, $nid);
$revisions = array();
while ($revision = db_fetch_object($result)) {
$revisions[$revision->vid] = $revision;
}
return $revisions;
}
/**
* Retrieve a list of all revisions (archive, current, pending) belonging to
* the supplied node.
*
* @param $nid
* The node id to retrieve.
* @param $include_taxonomy_terms
* Whether to also retrieve the taxonomy terms for each revision
* @return
* An array of revision objects, each with published flag, log message, vid,
* title, timestamp and name of user that created the revision
*/
function _revisioning_get_all_revisions_for_node($nid, $include_taxonomy_terms = FALSE) {
$sql_select = 'SELECT n.type, n.status, r.vid, r.title, r.log, r.uid, r.timestamp, u.name';
$sql_from = ' FROM {node_revisions} r LEFT JOIN {node} n ON n.vid=r.vid INNER JOIN {users} u ON u.uid=r.uid';
$sql_where = ' WHERE r.nid=%d ORDER BY r.vid DESC';
if ($include_taxonomy_terms) {
$sql_select .= ', td.name AS term';
$sql_from .= ' LEFT JOIN {term_node} tn ON r.vid=tn.vid LEFT JOIN {term_data} td ON tn.tid=td.tid';
$sql_where .= ', term ASC';
}
$sql = $sql_select . $sql_from . $sql_where;
$result = db_query($sql, $nid);
$revisions = array();
while ($revision = db_fetch_object($result)) {
if (empty($revisions[$revision->vid])) {
$revisions[$revision->vid] = $revision;
}
elseif ($include_taxonomy_terms) {
// If a revision has more than one taxonomy term, these will be returned
// by the query as seperate objects differing only in their term fields.
$existing_revision = $revisions[$revision->vid];
$existing_revision->term .= '/'. $revision->term;
}
}
return $revisions;
}
/**
* Return revision type of the supplied node.
*
* @param &$node
* Node object to check
* @return
* Revision type
*/
function _revisioning_revision_is(&$node) {
if ($node->is_pending) {
return REVISION_PENDING;
}
return ($node->is_current && $node->status) ? REVISION_CURRENT : REVISION_ARCHIVED;
}
/**
* Return a string with details about the node that is about to be displayed.
*
* Called from revisioning_nodeapi().
*
* @param $node
* The node that is about to be viewed
* @return
* A translatable message containing details about the node
*/
function _revisioning_node_info_msg($node) {
// Get username for the revision, not the creator of the node
$revision_author = user_load($node->revision_uid);
$placeholder_data = array(
'@content_type' => $node->type,
'%title' => $node->title,
'!author' => theme('username', $revision_author),
'@date' => format_date($node->revision_timestamp, 'small'),
);
$revision_type = _revisioning_revision_is($node);
switch ($revision_type) {
case REVISION_PENDING:
return t('Displaying pending revision of @content_type %title, last modified by !author on @date', $placeholder_data);
case REVISION_CURRENT:
return t('Displaying current, published revision of @content_type %title, last modified by !author on @date', $placeholder_data);
case REVISION_ARCHIVED:
return t('Displaying archived revision of @content_type %title, last modified by !author on @date', $placeholder_data);
}
}
/**
* Return TRUE only if the user account has ALL of the supplied permissions.
*
* @param $permissions
* An array of permissions (strings)
* @param $account
* The user account object. Defaults to the current user if omitted.
* @return bool
*/
function revisioning_user_all_access($permissions, $account = NULL) {
foreach ($permissions as $permission) {
if (!user_access($permission, $account)) {
return FALSE;
}
}
return TRUE;
}
/**
* Return an array of names of content types that are subject to moderation.
*
* @return array of strings, may be empty
*/
function revisioning_moderated_content_types() {
$moderated_content_types = array();
foreach (node_get_types() as $type) {
$content_type = check_plain($type->type);
if (node_tools_content_is_moderated($content_type)) {
$moderated_content_types[] = $content_type;
}
}
return $moderated_content_types;
}
/**
* Return the id of the user who created the revision by the supplied vid.
*/
function revisioning_get_revision_uid($vid) {
return db_result(db_query('SELECT uid FROM {node_revisions} WHERE vid = %d', $vid));
}