bugfix: the slice page was broken when nobody is in slice
[plewww.git] / drupal-hacks / node.module
1 <?php // -*- php -*-
2 // $Id$
3
4 /**
5  * @file
6  * The core that allows content to be submitted to the site.
7  */
8
9 define('NODE_NEW_LIMIT', time() - 30 * 24 * 60 * 60);
10
11 /**
12  * Implementation of hook_help().
13  */
14 function node_help($section) {
15   switch ($section) {
16     case 'admin/help#node':
17       $output = '<p>'. t('All content in a website is stored and treated as <b>nodes</b>. Therefore nodes are any postings such as blogs, stories, polls and forums. The node module manages these content types and is one of the strengths of Drupal over other content management systems.') .'</p>';
18       $output .= '<p>'. t('Treating all content as nodes allows the flexibility of creating new types of content. It also allows you to painlessly apply new features or changes to all content. Comments are not stored as nodes but are always associated with a node.') .'</p>';
19       $output .= t('<p>Node module features</p>
20 <ul>
21 <li>The list tab provides an interface to search and sort all content on your site.</li>
22 <li>The configure settings tab has basic settings for content on your site.</li>
23 <li>The configure content types tab lists all content types for your site and lets you configure their default workflow.</li>
24 <li>The search tab lets you search all content on your site</li>
25 </ul>
26 ');
27       $output .= t('<p>You can</p>
28 <ul>
29 <li>search for content at <a href="%search">search</a>.</li>
30 <li>administer nodes at <a href="%admin-settings-content-types">administer &gt;&gt; settings &gt;&gt; content types</a>.</li>
31 </ul>
32 ', array('%search' => url('search'), '%admin-settings-content-types' => url('admin/settings/content-types')));
33       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%node">Node page</a>.', array('%node' => 'http://drupal.org/handbook/modules/node/')) .'</p>';
34       return $output;
35     case 'admin/modules#description':
36       return t('Allows content to be submitted to the site and displayed on pages.');
37     case 'admin/node/configure':
38     case 'admin/node/configure/settings':
39       return t('<p>Settings for the core of Drupal. Almost everything is a node so these settings will affect most of the site.</p>');
40     case 'admin/node':
41       return t('<p>Below is a list of all of the posts on your site. Other forms of content are listed elsewhere (e.g. <a href="%comments">comments</a>).</p><p>Clicking a title views the post, while clicking an author\'s name views their user information.</p>', array('%comments' => url('admin/comment')));
42     case 'admin/node/search':
43       return t('<p>Enter a simple pattern to search for a post. This can include the wildcard character *.<br />For example, a search for "br*" might return "bread bakers", "our daily bread" and "brenda".</p>');
44   }
45
46   if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions' && !arg(3)) {
47     return t('The revisions let you track differences between multiple versions of a post.');
48   }
49
50   if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) {
51     return filter_xss_admin(variable_get($type .'_help', ''));
52   }
53 }
54
55 /**
56  * Implementation of hook_cron().
57  */
58 function node_cron() {
59   db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT);
60 }
61
62 /**
63  * Gather a listing of links to nodes.
64  *
65  * @param $result
66  *   A DB result object from a query to fetch node objects.  If your query joins the <code>node_comment_statistics</code> table so that the <code>comment_count</code> field is available, a title attribute will be added to show the number of comments.
67  * @param $title
68  *   A heading for the resulting list.
69  *
70  * @return
71  *   An HTML list suitable as content for a block.
72  */
73 function node_title_list($result, $title = NULL) {
74   while ($node = db_fetch_object($result)) {
75     $items[] = l($node->title, 'node/'. $node->nid, $node->comment_count ? array('title' => format_plural($node->comment_count, '1 comment', '%count comments')) : '');
76   }
77
78   return theme('node_list', $items, $title);
79 }
80
81 /**
82  * Format a listing of links to nodes.
83  */
84 function theme_node_list($items, $title = NULL) {
85   return theme('item_list', $items, $title);
86 }
87
88 /**
89  * Update the 'last viewed' timestamp of the specified node for current user.
90  */
91 function node_tag_new($nid) {
92   global $user;
93
94   if ($user->uid) {
95     if (node_last_viewed($nid)) {
96       db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid);
97     }
98     else {
99       @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time());
100     }
101   }
102 }
103
104 /**
105  * Retrieves the timestamp at which the current user last viewed the
106  * specified node.
107  */
108 function node_last_viewed($nid) {
109   global $user;
110   static $history;
111
112   if (!isset($history[$nid])) {
113     $history[$nid] = db_fetch_object(db_query("SELECT timestamp FROM {history} WHERE uid = '$user->uid' AND nid = %d", $nid));
114   }
115
116   return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0);
117 }
118
119 /**
120  * Decide on the type of marker to be displayed for a given node.
121  *
122  * @param $nid
123  *   Node ID whose history supplies the "last viewed" timestamp.
124  * @param $timestamp
125  *   Time which is compared against node's "last viewed" timestamp.
126  * @return
127  *   One of the MARK constants.
128  */
129 function node_mark($nid, $timestamp) {
130   global $user;
131   static $cache;
132
133   if (!$user->uid) {
134     return MARK_READ;
135   }
136   if (!isset($cache[$nid])) {
137     $cache[$nid] = node_last_viewed($nid);
138   }
139   if ($cache[$nid] == 0 && $timestamp > NODE_NEW_LIMIT) {
140     return MARK_NEW;
141   }
142   elseif ($timestamp > $cache[$nid] && $timestamp > NODE_NEW_LIMIT) {
143     return MARK_UPDATED;
144   }
145   return MARK_READ;
146 }
147
148 /**
149  * Automatically generate a teaser for a node body in a given format.
150  */
151 function node_teaser($body, $format = NULL) {
152
153   $size = variable_get('teaser_length', 600);
154
155   // find where the delimiter is in the body
156   $delimiter = strpos($body, '<!--break-->');
157
158   // If the size is zero, and there is no delimiter, the entire body is the teaser.
159   if ($size == 0 && $delimiter === FALSE) {
160     return $body;
161   }
162
163   // If a valid delimiter has been specified, use it to chop off the teaser.
164   if ($delimiter !== FALSE) {
165     return substr($body, 0, $delimiter);
166   }
167
168   // We check for the presence of the PHP evaluator filter in the current
169   // format. If the body contains PHP code, we do not split it up to prevent
170   // parse errors.
171   if (isset($format)) {
172     $filters = filter_list_format($format);
173     if (isset($filters['filter/1']) && strpos($body, '<?') !== FALSE) {
174       return $body;
175     }
176   }
177
178   // If we have a short body, the entire body is the teaser.
179   if (strlen($body) < $size) {
180     return $body;
181   }
182
183   // The teaser may not be longer than maximum length specified. Initial slice.
184   $teaser = truncate_utf8($body, $size);
185   $position = 0;
186   // Cache the reverse of the teaser.
187   $reversed = strrev($teaser);
188
189   // In some cases, no delimiter has been specified. In this case, we try to
190   // split at paragraph boundaries.
191   $breakpoints = array('</p>' => 0, '<br />' => 6, '<br>' => 4, "\n" => 1);
192   // We use strpos on the reversed needle and haystack for speed.
193   foreach ($breakpoints as $point => $offset) {
194     $length = strpos($reversed, strrev($point));
195     if ($length !== FALSE) {
196       $position = - $length - $offset;
197       return ($position == 0) ? $teaser : substr($teaser, 0, $position);
198     }
199   }
200
201   // When even the first paragraph is too long, we try to split at the end of
202   // the last full sentence.
203   $breakpoints = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);
204   $min_length = strlen($reversed);
205   foreach ($breakpoints as $point => $offset) {
206     $length = strpos($reversed, strrev($point));
207     if ($length !== FALSE) {
208       $min_length = min($length, $min_length);
209       $position = 0 - $length - $offset;
210     }
211   }
212   return ($position == 0) ? $teaser : substr($teaser, 0, $position);
213 }
214
215 function _node_names($op = '', $node = NULL) {
216   static $node_names = array();
217   static $node_list = array();
218
219   if (empty($node_names)) {
220     $node_names = module_invoke_all('node_info');
221     foreach ($node_names as $type => $value) {
222       $node_list[$type] = $value['name'];
223     }
224   }
225   if ($node) {
226     if (is_array($node)) {
227       $type = $node['type'];
228     }
229     elseif (is_object($node)) {
230       $type = $node->type;
231     }
232     elseif (is_string($node)) {
233       $type = $node;
234     }
235     if (!isset($node_names[$type])) {
236       return FALSE;
237     }
238   }
239   switch ($op) {
240     case 'base':
241       return $node_names[$type]['base'];
242     case 'list':
243       return $node_list;
244     case 'name':
245       return $node_list[$type];
246   }
247 }
248
249 /**
250  * Determine the basename for hook_load etc.
251  *
252  * @param $node
253  *   Either a node object, a node array, or a string containing the node type.
254  * @return
255  *   The basename for hook_load, hook_nodeapi etc.
256  */
257 function node_get_base($node) {
258   return _node_names('base', $node);
259 }
260
261 /**
262  * Determine the human readable name for a given type.
263  *
264  * @param $node
265  *   Either a node object, a node array, or a string containing the node type.
266  * @return
267  *   The human readable name of the node type.
268  */
269 function node_get_name($node) {
270   return _node_names('name', $node);
271 }
272
273 /**
274  * Return the list of available node types.
275  *
276  * @return
277  *   An array consisting ('#type' => name) pairs.
278  */
279 function node_get_types() {
280   return _node_names('list');
281 }
282
283 /**
284  * Determine whether a node hook exists.
285  *
286  * @param &$node
287  *   Either a node object, node array, or a string containing the node type.
288  * @param $hook
289  *   A string containing the name of the hook.
290  * @return
291  *   TRUE iff the $hook exists in the node type of $node.
292  */
293 function node_hook(&$node, $hook) {
294   return module_hook(node_get_base($node), $hook);
295 }
296
297 /**
298  * Invoke a node hook.
299  *
300  * @param &$node
301  *   Either a node object, node array, or a string containing the node type.
302  * @param $hook
303  *   A string containing the name of the hook.
304  * @param $a2, $a3, $a4
305  *   Arguments to pass on to the hook, after the $node argument.
306  * @return
307  *   The returned value of the invoked hook.
308  */
309 function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
310   if (node_hook($node, $hook)) {
311     $function = node_get_base($node) ."_$hook";
312     return ($function($node, $a2, $a3, $a4));
313   }
314 }
315
316 /**
317  * Invoke a hook_nodeapi() operation in all modules.
318  *
319  * @param &$node
320  *   A node object.
321  * @param $op
322  *   A string containing the name of the nodeapi operation.
323  * @param $a3, $a4
324  *   Arguments to pass on to the hook, after the $node and $op arguments.
325  * @return
326  *   The returned value of the invoked hooks.
327  */
328 function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
329   $return = array();
330   foreach (module_implements('nodeapi') as $name) {
331     $function = $name .'_nodeapi';
332     $result = $function($node, $op, $a3, $a4);
333     if (isset($result) && is_array($result)) {
334       $return = array_merge($return, $result);
335     }
336     else if (isset($result)) {
337       $return[] = $result;
338     }
339   }
340   return $return;
341 }
342
343 /**
344  * Load a node object from the database.
345  *
346  * @param $param
347  *   Either the nid of the node or an array of conditions to match against in the database query
348  * @param $revision
349  *   Which numbered revision to load. Defaults to the current version.
350  * @param $reset
351  *   Whether to reset the internal node_load cache.
352  *
353  * @return
354  *   A fully-populated node object.
355  */
356 function node_load($param = array(), $revision = NULL, $reset = NULL) {
357   static $nodes = array();
358
359   if ($reset) {
360     $nodes = array();
361   }
362
363   $cachable = ($revision == NULL);
364   $arguments = array();
365   if (is_numeric($param)) {
366     if ($cachable && isset($nodes[$param])) {
367       return is_object($nodes[$param]) ? drupal_clone($nodes[$param]) : $nodes[$param];
368     }
369     $cond = 'n.nid = %d';
370     $arguments[] = $param;
371   }
372   else {
373     // Turn the conditions into a query.
374     foreach ($param as $key => $value) {
375       $cond[] = 'n.'. db_escape_string($key) ." = '%s'";
376       $arguments[] = $value;
377     }
378     $cond = implode(' AND ', $cond);
379   }
380
381   // Retrieve the node.
382   // No db_rewrite_sql is applied so as to get complete indexing for search.
383   if ($revision) {
384     array_unshift($arguments, $revision);
385     $node = db_fetch_object(db_query('SELECT n.nid, r.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.moderate, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data 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));
386   }
387   else {
388     $node = db_fetch_object(db_query('SELECT n.nid, n.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.moderate, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data 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));
389   }
390
391   if ($node->nid) {
392     // Call the node specific callback (if any) and piggy-back the
393     // results to the node or overwrite some values.
394     if ($extra = node_invoke($node, 'load')) {
395       foreach ($extra as $key => $value) {
396         $node->$key = $value;
397       }
398     }
399
400     if ($extra = node_invoke_nodeapi($node, 'load')) {
401       foreach ($extra as $key => $value) {
402         $node->$key = $value;
403       }
404     }
405     if ($cachable) {
406       $nodes[$node->nid] = is_object($node) ? drupal_clone($node) : $node;
407     }
408   }
409
410   return $node;
411 }
412
413 /**
414  * Save a node object into the database.
415  */
416 function node_save(&$node) {
417   global $user;
418
419   $node->is_new = false;
420
421   // Apply filters to some default node fields:
422   if (empty($node->nid)) {
423     // Insert a new node.
424     $node->is_new = true;
425
426     $node->nid = db_next_id('{node}_nid');
427     $node->vid = db_next_id('{node_revisions}_vid');;
428   }
429   else {
430     // We need to ensure that all node fields are filled.
431     $node_current = node_load($node->nid);
432     foreach ($node as $field => $data) {
433       $node_current->$field = $data;
434     }
435     $node = $node_current;
436
437     if ($node->revision) {
438       $node->old_vid = $node->vid;
439       $node->vid = db_next_id('{node_revisions}_vid');
440     }
441   }
442
443   // Set some required fields:
444   if (empty($node->created)) {
445     $node->created = time();
446   }
447   // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
448   $node->changed = time();
449
450   // Split off revisions data to another structure
451   $revisions_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
452                      'title' => $node->title, 'body' => $node->body,
453                      'teaser' => $node->teaser, 'log' => $node->log, 'timestamp' => $node->changed,
454                      'uid' => $user->uid, 'format' => $node->format);
455   $revisions_table_types = array('nid' => '%d', 'vid' => '%d',
456                      'title' => "'%s'", 'body' => "'%s'",
457                      'teaser' => "'%s'", 'log' => "'%s'", 'timestamp' => '%d',
458                      'uid' => '%d', 'format' => '%d');
459   $node_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
460                     'title' => $node->title, 'type' => $node->type, 'uid' => $node->uid,
461                     'status' => $node->status, 'created' => $node->created,
462                     'changed' => $node->changed, 'comment' => $node->comment,
463                     'promote' => $node->promote, 'moderate' => $node->moderate,
464                     'sticky' => $node->sticky);
465   $node_table_types = array('nid' => '%d', 'vid' => '%d',
466                     'title' => "'%s'", 'type' => "'%s'", 'uid' => '%d',
467                     'status' => '%d', 'created' => '%d',
468                     'changed' => '%d', 'comment' => '%d',
469                     'promote' => '%d', 'moderate' => '%d',
470                     'sticky' => '%d');
471
472   //Generate the node table query and the
473   //the node_revisions table query
474   if ($node->is_new) {
475     $node_query = 'INSERT INTO {node} ('. implode(', ', array_keys($node_table_types)) .') VALUES ('. implode(', ', $node_table_types) .')';
476     $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
477   }
478   else {
479     $arr = array();
480     foreach ($node_table_types as $key => $value) {
481       $arr[] = $key .' = '. $value;
482     }
483     $node_table_values[] = $node->nid;
484     $node_query = 'UPDATE {node} SET '. implode(', ', $arr) .' WHERE nid = %d';
485     if ($node->revision) {
486       $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
487     }
488     else {
489       $arr = array();
490       foreach ($revisions_table_types as $key => $value) {
491         $arr[] = $key .' = '. $value;
492       }
493       $revisions_table_values[] = $node->vid;
494       $revisions_query = 'UPDATE {node_revisions} SET '. implode(', ', $arr) .' WHERE vid = %d';
495     }
496   }
497
498   // Insert the node into the database:
499   db_query($node_query, $node_table_values);
500   db_query($revisions_query, $revisions_table_values);
501
502   // Call the node specific callback (if any):
503   if ($node->is_new) {
504     node_invoke($node, 'insert');
505     node_invoke_nodeapi($node, 'insert');
506   }
507   else {
508     node_invoke($node, 'update');
509     node_invoke_nodeapi($node, 'update');
510   }
511
512   // Clear the cache so an anonymous poster can see the node being added or updated.
513   cache_clear_all();
514 }
515
516 /**
517  * Generate a display of the given node.
518  *
519  * @param $node
520  *   A node array or node object.
521  * @param $teaser
522  *   Whether to display the teaser only, as on the main page.
523  * @param $page
524  *   Whether the node is being displayed by itself as a page.
525  * @param $links
526  *   Whether or not to display node links. Links are omitted for node previews.
527  *
528  * @return
529  *   An HTML representation of the themed node.
530  */
531 function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) {
532   $node = (object)$node;
533
534   // Remove the delimiter (if any) that separates the teaser from the body.
535   // TODO: this strips legitimate uses of '<!--break-->' also.
536   $node->body = str_replace('<!--break-->', '', $node->body);
537
538   if ($node->log != '' && !$teaser && $node->moderate) {
539     $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. filter_xss($node->log) .'</div>';
540   }
541
542   // The 'view' hook can be implemented to overwrite the default function
543   // to display nodes.
544   if (node_hook($node, 'view')) {
545     node_invoke($node, 'view', $teaser, $page);
546   }
547   else {
548     $node = node_prepare($node, $teaser);
549   }
550   // Allow modules to change $node->body before viewing.
551   node_invoke_nodeapi($node, 'view', $teaser, $page);
552   if ($links) {
553     $node->links = module_invoke_all('link', 'node', $node, !$page);
554   }
555   // unset unused $node part so that a bad theme can not open a security hole
556   if ($teaser) {
557     unset($node->body);
558   }
559   else {
560     unset($node->teaser);
561   }
562
563   return theme('node', $node, $teaser, $page);
564 }
565
566 /**
567  * Apply filters to a node in preparation for theming.
568  */
569 function node_prepare($node, $teaser = FALSE) {
570   $node->readmore = (strlen($node->teaser) < strlen($node->body));
571   if ($teaser == FALSE) {
572     $node->body = check_markup($node->body, $node->format, FALSE);
573   }
574   else {
575     $node->teaser = check_markup($node->teaser, $node->format, FALSE);
576   }
577   return $node;
578 }
579
580 /**
581  * Generate a page displaying a single node, along with its comments.
582  */
583 function node_show($node, $cid) {
584   $output = node_view($node, FALSE, TRUE);
585
586   if (function_exists('comment_render') && $node->comment) {
587     $output .= comment_render($node, $cid);
588   }
589
590   // Update the history table, stating that this user viewed this node.
591   node_tag_new($node->nid);
592
593   return $output;
594 }
595
596 /**
597  * Implementation of hook_perm().
598  */
599 function node_perm() {
600   return array('administer nodes', 'access content', 'view revisions', 'revert revisions');
601 }
602
603 /**
604  * Implementation of hook_search().
605  */
606 function node_search($op = 'search', $keys = null) {
607   switch ($op) {
608     case 'name':
609       return t('content');
610
611     case 'reset':
612       variable_del('node_cron_last');
613       variable_del('node_cron_last_nid');
614       return;
615
616     case 'status':
617       $last = variable_get('node_cron_last', 0);
618       $last_nid = variable_get('node_cron_last_nid', 0);
619       $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
620       $remaining = db_result(db_query('SELECT COUNT(*) FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND ((GREATEST(n.created, n.changed, c.last_comment_timestamp) = %d AND n.nid > %d ) OR (n.created > %d OR n.changed > %d OR c.last_comment_timestamp > %d))', $last, $last_nid, $last, $last, $last));
621       return array('remaining' => $remaining, 'total' => $total);
622
623     case 'admin':
624       $form = array();
625       // Output form for defining rank factor weights.
626       $form['content_ranking'] = array('#type' => 'fieldset', '#title' => t('Content ranking'));
627       $form['content_ranking']['#theme'] = 'node_search_admin';
628       $form['content_ranking']['info'] = array('#type' => 'markup', '#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>');
629
630       $ranking = array('node_rank_relevance' => t('Keyword relevance'),
631                        'node_rank_recent' => t('Recently posted'));
632       if (module_exist('comment')) {
633         $ranking['node_rank_comments'] = t('Number of comments');
634       }
635       if (module_exist('statistics') && variable_get('statistics_count_content_views', 0)) {
636         $ranking['node_rank_views'] = t('Number of views');
637       }
638
639       // Note: reversed to reflect that higher number = higher ranking.
640       $options = drupal_map_assoc(range(0, 10));
641       foreach ($ranking as $var => $title) {
642         $form['content_ranking']['factors'][$var] = array('#title' => $title, '#type' => 'select', '#options' => $options, '#default_value' => variable_get($var, 5));
643       }
644       return $form;
645
646     case 'search':
647       // Build matching conditions
648       list($join1, $where1) = _db_rewrite_sql();
649       $arguments1 = array();
650       $conditions1 = 'n.status = 1';
651
652       if ($type = search_query_extract($keys, 'type')) {
653         $types = array();
654         foreach (explode(',', $type) as $t) {
655           $types[] = "n.type = '%s'";
656           $arguments1[] = $t;
657         }
658         $conditions1 .= ' AND ('. implode(' OR ', $types) .')';
659         $keys = search_query_insert($keys, 'type');
660       }
661
662       if ($category = search_query_extract($keys, 'category')) {
663         $categories = array();
664         foreach (explode(',', $category) as $c) {
665           $categories[] = "tn.tid = %d";
666           $arguments1[] = $c;
667         }
668         $conditions1 .= ' AND ('. implode(' OR ', $categories) .')';
669         $join1 .= ' INNER JOIN {term_node} tn ON n.nid = tn.nid';
670         $keys = search_query_insert($keys, 'category');
671       }
672
673       // Build ranking expression (we try to map each parameter to a
674       // uniform distribution in the range 0..1).
675       $ranking = array();
676       $arguments2 = array();
677       $join2 = '';
678       $total = 0;
679       // Used to avoid joining on node_comment_statistics twice
680       $stats_join = false;
681       if ($weight = (int)variable_get('node_rank_relevance', 5)) {
682         // Average relevance values hover around 0.15
683         $ranking[] = '%d * i.relevance';
684         $arguments2[] = $weight;
685         $total += $weight;
686       }
687       if ($weight = (int)variable_get('node_rank_recent', 5)) {
688         // Exponential decay with half-life of 6 months, starting at last indexed node
689         $ranking[] = '%d * POW(2, (GREATEST(n.created, n.changed, c.last_comment_timestamp) - %d) * 6.43e-8)';
690         $arguments2[] = $weight;
691         $arguments2[] = (int)variable_get('node_cron_last', 0);
692         $join2 .= ' INNER JOIN {node} n ON n.nid = i.sid LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
693         $stats_join = true;
694         $total += $weight;
695       }
696       if (module_exist('comment') && $weight = (int)variable_get('node_rank_comments', 5)) {
697         // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
698         $scale = variable_get('node_cron_comments_scale', 0.0);
699         $ranking[] = '%d * (2.0 - 2.0 / (1.0 + c.comment_count * %f))';
700         $arguments2[] = $weight;
701         $arguments2[] = $scale;
702         if (!$stats_join) {
703           $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
704         }
705         $total += $weight;
706       }
707       if (module_exist('statistics') && variable_get('statistics_count_content_views', 0) &&
708           $weight = (int)variable_get('node_rank_views', 5)) {
709         // Inverse law that maps the highest view count on the site to 1 and 0 to 0.
710         $scale = variable_get('node_cron_views_scale', 0.0);
711         $ranking[] = '%d * (2.0 - 2.0 / (1.0 + nc.totalcount * %f))';
712         $arguments2[] = $weight;
713         $arguments2[] = $scale;
714         $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid';
715         $total += $weight;
716       }
717       $select2 = (count($ranking) ? implode(' + ', $ranking) : 'i.relevance') . ' AS score';
718
719       // Do search
720       $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);
721
722       // Load results
723       $results = array();
724       foreach ($find as $item) {
725         $node = node_load($item->sid);
726
727         // Get node output (filtered and with module-specific fields).
728         if (node_hook($node, 'view')) {
729           node_invoke($node, 'view', false, false);
730         }
731         else {
732           $node = node_prepare($node, false);
733         }
734         // Allow modules to change $node->body before viewing.
735         node_invoke_nodeapi($node, 'view', false, false);
736
737         // Fetch comments for snippet
738         $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
739         // Fetch terms for snippet
740         $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
741
742         $extra = node_invoke_nodeapi($node, 'search result');
743         $results[] = array('link' => url('node/'. $item->sid),
744                            'type' => node_get_name($node),
745                            'title' => $node->title,
746                            'user' => theme('username', $node),
747                            'date' => $node->changed,
748                            'node' => $node,
749                            'extra' => $extra,
750                            'score' => $item->score / $total,
751                            'snippet' => search_excerpt($keys, $node->body));
752       }
753       return $results;
754   }
755 }
756
757 /**
758  * Implementation of hook_user().
759  */
760 function node_user($op, &$edit, &$user) {
761   if ($op == 'delete') {
762     db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid);
763     db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid);
764   }
765 }
766
767 function theme_node_search_admin($form) {
768   $output = form_render($form['info']);
769
770   $header = array(t('Factor'), t('Weight'));
771   foreach (element_children($form['factors']) as $key) {
772     $row = array();
773     $row[] = $form['factors'][$key]['#title'];
774     unset($form['factors'][$key]['#title']);
775     $row[] = form_render($form['factors'][$key]);
776     $rows[] = $row;
777   }
778   $output .= theme('table', $header, $rows);
779
780   $output .= form_render($form);
781   return $output;
782 }
783
784 /**
785  * Menu callback; presents general node configuration options.
786  */
787 function node_configure() {
788
789   $form['default_nodes_main'] = array(
790     '#type' => 'select', '#title' => t('Number of posts on main page'), '#default_value' => variable_get('default_nodes_main', 10),
791     '#options' =>  drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
792     '#description' => t('The default maximum number of posts to display per page on overview pages such as the main page.')
793   );
794
795   $form['teaser_length'] = array(
796     '#type' => 'select', '#title' => t('Length of trimmed posts'), '#default_value' => variable_get('teaser_length', 600),
797     '#options' => array(0 => t('Unlimited'), 200 => t('200 characters'), 400 => t('400 characters'), 600 => t('600 characters'),
798       800 => t('800 characters'), 1000 => t('1000 characters'), 1200 => t('1200 characters'), 1400 => t('1400 characters'),
799       1600 => t('1600 characters'), 1800 => t('1800 characters'), 2000 => t('2000 characters')),
800     '#description' => t("The maximum number of characters used in the trimmed version of a post.  Drupal will use this setting to determine at which offset long posts should be trimmed.  The trimmed version of a post is typically used as a teaser when displaying the post on the main page, in XML feeds, etc.  To disable teasers, set to 'Unlimited'. Note that this setting will only affect new or updated content and will not affect existing teasers.")
801   );
802
803   $form['node_preview'] = array(
804     '#type' => 'radios', '#title' => t('Preview post'), '#default_value' => variable_get('node_preview', 0),
805     '#options' => array(t('Optional'), t('Required')), '#description' => t('Must users preview posts before submitting?')
806   );
807
808   return system_settings_form('node_configure', $form);
809 }
810
811 /**
812  * Retrieve the comment mode for the given node ID (none, read, or read/write).
813  */
814 function node_comment_mode($nid) {
815   static $comment_mode;
816   if (!isset($comment_mode[$nid])) {
817     $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid));
818   }
819   return $comment_mode[$nid];
820 }
821
822 /**
823  * Implementation of hook_link().
824  */
825 function node_link($type, $node = 0, $main = 0) {
826   $links = array();
827
828   if ($type == 'node') {
829     if ($main == 1 && $node->teaser && $node->readmore) {
830       $links[] = l(t('read more'), "node/$node->nid", array('title' => t('Read the rest of this posting.'), 'class' => 'read-more'));
831     }
832   }
833
834   return $links;
835 }
836
837 /**
838  * Implementation of hook_menu().
839  */
840 function node_menu($may_cache) {
841   $items = array();
842
843   if ($may_cache) {
844     $items[] = array('path' => 'admin/node', 'title' => t('content'),
845       'callback' => 'node_admin_nodes',
846       'access' => user_access('administer nodes'));
847     $items[] = array('path' => 'admin/node/overview', 'title' => t('list'),
848       'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
849
850     if (module_exist('search')) {
851       $items[] = array('path' => 'admin/node/search', 'title' => t('search'),
852         'callback' => 'node_admin_search',
853         'access' => user_access('administer nodes'),
854         'type' => MENU_LOCAL_TASK);
855     }
856
857     $items[] = array('path' => 'admin/settings/node', 'title' => t('posts'),
858       'callback' => 'node_configure',
859       'access' => user_access('administer nodes'));
860     $items[] = array('path' => 'admin/settings/content-types', 'title' => t('content types'),
861       'callback' => 'node_types_configure',
862       'access' => user_access('administer nodes'));
863
864     $items[] = array('path' => 'node', 'title' => t('content'),
865       'callback' => 'node_page',
866       'access' => user_access('access content'),
867       'type' => MENU_MODIFIABLE_BY_ADMIN);
868     $items[] = array('path' => 'node/add', 'title' => t('create content'),
869       'callback' => 'node_page',
870       'access' => user_access('access content'),
871       'type' => MENU_ITEM_GROUPING,
872       'weight' => 1);
873     $items[] = array('path' => 'rss.xml', 'title' => t('rss feed'),
874       'callback' => 'node_feed',
875       'access' => user_access('access content'),
876       'type' => MENU_CALLBACK);
877   }
878   else {
879     if (arg(0) == 'node' && is_numeric(arg(1))) {
880       $node = node_load(arg(1));
881       if ($node->nid) {
882         $items[] = array('path' => 'node/'. arg(1), 'title' => t('view'),
883           'callback' => 'node_page',
884           'access' => node_access('view', $node),
885           'type' => MENU_CALLBACK);
886         $items[] = array('path' => 'node/'. arg(1) .'/view', 'title' => t('view'),
887             'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
888         $items[] = array('path' => 'node/'. arg(1) .'/edit', 'title' => t('edit'),
889           'callback' => 'node_page',
890           'access' => node_access('update', $node),
891           'weight' => 1,
892           'type' => MENU_LOCAL_TASK);
893         $items[] = array('path' => 'node/'. arg(1) .'/delete', 'title' => t('delete'),
894           'callback' => 'node_delete_confirm',
895           'access' => node_access('delete', $node),
896           'weight' => 1,
897           'type' => MENU_CALLBACK);
898         $revisions_access = ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node) && db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', arg(1))) > 1);
899         $items[] = array('path' => 'node/'. arg(1) .'/revisions', 'title' => t('revisions'),
900           'callback' => 'node_revisions',
901           'access' => $revisions_access,
902           'weight' => 2,
903           'type' => MENU_LOCAL_TASK);
904         $items[] = array('path' => 'node/'. arg(1) .'/revisions/' . arg(3) . '/delete', 
905           'title' => t('revisions'),
906           'callback' => 'node_revisions',
907           'access' => $revisions_access,
908           'weight' => 2,
909           'type' => MENU_CALLBACK);
910         $items[] = array('path' => 'node/'. arg(1) .'/revisions/' . arg(3) . '/revert', 
911           'title' => t('revisions'),
912           'callback' => 'node_revisions',
913           'access' => $revisions_access,
914           'weight' => 2,
915           'type' => MENU_CALLBACK);
916       }
917     }
918     else if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'content-types' && is_string(arg(3))) {
919       $items[] = array('path' => 'admin/settings/content-types/'. arg(3),
920         'title' => t("'%name' content type", array('%name' => node_get_name(arg(3)))),
921         'type' => MENU_CALLBACK);
922     }
923   }
924
925   return $items;
926 }
927
928 function node_last_changed($nid) {
929   $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
930   return ($node->changed);
931 }
932
933 /**
934  * List node administration operations that can be performed.
935  */
936 function node_operations() {
937   $operations = array(
938     'approve' =>   array(t('Approve the selected posts'), 'UPDATE {node} SET status = 1, moderate = 0 WHERE nid = %d'),
939     'promote' =>   array(t('Promote the selected posts'), 'UPDATE {node} SET status = 1, promote = 1, moderate = 0 WHERE nid = %d'),
940     'sticky' =>    array(t('Make the selected posts sticky'), 'UPDATE {node} SET status = 1, sticky = 1 WHERE nid = %d'),
941     'demote' =>    array(t('Demote the selected posts'), 'UPDATE {node} SET promote = 0 WHERE nid = %d'),
942     'unpublish' => array(t('Unpublish the selected posts'), 'UPDATE {node} SET status = 0 WHERE nid = %d'),
943     'delete' =>    array(t('Delete the selected posts'), '')
944   );
945   return $operations;
946 }
947
948 /**
949  * List node administration filters that can be applied.
950  */
951 function node_filters() {
952   // Regular filters
953   $filters['status'] = array('title' => t('status'),
954     'options' => array('status-1'   => t('published'),     'status-0' => t('not published'),
955                        'moderate-1' => t('in moderation'), 'moderate-0' => t('not in moderation'),
956                        'promote-1'  => t('promoted'),      'promote-0' => t('not promoted'),
957                        'sticky-1'   => t('sticky'),        'sticky-0' => t('not sticky')));
958   $filters['type'] = array('title' => t('type'), 'options' => node_get_types());
959   // The taxonomy filter
960   if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
961     $filters['category'] = array('title' => t('category'), 'options' => $taxonomy);
962   }
963
964   return $filters;
965 }
966
967 /**
968  * Build query for node administration filters based on session.
969  */
970 function node_build_filter_query() {
971   $filters = node_filters();
972
973   // Build query
974   $where = $args = array();
975   $join = '';
976   foreach ($_SESSION['node_overview_filter'] as $index => $filter) {
977     list($key, $value) = $filter;
978     switch($key) {
979       case 'status':
980         // Note: no exploitable hole as $key/$value have already been checked when submitted
981         list($key, $value) = explode('-', $value, 2);
982         $where[] = 'n.'. $key .' = %d';
983         break;
984       case 'category':
985         $table = "tn$index";
986         $where[] = "$table.tid = %d";
987         $join .= "INNER JOIN {term_node} $table ON n.nid = $table.nid ";
988         break;
989       case 'type':
990         $where[] = "n.type = '%s'";
991     }
992     $args[] = $value;
993   }
994   $where = count($where) ? 'WHERE '. implode(' AND ', $where) : '';
995
996   return array('where' => $where, 'join' => $join, 'args' => $args);
997 }
998
999 /**
1000  * Return form for node administration filters.
1001  */
1002 function node_filter_form() {
1003   $session = &$_SESSION['node_overview_filter'];
1004   $session = is_array($session) ? $session : array();
1005   $filters = node_filters();
1006
1007   $i = 0;
1008   $form['filters'] = array('#type' => 'fieldset',
1009     '#title' => t('Show only items where'),
1010     '#theme' => 'node_filters',
1011   );
1012   foreach ($session as $filter) {
1013     list($type, $value) = $filter;
1014     if ($type == 'category') {
1015       // Load term name from DB rather than search and parse options array.
1016       $value = module_invoke('taxonomy', 'get_term', $value);
1017       $value = $value->name;
1018     }
1019     else {
1020       $value = $filters[$type]['options'][$value];
1021     }
1022     $string = ($i++ ? '<em>and</em> where <strong>%a</strong> is <strong>%b</strong>' : '<strong>%a</strong> is <strong>%b</strong>');
1023     $form['filters']['current'][] = array('#value' => t($string, array('%a' => $filters[$type]['title'] , '%b' => $value)));
1024   }
1025
1026   foreach ($filters as $key => $filter) {
1027     $names[$key] = $filter['title'];
1028     $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
1029   }
1030
1031   $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
1032   $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
1033   if (count($session)) {
1034     $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
1035     $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
1036   }
1037
1038   return drupal_get_form('node_filter_form', $form);
1039 }
1040
1041 /**
1042  * Theme node administration filter form.
1043  */
1044 function theme_node_filter_form(&$form) {
1045   $output .= '<div id="node-admin-filter">';
1046   $output .= form_render($form['filters']);
1047   $output .= '</div>';
1048   $output .= form_render($form);
1049   return $output;
1050 }
1051
1052 /**
1053  * Theme node administraton filter selector.
1054  */
1055 function theme_node_filters(&$form) {
1056   $output .= '<ul>';
1057   if (sizeof($form['current'])) {
1058     foreach (element_children($form['current']) as $key) {
1059       $output .= '<li>' . form_render($form['current'][$key]) . '</li>';
1060     }
1061   }
1062
1063   $output .= '<li><dl class="multiselect">' . (sizeof($form['current']) ? '<dt><em>'. t('and') .'</em> '. t('where') .'</dt>' : '') . '<dd class="a">';
1064   foreach (element_children($form['filter']) as $key) {
1065     $output .= form_render($form['filter'][$key]);
1066   }
1067   $output .= '</dd>';
1068
1069   $output .= '<dt>'. t('is') .'</dt>' . '<dd class="b">';
1070
1071   foreach (element_children($form['status']) as $key) {
1072     $output .= form_render($form['status'][$key]);
1073   }
1074   $output .= '</dd>';
1075
1076   $output .= '</dl>';
1077   $output .= '<div class="container-inline" id="node-admin-buttons">'. form_render($form['buttons']) .'</div>';
1078   $output .= '</li></ul><br class="clear" />';
1079
1080   return $output;
1081 }
1082
1083 /**
1084  * Process result from node administration filter form.
1085  */
1086 function node_filter_form_submit() {
1087   global $form_values;
1088   $op = $_POST['op'];
1089   $filters = node_filters();
1090   switch ($op) {
1091     case t('Filter'):
1092     case t('Refine'):
1093       if (isset($form_values['filter'])) {
1094         $filter = $form_values['filter'];
1095
1096         // Flatten the options array to accommodate hierarchical/nested options.
1097         $flat_options = form_options_flatten($filters[$filter]['options']);
1098
1099         if (isset($flat_options[$form_values[$filter]])) {
1100           $_SESSION['node_overview_filter'][] = array($filter, $form_values[$filter]);
1101         }
1102       }
1103       break;
1104     case t('Undo'):
1105       array_pop($_SESSION['node_overview_filter']);
1106       break;
1107     case t('Reset'):
1108       $_SESSION['node_overview_filter'] = array();
1109       break;
1110   }
1111 }
1112
1113 /**
1114  * Generate the content administration overview.
1115  */
1116 function node_admin_nodes_submit($form_id, $edit) {
1117   $operations = node_operations();
1118   if ($operations[$edit['operation']][1]) {
1119     // Flag changes
1120     $operation = $operations[$edit['operation']][1];
1121     foreach ($edit['nodes'] as $nid => $value) {
1122       if ($value) {
1123         db_query($operation, $nid);
1124       }
1125     }
1126     cache_clear_all();
1127     drupal_set_message(t('The update has been performed.'));
1128   }
1129 }
1130
1131 function node_admin_nodes_validate($form_id, $edit) {
1132   $edit['nodes'] = array_diff($edit['nodes'], array(0));
1133   if (count($edit['nodes']) == 0) {
1134     if ($edit['operation'] == 'delete') {
1135       form_set_error('', t('Please select some items to perform the delete operation.'));
1136     }
1137     else {
1138       form_set_error('', t('Please select some items to perform the update on.'));
1139     }
1140   }
1141 }
1142
1143 /**
1144  * Menu callback: content administration.
1145  */
1146 function node_admin_nodes() {
1147   global $form_values;
1148   $output = node_filter_form();
1149
1150   if ($_POST['edit']['operation'] == 'delete' && $_POST['edit']['nodes']) {
1151     return node_multiple_delete_confirm();
1152   }
1153
1154   $filter = node_build_filter_query();
1155
1156   $result = pager_query('SELECT n.*, u.name, u.uid FROM {node} n '. $filter['join'] .' INNER JOIN {users} u ON n.uid = u.uid '. $filter['where'] .' ORDER BY n.changed DESC', 50, 0, NULL, $filter['args']);
1157
1158   $form['options'] = array('#type' => 'fieldset',
1159     '#title' => t('Update options'),
1160     '#prefix' => '<div class="container-inline">',
1161     '#suffix' => '</div>',
1162   );
1163   $options = array();
1164   foreach (node_operations() as $key => $value) {
1165     $options[$key] = $value[0];
1166   }
1167   $form['options']['operation'] = array('#type' => 'select', '#options' => $options,  '#default_value' => 'approve');
1168   $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
1169
1170   $destination = drupal_get_destination();
1171   while ($node = db_fetch_object($result)) {
1172     $nodes[$node->nid] = '';
1173     $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid) .' '. theme('mark', node_mark($node->nid, $node->changed)));
1174     $form['name'][$node->nid] =  array('#value' => node_get_name($node));
1175     $form['username'][$node->nid] = array('#value' => theme('username', $node));
1176     $form['status'][$node->nid] =  array('#value' =>  ($node->status ? t('published') : t('not published')));
1177     $form['operations'][$node->nid] = array('#value' => l(t('edit'), 'node/'. $node->nid .'/edit', array(), $destination));
1178   }
1179   $form['nodes'] = array('#type' => 'checkboxes', '#options' => $nodes);
1180   $form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
1181
1182   // Call the form first, to allow for the form_values array to be populated.
1183   $output .= drupal_get_form('node_admin_nodes', $form);
1184
1185   return $output;
1186 }
1187
1188 /**
1189  * Theme node administration overview.
1190  */
1191 function theme_node_admin_nodes($form) {
1192   // Overview table:
1193   $header = array(NULL, t('Title'), t('Type'), t('Author'), t('Status'), t('Operations'));
1194
1195   $output .= form_render($form['options']);
1196   if (isset($form['title']) && is_array($form['title'])) {
1197     foreach (element_children($form['title']) as $key) {
1198       $row = array();
1199       $row[] = form_render($form['nodes'][$key]);
1200       $row[] = form_render($form['title'][$key]);
1201       $row[] = form_render($form['name'][$key]);
1202       $row[] = form_render($form['username'][$key]);
1203       $row[] = form_render($form['status'][$key]);
1204       $row[] = form_render($form['operations'][$key]);
1205       $rows[] = $row;
1206     }
1207
1208   }
1209   else  {
1210     $rows[] = array(array('data' => t('No posts available.'), 'colspan' => '6'));
1211   }
1212
1213   $output .= theme('table', $header, $rows);
1214   if ($form['pager']['#value']) {
1215     $output .= form_render($form['pager']);
1216   }
1217
1218   $output .= form_render($form);
1219
1220   return $output;
1221 }
1222
1223 function node_multiple_delete_confirm() {
1224   $edit = $_POST['edit'];
1225
1226   $form['nodes'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
1227   // array_filter returns only elements with true values
1228   foreach (array_filter($edit['nodes']) as $nid => $value) {
1229     $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
1230     $form['nodes'][$nid] = array('#type' => 'hidden', '#value' => $nid, '#prefix' => '<li>', '#suffix' => check_plain($title) ."</li>\n");
1231   }
1232   $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
1233
1234   return confirm_form('node_multiple_delete_confirm', $form,
1235                       t('Are you sure you want to delete these items?'),
1236                       'admin/node', t('This action cannot be undone.'),
1237                       t('Delete all'), t('Cancel'));
1238 }
1239
1240 function node_multiple_delete_confirm_submit($form_id, $edit) {
1241   if ($edit['confirm']) {
1242     foreach ($edit['nodes'] as $nid => $value) {
1243       node_delete($nid);
1244     }
1245     drupal_set_message(t('The items have been deleted.'));
1246   }
1247   return 'admin/node';
1248 }
1249
1250 /**
1251  * Menu callback; presents each node type configuration page.
1252  */
1253 function node_types_configure($type = NULL) {
1254   if (isset($type)) {
1255     $node = new stdClass();
1256     $node->type = $type;
1257     $form['submission'] = array('#type' => 'fieldset', '#title' =>t('Submission form') );
1258     $form['submission'][$type . '_help']  = array(
1259       '#type' => 'textarea', '#title' => t('Explanation or submission guidelines'), '#default_value' =>  variable_get($type .'_help', ''),
1260       '#description' => t('This text will be displayed at the top of the %type submission form. It is useful for helping or instructing your users.', array('%type' => node_get_name($type)))
1261     );
1262     $form['submission']['minimum_'. $type .'_size'] = array(
1263       '#type' => 'select', '#title' => t('Minimum number of words'), '#default_value' => variable_get('minimum_'. $type .'_size', 0), '#options' => drupal_map_assoc(array(0, 10, 25, 50, 75, 100, 125, 150, 175, 200)),
1264       '#description' => t('The minimum number of words a %type must be to be considered valid. This can be useful to rule out submissions that do not meet the site\'s standards, such as short test posts.', array('%type' => node_get_name($type)))
1265     );
1266     $form['workflow'] = array('#type' => 'fieldset', '#title' =>t('Workflow'));
1267     $form['type'] = array('#type' => 'value', '#value' => $type);
1268
1269     $form['array_filter'] = array('#type' => 'value', '#value' => TRUE);
1270     return system_settings_form($type .'_node_settings', $form);
1271   }
1272   else {
1273     $header = array(t('Type'), t('Operations'));
1274
1275     $rows = array();
1276     foreach (node_get_types() as $type => $name) {
1277       $rows[] = array($name, l(t('configure'), 'admin/settings/content-types/'. $type));
1278     }
1279
1280     return theme('table', $header, $rows);
1281   }
1282 }
1283
1284 /**
1285  * Generate an overview table of older revisions of a node.
1286  */
1287 function node_revision_overview($node) {
1288   drupal_set_title(t('Revisions for %title', array('%title' => check_plain($node->title))));
1289
1290   $header = array(t('Revision'), array('data' => t('Operations'), 'colspan' => 2));
1291
1292   $revisions = node_revision_list($node);
1293
1294   $rows = array();
1295   $revert_permission = FALSE;
1296   if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
1297     $revert_permission = TRUE;
1298   }
1299   $delete_permission = FALSE;
1300   if (user_access('administer nodes')) {
1301     $delete_permission = TRUE;
1302   }
1303   foreach ($revisions as $revision) {
1304     $row = array();
1305     $operations = array();
1306
1307     if ($revision->current_vid > 0) {
1308       $row[] = array('data' => t('%date by %username', array('%date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"), '%username' => theme('username', $revision)))
1309                                . (($revision->log != '') ? '<p class="revision-log">'. filter_xss($revision->log) .'</p>' : ''),
1310                      'class' => 'revision-current');
1311       $operations[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => 2);
1312     }
1313     else {
1314       $row[] = t('%date by %username', array('%date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '%username' => theme('username', $revision)))
1315                . (($revision->log != '') ? '<p class="revision-log">'. filter_xss($revision->log) .'</p>' : '');
1316       if ($revert_permission) {
1317         $operations[] = l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert");
1318       }
1319       if ($delete_permission) {
1320         $operations[] = l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete");
1321       }
1322     }
1323     $rows[] = array_merge($row, $operations);
1324   }
1325   $output .= theme('table', $header, $rows);
1326
1327   return $output;
1328 }
1329
1330 /**
1331  * Revert to the revision with the specified revision number. A node and nodeapi "update" event is triggered
1332  * (via the node_save() call) when a revision is reverted.
1333  */
1334 function node_revision_revert($nid, $revision) {
1335   global $user;
1336
1337   $node = node_load($nid, $revision);
1338   if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
1339     if ($node->vid) {
1340       $form = array();
1341       $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
1342       $form['vid'] = array('#type' => 'value', '#value' => $node->vid);
1343       return confirm_form('node_revision_revert_confirm', $form, 
1344                      t('Are you sure you want to revert %title to the revision from %revision-date?', array('%title' => theme('placeholder', $node->title), '%revision-date' => theme('placeholder', format_date($node->revision_timestamp)))),
1345                      "node/$nid/revisions", ' ', t('Revert'), t('Cancel'));
1346     }
1347     else {
1348       drupal_set_message(t('You tried to revert to an invalid revision.'), 'error');
1349     }
1350     drupal_goto('node/'. $nid .'/revisions');
1351   }
1352   drupal_access_denied();
1353 }
1354
1355 function node_revision_revert_confirm_submit($form_id, $form_values) {
1356   $nid = $form_values['nid'];
1357   $revision = $form_values['vid'];
1358   $node = node_load($nid, $revision);
1359   $node->revision = 1;
1360   $node->log = t('Copy of the revision from %date.', array('%date' => theme('placeholder', format_date($node->revision_timestamp))));
1361   if (module_exist('taxonomy')) {
1362     $node->taxonomy = array_keys($node->taxonomy);
1363   }
1364
1365   node_save($node);
1366   drupal_set_message(t('%title has been reverted back to the revision from %revision-date', array('%revision-date' => theme('placeholder', format_date($node->revision_timestamp)), '%title' => theme('placeholder', check_plain($node->title)))));
1367   watchdog('content', t('%type: reverted %title revision %revision.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision))));
1368   return 'node/'. $nid .'/revisions';  
1369 }
1370
1371 /**
1372  * Delete the revision with specified revision number. A "delete revision" nodeapi event is invoked when a
1373  * revision is deleted.
1374  */
1375 function node_revision_delete($nid, $revision) {
1376   if (user_access('administer nodes')) {
1377     $node = node_load($nid);
1378     if (node_access('delete', $node)) {
1379       // Don't delete the current revision
1380       if ($revision != $node->vid) {
1381         $node = node_load($nid, $revision);
1382         $form = array();
1383         $form['nid'] = array('#type' => 'value', '#value' => $nid);
1384         $form['vid'] = array('#type' => 'value', '#value' => $revision);
1385         return confirm_form('node_revision_delete_confirm', $form, 
1386                      t('Are you sure you want to delete %title revision %revision?', array('%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision))),
1387                      "node/$nid/revisions", '', t('Delete'), t('Cancel'));
1388       }
1389       else {
1390         drupal_set_message(t('Deletion failed. You tried to delete the current revision.'));
1391       }
1392       if (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $nid)) > 1) {
1393         drupal_goto("node/$nid/revisions");
1394       }
1395       else {
1396         drupal_goto("node/$nid");
1397       }
1398     }
1399   }
1400   drupal_access_denied();
1401 }
1402
1403 function node_revision_delete_confirm_submit($form_id, $form_values) {
1404   $node = node_load($form_values['nid'], $form_values['vid']);
1405   db_query("DELETE FROM {node_revisions} WHERE nid = %d AND vid = %d", $node->nid, $node->vid);
1406   node_invoke_nodeapi($node, 'delete revision');
1407   drupal_set_message(t('Deleted %title revision %revision.', array('%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $node->vid))));
1408   watchdog('content', t('%type: deleted %title revision %revision.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $node->revision))));
1409  
1410   if (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $node->nid)) > 1) {
1411     return "node/$node->nid/revisions";
1412   }
1413   return "node/$node->nid";
1414 }
1415
1416 /**
1417  * Return a list of all the existing revision numbers.
1418  */
1419 function node_revision_list($node) {
1420   $revisions = array();
1421   $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);
1422   while ($revision = db_fetch_object($result)) {
1423     $revisions[] = $revision;
1424   }
1425
1426   return $revisions;
1427 }
1428
1429 function node_admin_search() {
1430   $output = search_form(url('admin/node/search'), $_POST['edit']['keys'], 'node') . search_data($_POST['edit']['keys'], 'node');
1431   return $output;
1432 }
1433
1434 /**
1435  * Implementation of hook_block().
1436  */
1437 function node_block($op = 'list', $delta = 0) {
1438   if ($op == 'list') {
1439     $blocks[0]['info'] = t('Syndicate');
1440     return $blocks;
1441   }
1442   else if ($op == 'view') {
1443     $block['subject'] = t('Syndicate');
1444     $block['content'] = theme('feed_icon', url('rss.xml'));
1445
1446     return $block;
1447   }
1448 }
1449
1450 /**
1451  * A generic function for generating RSS feeds from a set of nodes.
1452  *
1453  * @param $nodes
1454  *   An object as returned by db_query() which contains the nid field.
1455  * @param $channel
1456  *   An associative array containing title, link, description and other keys.
1457  *   The link should be an absolute URL.
1458  */
1459 function node_feed($nodes = 0, $channel = array()) {
1460   global $base_url, $locale;
1461
1462   if (!$nodes) {
1463     $nodes = 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));
1464   }
1465
1466   $item_length = variable_get('feed_item_length', 'teaser');
1467   $namespaces = array('xmlns:dc="http://purl.org/dc/elements/1.1/"');
1468
1469   while ($node = db_fetch_object($nodes)) {
1470     // Load the specified node:
1471     $item = node_load($node->nid);
1472     $link = url("node/$node->nid", NULL, NULL, 1);
1473
1474     if ($item_length != 'title') {
1475       $teaser = ($item_length == 'teaser') ? TRUE : FALSE;
1476
1477       // Filter and prepare node teaser
1478       if (node_hook($item, 'view')) {
1479         node_invoke($item, 'view', $teaser, FALSE);
1480       }
1481       else {
1482         $item = node_prepare($item, $teaser);
1483       }
1484
1485       // Allow modules to change $node->teaser before viewing.
1486       node_invoke_nodeapi($item, 'view', $teaser, FALSE);
1487     }
1488
1489     // Allow modules to add additional item fields
1490     $extra = node_invoke_nodeapi($item, 'rss item');
1491     $extra = array_merge($extra, array(array('key' => 'pubDate', 'value' =>  date('r', $item->created)), array('key' => 'dc:creator', 'value' => $item->name), array('key' => 'guid', 'value' => $item->nid . ' at ' . $base_url, 'attributes' => array('isPermaLink' => 'false'))));
1492     foreach ($extra as $element) {
1493       if ($element['namespace']) {
1494         $namespaces = array_merge($namespaces, $element['namespace']);
1495       }
1496     }
1497
1498     // Prepare the item description
1499     switch ($item_length) {
1500       case 'fulltext':
1501         $item_text = $item->body;
1502         break;
1503       case 'teaser':
1504         $item_text = $item->teaser;
1505         if ($item->readmore) {
1506           $item_text .= '<p>'. l(t('read more'), 'node/'. $item->nid, NULL, NULL, NULL, TRUE) .'</p>';
1507         }
1508         break;
1509       case 'title':
1510         $item_text = '';
1511         break;
1512     }
1513
1514     $items .= format_rss_item($item->title, $link, $item_text, $extra);
1515   }
1516
1517   $channel_defaults = array(
1518     'version'     => '2.0',
1519     'title'       => variable_get('site_name', 'drupal') .' - '. variable_get('site_slogan', ''),
1520     'link'        => $base_url,
1521     'description' => variable_get('site_mission', ''),
1522     'language'    => $locale
1523   );
1524   $channel = array_merge($channel_defaults, $channel);
1525
1526   $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1527   $output .= "<rss version=\"". $channel["version"] . "\" xml:base=\"". $base_url ."\" ". implode(' ', $namespaces) .">\n";
1528   $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']);
1529   $output .= "</rss>\n";
1530
1531   drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
1532   print $output;
1533 }
1534
1535 /**
1536  * Prepare node for save and allow modules to make changes.
1537  */
1538 function node_submit($node) {
1539   global $user;
1540
1541   // Convert the node to an object, if necessary.
1542   $node = (object)$node;
1543
1544   // Auto-generate the teaser, but only if it hasn't been set (e.g. by a
1545   // module-provided 'teaser' form item).
1546   if (!isset($node->teaser)) {
1547     $node->teaser = isset($node->body) ? node_teaser($node->body, isset($node->format) ? $node->format : NULL) : '';
1548   }
1549
1550   $access = user_access('administer nodes');
1551   if ($access) {
1552     // Populate the "authored by" field.
1553     if ($account = user_load(array('name' => $node->name))) {
1554       $node->uid = $account->uid;
1555     }
1556     else {
1557       $node->uid = 0;
1558     }
1559
1560     $node->created = $node->date ? strtotime($node->date) : NULL;
1561   }
1562   // Force defaults in case people modify the form:
1563   $node_options = variable_get('node_options_'. $node->type, array('status', 'promote'));
1564   foreach (array('status', 'moderate', 'promote', 'sticky', 'revision') as $key) {
1565     if (!$access || !isset($node->$key)) {
1566       $node->$key = in_array($key, $node_options);
1567     }
1568   }
1569
1570   // Do node-type-specific validation checks.
1571   node_invoke($node, 'submit');
1572   node_invoke_nodeapi($node, 'submit');
1573
1574   $node->validated = TRUE;
1575
1576   return $node;
1577 }
1578
1579 /**
1580  * Perform validation checks on the given node.
1581  */
1582 function node_validate($node, $form = array()) {
1583   // Convert the node to an object, if necessary.
1584   $node = (object)$node;
1585
1586   // Make sure the body has the minimum number of words.
1587   // todo use a better word counting algorithm that will work in other languages
1588   if (isset($node->body) && count(explode(' ', $node->body)) < variable_get('minimum_'. $node->type .'_size', 0)) {
1589     form_set_error('body', t('The body of your %type is too short. You need at least %words words.', array('%words' => variable_get('minimum_'. $node->type .'_size', 0), '%type' => node_get_name($node))));
1590   }
1591
1592   if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) {
1593     form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.'));
1594   }
1595
1596   if (user_access('administer nodes')) {
1597     // Validate the "authored by" field.
1598     if (!empty($node->name) && !($account = user_load(array('name' => $node->name)))) {
1599       // The use of empty() is mandatory in the context of usernames
1600       // as the empty string denotes the anonymous user.  In case we
1601       // are dealing with an anonymous user we set the user ID to 0.
1602       form_set_error('name', t('The username %name does not exist.', array ('%name' => theme('placeholder', $node->name))));
1603     }
1604
1605     // Validate the "authored on" field. As of PHP 5.1.0, strtotime returns FALSE instead of -1 upon failure.
1606     if (!empty($node->date) && strtotime($node->date) <= 0) {
1607       form_set_error('date', t('You have to specify a valid date.'));
1608     }
1609   }
1610
1611   // Do node-type-specific validation checks.
1612   node_invoke($node, 'validate', $form);
1613   node_invoke_nodeapi($node, 'validate', $form);
1614 }
1615
1616 function node_form_validate($form_id, $form_values, $form) {
1617   node_validate($form_values, $form);
1618 }
1619
1620 function node_object_prepare(&$node) {
1621   if (user_access('administer nodes')) {
1622     // Set up default values, if required.
1623     if (!isset($node->created)) {
1624       $node->created = time();
1625     }
1626
1627     if (!isset($node->date)) {
1628       $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
1629     }
1630   }
1631   node_invoke($node, 'prepare');
1632   node_invoke_nodeapi($node, 'prepare');
1633 }
1634
1635 /**
1636  * Generate the node editing form.
1637  */
1638 function node_form($node) {
1639   $node = (object)$node;
1640   $form = node_form_array($node);
1641   return drupal_get_form($node->type .'_node_form', $form, 'node_form');
1642 }
1643
1644 /**
1645 * Generate the node editing form array.
1646 */
1647 function node_form_array($node) {
1648   node_object_prepare($node);
1649
1650   // Set the id of the top-level form tag
1651   $form['#id'] = 'node-form';
1652
1653   /**
1654    * Basic node information.
1655    * These elements are just values so they are not even sent to the client.
1656    */
1657   foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) {
1658     $form[$key] = array('#type' => 'value', '#value' => $node->$key);
1659   }
1660
1661   // Changed must be sent to the client, for later overwrite error checking.
1662   $form['changed'] = array('#type' => 'hidden', '#default_value' => $node->changed);
1663
1664   // Get the node-specific bits.
1665   $form = array_merge_recursive($form, node_invoke($node, 'form'));
1666   if (!isset($form['title']['#weight'])) {
1667     $form['title']['#weight'] = -5;
1668   }
1669
1670   $node_options = variable_get('node_options_'. $node->type, array('status', 'promote'));
1671   // If this is a new node, fill in the default values.
1672   if (!isset($node->nid)) {
1673     foreach (array('status', 'moderate', 'promote', 'sticky', 'revision') as $key) {
1674       $node->$key = in_array($key, $node_options);
1675     }
1676     global $user;
1677     $node->uid = $user->uid;
1678   }
1679   else {
1680     // Nodes being edited should always be preset with the default revision setting.
1681     $node->revision = in_array('revision', $node_options);
1682   }
1683   $form['#node'] = $node;
1684
1685   if (user_access('administer nodes')) {
1686     // Node author information
1687     $form['author'] = array('#type' => 'fieldset', '#title' => t('Authoring information'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 20);
1688     $form['author']['name'] = array('#type' => 'textfield', '#title' => t('Authored by'), '#maxlength' => 60, '#autocomplete_path' => 'user/autocomplete', '#default_value' => $node->name ? $node->name : '', '#weight' => -1, '#description' => t('Leave blank for %anonymous.', array('%anonymous' => theme('placeholder', variable_get('anonymous', 'Anonymous')))));
1689     $form['author']['date'] = array('#type' => 'textfield', '#title' => t('Authored on'), '#maxlength' => 25, '#description' => t('Format: %time. Leave blank to use the time of form submission.', array('%time' => $node->date)));
1690
1691     if (isset($node->nid)) {
1692       $form['author']['date']['#default_value'] = $node->date;
1693     }
1694
1695     // Node options for administrators
1696     $form['options'] = array('#type' => 'fieldset', '#title' => t('Publishing options'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 25);
1697     $form['options']['status']   = array('#type' => 'checkbox', '#title' => t('Published'), '#default_value' => $node->status);
1698     $form['options']['moderate'] = array('#type' => 'checkbox', '#title' => t('In moderation queue'), '#default_value' => $node->moderate);
1699     $form['options']['promote']  = array('#type' => 'checkbox', '#title' => t('Promoted to front page'), '#default_value' => $node->promote);
1700     $form['options']['sticky']   = array('#type' => 'checkbox', '#title' => t('Sticky at top of lists'), '#default_value' => $node->sticky);
1701     $form['options']['revision'] = array('#type' => 'checkbox', '#title' => t('Create new revision'), '#default_value' => $node->revision);
1702   }
1703   else {
1704     // Put all of these through as values if the user doesn't have access to them.
1705     foreach (array('uid', 'created') as $key) {
1706       $form[$key] = array('#type' => 'value', '#value' => $node->$key);
1707     }
1708   }
1709
1710   // Add the buttons.
1711   $form['preview'] = array('#type' => 'button', '#value' => t('Preview'), '#weight' => 40);
1712   $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'), '#weight' => 45);
1713   if ($node->nid && node_access('delete', $node)) {
1714     $form['delete'] = array('#type' => 'button', '#value' => t('Delete'), '#weight' => 50);
1715   }
1716
1717   $form['#after_build'] = array('node_form_add_preview');
1718
1719   return $form;
1720 }
1721
1722 function node_form_add_preview($form) {
1723   global $form_values;
1724
1725   $op = isset($_POST['op']) ? $_POST['op'] : '';
1726   if ($op == t('Preview')) {
1727     // Invoke full validation for the form, to protect against cross site
1728     // request forgeries (CSRF) and setting arbitrary values for fields such as
1729     // the input format. Preview the node only when form validation does not
1730     // set any errors.
1731     drupal_validate_form($form['form_id']['#value'], $form);
1732     if (!form_get_errors()) {
1733       // Because the node preview may display a form, we must render it
1734       // outside the node submission form tags using the #prefix property
1735       // (i.e. to prevent illegally nested forms).
1736       // If the node form already has a #prefix, we must preserve it.
1737       // In this case, we put the preview before the #prefix so we keep
1738       // the #prefix as "close" to the rest of the form as possible,
1739       // for example, to keep a <div> only around the form, not the
1740       // preview. We pass the global $form_values here to preserve
1741       // changes made during form validation.
1742       $preview = node_preview((object)$form_values);
1743       $form['#prefix'] = isset($form['#prefix']) ? $preview . $form['#prefix'] : $preview;
1744     }
1745   }
1746   if (variable_get('node_preview', 0) && (form_get_errors() || $op != t('Preview'))) {
1747     unset($form['submit']);
1748   }
1749   return $form;
1750 }
1751
1752 function theme_node_form($form) {
1753   $output = "\n<div class=\"node-form\">\n";
1754
1755   // Admin form fields and submit buttons must be rendered first, because
1756   // they need to go to the bottom of the form, and so should not be part of
1757   // the catch-all call to form_render().
1758   $admin = '';
1759   if (isset($form['author'])) {
1760     $admin .= "    <div class=\"authored\">\n";
1761     $admin .= form_render($form['author']);
1762     $admin .= "    </div>\n";
1763   }
1764   if (isset($form['options'])) {
1765     $admin .= "    <div class=\"options\">\n";
1766     $admin .= form_render($form['options']);
1767     $admin .= "    </div>\n";
1768   }
1769   $buttons = form_render($form['preview']);
1770   $buttons .= form_render($form['submit']);
1771   $buttons .= isset($form['delete']) ? form_render($form['delete']) : '';
1772
1773   // Everything else gets rendered here, and is displayed before the admin form
1774   // field and the submit buttons.
1775   $output .= "  <div class=\"standard\">\n";
1776   $output .= form_render($form);
1777   $output .= "  </div>\n";
1778
1779   if (!empty($admin)) {
1780     $output .= "  <div class=\"admin\">\n";
1781     $output .= $admin;
1782     $output .= "  </div>\n";
1783   }
1784   $output .= $buttons;
1785   $output .= "</div>\n";
1786
1787   return $output;
1788 }
1789
1790 /**
1791  * Present a node submission form or a set of links to such forms.
1792  */
1793 function node_add($type) {
1794   global $user;
1795
1796   // If a node type has been specified, validate its existence.
1797   if (array_key_exists($type, node_get_types()) && node_access('create', $type)) {
1798     // Initialize settings:
1799     $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => $type);
1800
1801     $output = node_form($node);
1802     drupal_set_title(t('Submit %name', array('%name' => node_get_name($node))));
1803   }
1804   else {
1805     // If no (valid) node type has been provided, display a node type overview.
1806     foreach (node_get_types() as $type => $name) {
1807       if (node_access('create', $type)) {
1808         $out = '<dt>'. l($name, "node/add/$type", array('title' => t('Add a new %s.', array('%s' => $name)))) .'</dt>';
1809         $out .= '<dd>'. implode("\n", module_invoke_all('help', 'node/add#'. $type)) .'</dd>';
1810         $item[$name] = $out;
1811       }
1812     }
1813
1814     if (isset($item)) {
1815       uksort($item, 'strnatcasecmp');
1816       $output = t('Choose the appropriate item from the list:') .'<dl>'. implode('', $item) .'</dl>';
1817     }
1818     else {
1819       $output = t('You are not allowed to create content.');
1820     }
1821   }
1822
1823   return $output;
1824 }
1825
1826 /**
1827  * Generate a node preview.
1828  */
1829 function node_preview($node) {
1830   if (node_access('create', $node) || node_access('update', $node)) {
1831     // Load the user's name when needed:
1832     if (isset($node->name)) {
1833       // The use of isset() is mandatory in the context of user IDs, because
1834       // user ID 0 denotes the anonymous user.
1835       if ($user = user_load(array('name' => $node->name))) {
1836         $node->uid = $user->uid;
1837         $node->picture = $user->picture;
1838       }
1839       else {
1840         $node->uid = 0; // anonymous user
1841       }
1842     }
1843     else if ($node->uid) {
1844       $user = user_load(array('uid' => $node->uid));
1845       $node->name = $user->name;
1846       $node->picture = $user->picture;
1847     }
1848
1849     // Set the timestamps when needed:
1850     if ($node->date) {
1851       $node->created = strtotime($node->date);
1852     }
1853     $node->changed = time();
1854
1855     // Extract a teaser, if it hasn't been set (e.g. by a module-provided
1856     // 'teaser' form item).
1857     if (!isset($node->teaser)) {
1858       $node->teaser = node_teaser($node->body, $node->format);
1859     }
1860
1861     // Display a preview of the node:
1862     // Previewing alters $node so it needs to be cloned.
1863     if (!form_get_errors()) {
1864       $cloned_node = drupal_clone($node);
1865       $cloned_node->in_preview = TRUE;
1866       $output = theme('node_preview', $cloned_node);
1867     }
1868     drupal_set_title(t('Preview'));
1869     drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('create content'), 'node/add'), l(t('Submit %name', array('%name' => node_get_name($node))), 'node/add/'. $node->type)));
1870
1871     return $output;
1872   }
1873 }
1874
1875 /**
1876  * Display a node preview for display during node creation and editing.
1877  *
1878  * @param $node
1879  *   The node object which is being previewed.
1880  */
1881 function theme_node_preview($node) {
1882   $output = '<div class="preview">';
1883   if ($node->teaser && $node->teaser != $node->body) {
1884     drupal_set_message(t('The trimmed version of your post shows what your post looks like when promoted to the main page or when exported for syndication. You can insert the delimiter "&lt;!--break--&gt;" (without the quotes) to fine-tune where your post gets split.'));
1885     $output .= '<h3>'. t('Preview trimmed version') .'</h3>';
1886     $output .= node_view(drupal_clone($node), 1, FALSE, 0);
1887     $output .= '<h3>'. t('Preview full version') .'</h3>';
1888     $output .= node_view($node, 0, FALSE, 0);
1889   }
1890   else {
1891     $output .= node_view($node, 0, FALSE, 0);
1892   }
1893   $output .= "</div>\n";
1894
1895   return $output;
1896 }
1897
1898 function node_form_submit($form_id, $edit) {
1899   global $user;
1900
1901   // Fix up the node when required:
1902   $node = node_submit($edit);
1903
1904   // Prepare the node's body:
1905   if ($node->nid) {
1906     // Check whether the current user has the proper access rights to
1907     // perform this operation:
1908     $original_node = node_load($node->nid); //check access rights using the unmodified node
1909     if (node_access('update', $original_node)) {
1910       node_save($node);
1911       watchdog('content', t('%type: updated %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid));
1912       drupal_set_message(t('The %post was updated.', array ('%post' => node_get_name($node))));
1913     }
1914   }
1915   else {
1916     // Check whether the current user has the proper access rights to
1917     // perform this operation:
1918     if (node_access('create', $node)) {
1919       node_save($node);
1920       watchdog('content', t('%type: added %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
1921       drupal_set_message(t('Your %post was created.', array ('%post' => node_get_name($node))));
1922     }
1923   }
1924   if ($node->nid) {
1925     if (node_access('view', $node)) {
1926       return 'node/'. $node->nid;
1927     }
1928     else {
1929       return '';
1930     }
1931   }
1932   // it is very unlikely we get here
1933   return FALSE;
1934 }
1935
1936 /**
1937  * Menu callback -- ask for confirmation of node deletion
1938  */
1939 function node_delete_confirm() {
1940   $edit = $_POST['edit'];
1941   $edit['nid'] = $edit['nid'] ? $edit['nid'] : arg(1);
1942   $node = node_load($edit['nid']);
1943
1944   if (node_access('delete', $node)) {
1945     $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
1946     $output = confirm_form('node_delete_confirm', $form,
1947                    t('Are you sure you want to delete %title?', array('%title' => theme('placeholder', $node->title))),
1948                    $_GET['destination'] ? $_GET['destination'] : 'node/'. $node->nid, t('This action cannot be undone.'),
1949                    t('Delete'), t('Cancel')  );
1950   }
1951
1952   return $output;
1953 }
1954
1955 /**
1956  * Execute node deletion
1957  */
1958 function node_delete_confirm_submit($form_id, $form_values) {
1959   if ($form_values['confirm']) {
1960     node_delete($form_values['nid']);
1961   }
1962
1963   return '';
1964 }
1965
1966 /**
1967  * Delete a node.
1968  */
1969 function node_delete($nid) {
1970
1971   $node = node_load($nid);
1972
1973   if (node_access('delete', $node)) {
1974     db_query('DELETE FROM {node} WHERE nid = %d', $node->nid);
1975     db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid);
1976
1977     // Call the node-specific callback (if any):
1978     node_invoke($node, 'delete');
1979     node_invoke_nodeapi($node, 'delete');
1980
1981     // Clear the cache so an anonymous poster can see the node being deleted.
1982     cache_clear_all();
1983
1984     // Remove this node from the search index if needed.
1985     if (function_exists('search_wipe')) {
1986       search_wipe($node->nid, 'node');
1987     }
1988     drupal_set_message(t('%title has been deleted.', array('%title' => theme('placeholder', $node->title))));
1989     watchdog('content', t('%type: deleted %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))));
1990   }
1991 }
1992
1993 /**
1994  * Menu callback for revisions related activities.
1995  */
1996 function node_revisions() {
1997   if (is_numeric(arg(1)) && arg(2) == 'revisions') {
1998     $op = arg(4) ? arg(4) : 'overview';
1999     switch ($op) {
2000       case 'overview':
2001         $node = node_load(arg(1));
2002         if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) {
2003           return node_revision_overview($node);
2004         }
2005         drupal_access_denied();
2006         return;
2007       case 'view':
2008         if (is_numeric(arg(3))) {
2009           $node = node_load(arg(1), arg(3));
2010           if ($node->nid) {
2011             if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) {
2012               drupal_set_title(t('Revision of %title from %date', array('%title' => theme('placeholder', $node->title), '%date' => format_date($node->revision_timestamp))));
2013               return node_show($node, arg(2));
2014             }
2015             drupal_access_denied();
2016             return;
2017           }
2018         }
2019         break;
2020       case 'revert':
2021         return node_revision_revert(arg(1), arg(3));
2022         break;
2023       case 'delete':
2024         return node_revision_delete(arg(1), arg(3));
2025         break;
2026     }
2027   }
2028   drupal_not_found();
2029 }
2030
2031 /**
2032  * Generate a listing of promoted nodes.
2033  */
2034 function node_page_default() {
2035   $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));
2036
2037   if (db_num_rows($result)) {
2038     drupal_add_link(array('rel' => 'alternate',
2039                           'type' => 'application/rss+xml',
2040                           'title' => t('RSS'),
2041                           'href' => url('rss.xml', NULL, NULL, TRUE)));
2042
2043     $output = '';
2044     while ($node = db_fetch_object($result)) {
2045       $output .= node_view(node_load($node->nid), 1);
2046     }
2047     $output .= theme('pager', NULL, variable_get('default_nodes_main', 10));
2048   }
2049   else {
2050     $output = t('
2051       <h1 class="title">Welcome to your new Drupal-based MyPLC website!</h1>
2052       <p>Please follow these steps to set up and start using your website:</p>
2053       <ol>
2054         <li>
2055           <strong>Use your administrator account</strong>
2056           Enter the site using the credentials that you have provided when configuring MyPLC (see PLC_ROOT_USER and PLC_ROOT_PASSWORD). This account has full administration rights and allows you to configure your website using Drupal tools.
2057         </li>
2058         <li>
2059           <strong>Configure your website</strong>
2060           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.
2061         </li>
2062         <li>
2063           <strong>Enable additional functionality</strong>
2064           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>.
2065         </li>
2066         <li>
2067           <strong>Customize your website design</strong>
2068           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>.
2069         </li>
2070         <li>
2071           <strong>Start posting content</strong>
2072           Finally, you can <a href="%content">create content</a> for your website. This message will disappear once you have published your first post.
2073         </li>
2074       </ol>
2075       <p>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.</p>',
2076       array('%drupal' => 'http://drupal.org/', '%register' => url('user/register'), '%admin' => url('admin'), '%config' => url('admin/settings'), '%modules' => url('admin/modules'), '%download_modules' => 'http://drupal.org/project/modules', '%themes' => url('admin/themes'), '%download_themes' => 'http://drupal.org/project/themes', '%content' => url('node/add'), '%help' => url('admin/help'), '%handbook' => 'http://drupal.org/handbooks', '%forum' => 'http://drupal.org/forum', '%support' => 'http://drupal.org/support')
2077     );
2078     $output = '<div id="first-time">'. $output .'</div>';
2079   }
2080
2081   return $output;
2082 }
2083
2084 /**
2085  * Menu callback; dispatches control to the appropriate operation handler.
2086  */
2087 function node_page() {
2088   $op = arg(1);
2089
2090   if (is_numeric($op)) {
2091     $op = (arg(2) && !is_numeric(arg(2))) ? arg(2) : 'view';
2092   }
2093
2094   switch ($op) {
2095     case 'view':
2096       if (is_numeric(arg(1))) {
2097         $node = node_load(arg(1));
2098         if ($node->nid) {
2099           drupal_set_title(check_plain($node->title));
2100           return node_show($node, arg(2));
2101         }
2102         else if (db_result(db_query('SELECT nid FROM {node} WHERE nid = %d', arg(1)))) {
2103           drupal_access_denied();
2104         }
2105         else {
2106           drupal_not_found();
2107         }
2108       }
2109       break;
2110     case 'add':
2111       return node_add(arg(2));
2112       break;
2113     case 'edit':
2114       if ($_POST['op'] == t('Delete')) {
2115         // Note: we redirect from node/uid/edit to node/uid/delete to make the tabs disappear.
2116         if ($_REQUEST['destination']) {
2117           $destination = drupal_get_destination();
2118           unset($_REQUEST['destination']);
2119         }
2120         drupal_goto('node/'. arg(1) .'/delete', $destination);
2121       }
2122
2123       if (is_numeric(arg(1))) {
2124         $node = node_load(arg(1));
2125         if ($node->nid) {
2126           drupal_set_title(check_plain($node->title));
2127           return node_form($node);
2128         }
2129         else if (db_result(db_query('SELECT nid FROM {node} WHERE nid = %d', arg(1)))) {
2130           drupal_access_denied();
2131         }
2132         else {
2133           drupal_not_found();
2134         }
2135       }
2136       break;
2137     default:
2138       drupal_set_title('');
2139       return node_page_default();
2140   }
2141 }
2142
2143 /**
2144  * shutdown function to make sure we always mark the last node processed.
2145  */
2146 function node_update_shutdown() {
2147   global $last_change, $last_nid;
2148
2149   if ($last_change && $last_nid) {
2150     variable_set('node_cron_last', $last_change);
2151     variable_set('node_cron_last_nid', $last_nid);
2152   }
2153 }
2154
2155 /**
2156  * Implementation of hook_update_index().
2157  */
2158 function node_update_index() {
2159   global $last_change, $last_nid;
2160
2161   register_shutdown_function('node_update_shutdown');
2162
2163   $last = variable_get('node_cron_last', 0);
2164   $last_nid = variable_get('node_cron_last_nid', 0);
2165   $limit = (int)variable_get('search_cron_limit', 100);
2166
2167   // Store the maximum possible comments per thread (used for ranking by reply count)
2168   variable_set('node_cron_comments_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}'))));
2169   variable_set('node_cron_views_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(totalcount) FROM {node_counter}'))));
2170
2171   $result = db_query_range('SELECT GREATEST(IF(c.last_comment_timestamp IS NULL, 0, c.last_comment_timestamp), n.changed) as last_change, n.nid FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND ((GREATEST(n.changed, c.last_comment_timestamp) = %d AND n.nid > %d) OR (n.changed > %d OR c.last_comment_timestamp > %d)) ORDER BY GREATEST(n.changed, c.last_comment_timestamp) ASC, n.nid ASC', $last, $last_nid, $last, $last, $last, 0, $limit);
2172
2173   while ($node = db_fetch_object($result)) {
2174     $last_change = $node->last_change;
2175     $last_nid = $node->nid;
2176     $node = node_load($node->nid);
2177
2178     // Get node output (filtered and with module-specific fields).
2179     if (node_hook($node, 'view')) {
2180       node_invoke($node, 'view', false, false);
2181     }
2182     else {
2183       $node = node_prepare($node, false);
2184     }
2185     // Allow modules to change $node->body before viewing.
2186     node_invoke_nodeapi($node, 'view', false, false);
2187
2188     $text = '<h1>'. check_plain($node->title) .'</h1>'. $node->body;
2189
2190     // Fetch extra data normally not visible
2191     $extra = node_invoke_nodeapi($node, 'update index');
2192     foreach ($extra as $t) {
2193       $text .= $t;
2194     }
2195
2196     // Update index
2197     search_index($node->nid, 'node', $text);
2198   }
2199 }
2200
2201 /**
2202  * Implementation of hook_form_alter().
2203  */
2204 function node_form_alter($form_id, &$form) {
2205   // Node publishing options
2206   if (isset($form['type']) && $form['type']['#value'] .'_node_settings' == $form_id) {
2207     $form['workflow']['node_options_'. $form['type']['#value']] = array('#type' => 'checkboxes',
2208       '#title' => t('Default options'),
2209       '#default_value' => variable_get('node_options_'. $form['type']['#value'], array('status', 'promote')),
2210       '#options' => array(
2211         'status' => t('Published'),
2212         'moderate' => t('In moderation queue'),
2213         'promote' => t('Promoted to front page'),
2214         'sticky' => t('Sticky at top of lists'),
2215         'revision' => t('Create new revision'),
2216       ),
2217       '#description' => t('Users with the <em>administer nodes</em> permission will be able to override these options.'),
2218     );
2219   }
2220
2221   // Advanced node search form
2222   elseif ($form_id == 'search_form' && arg(1) == 'node') {
2223     // Keyword boxes:
2224     $form['advanced'] = array(
2225       '#type' => 'fieldset',
2226       '#title' => t('Advanced search'),
2227       '#collapsible' => TRUE,
2228       '#collapsed' => TRUE,
2229       '#attributes' => array('class' => 'search-advanced'),
2230     );
2231     $form['advanced']['keywords'] = array(
2232       '#prefix' => '<div class="criterion">',
2233       '#suffix' => '</div>',
2234     );
2235     $form['advanced']['keywords']['or'] = array(
2236       '#type' => 'textfield',
2237       '#title' => t('Containing any of the words'),
2238       '#size' => 30,
2239       '#maxlength' => 255,
2240     );
2241     $form['advanced']['keywords']['phrase'] = array(
2242       '#type' => 'textfield',
2243       '#title' => t('Containing the phrase'),
2244       '#size' => 30,
2245       '#maxlength' => 255,
2246     );
2247     $form['advanced']['keywords']['negative'] = array(
2248       '#type' => 'textfield',
2249       '#title' => t('Containing none of the words'),
2250       '#size' => 30,
2251       '#maxlength' => 255,
2252     );
2253
2254     // Taxonomy box:
2255     if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
2256       $form['advanced']['category'] = array(
2257         '#type' => 'select',
2258         '#title' => t('Only in the category(s)'),
2259         '#prefix' => '<div class="criterion">',
2260         '#size' => 10,
2261         '#suffix' => '</div>',
2262         '#options' => $taxonomy,
2263         '#multiple' => TRUE,
2264       );
2265     }
2266
2267     // Node types:
2268     $types = node_get_types();
2269     $form['advanced']['type'] = array(
2270       '#type' => 'checkboxes',
2271       '#title' => t('Only of the type(s)'),
2272       '#prefix' => '<div class="criterion">',
2273       '#suffix' => '</div>',
2274       '#options' => $types,
2275     );
2276     $form['advanced']['submit'] = array(
2277       '#type' => 'submit',
2278       '#value' => t('Advanced search'),
2279       '#prefix' => '<div class="action">',
2280       '#suffix' => '</div><br class="clear" />',
2281     );
2282
2283     $form['#validate']['node_search_validate'] = array();
2284   }
2285 }
2286
2287 /**
2288  * Form API callback for the search form. Registered in node_form_alter().
2289  */
2290 function node_search_validate($form_id, $form_values, $form) {
2291   // Initialise using any existing basic search keywords.
2292   $keys = $form_values['processed_keys'];
2293
2294   // Insert extra restrictions into the search keywords string.
2295   if (isset($form_values['type']) && is_array($form_values['type'])) {
2296     // Retrieve selected types - Forms API sets the value of unselected checkboxes to 0.
2297     $form_values['type'] = array_filter($form_values['type']);
2298     if (count($form_values['type'])) {
2299       $keys = search_query_insert($keys, 'type', implode(',', array_keys($form_values['type'])));
2300     }
2301   }
2302
2303   if (isset($form_values['category']) && is_array($form_values['category'])) {
2304     $keys = search_query_insert($keys, 'category', implode(',', $form_values['category']));
2305   }
2306   if ($form_values['or'] != '') {
2307     if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_values['or'], $matches)) {
2308       $keys .= ' '. implode(' OR ', $matches[1]);
2309     }
2310   }
2311   if ($form_values['negative'] != '') {
2312     if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_values['negative'], $matches)) {
2313       $keys .= ' -'. implode(' -', $matches[1]);
2314     }
2315   }
2316   if ($form_values['phrase'] != '') {
2317     $keys .= ' "'. str_replace('"', ' ', $form_values['phrase']) .'"';
2318   }
2319   if (!empty($keys)) {
2320     form_set_value($form['basic']['inline']['processed_keys'], trim($keys));
2321   }
2322 }
2323
2324 /**
2325  * @defgroup node_access Node access rights
2326  * @{
2327  * The node access system determines who can do what to which nodes.
2328  *
2329  * In determining access rights for a node, node_access() first checks
2330  * whether the user has the "administer nodes" permission. Such users have
2331  * unrestricted access to all nodes. Then the node module's hook_access()
2332  * is called, and a TRUE or FALSE return value will grant or deny access.
2333  * This allows, for example, the blog module to always grant access to the
2334  * blog author, and for the book module to always deny editing access to
2335  * PHP pages.
2336  *
2337  * If node module does not intervene (returns NULL), then the
2338  * node_access table is used to determine access. All node access
2339  * modules are queried using hook_node_grants() to assemble a list of
2340  * "grant IDs" for the user. This list is compared against the table.
2341  * If any row contains the node ID in question (or 0, which stands for "all
2342  * nodes"), one of the grant IDs returned, and a value of TRUE for the
2343  * operation in question, then access is granted. Note that this table is a
2344  * list of grants; any matching row is sufficient to grant access to the
2345  * node.
2346  *
2347  * In node listings, the process above is followed except that
2348  * hook_access() is not called on each node for performance reasons and for
2349  * proper functioning of the pager system. When adding a node listing to your
2350  * module, be sure to use db_rewrite_sql() to add
2351  * the appropriate clauses to your query for access checks.
2352  *
2353  * To see how to write a node access module of your own, see
2354  * node_access_example.module.
2355  */
2356
2357 /**
2358  * Determine whether the current user may perform the given operation on the
2359  * specified node.
2360  *
2361  * @param $op
2362  *   The operation to be performed on the node. Possible values are:
2363  *   - "view"
2364  *   - "update"
2365  *   - "delete"
2366  *   - "create"
2367  * @param $node
2368  *   The node object (or node array) on which the operation is to be performed,
2369  *   or node type (e.g. 'forum') for "create" operation.
2370  * @param $uid
2371  *   The user ID on which the operation is to be performed.
2372  * @return
2373  *   TRUE if the operation may be performed.
2374  */
2375 function node_access($op, $node = NULL, $uid = NULL) {
2376   // Convert the node to an object if necessary:
2377   if ($op != 'create') {
2378     $node = (object)$node;
2379   }
2380   // If the node is in a restricted format, disallow editing.
2381   if ($op == 'update' && !filter_access($node->format)) {
2382     return FALSE;
2383   }
2384
2385   if (user_access('administer nodes')) {
2386     return TRUE;
2387   }
2388
2389   if (!user_access('access content')) {
2390     return FALSE;
2391   }
2392
2393   // Can't use node_invoke(), because the access hook takes the $op parameter
2394   // before the $node parameter.
2395   $access = module_invoke(node_get_base($node), 'access', $op, $node);
2396   if (!is_null($access)) {
2397     return $access;
2398   }
2399
2400   // If the module did not override the access rights, use those set in the
2401   // node_access table.
2402   if ($op != 'create' && $node->nid && $node->status) {
2403     $grants = array();
2404     foreach (node_access_grants($op, $uid) as $realm => $gids) {
2405       foreach ($gids as $gid) {
2406         $grants[] = "(gid = $gid AND realm = '$realm')";
2407       }
2408     }
2409
2410     $grants_sql = '';
2411     if (count($grants)) {
2412       $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
2413     }
2414
2415     $sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1";
2416     $result = db_query($sql, $node->nid);
2417     return (db_result($result));
2418   }
2419   return FALSE;
2420 }
2421
2422 /**
2423  * Generate an SQL join clause for use in fetching a node listing.
2424  *
2425  * @param $node_alias
2426  *   If the node table has been given an SQL alias other than the default
2427  *   "n", that must be passed here.
2428  * @param $node_access_alias
2429  *   If the node_access table has been given an SQL alias other than the default
2430  *   "na", that must be passed here.
2431  * @return
2432  *   An SQL join clause.
2433  */
2434 function _node_access_join_sql($node_alias = 'n', $node_access_alias = 'na') {
2435   if (user_access('administer nodes')) {
2436     return '';
2437   }
2438
2439   return 'INNER JOIN {node_access} '. $node_access_alias .' ON '. $node_access_alias .'.nid = '. $node_alias .'.nid';
2440 }
2441
2442 /**
2443  * Generate an SQL where clause for use in fetching a node listing.
2444  *
2445  * @param $op
2446  *   The operation that must be allowed to return a node.
2447  * @param $node_access_alias
2448  *   If the node_access table has been given an SQL alias other than the default
2449  *   "na", that must be passed here.
2450  * @return
2451  *   An SQL where clause.
2452  */
2453 function _node_access_where_sql($op = 'view', $node_access_alias = 'na', $uid = NULL) {
2454   if (user_access('administer nodes')) {
2455     return;
2456   }
2457
2458   $grants = array();
2459   foreach (node_access_grants($op, $uid) as $realm => $gids) {
2460     foreach ($gids as $gid) {
2461       $grants[] = "($node_access_alias.gid = $gid AND $node_access_alias.realm = '$realm')";
2462     }
2463   }
2464
2465   $grants_sql = '';
2466   if (count($grants)) {
2467     $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
2468   }
2469
2470   $sql = "$node_access_alias.grant_$op >= 1 $grants_sql";
2471   return $sql;
2472 }
2473
2474 /**
2475  * Fetch an array of permission IDs granted to the given user ID.
2476  *
2477  * The implementation here provides only the universal "all" grant. A node
2478  * access module should implement hook_node_grants() to provide a grant
2479  * list for the user.
2480  *
2481  * @param $op
2482  *   The operation that the user is trying to perform.
2483  * @param $uid
2484  *   The user ID performing the operation. If omitted, the current user is used.
2485  * @return
2486  *   An associative array in which the keys are realms, and the values are
2487  *   arrays of grants for those realms.
2488  */
2489 function node_access_grants($op, $uid = NULL) {
2490   global $user;
2491
2492   if (isset($uid)) {
2493     $user_object = user_load(array('uid' => $uid));
2494   }
2495   else {
2496     $user_object = $user;
2497   }
2498
2499   return array_merge(array('all' => array(0)), module_invoke_all('node_grants', $user_object, $op));
2500 }
2501
2502 /**
2503  * Determine whether the user has a global viewing grant for all nodes.
2504  */
2505 function node_access_view_all_nodes() {
2506   static $access;
2507
2508   if (!isset($access)) {
2509     $grants = array();
2510     foreach (node_access_grants('view') as $realm => $gids) {
2511       foreach ($gids as $gid) {
2512         $grants[] = "(gid = $gid AND realm = '$realm')";
2513       }
2514     }
2515
2516     $grants_sql = '';
2517     if (count($grants)) {
2518       $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
2519     }
2520
2521     $sql = "SELECT COUNT(*) FROM {node_access} WHERE nid = 0 $grants_sql AND grant_view >= 1";
2522     $result = db_query($sql);
2523     $access = db_result($result);
2524   }
2525
2526   return $access;
2527 }
2528
2529 /**
2530  * Implementation of hook_db_rewrite_sql
2531  */
2532 function node_db_rewrite_sql($query, $primary_table, $primary_field) {
2533   if ($primary_field == 'nid' && !node_access_view_all_nodes()) {
2534     $return['join'] = _node_access_join_sql($primary_table);
2535     $return['where'] = _node_access_where_sql();
2536     $return['distinct'] = 1;
2537     return $return;
2538   }
2539 }
2540
2541 /**
2542  * @} End of "defgroup node_access".
2543  */
2544
2545