Merge tag '6.31' into 6.x
[project/drupal.git] / modules / node / node.admin.inc
1 <?php
2
3 /**
4 * @file
5 * Content administration and module settings UI.
6 */
7
8 /**
9 * Menu callback; presents general node configuration options.
10 */
11 function node_configure() {
12 $status = '<p>'. t('If the site is experiencing problems with permissions to content, you may have to rebuild the permissions cache. Possible causes for permission problems are disabling modules or configuration changes to permissions. Rebuilding will remove all privileges to posts, and replace them with permissions based on the current modules and settings.') .'</p>';
13 $status .= '<p>'. t('Rebuilding may take some time if there is a lot of content or complex permission settings. After rebuilding has completed posts will automatically use the new permissions.') .'</p>';
14
15 $form['access'] = array(
16 '#type' => 'fieldset',
17 '#title' => t('Node access status'),
18 );
19 $form['access']['status'] = array('#value' => $status);
20 $form['access']['rebuild'] = array(
21 '#type' => 'submit',
22 '#value' => t('Rebuild permissions'),
23 '#submit' => array('node_configure_access_submit'),
24 );
25
26 $form['default_nodes_main'] = array(
27 '#type' => 'select', '#title' => t('Number of posts on main page'), '#default_value' => variable_get('default_nodes_main', 10),
28 '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
29 '#description' => t('The default maximum number of posts to display per page on overview pages such as the main page.')
30 );
31 $form['teaser_length'] = array(
32 '#type' => 'select', '#title' => t('Length of trimmed posts'), '#default_value' => variable_get('teaser_length', 600),
33 '#options' => array(
34 0 => t('Unlimited'),
35 200 => t('200 characters'),
36 400 => t('400 characters'),
37 600 => t('600 characters'),
38 800 => t('800 characters'),
39 1000 => t('1000 characters'),
40 1200 => t('1200 characters'),
41 1400 => t('1400 characters'),
42 1600 => t('1600 characters'),
43 1800 => t('1800 characters'),
44 2000 => t('2000 characters'),
45 ),
46 '#description' => t("The maximum number of characters used in the trimmed version of a post. Drupal will use this setting to determine at which offset long posts should be trimmed. The trimmed version of a post is typically used as a teaser when displaying the post on the main page, in XML feeds, etc. To disable teasers, set to 'Unlimited'. Note that this setting will only affect new or updated content and will not affect existing teasers.")
47 );
48
49 $form['node_preview'] = array(
50 '#type' => 'radios',
51 '#title' => t('Preview post'),
52 '#default_value' => variable_get('node_preview', 0),
53 '#options' => array(t('Optional'), t('Required')),
54 '#description' => t('Must users preview posts before submitting?'),
55 );
56
57 return system_settings_form($form);
58 }
59
60 /**
61 * Form button submit callback.
62 */
63 function node_configure_access_submit($form, &$form_state) {
64 $form_state['redirect'] = 'admin/content/node-settings/rebuild';
65 }
66
67 /**
68 * Menu callback: confirm rebuilding of permissions.
69 */
70 function node_configure_rebuild_confirm() {
71 return confirm_form(array(), t('Are you sure you want to rebuild the permissions on site content?'),
72 'admin/content/node-settings', t('This action rebuilds all permissions on site content, and may be a lengthy process. This action cannot be undone.'), t('Rebuild permissions'), t('Cancel'));
73 }
74
75 /**
76 * Handler for wipe confirmation
77 */
78 function node_configure_rebuild_confirm_submit($form, &$form_state) {
79 node_access_rebuild(TRUE);
80 $form_state['redirect'] = 'admin/content/node-settings';
81 }
82
83 /**
84 * Implementation of hook_node_operations().
85 */
86 function node_node_operations() {
87 $operations = array(
88 'publish' => array(
89 'label' => t('Publish'),
90 'callback' => 'node_mass_update',
91 'callback arguments' => array('updates' => array('status' => 1)),
92 ),
93 'unpublish' => array(
94 'label' => t('Unpublish'),
95 'callback' => 'node_mass_update',
96 'callback arguments' => array('updates' => array('status' => 0)),
97 ),
98 'promote' => array(
99 'label' => t('Promote to front page'),
100 'callback' => 'node_mass_update',
101 'callback arguments' => array('updates' => array('status' => 1, 'promote' => 1)),
102 ),
103 'demote' => array(
104 'label' => t('Demote from front page'),
105 'callback' => 'node_mass_update',
106 'callback arguments' => array('updates' => array('promote' => 0)),
107 ),
108 'sticky' => array(
109 'label' => t('Make sticky'),
110 'callback' => 'node_mass_update',
111 'callback arguments' => array('updates' => array('status' => 1, 'sticky' => 1)),
112 ),
113 'unsticky' => array(
114 'label' => t('Remove stickiness'),
115 'callback' => 'node_mass_update',
116 'callback arguments' => array('updates' => array('sticky' => 0)),
117 ),
118 'delete' => array(
119 'label' => t('Delete'),
120 'callback' => NULL,
121 ),
122 );
123 return $operations;
124 }
125
126 /**
127 * List node administration filters that can be applied.
128 */
129 function node_filters() {
130 // Regular filters
131 $filters['status'] = array(
132 'title' => t('status'),
133 'options' => array(
134 'status-1' => t('published'),
135 'status-0' => t('not published'),
136 'promote-1' => t('promoted'),
137 'promote-0' => t('not promoted'),
138 'sticky-1' => t('sticky'),
139 'sticky-0' => t('not sticky'),
140 ),
141 );
142 // Include translation states if we have this module enabled
143 if (module_exists('translation')) {
144 $filters['status']['options'] += array(
145 'translate-0' => t('Up to date translation'),
146 'translate-1' => t('Outdated translation'),
147 );
148 }
149
150 $filters['type'] = array('title' => t('type'), 'options' => node_get_types('names'));
151
152 // The taxonomy filter
153 if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
154 $filters['category'] = array('title' => t('category'), 'options' => $taxonomy);
155 }
156 // Language filter if there is a list of languages
157 if ($languages = module_invoke('locale', 'language_list')) {
158 $languages = array('' => t('Language neutral')) + $languages;
159 $filters['language'] = array('title' => t('language'), 'options' => $languages);
160 }
161 return $filters;
162 }
163
164 /**
165 * Build query for node administration filters based on session.
166 */
167 function node_build_filter_query() {
168 $filters = node_filters();
169
170 // Build query
171 $where = $args = array();
172 $join = '';
173 foreach ($_SESSION['node_overview_filter'] as $index => $filter) {
174 list($key, $value) = $filter;
175 switch ($key) {
176 case 'status':
177 // Note: no exploitable hole as $key/$value have already been checked when submitted
178 list($key, $value) = explode('-', $value, 2);
179 $where[] = 'n.'. $key .' = %d';
180 break;
181 case 'category':
182 $table = "tn$index";
183 $where[] = "$table.tid = %d";
184 $join .= "INNER JOIN {term_node} $table ON n.vid = $table.vid ";
185 break;
186 case 'type':
187 $where[] = "n.type = '%s'";
188 break;
189 case 'language':
190 $where[] = "n.language = '%s'";
191 break;
192 }
193 $args[] = $value;
194 }
195 $where = count($where) ? 'WHERE '. implode(' AND ', $where) : '';
196
197 return array('where' => $where, 'join' => $join, 'args' => $args);
198 }
199
200 /**
201 * Return form for node administration filters.
202 */
203 function node_filter_form() {
204 $session = &$_SESSION['node_overview_filter'];
205 $session = is_array($session) ? $session : array();
206 $filters = node_filters();
207
208 $i = 0;
209 $form['filters'] = array(
210 '#type' => 'fieldset',
211 '#title' => t('Show only items where'),
212 '#theme' => 'node_filters',
213 );
214 $form['#submit'][] = 'node_filter_form_submit';
215 foreach ($session as $filter) {
216 list($type, $value) = $filter;
217 if ($type == 'category') {
218 // Load term name from DB rather than search and parse options array.
219 $value = module_invoke('taxonomy', 'get_term', $value);
220 $value = $value->name;
221 }
222 else if ($type == 'language') {
223 $value = empty($value) ? t('Language neutral') : module_invoke('locale', 'language_name', $value);
224 }
225 else {
226 $value = $filters[$type]['options'][$value];
227 }
228 if ($i++) {
229 $form['filters']['current'][] = array('#value' => t('<em>and</em> where <strong>%a</strong> is <strong>%b</strong>', array('%a' => $filters[$type]['title'], '%b' => $value)));
230 }
231 else {
232 $form['filters']['current'][] = array('#value' => t('<strong>%a</strong> is <strong>%b</strong>', array('%a' => $filters[$type]['title'], '%b' => $value)));
233 }
234 if (in_array($type, array('type', 'language'))) {
235 // Remove the option if it is already being filtered on.
236 unset($filters[$type]);
237 }
238 }
239
240 foreach ($filters as $key => $filter) {
241 $names[$key] = $filter['title'];
242 $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
243 }
244
245 $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
246 $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
247 if (count($session)) {
248 $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
249 $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
250 }
251
252 drupal_add_js('misc/form.js', 'core');
253
254 return $form;
255 }
256
257 /**
258 * Theme node administration filter form.
259 *
260 * @ingroup themeable
261 */
262 function theme_node_filter_form($form) {
263 $output = '';
264 $output .= '<div id="node-admin-filter">';
265 $output .= drupal_render($form['filters']);
266 $output .= '</div>';
267 $output .= drupal_render($form);
268 return $output;
269 }
270
271 /**
272 * Theme node administration filter selector.
273 *
274 * @ingroup themeable
275 */
276 function theme_node_filters($form) {
277 $output = '';
278 $output .= '<ul class="clear-block">';
279 if (!empty($form['current'])) {
280 foreach (element_children($form['current']) as $key) {
281 $output .= '<li>'. drupal_render($form['current'][$key]) .'</li>';
282 }
283 }
284
285 $output .= '<li><dl class="multiselect">'. (!empty($form['current']) ? '<dt><em>'. t('and') .'</em> '. t('where') .'</dt>' : '') .'<dd class="a">';
286 foreach (element_children($form['filter']) as $key) {
287 $output .= drupal_render($form['filter'][$key]);
288 }
289 $output .= '</dd>';
290
291 $output .= '<dt>'. t('is') .'</dt><dd class="b">';
292
293 foreach (element_children($form['status']) as $key) {
294 $output .= drupal_render($form['status'][$key]);
295 }
296 $output .= '</dd>';
297
298 $output .= '</dl>';
299 $output .= '<div class="container-inline" id="node-admin-buttons">'. drupal_render($form['buttons']) .'</div>';
300 $output .= '</li></ul>';
301
302 return $output;
303 }
304
305 /**
306 * Process result from node administration filter form.
307 */
308 function node_filter_form_submit($form, &$form_state) {
309 $filters = node_filters();
310 switch ($form_state['values']['op']) {
311 case t('Filter'):
312 case t('Refine'):
313 if (isset($form_state['values']['filter'])) {
314 $filter = $form_state['values']['filter'];
315
316 // Flatten the options array to accommodate hierarchical/nested options.
317 $flat_options = form_options_flatten($filters[$filter]['options']);
318
319 if (isset($flat_options[$form_state['values'][$filter]])) {
320 $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]);
321 }
322 }
323 break;
324 case t('Undo'):
325 array_pop($_SESSION['node_overview_filter']);
326 break;
327 case t('Reset'):
328 $_SESSION['node_overview_filter'] = array();
329 break;
330 }
331 }
332
333 /**
334 * Make mass update of nodes, changing all nodes in the $nodes array
335 * to update them with the field values in $updates.
336 *
337 * IMPORTANT NOTE: This function is intended to work when called
338 * from a form submit handler. Calling it outside of the form submission
339 * process may not work correctly.
340 *
341 * @param array $nodes
342 * Array of node nids to update.
343 * @param array $updates
344 * Array of key/value pairs with node field names and the
345 * value to update that field to.
346 */
347 function node_mass_update($nodes, $updates) {
348 // We use batch processing to prevent timeout when updating a large number
349 // of nodes.
350 if (count($nodes) > 10) {
351 $batch = array(
352 'operations' => array(
353 array('_node_mass_update_batch_process', array($nodes, $updates))
354 ),
355 'finished' => '_node_mass_update_batch_finished',
356 'title' => t('Processing'),
357 // We use a single multi-pass operation, so the default
358 // 'Remaining x of y operations' message will be confusing here.
359 'progress_message' => '',
360 'error_message' => t('The update has encountered an error.'),
361 // The operations do not live in the .module file, so we need to
362 // tell the batch engine which file to load before calling them.
363 'file' => drupal_get_path('module', 'node') .'/node.admin.inc',
364 );
365 batch_set($batch);
366 }
367 else {
368 foreach ($nodes as $nid) {
369 _node_mass_update_helper($nid, $updates);
370 }
371 drupal_set_message(t('The update has been performed.'));
372 }
373 }
374
375 /**
376 * Node Mass Update - helper function.
377 */
378 function _node_mass_update_helper($nid, $updates) {
379 $node = node_load($nid, NULL, TRUE);
380 foreach ($updates as $name => $value) {
381 $node->$name = $value;
382 }
383 node_save($node);
384 return $node;
385 }
386
387 /**
388 * Node Mass Update Batch operation
389 */
390 function _node_mass_update_batch_process($nodes, $updates, &$context) {
391 if (!isset($context['sandbox']['progress'])) {
392 $context['sandbox']['progress'] = 0;
393 $context['sandbox']['max'] = count($nodes);
394 $context['sandbox']['nodes'] = $nodes;
395 }
396
397 // Process nodes by groups of 5.
398 $count = min(5, count($context['sandbox']['nodes']));
399 for ($i = 1; $i <= $count; $i++) {
400 // For each nid, load the node, reset the values, and save it.
401 $nid = array_shift($context['sandbox']['nodes']);
402 $node = _node_mass_update_helper($nid, $updates);
403
404 // Store result for post-processing in the finished callback.
405 $context['results'][] = l($node->title, 'node/'. $node->nid);
406
407 // Update our progress information.
408 $context['sandbox']['progress']++;
409 }
410
411 // Inform the batch engine that we are not finished,
412 // and provide an estimation of the completion level we reached.
413 if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
414 $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
415 }
416 }
417
418 /**
419 * Node Mass Update Batch 'finished' callback.
420 */
421 function _node_mass_update_batch_finished($success, $results, $operations) {
422 if ($success) {
423 drupal_set_message(t('The update has been performed.'));
424 }
425 else {
426 drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
427 $message = format_plural(count($results), '1 item successfully processed:', '@count items successfully processed:');
428 $message .= theme('item_list', $results);
429 drupal_set_message($message);
430 }
431 }
432
433 /**
434 * Menu callback: content administration.
435 */
436 function node_admin_content($form_state) {
437 if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') {
438 return node_multiple_delete_confirm($form_state, array_filter($form_state['values']['nodes']));
439 }
440 $form = node_filter_form();
441
442 $form['#theme'] = 'node_filter_form';
443 $form['admin'] = node_admin_nodes();
444
445 return $form;
446 }
447
448 /**
449 * Form builder: Builds the node administration overview.
450 */
451 function node_admin_nodes() {
452
453 $filter = node_build_filter_query();
454
455 $result = pager_query(db_rewrite_sql('SELECT n.*, u.name FROM {node} n '. $filter['join'] .' INNER JOIN {users} u ON n.uid = u.uid '. $filter['where'] .' ORDER BY n.changed DESC'), 50, 0, NULL, $filter['args']);
456
457 // Enable language column if locale is enabled or if we have any node with language
458 $count = db_result(db_query("SELECT COUNT(*) FROM {node} n WHERE language != ''"));
459 $multilanguage = (module_exists('locale') || $count);
460
461 $form['options'] = array(
462 '#type' => 'fieldset',
463 '#title' => t('Update options'),
464 '#prefix' => '<div class="container-inline">',
465 '#suffix' => '</div>',
466 );
467 $options = array();
468 foreach (module_invoke_all('node_operations') as $operation => $array) {
469 $options[$operation] = $array['label'];
470 }
471 $form['options']['operation'] = array(
472 '#type' => 'select',
473 '#options' => $options,
474 '#default_value' => 'approve',
475 );
476 $form['options']['submit'] = array(
477 '#type' => 'submit',
478 '#value' => t('Update'),
479 '#submit' => array('node_admin_nodes_submit'),
480 );
481
482 $languages = language_list();
483 $destination = drupal_get_destination();
484 $nodes = array();
485 while ($node = db_fetch_object($result)) {
486 $nodes[$node->nid] = '';
487 $options = empty($node->language) ? array() : array('language' => $languages[$node->language]);
488 $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid, $options) .' '. theme('mark', node_mark($node->nid, $node->changed)));
489 $form['name'][$node->nid] = array('#value' => check_plain(node_get_types('name', $node)));
490 $form['username'][$node->nid] = array('#value' => theme('username', $node));
491 $form['status'][$node->nid] = array('#value' => ($node->status ? t('published') : t('not published')));
492 if ($multilanguage) {
493 $form['language'][$node->nid] = array('#value' => empty($node->language) ? t('Language neutral') : t($languages[$node->language]->name));
494 }
495 $form['operations'][$node->nid] = array('#value' => l(t('edit'), 'node/'. $node->nid .'/edit', array('query' => $destination)));
496 }
497 $form['nodes'] = array('#type' => 'checkboxes', '#options' => $nodes);
498 $form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
499 $form['#theme'] = 'node_admin_nodes';
500 return $form;
501 }
502
503 /**
504 * Validate node_admin_nodes form submissions.
505 *
506 * Check if any nodes have been selected to perform the chosen
507 * 'Update option' on.
508 */
509 function node_admin_nodes_validate($form, &$form_state) {
510 $nodes = array_filter($form_state['values']['nodes']);
511 if (count($nodes) == 0) {
512 form_set_error('', t('No items selected.'));
513 }
514 }
515
516 /**
517 * Process node_admin_nodes form submissions.
518 *
519 * Execute the chosen 'Update option' on the selected nodes.
520 */
521 function node_admin_nodes_submit($form, &$form_state) {
522 $operations = module_invoke_all('node_operations');
523 $operation = $operations[$form_state['values']['operation']];
524 // Filter out unchecked nodes
525 $nodes = array_filter($form_state['values']['nodes']);
526 if ($function = $operation['callback']) {
527 // Add in callback arguments if present.
528 if (isset($operation['callback arguments'])) {
529 $args = array_merge(array($nodes), $operation['callback arguments']);
530 }
531 else {
532 $args = array($nodes);
533 }
534 call_user_func_array($function, $args);
535
536 cache_clear_all();
537 }
538 else {
539 // We need to rebuild the form to go to a second step. For example, to
540 // show the confirmation form for the deletion of nodes.
541 $form_state['rebuild'] = TRUE;
542 }
543 }
544
545
546 /**
547 * Theme node administration overview.
548 *
549 * @ingroup themeable
550 */
551 function theme_node_admin_nodes($form) {
552 // If there are rows in this form, then $form['title'] contains a list of
553 // the title form elements.
554 $has_posts = isset($form['title']) && is_array($form['title']);
555 $select_header = $has_posts ? theme('table_select_header_cell') : '';
556 $header = array($select_header, t('Title'), t('Type'), t('Author'), t('Status'));
557 if (isset($form['language'])) {
558 $header[] = t('Language');
559 }
560 $header[] = t('Operations');
561 $output = '';
562
563 $output .= drupal_render($form['options']);
564 if ($has_posts) {
565 foreach (element_children($form['title']) as $key) {
566 $row = array();
567 $row[] = drupal_render($form['nodes'][$key]);
568 $row[] = drupal_render($form['title'][$key]);
569 $row[] = drupal_render($form['name'][$key]);
570 $row[] = drupal_render($form['username'][$key]);
571 $row[] = drupal_render($form['status'][$key]);
572 if (isset($form['language'])) {
573 $row[] = drupal_render($form['language'][$key]);
574 }
575 $row[] = drupal_render($form['operations'][$key]);
576 $rows[] = $row;
577 }
578
579 }
580 else {
581 $rows[] = array(array('data' => t('No posts available.'), 'colspan' => '6'));
582 }
583
584 $output .= theme('table', $header, $rows);
585 if ($form['pager']['#value']) {
586 $output .= drupal_render($form['pager']);
587 }
588
589 $output .= drupal_render($form);
590
591 return $output;
592 }
593
594 function node_multiple_delete_confirm(&$form_state, $nodes) {
595
596 $form['nodes'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
597 // array_filter returns only elements with TRUE values
598 foreach ($nodes as $nid => $value) {
599 $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
600 $form['nodes'][$nid] = array(
601 '#type' => 'hidden',
602 '#value' => $nid,
603 '#prefix' => '<li>',
604 '#suffix' => check_plain($title) ."</li>\n",
605 );
606 }
607 $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
608 $form['#submit'][] = 'node_multiple_delete_confirm_submit';
609 return confirm_form($form,
610 t('Are you sure you want to delete these items?'),
611 'admin/content/node', t('This action cannot be undone.'),
612 t('Delete all'), t('Cancel'));
613 }
614
615 function node_multiple_delete_confirm_submit($form, &$form_state) {
616 if ($form_state['values']['confirm']) {
617 foreach ($form_state['values']['nodes'] as $nid => $value) {
618 node_delete($nid);
619 }
620 drupal_set_message(t('The items have been deleted.'));
621 }
622 $form_state['redirect'] = 'admin/content/node';
623 return;
624 }
625