/[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.641.2.30 - (show annotations) (download) (as text)
Thu Jan 4 19:50:28 2007 UTC (2 years, 10 months ago) by killes
Branch: DRUPAL-4-7
Changes since 1.641.2.29: +28 -11 lines
File MIME type: text/x-php
#73910, node_teaser() not breaking properly, backport
1 <?php
2 // $Id: node.module,v 1.641.2.29 2007/01/01 22:16:11 killes 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/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'])) {