You are here

book_copy.module in Book Copy 7.2

Same filename and directory in other branches
  1. 6 book_copy.module
  2. 7 book_copy.module

Book copy, copy book outlines

This module allows for the copying of parts of entire book outlines in Drupal.

File

book_copy.module
View source
<?php

/**
 * @file
 * Book copy, copy book outlines
 *
 * This module allows for the copying of parts of entire book outlines in Drupal.
 */
define('BOOK_COPY_PROCESSING_LIMIT', 5);

/**
 * Implements hook_permission().
 */
function book_copy_permission() {
  return array(
    'copy books' => array(
      'title' => t('Copy Books'),
      'description' => t('Give user the ability to use the book copy functionality.'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function book_copy_menu() {
  $items['admin/content/book/copy/%node'] = array(
    'title' => 'Copy book',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'book_copy_copy_confirm',
      4,
    ),
    'access callback' => 'book_copy_node_copy_access',
    'access arguments' => array(
      4,
    ),
  );
  $items['node/%node/outline/copy'] = array(
    'title' => 'Copy',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'book_copy_copy_confirm',
      1,
    ),
    'access callback' => 'book_copy_node_copy_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 100,
  );
  return $items;
}

/**
 * Implements hook_module_implements_alter().
 */
function book_copy_module_implements_alter(&$implementations, $hook) {

  // remove the book_delete hook_menu_alter as it conflicts
  // both modules compete for book_delete_book_admin_overview
  // so we will provide built in support for book_delete
  // book_copy is moved to the end of the list for greater compatability
  if ($hook == 'menu_alter' && isset($implementations['book_delete'])) {
    unset($implementations['book_delete']);
    unset($implementations['book_copy']);
    $implementations['book_copy'] = FALSE;
  }
}

/**
 * Implements hook_menu_alter().
 */
function book_copy_menu_alter(&$items) {

  // replicate the outline tab so we can have sub-tabs
  $items['node/%node/outline/outline'] = $items['node/%node/outline'];
  $items['node/%node/outline/outline']['type'] = MENU_DEFAULT_LOCAL_TASK;
}

/**
 * Implements hook_admin_paths().
 */
function book_copy_admin_paths() {
  if (variable_get('node_admin_theme')) {
    $paths = array(
      'node/*/outline/copy' => TRUE,
    );
    return $paths;
  }
}

/**
 * Access check for ability to see Outline sub-tab on a node
 */
function book_copy_node_copy_access($node) {

  // test for not being in a book, no one can access this then
  if (!isset($node->book)) {
    return FALSE;
  }

  // test for root regardless of location
  if ($node->book['plid'] == 0 || arg(0) == 'admin') {

    // need ability to create new books, and add to books, and update this item, and copy books
    return user_access('create new books') && user_access('add content to books') && user_access('copy books');
  }

  // test for outline path when not root
  if (arg(0) == 'node') {

    // need ability to add to book, and update this item, and copy books
    return _book_outline_access($node) && user_access('add content to books') && user_access('copy books');
  }

  // this access routine only authorizes these two use-cases at the moment
  return FALSE;
}

/**
 * Menu callback.  Ask for confirmation of book copy.
 */
function book_copy_copy_confirm($form, &$form_state, $node) {

  // make sure we have a valid book or book item
  if (isset($node->book['bid'])) {
    $copytree = _book_copy_list($node->book);
    $form['nid'] = array(
      '#type' => 'value',
      '#value' => $node->nid,
    );
    $form['new_title'] = array(
      '#type' => 'textfield',
      '#default_value' => $node->title,
      '#description' => t('The new title for this book, this only applies to the top level book item being copied'),
      '#title' => t('New Title'),
    );
    $form['book_title'] = array(
      '#type' => 'value',
      '#value' => $node->title,
    );
    return confirm_form($form, t('Are you sure you want to copy the outline below "%title"? (this is %count nodes)', array(
      '%title' => $node->title,
      '%count' => count($copytree),
    )), 'admin/content/book', t('This action cannot be undone.'), t('Copy'), t('Cancel'));
  }
  return FALSE;
}

/**
 * Submit handler to start a batch process
 */
function book_copy_copy_confirm_submit($form, &$form_state) {
  if ($form_state['values']['confirm']) {
    $path = arg(0) . '/' . arg(1) . '/' . arg(2);
    book_copy_process_copy($form_state['values']['nid'], $form_state['values']['book_title'], $form_state['values']['new_title'], TRUE, $path);
  }
}

/**
 * Execute a batch process.
 */
function book_copy_process_copy($nid, $title, $new_title, $progressive = TRUE, $redirect = NULL) {
  $queue_id = rand(1, 9999);
  $queue = DrupalQueue::get($queue_id);
  $queue
    ->createQueue();
  $batch = array(
    'title' => t('Copying book %title', array(
      '%title' => $title,
    )),
    'operations' => array(
      array(
        'book_copy_copy',
        array(
          $nid,
          $new_title,
          $queue_id,
        ),
      ),
      array(
        'book_copy_update_internal_links',
        array(
          $queue_id,
        ),
      ),
    ),
    'finished' => '_book_copy_copy_finished',
    'init_message' => t('Outline Copy is starting.'),
    'progress_message' => t('We are now copying your outline please be patient...'),
    'error_message' => t('The process has encountered an error.'),
  );
  batch_set($batch);

  // there is a core glitch in D6/7 that requires progressive be defined this way
  // http://drupal.org/node/638712#comment-2289138
  $batch =& batch_get();

  // set if this should be done in the background or not, if FALSE, redirect is ignored
  $batch['progressive'] = $progressive;
  batch_process($redirect);
}

/**
 * Callback for replicating a book outline below a passed node.
 */
function book_copy_copy($initial_nid, $new_title, $queue_id, &$context) {

  // batch job is starting
  if (!isset($context['sandbox']['progress'])) {
    $node = node_load($initial_nid);
    $copy_list = _book_copy_list($node->book);
    $context['sandbox']['max'] = count($copy_list);
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['list_left'] = array_keys($copy_list);
    $context['sandbox']['map'] = array();
    $context['sandbox']['root'] = NULL;
    $context['sandbox']['copy_list'] = $copy_list;
  }
  $limit = BOOK_COPY_PROCESSING_LIMIT;
  $count = 0;
  $list_left = $context['sandbox']['list_left'];

  // Replicate limit nodes at a time.  This needs to be set fairly low for stability
  foreach ($list_left as $nid) {
    $count++;
    array_shift($context['sandbox']['list_left']);
    $entity = node_load($nid);

    // clone the item so we can modify it prior to save
    $clone = replicate_clone_entity('node', $entity);

    // reset the owner to the author committing this action
    if (!drupal_is_cli()) {
      $clone->uid = $GLOBALS['user']->uid;
    }

    // test for initial item with title update
    if ($nid == $initial_nid) {
      $clone->title = $new_title;
    }

    // test for root
    if ($clone->book['plid'] == 0) {
      unset($clone->book['nid']);
      $clone->book['mlid'] = NULL;

      // this will trigger a new book to be created
      $clone->book['bid'] = 'new';
    }
    else {

      // unset this menu id
      $clone->book['mlid'] = NULL;
      if (is_object($context['sandbox']['root'])) {
        $clone->book['bid'] = $context['sandbox']['root']->book['bid'];
        $clone->book['menu_name'] = $context['sandbox']['root']->book['menu_name'];
      }

      // perform the mapping look up if   we have it
      $parent = menu_link_load($clone->book['plid']);
      $pnid = str_replace('node/', '', $parent['link_path']);
      if (isset($context['sandbox']['map'][$pnid])) {
        $clone->book['plid'] = $context['sandbox']['map'][$pnid];
      }
    }

    // save the cloned item
    node_save($clone);

    // create a map of the odl nid and the new nid and save
    $old_node_nid = $entity->nid;
    $new_node_nid = $clone->nid;
    $nid_map = array(
      $old_node_nid => $new_node_nid,
    );

    // Add the old nid and the new nid to the Drupal Queue
    $queue = DrupalQueue::get($queue_id);
    $queue
      ->createItem($nid_map);
    $context['sandbox']['map'][$nid] = $clone->book['mlid'];

    // capture new root nid for the case that we have a whole new book
    if ($clone->book['plid'] == 0) {
      $context['sandbox']['root'] = $clone;
    }
    $context['sandbox']['progress']++;

    // exit early if we have reached execution limit
    if ($count == $limit) {
      break;
    }
  }

  // max should equal progress but just to be safe, process til out of items
  if (empty($context['sandbox']['list_left'])) {

    // Update our progress information.
    $context['sandbox']['progress'] = $context['sandbox']['max'];
  }

  // Multistep processing : report progress.
  $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}

/**
 * Update the internal links in body fields of new nodes that have just been cloned.
 *
 */
function book_copy_update_internal_links($queue_id, &$context) {
  $nid_map = array();

  // We are using the batch api for this.
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;

    // load the nid_map queue
    $queue = DrupalQueue::get($queue_id);

    // assemble the queue into the nid_map array
    while ($queue
      ->numberOfItems() > 0) {

      // load the item from the queue
      $item = $queue
        ->claimItem();

      // remove the item from the queue
      $queue
        ->deleteItem($item);

      // add the item to the nid_map array
      foreach ($item->data as $old_nid => $new_nid) {
        $context['sandbox']['nid_map'][$old_nid] = $new_nid;
      }
    }

    // Set the max, and the unprocessed pointer as well as create a pattern array and a replacement array.
    $context['sandbox']['max'] = count($context['sandbox']['nid_map']);
    $context['sandbox']['unprocessed'] = $context['sandbox']['nid_map'];
    $context['sandbox']['patterns'] = array();
    $context['sandbox']['replacements'] = array();

    // Build the patterns and replacements from the nid map.
    foreach ($context['sandbox']['nid_map'] as $old_nid => $new_nid) {

      // This pattern finds the exact old node nid followed by a ".
      // This way it will never be anything longer than the actual node id, for instance 300 when it should be 30
      $context['sandbox']['patterns'][] = '/node\\/' . $old_nid . '(?=\\")/';
      $context['sandbox']['replacements'][] = 'node/' . $new_nid;
    }
  }

  // Set a limit on how many nodes will process in each batch as well as initialize the counter.
  $limit = BOOK_COPY_PROCESSING_LIMIT;
  $count = 0;

  // loop over the newly created nids and attempt to update the internal links
  foreach ($context['sandbox']['unprocessed'] as $key => $new_nid) {
    $count++;

    // Start with a clean slate.
    $updated_node_body = NULL;

    // load the newly created node
    $new_node = node_load($new_nid);

    // see if we have a body field
    $node_body = isset($new_node->body['und'][0]['value']) ? $new_node->body['und'][0]['value'] : NULL;

    // if there is a body, attempt to replace old_nids with new_nids
    if ($node_body) {
      $updated_node_body = preg_replace($context['sandbox']['patterns'], $context['sandbox']['replacements'], $node_body);
    }

    // save the node with the updated nids
    if ($updated_node_body) {
      $new_node->body['und'][0]['value'] = $updated_node_body;
      node_save($new_node);
    }

    // Repeat the process on the next batch
    unset($context['sandbox']['unprocessed'][$key]);
    $context['sandbox']['progress']++;

    // exit early if we have reached execution limit
    if ($count == $limit) {
      break;
    }
  }

  // Are we there yet... if not keep going if so call finished.
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Book delete batch 'finished' callback.
 */
function _book_copy_copy_finished($success, $results, $operations) {
  if ($success) {
    drupal_set_message(t('The outline has been copied successfully!'));
  }
  else {
    drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
    $message = format_plural(count($results), '1 book item successfully copied.', '@count book items successfully copied.');
    $message .= theme('item_list', $results);
    drupal_set_message($message);
  }
}

/**
 * When passed a book link, assemble an array of nids => mlids
 * based on _book_toc_recurse and book_toc
 */
function _book_copy_list($book_link) {

  // assemble the full book based on bid
  $tree = menu_tree_all_data(book_menu_name($book_link['bid']));
  $copytree = array();

  // start off recursive operation, not copying anything by default
  _book_copy_list_recurse($tree, $book_link, $copytree, FALSE);
  return $copytree;
}

/**
 * Recursive function to build an ordered list of menu items for comparison
 */
function _book_copy_list_recurse($tree, $book_link, &$copytree, $copy_down) {

  // loop through the tree
  $just_set = FALSE;
  foreach ($tree as $data) {

    // once we have the book_link in question, start copying
    if ($data['link']['mlid'] == $book_link['mlid']) {

      // only copy items once we match the mlid in question
      $copy_down = TRUE;
      $just_set = TRUE;
    }

    // only store below the item we are targetting
    if ($copy_down) {

      // find the nid in link_path
      $nid = str_replace('node/', '', $data['link']['link_path']);

      // load the parent item
      if ($data['link']['plid'] != 0) {
        $parent = menu_link_load($data['link']['plid']);
        $pnid = str_replace('node/', '', $parent['link_path']);
      }
      else {
        $pnid = 0;
      }

      // save relationship of current node to parent id
      $copytree[$nid] = $pnid;
    }

    // if this level has data below it, decend into it
    if ($data['below']) {
      _book_copy_list_recurse($data['below'], $book_link, $copytree, $copy_down);
    }

    // if copy down was just set, make sure this branch escapes early
    if ($just_set) {
      break;
    }
  }
}

/**
 * Implements hook_outline_designer_form_overlay().
 */
function book_copy_outline_designer_form_overlay() {
  $book_copy['od_book_copy_title'] = array(
    '#title' => t('Title format'),
    '#id' => 'od_book_copy_title',
    '#type' => 'textfield',
    '#required' => TRUE,
    '#size' => 20,
    '#description' => t('This title only applies to the first item you are duplicating.'),
    '#weight' => 2,
  );
  $output = '<div id="od_book_copy" class="od_uiscreen">
    ' . drupal_render($book_copy) . '
  </div>';
  return $output;
}

/**
 * Implements hook_outline_designer_operations_alter().
 */
function book_copy_outline_designer_operations_alter(&$ops, $type) {

  // seems silly but this way other hooked in actions are last
  if (user_access('copy books')) {
    switch ($type) {
      case 'book':
        $icon_path = drupal_get_path('module', 'book_copy') . '/images/';
        $book_copy_ops = array(
          'book_copy' => array(
            'title' => t('Duplicate'),
            'icon' => $icon_path . 'book_copy.png',
            'callback' => 'book_copy_book_process_book_copy',
          ),
        );
        $ops = array_merge($ops, $book_copy_ops);
        break;
    }
  }
}

/**
 * Callback for book_copy ajax call from outline designer.
 */
function book_copy_book_process_book_copy($nid, $new_title) {

  // need to account for the 3 weird characters in URLs
  $new_title = str_replace("@2@F@", '/', $new_title);
  $new_title = str_replace("@2@3@", '#', $new_title);
  $new_title = str_replace("@2@B@", '+', $new_title);
  $new_title = str_replace("@2@6@", '&', $new_title);
  $node = node_load($nid);

  // verify that they can create / view the first item, administer outlines, add to books and copy books
  // this is probably a bit overkill but outline designer is for those with lots and lots of permissions
  if (node_access('create', $node) && _book_outline_access($node) && user_access('add content to books') && user_access('copy books')) {

    // run the bulk job in the background
    book_copy_process_copy($nid, '', $new_title, FALSE);
    return 1;
  }
  else {
    drupal_set_message(t('Outline copy denied because of permissions (need admin outline, create / view on the node, add to books and ability to copy).'));
    return 0;
  }
}

/**
 * Implements hook_outline_designer_ops_js().
 */
function book_copy_outline_designer_ops_js($ajax_path, $nid = NULL) {
  drupal_add_js(drupal_get_path('module', 'book_copy') . '/js/book_copy_ops.js', array(
    'scope' => 'footer',
  ));
}

Functions

Namesort descending Description
book_copy_admin_paths Implements hook_admin_paths().
book_copy_book_process_book_copy Callback for book_copy ajax call from outline designer.
book_copy_copy Callback for replicating a book outline below a passed node.
book_copy_copy_confirm Menu callback. Ask for confirmation of book copy.
book_copy_copy_confirm_submit Submit handler to start a batch process
book_copy_menu Implements hook_menu().
book_copy_menu_alter Implements hook_menu_alter().
book_copy_module_implements_alter Implements hook_module_implements_alter().
book_copy_node_copy_access Access check for ability to see Outline sub-tab on a node
book_copy_outline_designer_form_overlay Implements hook_outline_designer_form_overlay().
book_copy_outline_designer_operations_alter Implements hook_outline_designer_operations_alter().
book_copy_outline_designer_ops_js Implements hook_outline_designer_ops_js().
book_copy_permission Implements hook_permission().
book_copy_process_copy Execute a batch process.
book_copy_update_internal_links Update the internal links in body fields of new nodes that have just been cloned.
_book_copy_copy_finished Book delete batch 'finished' callback.
_book_copy_list When passed a book link, assemble an array of nids => mlids based on _book_toc_recurse and book_toc
_book_copy_list_recurse Recursive function to build an ordered list of menu items for comparison

Constants

Namesort descending Description
BOOK_COPY_PROCESSING_LIMIT @file Book copy, copy book outlines