node.module 82.5 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 30 31 32 33
      $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>
<li>administer nodes at <a href="%admin-node-configure-types">administer &gt;&gt; content &gt;&gt; configure &gt;&gt; content types</a>.</li>
</ul>
', array('%search' => url('search'), '%admin-node-configure-types' => url('admin/node/configure/types')));
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%node">Node page</a>.', array('%node' => 'http://www.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 51 52

  if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) {
    return variable_get($type .'_help', '');
  }
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.
Dries's avatar
 
Dries committed
159
  if ($size == 0 && $delimiter == 0) {
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.
Dries's avatar
 
Dries committed
174
  if ($delimiter > 0) {
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.
Dries's avatar
Dries committed
187
  $breakpoints = array('</p>' => 4, '<br />' => 0, '<br>' => 0, "\n" => 0, '. ' => 1, '! ' => 1, '? ' => 1, '。' => 1, '؟ ' => 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 199 200 201
function _node_names($op = '', $node = NULL) {
  static $node_names, $node_list;

  if (!isset($node_names)) {
202
    $node_names = module_invoke_all('node_info');
203 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
    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];
  }
}

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

243 244 245 246 247 248 249 250 251 252
/**
 * 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
253
}
Dries's avatar
 
Dries committed
254

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

267
/**
Dries's avatar
 
Dries committed
268 269 270 271 272 273 274 275 276 277
 * 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) {
278
  return module_hook(node_get_base($node), $hook);
Dries's avatar
 
Dries committed
279 280
}

281
/**
Dries's avatar
 
Dries committed
282 283 284 285 286 287 288 289 290
 * 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
291
 *   The returned value of the invoked hook.
Dries's avatar
 
Dries committed
292 293
 */
function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
294 295
  if (node_hook($node, $hook)) {
    $function = node_get_base($node) ."_$hook";
Dries's avatar
 
Dries committed
296
    return ($function($node, $a2, $a3, $a4));
Dries's avatar
 
Dries committed
297 298 299
  }
}

Dries's avatar
 
Dries committed
300 301 302 303
/**
 * Invoke a hook_nodeapi() operation in all modules.
 *
 * @param &$node
Dries's avatar
 
Dries committed
304
 *   A node object.
Dries's avatar
 
Dries committed
305 306 307 308 309 310 311
 * @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
312
function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
Dries's avatar
 
Dries committed
313
  $return = array();
314
  foreach (module_implements('nodeapi') as $name) {
Dries's avatar
 
Dries committed
315
    $function = $name .'_nodeapi';
316 317 318 319 320 321
    $result = $function($node, $op, $a3, $a4);
    if (is_array($result)) {
      $return = array_merge($return, $result);
    }
    else if (isset($result)) {
      $return[] = $result;
Dries's avatar
 
Dries committed
322 323 324 325 326
    }
  }
  return $return;
}

Dries's avatar
 
Dries committed
327 328 329
/**
 * Load a node object from the database.
 *
330 331
 * @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
332 333
 * @param $revision
 *   Which numbered revision to load. Defaults to the current version.
Dries's avatar
 
Dries committed
334 335
 * @param $reset
 *   Whether to reset the internal node_load cache.
Dries's avatar
 
Dries committed
336 337 338 339
 *
 * @return
 *   A fully-populated node object.
 */
340
function node_load($param = array(), $revision = NULL, $reset = NULL) {
Dries's avatar
 
Dries committed
341 342 343 344 345 346
  static $nodes = array();

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

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

Dries's avatar
 
Dries committed
362
  // Retrieve the node.
363 364
  if ($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), $revision));
Dries's avatar
 
Dries committed
365
  }
366 367
  else {
    $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)));
Dries's avatar
 
Dries committed
368 369
  }

370 371 372 373 374 375 376
  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
377 378
    }

379 380 381 382 383
    if ($extra = node_invoke_nodeapi($node, 'load')) {
      foreach ($extra as $key => $value) {
        $node->$key = $value;
      }
    }
Dries's avatar
 
Dries committed
384 385
  }

Dries's avatar
 
Dries committed
386
  if ($cachable) {
387
    $nodes[$param] = $node;
Dries's avatar
 
Dries committed
388 389
  }

Dries's avatar
 
Dries committed
390 391 392
  return $node;
}

Dries's avatar
 
