node.module 86 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
   
Dries committed
4
5
6
7
8
/**
 * @file
 * The core that allows content to be submitted to the site.
 */

9
10
define('NODE_NEW_LIMIT', time() - 30 * 24 * 60 * 60);

Dries's avatar
   
Dries committed
11
12
13
14
/**
 * Implementation of hook_help().
 */
function node_help($section) {
15
  switch ($section) {
Dries's avatar
   
Dries committed
16
    case 'admin/help#node':
17
18
19
20
21
22
23
24
25
26
27
28
29
      $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>';
      $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>';
      $output .= t('<p>Node module features</p>
<ul>
<li>The list tab provides an interface to search and sort all content on your site.</li>
<li>The configure settings tab has basic settings for content on your site.</li>
<li>The configure content types tab lists all content types for your site and lets you configure their default workflow.</li>
<li>The search tab lets you search all content on your site</li>
</ul>
');
      $output .= t('<p>You can</p>
<ul>
<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>';
Dries's avatar
   
Dries committed
34
      return $output;
Dries's avatar
   
Dries committed
35
    case 'admin/modules#description':
36
      return t('Allows content to be submitted to the site and displayed on pages.');
37
38
    case 'admin/node/configure':
    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>');
Dries's avatar
   
Dries committed
44
  }
Dries's avatar
   
Dries committed
45
46
47
48

  if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions') {
    return t('The revisions let you track differences between multiple versions of a post.');
  }
49
50

  if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) {
51
    return filter_xss_admin(variable_get($type .'_help', ''));
52
  }
Dries's avatar
   
Dries committed
53
54
}

Dries's avatar
   
Dries committed
55
56
57
/**
 * Implementation of hook_cron().
 */
58
function node_cron() {
Dries's avatar
   
Dries committed
59
  db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT);
60
61
}

Dries's avatar
   
Dries committed
62
63
64
65
/**
 * Gather a listing of links to nodes.
 *
 * @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.
Dries's avatar
   
Dries committed
67
68
69
70
71
72
 * @param $title
 *   A heading for the resulting list.
 *
 * @return
 *   An HTML list suitable as content for a block.
 */
Dries's avatar
   
Dries committed
73
74
function node_title_list($result, $title = NULL) {
  while ($node = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
75
    $items[] = l($node->title, 'node/'. $node->nid, $node->comment_count ? array('title' => format_plural($node->comment_count, '1 comment', '%count comments')) : '');
Dries's avatar
   
Dries committed
76
77
  }

Dries's avatar
   
Dries committed
78
  return theme('node_list', $items, $title);
Dries's avatar
   
Dries committed
79
80
}

Dries's avatar
   
Dries committed
81
82
83
/**
 * Format a listing of links to nodes.
 */
Dries's avatar
   
Dries committed
84
function theme_node_list($items, $title = NULL) {
Dries's avatar
   
Dries committed
85
  return theme('item_list', $items, $title);
Dries's avatar
   
Dries committed
86
87
}

Dries's avatar
   
Dries committed
88
89
90
/**
 * Update the 'last viewed' timestamp of the specified node for current user.
 */
Dries's avatar
   
Dries committed
91
92
93
94
function node_tag_new($nid) {
  global $user;

  if ($user->uid) {
Dries's avatar
   
Dries committed
95
    if (node_last_viewed($nid)) {
Dries's avatar
   
Dries committed
96
      db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid);
Dries's avatar
   
Dries committed
97
98
    }
    else {
Dries's avatar
   
Dries committed
99
      @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time());
Dries's avatar
   
Dries committed
100
101
102
103
    }
  }
}

Dries's avatar
   
Dries committed
104
105
106
107
/**
 * Retrieves the timestamp at which the current user last viewed the
 * specified node.
 */
Dries's avatar
   
Dries committed
108
109
function node_last_viewed($nid) {
  global $user;
Dries's avatar
   
Dries committed
110
  static $history;
Dries's avatar
   
Dries committed
111

Dries's avatar
   
Dries committed
112
113
114
115
  if (!isset($history[$nid])) {
    $history[$nid] = db_fetch_object(db_query("SELECT timestamp FROM {history} WHERE uid = '$user->uid' AND nid = %d", $nid));
  }

116
  return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0);
Dries's avatar
   
Dries committed
117
118
119
}

/**
120
 * Decide on the type of marker to be displayed for a given node.
Dries's avatar
   
Dries committed
121
 *
Dries's avatar
   
Dries committed
122
123
124
125
 * @param $nid
 *   Node ID whose history supplies the "last viewed" timestamp.
 * @param $timestamp
 *   Time which is compared against node's "last viewed" timestamp.
126
127
 * @return
 *   One of the MARK constants.
Dries's avatar
   
Dries committed
128
 */
129
function node_mark($nid, $timestamp) {
Dries's avatar
   
Dries committed
130
131
132
  global $user;
  static $cache;

133
134
135
  if (!$user->uid) {
    return MARK_READ;
  }
Dries's avatar
Dries committed
136
  if (!isset($cache[$nid])) {
137
    $cache[$nid] = node_last_viewed($nid);
Dries's avatar
   
Dries committed
138
  }
139
140
141
142
143
144
145
  if ($cache[$nid] == 0 && $timestamp > NODE_NEW_LIMIT) {
    return MARK_NEW;
  }
  elseif ($timestamp > $cache[$nid] && $timestamp > NODE_NEW_LIMIT) {
    return MARK_UPDATED;
  }
  return MARK_READ;
Dries's avatar
   
Dries committed
146
147
}

