cms/drupal/modules/node/node.admin.inc
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * @file
       
     5  * Content administration and module settings UI.
       
     6  */
       
     7 
       
     8 /**
       
     9  * Menu callback: confirm rebuilding of permissions.
       
    10  *
       
    11  * @see node_configure_rebuild_confirm_submit()
       
    12  * @see node_menu()
       
    13  * @ingroup forms
       
    14  */
       
    15 function node_configure_rebuild_confirm() {
       
    16   return confirm_form(array(), t('Are you sure you want to rebuild the permissions on site content?'),
       
    17                   'admin/reports/status', 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'));
       
    18 }
       
    19 
       
    20 /**
       
    21  * Handler for wipe confirmation
       
    22  *
       
    23  * @see node_configure_rebuild_confirm()
       
    24  */
       
    25 function node_configure_rebuild_confirm_submit($form, &$form_state) {
       
    26   node_access_rebuild(TRUE);
       
    27   $form_state['redirect'] = 'admin/reports/status';
       
    28 }
       
    29 
       
    30 /**
       
    31  * Implements hook_node_operations().
       
    32  */
       
    33 function node_node_operations() {
       
    34   $operations = array(
       
    35     'publish' => array(
       
    36       'label' => t('Publish selected content'),
       
    37       'callback' => 'node_mass_update',
       
    38       'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED)),
       
    39     ),
       
    40     'unpublish' => array(
       
    41       'label' => t('Unpublish selected content'),
       
    42       'callback' => 'node_mass_update',
       
    43       'callback arguments' => array('updates' => array('status' => NODE_NOT_PUBLISHED)),
       
    44     ),
       
    45     'promote' => array(
       
    46       'label' => t('Promote selected content to front page'),
       
    47       'callback' => 'node_mass_update',
       
    48       'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'promote' => NODE_PROMOTED)),
       
    49     ),
       
    50     'demote' => array(
       
    51       'label' => t('Demote selected content from front page'),
       
    52       'callback' => 'node_mass_update',
       
    53       'callback arguments' => array('updates' => array('promote' => NODE_NOT_PROMOTED)),
       
    54     ),
       
    55     'sticky' => array(
       
    56       'label' => t('Make selected content sticky'),
       
    57       'callback' => 'node_mass_update',
       
    58       'callback arguments' => array('updates' => array('status' => NODE_PUBLISHED, 'sticky' => NODE_STICKY)),
       
    59     ),
       
    60     'unsticky' => array(
       
    61       'label' => t('Make selected content not sticky'),
       
    62       'callback' => 'node_mass_update',
       
    63       'callback arguments' => array('updates' => array('sticky' => NODE_NOT_STICKY)),
       
    64     ),
       
    65     'delete' => array(
       
    66       'label' => t('Delete selected content'),
       
    67       'callback' => NULL,
       
    68     ),
       
    69   );
       
    70   return $operations;
       
    71 }
       
    72 
       
    73 /**
       
    74  * List node administration filters that can be applied.
       
    75  *
       
    76  * @return
       
    77  *   An associative array of filters.
       
    78  */
       
    79 function node_filters() {
       
    80   // Regular filters
       
    81   $filters['status'] = array(
       
    82     'title' => t('status'),
       
    83     'options' => array(
       
    84       '[any]' => t('any'),
       
    85       'status-1' => t('published'),
       
    86       'status-0' => t('not published'),
       
    87       'promote-1' => t('promoted'),
       
    88       'promote-0' => t('not promoted'),
       
    89       'sticky-1' => t('sticky'),
       
    90       'sticky-0' => t('not sticky'),
       
    91     ),
       
    92   );
       
    93   // Include translation states if we have this module enabled
       
    94   if (module_exists('translation')) {
       
    95     $filters['status']['options'] += array(
       
    96       'translate-0' => t('Up to date translation'),
       
    97       'translate-1' => t('Outdated translation'),
       
    98     );
       
    99   }
       
   100 
       
   101   $filters['type'] = array(
       
   102     'title' => t('type'),
       
   103     'options' => array(
       
   104       '[any]' => t('any'),
       
   105     ) + node_type_get_names(),
       
   106   );
       
   107 
       
   108   // Language filter if there is a list of languages
       
   109   if ($languages = module_invoke('locale', 'language_list')) {
       
   110     $languages = array(LANGUAGE_NONE => t('Language neutral')) + $languages;
       
   111     $filters['language'] = array(
       
   112       'title' => t('language'),
       
   113       'options' => array(
       
   114         '[any]' => t('any'),
       
   115       ) + $languages,
       
   116     );
       
   117   }
       
   118   return $filters;
       
   119 }
       
   120 
       
   121 /**
       
   122  * Applies filters for node administration filters based on session.
       
   123  *
       
   124  * @param $query
       
   125  *   A SelectQuery to which the filters should be applied.
       
   126  */
       
   127 function node_build_filter_query(SelectQueryInterface $query) {
       
   128   // Build query
       
   129   $filter_data = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array();
       
   130   foreach ($filter_data as $index => $filter) {
       
   131     list($key, $value) = $filter;
       
   132     switch ($key) {
       
   133       case 'status':
       
   134         // Note: no exploitable hole as $key/$value have already been checked when submitted
       
   135         list($key, $value) = explode('-', $value, 2);
       
   136       case 'type':
       
   137       case 'language':
       
   138         $query->condition('n.' . $key, $value);
       
   139         break;
       
   140     }
       
   141   }
       
   142 }
       
   143 
       
   144 /**
       
   145  * Returns the node administration filters form array to node_admin_content().
       
   146  *
       
   147  * @see node_admin_nodes()
       
   148  * @see node_admin_nodes_submit()
       
   149  * @see node_admin_nodes_validate()
       
   150  * @see node_filter_form_submit()
       
   151  * @see node_multiple_delete_confirm()
       
   152  * @see node_multiple_delete_confirm_submit()
       
   153  *
       
   154  * @ingroup forms
       
   155  */
       
   156 function node_filter_form() {
       
   157   $session = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array();
       
   158   $filters = node_filters();
       
   159 
       
   160   $i = 0;
       
   161   $form['filters'] = array(
       
   162     '#type' => 'fieldset',
       
   163     '#title' => t('Show only items where'),
       
   164     '#theme' => 'exposed_filters__node',
       
   165   );
       
   166   foreach ($session as $filter) {
       
   167     list($type, $value) = $filter;
       
   168     if ($type == 'term') {
       
   169       // Load term name from DB rather than search and parse options array.
       
   170       $value = module_invoke('taxonomy', 'term_load', $value);
       
   171       $value = $value->name;
       
   172     }
       
   173     elseif ($type == 'language') {
       
   174       $value = $value == LANGUAGE_NONE ? t('Language neutral') : module_invoke('locale', 'language_name', $value);
       
   175     }
       
   176     else {
       
   177       $value = $filters[$type]['options'][$value];
       
   178     }
       
   179     $t_args = array('%property' => $filters[$type]['title'], '%value' => $value);
       
   180     if ($i++) {
       
   181       $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args));
       
   182     }
       
   183     else {
       
   184       $form['filters']['current'][] = array('#markup' => t('where %property is %value', $t_args));
       
   185     }
       
   186     if (in_array($type, array('type', 'language'))) {
       
   187       // Remove the option if it is already being filtered on.
       
   188       unset($filters[$type]);
       
   189     }
       
   190   }
       
   191 
       
   192   $form['filters']['status'] = array(
       
   193     '#type' => 'container',
       
   194     '#attributes' => array('class' => array('clearfix')),
       
   195     '#prefix' => ($i ? '<div class="additional-filters">' . t('and where') . '</div>' : ''),
       
   196   );
       
   197   $form['filters']['status']['filters'] = array(
       
   198     '#type' => 'container',
       
   199     '#attributes' => array('class' => array('filters')),
       
   200   );
       
   201   foreach ($filters as $key => $filter) {
       
   202     $form['filters']['status']['filters'][$key] = array(
       
   203       '#type' => 'select',
       
   204       '#options' => $filter['options'],
       
   205       '#title' => $filter['title'],
       
   206       '#default_value' => '[any]',
       
   207     );
       
   208   }
       
   209 
       
   210   $form['filters']['status']['actions'] = array(
       
   211     '#type' => 'actions',
       
   212     '#attributes' => array('class' => array('container-inline')),
       
   213   );
       
   214   $form['filters']['status']['actions']['submit'] = array(
       
   215     '#type' => 'submit',
       
   216     '#value' => count($session) ? t('Refine') : t('Filter'),
       
   217   );
       
   218   if (count($session)) {
       
   219     $form['filters']['status']['actions']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
       
   220     $form['filters']['status']['actions']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
       
   221   }
       
   222 
       
   223   drupal_add_js('misc/form.js');
       
   224 
       
   225   return $form;
       
   226 }
       
   227 
       
   228 /**
       
   229  * Form submission handler for node_filter_form().
       
   230  *
       
   231  * @see node_admin_content()
       
   232  * @see node_admin_nodes()
       
   233  * @see node_admin_nodes_submit()
       
   234  * @see node_admin_nodes_validate()
       
   235  * @see node_filter_form()
       
   236  * @see node_multiple_delete_confirm()
       
   237  * @see node_multiple_delete_confirm_submit()
       
   238  */
       
   239 function node_filter_form_submit($form, &$form_state) {
       
   240   $filters = node_filters();
       
   241   switch ($form_state['values']['op']) {
       
   242     case t('Filter'):
       
   243     case t('Refine'):
       
   244       // Apply every filter that has a choice selected other than 'any'.
       
   245       foreach ($filters as $filter => $options) {
       
   246         if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') {
       
   247           // Flatten the options array to accommodate hierarchical/nested options.
       
   248           $flat_options = form_options_flatten($filters[$filter]['options']);
       
   249           // Only accept valid selections offered on the dropdown, block bad input.
       
   250           if (isset($flat_options[$form_state['values'][$filter]])) {
       
   251             $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]);
       
   252           }
       
   253         }
       
   254       }
       
   255       break;
       
   256     case t('Undo'):
       
   257       array_pop($_SESSION['node_overview_filter']);
       
   258       break;
       
   259     case t('Reset'):
       
   260       $_SESSION['node_overview_filter'] = array();
       
   261       break;
       
   262   }
       
   263 }
       
   264 
       
   265 /**
       
   266  * Make mass update of nodes, changing all nodes in the $nodes array
       
   267  * to update them with the field values in $updates.
       
   268  *
       
   269  * IMPORTANT NOTE: This function is intended to work when called from a form
       
   270  * submission handler. Calling it outside of the form submission process may not
       
   271  * work correctly.
       
   272  *
       
   273  * @param array $nodes
       
   274  *   Array of node nids to update.
       
   275  * @param array $updates
       
   276  *   Array of key/value pairs with node field names and the value to update that
       
   277  *   field to.
       
   278  */
       
   279 function node_mass_update($nodes, $updates) {
       
   280   // We use batch processing to prevent timeout when updating a large number
       
   281   // of nodes.
       
   282   if (count($nodes) > 10) {
       
   283     $batch = array(
       
   284       'operations' => array(
       
   285         array('_node_mass_update_batch_process', array($nodes, $updates))
       
   286       ),
       
   287       'finished' => '_node_mass_update_batch_finished',
       
   288       'title' => t('Processing'),
       
   289       // We use a single multi-pass operation, so the default
       
   290       // 'Remaining x of y operations' message will be confusing here.
       
   291       'progress_message' => '',
       
   292       'error_message' => t('The update has encountered an error.'),
       
   293       // The operations do not live in the .module file, so we need to
       
   294       // tell the batch engine which file to load before calling them.
       
   295       'file' => drupal_get_path('module', 'node') . '/node.admin.inc',
       
   296     );
       
   297     batch_set($batch);
       
   298   }
       
   299   else {
       
   300     foreach ($nodes as $nid) {
       
   301       _node_mass_update_helper($nid, $updates);
       
   302     }
       
   303     drupal_set_message(t('The update has been performed.'));
       
   304   }
       
   305 }
       
   306 
       
   307 /**
       
   308  * Updates individual nodes when fewer than 10 are queued.
       
   309  *
       
   310  * @param $nid
       
   311  *   ID of node to update.
       
   312  * @param $updates
       
   313  *   Associative array of updates.
       
   314  *
       
   315  * @return object
       
   316  *   An updated node object.
       
   317  *
       
   318  * @see node_mass_update()
       
   319  */
       
   320 function _node_mass_update_helper($nid, $updates) {
       
   321   $node = node_load($nid, NULL, TRUE);
       
   322   // For efficiency manually save the original node before applying any changes.
       
   323   $node->original = clone $node;
       
   324   foreach ($updates as $name => $value) {
       
   325     $node->$name = $value;
       
   326   }
       
   327   node_save($node);
       
   328   return $node;
       
   329 }
       
   330 
       
   331 /**
       
   332  * Implements callback_batch_operation().
       
   333  *
       
   334  * Executes a batch operation for node_mass_update().
       
   335  *
       
   336  * @param array $nodes
       
   337  *   An array of node IDs.
       
   338  * @param array $updates
       
   339  *   Associative array of updates.
       
   340  * @param array $context
       
   341  *   An array of contextual key/values.
       
   342  */
       
   343 function _node_mass_update_batch_process($nodes, $updates, &$context) {
       
   344   if (!isset($context['sandbox']['progress'])) {
       
   345     $context['sandbox']['progress'] = 0;
       
   346     $context['sandbox']['max'] = count($nodes);
       
   347     $context['sandbox']['nodes'] = $nodes;
       
   348   }
       
   349 
       
   350   // Process nodes by groups of 5.
       
   351   $count = min(5, count($context['sandbox']['nodes']));
       
   352   for ($i = 1; $i <= $count; $i++) {
       
   353     // For each nid, load the node, reset the values, and save it.
       
   354     $nid = array_shift($context['sandbox']['nodes']);
       
   355     $node = _node_mass_update_helper($nid, $updates);
       
   356 
       
   357     // Store result for post-processing in the finished callback.
       
   358     $context['results'][] = l($node->title, 'node/' . $node->nid);
       
   359 
       
   360     // Update our progress information.
       
   361     $context['sandbox']['progress']++;
       
   362   }
       
   363 
       
   364   // Inform the batch engine that we are not finished,
       
   365   // and provide an estimation of the completion level we reached.
       
   366   if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
       
   367     $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
       
   368   }
       
   369 }
       
   370 
       
   371 /**
       
   372  * Implements callback_batch_finished().
       
   373  *
       
   374  * Reports the status of batch operation for node_mass_update().
       
   375  *
       
   376  * @param bool $success
       
   377  *   A boolean indicating whether the batch mass update operation successfully
       
   378  *   concluded.
       
   379  * @param int $results
       
   380  *   The number of nodes updated via the batch mode process.
       
   381  * @param array $operations
       
   382  *   An array of function calls (not used in this function).
       
   383  */
       
   384 function _node_mass_update_batch_finished($success, $results, $operations) {
       
   385   if ($success) {
       
   386     drupal_set_message(t('The update has been performed.'));
       
   387   }
       
   388   else {
       
   389     drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
       
   390     $message = format_plural(count($results), '1 item successfully processed:', '@count items successfully processed:');
       
   391     $message .= theme('item_list', array('items' => $results));
       
   392     drupal_set_message($message);
       
   393   }
       
   394 }
       
   395 
       
   396 /**
       
   397  * Page callback: Form constructor for the content administration form.
       
   398  *
       
   399  * @see node_admin_nodes()
       
   400  * @see node_admin_nodes_submit()
       
   401  * @see node_admin_nodes_validate()
       
   402  * @see node_filter_form()
       
   403  * @see node_filter_form_submit()
       
   404  * @see node_menu()
       
   405  * @see node_multiple_delete_confirm()
       
   406  * @see node_multiple_delete_confirm_submit()
       
   407  * @ingroup forms
       
   408  */
       
   409 function node_admin_content($form, $form_state) {
       
   410   if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') {
       
   411     return node_multiple_delete_confirm($form, $form_state, array_filter($form_state['values']['nodes']));
       
   412   }
       
   413   $form['filter'] = node_filter_form();
       
   414   $form['#submit'][] = 'node_filter_form_submit';
       
   415   $form['admin'] = node_admin_nodes();
       
   416 
       
   417   return $form;
       
   418 }
       
   419 
       
   420 /**
       
   421  * Form builder: Builds the node administration overview.
       
   422  *
       
   423  * @see node_admin_nodes_submit()
       
   424  * @see node_admin_nodes_validate()
       
   425  * @see node_filter_form()
       
   426  * @see node_filter_form_submit()
       
   427  * @see node_multiple_delete_confirm()
       
   428  * @see node_multiple_delete_confirm_submit()
       
   429  *
       
   430  * @ingroup forms
       
   431  */
       
   432 function node_admin_nodes() {
       
   433   $admin_access = user_access('administer nodes');
       
   434 
       
   435   // Build the 'Update options' form.
       
   436   $form['options'] = array(
       
   437     '#type' => 'fieldset',
       
   438     '#title' => t('Update options'),
       
   439     '#attributes' => array('class' => array('container-inline')),
       
   440     '#access' => $admin_access,
       
   441   );
       
   442   $options = array();
       
   443   foreach (module_invoke_all('node_operations') as $operation => $array) {
       
   444     $options[$operation] = $array['label'];
       
   445   }
       
   446   $form['options']['operation'] = array(
       
   447     '#type' => 'select',
       
   448     '#title' => t('Operation'),
       
   449     '#title_display' => 'invisible',
       
   450     '#options' => $options,
       
   451     '#default_value' => 'approve',
       
   452   );
       
   453   $form['options']['submit'] = array(
       
   454     '#type' => 'submit',
       
   455     '#value' => t('Update'),
       
   456     '#validate' => array('node_admin_nodes_validate'),
       
   457     '#submit' => array('node_admin_nodes_submit'),
       
   458   );
       
   459 
       
   460   // Enable language column if translation module is enabled or if we have any
       
   461   // node with language.
       
   462   $multilanguage = (module_exists('translation') || db_query_range("SELECT 1 FROM {node} WHERE language <> :language", 0, 1, array(':language' => LANGUAGE_NONE))->fetchField());
       
   463 
       
   464   // Build the sortable table header.
       
   465   $header = array(
       
   466     'title' => array('data' => t('Title'), 'field' => 'n.title'),
       
   467     'type' => array('data' => t('Type'), 'field' => 'n.type'),
       
   468     'author' => t('Author'),
       
   469     'status' => array('data' => t('Status'), 'field' => 'n.status'),
       
   470     'changed' => array('data' => t('Updated'), 'field' => 'n.changed', 'sort' => 'desc')
       
   471   );
       
   472   if ($multilanguage) {
       
   473     $header['language'] = array('data' => t('Language'), 'field' => 'n.language');
       
   474   }
       
   475   $header['operations'] = array('data' => t('Operations'));
       
   476 
       
   477   $query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort');
       
   478   $query->addTag('node_admin_filter');
       
   479   node_build_filter_query($query);
       
   480 
       
   481   if (!user_access('bypass node access')) {
       
   482     // If the user is able to view their own unpublished nodes, allow them
       
   483     // to see these in addition to published nodes. Check that they actually
       
   484     // have some unpublished nodes to view before adding the condition.
       
   485     if (user_access('view own unpublished content') && $own_unpublished = db_query('SELECT nid FROM {node} WHERE uid = :uid AND status = :status', array(':uid' => $GLOBALS['user']->uid, ':status' => 0))->fetchCol()) {
       
   486       $query->condition(db_or()
       
   487         ->condition('n.status', 1)
       
   488         ->condition('n.nid', $own_unpublished, 'IN')
       
   489       );
       
   490     }
       
   491     else {
       
   492       // If not, restrict the query to published nodes.
       
   493       $query->condition('n.status', 1);
       
   494     }
       
   495   }
       
   496   $nids = $query
       
   497     ->fields('n',array('nid'))
       
   498     ->limit(50)
       
   499     ->orderByHeader($header)
       
   500     ->addTag('node_access')
       
   501     ->execute()
       
   502     ->fetchCol();
       
   503   $nodes = node_load_multiple($nids);
       
   504 
       
   505   // Prepare the list of nodes.
       
   506   $languages = language_list();
       
   507   $destination = drupal_get_destination();
       
   508   $options = array();
       
   509   foreach ($nodes as $node) {
       
   510     $langcode = entity_language('node', $node);
       
   511     $uri = entity_uri('node', $node);
       
   512     if ($langcode != LANGUAGE_NONE && isset($languages[$langcode])) {
       
   513       $uri['options']['language'] = $languages[$langcode];
       
   514     }
       
   515     $options[$node->nid] = array(
       
   516       'title' => array(
       
   517         'data' => array(
       
   518           '#type' => 'link',
       
   519           '#title' => $node->title,
       
   520           '#href' => $uri['path'],
       
   521           '#options' => $uri['options'],
       
   522           '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))),
       
   523         ),
       
   524       ),
       
   525       'type' => check_plain(node_type_get_name($node)),
       
   526       'author' => theme('username', array('account' => $node)),
       
   527       'status' => $node->status ? t('published') : t('not published'),
       
   528       'changed' => format_date($node->changed, 'short'),
       
   529     );
       
   530     if ($multilanguage) {
       
   531       if ($langcode == LANGUAGE_NONE || isset($languages[$langcode])) {
       
   532         $options[$node->nid]['language'] = $langcode == LANGUAGE_NONE ? t('Language neutral') : t($languages[$langcode]->name);
       
   533       }
       
   534       else {
       
   535         $options[$node->nid]['language'] = t('Undefined language (@langcode)', array('@langcode' => $langcode));
       
   536       }
       
   537     }
       
   538     // Build a list of all the accessible operations for the current node.
       
   539     $operations = array();
       
   540     if (node_access('update', $node)) {
       
   541       $operations['edit'] = array(
       
   542         'title' => t('edit'),
       
   543         'href' => 'node/' . $node->nid . '/edit',
       
   544         'query' => $destination,
       
   545       );
       
   546     }
       
   547     if (node_access('delete', $node)) {
       
   548       $operations['delete'] = array(
       
   549         'title' => t('delete'),
       
   550         'href' => 'node/' . $node->nid . '/delete',
       
   551         'query' => $destination,
       
   552       );
       
   553     }
       
   554     $options[$node->nid]['operations'] = array();
       
   555     if (count($operations) > 1) {
       
   556       // Render an unordered list of operations links.
       
   557       $options[$node->nid]['operations'] = array(
       
   558         'data' => array(
       
   559           '#theme' => 'links__node_operations',
       
   560           '#links' => $operations,
       
   561           '#attributes' => array('class' => array('links', 'inline')),
       
   562         ),
       
   563       );
       
   564     }
       
   565     elseif (!empty($operations)) {
       
   566       // Render the first and only operation as a link.
       
   567       $link = reset($operations);
       
   568       $options[$node->nid]['operations'] = array(
       
   569         'data' => array(
       
   570           '#type' => 'link',
       
   571           '#title' => $link['title'],
       
   572           '#href' => $link['href'],
       
   573           '#options' => array('query' => $link['query']),
       
   574         ),
       
   575       );
       
   576     }
       
   577   }
       
   578 
       
   579   // Only use a tableselect when the current user is able to perform any
       
   580   // operations.
       
   581   if ($admin_access) {
       
   582     $form['nodes'] = array(
       
   583       '#type' => 'tableselect',
       
   584       '#header' => $header,
       
   585       '#options' => $options,
       
   586       '#empty' => t('No content available.'),
       
   587     );
       
   588   }
       
   589   // Otherwise, use a simple table.
       
   590   else {
       
   591     $form['nodes'] = array(
       
   592       '#theme' => 'table',
       
   593       '#header' => $header,
       
   594       '#rows' => $options,
       
   595       '#empty' => t('No content available.'),
       
   596     );
       
   597   }
       
   598 
       
   599   $form['pager'] = array('#markup' => theme('pager'));
       
   600   return $form;
       
   601 }
       
   602 
       
   603 /**
       
   604  * Validate node_admin_nodes form submissions.
       
   605  *
       
   606  * Checks whether any nodes have been selected to perform the chosen 'Update
       
   607  * option' on.
       
   608  *
       
   609  * @see node_admin_nodes()
       
   610  * @see node_admin_nodes_submit()
       
   611  * @see node_filter_form()
       
   612  * @see node_filter_form_submit()
       
   613  * @see node_multiple_delete_confirm()
       
   614  * @see node_multiple_delete_confirm_submit()
       
   615  */
       
   616 function node_admin_nodes_validate($form, &$form_state) {
       
   617   // Error if there are no items to select.
       
   618   if (!is_array($form_state['values']['nodes']) || !count(array_filter($form_state['values']['nodes']))) {
       
   619     form_set_error('', t('No items selected.'));
       
   620   }
       
   621 }
       
   622 
       
   623 /**
       
   624  * Process node_admin_nodes form submissions.
       
   625  *
       
   626  * Executes the chosen 'Update option' on the selected nodes.
       
   627  *
       
   628  * @see node_admin_nodes()
       
   629  * @see node_admin_nodes_validate()
       
   630  * @see node_filter_form()
       
   631  * @see node_filter_form_submit()
       
   632  * @see node_multiple_delete_confirm()
       
   633  * @see node_multiple_delete_confirm_submit()
       
   634  */
       
   635 function node_admin_nodes_submit($form, &$form_state) {
       
   636   $operations = module_invoke_all('node_operations');
       
   637   $operation = $operations[$form_state['values']['operation']];
       
   638   // Filter out unchecked nodes
       
   639   $nodes = array_filter($form_state['values']['nodes']);
       
   640   if ($function = $operation['callback']) {
       
   641     // Add in callback arguments if present.
       
   642     if (isset($operation['callback arguments'])) {
       
   643       $args = array_merge(array($nodes), $operation['callback arguments']);
       
   644     }
       
   645     else {
       
   646       $args = array($nodes);
       
   647     }
       
   648     call_user_func_array($function, $args);
       
   649 
       
   650     cache_clear_all();
       
   651   }
       
   652   else {
       
   653     // We need to rebuild the form to go to a second step. For example, to
       
   654     // show the confirmation form for the deletion of nodes.
       
   655     $form_state['rebuild'] = TRUE;
       
   656   }
       
   657 }
       
   658 
       
   659 /**
       
   660  * Multiple node deletion confirmation form for node_admin_content().
       
   661  *
       
   662  * @see node_admin_nodes()
       
   663  * @see node_admin_nodes_submit()
       
   664  * @see node_admin_nodes_validate()
       
   665  * @see node_filter_form()
       
   666  * @see node_filter_form_submit()
       
   667  * @see node_multiple_delete_confirm_submit()
       
   668  * @ingroup forms
       
   669  */
       
   670 function node_multiple_delete_confirm($form, &$form_state, $nodes) {
       
   671   $form['nodes'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
       
   672   // array_filter returns only elements with TRUE values
       
   673   foreach ($nodes as $nid => $value) {
       
   674     $title = db_query('SELECT title FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchField();
       
   675     $form['nodes'][$nid] = array(
       
   676       '#type' => 'hidden',
       
   677       '#value' => $nid,
       
   678       '#prefix' => '<li>',
       
   679       '#suffix' => check_plain($title) . "</li>\n",
       
   680     );
       
   681   }
       
   682   $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
       
   683   $form['#submit'][] = 'node_multiple_delete_confirm_submit';
       
   684   $confirm_question = format_plural(count($nodes),
       
   685                                   'Are you sure you want to delete this item?',
       
   686                                   'Are you sure you want to delete these items?');
       
   687   return confirm_form($form,
       
   688                     $confirm_question,
       
   689                     'admin/content', t('This action cannot be undone.'),
       
   690                     t('Delete'), t('Cancel'));
       
   691 }
       
   692 
       
   693 /**
       
   694  * Form submission handler for node_multiple_delete_confirm().
       
   695  *
       
   696  * @see node_admin_nodes()
       
   697  * @see node_admin_nodes_submit()
       
   698  * @see node_admin_nodes_validate()
       
   699  * @see node_filter_form()
       
   700  * @see node_filter_form_submit()
       
   701  * @see node_multiple_delete_confirm()
       
   702  */
       
   703 function node_multiple_delete_confirm_submit($form, &$form_state) {
       
   704   if ($form_state['values']['confirm']) {
       
   705     node_delete_multiple(array_keys($form_state['values']['nodes']));
       
   706     cache_clear_all();
       
   707     $count = count($form_state['values']['nodes']);
       
   708     watchdog('content', 'Deleted @count posts.', array('@count' => $count));
       
   709     drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.'));
       
   710   }
       
   711   $form_state['redirect'] = 'admin/content';
       
   712 }