clone.module 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <?php
  2. /**
  3. * @file
  4. * Allow users to make a copy of an item of content (a node) and then edit that copy.
  5. */
  6. /**
  7. * Implementation of hook_help().
  8. */
  9. function clone_help($path, $arg) {
  10. switch ($path) {
  11. case 'admin/help#clone':
  12. $output = '<p>' . t('The clone module allows users to make a copy of an existing node and then edit that copy. The authorship is set to the current user, the menu and url aliases are reset, and the words "Clone of" are inserted into the title to remind you that you are not editing the original node.') . '</p>';
  13. $output .= '<p>' . t('Users with the "clone node" permission can utilize this functionality. A new tab will appear on node pages with the word "Clone".') . '</p>';
  14. return $output;
  15. case 'node/%/clone':
  16. $method = variable_get('clone_method', 'prepopulate');
  17. if ($method == 'prepopulate') {
  18. return t('This clone will not be saved to the database until you submit.');
  19. }
  20. }
  21. }
  22. /**
  23. * Implementation of hook_permission().
  24. */
  25. function clone_permission() {
  26. return array(
  27. 'clone node' => array('title' => t('Clone any node')),
  28. 'clone own nodes' => array('title' => t('Clone own nodes.')),
  29. );
  30. }
  31. /**
  32. * Implementation of hook_menu().
  33. */
  34. function clone_menu() {
  35. $items['admin/config/content/clone'] = array(
  36. 'access arguments' => array('administer site configuration'),
  37. 'page callback' => 'drupal_get_form',
  38. 'page arguments' => array('clone_settings'),
  39. 'title' => 'Node clone module',
  40. 'file' => 'clone.pages.inc',
  41. 'description' => 'Allows users to clone (copy then edit) an existing node.',
  42. );
  43. $items['node/%node/clone/%clone_token'] = array(
  44. 'access callback' => 'clone_access_cloning',
  45. 'access arguments' => array(1, TRUE, 3),
  46. 'page callback' => 'clone_node_check',
  47. 'page arguments' => array(1),
  48. 'title' => 'Clone content',
  49. 'title callback' => 'clone_action_link_title',
  50. 'title arguments' => array(1),
  51. 'weight' => 5,
  52. 'file' => 'clone.pages.inc',
  53. 'type' => MENU_LOCAL_ACTION,
  54. 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  55. );
  56. return $items;
  57. }
  58. /**
  59. * Implements a to_arg function for the clone local action.
  60. *
  61. * @param string $arg
  62. * The current argument value.
  63. * @param array $map
  64. * The path arguments.
  65. *
  66. * @return string
  67. * Either 'confirm' or a CSRF token.
  68. */
  69. function clone_token_to_arg($arg, $map) {
  70. // Supply CSRF token if needed.
  71. return clone_get_token($map[1]);
  72. }
  73. /**
  74. * Get the token that needs to be added to the clone link.
  75. *
  76. * @param int $nid
  77. * The node ID of the node to be cloned
  78. *
  79. * @return string
  80. * Either 'confirm' or a CSRF token.
  81. */
  82. function clone_get_token($nid) {
  83. if (variable_get('clone_nodes_without_confirm', FALSE) && variable_get('clone_method', 'prepopulate') != 'prepopulate') {
  84. return drupal_get_token('clone_access_cloning-' . $nid);
  85. }
  86. return 'confirm';
  87. }
  88. function clone_access_cloning($node, $check_token = FALSE, $token = FALSE) {
  89. global $user;
  90. // Check CSRF token if needed.
  91. if ($check_token) {
  92. if (!$token || $token !== clone_get_token($node->nid)) {
  93. return FALSE;
  94. }
  95. }
  96. // Check basic permissions first.
  97. $access = clone_is_permitted($node->type) && (user_access('clone node') || ($user->uid && ($node->uid == $user->uid) && user_access('clone own nodes')));
  98. // Make sure the user can view the original node content, and create a new one..
  99. $access = $access && node_access('view', $node) && node_access('create', $node->type);
  100. // Let other modules alter this.
  101. drupal_alter("clone_access", $access, $node);
  102. return $access;
  103. }
  104. function clone_is_permitted($type) {
  105. $omitted = variable_get('clone_omitted', array());
  106. return empty($omitted[$type]);
  107. }
  108. /**
  109. * Menu title callback.
  110. */
  111. function clone_action_link_title($node) {
  112. // A hack to present a shorter title in contextual links.
  113. if (current_path() != 'node/' . $node->nid) {
  114. return t('Clone');
  115. }
  116. if (variable_get('clone_use_node_type_name', 0)) {
  117. return t('Clone this !type', array('!type' => drupal_strtolower(node_type_get_name($node))));
  118. }
  119. return t('Clone content');
  120. }
  121. /**
  122. * Implements hook_menu_local_tasks_alter
  123. */
  124. function clone_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  125. // Remove tabs from the node clone form - these are confusing as they link to
  126. // the the original node and its edit form.
  127. if ($router_item['path'] == 'node/%/clone/%') {
  128. $data['tabs'] = array();
  129. }
  130. }
  131. /**
  132. * Implementation of hook_node_type_delete().
  133. */
  134. function clone_node_type_delete($info) {
  135. variable_del('clone_reset_' . $info->type);
  136. }
  137. /**
  138. * Implementation of hook_node_type_update().
  139. */
  140. function clone_node_type_update($info) {
  141. if (!empty($info->old_type) && $info->old_type != $info->type) {
  142. if (variable_get('clone_reset_' . $info->old_type, FALSE)) {
  143. variable_del('clone_reset_' . $info->old_type);
  144. variable_set('clone_reset_' . $info->type, TRUE);
  145. }
  146. }
  147. }
  148. /**
  149. * Implements hook_views_api.
  150. */
  151. function clone_views_api() {
  152. return array(
  153. 'api' => 3,
  154. 'path' => drupal_get_path('module', 'clone') . '/views',
  155. );
  156. }
  157. /**
  158. * Implementation of hook_admin_paths().
  159. */
  160. function clone_admin_paths() {
  161. if (variable_get('node_admin_theme')) {
  162. $paths = array(
  163. 'node/*/clone/*' => TRUE,
  164. );
  165. return $paths;
  166. }
  167. }
  168. /**
  169. * Implements hook_form_BASE_FORM_ID_alter().
  170. */
  171. function clone_form_node_form_alter(&$form, $form_state, $form_id) {
  172. // Add the clone_from_original_nid value for node forms triggered by cloning.
  173. // This will make sure the clone_from_original_nid property is still
  174. // attached to the node when passing through hook_node_insert().
  175. if (!empty($form['#node']->clone_from_original_nid)) {
  176. $form['clone_from_original_nid'] = array(
  177. '#type' => 'value',
  178. '#value' => $form['#node']->clone_from_original_nid,
  179. );
  180. }
  181. }
  182. /**
  183. * Implements hook_form_FORM_ID_alter().
  184. */
  185. function clone_form_node_admin_content_alter(&$form, $form_state, $form_id) {
  186. if (variable_get('clone_method', 'prepopulate') == 'prepopulate') {
  187. $destination = drupal_get_destination();
  188. }
  189. else {
  190. $destination = array();
  191. }
  192. // The property attribute changes in the $form array depending on the user role.
  193. $property = isset($form['admin']['nodes']['#options']) ? '#options' : '#rows';
  194. if (empty($form['admin']['nodes'][$property])) {
  195. return;
  196. }
  197. // Expose a Clone operation on each node.
  198. foreach ($form['admin']['nodes'][$property] as $nid => &$row) {
  199. $node = node_load($nid);
  200. if (clone_access_cloning($node)) {
  201. // The structure of this form is different if there is just 1 or more
  202. // than one operation.
  203. if (!isset($row['operations']['data']['#links'])) {
  204. $row['operations']['data']['#links'] = array();
  205. $row['operations']['data']['#attributes']['class'] = array('links', 'inline');
  206. $row['operations']['data']['#theme'] = 'links__node_operations';
  207. if (isset($row['operations']['data']['#title'])) {
  208. $title = $row['operations']['data']['#title'];
  209. $row['operations']['data']['#links'][$title] = array(
  210. 'title' => $title,
  211. 'href' => $row['operations']['data']['#href'],
  212. 'query' => $row['operations']['data']['#options']['query'],
  213. );
  214. unset($row['operations']['data']['#type']);
  215. }
  216. }
  217. $row['operations']['data']['#links']['clone'] = array(
  218. 'title' => t('clone'),
  219. 'href' => 'node/' . $nid . '/clone/' . clone_get_token($nid),
  220. 'query' => $destination,
  221. );
  222. }
  223. }
  224. }
  225. /**
  226. * Implements hook_action_info().
  227. */
  228. function clone_action_info() {
  229. return array(
  230. 'clone_action_clone' => array(
  231. 'type' => 'node',
  232. 'label' => t('Clone item'),
  233. 'configurable' => TRUE,
  234. 'hooks' => array('any' => TRUE),
  235. 'triggers' => array('any'),
  236. ),
  237. );
  238. }
  239. /**
  240. * Action callback.
  241. */
  242. function clone_action_clone($original_node, $context) {
  243. module_load_include('inc', 'clone', 'clone.pages');
  244. if (clone_is_permitted($original_node->type)) {
  245. $val = !empty($context['clone_context']) ? $context['clone_context'] : array();
  246. $node = _clone_node_prepare($original_node, !empty($val['prefix_title']));
  247. if (isset($val['substitute_from']) && strlen($val['substitute_from']) && isset($val['substitute_to'])) {
  248. $i = (!empty($val['substitute_case_insensitive']) ? 'i' : '');
  249. $pattern = '#' . strtr($val['substitute_from'], array('#' => '\#')) . '#' . $i;
  250. foreach (array('title') as $property) {
  251. $new = preg_replace($pattern, $val['substitute_to'], $node->{$property});
  252. if ($new) {
  253. $node->{$property} = $new;
  254. }
  255. }
  256. foreach (array('body') as $property) {
  257. foreach ($node->{$property} as $lang => $row) {
  258. foreach ($row as $delta => $data) {
  259. foreach (array('value', 'summary') as $key) {
  260. if (isset($node->{$property}[$lang][$delta][$key])) {
  261. $new = preg_replace($pattern, $val['substitute_to'], $node->{$property}[$lang][$delta][$key]);
  262. if ($new) {
  263. $node->{$property}[$lang][$delta][$key] = $new;
  264. }
  265. }
  266. }
  267. }
  268. }
  269. }
  270. }
  271. // Let other modules do special fixing up.
  272. $context = array('method' => 'action', 'original_node' => $original_node, 'clone_context' => $val);
  273. drupal_alter('clone_node', $node, $context);
  274. node_save($node);
  275. if (module_exists('rules')) {
  276. rules_invoke_event('clone_node', $node, $original_node);
  277. }
  278. }
  279. else {
  280. drupal_set_message(t('Clone failed for %title : not permitted for nodes of type %type', array('%title' => $original_node->title, '%type' => $original_node->type)), 'warning');
  281. }
  282. }
  283. /**
  284. * Action form.
  285. */
  286. function clone_action_clone_form($context) {
  287. $form['clone_context'] = array(
  288. '#tree' => TRUE,
  289. );
  290. $form['clone_context']['prefix_title'] = array(
  291. '#title' => t('Prefix title'),
  292. '#type' => 'checkbox',
  293. '#description' => t('Should cloned node tiles be prefixed?'),
  294. '#default_value' => isset($context['clone_context']['prefix_title']) ? $context['clone_context']['prefix_title'] : 1,
  295. );
  296. $form['clone_context']['substitute_from'] = array(
  297. '#title' => t('Substitute from string'),
  298. '#type' => 'textfield',
  299. '#description' => t('String (or regex) to substitute from in title and body.'),
  300. '#default_value' => isset($context['clone_context']['substitue_from']) ? $context['clone_context']['substitue_from'] : '',
  301. );
  302. $form['clone_context']['substitute_to'] = array(
  303. '#title' => t('Substitute to string'),
  304. '#type' => 'textfield',
  305. '#description' => t('String (or regex) to substitute to in title and body.'),
  306. '#default_value' => isset($context['clone_context']['substitue_to']) ? $context['clone_context']['substitue_to'] : '',
  307. );
  308. $form['clone_context']['substitute_case_insensitive'] = array(
  309. '#title' => t('Case insensitive substitution'),
  310. '#type' => 'checkbox',
  311. '#description' => t('Should the substituion match be case insensitive?'),
  312. '#default_value' => isset($context['clone_context']['substitute_case_insensitive']) ? $context['clone_context']['substitute_case_insensitive'] : NULL,
  313. );
  314. return $form;
  315. }
  316. /**
  317. * Action form submit.
  318. */
  319. function clone_action_clone_submit($form, $form_state) {
  320. return array('clone_context' => $form_state['values']['clone_context']);
  321. }