node.module 90.6 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
/**
 * @file
6
7
 * The core that allows content to be submitted to the site. Modules and scripts may
 * programmatically submit nodes using the usual form API pattern.
Dries's avatar
   
Dries committed
8
9
 */

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

12
13
14
15
16
define('NODE_BUILD_NORMAL', 0);
define('NODE_BUILD_PREVIEW', 1);
define('NODE_BUILD_SEARCH_INDEX', 2);
define('NODE_BUILD_SEARCH_RESULT', 3);
define('NODE_BUILD_RSS', 4);
Dries's avatar
Dries committed
17
define('NODE_BUILD_PRINT', 5);
18

Dries's avatar
   
Dries committed
19
20
21
/**
 * Implementation of hook_help().
 */
22
function node_help($path, $arg) {
23
24
25
26
  // Remind site administrators about the {node_access} table being flagged
  // for rebuild. We don't need to issue the message on the confirm form, or
  // while the rebuild is being processed.
  if ($path != 'admin/content/node-settings/rebuild' && $path != 'batch' && strpos($path, '#') === FALSE
27
28
29
30
31
32
33
34
35
36
      && user_access('access administration pages') && node_access_needs_rebuild()) {
    if ($path == 'admin/content/node-settings') {
      $message = t('The content access permissions need to be rebuilt.');
    }
    else {
      $message = t('The content access permissions need to be rebuilt. Please visit <a href="@node_access_rebuild">this page</a>.', array('@node_access_rebuild' => url('admin/content/node-settings/rebuild')));
    }
    drupal_set_message($message, 'error');
  }

37
  switch ($path) {
Dries's avatar
   
Dries committed
38
    case 'admin/help#node':
39
40
      $output = '<p>'. t('The node module manages content on your site, and stores all posts (regardless of type) as a "node". In addition to basic publishing settings, including whether the post has been published, promoted to the site front page, or should remain present (or sticky) at the top of lists, the node module also records basic information about the author of a post. Optional revision control over edits is available. For additional functionality, the node module is often extended by other modules.') .'</p>';
      $output .= '<p>'. t('Though each post on your site is a node, each post is also of a particular <a href="@content-type">content type</a>. <a href="@content-type">Content types</a> are used to define the characteristics of a post, including the title and description of the fields displayed on its add and edit pages. Each content type may have different default settings for <em>Publishing options</em> and other workflow controls. By default, the two content types in a standard Drupal installation are <em>Page</em> and <em>Story</em>. Use the <a href="@content-type">content types page</a> to add new or edit existing content types. Additional content types also become available as you enable additional core, contributed and custom modules.', array('@content-type' => url('admin/content/types'))) .'</p>';
41
      $output .= '<p>'. t('The administrative <a href="@content">content page</a> allows you to review and manage your site content. The <a href="@post-settings">post settings page</a> sets certain options for the display of posts. The node module makes a number of permissions available for each content type, which may be set by role on the <a href="@permissions">permissions page</a>.', array('@content' => url('admin/content/node'), '@post-settings' => url('admin/content/node-settings'), '@permissions' => url('admin/user/permissions'))) .'</p>';
42
      $output .= '<p>'. t('For more information, see the online handbook entry for <a href="@node">Node module</a>.', array('@node' => 'http://drupal.org/handbook/modules/node/')) .'</p>';
Dries's avatar
   
Dries committed
43
      return $output;
44
45
    case 'admin/content/node':
      return ' '; // Return a non-null value so that the 'more help' link is shown.
46
47
48
    case 'admin/content/types':
      return '<p>'. t('Below is a list of all the content types on your site. All posts that exist on your site are instances of one of these content types.') .'</p>';
    case 'admin/content/types/add':
49
      return '<p>'. t('To create a new content type, enter the human-readable name, the machine-readable name, and all other relevant fields that are on this page. Once created, users of your site will be able to create posts that are instances of this content type.') .'</p>';
50
51
52
53
54
    case 'node/%/revisions':
      return '<p>'. t('The revisions let you track differences between multiple versions of a post.') .'</p>';
    case 'node/%/edit':
      $node = node_load($arg[1]);
      $type = node_get_types('type', $node->type);
55
      return (!empty($type->help) ? '<p>'. filter_xss_admin($type->help) .'</p>' : '');
Dries's avatar
   
Dries committed
56
  }
Dries's avatar
   
Dries committed
57

58
59
  if ($arg[0] == 'node' && $arg[1] == 'add' && $arg[2]) {
    $type = node_get_types('type', str_replace('-', '_', $arg[2]));
60
    return (!empty($type->help) ? '<p>'. filter_xss_admin($type->help) .'</p>' : '');
61
  }
Dries's avatar
   
Dries committed
62
63
}

64
65
66
67
68
/**
 * Implementation of hook_theme()
 */