Dries's avatar
   
Dries committed
148
/**
149
 * Automatically generate a teaser for a node body in a given format.
Dries's avatar
   
Dries committed
150
 */
151
function node_teaser($body, $format = NULL) {
Dries's avatar
   
Dries committed
152

Dries's avatar
   
Dries committed
153
  $size = variable_get('teaser_length', 600);
Dries's avatar
   
Dries committed
154

Dries's avatar
   
Dries committed
155
156
157
  // find where the delimiter is in the body
  $delimiter = strpos($body, '<!--break-->');

158
  // If the size is zero, and there is no delimiter, the entire body is the teaser.
159
  if ($size == 0 && $delimiter === FALSE) {
Dries's avatar
   
Dries committed
160
161
    return $body;
  }
Dries's avatar
   
Dries committed
162

163
164
165
166
167
  // We check for the presence of the PHP evaluator filter in the current
  // format. If the body contains PHP code, we do not split it up to prevent
  // parse errors.
  if (isset($format)) {
    $filters = filter_list_format($format);
168
    if (isset($filters['filter/1']) && strpos($body, '<?') !== FALSE) {
169
170
      return $body;
    }
171
172
  }

Dries's avatar
   
Dries committed
173
  // If a valid delimiter has been specified, use it to chop of the teaser.
174
  if ($delimiter !== FALSE) {
Dries's avatar
   
Dries committed
175
176
177
    return substr($body, 0, $delimiter);
  }

178
  // If we have a short body, the entire body is the teaser.
Dries's avatar
   
Dries committed
179
180
181
182
  if (strlen($body) < $size) {
    return $body;
  }

Dries's avatar
   
Dries committed
183
184
  // In some cases, no delimiter has been specified (e.g. when posting using
  // the Blogger API). In this case, we try to split at paragraph boundaries.
185
  // When even the first paragraph is too long, we try to split at the end of
Dries's avatar
   
Dries committed
186
  // the next sentence.
187
  $breakpoints = array('</p>' => 4, '<br />' => 0, '<br>' => 0, "\n" => 0, '. ' => 1, '! ' => 1, '? ' => 1, '。' => 3, '؟ ' => 1);
188
189
190
191
  foreach ($breakpoints as $point => $charnum) {
    if ($length = strpos($body, $point, $size)) {
      return substr($body, 0, $length + $charnum);
    }
192
  }
Dries's avatar
Dries committed
193

194
  // If all else fails, we simply truncate the string.
195
  return truncate_utf8($body, $size);
Dries's avatar
   
Dries committed
196
197
}

198
function _node_names($op = '', $node = NULL) {
199
200
  static $node_names = array();
  static $node_list = array();
201

202
  if (empty($node_names)) {
203
    $node_names = module_invoke_all('node_info');
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
    foreach ($node_names as $type => $value) {
      $node_list[$type] = $value['name'];
    }
  }
  if ($node) {
    if (is_array($node)) {
      $type = $node['type'];
    }
    elseif (is_object($node)) {
      $type = $node->type;
    }
    elseif (is_string($node)) {
      $type = $node;
    }
    if (!isset($node_names[$type])) {
      return FALSE;
    }
  }
  switch ($op) {
    case 'base':
      return $node_names[$type]['base'];
    case 'list':
      return $node_list;
    case 'name':
      return $node_list[$type];
  }
}

232
/**
233
 * Determine the basename for hook_load etc.
Dries's avatar
   
Dries committed
234
 *
235
 * @param $node
Dries's avatar
   
Dries committed
236
 *   Either a node object, a node array, or a string containing the node type.
Dries's avatar
   
Dries committed
237
 * @return
238
 *   The basename for hook_load, hook_nodeapi etc.
Dries's avatar
   
Dries committed
239
 */
240
241
242
function node_get_base($node) {
  return _node_names('base', $node);
}
243

244
245
246
247
248
249
250
251
252
253
/**
 * Determine the human readable name for a given type.
 *
 * @param $node
 *   Either a node object, a node array, or a string containing the node type.
 * @return
 *   The human readable name of the node type.
 */
function node_get_name($node) {
  return _node_names('name', $node);
Dries's avatar
   
Dries committed
254
}
Dries's avatar
   
Dries committed
255

256
/**
257
 * Return the list of available node types.
Dries's avatar
   
Dries committed
258
 *
259
260
 * @param $node
 *   Either a node object, a node array, or a string containing the node type.
Dries's avatar
   
Dries committed
261
 * @return
262
 *   An array consisting ('#type' => name) pairs.
Dries's avatar
   
Dries committed
263
 */
264
265
function node_get_types() {
  return _node_names('list');
Dries's avatar
   
Dries committed
266
}
Dries's avatar
   
Dries committed
267

268
/**
Dries's avatar
   
Dries committed
269
270
271
272
273
274
275
276
277
278
 * Determine whether a node hook exists.
 *
 * @param &$node
 *   Either a node object, node array, or a string containing the node type.
 * @param $hook
 *   A string containing the name of the hook.
 * @return
 *   TRUE iff the $hook exists in the node type of $node.
 */
