book_copy.module in Book Copy 7.2
Same filename and directory in other branches
Book copy, copy book outlines
This module allows for the copying of parts of entire book outlines in Drupal.
File
book_copy.moduleView 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
Name![]() |
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
Name![]() |
Description |
---|---|
BOOK_COPY_PROCESSING_LIMIT | @file Book copy, copy book outlines |