function node_theme() {
  return array(
69
70
    'node' => array(
      'arguments' => array('node' => NULL, 'teaser' => FALSE, 'page' => FALSE),
71
      'template' => 'node',
72
    ),
73
74
75
76
77
78
79
80
    'node_list' => array(
      'arguments' => array('items' => NULL, 'title' => NULL),
    ),
    'node_search_admin' => array(
      'arguments' => array('form' => NULL),
    ),
    'node_filter_form' => array(
      'arguments' => array('form' => NULL),
81
      'file' => 'node.admin.inc',
82
83
84
    ),
    'node_filters' => array(
      'arguments' => array('form' => NULL),
85
      'file' => 'node.admin.inc',
86
87
88
    ),
    'node_admin_nodes' => array(
      'arguments' => array('form' => NULL),
89
90
91
92
93
      'file' => 'node.admin.inc',
    ),
    'node_add_list' => array(
      'arguments' => array('content' => NULL),
      'file' => 'node.pages.inc',
94
95
96
    ),
    'node_form' => array(
      'arguments' => array('form' => NULL),
97
      'file' => 'node.pages.inc',
98
99
100
    ),
    'node_preview' => array(
      'arguments' => array('node' => NULL),
101
      'file' => 'node.pages.inc',
102
103
104
105
    ),
    'node_log_message' => array(
      'arguments' => array('log' => NULL),
    ),
106
107
108
    'node_submitted' => array(
      'arguments' => array('node' => NULL),
    ),
109
110
111
  );
}

Dries's avatar
   
Dries committed
112
113
114
/**
 * Implementation of hook_cron().
 */
115
function node_cron() {
Dries's avatar
   
Dries committed
116
  db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT);
117
118
}

Dries's avatar
   
Dries committed
119
120
121
122
/**
 * Gather a listing of links to nodes.
 *
 * @param $result
123
124
125
126
 *   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
127
128
129
130
 * @param $title
 *   A heading for the resulting list.
 *
 * @return
131
132
 *   An HTML list suitable as content for a block, or FALSE if no result can
 *   fetch from DB result object.
Dries's avatar
   
Dries committed
133
 */
Dries's avatar
   
Dries committed
134
function node_title_list($result, $title = NULL) {
135
  $items = array();
136
  $num_rows = FALSE;
Dries's avatar
   
Dries committed
137
  while ($node = db_fetch_object($result)) {
138
    $items[] = l($node->title, 'node/'. $node->nid, !empty($node->comment_count) ? array('title' => format_plural($node->comment_count, '1 comment', '@count comments')) : array());
139
    $num_rows = TRUE;
Dries's avatar
   
Dries committed
140
141
  }

142
  return $num_rows ? theme('node_list', $items, $title) : FALSE;
Dries's avatar
   
Dries committed
143
144
}

Dries's avatar
   
Dries committed
145
146
/**
 * Format a listing of links to nodes.
147
148
 *
 * @ingroup themeable
Dries's avatar
   
Dries committed
149
 */
Dries's avatar
   
Dries committed
150
function theme_node_list($items, $title = NULL) {
Dries's avatar
   
Dries committed
151
  return theme('item_list', $items, $title);
Dries's avatar
   
Dries committed
152
153
}

Dries's avatar
   
Dries committed
154
155
156
/**
 * Update the 'last viewed' timestamp of the specified node for current user.
 */
Dries's avatar
   
Dries committed
157
158
159
160
function node_tag_new($nid) {
  global $user;

  if ($user->uid) {
Dries's avatar
   
Dries committed
161
    if (node_last_viewed($nid)) {
Dries's avatar
   
Dries committed
162
      db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid);
Dries's avatar
   
Dries committed
163
164
    }
    else {
Dries's avatar
   
Dries committed
165
      @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time());
Dries's avatar
   
Dries committed
166
167
168
169
    }
  }
}

Dries's avatar
   
Dries committed
170
171
172
173
/**
 * Retrieves the timestamp at which the current user last viewed the
 * specified node.
 */
Dries's avatar
   
Dries committed
174
175
function node_last_viewed($nid) {
  global $user;
Dries's avatar
   
Dries committed
176
  static $history;
Dries's avatar
   
Dries committed
177

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

182
  return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0);
Dries's avatar
   
Dries committed
183
184
185
}

/**
186
 * Decide on the type of marker to be displayed for a given node.
Dries's avatar
   
Dries committed
187
 *
Dries's avatar
   
Dries committed
188
189
190
191
 * @param $nid
 *   Node ID whose history supplies the "last viewed" timestamp.
 * @param $timestamp
 *   Time which is compared against node's "last viewed" timestamp.
192
193
 * @return
 *   One of the MARK constants.
Dries's avatar
   
Dries committed
194
 */
195
function node_mark($nid, $timestamp) {
Dries's avatar
   
Dries committed
196
197
198
  global $user;
  static $cache;

199
200
201
  if (!$user->uid) {
    return MARK_READ;
  }
Dries's avatar
Dries committed
202
  if (!isset($cache[$nid])) {
203
    $cache[$nid] = node_last_viewed($nid);
Dries's avatar
   
Dries committed
204
  }
205
206
207
208
209
210
211
  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
212
213
}

214
215
216
/**
 * See if the user used JS to submit a teaser.
 */
217
function node_teaser_js(&$form, &$form_state) {
218
219
  // Glue the teaser to the body.
  if (isset($form['#post']['teaser_js'])) {
220
    if (trim($form_state['values']['teaser_js'])) {
221
      // Space the teaser from the body
222
      $body = trim($form_state['values']['teaser_js']) ."\r\n<!--break-->\r\n". trim($form_state['values']['body']);
223
224
225
    }
    else {
      // Empty teaser, no spaces.
226
      $body = '<!--break-->'. $form_state['values']['body'];
227
228
    }
    // Pass value onto preview/submit
229
    form_set_value($form['body'], $body, $form_state);
230
231
232
233
234
235
    // Pass value back onto form
    $form['body']['#value'] = $body;
  }
  return $form;
}