function node_hook(&$node, $hook) {
279
  return module_hook(node_get_base($node), $hook);
Dries's avatar
   
Dries committed
280
281
}

282
/**
Dries's avatar
   
Dries committed
283
284
285
286
287
288
289
290
291
 * Invoke a node hook.
 *
 * @param &$node
 *   Either a node object, node array, or a string containing the node type.
 * @param $hook
 *   A string containing the name of the hook.
 * @param $a2, $a3, $a4
 *   Arguments to pass on to the hook, after the $node argument.
 * @return
Dries's avatar
   
Dries committed
292
 *   The returned value of the invoked hook.
Dries's avatar
   
Dries committed
293
294
 */
function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
295
296
  if (node_hook($node, $hook)) {
    $function = node_get_base($node) ."_$hook";
Dries's avatar
   
Dries committed
297
    return ($function($node, $a2, $a3, $a4));
Dries's avatar
   
Dries committed
298
299
300
  }
}

Dries's avatar
   
Dries committed
301
302
303
304
/**
 * Invoke a hook_nodeapi() operation in all modules.
 *
 * @param &$node
Dries's avatar
   
Dries committed
305
 *   A node object.
Dries's avatar
   
Dries committed
306
307
308
309
310
311
312
 * @param $op
 *   A string containing the name of the nodeapi operation.
 * @param $a3, $a4
 *   Arguments to pass on to the hook, after the $node and $op arguments.
 * @return
 *   The returned value of the invoked hooks.
 */
Dries's avatar
   
Dries committed
313
function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
Dries's avatar
   
Dries committed
314
  $return = array();
315
  foreach (module_implements('nodeapi') as $name) {
Dries's avatar
   
Dries committed
316
    $function = $name .'_nodeapi';
317
    $result = $function($node, $op, $a3, $a4);
318
    if (isset($result) && is_array($result)) {
319
320
321
322
      $return = array_merge($return, $result);
    }
    else if (isset($result)) {
      $return[] = $result;
Dries's avatar
   
Dries committed
323
324
325
326
327
    }
  }
  return $return;
}

Dries's avatar
   
Dries committed
328
329
330
/**
 * Load a node object from the database.
 *
331
332
 * @param $param
 *   Either the nid of the node or an array of conditions to match against in the database query
Dries's avatar
   
Dries committed
333
334
 * @param $revision
 *   Which numbered revision to load. Defaults to the current version.
Dries's avatar
   
Dries committed
335
336
 * @param $reset
 *   Whether to reset the internal node_load cache.
Dries's avatar
   
Dries committed
337
338
339
340
 *
 * @return
 *   A fully-populated node object.
 */
341
function node_load($param = array(), $revision = NULL, $reset = NULL) {
Dries's avatar
   
Dries committed
342
343
344
345
346
347
  static $nodes = array();

  if ($reset) {
    $nodes = array();
  }

348
  $arguments = array();
349
350
  if (is_numeric($param)) {
    $cachable = $revision == NULL;
351
    if ($cachable && isset($nodes[$param])) {
352
353
      return $nodes[$param];
    }
354
355
    $cond = 'n.nid = %d';
    $arguments[] = $param;
Dries's avatar
   
Dries committed
356
  }
357
358
  else {
    // Turn the conditions into a query.
359
    foreach ($param as $key => $value) {
360
361
      $cond[] = 'n.'. db_escape_string($key) ." = '%s'";
      $arguments[] = $value;
362
363
    }
    $cond = implode(' AND ', $cond);
Dries's avatar
   
Dries committed
364
365
  }

Dries's avatar
   
Dries committed
366
  // Retrieve the node.
367
  if ($revision) {
368
369
    array_unshift($arguments, $revision);
    $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.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));
Dries's avatar
   
Dries committed
370
  }
371
  else {
372
    $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.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));
Dries's avatar
   
Dries committed
373
374
  }

375
376
377
378
379
380
381
  if ($node->nid) {
    // Call the node specific callback (if any) and piggy-back the
    // results to the node or overwrite some values.
    if ($extra = node_invoke($node, 'load')) {
      foreach ($extra as $key => $value) {
        $node->$key = $value;
      }
Dries's avatar
   
Dries committed
382
383
    }

384
385
386
387
388
    if ($extra = node_invoke_nodeapi($node, 'load')) {
      foreach ($extra as $key => $value) {
        $node->$key = $value;
      }
    }
Dries's avatar
   
Dries committed
389
390
  }

Dries's avatar
   
Dries committed
391
  if ($cachable) {
392
    $nodes[$param] = $node;
Dries's avatar
   
Dries committed
393
394
  }

Dries's avatar
   
Dries committed
395
396
397
  return $node;
}

Dries's avatar
   
Dries committed
398
399
400
/**
 * Save a node object into the database.
 */
