From 447a4a7bf23485f0b4c1c65a2bef9d53194ae15c Mon Sep 17 00:00:00 2001
From: Henrique Recidive <recidive@gmail.com>
Date: Sat, 17 Mar 2012 15:28:15 -0300
Subject: [PATCH] Issue #1357058: Making possible to schedule publishing first
 entity revision and to schedule entity unpublishing.

---
 ers.module                                |   79 ++++++++++------
 plugins/entity/ERSEntityDefault.class.php |   65 +++++++++++++
 plugins/entity/ERSEntityNode.class.php    |  145 +++++++++++++++++++++++-----
 3 files changed, 234 insertions(+), 55 deletions(-)

diff --git a/ers.module b/ers.module
index bfbc081..1205860 100644
--- a/ers.module
+++ b/ers.module
@@ -430,57 +430,52 @@ function ers_set_on_edit_path($status = TRUE) {
  *
  * For now, #tree must not be set to true.
  */
-function ers_entity_schedule_form($entity_type, $entity, $checkbox = TRUE) {
+function ers_entity_schedule_form($entity_type, $entity, $action = 'publish', $checkbox = TRUE) {
   list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
 
+  $action_string = $action == 'publish' ? t('publish') : t('unpublish');
+
   if (!user_access("publish $entity_type $bundle content")) {
     return array();
   }
 
-  $form['ers']['ers_publish'] = array(
+  $form['ers']['ers_' . $action] = array(
     '#type' => 'checkbox',
-    '#title' => t('Publish draft'),
+    '#title' => drupal_ucfirst(t('!action draft', array('!action' => $action_string))),
   );
 
   if (!$checkbox) {
-    $form['ers']['ers_publish']['#access'] = FALSE;
-    $form['ers']['ers_publish']['#value'] = TRUE;
+    $form['ers']['ers_' . $action]['#access'] = FALSE;
+    $form['ers']['ers_' . $action]['#value'] = TRUE;
   }
 
-  $form['ers']['ers_publish_schedule'] = array(
+  $form['ers']['ers_' . $action . '_schedule'] = array(
     '#type' => 'textfield',
-    '#title' => t('Publish on'),
+    '#title' => drupal_ucfirst(t('!action on', array('!action' => $action_string))),
     '#maxlength' => 25,
-    '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to publish immediately.', array('%time' => format_date(time(), 'custom', 'Y-m-d H:i:s O'), '%timezone' => format_date(time(), 'custom', 'O'))),
+    '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to !action immediately.', array('%time' => format_date(time(), 'custom', 'Y-m-d H:i:s O'), '%timezone' => format_date(time(), 'custom', 'O'), '!action' => $action_string)),
     '#states' => array(
       'visible' => array(
-        ':input[name="ers_publish"]' => array('checked' => TRUE),
+        ':input[name="ers_' . $action . '"]' => array('checked' => TRUE),
       ),
     ),
   );
 
-  if (empty($revision_id)) {
-    // If there is no revision id yet, the first revision MUST be the published
-    // So hide the scheduling widget.
-    $form['ers']['#access'] = FALSE;
-    return $form;
-  }
-
   if (module_exists('date_popup')) {
-    $form['ers']['ers_publish_schedule']['#type'] = 'date_popup';
-    $form['ers']['ers_publish_schedule']['#description'] = t('Leave blank to publish this draft immediately.');
-    unset($form['ers']['ers_publish_schedule']['#maxlength']);
+    $form['ers']['ers_' . $action . '_schedule']['#type'] = 'date_popup';
+    $form['ers']['ers_' . $action . '_schedule']['#description'] = t('Leave blank to !action this draft immediately.', array('!action' => $action_string));
+    unset($form['ers']['ers_' . $action . '_schedule']['#maxlength']);
   }
 
-  if (!empty($entity->ers_schedule[$revision_id])) {
+  if (!empty($revision_id) && !empty($entity->ers_schedule[$revision_id])) {
     $schedule = $entity->ers_schedule[$revision_id];
-    $form['ers']['ers_publish']['#default_value'] = TRUE;
+    $form['ers']['ers_' . $action]['#default_value'] = TRUE;
     if (module_exists('date_popup')) {
       $date = new DateObject($schedule->publish_date);
-      $form['ers']['ers_publish_schedule']['#default_value'] = $date->format(DATE_FORMAT_DATETIME);
+      $form['ers']['ers_' . $action . '_schedule']['#default_value'] = $date->format(DATE_FORMAT_DATETIME);
     }
     else {
-      $form['ers']['ers_publish_schedule']['#default_value'] = format_date($schedule->publish_date, 'custom', 'Y-m-d H:i:s O');
+      $form['ers']['ers_' . $action . '_schedule']['#default_value'] = format_date($schedule->publish_date, 'custom', 'Y-m-d H:i:s O');
     }
   }
 
@@ -498,21 +493,21 @@ function ers_entity_schedule_form($entity_type, $entity, $checkbox = TRUE) {
  * handle writing the schedule. If the entity will not be saved, the caller
  * is responsible for the drupal_write_record() necessary to do this.
  */
-function ers_entity_schedule_form_submit($form, &$form_state, $entity_type, $entity) {
+function ers_entity_schedule_form_submit($form, &$form_state, $entity_type, $entity, $action = 'publish') {
   list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
 
-  if (!user_access("publish $entity_type $bundle content")) {
+  if (!user_access("$action $entity_type $bundle content")) {
     return;
   }
 
-  if (empty($form_state['values']['ers_publish'])) {
+  if (empty($form_state['values']['ers_' . $action])) {
     if (!empty($entity->ers_schedule[$revision_id])) {
       $entity->ers_remove_schedule = TRUE;
     }
     return;
   }
 
-  $publish_date = drupal_array_get_nested_value($form_state['values'], $form['ers']['ers_publish_schedule']['#parents']);
+  $publish_date = drupal_array_get_nested_value($form_state['values'], $form['ers']['ers_' . $action . '_schedule']['#parents']);
 
   if (!$publish_date) {
     $publish_date = time();
@@ -545,11 +540,12 @@ function ers_entity_schedule_full_form($form, &$form_state, $entity_type, $entit
   $entity = $revisions[$entity_id];
   */
 
-  $form += ers_entity_schedule_form($entity_type, $entity, FALSE);
+  $form += ers_entity_schedule_form($entity_type, $entity, empty($new_revision_id) ? 'unpublish' : 'publish', FALSE);
   $form_state['entity'] = $entity;
   $form_state['entity_type'] = $entity_type;
   $form_state['new_revision_id'] = $new_revision_id;
-  return confirm_form($form, t('Publish revision'), entity_uri($entity_type, $entity), '', t('Schedule'), t('Cancel'));
+  $title = empty($new_revision_id) ? t('Unpublish') : t('Publish');
+  return confirm_form($form, $title, entity_uri($entity_type, $entity), '', t('Schedule'), t('Cancel'));
 }
 
 /**
@@ -572,6 +568,31 @@ function ers_entity_schedule_full_form_submit($form, &$form_state) {
 }
 
 /**
+ * A confirm form for unpublishing a revision.
+ */
+function ers_entity_schedule_unpublish_form($form, &$form_state, $entity_type, $entity, $new_revision_id) {
+  // Reload the proper revision.
+  list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
+
+  $form_state['entity'] = $entity;
+  $form_state['entity_type'] = $entity_type;
+  $form_state['entity_id'] = $entity_id;
+  return confirm_form($form, t('Unpublish revision'), entity_uri($entity_type, $entity), '', t('Unpublish'), t('Cancel'));
+}
+
+/**
+ * Submit and upublish the revision.
+ */
+function ers_entity_schedule_unpublish_form_submit($form, &$form_state) {
+  $entity_type = $form_state['entity_type'];
+  $entity = $form_state['entity'];
+  $entity_id = $form_state['entity_id'];
+
+  $handler = ers_entity_plugin_get_handler($entity_type);
+  $handler->unpublish($entity_id, $entity);
+}
+
+/**
  * Callback used for setting the proper title on entity edit tabs.
  *
  * This ensures that the entity bundle is under control; if it is it makes
diff --git a/plugins/entity/ERSEntityDefault.class.php b/plugins/entity/ERSEntityDefault.class.php
index 87ed2eb..070b9a5 100644
--- a/plugins/entity/ERSEntityDefault.class.php
+++ b/plugins/entity/ERSEntityDefault.class.php
@@ -59,6 +59,11 @@ interface ERSEntityInterface {
   public function set_published_revision_current($entity_id, $entity);
 
   /**
+   * Unpublish entities that support publishing flag.
+   */
+  public function unpublish($entity_id, $entity);
+
+  /**
    * Get a full schedule for the given entity.
    */
   public function get_schedule($entity);
@@ -103,6 +108,11 @@ class ERSEntityDefault implements ERSEntityInterface {
   public $entity_type = '';
 
   /**
+   * Whether the entity supports published/unpublished state.
+   */
+  public $supports_publishing_flag = FALSE;
+
+  /**
    * Initialize the plugin and store the plugin info.
    */
   function init($plugin) {
@@ -172,6 +182,19 @@ class ERSEntityDefault implements ERSEntityInterface {
         'load arguments' => array(),
       );
 
+      // Add items for unpublish.
+      if ($this->supports_publishing_flag) {
+        $items[$this->plugin['revision path'] . '/%/unpublish'] = array(
+          'title' => 'Publish revision',
+          'page callback' => 'ers_entity_plugin_switcher_page',
+          'page arguments' => array($this->entity_type, 'unpublish', $position, $count + 2),
+          'access callback' => 'ers_entity_plugin_access_switcher',
+          'access arguments' => array($this->entity_type, 'unpublish', $position, $count + 2),
+          'type' => MENU_CALLBACK,
+          'load arguments' => array(),
+        );
+      }
+
     }
   }
 
@@ -369,6 +392,21 @@ class ERSEntityDefault implements ERSEntityInterface {
         ))
         ->execute();
     }
+
+    // Do we have to schedule publishing this revision?
+    if (!empty($entity->ers_new_schedule)) {
+      $this->update_entity_schedule($entity, $entity->ers_new_schedule, $revision_id);
+    }
+
+    // Store state information.
+    $this->update_entity_state($entity);
+
+    // When publishing a revision immediately after inserting the entity
+    // set_published_revision_current() doesn't get called, so we need to call
+    // it here to make sure entity types do what they need to do in this method.
+    if (!empty($entity->published_revision_id)) {
+      $this->set_published_revision_current($entity_id, $entity);
+    }
   }
 
   public function hook_entity_update($entity) {
@@ -522,6 +560,11 @@ class ERSEntityDefault implements ERSEntityInterface {
     // THIS MUST BE IMPLEMENTED PER ENTITY TYPE
   }
 
+  // General API
+  public function unpublish($entity_id, $entity) {
+    // THIS MUST BE IMPLEMENTED PER ENTITY TYPE
+  }
+
   /**
    * Get a full schedule for the given entity.
    */
@@ -614,6 +657,14 @@ class ERSEntityDefault implements ERSEntityInterface {
   }
 
   /**
+   * Determine if the current user has access to publish the entity.
+   *
+   */
+  public function access_unpublish($entity, $new_revision_id) {
+    return $this->access_publish($entity, $new_revision_id);
+  }
+
+  /**
    * Provide a call back page to set which revision is the draft revision.
    *
    * This produces no output; it changes the revision and then performs
@@ -647,4 +698,18 @@ class ERSEntityDefault implements ERSEntityInterface {
 
     return drupal_get_form('ers_entity_schedule_full_form', $this->entity_type, $entity, $new_revision_id);
   }
+
+  /**
+   * Provide a call back page to unpublish a revision for the entity types that
+   * support the publishing flag.
+   *
+   * This produces no output; it changes the published flag and then performs
+   * a goto. It uses a token to protect from CSRF attacks.
+   */
+  public function page_unpublish($js, $input, $entity) {
+    list($entity_id, $revision_id, $bundle) = entity_extract_ids($this->entity_type, $entity);
+
+    return drupal_get_form('ers_entity_schedule_full_form', $this->entity_type, $entity, NULL);
+  }
+
 }
diff --git a/plugins/entity/ERSEntityNode.class.php b/plugins/entity/ERSEntityNode.class.php
index 048fb36..e84f833 100644
--- a/plugins/entity/ERSEntityNode.class.php
+++ b/plugins/entity/ERSEntityNode.class.php
@@ -10,6 +10,9 @@
  * Handles node specific functionality for ERS.
  */
 class ERSEntityNode extends ERSEntityDefault {
+
+  public $supports_publishing_flag = TRUE;
+
   public function hook_menu_alter(&$items) {
     // Provide a nicer title to remind the user that this will edit drafts.
     $items['node/%node/edit']['title'] = 'Edit';
@@ -21,8 +24,8 @@ class ERSEntityNode extends ERSEntityDefault {
       'title' => 'Revisions',
       'page callback' => 'ers_entity_plugin_switcher_page',
       'page arguments' => array('node', 'revisions', 1),
-      'access callback' => '_node_revision_access',
-      'access arguments' => array(1),
+      'access callback' => 'ers_entity_plugin_access_switcher',
+      'access arguments' => array('node', 'revisions', 1),
       'weight' => 2,
       'type' => MENU_LOCAL_TASK,
     );
@@ -42,6 +45,10 @@ class ERSEntityNode extends ERSEntityDefault {
     // which we absolutely do no want in this context. So we store that
     // UID and restore it after the node_save.
     $uid = $published_revision->revision_uid;
+
+    // If node is unpublished, publish it.
+    $published_revision->status = !empty($entity->published_revision_id);
+
     node_save($published_revision);
     if ($uid != $GLOBALS['user']->uid) {
       db_update('node_revision')
@@ -60,6 +67,11 @@ class ERSEntityNode extends ERSEntityDefault {
     $this->ers_revision_reset = FALSE;
   }
 
+  public function unpublish($entity_id, $entity) {
+    $entity->status = NODE_NOT_PUBLISHED;
+    node_save($entity);
+  }
+
   /**
    * Implements a delegated hook_form_alter.
    */
@@ -70,25 +82,71 @@ class ERSEntityNode extends ERSEntityDefault {
         return;
       }
 
-      if (empty($node->nid)) {
-        return;
-      }
+      // Make sure to include this file when form is cached.
+      $form_state['build_info']['files'][] = $this->plugin['path'] . '/' . $this->plugin['handler'] . '.class.php';
+
+      // Set "published" checkbox to value, so it get hidden. And set it's
+      // value to FALSE as default.
+      $form['options']['status']['#type'] = 'value';
+      $form['options']['status']['#value'] = FALSE;
 
-      // Reset the revision field.
-      if ($node->published_revision_id == $node->draft_revision_id) {
-        $form['revision_information']['revision']['#default_value'] = TRUE;
-        $form['revision_information']['revision']['#description'] = t('Creating a new revision will create a draft revision. This revision will not be published until it is published from the revisions tab. Creating a new revision will create a new draft revision only.');
-        $form['revision_information']['revision']['#disabled'] = TRUE;
+      if (!empty($node->nid)) {
+        // Reset the revision field.
+        if ($node->published_revision_id == $node->draft_revision_id) {
+          $form['revision_information']['revision']['#default_value'] = TRUE;
+          $form['revision_information']['revision']['#description'] = t('Creating a new revision will create a draft revision. This revision will not be published until it is published from the revisions tab. Creating a new revision will create a new draft revision only.');
+          $form['revision_information']['revision']['#disabled'] = TRUE;
+        }
+        else {
+          $form['revision_information']['revision']['#default_value'] = FALSE;
+          $form['revision_information']['revision']['#description'] = t('You are currently editing the draft revision. This draft will not be published until it is published from the revisions tab.');
+        }
+        $publish_default = variable_get('ers_publish_draft_' . $node->type, TRUE);
       }
       else {
-        $form['revision_information']['revision']['#default_value'] = FALSE;
-        $form['revision_information']['revision']['#description'] = t('You are currently editing the draft revision. This draft will not be published until it is published from the revisions tab.');
+      	$publish_default = variable_get('ers_publish_new_' . $node->type, TRUE);
       }
 
-      // Provide a scheduling widget to schedule this draft.
-      $form['revision_information'] += ers_entity_schedule_form('node', $node);
+      $form['ers'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Scheduling'),
+        '#access' => ((user_access("publish node $node->type content") || user_access('administer nodes'))),
+        '#collapsible' => TRUE,
+        '#collapsed' => FALSE,
+        '#group' => 'additional_settings',
+      );
+      $form['ers'] += ers_entity_schedule_form('node', $node);
+      $form['ers']['ers']['ers_publish']['#default_value'] = $publish_default;
+
+      // @todo: add setting for this
+      $form['ers']['unpublish'] = ers_entity_schedule_form('node', $node, 'unpublish');
+
       $form['#submit'][] = 'ers_node_form_submit';
     }
+    elseif ($form_id == 'node_type_form') {
+      $type = $form['#node_type'];
+      $form['ers'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Scheduling options'),
+        '#collapsible' => TRUE,
+        '#collapsed' => TRUE,
+        '#group' => 'additional_settings',
+      );
+      $form['ers']['ers_publish_new'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Publish'),
+        '#description' => t('Set new nodes to be published by default.'),
+        '#default_value' => variable_get('ers_publish_new_' . $type->type, TRUE),
+      );
+      $form['ers']['ers_publish_draft'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Publish draft'),
+        '#description' => t('Set new drafts to be published by default.'),
+        '#default_value' => variable_get('ers_publish_draft_' . $type->type, TRUE),
+      );
+      // Remove default 'Published' checkbox.
+      unset($form['workflow']['node_options']['#options']['status']);
+    }
   }
 
   /**
@@ -117,21 +175,24 @@ class ERSEntityNode extends ERSEntityDefault {
     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) {
+      $operations = array(
+        'data' => array(),
+      );
+      if ($revision->current_vid > 0 && !empty($node->status)) {
+        $operations['class'] = array('revision-current');
         $row[] = array('data' => t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'short'), "node/$node->nid"), '!username' => theme('username', array('account' => $revision))))
                                  . (($revision->log != '') ? '<p class="revision-log">' . filter_xss($revision->log) . '</p>' : ''),
                        'class' => array('revision-current'));
-        $operations = array('data' => drupal_placeholder(t('published revision')), 'class' => array('revision-current'));
+        $operations['data'][] = drupal_placeholder(t('published revision'));
+        if ($publish_permission) {
+          $operations['data'][] = l(t('unpublish'), "node/$node->nid/revisions/$revision->vid/unpublish", array('query' => drupal_get_destination()));
+        }
         $schedule = array('data' => '', 'class' => 'revision-current');
       }
       else {
-        $operations = array(
-          'data' => ''
-        );
         $row[] = t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'short'), "node/$node->nid/revisions/$revision->vid/view"), '!username' => theme('username', array('account' => $revision))))
                  . (($revision->log != '') ? '<p class="revision-log">' . filter_xss($revision->log) . '</p>' : '');
         if ($publish_permission) {
@@ -164,10 +225,7 @@ class ERSEntityNode extends ERSEntityDefault {
       }
       $row[] = $schedule;
 
-      if (is_array($operations['data'])) {
-        $operations['data'] = implode(' &nbsp; ', $operations['data']);
-      }
-
+      $operations['data'] = implode(' &nbsp; ', $operations['data']);
       $row[] = $operations;
       $rows[] = $row;
     }
@@ -182,6 +240,41 @@ class ERSEntityNode extends ERSEntityDefault {
   }
 
   /**
+   * Determine if the current user has access to publish the entity.
+   *
+   * This is called indirectly via ers_entity_plugin_access_switcher which
+   * is a menu access callback.
+   *
+   * Access callback for "Revisions tab". This is a stripped down version of
+   * _node_revision_access() to handle only 'view' operation and to allow it to
+   * show up when there's only one revision.
+   */
+  public function access_revisions($node) {
+    $access = &drupal_static(__FUNCTION__, array());
+
+    if (!isset($access[$node->vid])) {
+      if ((!user_access('view revisions') && !user_access('administer nodes'))) {
+        $access[$node->vid] = FALSE;
+        return FALSE;
+      }
+
+      $node_current_revision = node_load($node->nid);
+      $is_current_revision = $node_current_revision->vid == $node->vid;
+
+      if (user_access('administer nodes')) {
+        $access[$node->vid] = TRUE;
+      }
+      else {
+        // First 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] = node_access('view', $node_current_revision) && ($is_current_revision || node_access('view', $node));
+      }
+    }
+
+    return $access[$node->vid];
+  }
+
+  /**
    * Implements a delegated hook_panels_pane_content_alter()
    *
    * This exists primarily to add some extra contextual items to give more
@@ -401,5 +494,5 @@ class ERSEntityNode extends ERSEntityDefault {
 }
 
 function ers_node_form_submit($form, &$form_state) {
-  ers_entity_schedule_form_submit($form['revision_information'], $form_state, 'node', $form_state['node']);
+  ers_entity_schedule_form_submit($form['ers'], $form_state, 'node', $form_state['node']);
 }
-- 
1.7.9.1