Dries committed
393 394 395
/**
 * Save a node object into the database.
 */
396
function node_save(&$node) {
397
  global $user;
Dries's avatar
 
Dries committed
398

399
  $node->is_new = false;
Dries's avatar
 
Dries committed
400

Dries's avatar
 
Dries committed
401
  // Apply filters to some default node fields:
Dries's avatar
 
Dries committed
402
  if (empty($node->nid)) {
Dries's avatar
 
Dries committed
403
    // Insert a new node.
404
    $node->is_new = true;
Dries's avatar
 
Dries committed
405

Dries's avatar
 
Dries committed
406
    // Set some required fields:
407 408 409
    if (!$node->created) {
      $node->created = time();
    }
Dries's avatar
 
Dries committed
410
    $node->nid = db_next_id('{node}_nid');
411 412 413 414 415 416 417
    $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
418
    }
419
    $node = $node_current;
Dries's avatar
 
Dries committed
420

421 422 423 424 425 426
    if ($node->revision) {
      $node->old_vid = $node->vid;
      $node->vid = db_next_id('{node_revisions}_vid');
      // We always update the timestamp for new revisions.
      $node->changed = time();
    }
Dries's avatar
 
Dries committed
427 428
  }

429
  if (!$node->changed) {
Dries's avatar
 
Dries committed
430
    $node->changed = time();
431
  }
Dries's avatar
 
Dries committed
432

433 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
  // 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
475
      }
476 477
      $revisions_table_values[] = $node->vid;
      $revisions_query = 'UPDATE {node_revisions} SET '. implode(', ', $arr) .' WHERE vid = %d';
Dries's avatar
 
Dries committed
478
    }
479
  }
Dries's avatar
 
Dries committed
480

481 482 483
  // 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
484

485 486 487 488 489 490
  // 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
491 492
    node_invoke($node, 'update');
    node_invoke_nodeapi($node, 'update');
Dries's avatar
 
Dries committed
493 494
  }

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

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

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

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

Dries's avatar
 
Dries committed
525 526
  // The 'view' hook can be implemented to overwrite the default function
  // to display nodes.
Dries's avatar
 
Dries committed
527
  if (node_hook($node, 'view')) {
Dries's avatar
 
Dries committed
528
    node_invoke($node, 'view', $teaser, $page);
Dries's avatar
 
Dries committed
529 530
  }
  else {
Dries's avatar
 
Dries committed
531
    $node = node_prepare($node, $teaser);
Dries's avatar
 
Dries committed
532
  }
Dries's avatar
 
Dries committed
533 534
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $teaser, $page);
535 536 537
  if ($links) {
    $node->links = module_invoke_all('link', 'node', $node, !$page);
  }
538 539 540 541 542 543 544
  // 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
545 546

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

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

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

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

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

Dries's avatar
 
Dries committed
576
  return $output;
Dries's avatar
 
Dries committed
577 578
}

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

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

Dries's avatar
Dries committed
594 595 596
    case 'reset':
      variable_del('node_cron_last');
      return;
597

598 599 600
    case 'status':
      $last = variable_get('node_cron_last', 0);
      $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1 AND moderate = 0'));
Steven Wittens's avatar
Steven Wittens committed
601
      $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 n.moderate = 0 AND (n.created > %d OR n.changed > %d OR c.last_comment_timestamp > %d)', $last, $last, $last));
602
      return array('remaining' => $remaining, 'total' => $total);
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626

    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';
      $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.') .'</em>');

      $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;

627
    case 'search':
628 629 630 631 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 695 696 697 698
      // 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;
        $join2 .= ' LEFT JOIN {node_counter} nc ON n.nid = nc.nid';
      }
      $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