Dries's avatar
   
Dries committed
236
/**
237
 * Automatically generate a teaser for a node body.
238
 *
239
240
241
242
 * If the end of the teaser is not indicated using the <!--break--> delimiter
 * then we try to end it at a sensible place, such as the end of a paragraph,
 * a line break, or the end of a sentence (in that order of preference).
 *
243
244
245
246
 * @param $body
 *   The content for which a teaser will be generated.
 * @param $format
 *   The format of the content. If the content contains PHP code, we do not
247
248
 *   split it up to prevent parse errors. If the line break filter is present
 *   then we treat newlines embedded in $body as line breaks.
249
 * @param $size
Dries's avatar
Dries committed
250
 *   The desired character length of the teaser. If omitted, the default
251
 *   value will be used. Ignored if the special delimiter is present
252
 *   in $body.
253
254
 * @return
 *   The generated teaser.
Dries's avatar
   
Dries committed
255
 */
256
function node_teaser($body, $format = NULL, $size = NULL) {
Dries's avatar
   
Dries committed
257

258
259
260
  if (!isset($size)) {
    $size = variable_get('teaser_length', 600);
  }
Dries's avatar
   
Dries committed
261

262
  // Find where the delimiter is in the body
Steven Wittens's avatar
Steven Wittens committed
263
  $delimiter = strpos($body, '<!--break-->');
Dries's avatar
   
Dries committed
264

265
  // If the size is zero, and there is no delimiter, the entire body is the teaser.
266
  if ($size == 0 && $delimiter === FALSE) {
Dries's avatar
   
Dries committed
267
268
    return $body;
  }
Dries's avatar
   
Dries committed
269

270
271
272
273
274
  // If a valid delimiter has been specified, use it to chop off the teaser.
  if ($delimiter !== FALSE) {
    return substr($body, 0, $delimiter);
  }

275
276
277
278
279
  // 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);
280
    if (isset($filters['php/0']) && strpos($body, '<?') !== FALSE) {
281
282
      return $body;
    }
283
284
  }

285
  // If we have a short body, the entire body is the teaser.
286
  if (strlen($body) <= $size) {
Dries's avatar
   
Dries committed
287
288
289
    return $body;
  }

290
291
292
  // If the delimiter has not been specified, try to split at paragraph or
  // sentence boundaries.

293
294
  // The teaser may not be longer than maximum length specified. Initial slice.
  $teaser = truncate_utf8($body, $size);
295
296
297
298
299
300
301
302
303
304
305
306

  // Store the actual length of the UTF8 string -- which might not be the same
  // as $size.
  $max_rpos = strlen($teaser);

  // How much to cut off the end of the teaser so that it doesn't end in the
  // middle of a paragraph, sentence, or word.
  // Initialize it to maximum in order to find the minimum.
  $min_rpos = $max_rpos;

  // Store the reverse of the teaser.  We use strpos on the reversed needle and
  // haystack for speed and convenience.
307
308
  $reversed = strrev($teaser);

309
310
311
312
313
314
  // Build an array of arrays of break points grouped by preference.
  $break_points = array();

  // A paragraph near the end of sliced teaser is most preferable.
  $break_points[] = array('</p>' => 0);

315
316
317
318
319
320
321
322
  // If no complete paragraph then treat line breaks as paragraphs.
  $line_breaks = array('<br />' => 6, '<br>' => 4);
  // Newline only indicates a line break if line break converter
  // filter is present.
  if (isset($filters['filter/1'])) {
    $line_breaks["\n"] = 1;
  }
  $break_points[] = $line_breaks;
323
324
325
326
327
328
329
330
331
332
333
334
335

  // If the first paragraph is too long, split at the end of a sentence.
  $break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);

  // Iterate over the groups of break points until a break point is found.
  foreach ($break_points as $points) {
    // Look for each break point, starting at the end of the teaser.
    foreach ($points as $point => $offset) {
      // The teaser is already reversed, but the break point isn't.
      $rpos = strpos($reversed, strrev($point));
      if ($rpos !== FALSE) {
        $min_rpos = min($rpos + $offset, $min_rpos);
      }
336
    }
Dries's avatar
Dries committed
337

338
339
340
341
    // If a break point was found in this group, slice and return the teaser.
    if ($min_rpos !== $max_rpos) {
      // Don't slice with length 0.  Length must be <0 to slice from RHS.
      return ($min_rpos === 0) ? $teaser : substr($teaser, 0, 0 - $min_rpos);
342
343
    }
  }
344
345
346

  // If a break point was not found, still return a teaser.
  return $teaser;
Dries's avatar
   
Dries committed
347
348
}

349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
/**
 * Builds a list of available node types, and returns all of part of this list
 * in the specified format.
 *
 * @param $op
 *   The format in which to return the list. When this is set to 'type',
 *   'module', or 'name', only the specified node type is returned. When set to
 *   'types' or 'names', all node types are returned.
 * @param $node
 *   A node object, array, or string that indicates the node type to return.
 *   Leave at default value (NULL) to return a list of all node types.
 * @param $reset
 *   Whether or not to reset this function's internal cache (defaults to
 *   FALSE).
 *
 * @return
 *   Either an array of all available node types, or a single node type, in a
366
 *   variable format. Returns FALSE if the node type is not found.
367
368
369
 */