401
function node_save(&$node) {
402
  global $user;
Dries's avatar
   
Dries committed
403

404
  $node->is_new = false;
Dries's avatar
   
Dries committed
405

Dries's avatar
   
Dries committed
406
  // Apply filters to some default node fields:
Dries's avatar
   
Dries committed
407
  if (empty($node->nid)) {
Dries's avatar
   
Dries committed
408
    // Insert a new node.
409
    $node->is_new = true;
Dries's avatar
   
Dries committed
410

Dries's avatar
   
Dries committed
411
    $node->nid = db_next_id('{node}_nid');
412
413
414
415
416
417
418
    $node->vid = db_next_id('{node_revisions}_vid');;
  }
  else {
    // We need to ensure that all node fields are filled.
    $node_current = node_load($node->nid);
    foreach ($node as $field => $data) {
      $node_current->$field = $data;
Dries's avatar
   
Dries committed
419
    }
420
    $node = $node_current;
Dries's avatar
   
Dries committed
421

422
423
424
425
    if ($node->revision) {
      $node->old_vid = $node->vid;
      $node->vid = db_next_id('{node_revisions}_vid');
    }
Dries's avatar
   
Dries committed
426
427
  }

428
429
430
431
  // Set some required fields:
  if (empty($node->created)) {
    $node->created = time();
  }
432
  // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
433
  $node->changed = time();
Dries's avatar
   
Dries committed
434

435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
  // Split off revisions data to another structure
  $revisions_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
                     'title' => $node->title, 'body' => $node->body,
                     'teaser' => $node->teaser, 'log' => $node->log, 'timestamp' => $node->changed,
                     'uid' => $user->uid, 'format' => $node->format);
  $revisions_table_types = array('nid' => '%d', 'vid' => '%d',
                     'title' => "'%s'", 'body' => "'%s'",
                     'teaser' => "'%s'", 'log' => "'%s'", 'timestamp' => '%d',
                     'uid' => '%d', 'format' => '%d');
  $node_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
                    'title' => $node->title, 'type' => $node->type, 'uid' => $node->uid,
                    'status' => $node->status, 'created' => $node->created,
                    'changed' => $node->changed, 'comment' => $node->comment,
                    'promote' => $node->promote, 'moderate' => $node->moderate,
                    'sticky' => $node->sticky);
  $node_table_types = array('nid' => '%d', 'vid' => '%d',
                    'title' => "'%s'", 'type' => "'%s'", 'uid' => '%d',
                    'status' => '%d', 'created' => '%d',
                    'changed' => '%d', 'comment' => '%d',
                    'promote' => '%d', 'moderate' => '%d',
                    'sticky' => '%d');

  //Generate the node table query and the
  //the node_revisions table query
  if ($node->is_new) {
    $node_query = 'INSERT INTO {node} ('. implode(', ', array_keys($node_table_types)) .') VALUES ('. implode(', ', $node_table_types) .')';
    $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
  }
  else {
    $arr = array();
    foreach ($node_table_types as $key => $value) {
      $arr[] = $key .' = '. $value;
    }
    $node_table_values[] = $node->nid;
    $node_query = 'UPDATE {node} SET '. implode(', ', $arr) .' WHERE nid = %d';
    if ($node->revision) {
      $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
    }
    else {
      $arr = array();
      foreach ($revisions_table_types as $key => $value) {
        $arr[] = $key .' = '. $value;
Dries's avatar
   
Dries committed
477
      }
478
479
      $revisions_table_values[] = $node->vid;
      $revisions_query = 'UPDATE {node_revisions} SET '. implode(', ', $arr) .' WHERE vid = %d';
Dries's avatar
   
Dries committed
480
    }
481
  }
Dries's avatar
   
Dries committed
482

483
484
485
  // Insert the node into the database:
  db_query($node_query, $node_table_values);
  db_query($revisions_query, $revisions_table_values);
Dries's avatar
   
Dries committed
486

487
488
489
490
491
492
  // Call the node specific callback (if any):
  if ($node->is_new) {
    node_invoke($node, 'insert');
    node_invoke_nodeapi($node, 'insert');
  }
  else {
Dries's avatar
   
Dries committed
493
494
    node_invoke($node, 'update');
    node_invoke_nodeapi($node, 'update');
Dries's avatar
   
Dries committed
495
496
  }

Dries's avatar
   
Dries committed
497
  // Clear the cache so an anonymous poster can see the node being added or updated.
Dries's avatar
   
Dries committed
498
  cache_clear_all();
Dries's avatar
   
Dries committed
499
500
}

Dries's avatar
   
Dries committed
501
502
503
504
505
506
/**
 * Generate a display of the given node.
 *
 * @param $node
 *   A node array or node object.
 * @param $teaser
507
 *   Whether to display the teaser only, as on the main page.
Dries's avatar
   
Dries committed
508
509
 * @param $page
 *   Whether the node is being displayed by itself as a page.
510
511
 * @param $links
 *   Whether or not to display node links. Links are omitted for node previews.
Dries's avatar
   
Dries committed
512
513
514
515
 *
 * @return
 *   An HTML representation of the themed node.
 */
516
function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) {
517
  $node = (object)$node;
Dries's avatar
   
Dries committed
518

519
  // Remove the delimiter (if any) that separates the teaser from the body.
Dries's avatar
   
Dries committed
520
  // TODO: this strips legitimate uses of '<!--break-->' also.
Dries's avatar
   
Dries committed
521
  $node->body = str_replace('<!--break-->', '', $node->body);
Dries's avatar
   
Dries committed
522

523
  if ($node->log != '' && !$teaser && $node->moderate) {
524
    $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. filter_xss($node->log) .'</div>';
525
526
  }

Dries's avatar
   
Dries committed
527
528
  // The 'view' hook can be implemented to overwrite the default function
  // to display nodes.
Dries's avatar
   
Dries committed
529
  if (node_hook($node, 'view')) {
Dries's avatar
   
Dries committed
530
    node_invoke($node, 'view', $teaser, $page);
Dries's avatar
   
Dries committed
531
532
  }
  else {
Dries's avatar
   
Dries committed
533
    $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
534
  }
Dries's avatar
   
Dries committed
535
536
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $teaser, $page);
537
538
539
  if ($links) {
    $node->links = module_invoke_all('link', 'node', $node, !$page);
  }
