/[drupal]/drupal/modules/node.module
ViewVC logotype

Contents of /drupal/modules/node.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.657 - (show annotations) (download) (as text)
Wed Jul 12 14:30:01 2006 UTC (3 years, 4 months ago) by dries
Branch: MAIN
CVS Tags: DRUPAL-6-0-BETA-4, DRUPAL-6-0-RC-2, DRUPAL-6-0-RC-3, DRUPAL-6-0-BETA-1, DRUPAL-5-0-BETA-1, DRUPAL-5-0-BETA-2, DRUPAL-6-0-BETA-2, DRUPAL-6-0-BETA-3, DRUPAL-5-0-RC-1, DRUPAL-5-0-RC-2, DRUPAL-6-0-RC-1, HEAD
Changes since 1.656: +1 -1 lines
File MIME type: text/x-php
FILE REMOVED
#64280: Renamed from node.module to node/node.module
1 <?php
2 // $Id: node.module,v 1.656 2006/07/10 19:27:52 dries Exp $
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':
38 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')));
39 case 'admin/node/search':
40 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>');
41 }
42
43 if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions') {
44 return t('The revisions let you track differences between multiple versions of a post.');
45 }
46
47 if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) {
48 return filter_xss_admin(variable_get($type .'_help', ''));
49 }
50 }
51
52 /**
53 * Implementation of hook_cron().
54 */
55 function node_cron() {
56 db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT);
57 }
58
59 /**
60 * Gather a listing of links to nodes.
61 *
62 * @param $result
63 * 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.
64 * @param $title
65 * A heading for the resulting list.
66 *
67 * @return
68 * An HTML list suitable as content for a block.
69 */
70 function node_title_list($result, $title = NULL) {
71 while ($node = db_fetch_object($result)) {
72 $items[] = l($node->title, 'node/'. $node->nid, $node->comment_count ? array('title' => format_plural($node->comment_count, '1 comment', '%count comments')) : '');
73 }
74
75 return theme('node_list', $items, $title);
76 }
77
78 /**
79 * Format a listing of links to nodes.
80 */
81 function theme_node_list($items, $title = NULL) {
82 return theme('item_list', $items, $title);
83 }
84
85 /**
86 * Update the 'last viewed' timestamp of the specified node for current user.
87 */
88 function node_tag_new($nid) {
89 global $user;
90
91 if ($user->uid) {
92 if (node_last_viewed($nid)) {
93 db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid);
94 }
95 else {
96 @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time());
97 }
98 }
99 }
100
101 /**
102 * Retrieves the timestamp at which the current user last viewed the
103 * specified node.
104 */
105 function node_last_viewed($nid) {
106 global $user;
107 static $history;
108
109 if (!isset($history[$nid])) {
110 $history[$nid] = db_fetch_object(db_query("SELECT timestamp FROM {history} WHERE uid = '$user->uid' AND nid = %d", $nid));
111 }
112
113 return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0);
114 }
115
116 /**
117 * Decide on the type of marker to be displayed for a given node.
118 *
119 * @param $nid
120 * Node ID whose history supplies the "last viewed" timestamp.
121 * @param $timestamp
122 * Time which is compared against node's "last viewed" timestamp.
123 * @return
124 * One of the MARK constants.
125 */
126 function node_mark($nid, $timestamp) {
127 global $user;
128 static $cache;
129
130 if (!$user->uid) {
131 return MARK_READ;
132 }
133 if (!isset($cache[$nid])) {
134 $cache[$nid] = node_last_viewed($nid);
135 }
136 if ($cache[$nid] == 0 && $timestamp > NODE_NEW_LIMIT) {
137 return MARK_NEW;
138 }
139 elseif ($timestamp > $cache[$nid] && $timestamp > NODE_NEW_LIMIT) {
140 return MARK_UPDATED;
141 }
142 return MARK_READ;
143 }
144
145 /**
146 * Automatically generate a teaser for a node body in a given format.
147 */
148 function node_teaser($body, $format = NULL) {
149
150 $size = variable_get('teaser_length', 600);
151
152 // find where the delimiter is in the body
153 $delimiter = strpos($body, '<!--break-->');
154
155 // If the size is zero, and there is no delimiter, the entire body is the teaser.
156 if ($size == 0 && $delimiter === FALSE) {
157 return $body;
158 }
159
160 // We check for the presence of the PHP evaluator filter in the current
161 // format. If the body contains PHP code, we do not split it up to prevent
162 // parse errors.
163 if (isset($format)) {
164 $filters = filter_list_format($format);
165 if (isset($filters['filter/1']) && strpos($body, '<?') !== FALSE) {
166 return $body;
167 }
168 }
169
170 // If a valid delimiter has been specified, use it to chop of the teaser.
171 if ($delimiter !== FALSE) {
172 return substr($body, 0, $delimiter);
173 }
174
175 // If we have a short body, the entire body is the teaser.
176 if (strlen($body) < $size) {
177 return $body;
178 }
179
180 // In some cases, no delimiter has been specified (e.g. when posting using
181 // the Blogger API). In this case, we try to split at paragraph boundaries.
182 // When even the first paragraph is too long, we try to split at the end of
183 // the next sentence.
184 $breakpoints = array('</p>' => 4, '<br />' => 0, '<br>' => 0, "\n" => 0, '. ' => 1, '! ' => 1, '? ' => 1, '。' => 3, '؟ ' => 1);
185 foreach ($breakpoints as $point => $charnum) {
186 if ($length = strpos($body, $point, $size)) {
187 return substr($body, 0, $length + $charnum);
188 }
189 }
190
191 // If all else fails, we simply truncate the string.
192 return truncate_utf8($body, $size);
193 }
194
195 function _node_names($op = '', $node = NULL) {
196 static $node_names = array();
197 static $node_list = array();
198
199 if (empty($node_names)) {
200 $node_names = module_invoke_all('node_info');
201 foreach ($node_names as $type => $value) {
202 $node_list[$type] = $value['name'];
203 }
204 }
205 if ($node) {
206 if (is_array($node)) {
207 $type = $node['type'];
208 }
209 elseif (is_object($node)) {
210 $type = $node->type;
211 }
212 elseif (is_string($node)) {
213 $type = $node;
214 }
215 if (!isset($node_names[$type])) {
216 return FALSE;
217 }
218 }
219 switch ($op) {
220 case 'base':
221 return $node_names[$type]['base'];
222 case 'list':
223 return $node_list;
224 case 'name':
225 return $node_list[$type];
226 }
227 }
228
229 /**
230 * Determine the basename for hook_load etc.
231 *
232 * @param $node
233 * Either a node object, a node array, or a string containing the node type.
234 * @return
235 * The basename for hook_load, hook_nodeapi etc.
236 */
237 function node_get_base($node) {
238 return _node_names('base', $node);
239 }
240
241 /**
242 * Determine the human readable name for a given type.
243 *
244 * @param $node
245 * Either a node object, a node array, or a string containing the node type.
246 * @return
247 * The human readable name of the node type.
248 */
249 function node_get_name($node) {
250 return _node_names('name', $node);
251 }
252
253 /**
254 * Return the list of available node types.
255 *
256 * @param $node
257 * Either a node object, a node array, or a string containing the node type.
258 * @return
259 * An array consisting ('#type' => name) pairs.
260 */
261 function node_get_types() {
262 return _node_names('list');
263 }
264
265 /**
266 * Determine whether a node hook exists.
267 *
268 * @param &$node
269 * Either a node object, node array, or a string containing the node type.
270 * @param $hook
271 * A string containing the name of the hook.
272 * @return
273 * TRUE iff the $hook exists in the node type of $node.
274 */
275 function node_hook(&$node, $hook) {
276 return module_hook(node_get_base($node), $hook);
277 }
278
279 /**
280 * Invoke a node hook.
281 *
282 * @param &$node
283 * Either a node object, node array, or a string containing the node type.
284 * @param $hook
285 * A string containing the name of the hook.
286 * @param $a2, $a3, $a4
287 * Arguments to pass on to the hook, after the $node argument.
288 * @return
289 * The returned value of the invoked hook.
290 */
291 function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
292 if (node_hook($node, $hook)) {
293 $function = node_get_base($node) ."_$hook";
294 return ($function($node, $a2, $a3, $a4));
295 }
296 }
297
298 /**
299 * Invoke a hook_nodeapi() operation in all modules.
300 *
301 * @param &$node
302 * A node object.
303 * @param $op
304 * A string containing the name of the nodeapi operation.
305 * @param $a3, $a4
306 * Arguments to pass on to the hook, after the $node and $op arguments.
307 * @return
308 * The returned value of the invoked hooks.
309 */
310 function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
311 $return = array();
312 foreach (module_implements('nodeapi') as $name) {
313 $function = $name .'_nodeapi';
314 $result = $function($node, $op, $a3, $a4);
315 if (isset($result) && is_array($result)) {
316 $return = array_merge($return, $result);
317 }
318 else if (isset($result)) {
319 $return[] = $result;
320 }
321 }
322 return $return;
323 }
324
325 /**
326 * Load a node object from the database.
327 *
328 * @param $param
329 * Either the nid of the node or an array of conditions to match against in the database query
330 * @param $revision
331 * Which numbered revision to load. Defaults to the current version.
332 * @param $reset
333 * Whether to reset the internal node_load cache.
334 *
335 * @return
336 * A fully-populated node object.
337 */
338 function node_load($param = array(), $revision = NULL, $reset = NULL) {
339 static $nodes = array();
340
341 if ($reset) {
342 $nodes = array();
343 }
344
345 $arguments = array();
346 if (is_numeric($param)) {
347 $cachable = $revision == NULL;
348 if ($cachable && isset($nodes[$param])) {
349 return $nodes[$param];
350 }
351 $cond = 'n.nid = %d';
352 $arguments[] = $param;
353 }
354 else {
355 // Turn the conditions into a query.
356 foreach ($param as $key => $value) {
357 $cond[] = 'n.'. db_escape_string($key) ." = '%s'";
358 $arguments[] = $value;
359 }
360 $cond = implode(' AND ', $cond);
361 }
362
363 // Retrieve the node.
364 if ($revision) {
365 array_unshift($arguments, $revision);
366 $node = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, r.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, 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));
367 }
368 else {
369 $node = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, 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));
370 }
371
372 if ($node->nid) {
373 // Call the node specific callback (if any) and piggy-back the
374 // results to the node or overwrite some values.
375 if ($extra = node_invoke($node, 'load')) {
376 foreach ($extra as $key => $value) {
377 $node->$key = $value;
378 }
379 }
380
381 if ($extra = node_invoke_nodeapi($node, 'load')) {
382 foreach ($extra as $key => $value) {
383 $node->$key = $value;
384 }
385 }
386 }
387
388 if ($cachable) {
389 $nodes[$param] = $node;
390 }
391
392 return $node;
393 }
394
395 /**
396 * Save a node object into the database.
397 */
398 function node_save(&$node) {
399 global $user;
400
401 $node->is_new = FALSE;
402
403 // Apply filters to some default node fields:
404 if (empty($node->nid)) {
405 // Insert a new node.
406 $node->is_new = TRUE;
407
408 $node->nid = db_next_id('{node}_nid');
409 $node->vid = db_next_id('{node_revisions}_vid');;
410 }
411 else {
412 // We need to ensure that all node fields are filled.
413 $node_current = node_load($node->nid);
414 foreach ($node as $field => $data) {
415 $node_current->$field = $data;
416 }
417 $node = $node_current;
418
419 if ($node->revision) {
420 $node->old_vid = $node->vid;
421 $node->vid = db_next_id('{node_revisions}_vid');
422 }
423 }
424
425 // Set some required fields:
426 if (empty($node->created)) {
427 $node->created = time();
428 }
429 // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
430 $node->changed = time();
431
432 // Split off revisions data to another structure
433 $revisions_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
434 'title' => $node->title, 'body' => $node->body,
435 'teaser' => $node->teaser, 'log' => $node->log, 'timestamp' => $node->changed,
436 'uid' => $user->uid, 'format' => $node->format);
437 $revisions_table_types = array('nid' => '%d', 'vid' => '%d',
438 'title' => "'%s'", 'body' => "'%s'",
439 'teaser' => "'%s'", 'log' => "'%s'", 'timestamp' => '%d',
440 'uid' => '%d', 'format' => '%d');
441 $node_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
442 'title' => $node->title, 'type' => $node->type, 'uid' => $node->uid,
443 'status' => $node->status, 'created' => $node->created,
444 'changed' => $node->changed, 'comment' => $node->comment,
445 'promote' => $node->promote, 'sticky' => $node->sticky);
446 $node_table_types = array('nid' => '%d', 'vid' => '%d',
447 'title' => "'%s'", 'type' => "'%s'", 'uid' => '%d',
448 'status' => '%d', 'created' => '%d',
449 'changed' => '%d', 'comment' => '%d',
450 'promote' => '%d', 'sticky' => '%d');
451
452 //Generate the node table query and the
453 //the node_revisions table query
454 if ($node->is_new) {
455 $node_query = 'INSERT INTO {node} ('. implode(', ', array_keys($node_table_types)) .') VALUES ('. implode(', ', $node_table_types) .')';
456 $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
457 }
458 else {
459 $arr = array();
460 foreach ($node_table_types as $key => $value) {
461 $arr[] = $key .' = '. $value;
462 }
463 $node_table_values[] = $node->nid;
464 $node_query = 'UPDATE {node} SET '. implode(', ', $arr) .' WHERE nid = %d';
465 if ($node->revision) {
466 $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
467 }
468 else {
469 $arr = array();
470 foreach ($revisions_table_types as $key => $value) {
471 $arr[] = $key .' = '. $value;
472 }
473 $revisions_table_values[] = $node->vid;
474 $revisions_query = 'UPDATE {node_revisions} SET '. implode(', ', $arr) .' WHERE vid = %d';
475 }
476 }
477
478 // Insert the node into the database:
479 db_query($node_query, $node_table_values);
480 db_query($revisions_query, $revisions_table_values);
481
482 // Call the node specific callback (if any):
483 if ($node->is_new) {
484 node_invoke($node, 'insert');
485 node_invoke_nodeapi($node, 'insert');
486 }
487 else {
488 node_invoke($node, 'update');
489 node_invoke_nodeapi($node, 'update');
490 }
491
492 // Clear the cache so an anonymous poster can see the node being added or updated.
493 cache_clear_all();
494 }
495
496 /**
497 * Generate a display of the given node.
498 *
499 * @param $node
500 * A node array or node object.
501 * @param $teaser
502 * Whether to display the teaser only, as on the main page.
503 * @param $page
504 * Whether the node is being displayed by itself as a page.
505 * @param $links
506 * Whether or not to display node links. Links are omitted for node previews.
507 *
508 * @return
509 * An HTML representation of the themed node.
510 */
511 function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) {
512 $node = (object)$node;
513
514 // Remove the delimiter (if any) that separates the teaser from the body.
515 // TODO: this strips legitimate uses of '<!--break-->' also.
516 $node->body = str_replace('<!--break-->', '', $node->body);
517
518 if ($node->log != '' && !$teaser) {
519 $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. filter_xss($node->log) .'</div>';
520 }
521
522 // The 'view' hook can be implemented to overwrite the default function
523 // to display nodes.
524 if (node_hook($node, 'view')) {
525 node_invoke($node, 'view', $teaser, $page);
526 }
527 else {
528 $node = node_prepare($node, $teaser);
529 }
530 // Allow modules to change $node->body before viewing.
531 node_invoke_nodeapi($node, 'view', $teaser, $page);
532 if ($links) {
533 $node->links = module_invoke_all('link', 'node', $node, !$page);
534
535 foreach (module_implements('link_alter') AS $module) {
536 $function = $module .'_link_alter';
537 $function($node, $node->links);
538 }
539 }
540 // unset unused $node part so that a bad theme can not open a security hole
541 if ($teaser) {
542 unset($node->body);
543 }
544 else {
545 unset($node->teaser);
546 }
547
548 return theme('node', $node, $teaser, $page);
549 }
550
551 /**
552 * Apply filters to a node in preparation for theming.
553 */
554 function node_prepare($node, $teaser = FALSE) {
555 $node->readmore = (strlen($node->teaser) < strlen($node->body));
556 if ($teaser == FALSE) {
557 $node->body = check_markup($node->body, $node->format, FALSE);
558 }
559 else {
560 $node->teaser = check_markup($node->teaser, $node->format, FALSE);
561 }
562 return $node;
563 }
564
565 /**
566 * Generate a page displaying a single node, along with its comments.
567 */
568 function node_show($node, $cid) {
569 $output = node_view($node, FALSE, TRUE);
570
571 if (function_exists('comment_render') && $node->comment) {
572 $output .= comment_render($node, $cid);
573 }
574
575 // Update the history table, stating that this user viewed this node.
576 node_tag_new($node->nid);
577
578 return $output;
579 }
580
581 /**
582 * Implementation of hook_perm().
583 */
584 function node_perm() {
585 return array('administer nodes', 'access content', 'view revisions', 'revert revisions');
586 }
587
588 /**
589 * Implementation of hook_search().
590 */
591 function node_search($op = 'search', $keys = NULL) {
592 switch ($op) {
593 case 'name':
594 return t('content');
595
596 case 'reset':
597 variable_del('node_cron_last');
598 variable_del('node_cron_last_nid');
599 return;
600
601 case 'status':
602 $last = variable_get('node_cron_last', 0);
603 $last_nid = variable_get('node_cron_last_nid', 0);
604 $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
605 $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));
606 return array('remaining' => $remaining, 'total' => $total);
607
608 case 'admin':
609 $form = array();
610 // Output form for defining rank factor weights.
611 $form['content_ranking'] = array('#type' => 'fieldset', '#title' => t('Content ranking'));
612 $form['content_ranking']['#theme'] = 'node_search_admin';
613 $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>');
614
615 $ranking = array('node_rank_relevance' => t('Keyword relevance'),
616 'node_rank_recent' => t('Recently posted'));
617 if (module_exist('comment')) {
618 $ranking['node_rank_comments'] = t('Number of comments');
619 }
620 if (module_exist('statistics') && variable_get('statistics_count_content_views', 0)) {
621 $ranking['node_rank_views'] = t('Number of views');
622 }
623
624 // Note: reversed to reflect that higher number = higher ranking.
625 $options = drupal_map_assoc(range(0, 10));
626 foreach ($ranking as $var => $title) {
627 $form['content_ranking']['factors'][$var] = array('#title' => $title, '#type' => 'select', '#options' => $options, '#default_value' => variable_get($var, 5));
628 }
629 return $form;
630
631 case 'search':
632 // Build matching conditions
633 list($join1, $where1) = _db_rewrite_sql();
634 $arguments1 = array();
635 $conditions1 = 'n.status = 1';
636
637 if ($type = search_query_extract($keys, 'type')) {
638 $types = array();
639 foreach (explode(',', $type) as $t) {
640 $types[] = "n.type = '%s'";
641 $arguments1[] = $t;
642 }
643 $conditions1 .= ' AND ('. implode(' OR ', $types) .')';
644 $keys = search_query_insert($keys, 'type');
645 }
646
647 if ($category = search_query_extract($keys, 'category')) {
648 $categories = array();
649 foreach (explode(',', $category) as $c) {
650 $categories[] = "tn.tid = %d";
651 $arguments1[] = $c;
652 }
653 $conditions1 .= ' AND ('. implode(' OR ', $categories) .')';
654 $join1 .= ' INNER JOIN {term_node} tn ON n.nid = tn.nid';
655 $keys = search_query_insert($keys, 'category');
656 }
657
658 // Build ranking expression (we try to map each parameter to a
659 // uniform distribution in the range 0..1).
660 $ranking = array();
661 $arguments2 = array();
662 $join2 = '';
663 // Used to avoid joining on node_comment_statistics twice
664 $stats_join = FALSE;
665 if ($weight = (int)variable_get('node_rank_relevance', 5)) {
666 // Average relevance values hover around 0.15
667 $ranking[] = '%d * i.relevance';
668 $arguments2[] = $weight;
669 }
670 if ($weight = (int)variable_get('node_rank_recent', 5)) {
671 // Exponential decay with half-life of 6 months, starting at last indexed node
672 $ranking[] = '%d * POW(2, (GREATEST(n.created, n.changed, c.last_comment_timestamp) - %d) * 6.43e-8)';
673 $arguments2[] = $weight;
674 $arguments2[] = (int)variable_get('node_cron_last', 0);
675 $join2 .= ' INNER JOIN {node} n ON n.nid = i.sid LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
676 $stats_join = TRUE;
677 }
678 if (module_exist('comment') && $weight = (int)variable_get('node_rank_comments', 5)) {
679 // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
680 $scale = variable_get('node_cron_comments_scale', 0.0);
681 $ranking[] = '%d * (2.0 - 2.0 / (1.0 + c.comment_count * %f))';
682 $arguments2[] = $weight;
683 $arguments2[] = $scale;
684 if (!$stats_join) {
685 $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
686 }
687 }
688 if (module_exist('statistics') && variable_get('statistics_count_content_views', 0) &&
689 $weight = (int)variable_get('node_rank_views', 5)) {
690 // Inverse law that maps the highest view count on the site to 1 and 0 to 0.
691 $scale = variable_get('node_cron_views_scale', 0.0);
692 $ranking[] = '%d * (2.0 - 2.0 / (1.0 + nc.totalcount * %f))';
693 $arguments2[] = $weight;
694 $arguments2[] = $scale;
695 $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid';
696 }
697 $select2 = (count($ranking) ? implode(' + ', $ranking) : 'i.relevance') . ' AS score';
698
699 // Do search
700 $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);
701
702 // Load results
703 $results = array();
704 foreach ($find as $item) {
705 $node = node_load($item->sid);
706
707 // Get node output (filtered and with module-specific fields).
708 if (node_hook($node, 'view')) {
709 node_invoke($node, 'view', FALSE, FALSE);
710 }
711 else {
712 $node = node_prepare($node, FALSE);
713 }
714 // Allow modules to change $node->body before viewing.
715 node_invoke_nodeapi($node, 'view', FALSE, FALSE);
716
717 // Fetch comments for snippet
718 $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
719 // Fetch terms for snippet
720 $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
721
722 $extra = node_invoke_nodeapi($node, 'search result');
723 $results[] = array('link' => url('node/'. $item->sid),
724 'type' => node_get_name($node),
725 'title' => $node->title,
726 'user' => theme('username', $node),
727 'date' => $node->changed,
728 'node' => $node,
729 'extra' => $extra,
730 'snippet' => search_excerpt($keys, $node->body));
731 }
732 return $results;
733 }
734 }
735
736 /**
737 * Implementation of hook_user().
738 */
739 function node_user($op, &$edit, &$user) {
740 if ($op == 'delete') {
741 db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid);
742 db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid);
743 }
744 }
745
746 function theme_node_search_admin($form) {
747 $output = form_render($form['info']);
748
749 $header = array(t('Factor'), t('Weight'));
750 foreach (element_children($form['factors']) as $key) {
751 $row = array();
752 $row[] = $form['factors'][$key]['#title'];
753 unset($form['factors'][$key]['#title']);
754 $row[] = form_render($form['factors'][$key]);
755 $rows[] = $row;
756 }
757 $output .= theme('table', $header, $rows);
758
759 $output .= form_render($form);
760 return $output;
761 }
762
763 /**
764 * Menu callback; presents general node configuration options.
765 */
766 function node_configure() {
767
768 $form['default_nodes_main'] = array(
769 '#type' => 'select', '#title' => t('Number of posts on main page'), '#default_value' => variable_get('default_nodes_main', 10),
770 '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
771 '#description' => t('The default maximum number of posts to display per page on overview pages such as the main page.')
772 );
773
774 $form['teaser_length'] = array(
775 '#type' => 'select', '#title' => t('Length of trimmed posts'), '#default_value' => variable_get('teaser_length', 600),
776 '#options' => array(0 => t('Unlimited'), 200 => t('200 characters'), 400 => t('400 characters'), 600 => t('600 characters'),
777 800 => t('800 characters'), 1000 => t('1000 characters'), 1200 => t('1200 characters'), 1400 => t('1400 characters'),
778 1600 => t('1600 characters'), 1800 => t('1800 characters'), 2000 => t('2000 characters')),
779 '#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.")
780 );
781
782 $form['node_preview'] = array(
783 '#type' => 'radios', '#title' => t('Preview post'), '#default_value' => variable_get('node_preview', 0),
784 '#options' => array(t('Optional'), t('Required')), '#description' => t('Must users preview posts before submitting?')
785 );
786
787 return system_settings_form('node_configure', $form);
788 }
789
790 /**
791 * Retrieve the comment mode for the given node ID (none, read, or read/write).
792 */
793 function node_comment_mode($nid) {
794 static $comment_mode;
795 if (!isset($comment_mode[$nid])) {
796 $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid));
797 }
798 return $comment_mode[$nid];
799 }
800
801 /**
802 * Implementation of hook_link().
803 */
804 function node_link($type, $node = 0, $main = 0) {
805 $links = array();
806
807 if ($type == 'node') {
808 if (array_key_exists('links', $node)) {
809 $links = $node->links;
810 }
811
812 if ($main == 1 && $node->teaser && $node->readmore) {
813 $links['node_read_more'] = array(
814 'title' => t('read more'),
815 'href' => "node/$node->nid",
816 'attributes' => array('title' => t('Read the rest of this posting.'))
817 );
818 }
819 }
820
821 return $links;
822 }
823
824 /**
825 * Implementation of hook_menu().
826 */
827 function node_menu($may_cache) {
828 $items = array();
829
830 if ($may_cache) {
831 $items[] = array('path' => 'admin/node', 'title' => t('content'),
832 'callback' => 'node_admin_nodes',
833 'access' => user_access('administer nodes'));
834 $items[] = array('path' => 'admin/node/overview', 'title' => t('list'),
835 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
836
837 if (module_exist('search')) {
838 $items[] = array('path' => 'admin/node/search', 'title' => t('search'),
839 'callback' => 'node_admin_search',
840 'access' => user_access('administer nodes'),
841 'type' => MENU_LOCAL_TASK);
842 }
843
844 $items[] = array('path' => 'admin/settings/node', 'title' => t('posts'),
845 'callback' => 'node_configure',
846 'access' => user_access('administer nodes'));
847 $items[] = array('path' => 'admin/settings/content-types', 'title' => t('content types'),
848 'callback' => 'node_types_configure',
849 'access' => user_access('administer nodes'));
850
851 $items[] = array('path' => 'node', 'title' => t('content'),
852 'callback' => 'node_page',
853 'access' => user_access('access content'),
854 'type' => MENU_MODIFIABLE_BY_ADMIN);
855 $items[] = array('path' => 'node/add', 'title' => t('create content'),
856 'callback' => 'node_page',
857 'access' => user_access('access content'),
858 'type' => MENU_ITEM_GROUPING,
859 'weight' => 1);
860 $items[] = array('path' => 'rss.xml', 'title' => t('rss feed'),
861 'callback' => 'node_feed',
862 'access' => user_access('access content'),
863 'type' => MENU_CALLBACK);
864 }
865 else {
866 if (arg(0) == 'node' && is_numeric(arg(1))) {
867 $node = node_load(arg(1));
868 if ($node->nid) {
869 $items[] = array('path' => 'node/'. arg(1), 'title' => t('view'),
870 'callback' => 'node_page',
871 'access' => node_access('view', $node),
872 'type' => MENU_CALLBACK);
873 $items[] = array('path' => 'node/'. arg(1) .'/view', 'title' => t('view'),
874 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
875 $items[] = array('path' => 'node/'. arg(1) .'/edit', 'title' => t('edit'),
876 'callback' => 'node_page',
877 'access' => node_access('update', $node),
878 'weight' => 1,
879 'type' => MENU_LOCAL_TASK);
880 $items[] = array('path' => 'node/'. arg(1) .'/delete', 'title' => t('delete'),
881 'callback' => 'node_delete_confirm',
882 'access' => node_access('delete', $node),
883 'weight' => 1,
884 'type' => MENU_CALLBACK);
885 $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);
886 $items[] = array('path' => 'node/'. arg(1) .'/revisions', 'title' => t('revisions'),
887 'callback' => 'node_revisions',
888 'access' => $revisions_access,
889 'weight' => 2,
890 'type' => MENU_LOCAL_TASK);
891 }
892 }
893 else if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'content-types' && is_string(arg(3))) {
894 $items[] = array('path' => 'admin/settings/content-types/'. arg(3),
895 'title' => t("'%name' content type", array('%name' => node_get_name(arg(3)))),
896 'type' => MENU_CALLBACK);
897 }
898 }
899
900 return $items;
901 }
902
903 function node_last_changed($nid) {
904 $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
905 return ($node->changed);
906 }
907
908 /**
909 * List node administration operations that can be performed.
910 */
911 function node_operations() {
912 $operations = array(
913 'approve' => array(t('Approve the selected posts'), 'UPDATE {node} SET status = 1 WHERE nid = %d'),
914 'promote' => array(t('Promote the selected posts'), 'UPDATE {node} SET status = 1, promote = 1 WHERE nid = %d'),
915 'sticky' => array(t('Make the selected posts sticky'), 'UPDATE {node} SET status = 1, sticky = 1 WHERE nid = %d'),
916 'demote' => array(t('Demote the selected posts'), 'UPDATE {node} SET promote = 0 WHERE nid = %d'),
917 'unpublish' => array(t('Unpublish the selected posts'), 'UPDATE {node} SET status = 0 WHERE nid = %d'),
918 'delete' => array(t('Delete the selected posts'), '')
919 );
920 return $operations;
921 }
922
923 /**
924 * List node administration filters that can be applied.
925 */
926 function node_filters() {
927 // Regular filters
928 $filters['status'] = array('title' => t('status'),
929 'options' => array('status-1' => t('published'), 'status-0' => t('not published'),
930 'promote-1' => t('promoted'), 'promote-0' => t('not promoted'),
931 'sticky-1' => t('sticky'), 'sticky-0' => t('not sticky')));
932 $filters['type'] = array('title' => t('type'), 'options' => node_get_types());
933 // The taxonomy filter
934 if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
935 $filters['category'] = array('title' => t('category'), 'options' => $taxonomy);
936 }
937
938 return $filters;
939 }
940
941 /**
942 * Build query for node administration filters based on session.
943 */
944 function node_build_filter_query() {
945 $filters = node_filters();
946
947 // Build query
948 $where = $args = array();
949 $join = '';
950 foreach ($_SESSION['node_overview_filter'] as $index => $filter) {
951 list($key, $value) = $filter;
952 switch($key) {
953 case 'status':
954 // Note: no exploitable hole as $key/$value have already been checked when submitted
955 list($key, $value) = explode('-', $value, 2);
956 $where[] = 'n.'. $key .' = %d';
957 break;
958 case 'category':
959 $table = "tn$index";
960 $where[] = "$table.tid = %d";
961 $join .= "INNER JOIN {term_node} $table ON n.nid = $table.nid ";
962 break;
963 case 'type':
964 $where[] = "n.type = '%s'";
965 }
966 $args[] = $value;
967 }
968 $where = count($where) ? 'WHERE '. implode(' AND ', $where) : '';
969
970 return array('where' => $where, 'join' => $join, 'args' => $args);
971 }
972
973 /**
974 * Return form for node administration filters.
975 */
976 function node_filter_form() {
977 $session = &$_SESSION['node_overview_filter'];
978 $session = is_array($session) ? $session : array();
979 $filters = node_filters();
980
981 $i = 0;
982 $form['filters'] = array('#type' => 'fieldset',
983 '#title' => t('Show only items where'),
984 '#theme' => 'node_filters',
985 );
986 foreach ($session as $filter) {
987 list($type, $value) = $filter;
988 if ($type == 'category') {
989 // Load term name from DB rather than search and parse options array.
990 $value = module_invoke('taxonomy', 'get_term', $value);
991 $value = $value->name;
992 }
993 else {
994 $value = $filters[$type]['options'][$value];
995 }
996 $string = ($i++ ? '<em>and</em> where <strong>%a</strong> is <strong>%b</strong>' : '<strong>%a</strong> is <strong>%b</strong>');
997 $form['filters']['current'][] = array('#value' => t($string, array('%a' => $filters[$type]['title'] , '%b' => $value)));
998 }
999
1000 foreach ($filters as $key => $filter) {
1001 $names[$key] = $filter['title'];
1002 $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
1003 }
1004
1005 $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
1006 $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
1007 if (count($session)) {
1008 $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
1009 $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
1010 }
1011
1012 return drupal_get_form('node_filter_form', $form);
1013 }
1014
1015 /**
1016 * Theme node administration filter form.
1017 */
1018 function theme_node_filter_form(&$form) {
1019 $output .= '<div id="node-admin-filter">';
1020 $output .= form_render($form['filters']);
1021 $output .= '</div>';
1022 $output .= form_render($form);
1023 return $output;
1024 }
1025
1026 /**
1027 * Theme node administraton filter selector.
1028 */
1029 function theme_node_filters(&$form) {
1030 $output .= '<ul>';
1031 if (sizeof($form['current'])) {
1032 foreach (element_children($form['current']) as $key) {
1033 $output .= '<li>' . form_render($form['current'][$key]) . '</li>';
1034 }
1035 }
1036
1037 $output .= '<li><dl class="multiselect">' . (sizeof($form['current']) ? '<dt><em>'. t('and') .'</em> '. t('where') .'</dt>' : '') . '<dd class="a">';
1038 foreach (element_children($form['filter']) as $key) {
1039 $output .= form_render($form['filter'][$key]);
1040 }
1041 $output .= '</dd>';
1042
1043 $output .= '<dt>'. t('is') .'</dt>' . '<dd class="b">';
1044
1045 foreach (element_children($form['status']) as $key) {
1046 $output .= form_render($form['status'][$key]);
1047 }
1048 $output .= '</dd>';
1049
1050 $output .= '</dl>';
1051 $output .= '<div class="container-inline" id="node-admin-buttons">'. form_render($form['buttons']) .'</div>';
1052 $output .= '</li></ul><br class="clear" />';
1053
1054 return $output;
1055 }
1056
1057 /**
1058 * Process result from node administration filter form.
1059 */
1060 function node_filter_form_submit() {
1061 global $form_values;
1062 $op = $_POST['op'];
1063 $filters = node_filters();
1064 switch ($op) {
1065 case t('Filter'):
1066 case t('Refine'):
1067 if (isset($form_values['filter'])) {
1068 $filter = $form_values['filter'];
1069
1070