function node_get_types($op = 'types', $node = NULL, $reset = FALSE) {
  static $_node_types, $_node_names;
370

371
372
  if ($reset || !isset($_node_types)) {
    list($_node_types, $_node_names) = _node_types_build();
373
  }
374

375
376
377
378
379
380
381
382
383
384
  if ($node) {
    if (is_array($node)) {
      $type = $node['type'];
    }
    elseif (is_object($node)) {
      $type = $node->type;
    }
    elseif (is_string($node)) {
      $type = $node;
    }
385
    if (!isset($_node_types[$type])) {
386
387
388
389
      return FALSE;
    }
  }
  switch ($op) {
390
391
392
    case 'types':
      return $_node_types;
    case 'type':
393
      return isset($_node_types[$type]) ? $_node_types[$type] : FALSE;
394
    case 'module':
395
      return isset($_node_types[$type]->module) ? $_node_types[$type]->module : FALSE;
396
397
    case 'names':
      return $_node_names;
398
    case 'name':
399
      return isset($_node_names[$type]) ? $_node_names[$type] : FALSE;
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
  }
}

/**
 * Resets the database cache of node types, and saves all new or non-modified
 * module-defined node types to the database.
 */
function node_types_rebuild() {
  _node_types_build();

  $node_types = node_get_types('types', NULL, TRUE);

  foreach ($node_types as $type => $info) {
    if (!empty($info->is_new)) {
      node_type_save($info);
    }
416
417
418
    if (!empty($info->disabled)) {
      node_type_delete($info->type);
    }
419
  }
420
421

  _node_types_build();
422
423
}

424
/**
425
426
427
428
 * Saves a node type to the database.
 *
 * @param $info
 *   The node type to save, as an object.
Dries's avatar
   
Dries committed
429
430
 *
 * @return
431
 *   Status flag indicating outcome of the operation.
Dries's avatar
   
Dries committed
432
 */
433
434
435
function node_type_save($info) {
  $is_existing = FALSE;
  $existing_type = !empty($info->old_type) ? $info->old_type : $info->type;
436
  $is_existing = db_result(db_query("SELECT COUNT(*) FROM {node_type} WHERE type = '%s'", $existing_type));
437
438
439
440
441
442
443
444
445
  if (!isset($info->help)) {
    $info->help = '';
  }
  if (!isset($info->min_word_count)) {
    $info->min_word_count = 0;
  }
  if (!isset($info->body_label)) {
    $info->body_label = '';
  }
446
447
448

  if ($is_existing) {
    db_query("UPDATE {node_type} SET type = '%s', name = '%s', module = '%s', has_title = %d, title_label = '%s', has_body = %d, body_label = '%s', description = '%s', help = '%s', min_word_count = %d, custom = %d, modified = %d, locked = %d WHERE type = '%s'", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $existing_type);
449
450

    module_invoke_all('node_type', 'update', $info);
451
452
453
454
    return SAVED_UPDATED;
  }
  else {
    db_query("INSERT INTO {node_type} (type, name, module, has_title, title_label, has_body, body_label, description, help, min_word_count, custom, modified, locked, orig_type) VALUES ('%s', '%s', '%s', %d, '%s', %d, '%s', '%s', '%s', %d, %d, %d, %d, '%s')", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $info->orig_type);
455
456

    module_invoke_all('node_type', 'insert', $info);
457
458
    return SAVED_NEW;
  }
459
}
460

461
462
463
464
465
466
467
468
/**
 * Deletes a node type from the database.
 *
 * @param $type
 *   The machine-readable name of the node type to be deleted.
 */
function node_type_delete($type) {
  $info = node_get_types('type', $type);
469
  db_query("DELETE FROM {node_type} WHERE type = '%s'", $type);
470
471
472
  module_invoke_all('node_type', 'delete', $info);
}

473
/**
474
475
 * Updates all nodes of one type to be of another type.
 *
476
 * @param $old_type
477
478
479
 *   The current node type of the nodes.
 * @param $type
 *   The new node type of the nodes.
480
481
 *
 * @return
482
 *   The number of nodes whose node type field was modified.
483
 */
484
485
486
function node_type_update_nodes($old_type, $type) {
  db_query("UPDATE {node} SET type = '%s' WHERE type = '%s'", $type, $old_type);
  return db_affected_rows();
Dries's avatar
   
Dries committed
487
}
Dries's avatar
   
Dries committed
488

489
/**
490
491
492
493
 * Builds and returns the list of available node types.
 *
 * The list of types is built by querying hook_node_info() in all modules, and
 * by comparing this information with the node types in the {node_type} table.
Dries's avatar
   
Dries committed
494
495
 *
 */