540
541
542
543
544
545
546
  // unset unused $node part so that a bad theme can not open a security hole
  if ($teaser) {
    unset($node->body);
  }
  else {
    unset($node->teaser);
  }
Dries's avatar
   
Dries committed
547
548

  return theme('node', $node, $teaser, $page);
Dries's avatar
   
Dries committed
549
}
Dries's avatar
   
Dries committed
550

Dries's avatar
   
Dries committed
551
552
553
/**
 * Apply filters to a node in preparation for theming.
 */
Dries's avatar
   
Dries committed
554
function node_prepare($node, $teaser = FALSE) {
Dries's avatar
   
Dries committed
555
  $node->readmore = (strlen($node->teaser) < strlen($node->body));
Dries's avatar
   
Dries committed
556
  if ($teaser == FALSE) {
557
    $node->body = check_markup($node->body, $node->format, FALSE);
Dries's avatar
   
Dries committed
558
559
  }
  else {
560
    $node->teaser = check_markup($node->teaser, $node->format, FALSE);
Dries's avatar
   
Dries committed
561
  }
Dries's avatar
   
Dries committed
562
  return $node;
Dries's avatar
   
Dries committed
563
564
}

Dries's avatar
   
Dries committed
565
566
567
/**
 * Generate a page displaying a single node, along with its comments.
 */
Dries's avatar
   
Dries committed
568
function node_show($node, $cid) {
Dries's avatar
   
Dries committed
569
  $output = node_view($node, FALSE, TRUE);
Dries's avatar
   
Dries committed
570

Dries's avatar
   
Dries committed
571
572
  if (function_exists('comment_render') && $node->comment) {
    $output .= comment_render($node, $cid);
Dries's avatar
   
Dries committed
573
574
  }

Dries's avatar
   
Dries committed
575
576
  // Update the history table, stating that this user viewed this node.
  node_tag_new($node->nid);
Dries's avatar
   
Dries committed
577

Dries's avatar
   
Dries committed
578
  return $output;
Dries's avatar
   
Dries committed
579
580
}

Dries's avatar
   
Dries committed
581
582
583
/**
 * Implementation of hook_perm().
 */
Dries's avatar
   
Dries committed
584
function node_perm() {
585
  return array('administer nodes', 'access content', 'view revisions', 'revert revisions');
Dries's avatar
   
Dries committed
586
587
}

Dries's avatar
   
Dries committed
588
589
590
/**
 * Implementation of hook_search().
 */