699 700
      $results = array();
      foreach ($find as $item) {
701
        $node = node_load($item);
702 703 704 705 706 707 708 709 710 711 712

        // 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);

713 714 715
        // Fetch comments for snippet
        $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');

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

    case 'form':
      $form = array();

      // Keyword boxes
732
      $form['advanced'] = array('#type' => 'fieldset', '#title' => t('Advanced search'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#attributes' => array('class' => 'search-advanced'));
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772

      $form['advanced']['keywords'] = array('#type' => 'markup', '#prefix' => '<div class="criterium">', '#suffix' => '</div>');
      $form['advanced']['keywords']['or'] = array('#type' => 'textfield', '#title' => t('Containing any of the words'), '#size' => 30, '#maxlength' => 255);
      $form['advanced']['keywords']['phrase'] = array('#type' => 'textfield', '#title' => t('Containing the phrase'), '#size' => 30, '#maxlength' => 255);
      $form['advanced']['keywords']['negative'] = array('#type' => 'textfield', '#title' => t('Containing none of the words'), '#size' => 30, '#maxlength' => 255);

      // Taxonomy box
      if ($taxonomy = module_invoke('taxonomy', 'form_all')) {
        $form['advanced']['category'] = array('#type' => 'select', '#title' => t('Only in the category'), '#prefix' => '<div class="criterium">', '#suffix' => '</div>', '#options' => $taxonomy, '#extra' => 'size="10"', '#multiple' => true);
      }

      // Node types
      $types = node_get_types();
      $form['advanced']['type'] = array('#type' => 'checkboxes', '#title' => t('Only of the type'), '#prefix' => '<div class="criterium">', '#suffix' => '</div>', '#options' => $types, '#multiple' => true);
      $form['advanced']['submit'] = array('#type' => 'submit', '#value' => t('Advanced Search'), '#prefix' => '<div class="action">', '#suffix' => '</div>');
      return $form;

    case 'post':
      // Insert extra restrictions into the search keywords string.
      $edit = &$_POST['edit'];
      if (is_array($edit['type'])) {
        $keys = search_query_insert($keys, 'type', implode(',', array_keys($edit['type'])));
      }
      if (is_array($edit['category'])) {
        $keys = search_query_insert($keys, 'category', implode(',', $edit['category']));
      }
      if ($edit['or'] != '') {
        if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $edit['or'], $matches)) {
          $keys = $keys .' '. implode(' OR ', $matches[1]);
        }
      }
      if ($edit['negative'] != '') {
        if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $edit['negative'], $matches)) {
          $keys = $keys .' -'. implode(' -', $matches[1]);
        }
      }
      if ($edit['phrase'] != '') {
        $keys .= ' "'. str_replace('"', ' ', $edit['phrase']) .'"';
      }
      return trim($keys);
773
  }
Dries's avatar
 
Dries committed
774 775
}

776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792
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
793
/**
Dries's avatar
 
Dries committed
794
 * Menu callback; presents general node configuration options.
Dries's avatar
 
Dries committed
795 796 797
 */
function node_configure() {

798
  $form['default_nodes_main'] = array(
799 800 801
    '#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.')
802 803 804
  );

  $form['teaser_length'] = array(
805 806
    '#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'),
807 808
      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')),
809
    '#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.")
810 811 812
  );

  $form['node_preview'] = array(
813 814
    '#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?')
815
  );
Dries's avatar
 
Dries committed
816

817
  return system_settings_form('node_configure', $form);
Dries's avatar
 
Dries committed
818 819
}

Dries's avatar
 
Dries committed
820 821 822
/**
 * Retrieve the comment mode for the given node ID (none, read, or read/write).
 */
Dries's avatar
 
Dries committed
823
function node_comment_mode($nid) {
Dries's avatar
 
Dries committed
824 825
  static $comment_mode;
  if (!isset($comment_mode[$nid])) {
Dries's avatar
 
Dries committed
826
    $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid));
Dries's avatar
 
Dries committed
827 828
  }
  return $comment_mode[$nid];
Dries's avatar
 
Dries committed
829 830
}

Dries's avatar
 
Dries committed
831 832 833
/**
 * Implementation of hook_link().
 */
834
function node_link($type, $node = 0, $main = 0) {
Dries's avatar
 
Dries committed
835 836
  $links = array();

Dries's avatar
 
Dries committed
837
  if ($type == 'node') {
Dries's avatar
 
Dries committed
838
    if (array_key_exists('links', $node)) {
Kjartan's avatar
Kjartan committed
839 840
      $links = $node->links;
    }
Dries's avatar
 
Dries committed
841

Dries's avatar
 
Dries committed
842
    if ($main == 1 && $node->teaser && $node->readmore) {
Dries's avatar
 
Dries committed
843
      $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
844
    }
Dries's avatar
 
Dries committed
845 846
  }

Dries's avatar
 
Dries committed
847
  return $links;
Dries's avatar
 
Dries committed
848 849
}