496
497
498
499
500
501
502
503
504
505
506
507
508
function _node_types_build() {
  $_node_types = array();
  $_node_names = array();

  $info_array = module_invoke_all('node_info');
  foreach ($info_array as $type => $info) {
    $info['type'] = $type;
    $_node_types[$type] = (object) _node_type_set_defaults($info);
    $_node_names[$type] = $info['name'];
  }

  $type_result = db_query(db_rewrite_sql('SELECT nt.type, nt.* FROM {node_type} nt ORDER BY nt.type ASC', 'nt', 'type'));
  while ($type_object = db_fetch_object($type_result)) {
509
510
511
512
    // Check for node types from disabled modules and mark their types for removal.
    // Types defined by the node module in the database (rather than by a separate
    // module using hook_node_info) have a module value of 'node'.
    if ($type_object->module != 'node' && empty($info_array[$type_object->type])) {
513
      $type_object->disabled = TRUE;
514
    }
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
    if (!isset($_node_types[$type_object->type]) || $type_object->modified) {
      $_node_types[$type_object->type] = $type_object;
      $_node_names[$type_object->type] = $type_object->name;

      if ($type_object->type != $type_object->orig_type) {
        unset($_node_types[$type_object->orig_type]);
        unset($_node_names[$type_object->orig_type]);
      }
    }
  }

  asort($_node_names);

  return array($_node_types, $_node_names);
}

/**
 * Set default values for a node type defined through hook_node_info().
 */
function _node_type_set_defaults($info) {
  if (!isset($info['has_title'])) {
    $info['has_title'] = TRUE;
  }
  if ($info['has_title'] && !isset($info['title_label'])) {
    $info['title_label'] = t('Title');
  }

  if (!isset($info['has_body'])) {
    $info['has_body'] = TRUE;
  }
  if ($info['has_body'] && !isset($info['body_label'])) {
    $info['body_label'] = t('Body');
  }

549
550
551
552
553
554
  if (!isset($info['help'])) {
    $info['help'] = '';
  }
  if (!isset($info['min_word_count'])) {
    $info['min_word_count'] = 0;
  }
555
556
557
558
559
560
561
562
563
564
565
566
567
568
  if (!isset($info['custom'])) {
    $info['custom'] = FALSE;
  }
  if (!isset($info['modified'])) {
    $info['modified'] = FALSE;
  }
  if (!isset($info['locked'])) {
    $info['locked'] = TRUE;
  }

  $info['orig_type'] = $info['type'];
  $info['is_new'] = TRUE;

  return $info;
Dries's avatar
   
Dries committed
569
}
Dries's avatar
   
Dries committed
570

571
/**
Dries's avatar
   
Dries committed
572
573
574
575
576
577
578
579
580
581
 * 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) {
582
583
584
585
586
  $module = node_get_types('module', $node);
  if ($module == 'node') {
    $module = 'node_content'; // Avoid function name collisions.
  }
  return module_hook($module, $hook);
Dries's avatar
   
Dries committed
587
588
}

589
/**
Dries's avatar
   
Dries committed
590
591
592
593
594
595
596
597
598
 * 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
599
 *   The returned value of the invoked hook.
Dries's avatar
   
Dries committed
600
601
 */
function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
602
  if (node_hook($node, $hook)) {
603
604
605
606
607
    $module = node_get_types('module', $node);
    if ($module == 'node') {
      $module = 'node_content'; // Avoid function name collisions.
    }
    $function = $module .'_'. $hook;
Dries's avatar
   
Dries committed
608
    return ($function($node, $a2, $a3, $a4));
Dries's avatar
   
Dries committed
609
610
611
  }
}

Dries's avatar
   
Dries committed
612
613
614
615
/**
 * Invoke a hook_nodeapi() operation in all modules.
 *
 * @param &$node
Dries's avatar
   
Dries committed
616
 *   A node object.
Dries's avatar
   
Dries committed
617
618
619
620
621
622
623
 * @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
624
function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
Dries's avatar
   
Dries committed
625
  $return = array();
626
  foreach (module_implements('nodeapi') as $name) {
Dries's avatar
   
Dries committed
627
    $function = $name .'_nodeapi';
628
    $result = $function($node, $op, $a3, $a4);
629
    if (isset($result) && is_array($result)) {
630
631
632
633
      $return = array_merge($return, $result);
    }
    else if (isset($result)) {
      $return[] = $result;
Dries's avatar
   
Dries committed
634
635
636
637
638
    }
  }
  return $return;
}

Dries's avatar
   
Dries committed
639
640
641
/**
 * Load a node object from the database.
 *
642
643
 * @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
644
645
 * @param $revision
 *   Which numbered revision to load. Defaults to the current version.
Dries's avatar
   
Dries committed
646
647
 * @param $reset
 *   Whether to reset the internal node_load cache.
Dries's avatar
   
Dries committed
648
649
650
651
 *
 * @return
 *   A fully-populated node object.
 */