591
592
593
594
function node_search($op = 'search', $keys = null) {
  switch ($op) {
    case 'name':
      return t('content');
595

Dries's avatar
Dries committed
596
597
    case 'reset':
      variable_del('node_cron_last');
598
      variable_del('node_cron_last_nid');
Dries's avatar
Dries committed
599
      return;
600

601
602
    case 'status':
      $last = variable_get('node_cron_last', 0);
603
      $last_nid = variable_get('node_cron_last_nid', 0);
604
605
      $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
      $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
609
610
611
612

    case 'admin':
      $form = array();
      // Output form for defining rank factor weights.
      $form['content_ranking'] = array('#type' => 'fieldset', '#title' => t('Content ranking'));
      $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
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630

      $ranking = array('node_rank_relevance' => t('Keyword relevance'),
                       'node_rank_recent' => t('Recently posted'));
      if (module_exist('comment')) {
        $ranking['node_rank_comments'] = t('Number of comments');
      }
      if (module_exist('statistics') && variable_get('statistics_count_content_views', 0)) {
        $ranking['node_rank_views'] = t('Number of views');
      }

      // Note: reversed to reflect that higher number = higher ranking.
      $options = drupal_map_assoc(range(0, 10));
      foreach ($ranking as $var => $title) {
        $form['content_ranking']['factors'][$var] = array('#title' => $title, '#type' => 'select', '#options' => $options, '#default_value' => variable_get($var, 5));
      }
      return $form;

631
    case 'search':
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
      // Build matching conditions
      list($join1, $where1) = _db_rewrite_sql();
      $arguments1 = array();
      $conditions1 = 'n.status = 1';

      if ($type = search_query_extract($keys, 'type')) {
        $types = array();
        foreach (explode(',', $type) as $t) {
          $types[] = "n.type = '%s'";
          $arguments1[] = $t;
        }
        $conditions1 .= ' AND ('. implode(' OR ', $types) .')';
        $keys = search_query_insert($keys, 'type');
      }

      if ($category = search_query_extract($keys, 'category')) {
        $categories = array();
        foreach (explode(',', $category) as $c) {
          $categories[] = "tn.tid = %d";
          $arguments1[] = $c;
        }
        $conditions1 .= ' AND ('. implode(' OR ', $categories) .')';
        $join1 .= ' INNER JOIN {term_node} tn ON n.nid = tn.nid';
        $keys = search_query_insert($keys, 'category');
      }

      // Build ranking expression (we try to map each parameter to a
      // uniform distribution in the range 0..1).
      $ranking = array();
      $arguments2 = array();
      $join2 = '';
      // Used to avoid joining on node_comment_statistics twice
      $stats_join = false;
      if ($weight = (int)variable_get('node_rank_relevance', 5)) {
        // Average relevance values hover around 0.15
        $ranking[] = '%d * i.relevance';
        $arguments2[] = $weight;
      }
      if ($weight = (int)variable_get('node_rank_recent', 5)) {
        // Exponential decay with half-life of 6 months, starting at last indexed node
        $ranking[] = '%d * POW(2, (GREATEST(n.created, n.changed, c.last_comment_timestamp) - %d) * 6.43e-8)';
        $arguments2[] = $weight;
        $arguments2[] = (int)variable_get('node_cron_last', 0);
        $join2 .= ' INNER JOIN {node} n ON n.nid = i.sid LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
        $stats_join = true;
      }
      if (module_exist('comment') && $weight = (int)variable_get('node_rank_comments', 5)) {
        // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
        $scale = variable_get('node_cron_comments_scale', 0.0);
        $ranking[] = '%d * (2.0 - 2.0 / (1.0 + c.comment_count * %f))';
        $arguments2[] = $weight;
        $arguments2[] = $scale;
        if (!$stats_join) {
          $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
        }
      }
      if (module_exist('statistics') && variable_get('statistics_count_content_views', 0) &&
          $weight = (int)variable_get('node_rank_views', 5)) {
        // Inverse law that maps the highest view count on the site to 1 and 0 to 0.
        $scale = variable_get('node_cron_views_scale', 0.0);
        $ranking[] = '%d * (2.0 - 2.0 / (1.0 + nc.totalcount * %f))';
        $arguments2[] = $weight;
        $arguments2[] = $scale;
695
        $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid';
696
697
698
699
700
701
702
      }
      $select2 = (count($ranking) ? implode(' + ', $ranking) : 'i.relevance') . ' AS score';

      // Do search
      $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);

      // Load results
703
704
      $results = array();
      foreach ($find as $item) {
705
        $node = node_load($item->sid);
706
707
708
709
710
711
712
713
714
715
716

        // Get node output (filtered and with module-specific fields).
        if (node_hook($node, 'view')) {
          node_invoke($node, 'view', false, false);
        }
        else {
          $node = node_prepare($node, false);
        }
        // Allow modules to change $node->body before viewing.
        node_invoke_nodeapi($node, 'view', false, false);

717
718
719
        // Fetch comments for snippet
        $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');

Dries's avatar
Dries committed
720
        $extra = node_invoke_nodeapi($node, 'search result');
721
        $results[] = array('link' => url('node/'. $item->sid),
722
                           'type' => node_get_name($node),
723
                           'title' => $node->title,
724
                           'user' => theme('username', $node),
725
                           'date' => $node->changed,
726
                           'node' => $node,
Dries's avatar
Dries committed
727
                           'extra' => $extra,
728
                           'snippet' => search_excerpt($keys, $node->body));
729
730
731
      }
      return $results;
  }
Dries's avatar
   
Dries committed
732
733
}

734
735
736
737
738
739
740
741
742
743
/**
 * Implementation of hook_user().
 */
function node_user($op, &$edit, &$user) {
  if ($op == 'delete') {
    db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid);
    db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid);
  }
}

744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
function theme_node_search_admin($form) {
  $output = form_render($form['info']);

  $header = array(t('Factor'), t('Weight'));
  foreach (element_children($form['factors']) as $key) {
    $row = array();
    $row[] = $form['factors'][$key]['#title'];
    unset($form['factors'][$key]['#title']);
    $row[] = form_render($form['factors'][$key]);
    $rows[] = $row;
  }
  $output .= theme('table', $header, $rows);

  $output .= form_render($form);
  return $output;
}

Dries's avatar
   
Dries committed
761
/**
Dries's avatar
   
Dries committed
762
 * Menu callback; presents general node configuration options.
Dries's avatar
   
Dries committed
763
764
765
 */
function node_configure() {

766
  $form['default_nodes_main'] = array(
767
768
769
    '#type' => 'select', '#title' => t('Number of posts on main page'), '#default_value' => variable_get('default_nodes_main', 10),
    '#options' =>  drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
    '#description' => t('The default maximum number of posts to display per page on overview pages such as the main page.')
770
771
772
  );

  $form['teaser_length'] = array(
773
774
    '#type' => 'select', '#title' => t('Length of trimmed posts'), '#default_value' => variable_get('teaser_length', 600),
    '#options' => array(0 => t('Unlimited'), 200 => t('200 characters'), 400 => t('400 characters'), 600 => t('600 characters'),
775
776
      800 => t('800 characters'), 1000 => t('1000 characters'), 1200 => t('1200 characters'), 1400 => t('1400 characters'),
      1600 => t('1600 characters'), 1800 => t('1800 characters'), 2000 => t('2000 characters')),
777
    '#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.")
778
779
780
  );

  $form['node_preview'] = array(
781
782
    '#type' => 'radios', '#title' => t('Preview post'), '#default_value' => variable_get('node_preview', 0),
    '#options' => array(t('Optional'), t('Required')), '#description' => t('Must users preview posts before submitting?')
783
  );
Dries's avatar
   
Dries committed
784

785
  return system_settings_form('node_configure', $form);
Dries's avatar
   
Dries committed
786
787
}

