drafty.module in Drafty 7
Hook implementations and API functions for the Drafty module.
File
drafty.moduleView source
<?php
/**
* @file
* Hook implementations and API functions for the Drafty module.
*/
/**
* Implements hook_menu().
*/
function drafty_menu() {
$items = array();
$items['admin/config/system/drafty'] = array(
'title' => 'Drafty',
'description' => 'Manage Drafty settings.',
'type' => MENU_NORMAL_ITEM,
'page callback' => 'drupal_get_form',
'page arguments' => array(
'drafty_form',
),
'access arguments' => array(
'administer drafty',
),
'file' => 'includes/drafty.admin.inc',
);
return $items;
}
/**
* Implements hook_permission().
*/
function drafty_permission() {
return array(
'administer drafty' => array(
'title' => t('Administer Drafty'),
),
);
}
/**
* Implements hook_module_implements_alter().
*/
function drafty_module_implements_alter(&$implementations) {
if (isset($implementations['drafty'])) {
$group = $implementations['drafty'];
unset($implementations['drafty']);
$implementations['drafty'] = $group;
}
}
/**
* Implements hook_entity_presave().
*/
function drafty_entity_presave($entity, $type) {
$recursion_level =& drupal_static('drafty_recursion_level', 0);
if (!$recursion_level && !drafty_entity_is_new($entity, $type) && !empty($entity->is_draft_revision)) {
// Since this is a draft revision, after saving we want the current,
// published revision to remain in place in the base entity table and
// field_data_*() tables. Set the revision to publish once the draft entity
// has been written to the database.
list($id) = entity_extract_ids($type, $entity);
$vid = drafty()
->getPublishedRevisionId($type, $id);
drafty()
->setRevisionToBePublished($type, $id, $vid);
}
$recursion_level++;
}
/**
* Implements hook_entity_update().
*/
function drafty_entity_update($entity, $type) {
$recursion_level =& drupal_static('drafty_recursion_level', 0);
if ($recursion_level == 1) {
// Doing this in hook_entity_update() so that the entire process is
// completed within entity saving. However this results in two entity saves
// within entity insert. The other option is hook_exit(), which is not
// better since for example that would happen outside the transaction.
drafty()
->restorePublishedRevisions();
}
$recursion_level--;
}
/**
* Implements hook_entity_insert().
*/
function drafty_entity_insert($entity, $type) {
$recursion_level =& drupal_static('drafty_recursion_level', 0);
$recursion_level--;
}
/**
* Implements hook_field_attach_load().
*/
function drafty_field_attach_load($entity_type, $entities, $age, $options) {
// Entity API provides the function entity_revision_is_default() to determine
// whether an entity was loaded with the default revision or not. However this
// is not sufficient for two reasons.
// - It relies on entity_load() for core entities, which makes it unsafe to
// call within hook_entity_load() implementations. This can be useful when
// allowing drafts to be previewed in context such as listings.
// - The entity API implementation only tells you whether the entity was
// loaded with that revision or not, but not whether it was requested
// with the ID or with the revision explicitly specified.
// Note that hook_field_attach_load() is the only hook in core where it is
// possible to determine whether calling code requested a revision or not,
// this information is not available to hook_entity_load(). Also note that
// hook_field_attach_load() is cached when entities are loaded only be ID, but
// since revision loads don't use the field cache it works fine for our
// purposes.
foreach ($entities as $entity) {
$entity->_drafty_revision_requested = $age;
}
}
/**
* Callback function to delete a single temporary-draft revision.
*
* Could be called either from a batch job or from a cron-queue.
*/
function drafty_queue_delete_revision($drafty_queue_item, &$context = NULL) {
try {
// Trigger hook_drafty_predelete_revision(). Allow other modules to
// preserve information on the old revision before it's deleted.
module_invoke_all('drafty_predelete_revision', $drafty_queue_item['entity_type'], $drafty_queue_item['entity_id'], $drafty_queue_item['revision_id'], $drafty_queue_item['replaced_by']);
// Delete the old revision.
entity_revision_delete($drafty_queue_item['entity_type'], $drafty_queue_item['revision_id']);
// Document the successful batch operation for the batch-finished function
// to use.
if (!empty($context) && isset($context['results'])) {
$context['results'][] = $drafty_queue_item;
}
} catch (Exception $ex) {
// Log the problem.
$message = 'An error occurred while deleting temporary revision: @args. Exception details: @error';
$args = array(
'@args' => print_r($drafty_queue_item, TRUE),
'@error' => print_r($ex, TRUE),
);
watchdog('drafty', $message, $args, WATCHDOG_ERROR);
throw $ex;
// Batch API will pass $success = FALSE.
}
}
/**
* Implements hook_cron_queue_info().
*/
function drafty_cron_queue_info() {
$queues['drafty_revision_delete'] = array(
'worker callback' => 'drafty_queue_delete_revision',
'time' => 60,
);
return $queues;
}
/**
* Batch completion callback for when draft-deletes happen in a batch job.
* $results contains info about successfully deleted revisions, and
* exception objects for revision-delete options that caused problems.
* $operations contains the operations that remained unprocessed.
*/
function drafty_delete_batch_finished($success, $results, $operations) {
// TODO: Display a message about temporary-revision-cleanup here?
// Note that logging of failed deletes is handled by the operation callback.
}
/**
* Factory function for the DraftyTracker class.
*/
function drafty() {
$tracker =& drupal_static(__FUNCTION__);
if (!isset($tracker)) {
$tracker = new Drafty();
}
return $tracker;
}
/**
* Helper function to determine if an entity is new or not.
*
* The $is_new property cannot be relied on, as not all entities use this. E.g.
* files.
*
* @param mixed $entity
* The entity to check.
* @param string $entity_type
* The entity type being passed in.
*
* @return bool
*/
function drafty_entity_is_new($entity, $entity_type) {
// Use is_new if it's available, since it's not impossible for new entities to
// have an ID set.
if (!empty($entity->is_new)) {
return TRUE;
}
list($id) = entity_extract_ids($entity_type, $entity);
return empty($id);
}
/**
* Handles tracking, selecting and publishing revisions.
*/
class Drafty {
/**
* A list of entity types, ids and version IDs to be published.
*/
protected $revisionsToPublish = array();
public function wasRevisionRequested($entity) {
return isset($entity->_drafty_revision_requested) && $entity->_drafty_revision_requested === FIELD_LOAD_REVISION;
}
public function isDraftRevision($entity) {
return !empty($entity->is_draft_revision);
}
/**
* Get the current published revision for an entity.
*
* @param $type
* The entity type.
* @param $id
* The entity ID.
*
* @return A version ID.
*/
public function getPublishedRevisionId($type, $id) {
$info = entity_get_info();
// Get the version ID of the published revision directly from the database.
// It is not possible to rely on $entity->original here since that does not
// guarantee being the published revision. Also avoid loading the entity
// because we may be in the process of saving it.
$query = db_select($info[$type]['base table'], 'b');
$query
->addField('b', $info[$type]['entity keys']['revision']);
$query
->condition($info[$type]['entity keys']['id'], $id);
$vid = $query
->execute()
->fetchField();
return $vid;
}
/**
* Add a revision to be published to the tracker.
*
* @param $type
* The entity type.
* @param $id
* The entity ID.
* @param $vid
* The entity version ID.
*
* @return $this
*/
public function setRevisionToBePublished($type, $id, $vid) {
// Only one revision can be published during a request, so just overwrite
// and for now last one wins.
$this->revisionsToPublish[$type][$id] = $vid;
return $this;
}
/**
* Publish a revision.
*
* @param $type
* The entity type.
* @param $vid
* The entity version ID.
*
* @return The newly published revision.
*/
function publishRevision($type, $id, $vid) {
// Title module assumes that the current content language is used when
// saving an entity. This is OK for the new draft revision, but it does not
// work when publishing a revision. Therefore, ensure that
// title_active_language() reflects the original language of the entity.
// Without this, title may overwrite {$title}_field in the original language
// with the contents of the legacy field.
// @todo: this might not be necessary after one or both of these patches
// lands:
// https://www.drupal.org/node/2267251
// https://www.drupal.org/node/2098097
if (module_exists('title')) {
$entity = entity_load_single($type, $id);
$langcode = entity_language($type, $entity);
title_active_language($langcode);
}
$revision = entity_revision_load($type, $vid);
// Publishing a revision sometimes happens within hook_entity_update(). When
// we do that, set $entity->original to the entity we're in the process of
// saving. i.e. the draft we're in the process of creating and need to
// replace with the published version again.
$revision->is_draft_revision = FALSE;
return $this
->saveRevisionAsNew($type, $revision);
}
/**
* Save a revision as new.
*
* @param $type
* The entity type.
* @param $revision
* An entity object.
*
* @return The newly saved revision.
*/
public function saveRevisionAsNew($type, $revision) {
list($id) = entity_extract_ids($type, $revision);
entity_get_controller($type)
->resetCache();
$original = entity_load_single($type, $id);
$revision->original = $original;
// @todo: entity API function?
$revision->revision = TRUE;
$revision->is_new_revision = TRUE;
$revision->default_revision = TRUE;
entity_save($type, $revision);
return $revision;
}
/**
* Publish revisions previously set with setRevisionToBePublished().
*/
public function restorePublishedRevisions() {
$delete_old_revisions = variable_get('drafty_delete_old_revisions', FALSE);
$delete_with_cron = variable_get('drafty_delete_with_cron', TRUE);
$queue = NULL;
$operations = array();
foreach ($this->revisionsToPublish as $type => $value) {
foreach ($value as $id => $vid) {
unset($this->revisionsToPublish[$type][$id]);
$published_revision = $this
->publishRevision($type, $id, $vid);
// Now that the revision is deleted, there are two identical copies of
// the revision in the system. The original 'draft' revision and the
// newly saved published revision.
if ($delete_old_revisions) {
list(, $replaced_by) = entity_extract_ids($type, $published_revision);
// Deletion must be done on a different request thread, or any other
// hooks called for the revision-to-be-deleted will fail.
$drafty_queue_item = array(
'entity_type' => $type,
'entity_id' => $id,
'revision_id' => $vid,
'replaced_by' => $replaced_by,
);
if ($delete_with_cron) {
if (empty($queue)) {
$queue = DrupalQueue::get('drafty_revision_delete');
}
$queue
->createItem($drafty_queue_item);
}
else {
$operations[] = array(
'drafty_queue_delete_revision',
array(
$drafty_queue_item,
),
);
}
}
// @todo: when restoring a published revision, should the revision
// timestamp be set to the old value?
}
}
if (!empty($operations)) {
$batch = array(
'operations' => $operations,
'finished' => 'drafty_delete_batch_finished',
);
batch_set($batch);
}
}
}
Functions
Name | Description |
---|---|
drafty | Factory function for the DraftyTracker class. |
drafty_cron_queue_info | Implements hook_cron_queue_info(). |
drafty_delete_batch_finished | Batch completion callback for when draft-deletes happen in a batch job. $results contains info about successfully deleted revisions, and exception objects for revision-delete options that caused problems. $operations contains the operations that… |
drafty_entity_insert | Implements hook_entity_insert(). |
drafty_entity_is_new | Helper function to determine if an entity is new or not. |
drafty_entity_presave | Implements hook_entity_presave(). |
drafty_entity_update | Implements hook_entity_update(). |
drafty_field_attach_load | Implements hook_field_attach_load(). |
drafty_menu | Implements hook_menu(). |
drafty_module_implements_alter | Implements hook_module_implements_alter(). |
drafty_permission | Implements hook_permission(). |
drafty_queue_delete_revision | Callback function to delete a single temporary-draft revision. |