Dries's avatar
 
Dries committed
850 851 852
/**
 * Implementation of hook_menu().
 */
Dries's avatar
 
Dries committed
853
function node_menu($may_cache) {
Dries's avatar
 
Dries committed
854 855
  $items = array();

Dries's avatar
 
Dries committed
856 857
  if ($may_cache) {
    $items[] = array('path' => 'admin/node', 'title' => t('content'),
Dries's avatar
 
Dries committed
858
      'callback' => 'node_admin',
Dries's avatar
 
Dries committed
859
      'access' => user_access('administer nodes'));
860 861
    $items[] = array('path' => 'admin/node/action', 'title' => t('content'),
      'type' => MENU_CALLBACK);
Dries's avatar
 
Dries committed
862 863
    $items[] = array('path' => 'admin/node/overview', 'title' => t('list'),
      'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
864
    $items[] = array('path' => 'admin/settings/node', 'title' => t('posts'),
Dries's avatar
 
Dries committed
865
      'callback' => 'node_configure',
866 867
      'access' => user_access('administer nodes'));
    $items[] = array('path' => 'admin/settings/content-types', 'title' => t('content types'),
868
      'callback' => 'node_types_configure',
869
      'access' => user_access('administer nodes'));
Dries's avatar
 
Dries committed
870 871 872 873 874 875
    if (module_exist('search')) {
      $items[] = array('path' => 'admin/node/search', 'title' => t('search'),
        'callback' => 'node_admin',
        'access' => user_access('administer nodes'),
        'type' => MENU_LOCAL_TASK);
    }
Dries's avatar
 
Dries committed
876

Dries's avatar
 
Dries committed
877
    $items[] = array('path' => 'node', 'title' => t('content'),
Dries's avatar
 
Dries committed
878
      'callback' => 'node_page',
Dries's avatar
 
Dries committed
879 880 881
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
    $items[] = array('path' => 'node/add', 'title' => t('create content'),
Dries's avatar
 
Dries committed
882
      'callback' => 'node_page',
Dries's avatar
 
Dries committed
883 884 885 886 887 888
      'access' => user_access('access content'),
      'type' => MENU_ITEM_GROUPING,
      'weight' => 1);
  }
  else {
    if (arg(0) == 'node' && is_numeric(arg(1))) {
889
      $node = node_load(arg(1));
890 891
      if ($node->nid) {
        $items[] = array('path' => 'node/'. arg(1), 'title' => t('view'),
Dries's avatar
 
Dries committed
892
          'callback' => 'node_page',
893 894 895 896 897 898 899 900
          'access' => node_access('view', $node),
          'type' => MENU_CALLBACK);
        $items[] = array('path' => 'node/'. arg(1) .'/view', 'title' => t('view'),
            'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
        $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
901
          'type' => MENU_LOCAL_TASK);
902
        $items[] = array('path' => 'node/'. arg(1) .'/delete', 'title' => t('delete'),
903
          'callback' => 'node_delete_confirm',
904 905 906
          'access' => node_access('delete', $node),
          'weight' => 1,
          'type' => MENU_CALLBACK);
907
        if (user_access('administer nodes') && db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', arg(1))) > 1) {
908 909 910 911 912 913
          $items[] = array('path' => 'node/'. arg(1) .'/revisions', 'title' => t('revisions'),
            'callback' => 'node_page',
            'access' => user_access('administer nodes'),
            'weight' => 2,
            'type' => MENU_LOCAL_TASK);
        }
Dries's avatar
 
Dries committed
914
      }
915
    }
916 917
    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),
918
        'title' => t("'%name' content type", array('%name' => node_get_name(arg(3)))),
919 920
        'type' => MENU_CALLBACK);
    }
Dries's avatar
 
Dries committed
921 922 923 924 925
  }

  return $items;
}

926 927 928 929 930
function node_last_changed($nid) {
  $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
  return ($node->changed);
}

931 932 933 934
/*
** Node operations
*/
function node_operations() {
Dries's avatar
 
Dries committed
935
  $operations = array(
936 937 938 939 940 941
    '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
942
  );
943 944
  return $operations;
}
Dries's avatar
 
Dries committed
945

946 947 948 949
/*
** Filters
*/
function node_filters() {