Dries's avatar
   
Dries committed
788
789
790
/**
 * Retrieve the comment mode for the given node ID (none, read, or read/write).
 */
Dries's avatar
   
Dries committed
791
function node_comment_mode($nid) {
Dries's avatar
   
Dries committed
792
793
  static $comment_mode;
  if (!isset($comment_mode[$nid])) {
Dries's avatar
   
Dries committed
794
    $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid));
Dries's avatar
   
Dries committed
795
796
  }
  return $comment_mode[$nid];
Dries's avatar
   
Dries committed
797
798
}

Dries's avatar
   
Dries committed
799
800
801
/**
 * Implementation of hook_link().
 */
802
function node_link($type, $node = 0, $main = 0) {
Dries's avatar
   
Dries committed
803
804
  $links = array();

Dries's avatar
   
Dries committed
805
  if ($type == 'node') {
Dries's avatar
   
Dries committed
806
    if (array_key_exists('links', $node)) {
Kjartan's avatar
Kjartan committed
807
808
      $links = $node->links;
    }
Dries's avatar
   
Dries committed
809

Dries's avatar
   
Dries committed
810
    if ($main == 1 && $node->teaser && $node->readmore) {
Dries's avatar
   
Dries committed
811
      $links[] = l(t('read more'), "node/$node->nid", array('title' => t('Read the rest of this posting.'), 'class' => 'read-more'));
Dries's avatar
   
Dries committed
812
    }
Dries's avatar
   
Dries committed
813
814
  }

Dries's avatar
   
Dries committed
815
  return $links;
Dries's avatar
   
Dries committed
816
817
}

Dries's avatar
   
Dries committed
818
819
820
/**
 * Implementation of hook_menu().
 */
Dries's avatar
   