652
function node_load($param = array(), $revision = NULL, $reset = NULL) {
Dries's avatar
   
Dries committed
653
654
655
656
657
658
  static $nodes = array();

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

659
  $cachable = ($revision == NULL);
660
  $arguments = array();
661
  if (is_numeric($param)) {
662
    if ($cachable) {
663
      // Is the node statically cached?
664
665
666
      if (isset($nodes[$param])) {
        return is_object($nodes[$param]) ? drupal_clone($nodes[$param]) : $nodes[$param];
      }
667
    }
668
669
    $cond = 'n.nid = %d';
    $arguments[] = $param;
Dries's avatar
   
Dries committed
670
  }
671
  elseif (is_array($param)) {
672
    // Turn the conditions into a query.
673
    foreach ($param as $key => $value) {
674
675
      $cond[] = 'n.'. db_escape_string($key) ." = '%s'";
      $arguments[] = $value;
676
677
    }
    $cond = implode(' AND ', $cond);
Dries's avatar
   
Dries committed
678
  }
679
680
681
  else {
    return FALSE;
  }
Dries's avatar
   
Dries committed
682

683
684
685
  // Retrieve a field list based on the site's schema.
  $fields = drupal_schema_fields_sql('node', 'n');
  $fields = array_merge($fields, drupal_schema_fields_sql('node_revisions', 'r'));
686
  $fields = array_merge($fields, array('u.name', 'u.picture', 'u.data'));
687
688
689
690
  // Remove fields not needed in the query: n.vid and r.nid are redundant,
  // n.title is unnecessary because the node title comes from the
  // node_revisions table.  We'll keep r.vid, r.title, and n.nid.
  $fields = array_diff($fields, array('n.vid', 'n.title', 'r.nid'));
691
  $fields = implode(', ', $fields);
692
  // Rename timestamp field for clarity.
693
  $fields = str_replace('r.timestamp', 'r.timestamp AS revision_timestamp', $fields);
694
695
  // Change name of revision uid so it doesn't conflict with n.uid.
  $fields = str_replace('r.uid', 'r.uid AS revision_uid', $fields);
696

Dries's avatar
   
Dries committed
697
  // Retrieve the node.
698
  // No db_rewrite_sql is applied so as to get complete indexing for search.
699
  if ($revision) {
700
    array_unshift($arguments, $revision);
701
    $node = db_fetch_object(db_query('SELECT '. $fields .' 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
702
  }
703
  else {
704
    $node = db_fetch_object(db_query('SELECT '. $fields .' 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
705
706
  }

707
  if ($node && $node->nid) {
708
709
710
711
712
713
    // 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
714
715
    }

716
717
718
719
720
    if ($extra = node_invoke_nodeapi($node, 'load')) {
      foreach ($extra as $key => $value) {
        $node->$key = $value;
      }
    }
721
722
723
    if ($cachable) {
      $nodes[$node->nid] = is_object($node) ? drupal_clone($node) : $node;
    }
Dries's avatar
   
Dries committed
724
725
  }

Dries's avatar
   
Dries committed
726
727
728
  return $node;
}

729
730
731
732
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
773
774
775
776
777
778
779
780
781
/**
 * Perform validation checks on the given node.
 */
function node_validate($node, $form = array()) {
  // Convert the node to an object, if necessary.
  $node = (object)$node;
  $type = node_get_types('type', $node);

  // Make sure the body has the minimum number of words.
  // todo use a better word counting algorithm that will work in other languages
  if (!empty($type->min_word_count) && isset($node->body) && count(explode(' ', $node->body)) < $type->min_word_count) {
    form_set_error('body', t('The body of your @type is too short. You need at least %words words.', array('%words' => $type->min_word_count, '@type' => $type->name)));
  }

  if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) {
    form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.'));
  }

  if (user_access('administer nodes')) {
    // Validate the "authored by" field.
    if (!empty($node->name) && !($account = user_load(array('name' => $node->name)))) {
      // The use of empty() is mandatory in the context of usernames
      // as the empty string denotes the anonymous user. In case we
      // are dealing with an anonymous user we set the user ID to 0.
      form_set_error('name', t('The username %name does not exist.', array('%name' => $node->name)));
    }

    // Validate the "authored on" field. As of PHP 5.1.0, strtotime returns FALSE instead of -1 upon failure.
    if (!empty($node->date) && strtotime($node->date) <= 0) {
      form_set_error('date', t('You have to specify a valid date.'));
    }
  }

  // Do node-type-specific validation checks.
  node_invoke($node, 'validate', $form);
  node_invoke_nodeapi($node, 'validate', $form);
}

/**
 * Prepare node for save and allow modules to make changes.
 */
function node_submit($node) {
  global $user;

  // Convert the node to an object, if necessary.
  $node = (object)$node;

  // Auto-generate the teaser, but only if it hasn't been set (e.g. by a
  // module-provided 'teaser' form item).
  if (!isset($node->teaser)) {
    if (isset($node->body)) {
      $node->teaser = node_teaser($node->body, isset($node->format) ? $node->format : NULL);
      // Chop off the teaser from the body if needed.
782
      if (empty($node->teaser_include) && $node->teaser == substr($node->body, 0, strlen($node->teaser))) {
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
        $node->body = substr($node->body, strlen($node->teaser));
      }
    }
    else {
      $node->teaser = '';
    }
  }

  if (user_access('administer nodes')) {
    // Populate the "authored by" field.
    if ($account = user_load(array('name' => $node->name))) {
      $node->uid = $account->uid;
    }
    else {
      $node->uid = 0;
    }
  }
800
  $node->created = !empty($node->date) ? strtotime($node->date) : time();
801
802
803
804
805
  $node->validated = TRUE;

  return $node;
}

Dries's avatar
   
Dries committed
806
807
808
/**
 * Save a node object into the database.
 */
809
function node_save(&$node) {
810
811
  // Let modules modify the node before it is saved to the database.
  node_invoke_nodeapi($node, 'presave');
812
  global $user;
Dries's avatar
   
Dries committed
813

814
  $node->is_new = FALSE;
Dries's avatar
   
Dries committed
815

Dries's avatar
   
Dries committed
816
  // Apply filters to some default node fields:
Dries's avatar
   
Dries committed
817
  if (empty($node->nid)) {
Dries's avatar
   
Dries committed
818
    // Insert a new node.
819
    $node->is_new = TRUE;
820
821
822
823
824
825
826
827
828

    // When inserting a node, $node->log must be set because
    // {node_revisions}.log does not (and cannot) have a default
    // value.  If the user does not have permission to create
    // revisions, however, the form will not contain an element for
    // log so $node->log will be unset at this point.
    if (!isset($node->log)) {
      $node->log = '';
    }
829
830
831
832
833
834
835
836
837
838

    // For the same reasons, make sure we have $node->teaser and
    // $node->body.  We should consider making these fields nullable
    // in a future version since node types are not required to use them.
    if (!isset($node->teaser)) {
      $node->teaser = '';
    }
    if (!isset($node->body)) {
      $node->body = '';
    }
839
  }
840
841
842
  elseif (!empty($node->revision)) {
    $node->old_vid = $node->vid;
  }
843
  else {
844
845
846
    // When updating a node, avoid clobberring an existing log entry with an empty one.
    if (empty($node->log)) {
      unset($node->log);
847
    }
Dries's avatar
   
Dries committed
848
849
  }

850
851
852
853
  // Set some required fields:
  if (empty($node->created)) {
    $node->created = time();
  }
854
  // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
855
  $node->changed = time();
Dries's avatar
   
Dries committed
856

857
858
  $node->timestamp = time();
  $node->format = isset($node->format) ? $node->format : FILTER_FORMAT_DEFAULT;
859
  $update_node = TRUE;
860

861
862
863
  //Generate the node table query and the
  //the node_revisions table query
  if ($node->is_new) {
864
    drupal_write_record('node', $node);
865
    _node_save_revision($node, $user->uid);
866
    $op = 'insert';
867
868
  }
  else {
869
    drupal_write_record('node', $node, 'nid');
870
    if (!empty($node->revision)) {
871
      _node_save_revision($node, $user->uid);
872
873
    }
    else {
874
      _node_save_revision($node, $user->uid, 'vid');
875
      $update_node = FALSE;
Dries's avatar
   
Dries committed
876
    }
877
    $op = 'update';
878
  }
879
  if ($update_node) {
880
    db_query('UPDATE {node} SET vid = %d WHERE nid = %d', $node->vid, $node->nid);
881
882
  }

883
884
885
886
 // Call the node specific callback (if any):
  node_invoke($node, $op);
  node_invoke_nodeapi($node, $op);

887
888
889
  // Update the node access table for this node.
  node_access_acquire_grants($node);

890
  // Clear the page and block caches.
Dries's avatar
   
Dries committed
891
  cache_clear_all();
Dries's avatar
   
Dries committed
892
893
}

894
895

/**
896
897
898
899
 * Helper function to save a revision with the uid of the current user.
 *
 * Node is taken by reference, becuse drupal_write_record() updates the
 * $node with the revision id, and we need to pass that back to the caller.
900
 */
901
function _node_save_revision(&$node, $uid, $update = NULL) {
902
903
904
905
906
907
908
909
910
911
912
  $temp_uid = $node->uid;
  $node->uid = $uid;
  if (isset($update)) {
    drupal_write_record('node_revisions', $node, $update);
  }
  else {
    drupal_write_record('node_revisions', $node);
  }
  $node->uid = $temp_uid;
}

913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
/**
 * Delete a node.
 */
function node_delete($nid) {

  $node = node_load($nid);

  if (node_access('delete', $node)) {
    db_query('DELETE FROM {node} WHERE nid = %d', $node->nid);
    db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid);

    // Call the node-specific callback (if any):
    node_invoke($node, 'delete');
    node_invoke_nodeapi($node, 'delete');

    // Clear the page and block caches.
    cache_clear_all();

    // Remove this node from the search index if needed.
    if (function_exists('search_wipe')) {
      search_wipe($node->nid, 'node');
    }
    watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title));
936
    drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
937
938
939
  }
}

