Spike PHPCoverage Details: node.module

Line #FrequencySource Line
1 <?php
2 // $Id: node.module,v 1.953 2008/04/01 19:45:21 dries Exp $
3 
4 /**
5  * @file
6  * The core that allows content to be submitted to the site. Modules and scripts may
7  * programmatically submit nodes using the usual form API pattern.
8  */
9 
10 define('NODE_NEW_LIMIT', time() - 30 * 24 * 60 * 60);
11 
12 define('NODE_BUILD_NORMAL', 0);
13 define('NODE_BUILD_PREVIEW', 1);
14 define('NODE_BUILD_SEARCH_INDEX', 2);
15 define('NODE_BUILD_SEARCH_RESULT', 3);
16 define('NODE_BUILD_RSS', 4);
17 define('NODE_BUILD_PRINT', 5);
18 
19 /**
20  * Implementation of hook_help().
21  */
22 function node_help($path, $arg) {
23   // Remind site administrators about the {node_access} table being flagged
24   // for rebuild. We don't need to issue the message on the confirm form, or
25   // while the rebuild is being processed.
26   if ($path != 'content/node-settings/rebuild' && $path != 'batch' && strpos($path, '#') === FALSE
27       && user_access('access administration pages') && node_access_needs_rebuild()) {
28     if ($path == 'admin/content/node-settings') {
29       $message = t('The content access permissions need to be rebuilt.');
30     }
31     else {
32       $message = t('The content access permissions need to be rebuilt. Please visit <a href="@node_access_rebuild">this page</a>.', array('@node_access_rebuild' => url('content/node-settings/rebuild')));
33     }
34     drupal_set_message($message, 'error');
35   }
36 
37   switch ($path) {
38     case 'help#node':
39       $output = '<p>'. t('The node module manages content on your site, and stores all posts (regardless of type) as a "node". In addition to basic publishing settings, including whether the post has been published, promoted to the site front page, or should remain present (or sticky) at the top of lists, the node module also records basic information about the author of a post. Optional revision control over edits is available. For additional functionality, the node module is often extended by other modules.') .'</p>';
40       $output .= '<p>'. t('Though each post on your site is a node, each post is also of a particular <a href="@content-type">content type</a>. <a href="@content-type">Content types</a> are used to define the characteristics of a post, including the title and description of the fields displayed on its add and edit pages. Each content type may have different default settings for <em>Publishing options</em> and other workflow controls. By default, the two content types in a standard Drupal installation are <em>Page</em> and <em>Article</em>. Use the <a href="@content-type">content types page</a> to add new or edit existing content types. Additional content types also become available as you enable additional core, contributed and custom modules.', array('@content-type' => url('admin/content/types'))) .'</p>';
41       $output .= '<p>'. t('The administrative <a href="@content">content page</a> allows you to review and manage your site content. The <a href="@post-settings">post settings page</a> sets certain options for the display of posts. The node module makes a number of permissions available for each content type, which may be set by role on the <a href="@permissions">permissions page</a>.', array('@content' => url('admin/content/node'), '@post-settings' => url('admin/content/node-settings'), '@permissions' => url('admin/user/permissions'))) .'</p>';
42       $output .= '<p>'. t('For more information, see the online handbook entry for <a href="@node">Node module</a>.', array('@node' => 'http://drupal.org/handbook/modules/node/')) .'</p>';
43       return $output;
44     case 'admin/content/node':
45       return ' '; // Return a non-null value so that the 'more help' link is shown.
46     case 'admin/content/types':
47       return '<p>'. t('Below is a list of all the content types on your site. All posts that exist on your site are instances of one of these content types.') .'</p>';
48     case 'admin/content/types/add':
49       return '<p>'. t('To create a new content type, enter the human-readable name, the machine-readable name, and all other relevant fields that are on this page. Once created, users of your site will be able to create posts that are instances of this content type.') .'</p>';
50     case 'node/%/revisions':
51       return '<p>'. t('The revisions let you track differences between multiple versions of a post.') .'</p>';
52     case 'node/%/edit':
53       $node = node_load($arg[1]);
54       $type = node_get_types('type', $node->type);
55       return (!empty($type->help) ? '<p>'. filter_xss_admin($type->help) .'</p>' : '');
56   }
57 
58   if ($arg[0] == 'node' && $arg[1] == 'add' && $arg[2]) {
59     $type = node_get_types('type', str_replace('-', '_', $arg[2]));
60     return (!empty($type->help) ? '<p>'. filter_xss_admin($type->help) .'</p>' : '');
61   }
62 }
63 
64 /**
65  * Implementation of hook_theme()
66  */
67 function node_theme() {
68   return array(
69     'node' => array(
70       'arguments' => array('node' => NULL, 'teaser' => FALSE, 'page' => FALSE),
71       'template' => 'node',
72     ),
73     'node_list' => array(
74       'arguments' => array('items' => NULL, 'title' => NULL),
75     ),
76     'node_search_admin' => array(
77       'arguments' => array('form' => NULL),
78     ),
79     'node_filter_form' => array(
80       'arguments' => array('form' => NULL),
81       'file' => 'node.admin.inc',
82     ),
83     'node_filters' => array(
84       'arguments' => array('form' => NULL),
85       'file' => 'node.admin.inc',
86     ),
87     'node_admin_nodes' => array(
88       'arguments' => array('form' => NULL),
89       'file' => 'node.admin.inc',
90     ),
91     'node_add_list' => array(
92       'arguments' => array('content' => NULL),
93       'file' => 'node.pages.inc',
94     ),
95     'node_form' => array(
96       'arguments' => array('form' => NULL),
97       'file' => 'node.pages.inc',
98     ),
99     'node_preview' => array(
100       'arguments' => array('node' => NULL),
101       'file' => 'node.pages.inc',
102     ),
103     'node_log_message' => array(
104       'arguments' => array('log' => NULL),
105     ),
106     'node_submitted' => array(
107       'arguments' => array('node' => NULL),
108     ),
109   );
110 }
111 
112 /**
113  * Implementation of hook_cron().
114  */
115 function node_cron() {
116   db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT);
117 }
118 
119 /**
120  * Gather a listing of links to nodes.
121  *
122  * @param $result
123  *   A DB result object from a query to fetch node objects. If your query
124  *   joins the <code>node_comment_statistics</code> table so that the
125  *   <code>comment_count</code> field is available, a title attribute will
126  *   be added to show the number of comments.
127  * @param $title
128  *   A heading for the resulting list.
129  *
130  * @return
131  *   An HTML list suitable as content for a block, or FALSE if no result can
132  *   fetch from DB result object.
133  */
134 function node_title_list($result, $title = NULL) {
135   $items = array();
136   $num_rows = FALSE;
137   while ($node = db_fetch_object($result)) {
138     $items[] = l($node->title, 'node/'. $node->nid, !empty($node->comment_count) ? array('title' => format_plural($node->comment_count, '1 comment', '@count comments')) : array());
139     $num_rows = TRUE;
140   }
141 
142   return $num_rows ? theme('node_list', $items, $title) : FALSE;
143 }
144 
145 /**
146  * Format a listing of links to nodes.
147  *
148  * @ingroup themeable
149  */
150 function theme_node_list($items, $title = NULL) {
151   return theme('item_list', $items, $title);
152 }
153 
154 /**
155  * Update the 'last viewed' timestamp of the specified node for current user.
156  */
157 function node_tag_new($nid) {
158   global $user;
159 
160   if ($user->uid) {
161     if (node_last_viewed($nid)) {
162       db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid);
163     }
164     else {
165       @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time());
166     }
167   }
168 }
169 
170 /**
171  * Retrieves the timestamp at which the current user last viewed the
172  * specified node.
173  */
174 function node_last_viewed($nid) {
175   global $user;
176   static $history;
177 
178   if (!isset($history[$nid])) {
179     $history[$nid] = db_fetch_object(db_query("SELECT timestamp FROM {history} WHERE uid = %d AND nid = %d", $user->uid, $nid));
180   }
181 
182   return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0);
183 }
184 
185 /**
186  * Decide on the type of marker to be displayed for a given node.
187  *
188  * @param $nid
189  *   Node ID whose history supplies the "last viewed" timestamp.
190  * @param $timestamp
191  *   Time which is compared against node's "last viewed" timestamp.
192  * @return
193  *   One of the MARK constants.
194  */
195 function node_mark($nid, $timestamp) {
196   global $user;
197   static $cache;
198 
199   if (!$user->uid) {
200     return MARK_READ;
201   }
202   if (!isset($cache[$nid])) {
203     $cache[$nid] = node_last_viewed($nid);
204   }
205   if ($cache[$nid] == 0 && $timestamp > NODE_NEW_LIMIT) {
206     return MARK_NEW;
207   }
208   elseif ($timestamp > $cache[$nid] && $timestamp > NODE_NEW_LIMIT) {
209     return MARK_UPDATED;
210   }
211   return MARK_READ;
212 }
213 
214 /**
215  * See if the user used JS to submit a teaser.
216  */
217 function node_teaser_js(&$form, &$form_state) {
218   if (isset($form['#post']['teaser_js'])) {
219     // Glue the teaser to the body.
220     if (trim($form_state['values']['teaser_js'])) {
221       // Space the teaser from the body
222       $body = trim($form_state['values']['teaser_js']) ."\r\n<!--break-->\r\n". trim($form_state['values']['body']);
223     }
224     else {
225       // Empty teaser, no spaces.
226       $body = '<!--break-->'. $form_state['values']['body'];
227     }
228     // Pass updated body value on to preview/submit form processing.
229     form_set_value($form['body'], $body, $form_state);
230     // Pass updated body value back onto form for those cases
231     // in which the form is redisplayed.
232     $form['body']['#value'] = $body;
233   }
234   return $form;
235 }
236 
237 /**
238  * Ensure value of "teaser_include" checkbox is consistent with other form data.
239  *
240  * This handles two situations in which an unchecked checkbox is rejected:
241  *
242  *   1. The user defines a teaser (summary) but it is empty;
243  *   2. The user does not define a teaser (summary) (in this case an
244  *      unchecked checkbox would cause the body to be empty, or missing
245  *      the auto-generated teaser).
246  *
247  * If JavaScript is active then it is used to force the checkbox to be
248  * checked when hidden, and so the second case will not arise.
249  *
250  * In either case a warning message is output.
251  */
252 function node_teaser_include_verify(&$form, &$form_state) {
253   $message = '';
254 
255   // $form['#post'] is set only when the form is built for preview/submit.
256   if (isset($form['#post']['body']) && isset($form_state['values']['teaser_include']) && !$form_state['values']['teaser_include']) {
257     // "teaser_include" checkbox is present and unchecked.
258     if (strpos($form_state['values']['body'], '<!--break-->') === 0) {
259       // Teaser is empty string.
260       $message = t('You specified that the summary should not be shown when this post is displayed in full view. This setting is ignored when the summary is empty.');
261     }
262     elseif (strpos($form_state['values']['body'], '<!--break-->') === FALSE) {
263       // Teaser delimiter is not present in the body.
264       $message = t('You specified that the summary should not be shown when this post is displayed in full view. This setting has been ignored since you have not defined a summary for the post. (To define a summary, insert the delimiter "&lt;!--break--&gt;" (without the quotes) in the Body of the post to indicate the end of the summary and the start of the main content.)');
265     }
266 
267     if (!empty($message)) {
268       drupal_set_message($message, 'warning');
269       // Pass new checkbox value on to preview/submit form processing.
270       form_set_value($form['teaser_include'], 1, $form_state);
271       // Pass new checkbox value back onto form for those cases
272       // in which form is redisplayed.
273       $form['teaser_include']['#value'] = 1;
274     }
275   }
276 
277   return $form;
278 }
279 
280 /**
281  * Generate a teaser for a node body.
282  *
283  * If the end of the teaser is not indicated using the <!--break--> delimiter
284  * then we generate the teaser automatically, trying to end it at a sensible
285  * place such as the end of a paragraph, a line break, or the end of a
286  * sentence (in that order of preference).
287  *
288  * @param $body
289  *   The content for which a teaser will be generated.
290  * @param $format
291  *   The format of the content. If the content contains PHP code, we do not
292  *   split it up to prevent parse errors. If the line break filter is present
293  *   then we treat newlines embedded in $body as line breaks.
294  * @param $size
295  *   The desired character length of the teaser. If omitted, the default
296  *   value will be used. Ignored if the special delimiter is present
297  *   in $body.
298  * @return
299  *   The generated teaser.
300  */
301 function node_teaser($body, $format = NULL, $size = NULL) {
302 
303   if (!isset($size)) {
304     $size = variable_get('teaser_length', 600);
305   }
306 
307   // Find where the delimiter is in the body
308   $delimiter = strpos($body, '<!--break-->');
309 
310   // If the size is zero, and there is no delimiter, the entire body is the teaser.
311   if ($size == 0 && $delimiter === FALSE) {
312     return $body;
313   }
314 
315   // If a valid delimiter has been specified, use it to chop off the teaser.
316   if ($delimiter !== FALSE) {
317     return substr($body, 0, $delimiter);
318   }
319 
320   // We check for the presence of the PHP evaluator filter in the current
321   // format. If the body contains PHP code, we do not split it up to prevent
322   // parse errors.
323   if (isset($format)) {
324     $filters = filter_list_format($format);
325     if (isset($filters['php/0']) && strpos($body, '<?') !== FALSE) {
326       return $body;
327     }
328   }
329 
330   // If we have a short body, the entire body is the teaser.
331   if (drupal_strlen($body) <= $size) {
332     return $body;
333   }
334 
335   // If the delimiter has not been specified, try to split at paragraph or
336   // sentence boundaries.
337 
338   // The teaser may not be longer than maximum length specified. Initial slice.
339   $teaser = truncate_utf8($body, $size);
340 
341   // Store the actual length of the UTF8 string -- which might not be the same
342   // as $size.
343   $max_rpos = strlen($teaser);
344 
345   // How much to cut off the end of the teaser so that it doesn't end in the
346   // middle of a paragraph, sentence, or word.
347   // Initialize it to maximum in order to find the minimum.
348   $min_rpos = $max_rpos;
349 
350   // Store the reverse of the teaser.  We use strpos on the reversed needle and
351   // haystack for speed and convenience.
352   $reversed = strrev($teaser);
353 
354   // Build an array of arrays of break points grouped by preference.
355   $break_points = array();
356 
357   // A paragraph near the end of sliced teaser is most preferable.
358   $break_points[] = array('</p>' => 0);
359 
360   // If no complete paragraph then treat line breaks as paragraphs.
361   $line_breaks = array('<br />' => 6, '<br>' => 4);
362   // Newline only indicates a line break if line break converter
363   // filter is present.
364   if (isset($filters['filter/1'])) {
365     $line_breaks["\n"] = 1;
366   }
367   $break_points[] = $line_breaks;
368 
369   // If the first paragraph is too long, split at the end of a sentence.
370   $break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, 'ØŸ ' => 1);
371 
372   // Iterate over the groups of break points until a break point is found.
373   foreach ($break_points as $points) {
374     // Look for each break point, starting at the end of the teaser.
375     foreach ($points as $point => $offset) {
376       // The teaser is already reversed, but the break point isn't.
377       $rpos = strpos($reversed, strrev($point));
378       if ($rpos !== FALSE) {
379         $min_rpos = min($rpos + $offset, $min_rpos);
380       }
381     }
382 
383     // If a break point was found in this group, slice and return the teaser.
384     if ($min_rpos !== $max_rpos) {
385       // Don't slice with length 0.  Length must be <0 to slice from RHS.
386       return ($min_rpos === 0) ? $teaser : substr($teaser, 0, 0 - $min_rpos);
387     }
388   }
389 
390   // If a break point was not found, still return a teaser.
391   return $teaser;
392 }
393 
394 /**
395  * Builds a list of available node types, and returns all of part of this list
396  * in the specified format.
397  *
398  * @param $op
399  *   The format in which to return the list. When this is set to 'type',
400  *   'module', or 'name', only the specified node type is returned. When set to
401  *   'types' or 'names', all node types are returned.
402  * @param $node
403  *   A node object, array, or string that indicates the node type to return.
404  *   Leave at default value (NULL) to return a list of all node types.
405  * @param $reset
406  *   Whether or not to reset this function's internal cache (defaults to
407  *   FALSE).
408  *
409  * @return
410  *   Either an array of all available node types, or a single node type, in a
411  *   variable format. Returns FALSE if the node type is not found.
412  */
413 function node_get_types($op = 'types', $node = NULL, $reset = FALSE) {
4141  static $_node_types, $_node_names;
415 
4161  if ($reset || !isset($_node_types)) {
4171    list($_node_types, $_node_names) = _node_types_build();
418   }
419 
4201  if ($node) {
421     if (is_array($node)) {
422       $type = $node['type'];
423     }
424     elseif (is_object($node)) {
425       $type = $node->type;
426     }
427     elseif (is_string($node)) {
428       $type = $node;
429     }
430     if (!isset($_node_types[$type])) {
431       return FALSE;
432     }
433   }
434   switch ($op) {
4351    case 'types':
4361      return $_node_types;
437     case 'type':
438       return isset($_node_types[$type]) ? $_node_types[$type] : FALSE;
439     case 'module':
440       return isset($_node_types[$type]->module) ? $_node_types[$type]->module : FALSE;
441     case 'names':
442       return $_node_names;
443     case 'name':
444       return isset($_node_names[$type]) ? $_node_names[$type] : FALSE;
445   }
446 }
447 
448 /**
449  * Resets the database cache of node types, and saves all new or non-modified
450  * module-defined node types to the database.
451  */
452 function node_types_rebuild() {
453   _node_types_build();
454 
455   $node_types = node_get_types('types', NULL, TRUE);
456 
457   foreach ($node_types as $type => $info) {
458     if (!empty($info->is_new)) {
459       node_type_save($info);
460     }
461     if (!empty($info->disabled)) {
462       node_type_delete($info->type);
463     }
464   }
465 
466   _node_types_build();
467 }
468 
469 /**
470  * Saves a node type to the database.
471  *
472  * @param $info
473  *   The node type to save, as an object.
474  *
475  * @return
476  *   Status flag indicating outcome of the operation.
477  */
478 function node_type_save($info) {
4791  $is_existing = FALSE;
4801  $existing_type = !empty($info->old_type) ? $info->old_type : $info->type;
4811  $is_existing = db_result(db_query("SELECT COUNT(*) FROM {node_type} WHERE type = '%s'", $existing_type));
4821  if (!isset($info->help)) {
483     $info->help = '';
484   }
4851  if (!isset($info->min_word_count)) {
486     $info->min_word_count = 0;
487   }
4881  if (!isset($info->body_label)) {
489     $info->body_label = '';
490   }
491 
4921  if ($is_existing) {
493     db_query("UPDATE {node_type} SET type = '%s', name = '%s', module = '%s', has_title = %d, title_label = '%s', has_body = %d, body_label = '%s', description = '%s', help = '%s', min_word_count = %d, custom = %d, modified = %d, locked = %d WHERE type = '%s'", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $existing_type);
494 
495     module_invoke_all('node_type', 'update', $info);
496     return SAVED_UPDATED;
497   }
498   else {
4991    db_query("INSERT INTO {node_type} (type, name, module, has_title, title_label, has_body, body_label, description, help, min_word_count, custom, modified, locked, orig_type) VALUES ('%s', '%s', '%s', %d, '%s', %d, '%s', '%s', '%s', %d, %d, %d, %d, '%s')", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $info->orig_type);
500 
5011    module_invoke_all('node_type', 'insert', $info);
5021    return SAVED_NEW;
503   }
504 }
505 
506 /**
507  * Deletes a node type from the database.
508  *
509  * @param $type
510  *   The machine-readable name of the node type to be deleted.
511  */
512 function node_type_delete($type) {
513   $info = node_get_types('type', $type);
514   db_query("DELETE FROM {node_type} WHERE type = '%s'", $type);
515   module_invoke_all('node_type', 'delete', $info);
516 }
517 
518 /**
519  * Updates all nodes of one type to be of another type.
520  *
521  * @param $old_type
522  *   The current node type of the nodes.
523  * @param $type
524  *   The new node type of the nodes.
525  *
526  * @return
527  *   The number of nodes whose node type field was modified.
528  */
529 function node_type_update_nodes($old_type, $type) {
530   db_query("UPDATE {node} SET type = '%s' WHERE type = '%s'", $type, $old_type);
531   return db_affected_rows();
532 }
533 
534 /**
535  * Builds and returns the list of available node types.
536  *
537  * The list of types is built by querying hook_node_info() in all modules, and
538  * by comparing this information with the node types in the {node_type} table.
539  *
540  */
541 function _node_types_build() {
542   $_node_types = array();
543   $_node_names = array();
544 
5451  $info_array = module_invoke_all('node_info');
5461  foreach ($info_array as $type => $info) {
547     $info['type'] = $type;
548     $_node_types[$type] = (object) _node_type_set_defaults($info);
549     $_node_names[$type] = $info['name'];
550   }
551 
5521  $type_result = db_query(db_rewrite_sql('SELECT nt.type, nt.* FROM {node_type} nt ORDER BY nt.type ASC', 'nt', 'type'));
5531  while ($type_object = db_fetch_object($type_result)) {
554     // Check for node types from disabled modules and mark their types for removal.
555     // Types defined by the node module in the database (rather than by a separate
556     // module using hook_node_info) have a module value of 'node'.
5571    if ($type_object->module != 'node' && empty($info_array[$type_object->type])) {
558       $type_object->disabled = TRUE;
559     }
5601    if (!isset($_node_types[$type_object->type]) || $type_object->modified) {
5611      $_node_types[$type_object->type] = $type_object;
5621      $_node_names[$type_object->type] = $type_object->name;
563 
5641      if ($type_object->type != $type_object->orig_type) {
565         unset($_node_types[$type_object->orig_type]);
566         unset($_node_names[$type_object->orig_type]);
567       }
568     }
569   }
570 
5711  asort($_node_names);
572 
5731  return array($_node_types, $_node_names);
574 }
575 
576 /**
577  * Set default values for a node type defined through hook_node_info().
578  */
579 function _node_type_set_defaults($info) {
5801  if (!isset($info['has_title'])) {
5811    $info['has_title'] = TRUE;
582   }
5831  if ($info['has_title'] && !isset($info['title_label'])) {
5841    $info['title_label'] = t('Title');
585   }
586 
5871  if (!isset($info['has_body'])) {
5881    $info['has_body'] = TRUE;
589   }
5901  if ($info['has_body'] && !isset($info['body_label'])) {
5911    $info['body_label'] = t('Body');
592   }
593 
5941  if (!isset($info['help'])) {
595     $info['help'] = '';
596   }
5971  if (!isset($info['min_word_count'])) {
598     $info['min_word_count'] = 0;
599   }
6001  if (!isset($info['custom'])) {
601     $info['custom'] = FALSE;
602   }
6031  if (!isset($info['modified'])) {
604     $info['modified'] = FALSE;
605   }
6061  if (!isset($info['locked'])) {
607     $info['locked'] = TRUE;
608   }
609 
6101  $info['orig_type'] = $info['type'];
6111  $info['is_new'] = TRUE;
612 
6131  return $info;
614 }
615 
616 /**
617  * Determine whether a node hook exists.
618  *
619  * @param &$node
620  *   Either a node object, node array, or a string containing the node type.
621  * @param $hook
622  *   A string containing the name of the hook.
623  * @return
624  *   TRUE iff the $hook exists in the node type of $node.
625  */
626 function node_hook(&$node, $hook) {
627   $module = node_get_types('module', $node);
628   if ($module == 'node') {
629     $module = 'node_content'; // Avoid function name collisions.
630   }
631   return module_hook($module, $hook);
632 }
633 
634 /**
635  * Invoke a node hook.
636  *
637  * @param &$node
638  *   Either a node object, node array, or a string containing the node type.
639  * @param $hook
640  *   A string containing the name of the hook.
641  * @param $a2, $a3, $a4
642  *   Arguments to pass on to the hook, after the $node argument.
643  * @return
644  *   The returned value of the invoked hook.
645  */
646 function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
647   if (node_hook($node, $hook)) {
648     $module = node_get_types('module', $node);
649     if ($module == 'node') {
650       $module = 'node_content'; // Avoid function name collisions.
651     }
652     $function = $module .'_'. $hook;
653     return ($function($node, $a2, $a3, $a4));
654   }
655 }
656 
657 /**
658  * Invoke a hook_nodeapi() operation in all modules.
659  *
660  * @param &$node
661  *   A node object.
662  * @param $op
663  *   A string containing the name of the nodeapi operation.
664  * @param $a3, $a4
665  *   Arguments to pass on to the hook, after the $node and $op arguments.
666  * @return
667  *   The returned value of the invoked hooks.
668  */
669 function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
670   $return = array();
671   foreach (module_implements('nodeapi') as $name) {
672     $function = $name .'_nodeapi';
673     $result = $function($node, $op, $a3, $a4);
674     if (isset($result) && is_array($result)) {
675       $return = array_merge($return, $result);
676     }
677     else if (isset($result)) {
678       $return[] = $result;
679     }
680   }
681   return $return;
682 }
683 
684 /**
685  * Load a node object from the database.
686  *
687  * @param $param
688  *   Either the nid of the node or an array of conditions to match against in the database query
689  * @param $revision
690  *   Which numbered revision to load. Defaults to the current version.
691  * @param $reset
692  *   Whether to reset the internal node_load cache.
693  *
694  * @return
695  *   A fully-populated node object.
696  */
697 function node_load($param = array(), $revision = NULL, $reset = NULL) {
698   static $nodes = array();
699 
700   if ($reset) {
701     $nodes = array();
702   }
703 
704   $cachable = ($revision == NULL);
705   $arguments = array();
706   if (is_numeric($param)) {
707     if ($cachable) {
708       // Is the node statically cached?
709       if (isset($nodes[$param])) {
710         return is_object($nodes[$param]) ? clone $nodes[$param] : $nodes[$param];
711       }
712     }
713     $cond = 'n.nid = %d';
714     $arguments[] = $param;
715   }
716   elseif (is_array($param)) {
717     // Turn the conditions into a query.
718     foreach ($param as $key => $value) {
719       $cond[] = 'n.'. db_escape_string($key) ." = '%s'";
720       $arguments[] = $value;
721     }
722     $cond = implode(' AND ', $cond);
723   }
724   else {
725     return FALSE;
726   }
727 
728   // Retrieve a field list based on the site's schema.
729   $fields = drupal_schema_fields_sql('node', 'n');
730   $fields = array_merge($fields, drupal_schema_fields_sql('node_revisions', 'r'));
731   $fields = array_merge($fields, array('u.name', 'u.picture', 'u.data'));
732   // Remove fields not needed in the query: n.vid and r.nid are redundant,
733   // n.title is unnecessary because the node title comes from the
734   // node_revisions table.  We'll keep r.vid, r.title, and n.nid.
735   $fields = array_diff($fields, array('n.vid', 'n.title', 'r.nid'));
736   $fields = implode(', ', $fields);
737   // Rename timestamp field for clarity.
738   $fields = str_replace('r.timestamp', 'r.timestamp AS revision_timestamp', $fields);
739   // Change name of revision uid so it doesn't conflict with n.uid.
740   $fields = str_replace('r.uid', 'r.uid AS revision_uid', $fields);
741 
742   // Retrieve the node.
743   // No db_rewrite_sql is applied so as to get complete indexing for search.
744   if ($revision) {
745     array_unshift($arguments, $revision);
746     $node = db_fetch_object(db_query('SELECT '. $fields .' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.nid = n.nid AND r.vid = %d WHERE '. $cond, $arguments));
747   }
748   else {
749     $node = db_fetch_object(db_query('SELECT '. $fields .' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.vid = n.vid WHERE '. $cond, $arguments));
750   }
751 
752   if ($node && $node->nid) {
753     // Call the node specific callback (if any) and piggy-back the
754     // results to the node or overwrite some values.
755     if ($extra = node_invoke($node, 'load')) {
756       foreach ($extra as $key => $value) {
757         $node->$key = $value;
758       }
759     }
760 
761     if ($extra = node_invoke_nodeapi($node, 'load')) {
762       foreach ($extra as $key => $value) {
763         $node->$key = $value;
764       }
765     }
766     if ($cachable) {
767       $nodes[$node->nid] = is_object($node) ? clone $node : $node;
768     }
769   }
770 
771   return $node;
772 }
773 
774 /**
775  * Perform validation checks on the given node.
776  */
777 function node_validate($node, $form = array()) {
778   // Convert the node to an object, if necessary.
779   $node = (object)$node;
780   $type = node_get_types('type', $node);
781 
782   // Make sure the body has the minimum number of words.
783   // TODO : use a better word counting algorithm that will work in other languages
784   if (!empty($type->min_word_count) && isset($node->body) && count(explode(' ', $node->body)) < $type->min_word_count) {
785     form_set_error('body', t('The body of your @type is too short. You need at least %words words.', array('%words' => $type->min_word_count, '@type' => $type->name)));
786   }
787 
788   if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) {
789     form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.'));
790   }
791 
792   if (user_access('administer nodes')) {
793     // Validate the "authored by" field.
794     if (!empty($node->name) && !($account = user_load(array('name' => $node->name)))) {
795       // The use of empty() is mandatory in the context of usernames
796       // as the empty string denotes the anonymous user. In case we
797       // are dealing with an anonymous user we set the user ID to 0.
798       form_set_error('name', t('The username %name does not exist.', array('%name' => $node->name)));
799     }
800 
801     // Validate the "authored on" field. As of PHP 5.1.0, strtotime returns FALSE instead of -1 upon failure.
802     if (!empty($node->date) && strtotime($node->date) <= 0) {
803       form_set_error('date', t('You have to specify a valid date.'));
804     }
805   }
806 
807   // Do node-type-specific validation checks.
808   node_invoke($node, 'validate', $form);
809   node_invoke_nodeapi($node, 'validate', $form);
810 }
811 
812 /**
813  * Prepare node for save and allow modules to make changes.
814  */
815 function node_submit($node) {
816   global $user;
817 
818   // Convert the node to an object, if necessary.
819   $node = (object)$node;
820 
821   // Generate the teaser, but only if it hasn't been set (e.g. by a
822   // module-provided 'teaser' form item).
823   if (!isset($node->teaser)) {
824     if (isset($node->body)) {
825       $node->teaser = node_teaser($node->body, isset($node->format) ? $node->format : NULL);
826       // Chop off the teaser from the body if needed. The teaser_include
827       // property might not be set (eg. in Blog API postings), so only act on
828       // it, if it was set with a given value.
829       if (isset($node->teaser_include) && !$node->teaser_include && $node->teaser == substr($node->body, 0, strlen($node->teaser))) {
830         $node->body = substr($node->body, strlen($node->teaser));
831       }
832     }
833     else {
834       $node->teaser = '';
835     }
836   }
837 
838   if (user_access('administer nodes')) {
839     // Populate the "authored by" field.
840     if ($account = user_load(array('name' => $node->name))) {
841       $node->uid = $account->uid;
842     }
843     else {
844       $node->uid = 0;
845     }
846   }
847   $node->created = !empty($node->date) ? strtotime($node->date) : time();
848   $node->validated = TRUE;
849 
850   return $node;
851 }
852 
853 /**
854  * Save a node object into the database.
855  */
856 function node_save(&$node) {
857   // Let modules modify the node before it is saved to the database.
858   node_invoke_nodeapi($node, 'presave');
859   global $user;
860 
861   $node->is_new = FALSE;
862 
863   // Apply filters to some default node fields:
864   if (empty($node->nid)) {
865     // Insert a new node.
866     $node->is_new = TRUE;
867 
868     // When inserting a node, $node->log must be set because
869     // {node_revisions}.log does not (and cannot) have a default
870     // value.  If the user does not have permission to create
871     // revisions, however, the form will not contain an element for
872     // log so $node->log will be unset at this point.
873     if (!isset($node->log)) {
874       $node->log = '';
875     }
876 
877     // For the same reasons, make sure we have $node->teaser and
878     // $node->body.  We should consider making these fields nullable
879     // in a future version since node types are not required to use them.
880     if (!isset($node->teaser)) {
881       $node->teaser = '';
882     }
883     if (!isset($node->body)) {
884       $node->body = '';
885     }
886   }
887   elseif (!empty($node->revision)) {
888     $node->old_vid = $node->vid;
889   }
890   else {
891     // When updating a node, avoid clobberring an existing log entry with an empty one.
892     if (empty($node->log)) {
893       unset($node->log);
894     }
895   }
896 
897   // Set some required fields:
898   if (empty($node->created)) {
899     $node->created = time();
900   }
901   // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
902   $node->changed = time();
903 
904   $node->timestamp = time();
905   $node->format = isset($node->format) ? $node->format : FILTER_FORMAT_DEFAULT;
906   $update_node = TRUE;
907 
908   // Generate the node table query and the node_revisions table query.
909   if ($node->is_new) {
910     drupal_write_record('node', $node);
911     _node_save_revision($node, $user->uid);
912     $op = 'insert';
913   }
914   else {
915     drupal_write_record('node', $node, 'nid');
916     if (!empty($node->revision)) {
917       _node_save_revision($node, $user->uid);
918     }
919     else {
920       _node_save_revision($node, $user->uid, 'vid');
921       $update_node = FALSE;
922     }
923     $op = 'update';
924   }
925   if ($update_node) {
926     db_query('UPDATE {node} SET vid = %d WHERE nid = %d', $node->vid, $node->nid);
927   }
928 
929   // Call the node specific callback (if any).
930   node_invoke($node, $op);
931   node_invoke_nodeapi($node, $op);
932 
933   // Update the node access table for this node.
934   node_access_acquire_grants($node);
935 
936   // Clear the page and block caches.
937   cache_clear_all();
938 }
939 
940 /**
941  * Helper function to save a revision with the uid of the current user.
942  *
943  * Node is taken by reference, becuse drupal_write_record() updates the
944  * $node with the revision id, and we need to pass that back to the caller.
945  */
946 function _node_save_revision(&$node, $uid, $update = NULL) {
947   $temp_uid = $node->uid;
948   $node->uid = $uid;
949   if (isset($update)) {
950     drupal_write_record('node_revisions', $node, $update);
951   }
952   else {
953     drupal_write_record('node_revisions', $node);
954   }
955   $node->uid = $temp_uid;
956 }
957 
958 /**
959  * Delete a node.
960  */
961 function node_delete($nid) {
962 
963   $node = node_load($nid);
964 
965   if (node_access('delete', $node)) {
966     db_query('DELETE FROM {node} WHERE nid = %d', $node->nid);
967     db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid);
968 
969     // Call the node-specific callback (if any):
970     node_invoke($node, 'delete');
971     node_invoke_nodeapi($node, 'delete');
972 
973     // Clear the page and block caches.
974     cache_clear_all();
975 
976     // Remove this node from the search index if needed.
977     if (function_exists('search_wipe')) {
978       search_wipe($node->nid, 'node');
979     }
980     watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title));
981     drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
982   }
983 }
984 
985 /**
986  * Generate a display of the given node.
987  *
988  * @param $node
989  *   A node array or node object.
990  * @param $teaser
991  *   Whether to display the teaser only or the full form.
992  * @param $page
993  *   Whether the node is being displayed by itself as a page.
994  * @param $links
995  *   Whether or not to display node links. Links are omitted for node previews.
996  *
997  * @return
998  *   An HTML representation of the themed node.
999  */
1000 function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) {
1001   $node = (object)$node;
1002 
1003   $node = node_build_content($node, $teaser, $page);
1004 
1005   if ($links) {
1006     $node->links = module_invoke_all('link', 'node', $node, $teaser);
1007     drupal_alter('link', $node->links, $node);
1008   }
1009 
1010   // Set the proper node part, then unset unused $node part so that a bad
1011   // theme can not open a security hole.
1012   $content = drupal_render($node->content);
1013   if ($teaser) {
1014     $node->teaser = $content;
1015     unset($node->body);
1016   }
1017   else {
1018     $node->body = $content;
1019     unset($node->teaser);
1020   }
1021 
1022   // Allow modules to modify the fully-built node.
1023   node_invoke_nodeapi($node, 'alter', $teaser, $page);
1024 
1025   return theme('node', $node, $teaser, $page);
1026 }
1027 
1028 /**
1029  * Apply filters and build the node's standard elements.
1030  */
1031 function node_prepare($node, $teaser = FALSE) {
1032   // First we'll overwrite the existing node teaser and body with
1033   // the filtered copies! Then, we'll stick those into the content
1034   // array and set the read more flag if appropriate.
1035   $node->readmore = (strlen($node->teaser) < strlen($node->body));
1036 
1037   if ($teaser == FALSE) {
1038     $node->body = check_markup($node->body, $node->format, FALSE);
1039   }
1040   else {
1041     $node->teaser = check_markup($node->teaser, $node->format, FALSE);
1042   }
1043 
1044   $node->content['body'] = array(
1045     '#value' => $teaser ? $node->teaser : $node->body,
1046     '#weight' => 0,
1047   );
1048 
1049   return $node;
1050 }
1051 
1052 /**
1053  * Builds a structured array representing the node's content.
1054  *
1055  * @param $node
1056  *   A node object.
1057  * @param $teaser
1058  *   Whether to display the teaser only, as on the main page.
1059  * @param $page
1060  *   Whether the node is being displayed by itself as a page.
1061  *
1062  * @return
1063  *   An structured array containing the individual elements
1064  *   of the node's body.
1065  */
1066 function node_build_content($node, $teaser = FALSE, $page = FALSE) {
1067 
1068   // The build mode identifies the target for which the node is built.
1069   if (!isset($node->build_mode)) {
1070     $node->build_mode = NODE_BUILD_NORMAL;
1071   }
1072 
1073   // Remove the delimiter (if any) that separates the teaser from the body.
1074   $node->body = isset($node->body) ? str_replace('<!--break-->', '', $node->body) : '';
1075 
1076   // The 'view' hook can be implemented to overwrite the default function
1077   // to display nodes.
1078   if (node_hook($node, 'view')) {
1079     $node = node_invoke($node, 'view', $teaser, $page);
1080   }
1081   else {
1082     $node = node_prepare($node, $teaser);
1083   }
1084 
1085   // Allow modules to make their own additions to the node.
1086   node_invoke_nodeapi($node, 'view', $teaser, $page);
1087 
1088   return $node;
1089 }
1090 
1091 /**
1092  * Generate a page displaying a single node, along with its comments.
1093  */
1094 function node_show($node, $cid, $message = FALSE) {
1095   if ($message) {
1096     drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))));
1097   }
1098   $output = node_view($node, FALSE, TRUE);
1099 
1100   if (function_exists('comment_render') && $node->comment) {
1101     $output .= comment_render($node, $cid);
1102   }
1103 
1104   // Update the history table, stating that this user viewed this node.
1105   node_tag_new($node->nid);
1106 
1107   return $output;
1108 }
1109 
1110 /**
1111  * Theme a log message.
1112  *
1113  * @ingroup themeable
1114  */
1115 function theme_node_log_message($log) {
1116   return '<div class="log"><div class="title">'. t('Log') .':</div>'. $log .'</div>';
1117 }
1118 
1119 /**
1120  * Implementation of hook_perm().
1121  */
1122 function node_perm() {
1123   $perms = array(
1124     'administer content types' => t('Manage content types and content type administration settings.'),
1125     'administer nodes' => t('Manage all website content, and bypass any content-related access control. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
1126     'access content' => t('View published content.'),
1127     'view revisions' => t('View content revisions.'),
1128     'revert revisions' => t('Replace content with an older revision.'),
1129     'delete revisions' => t('Delete content revisions.'),
1130   );
1131 
1132   foreach (node_get_types() as $type) {
1133     if ($type->module == 'node') {
1134       $perms += node_list_permissions($type);
1135     }
1136   }
1137 
1138   return $perms;
1139 }
1140 
1141 /**
1142  * Implementation of hook_search().
1143  */
1144 function node_search($op = 'search', $keys = NULL) {
1145   switch ($op) {
1146     case 'name':
1147       return t('Content');
1148 
1149     case 'reset':
1150       db_query("UPDATE {search_dataset} SET reindex = %d WHERE type = 'node'", time());
1151       return;
1152 
1153     case 'status':
1154       $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
1155       $remaining = db_result(db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0"));
1156       return array('remaining' => $remaining, 'total' => $total);
1157 
1158     case 'admin':
1159       $form = array();
1160       // Output form for defining rank factor weights.
1161       $form['content_ranking'] = array(
1162         '#type' => 'fieldset',
1163         '#title' => t('Content ranking'),
1164       );
1165       $form['content_ranking']['#theme'] = 'node_search_admin';
1166       $form['content_ranking']['info'] = array(
1167         '#value' => '<em>'. t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') .'</em>'
1168       );
1169 
1170       $ranking = array('node_rank_relevance' => t('Keyword relevance'),
1171                        'node_rank_recent' => t('Recently posted'));
1172       if (module_exists('comment')) {
1173         $ranking['node_rank_comments'] = t('Number of comments');
1174       }
1175       if (module_exists('statistics') && variable_get('statistics_count_content_views', 0)) {
1176         $ranking['node_rank_views'] = t('Number of views');
1177       }
1178 
1179       // Note: reversed to reflect that higher number = higher ranking.
1180       $options = drupal_map_assoc(range(0, 10));
1181       foreach ($ranking as $var => $title) {
1182         $form['content_ranking']['factors'][$var] = array(
1183           '#title' => $title,
1184           '#type' => 'select',
1185           '#options' => $options,
1186           '#default_value' => variable_get($var, 5),
1187         );
1188       }
1189       return $form;
1190 
1191     case 'search':
1192       // Build matching conditions
1193       list($join1, $where1) = _db_rewrite_sql();
1194       $arguments1 = array();
1195       $conditions1 = 'n.status = 1';
1196 
1197       if ($type = search_query_extract($keys, 'type')) {
1198         $types = array();
1199         foreach (explode(',', $type) as $t) {
1200           $types[] = "n.type = '%s'";
1201           $arguments1[] = $t;
1202         }
1203         $conditions1 .= ' AND ('. implode(' OR ', $types) .')';
1204         $keys = search_query_insert($keys, 'type');
1205       }
1206 
1207       if ($category = search_query_extract($keys, 'category')) {
1208         $categories = array();
1209         foreach (explode(',', $category) as $c) {
1210           $categories[] = "tn.tid = %d";
1211           $arguments1[] = $c;
1212         }
1213         $conditions1 .= ' AND ('. implode(' OR ', $categories) .')';
1214         $join1 .= ' INNER JOIN {term_node} tn ON n.vid = tn.vid';
1215         $keys = search_query_insert($keys, 'category');
1216       }
1217 
1218       // Build ranking expression (we try to map each parameter to a
1219       // uniform distribution in the range 0..1).
1220       $ranking = array();
1221       $arguments2 = array();
1222       $join2 = '';
1223       // Used to avoid joining on node_comment_statistics twice
1224       $stats_join = FALSE;
1225       $total = 0;
1226       if ($weight = (int)variable_get('node_rank_relevance', 5)) {
1227         // Average relevance values hover around 0.15
1228         $ranking[] = '%d * i.relevance';
1229         $arguments2[] = $weight;
1230         $total += $weight;
1231       }
1232       if ($weight = (int)variable_get('node_rank_recent', 5)) {
1233         // Exponential decay with half-life of 6 months, starting at last indexed node
1234         $ranking[] = '%d * POW(2, (GREATEST(MAX(n.created), MAX(n.changed), MAX(c.last_comment_timestamp)) - %d) * 6.43e-8)';
1235         $arguments2[] = $weight;
1236         $arguments2[] = (int)variable_get('node_cron_last', 0);
1237         $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
1238         $stats_join = TRUE;
1239         $total += $weight;
1240       }
1241       if (module_exists('comment') && $weight = (int)variable_get('node_rank_comments', 5)) {
1242         // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
1243         $scale = variable_get('node_cron_comments_scale', 0.0);
1244         $ranking[] = '%d * (2.0 - 2.0 / (1.0 + MAX(c.comment_count) * %f))';
1245         $arguments2[] = $weight;
1246         $arguments2[] = $scale;
1247         if (!$stats_join) {
1248           $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
1249         }
1250         $total += $weight;
1251       }
1252       if (module_exists('statistics') && variable_get('statistics_count_content_views', 0) &&
1253           $weight = (int)variable_get('node_rank_views', 5)) {
1254         // Inverse law that maps the highest view count on the site to 1 and 0 to 0.
1255         $scale = variable_get('node_cron_views_scale', 0.0);
1256         $ranking[] = '%d * (2.0 - 2.0 / (1.0 + MAX(nc.totalcount) * %f))';
1257         $arguments2[] = $weight;
1258         $arguments2[] = $scale;
1259         $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid';
1260         $total += $weight;
1261       }
1262       $select2 = (count($ranking) ? implode(' + ', $ranking) : 'i.relevance') .' AS score';
1263 
1264       // Do search
1265       $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid '. $join1 .' INNER JOIN {users} u ON n.uid = u.uid', $conditions1 . (empty($where1) ? '' : ' AND '. $where1), $arguments1, $select2, $join2, $arguments2);
1266 
1267       // Load results
1268       $results = array();
1269       foreach ($find as $item) {
1270         // Build the node body.
1271         $node = node_load($item->sid);
1272         $node->build_mode = NODE_BUILD_SEARCH_RESULT;
1273         $node = node_build_content($node, FALSE, FALSE);
1274         $node->body = drupal_render($node->content);
1275 
1276         // Fetch comments for snippet
1277         $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
1278         // Fetch terms for snippet
1279         $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
1280 
1281         $extra = node_invoke_nodeapi($node, 'search result');
1282         $results[] = array(
1283           'link' => url('node/'. $item->sid, array('absolute' => TRUE)),
1284           'type' => check_plain(node_get_types('name', $node)),
1285           'title' => $node->title,
1286           'user' => theme('username', $node),
1287           'date' => $node->changed,
1288           'node' => $node,
1289           'extra' => $extra,
1290           'score' => $item->score / $total,
1291           'snippet' => search_excerpt($keys, $node->body),
1292         );
1293       }
1294       return $results;
1295   }
1296 }
1297 
1298 /**
1299  * Implementation of hook_user().
1300  */
1301 function node_user($op, &$edit, &$user) {
13021  if ($op == 'delete') {
1303     db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid);
1304     db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid);
1305   }
1306 }
1307 
1308 /**
1309  * Theme the content ranking part of the search settings admin page.
1310  *
1311  * @ingroup themeable
1312  */
1313 function theme_node_search_admin($form) {
1314   $output = drupal_render($form['info']);
1315 
1316   $header = array(t('Factor'), t('Weight'));
1317   foreach (element_children($form['factors']) as $key) {
1318     $row = array();
1319     $row[] = $form['factors'][$key]['#title'];
1320     unset($form['factors'][$key]['#title']);
1321     $row[] = drupal_render($form['factors'][$key]);
1322     $rows[] = $row;
1323   }
1324   $output .= theme('table', $header, $rows);
1325 
1326   $output .= drupal_render($form);
1327   return $output;
1328 }
1329 
1330 /**
1331  * Retrieve the comment mode for the given node ID (none, read, or read/write).
1332  */
1333 function node_comment_mode($nid) {
1334   static $comment_mode;
1335   if (!isset($comment_mode[$nid])) {
1336     $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid));
1337   }
1338   return $comment_mode[$nid];
1339 }
1340 
1341 /**
1342  * Implementation of hook_link().
1343  */
1344 function node_link($type, $node = NULL, $teaser = FALSE) {
1345   $links = array();
1346 
1347   if ($type == 'node') {
1348     if ($teaser == 1 && $node->teaser && !empty($node->readmore)) {
1349       $links['node_read_more'] = array(
1350         'title' => t('Read more'),
1351         'href' => "node/$node->nid",
1352         // The title attribute gets escaped when the links are processed, so
1353         // there is no need to escape here.
1354         'attributes' => array('title' => t('Read the rest of !title.', array('!title' => $node->title)))
1355       );
1356     }
1357   }
1358 
1359   return $links;
1360 }
1361 
1362 function _node_revision_access($node, $op = 'view') {
1363   static $access = array();
1364   if (!isset($access[$node->vid])) {
1365     $node_current_revision = node_load($node->nid);
1366     $is_current_revision = $node_current_revision->vid == $node->vid;
1367     // There should be at least two revisions. If the vid of the given node
1368     // and the vid of the current revision differs, then we already have two
1369     // different revisions so there is no need for a separate database check.
1370     // Also, if you try to revert to or delete the current revision, that's
1371     // not good.
1372     if ($is_current_revision && (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $node->nid)) == 1 || $op == 'update' || $op == 'delete')) {
1373       $access[$node->vid] = FALSE;
1374     }
1375     elseif (user_access('administer nodes')) {
1376       $access[$node->vid] = TRUE;
1377     }
1378     else {
1379       $map = array('view' => 'view revisions', 'update' => 'revert revisions', 'delete' => 'delete revisions');
1380       // First check the user permission, second check the access to the
1381       // current revision and finally, if the node passed in is not the current
1382       // revision then access to that, too.
1383       $access[$node->vid] = isset($map[$op]) && user_access($map[$op]) && node_access($op, $node_current_revision) && ($is_current_revision || node_access($op, $node));
1384     }
1385   }
1386   return $access[$node->vid];
1387 }
1388 
1389 function _node_add_access() {
1390   $types = node_get_types();
1391   foreach ($types as $type) {
1392     if (node_hook($type->type, 'form') && node_access('create', $type->type)) {
1393       return TRUE;
1394     }
1395   }
1396   return FALSE;
1397 }
1398 
1399 /**
1400  * Implementation of hook_menu().
1401  */
1402 function node_menu() {
1403   $items['admin/content/node'] = array(
14041    'title' => 'Content',
1405     'description' => "View, edit, and delete your site's content.",
1406     'page callback' => 'drupal_get_form',
1407     'page arguments' => array('node_admin_content'),
1408     'access arguments' => array('administer nodes'),
1409     'file' => 'node.admin.inc',
1410   );
1411 
1412   $items['admin/content/node/overview'] = array(
14131    'title' => 'List',
1414     'type' => MENU_DEFAULT_LOCAL_TASK,
1415     'weight' => -10,
1416   );
1417 
1418   $items['admin/content/node-settings'] = array(
14191    'title' => 'Post settings',
1420     'description' => 'Control posting behavior, such as teaser length, requiring previews before posting, and the number of posts on the front page.',
1421     'page callback' => 'drupal_get_form',
1422     'page arguments' => array('node_configure'),
1423     'access arguments' => array('administer nodes'),
1424     'file' => 'node.admin.inc',
1425   );
1426   $items['content/node-settings/rebuild'] = array(
14271    'title' => 'Rebuild permissions',
1428     'page arguments' => array('node_configure_rebuild_confirm'),
1429     'file' => 'node.admin.inc',
1430     // Any user than can potentially trigger a node_acess_needs_rebuild(TRUE)
1431     // has to be allowed access to the 'node access rebuild' confirm form.
14321    'access arguments' => array('access administration pages'),
1433     'type' => MENU_CALLBACK,
1434   );
1435 
1436   $items['admin/content/types'] = array(
14371    'title' => 'Content types',
1438     'description' => 'Manage posts by content type, including default status, front page promotion, etc.',
1439     'page callback' => 'node_overview_types',
1440     'access arguments' => array('administer content types'),
1441     'file' => 'content_types.inc',
1442   );
1443   $items['admin/content/types/list'] = array(
14441    'title' => 'List',
1445     'type' => MENU_DEFAULT_LOCAL_TASK,
1446     'weight' => -10,
1447   );
1448   $items['admin/content/types/add'] = array(
14491    'title' => 'Add content type',
1450     'page callback' => 'drupal_get_form',
1451     'page arguments' => array('node_type_form'),
1452     'file' => 'content_types.inc',
1453     'type' => MENU_LOCAL_TASK,
1454   );
1455   $items['node'] = array(
14561    'title' => 'Content',
1457     'page callback' => 'node_page_default',
1458     'access arguments' => array('access content'),
1459     'type' => MENU_CALLBACK,
1460   );
1461   $items['node/add'] = array(
14621    'title' => 'Create content',
1463     'page callback' => 'node_add_page',
1464     'access callback' => '_node_add_access',
1465     'weight' => 1,
1466     'file' => 'node.pages.inc',
1467   );
1468   $items['rss.xml'] = array(
14691    'title' => 'RSS feed',
1470     'page callback' => 'node_feed',
1471     'access arguments' => array('access content'),
1472     'type' => MENU_CALLBACK,
1473   );
14741  foreach (node_get_types('types', NULL, TRUE) as $type) {
14751    $type_url_str = str_replace('_', '-', $type->type);
1476     $items['node/add/'. $type_url_str] = array(
14771      'title' => drupal_ucfirst($type->name),
1478       'title callback' => 'check_plain',
1479       'page callback' => 'node_add',
1480       'page arguments' => array(2),
1481       'access callback' => 'node_access',
1482       'access arguments' => array('create', $type->type),
1483       'description' => $type->description,
1484       'file' => 'node.pages.inc',
1485     );
1486     $items['admin/content/node-type/'. $type_url_str] = array(
14871      'title' => $type->name,
1488       'page callback' => 'drupal_get_form',
1489       'page arguments' => array('node_type_form', $type),
1490       'file' => 'content_types.inc',
1491       'type' => MENU_CALLBACK,
1492     );
1493     $items['admin/content/node-type/'. $type_url_str .'/edit'] = array(
14941      'title' => 'Edit',
1495       'type' => MENU_DEFAULT_LOCAL_TASK,
1496     );
1497     $items['admin/content/node-type/'. $type_url_str .'/delete'] = array(
14981      'title' => 'Delete',
1499       'page arguments' => array('node_type_delete_confirm', $type),
1500       'file' => 'content_types.inc',
1501       'type' => MENU_CALLBACK,
1502     );
1503   }
1504   $items['node/%node'] = array(
15051    'title callback' => 'node_page_title',
1506     'title arguments' => array(1),
1507     'page callback' => 'node_page_view',
1508     'page arguments' => array(1),
1509     'access callback' => 'node_access',
1510     'access arguments' => array('view', 1),
1511     'type' => MENU_CALLBACK);
1512   $items['node/%node/view'] = array(
15131    'title' => 'View',
1514     'type' => MENU_DEFAULT_LOCAL_TASK,
1515     'weight' => -10);
1516   $items['node/%node/edit'] = array(
15171    'title' => 'Edit',
1518     'page callback' => 'node_page_edit',
1519     'page arguments' => array(1),
1520     'access callback' => 'node_access',
1521     'access arguments' => array('update', 1),
1522     'weight' => 1,
1523     'file' => 'node.pages.inc',
1524     'type' => MENU_LOCAL_TASK,
1525   );
1526   $items['node/%node/delete'] = array(
15271    'title' => 'Delete',
1528     'page callback' => 'drupal_get_form',
1529     'page arguments' => array('node_delete_confirm', 1),
1530     'access callback' => 'node_access',
1531     'access arguments' => array('delete', 1),
1532     'file' => 'node.pages.inc',
1533     'weight' => 1,
1534     'type' => MENU_CALLBACK);
1535   $items['node/%node/revisions'] = array(
15361    'title' => 'Revisions',
1537     'page callback' => 'node_revision_overview',
1538     'page arguments' => array(1),
1539     'access callback' => '_node_revision_access',
1540     'access arguments' => array(1),
1541     'weight' => 2,
1542     'file' => 'node.pages.inc',
1543     'type' => MENU_LOCAL_TASK,
1544   );
1545   $items['node/%node/revisions/%/view'] = array(
15461    'title' => 'Revisions',
1547     'load arguments' => array(3),
1548     'page callback' => 'node_show',
1549     'page arguments' => array(1, NULL, TRUE),
1550     'type' => MENU_CALLBACK,
1551   );
1552   $items['node/%node/revisions/%/revert'] = array(
15531    'title' => 'Revert to earlier revision',
1554     'load arguments' => array(3),
1555     'page callback' => 'drupal_get_form',
1556     'page arguments' => array('node_revision_revert_confirm', 1),
1557     'access callback' => '_node_revision_access',
1558     'access arguments' => array(1, 'update'),
1559     'file' => 'node.pages.inc',
1560     'type' => MENU_CALLBACK,
1561   );
1562   $items['node/%node/revisions/%/delete'] = array(
15631    'title' => 'Delete earlier revision',
1564     'load arguments' => array(3),
1565     'page callback' => 'drupal_get_form',
1566     'page arguments' => array('node_revision_delete_confirm', 1),
1567     'access callback' => '_node_revision_access',
1568     'access arguments' => array(1, 'delete'),
1569     'file' => 'node.pages.inc',
1570     'type' => MENU_CALLBACK,
1571   );
15721  return $items;
1573 }
1574 
1575 /**
1576  * Title callback.
1577  */
1578 function node_page_title($node) {
1579   return $node->title;
1580 }
1581 
1582 /**
1583  * Implementation of hook_init().
1584  */
1585 function node_init() {
1586   drupal_add_css(drupal_get_path('module', 'node') .'/node.css');
1587 }
1588 
1589 function node_last_changed($nid) {
1590   $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
1591   return ($node->changed);
1592 }
1593 
1594 /**
1595  * Return a list of all the existing revision numbers.
1596  */
1597 function node_revision_list($node) {
1598   $revisions = array();
1599   $result = db_query('SELECT r.vid, r.title, r.log, r.uid, n.vid AS current_vid, r.timestamp, u.name FROM {node_revisions} r LEFT JOIN {node} n ON n.vid = r.vid INNER JOIN {users} u ON u.uid = r.uid WHERE r.nid = %d ORDER BY r.timestamp DESC', $node->nid);
1600   while ($revision = db_fetch_object($result)) {
1601     $revisions[$revision->vid] = $revision;
1602   }
1603 
1604   return $revisions;
1605 }
1606 
1607 /**
1608  * Implementation of hook_block().
1609  */
1610 function node_block($op = 'list', $delta = 0) {
1611   if ($op == 'list') {
1612     $blocks[0]['info'] = t('Syndicate');
1613     // Not worth caching.
1614     $blocks[0]['cache'] = BLOCK_NO_CACHE;
1615     return $blocks;
1616   }
1617   else if ($op == 'view') {
1618     $block['subject'] = t('Syndicate');
1619     $block['content'] = theme('feed_icon', url('rss.xml'), t('Syndicate'));
1620 
1621     return $block;
1622   }
1623 }
1624 
1625 /**
1626  * A generic function for generating RSS feeds from a set of nodes.
1627  *
1628  * @param $nids
1629  *   An array of node IDs (nid). Defaults to FALSE so empty feeds can be
1630  *   generated with passing an empty array, if no items are to be added
1631  *   to the feed.
1632  * @param $channel
1633  *   An associative array containing title, link, description and other keys.
1634  *   The link should be an absolute URL.
1635  */
1636 function node_feed($nids = FALSE, $channel = array()) {
1637   global $base_url, $language;
1638 
1639   if ($nids === FALSE) {
1640     $nids = array();
1641     $result = db_query_range(db_rewrite_sql('SELECT n.nid, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.created DESC'), 0, variable_get('feed_default_items', 10));
1642     while ($row = db_fetch_object($result)) {
1643       $nids[] = $row->nid;
1644     }
1645   }
1646 
1647   $item_length = variable_get('feed_item_length', 'teaser');
1648   $namespaces = array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/');
1649 
1650   $items = '';
1651   foreach ($nids as $nid) {
1652     // Load the specified node:
1653     $item = node_load($nid);
1654     $item->build_mode = NODE_BUILD_RSS;
1655     $item->link = url($nid", array('absolute' => TRUE));
1656 
1657     if ($item_length != 'title') {
1658       $teaser = ($item_length == 'teaser') ? TRUE : FALSE;
1659 
1660       // Filter and prepare node teaser
1661       if (node_hook($item, 'view')) {
1662         $item = node_invoke($item, 'view', $teaser, FALSE);
1663       }
1664       else {
1665         $item = node_prepare($item, $teaser);
1666       }
1667 
1668       // Allow modules to change $node->teaser before viewing.
1669       node_invoke_nodeapi($item, 'view', $teaser, FALSE);
1670     }
1671 
1672     // Allow modules to add additional item fields and/or modify $item
1673     $extra = node_invoke_nodeapi($item, 'rss item');
1674     $extra = array_merge($extra, array(array('key' => 'pubDate', 'value' => format_date($item->created, 'custom', 'r')), array('key' => 'dc:creator', 'value' => $item->name), array('key' => 'guid', 'value' => $item->nid .' at '. $base_url, 'attributes' => array('isPermaLink' => 'false'))));
1675     foreach ($extra as $element) {
1676       if (isset($element['namespace'])) {
1677         $namespaces = array_merge($namespaces, $element['namespace']);
1678       }
1679     }
1680 
1681     // Prepare the item description
1682     switch ($item_length) {
1683       case 'fulltext':
1684         $item_text = $item->body;
1685         break;
1686       case 'teaser':
1687         $item_text = $item->teaser;
1688         if (!empty($item->readmore)) {
1689           $item_text .= '<p>'. l(t('read more'), 'node/'. $item->nid, array('absolute' => TRUE, 'attributes' => array('target' => '_blank'))) .'</p>';
1690         }
1691         break;
1692       case 'title':
1693         $item_text = '';
1694         break;
1695     }
1696 
1697     $items .= format_rss_item($item->title, $item->link, $item_text, $extra);
1698   }
1699 
1700   $channel_defaults = array(
1701     'version'     => '2.0',
1702     'title'       => variable_get('site_name', 'Drupal'),
1703     'link'        => $base_url,
1704     'description' => variable_get('site_mission', ''),
1705     'language'    => $language->language
1706   );
1707   $channel = array_merge($channel_defaults, $channel);
1708 
1709   $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1710   $output .= "<rss version=\"". $channel["version"] ."\" xml:base=\"". $base_url ."\" ". drupal_attributes($namespaces) .">\n";
1711   $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']);
1712   $output .= "</rss>\n";
1713 
1714   drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
1715   print $output;
1716 }
1717 
1718 /**
1719  * Menu callback; Generate a listing of promoted nodes.
1720  */
1721 function node_page_default() {
1722   $result = pager_query(db_rewrite_sql('SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), variable_get('default_nodes_main', 10));
1723 
1724   $output = '';
1725   $num_rows = FALSE;
1726   while ($node = db_fetch_object($result)) {
1727     $output .= node_view(node_load($node->nid), 1);
1728     $num_rows = TRUE;
1729   }
1730 
1731   if ($num_rows) {
1732     $feed_url = url('rss.xml', array('absolute' => TRUE));
1733     drupal_add_feed($feed_url, variable_get('site_name', 'Drupal') .' '. t('RSS'));
1734     $output .= theme('pager', NULL, variable_get('default_nodes_main', 10));
1735   }
1736   else {
1737     $default_message = '<h1 class="title">'. t('Welcome to your new Drupal website!') .'</h1>';
1738     $default_message .= '<p>'. t('Please follow these steps to set up and start using your website:') .'</p>';
1739     $default_message .= '<ol>';
1740     $default_message .= '<li>'. t('<strong>Configure your website</strong> Once logged in, visit the <a href="@admin">administration section</a>, where you can <a href="@config">customize and configure</a> all aspects of your website.', array('@admin' => url('admin'), '@config' => url('settings'))) .'</li>';
1741     $default_message .= '<li>'. t('<strong>Enable additional functionality</strong> Next, visit the <a href="@modules">module list</a> and enable features which suit your specific needs. You can find additional modules in the <a href="@download_modules">Drupal modules download section</a>.', array('@modules' => url('admin/build/modules'), '@download_modules' => 'http://drupal.org/project/modules')) .'</li>';
1742     $default_message .= '<li>'. t('<strong>Customize your website design</strong> To change the "look and feel" of your website, visit the <a href="@themes">themes section</a>. You may choose from one of the included themes or download additional themes from the <a href="@download_themes">Drupal themes download section</a>.', array('@themes' => url('admin/build/themes'), '@download_themes' => 'http://drupal.org/project/themes')) .'</li>';
1743     $default_message .= '<li>'. t('<strong>Start posting content</strong> Finally, you can <a href="@content">create content</a> for your website. This message will disappear once you have promoted a post to the front page.', array('@content' => url('node/add'))) .'</li>';
1744     $default_message .= '</ol>';
1745     $default_message .= '<p>'. t('For more information, please refer to the <a href="@help">help section</a>, or the <a href="@handbook">online Drupal handbooks</a>. You may also post at the <a href="@forum">Drupal forum</a>, or view the wide range of <a href="@support">other support options</a> available.', array('@help' => url('help'), '@handbook' => 'http://drupal.org/handbooks', '@forum' => 'http://drupal.org/forum', '@support' => 'http://drupal.org/support')) .'</p>';
1746 
1747     $output = '<div id="first-time">'. $default_message .'</div>';
1748   }
1749   drupal_set_title('');
1750 
1751   return $output;
1752 }
1753 
1754 /**
1755  * Menu callback; view a single node.
1756  */
1757 function node_page_view($node, $cid = NULL) {
1758   drupal_set_title(check_plain($node->title));
1759   return node_show($node, $cid);
1760 }
1761 
1762 /**
1763  * Implementation of hook_update_index().
1764  */
1765 function node_update_index() {
1766   $limit = (int)variable_get('search_cron_limit', 100);
1767 
1768   // Store the maximum possible comments per thread (used for ranking by reply count)
1769   variable_set('node_cron_comments_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}'))));
1770   variable_set('node_cron_views_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(totalcount) FROM {node_counter}'))));
1771 
1772   $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit);
1773 
1774   while ($node = db_fetch_object($result)) {
1775     _node_index_node($node);
1776   }
1777 }
1778 
1779 /**
1780  * Index a single node.
1781  *
1782  * @param $node
1783  *   The node to index.
1784  */
1785 function _node_index_node($node) {
1786   $node = node_load($node->nid);
1787 
1788   // save the changed time of the most recent indexed node, for the search results half-life calculation
1789   variable_set('node_cron_last', $node->changed);
1790 
1791   // Build the node body.
1792   $node->build_mode = NODE_BUILD_SEARCH_INDEX;
1793   $node = node_build_content($node, FALSE, FALSE);
1794   $node->body = drupal_render($node->content);
1795 
1796   $text = '<h1>'. check_plain($node->title) .'</h1>'. $node->body;
1797 
1798   // Fetch extra data normally not visible
1799   $extra = node_invoke_nodeapi($node, 'update index');
1800   foreach ($extra as $t) {
1801     $text .= $t;
1802   }
1803 
1804   // Update index
1805   search_index($node->nid, 'node', $text);
1806 }
1807 
1808 /**
1809  * Implementation of hook_form_alter().
1810  */
1811 function node_form_alter(&$form, $form_state, $form_id) {
1812   // Advanced node search form
1813   if ($form_id == 'search_form' && $form['module']['#value'] == 'node' && user_access('use advanced search')) {
1814     // Keyword boxes:
1815     $form['advanced'] = array(
1816       '#type' => 'fieldset',
1817       '#title' => t('Advanced search'),
1818       '#collapsible' => TRUE,
1819       '#collapsed' => TRUE,
1820       '#attributes' => array('class' => 'search-advanced'),
1821     );
1822     $form['advanced']['keywords'] = array(
1823       '#prefix' => '<div class="criterion">',
1824       '#suffix' => '</div>',
1825     );
1826     $form['advanced']['keywords']['or'] = array(
1827       '#type' => 'textfield',
1828       '#title' => t('Containing any of the words'),
1829       '#size' => 30,
1830       '#maxlength' => 255,
1831     );
1832     $form['advanced']['keywords']['phrase'] = array(
1833       '#type' => 'textfield',
1834       '#title' => t('Containing the phrase'),
1835       '#size' => 30,
1836       '#maxlength' => 255,
1837     );
1838     $form['advanced']['keywords']['negative'] = array(
1839       '#type' => 'textfield',
1840       '#title' => t('Containing none of the words'),
1841       '#size' => 30,
1842       '#maxlength' => 255,
1843     );
1844 
1845     // Taxonomy box:
1846     if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
1847       $form['advanced']['category'] = array(
1848         '#type' => 'select',
1849         '#title' => t('Only in the category(s)'),
1850         '#prefix' => '<div class="criterion">',
1851         '#size' => 10,
1852         '#suffix' => '</div>',
1853         '#options' => $taxonomy,
1854         '#multiple' => TRUE,
1855       );
1856     }
1857 
1858     // Node types:
1859     $types = array_map('check_plain', node_get_types('names'));
1860     $form['advanced']['type'] = array(
1861       '#type' => 'checkboxes',
1862       '#title' => t('Only of the type(s)'),
1863       '#prefix' => '<div class="criterion">',
1864       '#suffix' => '</div>',
1865       '#options' => $types,
1866     );
1867     $form['advanced']['submit'] = array(
1868       '#type' => 'submit',
1869       '#value' => t('Advanced search'),
1870       '#prefix' => '<div class="action">',
1871       '#suffix' => '</div>',
1872     );
1873 
1874     $form['#validate'][] = 'node_search_validate';
1875   }
1876 }
1877 
1878 /**
1879  * Form API callback for the search form. Registered in node_form_alter().
1880  */
1881 function node_search_validate($form, &$form_state) {
1882   // Initialise using any existing basic search keywords.
1883   $keys = $form_state['values']['processed_keys'];
1884 
1885   // Insert extra restrictions into the search keywords string.
1886   if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) {
1887     // Retrieve selected types - Forms API sets the value of unselected checkboxes to 0.
1888     $form_state['values']['type'] = array_filter($form_state['values']['type']);
1889     if (count($form_state['values']['type'])) {
1890       $keys = search_query_insert($keys, 'type', implode(',', array_keys($form_state['values']['type'])));
1891     }
1892   }
1893 
1894   if (isset($form_state['values']['category']) && is_array($form_state['values']['category'])) {
1895     $keys = search_query_insert($keys, 'category', implode(',', $form_state['values']['category']));
1896   }
1897   if ($form_state['values']['or'] != '') {
1898     if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_state['values']['or'], $matches)) {
1899       $keys .= ' '. implode(' OR ', $matches[1]);
1900     }
1901   }
1902   if ($form_state['values']['negative'] != '') {
1903     if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_state['values']['negative'], $matches)) {
1904       $keys .= ' -'. implode(' -', $matches[1]);
1905     }
1906   }
1907   if ($form_state['values']['phrase'] != '') {
1908     $keys .= ' "'. str_replace('"', ' ', $form_state['values']['phrase']) .'"';
1909   }
1910   if (!empty($keys)) {
1911     form_set_value($form['basic']['inline']['processed_keys'], trim($keys), $form_state);
1912   }
1913 }
1914 
1915 /**
1916  * @defgroup node_access Node access rights
1917  * @{
1918  * The node access system determines who can do what to which nodes.
1919  *
1920  * In determining access rights for a node, node_access() first checks
1921  * whether the user has the "administer nodes" permission. Such users have
1922  * unrestricted access to all nodes. Then the node module's hook_access()
1923  * is called, and a TRUE or FALSE return value will grant or deny access.
1924  * This allows, for example, the blog module to always grant access to the
1925  * blog author, and for the book module to always deny editing access to
1926  * PHP pages.
1927  *
1928  * If node module does not intervene (returns NULL), then the
1929  * node_access table is used to determine access. All node access
1930  * modules are queried using hook_node_grants() to assemble a list of
1931  * "grant IDs" for the user. This list is compared against the table.
1932  * If any row contains the node ID in question (or 0, which stands for "all
1933  * nodes"), one of the grant IDs returned, and a value of TRUE for the
1934  * operation in question, then access is granted. Note that this table is a
1935  * list of grants; any matching row is sufficient to grant access to the
1936  * node.
1937  *
1938  * In node listings, the process above is followed except that
1939  * hook_access() is not called on each node for performance reasons and for
1940  * proper functioning of the pager system. When adding a node listing to your
1941  * module, be sure to use db_rewrite_sql() to add
1942  * the appropriate clauses to your query for access checks.
1943  *
1944  * To see how to write a node access module of your own, see
1945  * node_access_example.module.
1946  */
1947 
1948 /**
1949  * Determine whether the current user may perform the given operation on the
1950  * specified node.
1951  *
1952  * @param $op
1953  *   The operation to be performed on the node. Possible values are:
1954  *   - "view"
1955  *   - "update"
1956  *   - "delete"
1957  *   - "create"
1958  * @param $node
1959  *   The node object (or node array) on which the operation is to be performed,
1960  *   or node type (e.g. 'forum') for "create" operation.
1961  * @param $account
1962  *   Optional, a user object representing the user for whom the operation is to
1963  *   be performed. Determines access for a user other than the current user.
1964  * @return
1965  *   TRUE if the operation may be performed.
1966  */
1967 function node_access($op, $node, $account = NULL) {
1968   global $user;
1969 
1970   if (!$node) {
1971     return FALSE;
1972   }
1973   // Convert the node to an object if necessary:
1974   if ($op != 'create') {
1975     $node = (object)$node;
1976   }
1977   // If no user object is supplied, the access check is for the current user.
1978   if (empty($account)) {
1979     $account = $user;
1980   }
1981   // If the node is in a restricted format, disallow editing.
1982   if ($op == 'update' && !filter_access($node->format)) {
1983     return FALSE;
1984   }
1985 
1986   if (user_access('administer nodes', $account)) {
1987     return TRUE;
1988   }
1989 
1990   if (!user_access('access content', $account)) {
1991     return FALSE;
1992   }
1993 
1994   // Can't use node_invoke(), because the access hook takes the $op parameter
1995   // before the $node parameter.
1996   $module = node_get_types('module', $node);
1997   if ($module == 'node') {
1998     $module = 'node_content'; // Avoid function name collisions.
1999   }
2000   $access = module_invoke($module, 'access', $op, $node, $account);
2001   if (!is_null($access)) {
2002     return $access;
2003   }
2004 
2005   // If the module did not override the access rights, use those set in the
2006   // node_access table.
2007   if ($op != 'create' && $node->nid && $node->status) {
2008     $grants = array();
2009     foreach (node_access_grants($op, $account) as $realm => $gids) {
2010       foreach ($gids as $gid) {
2011         $grants[] = "(gid = $gid AND realm = '$realm')";
2012       }
2013     }
2014 
2015     $grants_sql = '';
2016     if (count($grants)) {
2017       $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
2018     }
2019 
2020     $sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1";
2021     $result = db_query($sql, $node->nid);
2022     return (db_result($result));
2023   }
2024 
2025   // Let authors view their own nodes.
2026   if ($op == 'view' && $account->uid == $node->uid && $account->uid != 0) {
2027     return TRUE;
2028   }
2029 
2030   return FALSE;
2031 }
2032 
2033 /**
2034  * Generate an SQL join clause for use in fetching a node listing.
2035  *
2036  * @param $node_alias
2037  *   If the node table has been given an SQL alias other than the default
2038  *   "n", that must be passed here.
2039  * @param $node_access_alias
2040  *   If the node_access table has been given an SQL alias other than the default
2041  *   "na", that must be passed here.
2042  * @return
2043  *   An SQL join clause.
2044  */
2045 function _node_access_join_sql($node_alias = 'n', $node_access_alias = 'na') {
2046   if (user_access('administer nodes')) {
2047     return '';
2048   }
2049 
2050   return 'INNER JOIN {node_access} '. $node_access_alias .' ON '. $node_access_alias .'.nid = '. $node_alias .'.nid';
2051 }
2052 
2053 /**
2054  * Generate an SQL where clause for use in fetching a node listing.
2055  *
2056  * @param $op
2057  *   The operation that must be allowed to return a node.
2058  * @param $node_access_alias
2059  *   If the node_access table has been given an SQL alias other than the default
2060  *   "na", that must be passed here.
2061  * @param $account
2062  *   The user object for the user performing the operation. If omitted, the
2063  *   current user is used.
2064  * @return
2065  *   An SQL where clause.
2066  */
2067 function _node_access_where_sql($op = 'view', $node_access_alias = 'na', $account = NULL) {
2068   if (user_access('administer nodes')) {
2069     return;
2070   }
2071 
2072   $grants = array();
2073   foreach (node_access_grants($op, $account) as $realm => $gids) {
2074     foreach ($gids as $gid) {
2075       $grants[] = "($node_access_alias.gid = $gid AND $node_access_alias.realm = '$realm')";
2076     }
2077   }
2078 
2079   $grants_sql = '';
2080   if (count($grants)) {
2081     $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
2082   }
2083 
2084   $sql = "$node_access_alias.grant_$op >= 1 $grants_sql";
2085   return $sql;
2086 }
2087 
2088 /**
2089  * Fetch an array of permission IDs granted to the given user ID.
2090  *
2091  * The implementation here provides only the universal "all" grant. A node
2092  * access module should implement hook_node_grants() to provide a grant
2093  * list for the user.
2094  *
2095  * @param $op
2096  *   The operation that the user is trying to perform.
2097  * @param $account
2098  *   The user object for the user performing the operation. If omitted, the
2099  *   current user is used.
2100  * @return
2101  *   An associative array in which the keys are realms, and the values are
2102  *   arrays of grants for those realms.
2103  */
2104 function node_access_grants($op, $account = NULL) {
2105 
2106   if (!isset($account)) {
2107     $account = $GLOBALS['user'];
2108   }
2109 
2110   return array_merge(array('all' => array(0)), module_invoke_all('node_grants', $account, $op));
2111 }
2112 
2113 /**
2114  * Determine whether the user has a global viewing grant for all nodes.
2115  */
2116 function node_access_view_all_nodes() {
2117   static $access;
2118 
2119   if (!isset($access)) {
2120     $grants = array();
2121     foreach (node_access_grants('view') as $realm => $gids) {
2122       foreach ($gids as $gid) {
2123         $grants[] = "(gid = $gid AND realm = '$realm')";
2124       }
2125     }
2126 
2127     $grants_sql = '';
2128     if (count($grants)) {
2129       $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
2130     }
2131 
2132     $sql = "SELECT COUNT(*) FROM {node_access} WHERE nid = 0 $grants_sql AND grant_view >= 1";
2133     $result = db_query($sql);
2134     $access = db_result($result);
2135   }
2136 
2137   return $access;
2138 }
2139 
2140 /**
2141  * Implementation of hook_db_rewrite_sql
2142  */
2143 function node_db_rewrite_sql($query, $primary_table, $primary_field) {
21441  if ($primary_field == 'nid' && !node_access_view_all_nodes()) {
2145     $return['join'] = _node_access_join_sql($primary_table);
2146     $return['where'] = _node_access_where_sql();
2147     $return['distinct'] = 1;
2148     return $return;
2149   }
2150 }
2151 
2152 /**
2153  * This function will call module invoke to get a list of grants and then
2154  * write them to the database. It is called at node save, and should be
2155  * called by modules whenever something other than a node_save causes
2156  * the permissions on a node to change.
2157  *
2158  * This function is the only function that should write to the node_access
2159  * table.
2160  *
2161  * @param $node
2162  *   The $node to acquire grants for.
2163  */
2164 function node_access_acquire_grants($node) {
2165   $grants = module_invoke_all('node_access_records', $node);
2166   if (empty($grants)) {
2167     $grants[] = array('realm' => 'all', 'gid' => 0, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0);
2168   }
2169   else {
2170     // retain grants by highest priority
2171     $grant_by_priority = array();
2172     foreach ($grants as $g) {
2173       $grant_by_priority[intval($g['priority'])][] = $g;
2174     }
2175     krsort($grant_by_priority);
2176     $grants = array_shift($grant_by_priority);
2177   }
2178 
2179   node_access_write_grants($node, $grants);
2180 }
2181 
2182 /**
2183  * This function will write a list of grants to the database, deleting
2184  * any pre-existing grants. If a realm is provided, it will only
2185  * delete grants from that realm, but it will always delete a grant
2186  * from the 'all' realm. Modules which utilize node_access can
2187  * use this function when doing mass updates due to widespread permission
2188  * changes.
2189  *
2190  * @param $node
2191  *   The $node being written to. All that is necessary is that it contain a nid.
2192  * @param $grants
2193  *   A list of grants to write. Each grant is an array that must contain the
2194  *   following keys: realm, gid, grant_view, grant_update, grant_delete.
2195  *   The realm is specified by a particular module; the gid is as well, and
2196  *   is a module-defined id to define grant privileges. each grant_* field
2197  *   is a boolean value.
2198  * @param $realm
2199  *   If provided, only read/write grants for that realm.
2200  * @param $delete
2201  *   If false, do not delete records. This is only for optimization purposes,
2202  *   and assumes the caller has already performed a mass delete of some form.
2203  */
2204 function node_access_write_grants($node, $grants, $realm = NULL, $delete = TRUE) {
2205   if ($delete) {
2206     $query = 'DELETE FROM {node_access} WHERE nid = %d';
2207     if ($realm) {
2208       $query .= " AND realm in ('%s', 'all')";
2209     }
2210     db_query($query, $node->nid, $realm);
2211   }
2212 
2213   // Only perform work when node_access modules are active.
2214   if (count(module_implements('node_grants'))) {
2215     foreach ($grants as $grant) {
2216       if ($realm && $realm != $grant['realm']) {
2217         continue;
2218       }
2219       // Only write grants; denies are implicit.
2220       if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) {
2221         db_query("INSERT INTO {node_access} (nid, realm, gid, grant_view, grant_update, grant_delete) VALUES (%d, '%s', %d, %d, %d, %d)", $node->nid, $grant['realm'], $grant['gid'], $grant['grant_view'], $grant['grant_update'], $grant['grant_delete']);
2222       }
2223     }
2224   }
2225 }
2226 
2227 /**
2228  * Flag / unflag the node access grants for rebuilding, or read the current
2229  * value of the flag.
2230  *
2231  * When the flag is set, a message is displayed to users with 'access
2232  * administration pages' permission, pointing to the 'rebuild' confirm form.
2233  * This can be used as an alternative to direct node_access_rebuild calls,
2234  * allowing administrators to decide when they want to perform the actual
2235  * (possibly time consuming) rebuild.
2236  * When unsure the current user is an adminisrator, node_access_rebuild
2237  * should be used instead.
2238  *
2239  * @param $rebuild
2240  *   (Optional) The boolean value to be written.
2241   * @return
2242  *   (If no value was provided for $rebuild) The current value of the flag.
2243  */
2244 function node_access_needs_rebuild($rebuild = NULL) {
22451  if (!isset($rebuild)) {
22461    return variable_get('node_access_needs_rebuild', FALSE);
2247   }
2248   elseif ($rebuild) {
2249     variable_set('node_access_needs_rebuild', TRUE);
2250   }
2251   else {
2252     variable_del('node_access_needs_rebuild');
2253   }
2254 }
2255 
2256 /**
2257  * Rebuild the node access database. This is occasionally needed by modules
2258  * that make system-wide changes to access levels.
2259  *
2260  * When the rebuild is required by an admin-triggered action (e.g module
2261  * settings form), calling node_access_needs_rebuild(TRUE) instead of
2262  * node_access_rebuild() lets the user perform his changes and actually
2263  * rebuild only once he is done.
2264  *
2265  * Note : As of Drupal 6, node access modules are not required to (and actually
2266  * should not) call node_access_rebuild() in hook_enable/disable anymore.
2267  *
2268  * @see node_access_needs_rebuild()
2269  *
2270  * @param $batch_mode
2271  *   Set to TRUE to process in 'batch' mode, spawning processing over several
2272  *   HTTP requests (thus avoiding the risk of PHP timeout if the site has a
2273  *   large number of nodes).
2274  *   hook_update_N and any form submit handler are safe contexts to use the
2275  *   'batch mode'. Less decidable cases (such as calls from hook_user,
2276  *   hook_taxonomy, hook_node_type...) might consider using the non-batch mode.
2277  */
2278 function node_access_rebuild($batch_mode = FALSE) {
2279   db_query("DELETE FROM {node_access}");
2280   // Only recalculate if the site is using a node_access module.
2281   if (count(module_implements('node_grants'))) {
2282     if ($batch_mode) {
2283       $batch = array(
2284         'title' => t('Rebuilding content access permissions'),
2285         'operations' => array(
2286           array('_node_access_rebuild_batch_operation', array()),
2287         ),
2288         'finished' => '_node_access_rebuild_batch_finished'
2289       );
2290       batch_set($batch);
2291     }
2292     else {
2293       // If not in 'safe mode', increase the maximum execution time.
2294       if (!ini_get('safe_mode')) {
2295         set_time_limit(240);
2296       }
2297       $result = db_query("SELECT nid FROM {node}");
2298       while ($node = db_fetch_object($result)) {
2299         $loaded_node = node_load($node->nid, NULL, TRUE);
2300         // To preserve database integrity, only aquire grants if the node
2301         // loads successfully.
2302         if (!empty($loaded_node)) {
2303           node_access_acquire_grants($loaded_node);
2304         }
2305       }
2306     }
2307   }
2308   else {
2309     // Not using any node_access modules. Add the default grant.
2310     db_query("INSERT INTO {node_access} VALUES (0, 0, 'all', 1, 0, 0)");
2311   }
2312 
2313   if (!isset($batch)) {
2314     drupal_set_message(t('Content permissions have been rebuilt.'));
2315     node_access_needs_rebuild(FALSE);
2316     cache_clear_all();
2317   }
2318 }
2319 
2320 /**
2321  * Batch operation for node_access_rebuild_batch.
2322  *
2323  * This is a mutlistep operation : we go through all nodes by packs of 20.
2324  * The batch processing engine interrupts processing and sends progress
2325  * feedback after 1 second execution time.
2326  */
2327 function _node_access_rebuild_batch_operation(&$context) {
2328   if (empty($context['sandbox'])) {
2329     // Initiate multistep processing.
2330     $context['sandbox']['progress'] = 0;
2331     $context['sandbox']['current_node'] = 0;
2332     $context['sandbox']['max'] = db_result(db_query('SELECT COUNT(DISTINCT nid) FROM {node}'));
2333   }
2334 
2335   // Process the next 20 nodes.
2336   $limit = 20;
2337   $result = db_query_range("SELECT nid FROM {node} WHERE nid > %d ORDER BY nid ASC", $context['sandbox']['current_node'], 0, $limit);
2338   while ($row = db_fetch_array($result)) {
2339     $loaded_node = node_load($row['nid'], NULL, TRUE);
2340     // To preserve database integrity, only aquire grants if the node
2341     // loads successfully.
2342     if (!empty($loaded_node)) {
2343       node_access_acquire_grants($loaded_node);
2344     }
2345     $context['sandbox']['progress']++;
2346     $context['sandbox']['current_node'] = $loaded_node->nid;
2347   }
2348 
2349   // Multistep processing : report progress.
2350   if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
2351     $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
2352   }
2353 }
2354 
2355 /**
2356  * Post-processing for node_access_rebuild_batch.
2357  */
2358 function _node_access_rebuild_batch_finished($success, $results, $operations) {
2359   if ($success) {
2360     drupal_set_message(t('The content access permissions have been rebuilt.'));
2361     node_access_needs_rebuild(FALSE);
2362   }
2363   else {
2364     drupal_set_message(t('The content access permissions have not been properly rebuilt.'), 'error');
2365   }
2366   cache_clear_all();
2367 }
2368 
2369 /**
2370  * @} End of "defgroup node_access".
2371  */
2372 
2373 
2374 /**
2375  * @defgroup node_content Hook implementations for user-created content types.
2376  * @{
2377  */
2378 
2379 /**
2380  * Implementation of hook_access().
2381  *
2382  * Named so as not to conflict with node_access()
2383  */
2384 function node_content_access($op, $node, $account) {
2385   $type = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type);
2386 
2387   if ($op == 'create') {
2388     return user_access('create '. $type .' content', $account);
2389   }
2390 
2391   if ($op == 'update') {
2392     if (user_access('edit any '. $type .' content', $account) || (user_access('edit own '. $type .' content', $account) && ($account->uid == $node->uid))) {
2393       return TRUE;
2394     }
2395   }
2396 
2397   if ($op == 'delete') {
2398     if (user_access('delete any '. $type .' content', $account) || (user_access('delete own '. $type .' content', $account) && ($account->uid == $node->uid))) {
2399       return TRUE;
2400     }
2401   }
2402 }
2403 
2404 /**
2405  * Implementation of hook_form().
2406  */
2407 function node_content_form($node, $form_state) {
2408   $type = node_get_types('type', $node);
2409   $form = array();
2410 
2411   if ($type->has_title) {
2412     $form['title'] = array(
2413       '#type' => 'textfield',
2414       '#title' => check_plain($type->title_label),
2415       '#required' => TRUE,
2416       '#default_value' => $node->title,
2417       '#maxlength' => 255,
2418       '#weight' => -5,
2419     );
2420   }
2421 
2422   if ($type->has_body) {
2423     $form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
2424   }
2425 
2426   return $form;
2427 }
2428 
2429 /**
2430  * @} End of "defgroup node_content".
2431  */
2432 
2433 /**
2434  * Implementation of hook_forms(). All node forms share the same form handler
2435  */
2436 function node_forms() {
2437   $forms = array();
2438   if ($types = node_get_types()) {
2439     foreach (array_keys($types) as $type) {
2440       $forms[$type .'_node_form']['callback'] = 'node_form';
2441     }
2442   }
2443   return $forms;
2444 }
2445 
2446 /**
2447  * Format the "Submitted by username on date/time" for each node
2448  *
2449  * @ingroup themeable
2450  */
2451 function theme_node_submitted($node) {
2452   return t('Submitted by !username on @datetime',
2453     array(
2454       '!username' => theme('username', $node),
2455       '@datetime' => format_date($node->created),
2456     ));
2457 }
2458 
2459 /**
2460  * Implementation of hook_hook_info().
2461  */
2462 function node_hook_info() {
2463   return array(
2464     'node' => array(
2465       'nodeapi' => array(
2466         'presave' => array(
2467           'runs when' => t('When either saving a new post or updating an existing post'),
2468         ),
2469         'insert' => array(
2470           'runs when' => t('After saving a new post'),
2471         ),
2472         'update' => array(
2473           'runs when' => t('After saving an updated post'),
2474         ),
2475         'delete' => array(
2476           'runs when' => t('After deleting a post')
2477         ),
2478         'view' => array(
2479           'runs when' => t('When content is viewed by an authenticated user')
2480         ),
2481       ),
2482     ),
2483   );
2484 }
2485 
2486 /**
2487  * Implementation of hook_action_info().
2488  */
2489 function node_action_info() {
2490   return array(
2491     'node_publish_action' => array(
24921      'type' => 'node',
2493       'description' => t('Publish post'),
2494       'configurable' => FALSE,
2495       'behavior' => array('changes_node_property'),
2496       'hooks' => array(
2497         'nodeapi' => array('presave'),
2498         'comment' => array('insert', 'update'),
2499       ),
2500     ),
2501     'node_unpublish_action' => array(
2502       'type' => 'node',
2503       'description' => t('Unpublish post'),
2504       'configurable' => FALSE,
2505       'behavior' => array('changes_node_property'),
2506       'hooks' => array(
2507         'nodeapi' => array('presave'),
2508         'comment' => array('delete', 'insert', 'update'),
2509       ),
2510     ),
2511     'node_make_sticky_action' => array(
2512       'type' => 'node',
2513       'description' => t('Make post sticky'),
2514       'configurable' => FALSE,
2515       'behavior' => array('changes_node_property'),
2516       'hooks' => array(
2517         'nodeapi' => array('presave'),
2518         'comment' => array('insert', 'update'),
2519       ),
2520     ),
2521     'node_make_unsticky_action' => array(
2522       'type' => 'node',
2523       'description' => t('Make post unsticky'),
2524       'configurable' => FALSE,
2525       'behavior' => array('changes_node_property'),
2526       'hooks' => array(
2527         'nodeapi' => array('presave'),
2528         'comment' => array('delete', 'insert', 'update'),
2529       ),
2530     ),
2531     'node_promote_action' => array(
2532       'type' => 'node',
2533       'description' => t('Promote post to front page'),
2534       'configurable' => FALSE,
2535       'behavior' => array('changes_node_property'),
2536       'hooks' => array(
2537         'nodeapi' => array('presave'),
2538         'comment' => array('insert', 'update'),
2539       ),
2540     ),
2541     'node_unpromote_action' => array(
2542       'type' => 'node',
2543       'description' => t('Remove post from front page'),
2544       'configurable' => FALSE,
2545       'behavior' => array('changes_node_property'),
2546       'hooks' => array(
2547         'nodeapi' => array('presave'),
2548         'comment' => array('delete', 'insert', 'update'),
2549       ),
2550     ),
2551     'node_assign_owner_action' => array(
2552       'type' => 'node',
2553       'description' => t('Change the author of a post'),
2554       'configurable' => TRUE,
2555       'behavior' => array('changes_node_property'),
2556       'hooks' => array(
2557         'any' => TRUE,
2558         'nodeapi' => array('presave'),
2559         'comment' => array('delete', 'insert', 'update'),
2560       ),
2561     ),
2562     'node_save_action' => array(
2563       'type' => 'node',
2564       'description' => t('Save post'),
2565       'configurable' => FALSE,
2566       'hooks' => array(
2567         'comment' => array('delete', 'insert', 'update'),
2568       ),
2569     ),
2570     'node_unpublish_by_keyword_action' => array(
2571       'type' => 'node',
2572       'description' => t('Unpublish post containing keyword(s)'),
2573       'configurable' => TRUE,
2574       'hooks' => array(
2575         'nodeapi' => array('presave', 'insert', 'update'),
2576       ),
2577     ),
2578   );
2579 }
2580 
2581 /**
2582  * Implementation of a Drupal action.
2583  * Sets the status of a node to 1, meaning published.
2584  */
2585 function node_publish_action(&$node, $context = array()) {
2586   $node->status = 1;
2587   watchdog('action', 'Set @type %title to published.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2588 }
2589 
2590 /**
2591  * Implementation of a Drupal action.
2592  * Sets the status of a node to 0, meaning unpublished.
2593  */
2594 function node_unpublish_action(&$node, $context = array()) {
2595   $node->status = 0;
2596   watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2597 }
2598 
2599 /**
2600  * Implementation of a Drupal action.
2601  * Sets the sticky-at-top-of-list property of a node to 1.
2602  */
2603 function node_make_sticky_action(&$node, $context = array()) {
2604   $node->sticky = 1;
2605   watchdog('action', 'Set @type %title to sticky.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2606 }
2607 
2608 /**
2609  * Implementation of a Drupal action.
2610  * Sets the sticky-at-top-of-list property of a node to 0.
2611  */
2612 function node_make_unsticky_action(&$node, $context = array()) {
2613   $node->sticky = 0;
2614   watchdog('action', 'Set @type %title to unsticky.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2615 }
2616 
2617 /**
2618  * Implementation of a Drupal action.
2619  * Sets the promote property of a node to 1.
2620  */
2621 function node_promote_action(&$node, $context = array()) {
2622   $node->promote = 1;
2623   watchdog('action', 'Promoted @type %title to front page.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2624 }
2625 
2626 /**
2627  * Implementation of a Drupal action.
2628  * Sets the promote property of a node to 0.
2629  */
2630 function node_unpromote_action(&$node, $context = array()) {
2631   $node->promote = 0;
2632   watchdog('action', 'Removed @type %title from front page.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2633 }
2634 
2635 /**
2636  * Implementation of a Drupal action.
2637  * Saves a node.
2638  */
2639 function node_save_action($node) {
2640   node_save($node);
2641   watchdog('action', 'Saved @type %title', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2642 }
2643 
2644 /**
2645  * Implementation of a configurable Drupal action.
2646  * Assigns ownership of a node to a user.
2647  */
2648 function node_assign_owner_action(&$node, $context) {
2649   $node->uid = $context['owner_uid'];
2650   $owner_name = db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $context['owner_uid']));
2651   watchdog('action', 'Changed owner of @type %title to uid %name.', array('@type' => node_get_types('type', $node), '%title' => $node->title, '%name' => $owner_name));
2652 }
2653 
2654 function node_assign_owner_action_form($context) {
2655   $description = t('The username of the user to which you would like to assign ownership.');
2656   $count = db_result(db_query("SELECT COUNT(*) FROM {users}"));
2657   $owner_name = '';
2658   if (isset($context['owner_uid'])) {
2659     $owner_name = db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $context['owner_uid']));
2660   }
2661 
2662   // Use dropdown for fewer than 200 users; textbox for more than that.
2663   if (intval($count) < 200) {
2664     $options = array();
2665     $result = db_query("SELECT uid, name FROM {users} WHERE uid > 0 ORDER BY name");
2666     while ($data = db_fetch_object($result)) {
2667       $options[$data->name] = $data->name;
2668     }
2669     $form['owner_name'] = array(
2670       '#type' => 'select',
2671       '#title' => t('Username'),
2672       '#default_value' => $owner_name,
2673       '#options' => $options,
2674       '#description' => $description,
2675     );
2676   }
2677   else {
2678     $form['owner_name'] = array(
2679       '#type' => 'textfield',
2680       '#title' => t('Username'),
2681       '#default_value' => $owner_name,
2682       '#autocomplete_path' => 'user/autocomplete',
2683       '#size' => '6',
2684       '#maxlength' => '7',
2685       '#description' => $description,
2686     );
2687   }
2688   return $form;
2689 }
2690 
2691 function node_assign_owner_action_validate($form, $form_state) {
2692   $count = db_result(db_query("SELECT COUNT(*) FROM {users} WHERE name = '%s'", $form_state['values']['owner_name']));
2693   if (intval($count) != 1) {
2694     form_set_error('owner_name', t('Please enter a valid username.'));
2695   }
2696 }
2697 
2698 function node_assign_owner_action_submit($form, $form_state) {
2699   // Username can change, so we need to store the ID, not the username.
2700   $uid = db_result(db_query("SELECT uid from {users} WHERE name = '%s'", $form_state['values']['owner_name']));
2701   return array('owner_uid' => $uid);
2702 }
2703 
2704 function node_unpublish_by_keyword_action_form($context) {
2705   $form['keywords'] = array(
2706     '#title' => t('Keywords'),
2707     '#type' => 'textarea',
2708     '#description' => t('The post will be unpublished if it contains any of the character sequences above. Use a comma-separated list of character sequences. Example: funny, bungee jumping, "Company, Inc.". Character sequences are case-sensitive.'),
2709     '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
2710   );
2711   return $form;
2712 }
2713 
2714 function node_unpublish_by_keyword_action_submit($form, $form_state) {
2715   return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
2716 }
2717 
2718 /**
2719  * Implementation of a configurable Drupal action.
2720  * Unpublish a node if it contains a certain string.
2721  *
2722  * @param $context
2723  *   An array providing more information about the context of the call to this action.
2724  * @param $comment
2725  *   A node object.
2726  */
2727 function node_unpublish_by_keyword_action($node, $context) {
2728   foreach ($context['keywords'] as $keyword) {
2729     if (strstr(node_view(clone $node), $keyword) || strstr($node->title, $keyword)) {
2730       $node->status = 0;
2731       watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
2732       break;
2733     }
2734   }
2735 }
2736 
2737 /**
2738  * Helper function to generate standard node permission list for a given type.
2739  *
2740  * @param $type
2741  *   The machine-readable name of the node type.
2742  * @return array
2743  *   An array of permission names and descriptions.
2744  */
2745 function node_list_permissions($type) {
2746   $info = node_get_types('type', $type);
2747   $type = check_plain($info->type);
2748 
2749   // Build standard list of node permissions for this type.
2750   $perms["create $type content"] = t('Create new %type_name content.', array('%type_name' => $info->name));
2751   $perms["delete any $type content"] = t('Delete any %type_name content, regardless of its author.', array('%type_name' => $info->name));
2752   $perms["delete own $type content"] = t('Delete %type_name content created by the user.', array('%type_name' => $info->name));
2753   $perms["edit own $type content"] = t('Edit %type_name content created by the user.', array('%type_name' => $info->name));
2754   $perms["edit any $type content"] = t('Edit any %type_name content, regardless of its author.', array('%type_name' => $info->name));
2755 
2756   return $perms;
2757 }