Dries committed
821
function node_menu($may_cache) {
Dries's avatar
   
Dries committed
822
823
  $items = array();

Dries's avatar
   
Dries committed
824
825
  if ($may_cache) {
    $items[] = array('path' => 'admin/node', 'title' => t('content'),
Steven Wittens's avatar
Steven Wittens committed
826
      'callback' => 'node_admin_nodes',
Dries's avatar
   
Dries committed
827
828
829
      'access' => user_access('administer nodes'));
    $items[] = array('path' => 'admin/node/overview', 'title' => t('list'),
      'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
Steven Wittens's avatar
Steven Wittens committed
830
831
832
833
834
835
836
837

    if (module_exist('search')) {
      $items[] = array('path' => 'admin/node/search', 'title' => t('search'),
        'callback' => 'node_admin_search',
        'access' => user_access('administer nodes'),
        'type' => MENU_LOCAL_TASK);
    }

838
    $items[] = array('path' => 'admin/settings/node', 'title' => t('posts'),
Dries's avatar
   
Dries committed
839
      'callback' => 'node_configure',
840
841
      'access' => user_access('administer nodes'));
    $items[] = array('path' => 'admin/settings/content-types', 'title' => t('content types'),
842
      'callback' => 'node_types_configure',
843
      'access' => user_access('administer nodes'));
Dries's avatar
   
Dries committed
844

Dries's avatar
   
Dries committed
845
    $items[] = array('path' => 'node', 'title' => t('content'),
Dries's avatar
   
Dries committed
846
      'callback' => 'node_page',
Dries's avatar
   
Dries committed
847
848
849
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
    $items[] = array('path' => 'node/add', 'title' => t('create content'),
Dries's avatar
   
Dries committed
850
      'callback' => 'node_page',
Dries's avatar
   
Dries committed
851
852
853
      'access' => user_access('access content'),
      'type' => MENU_ITEM_GROUPING,
      'weight' => 1);
854
855
856
857
    $items[] = array('path' => 'rss.xml', 'title' => t('rss feed'),
      'callback' => 'node_feed',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
Dries's avatar
   
Dries committed
858
859
860
  }
  else {
    if (arg(0) == 'node' && is_numeric(arg(1))) {
861
      $node = node_load(arg(1));
862
863
      if ($node->nid) {
        $items[] = array('path' => 'node/'. arg(1), 'title' => t('view'),
Dries's avatar
   
Dries committed
864
          'callback' => 'node_page',
865
866
867
          'access' => node_access('view', $node),
          'type' => MENU_CALLBACK);
        $items[] = array('path' => 'node/'. arg(1) .'/view', 'title' => t('view'),
868
            'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
869
870
871
872
        $items[] = array('path' => 'node/'. arg(1) .'/edit', 'title' => t('edit'),
          'callback' => 'node_page',
          'access' => node_access('update', $node),
          'weight' => 1,
Dries's avatar
   
Dries committed
873
          'type' => MENU_LOCAL_TASK);
874
        $items[] = array('path' => 'node/'. arg(1) .'/delete', 'title' => t('delete'),
875
          'callback' => 'node_delete_confirm',
876
877
878
          'access' => node_access('delete', $node),
          'weight' => 1,
          'type' => MENU_CALLBACK);
879
880
881
882
883
884
        $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);
        $items[] = array('path' => 'node/'. arg(1) .'/revisions', 'title' => t('revisions'),
          'callback' => 'node_revisions',
          'access' => $revisions_access,
          'weight' => 2,
          'type' => MENU_LOCAL_TASK);
Dries's avatar
   
Dries committed
885
      }
886
    }
887
888
    else if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'content-types' && is_string(arg(3))) {
      $items[] = array('path' => 'admin/settings/content-types/'. arg(3),
889
        'title' => t("'%name' content type", array('%name' => node_get_name(arg(3)))),
890
891
        'type' => MENU_CALLBACK);
    }
Dries's avatar
   
Dries committed
892
893
894
895
896
  }

  return $items;
}

897
898
899
900
901
function node_last_changed($nid) {
  $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
  return ($node->changed);
}

902
903
904
/**
 * List node administration operations that can be performed.
 */
905
function node_operations() {
Dries's avatar
   
Dries committed
906
  $operations = array(
907
908
909
910
911
912
    'approve' =>   array(t('Approve the selected posts'), 'UPDATE {node} SET status = 1, moderate = 0 WHERE nid = %d'),
    'promote' =>   array(t('Promote the selected posts'), 'UPDATE {node} SET status = 1, promote = 1 WHERE nid = %d'),
    'sticky' =>    array(t('Make the selected posts sticky'), 'UPDATE {node} SET status = 1, sticky = 1 WHERE nid = %d'),
    'demote' =>    array(t('Demote the selected posts'), 'UPDATE {node} SET promote = 0 WHERE nid = %d'),
    'unpublish' => array(t('Unpublish the selected posts'), 'UPDATE {node} SET status = 0 WHERE nid = %d'),
    'delete' =>    array(t('Delete the selected posts'), '')
Dries's avatar
   
Dries committed
913
  );
914
915
  return $operations;
}
Dries's avatar
   
Dries committed
916

917
918
919
/**
 * List node administration filters that can be applied.
 */
920
function node_filters() {
921
  // Regular filters
922
923
924
925
926
927
928
929
930
  $filters['status'] = array('title' => t('status'),
    'options' => array('status-1'   => t('published'),     'status-0' => t('not published'),
                       'moderate-1' => t('in moderation'), 'moderate-0' => t('not in moderation'),
                       'promote-1'  => t('promoted'),      'promote-0' => t('not promoted'),
                       'sticky-1'   => t('sticky'),        'sticky-0' => t('not sticky')));
  $filters['type'] = array('title' => t('type'), 'options' => node_get_types());
  // The taxonomy filter
  if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
    $filters['category'] = array('title' => t('category'), 'options' => $taxonomy);
931
  }
Dries's avatar
   
Dries committed
932

933
934
935
  return $filters;
}

936
937
938
/**
 * Build query for node administration filters based on session.
 */
939
940
function node_build_filter_query() {
  $filters = node_filters();
941
942
943
944

  // Build query
  $where = $args = array();
  $join = '';
945
  foreach ($_SESSION['node_overview_filter'] as $index => $filter) {
946
    list($key, $value) = $filter;
947
948
949
950
951
952
953
954
955
956
957
958
959
    switch($key) {
      case 'status':
        // Note: no exploitable hole as $key/$value have already been checked when submitted
        list($key, $value) = explode('-', $value, 2);
        $where[] = 'n.'. $key .' = %d';
        break;
      case 'category':
        $table = "tn$index";
        $where[] = "$table.tid = %d";
        $join .= "INNER JOIN {term_node} $table ON n.nid = $table.nid ";
        break;
      case 'type':
        $where[] = "n.type = '%s'";
960
961
962
963
    }
    $args[] = $value;
  }
  $where = count($where) ? 'WHERE '. implode(' AND ', $where) : '';
964

965
966
  return array('where' => $where, 'join' => $join, 'args' => $args);
}
967

968
969
970
/**
 * Return form for node administration filters.
 */
971
972
973
974
975
976
function node_filter_form() {
  $session = &$_SESSION['node_overview_filter'];
  $session = is_array($session) ? $session : array();
  $filters = node_filters();

  $i = 0;
977
978
979
980
  $form['filters'] = array('#type' => 'fieldset',
    '#title' => t('Show only items where'),
    '#theme' => 'node_filters',
  );
981
982
  foreach ($session as $filter) {
    list($type, $value) = $filter;
983
984
985
986
987
988
989
990
    if ($type == 'category') {
      // Load term name from DB rather than search and parse options array.
      $value = module_invoke('taxonomy', 'get_term', $value);
      $value = $value->name;
    }
    else {
      $value = $filters[$type]['options'][$value];
    }
991
    $string = ($i++ ? '<em>and</em> where <strong>%a</strong> is <strong>%b</strong>' : '<strong>%a</strong> is <strong>%b</strong>');
992
    $form['filters']['current'][] = array('#value' => t($string, array('%a' => $filters[$type]['title'] , '%b' => $value)));
Dries's avatar
   
Dries committed
993
  }
994

995
996
  foreach ($filters as $key => $filter) {
    $names[$key] = $filter['title'];
997
    $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
998
999
  }

1000
  $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
For faster browsing, not all history is shown. View entire blame