Dries's avatar
   
Dries committed
940
941
942
943
944
945
/**
 * Generate a display of the given node.
 *
 * @param $node
 *   A node array or node object.
 * @param $teaser
946
 *   Whether to display the teaser only or the full form.
Dries's avatar
   
Dries committed
947
948
 * @param $page
 *   Whether the node is being displayed by itself as a page.
949
950
 * @param $links
 *   Whether or not to display node links. Links are omitted for node previews.
Dries's avatar
   
Dries committed
951
952
953
954
 *
 * @return
 *   An HTML representation of the themed node.
 */
955
function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) {
956
  $node = (object)$node;
Dries's avatar
   
Dries committed
957

958
959
  $node = node_build_content($node, $teaser, $page);

960
  if ($links) {
961
    $node->links = module_invoke_all('link', 'node', $node, $teaser);
962
    drupal_alter('link', $node->links, $node);
963
  }
964
965
966
967

  // Set the proper node part, then unset unused $node part so that a bad
  // theme can not open a security hole.
  $content = drupal_render($node->content);
968
  if ($teaser) {
969
    $node->teaser = $content;
970
971
972
    unset($node->body);
  }
  else {
973
    $node->body = $content;
974
975
    unset($node->teaser);
  }
Dries's avatar
   
Dries committed
976

977
978
979
  // Allow modules to modify the fully-built node.
  node_invoke_nodeapi($node, 'alter', $teaser, $page);

Dries's avatar
   
Dries committed
980
  return theme('node', $node, $teaser, $page);
Dries's avatar
   
Dries committed
981
}
Dries's avatar
   
Dries committed
982

Dries's avatar
   
Dries committed
983
/**
984
 * Apply filters and build the node's standard elements.
Dries's avatar
   
Dries committed
985
 */
Dries's avatar
   
Dries committed
986
function node_prepare($node, $teaser = FALSE) {
987
988
989
  // First we'll overwrite the existing node teaser and body with
  // the filtered copies! Then, we'll stick those into the content
  // array and set the read more flag if appropriate.
990
  $node->readmore = (strlen($node->teaser) < strlen($node->body));
991
992
993
994
995
996
997
998

  if ($teaser == FALSE) {
    $node->body = check_markup($node->body, $node->format, FALSE);
  }
  else {
    $node->teaser = check_markup($node->teaser, $node->format, FALSE);
  }

999
  $node->content['body'] = array(
1000
    '#value' => $teaser ? $node->teaser : $node->body,
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
    '#weight' => 0,
  );

  return $node;
}

/**
 * Builds a structured array representing the node's content.
 *
 * @param $node
 *   A node object.
 * @param $teaser
 *   Whether to display the teaser only, as on the main page.
 * @param $page
 *   Whether the node is being displayed by itself as a page.
 *
 * @return
 *   An structured array containing the individual elements
 *   of the node's body.
 */
function node_build_content($node, $teaser = FALSE, $page = FALSE) {
1022
1023
1024
1025
1026
1027

  // The build mode identifies the target for which the node is built.
  if (!isset($node->build_mode)) {
    $node->build_mode = NODE_BUILD_NORMAL;
  }

1028
  // Remove the delimiter (if any) that separates the teaser from the body.
1029
  $node->body = isset($node->body) ? str_replace('<!--break-->', '', $node->body) : '';
1030
1031
1032
1033
1034

  // The 'view' hook can be implemented to overwrite the default function
  // to display nodes.
  if (node_hook($node, 'view')) {
    $node = node_invoke($node, 'view', $teaser, $page);
Dries's avatar
   
Dries committed
1035
1036
  }
  else {
1037
    $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
1038
  }
1039
1040
1041
1042

  // Allow modules to make their own additions to the node.
  node_invoke_nodeapi($node, 'view', $teaser, $page);

Dries's avatar
   
Dries committed
1043
  return $node;
Dries's avatar
   
Dries committed
1044
1045
}

Dries's avatar
   
Dries committed
1046
1047
1048
/**
 * Generate a page displaying a single node, along with its comments.
 */
1049
1050
1051
1052
function node_show($node, $cid, $message = FALSE) {
  if ($message) {
    drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))));
  }
Dries's avatar
   
Dries committed
1053
  $output = node_view($node, FALSE, TRUE);
Dries's avatar
   
Dries committed
1054

Dries's avatar
   
Dries committed
1055
1056
  if (function_exists('comment_render') && $node->comment) {
    $output .= comment_render($node, $cid);
Dries's avatar
   
Dries committed
1057
1058
  }

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

Dries's avatar
   
Dries committed
1062
  return $output;
Dries's avatar
   
Dries committed
1063
1064
}

1065
1066
1067
1068
1069
/**
 * Theme a log message.
 *
 * @ingroup themeable
 */
1070
1071
1072
1073
function theme_node_log_message($log) {
  return '<div class="log"><div class="title">'. t('Log') .':</div>'. $log .'</div>';
}

Dries's avatar
   
Dries committed
1074
1075
1076
/**
 * Implementation of hook_perm().
 */
Dries's avatar
   
Dries committed
1077
function node_perm() {
1078
  $perms = array('administer content types', 'administer nodes', 'access content', 'view revisions', 'revert revisions', 'delete revisions');
1079
1080
1081

  foreach (node_get_types() as $type) {
    if ($type->module == 'node') {
1082
      $name = check_plain($type->type);
1083
      $perms[] = 'create '. $name .' content';
1084
      $perms[] = 'delete own '. $name .' content';
1085
      $perms[] = 'delete any '. $name .' content';
1086
      $perms[] = 'edit own '. $name .' content';
1087
      $perms[] = 'edit any '. $name .' content';
1088
1089
1090
1091
    }
  }

  return $perms;
Dries's avatar
   
Dries committed
1092
1093
}

Dries's avatar
   
Dries committed
1094
1095
1096
/**
 * Implementation of hook_search().
 */
1097
function node_search($op = 'search', $keys = NULL) {
1098
1099
  switch ($op) {
    case 'name':
1100
      return t('Content');
1101

Dries's avatar
Dries committed
1102
    case 'reset':
1103
      db_query("UPDATE {search_dataset} SET reindex = %d WHERE type = 'node'", time());
Dries's avatar
Dries committed
1104
      return;
1105

1106
    case 'status':
1107
      $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
1108
      $remaining = db_result(db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0"));
1109
      return array('remaining' => $remaining, 'total' => $total);
1110
1111
1112
1113

    case 'admin':
      $form = array();
      // Output form for defining rank factor weights.
1114
      $form['content_ranking'] = array(
1115