|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * Enables users to comment on published content. |
|
6 * |
|
7 * When enabled, the Drupal comment module creates a discussion |
|
8 * board for each Drupal node. Users can post comments to discuss |
|
9 * a forum topic, weblog post, story, collaborative book page, etc. |
|
10 */ |
|
11 |
|
12 /** |
|
13 * Comment is awaiting approval. |
|
14 */ |
|
15 define('COMMENT_NOT_PUBLISHED', 0); |
|
16 |
|
17 /** |
|
18 * Comment is published. |
|
19 */ |
|
20 define('COMMENT_PUBLISHED', 1); |
|
21 |
|
22 /** |
|
23 * Comments are displayed in a flat list - expanded. |
|
24 */ |
|
25 define('COMMENT_MODE_FLAT', 0); |
|
26 |
|
27 /** |
|
28 * Comments are displayed as a threaded list - expanded. |
|
29 */ |
|
30 define('COMMENT_MODE_THREADED', 1); |
|
31 |
|
32 /** |
|
33 * Anonymous posters cannot enter their contact information. |
|
34 */ |
|
35 define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0); |
|
36 |
|
37 /** |
|
38 * Anonymous posters may leave their contact information. |
|
39 */ |
|
40 define('COMMENT_ANONYMOUS_MAY_CONTACT', 1); |
|
41 |
|
42 /** |
|
43 * Anonymous posters are required to leave their contact information. |
|
44 */ |
|
45 define('COMMENT_ANONYMOUS_MUST_CONTACT', 2); |
|
46 |
|
47 /** |
|
48 * Comment form should be displayed on a separate page. |
|
49 */ |
|
50 define('COMMENT_FORM_SEPARATE_PAGE', 0); |
|
51 |
|
52 /** |
|
53 * Comment form should be shown below post or list of comments. |
|
54 */ |
|
55 define('COMMENT_FORM_BELOW', 1); |
|
56 |
|
57 /** |
|
58 * Comments for this node are hidden. |
|
59 */ |
|
60 define('COMMENT_NODE_HIDDEN', 0); |
|
61 |
|
62 /** |
|
63 * Comments for this node are closed. |
|
64 */ |
|
65 define('COMMENT_NODE_CLOSED', 1); |
|
66 |
|
67 /** |
|
68 * Comments for this node are open. |
|
69 */ |
|
70 define('COMMENT_NODE_OPEN', 2); |
|
71 |
|
72 /** |
|
73 * Implements hook_help(). |
|
74 */ |
|
75 function comment_help($path, $arg) { |
|
76 switch ($path) { |
|
77 case 'admin/help#comment': |
|
78 $output = '<h3>' . t('About') . '</h3>'; |
|
79 $output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/documentation/modules/comment/')) . '</p>'; |
|
80 $output .= '<h3>' . t('Uses') . '</h3>'; |
|
81 $output .= '<dl>'; |
|
82 $output .= '<dt>' . t('Default and custom settings') . '</dt>'; |
|
83 $output .= '<dd>' . t("Each <a href='@content-type'>content type</a> can have its own default comment settings configured as: <em>Open</em> to allow new comments, <em>Hidden</em> to hide existing comments and prevent new comments, or <em>Closed</em> to view existing comments, but prevent new comments. These defaults will apply to all new content created (changes to the settings on existing content must be done manually). Other comment settings can also be customized per content type, and can be overridden for any given item of content. When a comment has no replies, it remains editable by its author, as long as the author has a user account and is logged in.", array('@content-type' => url('admin/structure/types'))) . '</dd>'; |
|
84 $output .= '<dt>' . t('Comment approval') . '</dt>'; |
|
85 $output .= '<dd>' . t("Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href='@comment-approval'>Unapproved comments</a> queue, until a user who has permission to <em>Administer comments</em> publishes or deletes them. Published comments can be bulk managed on the <a href='@admin-comment'>Published comments</a> administration page.", array('@comment-approval' => url('admin/content/comment/approval'), '@admin-comment' => url('admin/content/comment'))) . '</dd>'; |
|
86 $output .= '</dl>'; |
|
87 return $output; |
|
88 } |
|
89 } |
|
90 |
|
91 /** |
|
92 * Implements hook_entity_info(). |
|
93 */ |
|
94 function comment_entity_info() { |
|
95 $return = array( |
|
96 'comment' => array( |
|
97 'label' => t('Comment'), |
|
98 'base table' => 'comment', |
|
99 'uri callback' => 'comment_uri', |
|
100 'fieldable' => TRUE, |
|
101 'controller class' => 'CommentController', |
|
102 'entity keys' => array( |
|
103 'id' => 'cid', |
|
104 'bundle' => 'node_type', |
|
105 'label' => 'subject', |
|
106 'language' => 'language', |
|
107 ), |
|
108 'bundles' => array(), |
|
109 'view modes' => array( |
|
110 'full' => array( |
|
111 'label' => t('Full comment'), |
|
112 'custom settings' => FALSE, |
|
113 ), |
|
114 ), |
|
115 'static cache' => FALSE, |
|
116 ), |
|
117 ); |
|
118 |
|
119 foreach (node_type_get_names() as $type => $name) { |
|
120 $return['comment']['bundles']['comment_node_' . $type] = array( |
|
121 'label' => t('@node_type comment', array('@node_type' => $name)), |
|
122 // Provide the node type/bundle name for other modules, so it does not |
|
123 // have to be extracted manually from the bundle name. |
|
124 'node bundle' => $type, |
|
125 'admin' => array( |
|
126 // Place the Field UI paths for comments one level below the |
|
127 // corresponding paths for nodes, so that they appear in the same set |
|
128 // of local tasks. Note that the paths use a different placeholder name |
|
129 // and thus a different menu loader callback, so that Field UI page |
|
130 // callbacks get a comment bundle name from the node type in the URL. |
|
131 // See comment_node_type_load() and comment_menu_alter(). |
|
132 'path' => 'admin/structure/types/manage/%comment_node_type/comment', |
|
133 'bundle argument' => 4, |
|
134 'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment', |
|
135 'access arguments' => array('administer content types'), |
|
136 ), |
|
137 ); |
|
138 } |
|
139 |
|
140 return $return; |
|
141 } |
|
142 |
|
143 /** |
|
144 * Menu loader callback for Field UI paths. |
|
145 * |
|
146 * Return a comment bundle name from a node type in the URL. |
|
147 */ |
|
148 function comment_node_type_load($name) { |
|
149 if ($type = node_type_get_type(strtr($name, array('-' => '_')))) { |
|
150 return 'comment_node_' . $type->type; |
|
151 } |
|
152 } |
|
153 |
|
154 /** |
|
155 * Implements callback_entity_info_uri(). |
|
156 */ |
|
157 function comment_uri($comment) { |
|
158 return array( |
|
159 'path' => 'comment/' . $comment->cid, |
|
160 'options' => array('fragment' => 'comment-' . $comment->cid), |
|
161 ); |
|
162 } |
|
163 |
|
164 /** |
|
165 * Implements hook_field_extra_fields(). |
|
166 */ |
|
167 function comment_field_extra_fields() { |
|
168 $return = array(); |
|
169 |
|
170 foreach (node_type_get_types() as $type) { |
|
171 if (variable_get('comment_subject_field_' . $type->type, 1) == 1) { |
|
172 $return['comment']['comment_node_' . $type->type] = array( |
|
173 'form' => array( |
|
174 'author' => array( |
|
175 'label' => t('Author'), |
|
176 'description' => t('Author textfield'), |
|
177 'weight' => -2, |
|
178 ), |
|
179 'subject' => array( |
|
180 'label' => t('Subject'), |
|
181 'description' => t('Subject textfield'), |
|
182 'weight' => -1, |
|
183 ), |
|
184 ), |
|
185 ); |
|
186 } |
|
187 } |
|
188 |
|
189 return $return; |
|
190 } |
|
191 |
|
192 /** |
|
193 * Implements hook_theme(). |
|
194 */ |
|
195 function comment_theme() { |
|
196 return array( |
|
197 'comment_block' => array( |
|
198 'variables' => array(), |
|
199 ), |
|
200 'comment_preview' => array( |
|
201 'variables' => array('comment' => NULL), |
|
202 ), |
|
203 'comment' => array( |
|
204 'template' => 'comment', |
|
205 'render element' => 'elements', |
|
206 ), |
|
207 'comment_post_forbidden' => array( |
|
208 'variables' => array('node' => NULL), |
|
209 ), |
|
210 'comment_wrapper' => array( |
|
211 'template' => 'comment-wrapper', |
|
212 'render element' => 'content', |
|
213 ), |
|
214 ); |
|
215 } |
|
216 |
|
217 /** |
|
218 * Implements hook_menu(). |
|
219 */ |
|
220 function comment_menu() { |
|
221 $items['admin/content/comment'] = array( |
|
222 'title' => 'Comments', |
|
223 'description' => 'List and edit site comments and the comment approval queue.', |
|
224 'page callback' => 'comment_admin', |
|
225 'access arguments' => array('administer comments'), |
|
226 'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM, |
|
227 'file' => 'comment.admin.inc', |
|
228 ); |
|
229 // Tabs begin here. |
|
230 $items['admin/content/comment/new'] = array( |
|
231 'title' => 'Published comments', |
|
232 'type' => MENU_DEFAULT_LOCAL_TASK, |
|
233 'weight' => -10, |
|
234 ); |
|
235 $items['admin/content/comment/approval'] = array( |
|
236 'title' => 'Unapproved comments', |
|
237 'title callback' => 'comment_count_unpublished', |
|
238 'page arguments' => array('approval'), |
|
239 'access arguments' => array('administer comments'), |
|
240 'type' => MENU_LOCAL_TASK, |
|
241 ); |
|
242 $items['comment/%'] = array( |
|
243 'title' => 'Comment permalink', |
|
244 'page callback' => 'comment_permalink', |
|
245 'page arguments' => array(1), |
|
246 'access arguments' => array('access comments'), |
|
247 ); |
|
248 $items['comment/%/view'] = array( |
|
249 'title' => 'View comment', |
|
250 'type' => MENU_DEFAULT_LOCAL_TASK, |
|
251 'weight' => -10, |
|
252 ); |
|
253 // Every other comment path uses %, but this one loads the comment directly, |
|
254 // so we don't end up loading it twice (in the page and access callback). |
|
255 $items['comment/%comment/edit'] = array( |
|
256 'title' => 'Edit', |
|
257 'page callback' => 'comment_edit_page', |
|
258 'page arguments' => array(1), |
|
259 'access callback' => 'comment_access', |
|
260 'access arguments' => array('edit', 1), |
|
261 'type' => MENU_LOCAL_TASK, |
|
262 'weight' => 0, |
|
263 ); |
|
264 $items['comment/%/approve'] = array( |
|
265 'title' => 'Approve', |
|
266 'page callback' => 'comment_approve', |
|
267 'page arguments' => array(1), |
|
268 'access arguments' => array('administer comments'), |
|
269 'file' => 'comment.pages.inc', |
|
270 'weight' => 1, |
|
271 ); |
|
272 $items['comment/%/delete'] = array( |
|
273 'title' => 'Delete', |
|
274 'page callback' => 'comment_confirm_delete_page', |
|
275 'page arguments' => array(1), |
|
276 'access arguments' => array('administer comments'), |
|
277 'type' => MENU_LOCAL_TASK, |
|
278 'file' => 'comment.admin.inc', |
|
279 'weight' => 2, |
|
280 ); |
|
281 $items['comment/reply/%node'] = array( |
|
282 'title' => 'Add new comment', |
|
283 'page callback' => 'comment_reply', |
|
284 'page arguments' => array(2), |
|
285 'access callback' => 'node_access', |
|
286 'access arguments' => array('view', 2), |
|
287 'file' => 'comment.pages.inc', |
|
288 ); |
|
289 |
|
290 return $items; |
|
291 } |
|
292 |
|
293 /** |
|
294 * Implements hook_menu_alter(). |
|
295 */ |
|
296 function comment_menu_alter(&$items) { |
|
297 // Add comments to the description for admin/content. |
|
298 $items['admin/content']['description'] = 'Administer content and comments.'; |
|
299 |
|
300 // Adjust the Field UI tabs on admin/structure/types/manage/[node-type]. |
|
301 // See comment_entity_info(). |
|
302 $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields'; |
|
303 $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3; |
|
304 $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display'; |
|
305 $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4; |
|
306 } |
|
307 |
|
308 /** |
|
309 * Returns a menu title which includes the number of unapproved comments. |
|
310 */ |
|
311 function comment_count_unpublished() { |
|
312 $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array( |
|
313 ':status' => COMMENT_NOT_PUBLISHED, |
|
314 ))->fetchField(); |
|
315 return t('Unapproved comments (@count)', array('@count' => $count)); |
|
316 } |
|
317 |
|
318 /** |
|
319 * Implements hook_node_type_insert(). |
|
320 * |
|
321 * Creates a comment body field for a node type created while the comment module |
|
322 * is enabled. For node types created before the comment module is enabled, |
|
323 * hook_modules_enabled() serves to create the body fields. |
|
324 * |
|
325 * @see comment_modules_enabled() |
|
326 */ |
|
327 function comment_node_type_insert($info) { |
|
328 _comment_body_field_create($info); |
|
329 } |
|
330 |
|
331 /** |
|
332 * Implements hook_node_type_update(). |
|
333 */ |
|
334 function comment_node_type_update($info) { |
|
335 if (!empty($info->old_type) && $info->type != $info->old_type) { |
|
336 field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type); |
|
337 } |
|
338 } |
|
339 |
|
340 /** |
|
341 * Implements hook_node_type_delete(). |
|
342 */ |
|
343 function comment_node_type_delete($info) { |
|
344 field_attach_delete_bundle('comment', 'comment_node_' . $info->type); |
|
345 $settings = array( |
|
346 'comment', |
|
347 'comment_default_mode', |
|
348 'comment_default_per_page', |
|
349 'comment_anonymous', |
|
350 'comment_subject_field', |
|
351 'comment_preview', |
|
352 'comment_form_location', |
|
353 ); |
|
354 foreach ($settings as $setting) { |
|
355 variable_del($setting . '_' . $info->type); |
|
356 } |
|
357 } |
|
358 |
|
359 /** |
|
360 * Creates a comment_body field instance for a given node type. |
|
361 */ |
|
362 function _comment_body_field_create($info) { |
|
363 // Create the field if needed. |
|
364 if (!field_read_field('comment_body', array('include_inactive' => TRUE))) { |
|
365 $field = array( |
|
366 'field_name' => 'comment_body', |
|
367 'type' => 'text_long', |
|
368 'entity_types' => array('comment'), |
|
369 ); |
|
370 field_create_field($field); |
|
371 } |
|
372 // Create the instance if needed. |
|
373 if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) { |
|
374 field_attach_create_bundle('comment', 'comment_node_' . $info->type); |
|
375 // Attaches the body field by default. |
|
376 $instance = array( |
|
377 'field_name' => 'comment_body', |
|
378 'label' => 'Comment', |
|
379 'entity_type' => 'comment', |
|
380 'bundle' => 'comment_node_' . $info->type, |
|
381 'settings' => array('text_processing' => 1), |
|
382 'required' => TRUE, |
|
383 'display' => array( |
|
384 'default' => array( |
|
385 'label' => 'hidden', |
|
386 'type' => 'text_default', |
|
387 'weight' => 0, |
|
388 ), |
|
389 ), |
|
390 ); |
|
391 field_create_instance($instance); |
|
392 } |
|
393 } |
|
394 |
|
395 /** |
|
396 * Implements hook_permission(). |
|
397 */ |
|
398 function comment_permission() { |
|
399 return array( |
|
400 'administer comments' => array( |
|
401 'title' => t('Administer comments and comment settings'), |
|
402 ), |
|
403 'access comments' => array( |
|
404 'title' => t('View comments'), |
|
405 ), |
|
406 'post comments' => array( |
|
407 'title' => t('Post comments'), |
|
408 ), |
|
409 'skip comment approval' => array( |
|
410 'title' => t('Skip comment approval'), |
|
411 ), |
|
412 'edit own comments' => array( |
|
413 'title' => t('Edit own comments'), |
|
414 ), |
|
415 ); |
|
416 } |
|
417 |
|
418 /** |
|
419 * Implements hook_block_info(). |
|
420 */ |
|
421 function comment_block_info() { |
|
422 $blocks['recent']['info'] = t('Recent comments'); |
|
423 $blocks['recent']['properties']['administrative'] = TRUE; |
|
424 |
|
425 return $blocks; |
|
426 } |
|
427 |
|
428 /** |
|
429 * Implements hook_block_configure(). |
|
430 */ |
|
431 function comment_block_configure($delta = '') { |
|
432 $form['comment_block_count'] = array( |
|
433 '#type' => 'select', |
|
434 '#title' => t('Number of recent comments'), |
|
435 '#default_value' => variable_get('comment_block_count', 10), |
|
436 '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)), |
|
437 ); |
|
438 |
|
439 return $form; |
|
440 } |
|
441 |
|
442 /** |
|
443 * Implements hook_block_save(). |
|
444 */ |
|
445 function comment_block_save($delta = '', $edit = array()) { |
|
446 variable_set('comment_block_count', (int) $edit['comment_block_count']); |
|
447 } |
|
448 |
|
449 /** |
|
450 * Implements hook_block_view(). |
|
451 * |
|
452 * Generates a block with the most recent comments. |
|
453 */ |
|
454 function comment_block_view($delta = '') { |
|
455 if (user_access('access comments')) { |
|
456 $block['subject'] = t('Recent comments'); |
|
457 $block['content'] = theme('comment_block'); |
|
458 |
|
459 return $block; |
|
460 } |
|
461 } |
|
462 |
|
463 /** |
|
464 * Redirects comment links to the correct page depending on comment settings. |
|
465 * |
|
466 * Since comments are paged there is no way to guarantee which page a comment |
|
467 * appears on. Comment paging and threading settings may be changed at any time. |
|
468 * With threaded comments, an individual comment may move between pages as |
|
469 * comments can be added either before or after it in the overall discussion. |
|
470 * Therefore we use a central routing function for comment links, which |
|
471 * calculates the page number based on current comment settings and returns |
|
472 * the full comment view with the pager set dynamically. |
|
473 * |
|
474 * @param $cid |
|
475 * A comment identifier. |
|
476 * @return |
|
477 * The comment listing set to the page on which the comment appears. |
|
478 */ |
|
479 function comment_permalink($cid) { |
|
480 if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) { |
|
481 |
|
482 // Find the current display page for this comment. |
|
483 $page = comment_get_display_page($comment->cid, $node->type); |
|
484 |
|
485 // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback |
|
486 // behaves as it would when visiting the page directly. |
|
487 $_GET['q'] = 'node/' . $node->nid; |
|
488 $_GET['page'] = $page; |
|
489 |
|
490 // Return the node view, this will show the correct comment in context. |
|
491 return menu_execute_active_handler('node/' . $node->nid, FALSE); |
|
492 } |
|
493 return MENU_NOT_FOUND; |
|
494 } |
|
495 |
|
496 /** |
|
497 * Find the most recent comments that are available to the current user. |
|
498 * |
|
499 * @param integer $number |
|
500 * (optional) The maximum number of comments to find. Defaults to 10. |
|
501 * |
|
502 * @return |
|
503 * An array of comment objects or an empty array if there are no recent |
|
504 * comments visible to the current user. |
|
505 */ |
|
506 function comment_get_recent($number = 10) { |
|
507 $query = db_select('comment', 'c'); |
|
508 $query->innerJoin('node', 'n', 'n.nid = c.nid'); |
|
509 $query->addTag('node_access'); |
|
510 $comments = $query |
|
511 ->fields('c') |
|
512 ->condition('c.status', COMMENT_PUBLISHED) |
|
513 ->condition('n.status', NODE_PUBLISHED) |
|
514 ->orderBy('c.created', 'DESC') |
|
515 // Additionally order by cid to ensure that comments with the same timestamp |
|
516 // are returned in the exact order posted. |
|
517 ->orderBy('c.cid', 'DESC') |
|
518 ->range(0, $number) |
|
519 ->execute() |
|
520 ->fetchAll(); |
|
521 |
|
522 return $comments ? $comments : array(); |
|
523 } |
|
524 |
|
525 /** |
|
526 * Calculate page number for first new comment. |
|
527 * |
|
528 * @param $num_comments |
|
529 * Number of comments. |
|
530 * @param $new_replies |
|
531 * Number of new replies. |
|
532 * @param $node |
|
533 * The first new comment node. |
|
534 * @return |
|
535 * "page=X" if the page number is greater than zero; empty string otherwise. |
|
536 */ |
|
537 function comment_new_page_count($num_comments, $new_replies, $node) { |
|
538 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED); |
|
539 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); |
|
540 $pagenum = NULL; |
|
541 $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE; |
|
542 if ($num_comments <= $comments_per_page) { |
|
543 // Only one page of comments. |
|
544 $pageno = 0; |
|
545 } |
|
546 elseif ($flat) { |
|
547 // Flat comments. |
|
548 $count = $num_comments - $new_replies; |
|
549 $pageno = $count / $comments_per_page; |
|
550 } |
|
551 else { |
|
552 // Threaded comments: we build a query with a subquery to find the first |
|
553 // thread with a new comment. |
|
554 |
|
555 // 1. Find all the threads with a new comment. |
|
556 $unread_threads_query = db_select('comment') |
|
557 ->fields('comment', array('thread')) |
|
558 ->condition('nid', $node->nid) |
|
559 ->condition('status', COMMENT_PUBLISHED) |
|
560 ->orderBy('created', 'DESC') |
|
561 ->orderBy('cid', 'DESC') |
|
562 ->range(0, $new_replies); |
|
563 |
|
564 // 2. Find the first thread. |
|
565 $first_thread = db_select($unread_threads_query, 'thread') |
|
566 ->fields('thread', array('thread')) |
|
567 ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))') |
|
568 ->range(0, 1) |
|
569 ->execute() |
|
570 ->fetchField(); |
|
571 |
|
572 // Remove the final '/'. |
|
573 $first_thread = substr($first_thread, 0, -1); |
|
574 |
|
575 // Find the number of the first comment of the first unread thread. |
|
576 $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array( |
|
577 ':status' => COMMENT_PUBLISHED, |
|
578 ':nid' => $node->nid, |
|
579 ':thread' => $first_thread, |
|
580 ))->fetchField(); |
|
581 |
|
582 $pageno = $count / $comments_per_page; |
|
583 } |
|
584 |
|
585 if ($pageno >= 1) { |
|
586 $pagenum = array('page' => intval($pageno)); |
|
587 } |
|
588 |
|
589 return $pagenum; |
|
590 } |
|
591 |
|
592 /** |
|
593 * Returns HTML for a list of recent comments to be displayed in the comment block. |
|
594 * |
|
595 * @ingroup themeable |
|
596 */ |
|
597 function theme_comment_block() { |
|
598 $items = array(); |
|
599 $number = variable_get('comment_block_count', 10); |
|
600 foreach (comment_get_recent($number) as $comment) { |
|
601 $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . ' <span>' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) . '</span>'; |
|
602 } |
|
603 |
|
604 if ($items) { |
|
605 return theme('item_list', array('items' => $items)); |
|
606 } |
|
607 else { |
|
608 return t('No comments available.'); |
|
609 } |
|
610 } |
|
611 |
|
612 /** |
|
613 * Implements hook_node_view(). |
|
614 */ |
|
615 function comment_node_view($node, $view_mode) { |
|
616 $links = array(); |
|
617 |
|
618 if ($node->comment != COMMENT_NODE_HIDDEN) { |
|
619 if ($view_mode == 'rss') { |
|
620 // Add a comments RSS element which is a URL to the comments of this node. |
|
621 $node->rss_elements[] = array( |
|
622 'key' => 'comments', |
|
623 'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE)) |
|
624 ); |
|
625 } |
|
626 elseif ($view_mode == 'teaser') { |
|
627 // Teaser view: display the number of comments that have been posted, |
|
628 // or a link to add new comments if the user has permission, the node |
|
629 // is open to new comments, and there currently are none. |
|
630 if (user_access('access comments')) { |
|
631 if (!empty($node->comment_count)) { |
|
632 $links['comment-comments'] = array( |
|
633 'title' => format_plural($node->comment_count, '1 comment', '@count comments'), |
|
634 'href' => "node/$node->nid", |
|
635 'attributes' => array('title' => t('Jump to the first comment of this posting.')), |
|
636 'fragment' => 'comments', |
|
637 'html' => TRUE, |
|
638 ); |
|
639 // Show a link to the first new comment. |
|
640 if ($new = comment_num_new($node->nid)) { |
|
641 $links['comment-new-comments'] = array( |
|
642 'title' => format_plural($new, '1 new comment', '@count new comments'), |
|
643 'href' => "node/$node->nid", |
|
644 'query' => comment_new_page_count($node->comment_count, $new, $node), |
|
645 'attributes' => array('title' => t('Jump to the first new comment of this posting.')), |
|
646 'fragment' => 'new', |
|
647 'html' => TRUE, |
|
648 ); |
|
649 } |
|
650 } |
|
651 } |
|
652 if ($node->comment == COMMENT_NODE_OPEN) { |
|
653 if (user_access('post comments')) { |
|
654 $links['comment-add'] = array( |
|
655 'title' => t('Add new comment'), |
|
656 'href' => "comment/reply/$node->nid", |
|
657 'attributes' => array('title' => t('Add a new comment to this page.')), |
|
658 'fragment' => 'comment-form', |
|
659 ); |
|
660 } |
|
661 else { |
|
662 $links['comment_forbidden'] = array( |
|
663 'title' => theme('comment_post_forbidden', array('node' => $node)), |
|
664 'html' => TRUE, |
|
665 ); |
|
666 } |
|
667 } |
|
668 } |
|
669 elseif ($view_mode != 'search_index' && $view_mode != 'search_result') { |
|
670 // Node in other view modes: add a "post comment" link if the user is |
|
671 // allowed to post comments and if this node is allowing new comments. |
|
672 // But we don't want this link if we're building the node for search |
|
673 // indexing or constructing a search result excerpt. |
|
674 if ($node->comment == COMMENT_NODE_OPEN) { |
|
675 $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW); |
|
676 if (user_access('post comments')) { |
|
677 // Show the "post comment" link if the form is on another page, or |
|
678 // if there are existing comments that the link will skip past. |
|
679 if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->comment_count) && user_access('access comments'))) { |
|
680 $links['comment-add'] = array( |
|
681 'title' => t('Add new comment'), |
|
682 'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')), |
|
683 'href' => "node/$node->nid", |
|
684 'fragment' => 'comment-form', |
|
685 ); |
|
686 if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) { |
|
687 $links['comment-add']['href'] = "comment/reply/$node->nid"; |
|
688 } |
|
689 } |
|
690 } |
|
691 else { |
|
692 $links['comment_forbidden'] = array( |
|
693 'title' => theme('comment_post_forbidden', array('node' => $node)), |
|
694 'html' => TRUE, |
|
695 ); |
|
696 } |
|
697 } |
|
698 } |
|
699 |
|
700 $node->content['links']['comment'] = array( |
|
701 '#theme' => 'links__node__comment', |
|
702 '#links' => $links, |
|
703 '#attributes' => array('class' => array('links', 'inline')), |
|
704 ); |
|
705 |
|
706 // Only append comments when we are building a node on its own node detail |
|
707 // page. We compare $node and $page_node to ensure that comments are not |
|
708 // appended to other nodes shown on the page, for example a node_reference |
|
709 // displayed in 'full' view mode within another node. |
|
710 if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) { |
|
711 $node->content['comments'] = comment_node_page_additions($node); |
|
712 } |
|
713 } |
|
714 } |
|
715 |
|
716 /** |
|
717 * Build the comment-related elements for node detail pages. |
|
718 * |
|
719 * @param $node |
|
720 * A node object. |
|
721 */ |
|
722 function comment_node_page_additions($node) { |
|
723 $additions = array(); |
|
724 |
|
725 // Only attempt to render comments if the node has visible comments. |
|
726 // Unpublished comments are not included in $node->comment_count, so show |
|
727 // comments unconditionally if the user is an administrator. |
|
728 if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) { |
|
729 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED); |
|
730 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); |
|
731 if ($cids = comment_get_thread($node, $mode, $comments_per_page)) { |
|
732 $comments = comment_load_multiple($cids); |
|
733 comment_prepare_thread($comments); |
|
734 $build = comment_view_multiple($comments, $node); |
|
735 $build['pager']['#theme'] = 'pager'; |
|
736 $additions['comments'] = $build; |
|
737 } |
|
738 } |
|
739 |
|
740 // Append comment form if needed. |
|
741 if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) { |
|
742 $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid)); |
|
743 $additions['comment_form'] = $build; |
|
744 } |
|
745 |
|
746 if ($additions) { |
|
747 $additions += array( |
|
748 '#theme' => 'comment_wrapper__node_' . $node->type, |
|
749 '#node' => $node, |
|
750 'comments' => array(), |
|
751 'comment_form' => array(), |
|
752 ); |
|
753 } |
|
754 |
|
755 return $additions; |
|
756 } |
|
757 |
|
758 /** |
|
759 * Retrieve comments for a thread. |
|
760 * |
|
761 * @param $node |
|
762 * The node whose comment(s) needs rendering. |
|
763 * @param $mode |
|
764 * The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED. |
|
765 * @param $comments_per_page |
|
766 * The amount of comments to display per page. |
|
767 * |
|
768 * To display threaded comments in the correct order we keep a 'thread' field |
|
769 * and order by that value. This field keeps this data in |
|
770 * a way which is easy to update and convenient to use. |
|
771 * |
|
772 * A "thread" value starts at "1". If we add a child (A) to this comment, |
|
773 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next |
|
774 * brother of (A) will get "1.2". Next brother of the parent of (A) will get |
|
775 * "2" and so on. |
|
776 * |
|
777 * First of all note that the thread field stores the depth of the comment: |
|
778 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc. |
|
779 * |
|
780 * Now to get the ordering right, consider this example: |
|
781 * |
|
782 * 1 |
|
783 * 1.1 |
|
784 * 1.1.1 |
|
785 * 1.2 |
|
786 * 2 |
|
787 * |
|
788 * If we "ORDER BY thread ASC" we get the above result, and this is the |
|
789 * natural order sorted by time. However, if we "ORDER BY thread DESC" |
|
790 * we get: |
|
791 * |
|
792 * 2 |
|
793 * 1.2 |
|
794 * 1.1.1 |
|
795 * 1.1 |
|
796 * 1 |
|
797 * |
|
798 * Clearly, this is not a natural way to see a thread, and users will get |
|
799 * confused. The natural order to show a thread by time desc would be: |
|
800 * |
|
801 * 2 |
|
802 * 1 |
|
803 * 1.2 |
|
804 * 1.1 |
|
805 * 1.1.1 |
|
806 * |
|
807 * which is what we already did before the standard pager patch. To achieve |
|
808 * this we simply add a "/" at the end of each "thread" value. This way, the |
|
809 * thread fields will look like this: |
|
810 * |
|
811 * 1/ |
|
812 * 1.1/ |
|
813 * 1.1.1/ |
|
814 * 1.2/ |
|
815 * 2/ |
|
816 * |
|
817 * we add "/" since this char is, in ASCII, higher than every number, so if |
|
818 * now we "ORDER BY thread DESC" we get the correct order. However this would |
|
819 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need |
|
820 * to consider the trailing "/" so we use a substring only. |
|
821 */ |
|
822 function comment_get_thread($node, $mode, $comments_per_page) { |
|
823 $query = db_select('comment', 'c')->extend('PagerDefault'); |
|
824 $query->addField('c', 'cid'); |
|
825 $query |
|
826 ->condition('c.nid', $node->nid) |
|
827 ->addTag('node_access') |
|
828 ->addTag('comment_filter') |
|
829 ->addMetaData('node', $node) |
|
830 ->limit($comments_per_page); |
|
831 |
|
832 $count_query = db_select('comment', 'c'); |
|
833 $count_query->addExpression('COUNT(*)'); |
|
834 $count_query |
|
835 ->condition('c.nid', $node->nid) |
|
836 ->addTag('node_access') |
|
837 ->addTag('comment_filter') |
|
838 ->addMetaData('node', $node); |
|
839 |
|
840 if (!user_access('administer comments')) { |
|
841 $query->condition('c.status', COMMENT_PUBLISHED); |
|
842 $count_query->condition('c.status', COMMENT_PUBLISHED); |
|
843 } |
|
844 if ($mode === COMMENT_MODE_FLAT) { |
|
845 $query->orderBy('c.cid', 'ASC'); |
|
846 } |
|
847 else { |
|
848 // See comment above. Analysis reveals that this doesn't cost too |
|
849 // much. It scales much much better than having the whole comment |
|
850 // structure. |
|
851 $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder'); |
|
852 $query->orderBy('torder', 'ASC'); |
|
853 } |
|
854 |
|
855 $query->setCountQuery($count_query); |
|
856 $cids = $query->execute()->fetchCol(); |
|
857 |
|
858 return $cids; |
|
859 } |
|
860 |
|
861 /** |
|
862 * Loop over comment thread, noting indentation level. |
|
863 * |
|
864 * @param array $comments |
|
865 * An array of comment objects, keyed by cid. |
|
866 * @return |
|
867 * The $comments argument is altered by reference with indentation information. |
|
868 */ |
|
869 function comment_prepare_thread(&$comments) { |
|
870 // A flag stating if we are still searching for first new comment on the thread. |
|
871 $first_new = TRUE; |
|
872 |
|
873 // A counter that helps track how indented we are. |
|
874 $divs = 0; |
|
875 |
|
876 foreach ($comments as $key => $comment) { |
|
877 if ($first_new && $comment->new != MARK_READ) { |
|
878 // Assign the anchor only for the first new comment. This avoids duplicate |
|
879 // id attributes on a page. |
|
880 $first_new = FALSE; |
|
881 $comment->first_new = TRUE; |
|
882 } |
|
883 |
|
884 // The $divs element instructs #prefix whether to add an indent div or |
|
885 // close existing divs (a negative value). |
|
886 $comment->depth = count(explode('.', $comment->thread)) - 1; |
|
887 if ($comment->depth > $divs) { |
|
888 $comment->divs = 1; |
|
889 $divs++; |
|
890 } |
|
891 else { |
|
892 $comment->divs = $comment->depth - $divs; |
|
893 while ($comment->depth < $divs) { |
|
894 $divs--; |
|
895 } |
|
896 } |
|
897 $comments[$key] = $comment; |
|
898 } |
|
899 |
|
900 // The final comment must close up some hanging divs |
|
901 $comments[$key]->divs_final = $divs; |
|
902 } |
|
903 |
|
904 /** |
|
905 * Generate an array for rendering the given comment. |
|
906 * |
|
907 * @param $comment |
|
908 * A comment object. |
|
909 * @param $node |
|
910 * The node the comment is attached to. |
|
911 * @param $view_mode |
|
912 * View mode, e.g. 'full', 'teaser'... |
|
913 * @param $langcode |
|
914 * (optional) A language code to use for rendering. Defaults to the global |
|
915 * content language of the current request. |
|
916 * |
|
917 * @return |
|
918 * An array as expected by drupal_render(). |
|
919 */ |
|
920 function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) { |
|
921 if (!isset($langcode)) { |
|
922 $langcode = $GLOBALS['language_content']->language; |
|
923 } |
|
924 |
|
925 // Populate $comment->content with a render() array. |
|
926 comment_build_content($comment, $node, $view_mode, $langcode); |
|
927 |
|
928 $build = $comment->content; |
|
929 // We don't need duplicate rendering info in comment->content. |
|
930 unset($comment->content); |
|
931 |
|
932 $build += array( |
|
933 '#theme' => 'comment__node_' . $node->type, |
|
934 '#comment' => $comment, |
|
935 '#node' => $node, |
|
936 '#view_mode' => $view_mode, |
|
937 '#language' => $langcode, |
|
938 ); |
|
939 |
|
940 if (empty($comment->in_preview)) { |
|
941 $prefix = ''; |
|
942 $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED; |
|
943 |
|
944 // Add 'new' anchor if needed. |
|
945 if (!empty($comment->first_new)) { |
|
946 $prefix .= "<a id=\"new\"></a>\n"; |
|
947 } |
|
948 |
|
949 // Add indentation div or close open divs as needed. |
|
950 if ($is_threaded) { |
|
951 $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">'; |
|
952 } |
|
953 |
|
954 // Add anchor for each comment. |
|
955 $prefix .= "<a id=\"comment-$comment->cid\"></a>\n"; |
|
956 $build['#prefix'] = $prefix; |
|
957 |
|
958 // Close all open divs. |
|
959 if ($is_threaded && !empty($comment->divs_final)) { |
|
960 $build['#suffix'] = str_repeat('</div>', $comment->divs_final); |
|
961 } |
|
962 } |
|
963 |
|
964 // Allow modules to modify the structured comment. |
|
965 $type = 'comment'; |
|
966 drupal_alter(array('comment_view', 'entity_view'), $build, $type); |
|
967 |
|
968 return $build; |
|
969 } |
|
970 |
|
971 /** |
|
972 * Builds a structured array representing the comment's content. |
|
973 * |
|
974 * The content built for the comment (field values, comments, file attachments or |
|
975 * other comment components) will vary depending on the $view_mode parameter. |
|
976 * |
|
977 * @param $comment |
|
978 * A comment object. |
|
979 * @param $node |
|
980 * The node the comment is attached to. |
|
981 * @param $view_mode |
|
982 * View mode, e.g. 'full', 'teaser'... |
|
983 * @param $langcode |
|
984 * (optional) A language code to use for rendering. Defaults to the global |
|
985 * content language of the current request. |
|
986 */ |
|
987 function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) { |
|
988 if (!isset($langcode)) { |
|
989 $langcode = $GLOBALS['language_content']->language; |
|
990 } |
|
991 |
|
992 // Remove previously built content, if exists. |
|
993 $comment->content = array(); |
|
994 |
|
995 // Allow modules to change the view mode. |
|
996 $view_mode = key(entity_view_mode_prepare('comment', array($comment->cid => $comment), $view_mode, $langcode)); |
|
997 |
|
998 // Build fields content. |
|
999 field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode); |
|
1000 entity_prepare_view('comment', array($comment->cid => $comment), $langcode); |
|
1001 $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode); |
|
1002 |
|
1003 $comment->content['links'] = array( |
|
1004 '#theme' => 'links__comment', |
|
1005 '#pre_render' => array('drupal_pre_render_links'), |
|
1006 '#attributes' => array('class' => array('links', 'inline')), |
|
1007 ); |
|
1008 if (empty($comment->in_preview)) { |
|
1009 $comment->content['links']['comment'] = array( |
|
1010 '#theme' => 'links__comment__comment', |
|
1011 '#links' => comment_links($comment, $node), |
|
1012 '#attributes' => array('class' => array('links', 'inline')), |
|
1013 ); |
|
1014 } |
|
1015 |
|
1016 // Allow modules to make their own additions to the comment. |
|
1017 module_invoke_all('comment_view', $comment, $view_mode, $langcode); |
|
1018 module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode); |
|
1019 |
|
1020 // Make sure the current view mode is stored if no module has already |
|
1021 // populated the related key. |
|
1022 $comment->content += array('#view_mode' => $view_mode); |
|
1023 } |
|
1024 |
|
1025 /** |
|
1026 * Helper function, build links for an individual comment. |
|
1027 * |
|
1028 * Adds reply, edit, delete etc. depending on the current user permissions. |
|
1029 * |
|
1030 * @param $comment |
|
1031 * The comment object. |
|
1032 * @param $node |
|
1033 * The node the comment is attached to. |
|
1034 * @return |
|
1035 * A structured array of links. |
|
1036 */ |
|
1037 function comment_links($comment, $node) { |
|
1038 $links = array(); |
|
1039 if ($node->comment == COMMENT_NODE_OPEN) { |
|
1040 if (user_access('administer comments') && user_access('post comments')) { |
|
1041 $links['comment-delete'] = array( |
|
1042 'title' => t('delete'), |
|
1043 'href' => "comment/$comment->cid/delete", |
|
1044 'html' => TRUE, |
|
1045 ); |
|
1046 $links['comment-edit'] = array( |
|
1047 'title' => t('edit'), |
|
1048 'href' => "comment/$comment->cid/edit", |
|
1049 'html' => TRUE, |
|
1050 ); |
|
1051 $links['comment-reply'] = array( |
|
1052 'title' => t('reply'), |
|
1053 'href' => "comment/reply/$comment->nid/$comment->cid", |
|
1054 'html' => TRUE, |
|
1055 ); |
|
1056 if ($comment->status == COMMENT_NOT_PUBLISHED) { |
|
1057 $links['comment-approve'] = array( |
|
1058 'title' => t('approve'), |
|
1059 'href' => "comment/$comment->cid/approve", |
|
1060 'html' => TRUE, |
|
1061 'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")), |
|
1062 ); |
|
1063 } |
|
1064 } |
|
1065 elseif (user_access('post comments')) { |
|
1066 if (comment_access('edit', $comment)) { |
|
1067 $links['comment-edit'] = array( |
|
1068 'title' => t('edit'), |
|
1069 'href' => "comment/$comment->cid/edit", |
|
1070 'html' => TRUE, |
|
1071 ); |
|
1072 } |
|
1073 $links['comment-reply'] = array( |
|
1074 'title' => t('reply'), |
|
1075 'href' => "comment/reply/$comment->nid/$comment->cid", |
|
1076 'html' => TRUE, |
|
1077 ); |
|
1078 } |
|
1079 else { |
|
1080 $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node)); |
|
1081 $links['comment_forbidden']['html'] = TRUE; |
|
1082 } |
|
1083 } |
|
1084 return $links; |
|
1085 } |
|
1086 |
|
1087 /** |
|
1088 * Construct a drupal_render() style array from an array of loaded comments. |
|
1089 * |
|
1090 * @param $comments |
|
1091 * An array of comments as returned by comment_load_multiple(). |
|
1092 * @param $node |
|
1093 * The node the comments are attached to. |
|
1094 * @param $view_mode |
|
1095 * View mode, e.g. 'full', 'teaser'... |
|
1096 * @param $weight |
|
1097 * An integer representing the weight of the first comment in the list. |
|
1098 * @param $langcode |
|
1099 * A string indicating the language field values are to be shown in. If no |
|
1100 * language is provided the current content language is used. |
|
1101 * |
|
1102 * @return |
|
1103 * An array in the format expected by drupal_render(). |
|
1104 */ |
|
1105 function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) { |
|
1106 $build = array(); |
|
1107 $entities_by_view_mode = entity_view_mode_prepare('comment', $comments, $view_mode, $langcode); |
|
1108 foreach ($entities_by_view_mode as $entity_view_mode => $entities) { |
|
1109 field_attach_prepare_view('comment', $entities, $entity_view_mode, $langcode); |
|
1110 entity_prepare_view('comment', $entities, $langcode); |
|
1111 |
|
1112 foreach ($entities as $entity) { |
|
1113 $build[$entity->cid] = comment_view($entity, $node, $entity_view_mode, $langcode); |
|
1114 } |
|
1115 } |
|
1116 |
|
1117 foreach ($comments as $comment) { |
|
1118 $build[$comment->cid]['#weight'] = $weight; |
|
1119 $weight++; |
|
1120 } |
|
1121 // Sort here, to preserve the input order of the entities that were passed to |
|
1122 // this function. |
|
1123 uasort($build, 'element_sort'); |
|
1124 $build['#sorted'] = TRUE; |
|
1125 |
|
1126 return $build; |
|
1127 } |
|
1128 |
|
1129 /** |
|
1130 * Implements hook_form_FORM_ID_alter(). |
|
1131 */ |
|
1132 function comment_form_node_type_form_alter(&$form, $form_state) { |
|
1133 if (isset($form['type'])) { |
|
1134 $form['comment'] = array( |
|
1135 '#type' => 'fieldset', |
|
1136 '#title' => t('Comment settings'), |
|
1137 '#collapsible' => TRUE, |
|
1138 '#collapsed' => TRUE, |
|
1139 '#group' => 'additional_settings', |
|
1140 '#attributes' => array( |
|
1141 'class' => array('comment-node-type-settings-form'), |
|
1142 ), |
|
1143 '#attached' => array( |
|
1144 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'), |
|
1145 ), |
|
1146 ); |
|
1147 // Unlike coment_form_node_form_alter(), all of these settings are applied |
|
1148 // as defaults to all new nodes. Therefore, it would be wrong to use #states |
|
1149 // to hide the other settings based on the primary comment setting. |
|
1150 $form['comment']['comment'] = array( |
|
1151 '#type' => 'select', |
|
1152 '#title' => t('Default comment setting for new content'), |
|
1153 '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN), |
|
1154 '#options' => array( |
|
1155 COMMENT_NODE_OPEN => t('Open'), |
|
1156 COMMENT_NODE_CLOSED => t('Closed'), |
|
1157 COMMENT_NODE_HIDDEN => t('Hidden'), |
|
1158 ), |
|
1159 ); |
|
1160 $form['comment']['comment_default_mode'] = array( |
|
1161 '#type' => 'checkbox', |
|
1162 '#title' => t('Threading'), |
|
1163 '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED), |
|
1164 '#description' => t('Show comment replies in a threaded list.'), |
|
1165 ); |
|
1166 $form['comment']['comment_default_per_page'] = array( |
|
1167 '#type' => 'select', |
|
1168 '#title' => t('Comments per page'), |
|
1169 '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50), |
|
1170 '#options' => _comment_per_page(), |
|
1171 ); |
|
1172 $form['comment']['comment_anonymous'] = array( |
|
1173 '#type' => 'select', |
|
1174 '#title' => t('Anonymous commenting'), |
|
1175 '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT), |
|
1176 '#options' => array( |
|
1177 COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'), |
|
1178 COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'), |
|
1179 COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'), |
|
1180 ), |
|
1181 '#access' => user_access('post comments', drupal_anonymous_user()), |
|
1182 ); |
|
1183 $form['comment']['comment_subject_field'] = array( |
|
1184 '#type' => 'checkbox', |
|
1185 '#title' => t('Allow comment title'), |
|
1186 '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1), |
|
1187 ); |
|
1188 $form['comment']['comment_form_location'] = array( |
|
1189 '#type' => 'checkbox', |
|
1190 '#title' => t('Show reply form on the same page as comments'), |
|
1191 '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW), |
|
1192 ); |
|
1193 $form['comment']['comment_preview'] = array( |
|
1194 '#type' => 'radios', |
|
1195 '#title' => t('Preview comment'), |
|
1196 '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL), |
|
1197 '#options' => array( |
|
1198 DRUPAL_DISABLED => t('Disabled'), |
|
1199 DRUPAL_OPTIONAL => t('Optional'), |
|
1200 DRUPAL_REQUIRED => t('Required'), |
|
1201 ), |
|
1202 ); |
|
1203 } |
|
1204 } |
|
1205 |
|
1206 /** |
|
1207 * Implements hook_form_BASE_FORM_ID_alter(). |
|
1208 */ |
|
1209 function comment_form_node_form_alter(&$form, $form_state) { |
|
1210 $node = $form['#node']; |
|
1211 $form['comment_settings'] = array( |
|
1212 '#type' => 'fieldset', |
|
1213 '#access' => user_access('administer comments'), |
|
1214 '#title' => t('Comment settings'), |
|
1215 '#collapsible' => TRUE, |
|
1216 '#collapsed' => TRUE, |
|
1217 '#group' => 'additional_settings', |
|
1218 '#attributes' => array( |
|
1219 'class' => array('comment-node-settings-form'), |
|
1220 ), |
|
1221 '#attached' => array( |
|
1222 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'), |
|
1223 ), |
|
1224 '#weight' => 30, |
|
1225 ); |
|
1226 $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0; |
|
1227 $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment; |
|
1228 $form['comment_settings']['comment'] = array( |
|
1229 '#type' => 'radios', |
|
1230 '#title' => t('Comments'), |
|
1231 '#title_display' => 'invisible', |
|
1232 '#parents' => array('comment'), |
|
1233 '#default_value' => $comment_settings, |
|
1234 '#options' => array( |
|
1235 COMMENT_NODE_OPEN => t('Open'), |
|
1236 COMMENT_NODE_CLOSED => t('Closed'), |
|
1237 COMMENT_NODE_HIDDEN => t('Hidden'), |
|
1238 ), |
|
1239 COMMENT_NODE_OPEN => array( |
|
1240 '#description' => t('Users with the "Post comments" permission can post comments.'), |
|
1241 ), |
|
1242 COMMENT_NODE_CLOSED => array( |
|
1243 '#description' => t('Users cannot post comments, but existing comments will be displayed.'), |
|
1244 ), |
|
1245 COMMENT_NODE_HIDDEN => array( |
|
1246 '#description' => t('Comments are hidden from view.'), |
|
1247 ), |
|
1248 ); |
|
1249 // If the node doesn't have any comments, the "hidden" option makes no |
|
1250 // sense, so don't even bother presenting it to the user. |
|
1251 if (empty($comment_count)) { |
|
1252 $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE; |
|
1253 // Also adjust the description of the "closed" option. |
|
1254 $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.'); |
|
1255 } |
|
1256 } |
|
1257 |
|
1258 /** |
|
1259 * Implements hook_node_load(). |
|
1260 */ |
|
1261 function comment_node_load($nodes, $types) { |
|
1262 $comments_enabled = array(); |
|
1263 |
|
1264 // Check if comments are enabled for each node. If comments are disabled, |
|
1265 // assign values without hitting the database. |
|
1266 foreach ($nodes as $node) { |
|
1267 // Store whether comments are enabled for this node. |
|
1268 if ($node->comment != COMMENT_NODE_HIDDEN) { |
|
1269 $comments_enabled[] = $node->nid; |
|
1270 } |
|
1271 else { |
|
1272 $node->cid = 0; |
|
1273 $node->last_comment_timestamp = $node->created; |
|
1274 $node->last_comment_name = ''; |
|
1275 $node->last_comment_uid = $node->uid; |
|
1276 $node->comment_count = 0; |
|
1277 } |
|
1278 } |
|
1279 |
|
1280 // For nodes with comments enabled, fetch information from the database. |
|
1281 if (!empty($comments_enabled)) { |
|
1282 $result = db_query('SELECT nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count FROM {node_comment_statistics} WHERE nid IN (:comments_enabled)', array(':comments_enabled' => $comments_enabled)); |
|
1283 foreach ($result as $record) { |
|
1284 $nodes[$record->nid]->cid = $record->cid; |
|
1285 $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp; |
|
1286 $nodes[$record->nid]->last_comment_name = $record->last_comment_name; |
|
1287 $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid; |
|
1288 $nodes[$record->nid]->comment_count = $record->comment_count; |
|
1289 } |
|
1290 } |
|
1291 } |
|
1292 |
|
1293 /** |
|
1294 * Implements hook_node_prepare(). |
|
1295 */ |
|
1296 function comment_node_prepare($node) { |
|
1297 if (!isset($node->comment)) { |
|
1298 $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN); |
|
1299 } |
|
1300 } |
|
1301 |
|
1302 /** |
|
1303 * Implements hook_node_insert(). |
|
1304 */ |
|
1305 function comment_node_insert($node) { |
|
1306 // Allow bulk updates and inserts to temporarily disable the |
|
1307 // maintenance of the {node_comment_statistics} table. |
|
1308 if (variable_get('comment_maintain_node_statistics', TRUE)) { |
|
1309 db_insert('node_comment_statistics') |
|
1310 ->fields(array( |
|
1311 'nid' => $node->nid, |
|
1312 'cid' => 0, |
|
1313 'last_comment_timestamp' => $node->changed, |
|
1314 'last_comment_name' => NULL, |
|
1315 'last_comment_uid' => $node->uid, |
|
1316 'comment_count' => 0, |
|
1317 )) |
|
1318 ->execute(); |
|
1319 } |
|
1320 } |
|
1321 |
|
1322 /** |
|
1323 * Implements hook_node_delete(). |
|
1324 */ |
|
1325 function comment_node_delete($node) { |
|
1326 $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol(); |
|
1327 comment_delete_multiple($cids); |
|
1328 db_delete('node_comment_statistics') |
|
1329 ->condition('nid', $node->nid) |
|
1330 ->execute(); |
|
1331 } |
|
1332 |
|
1333 /** |
|
1334 * Implements hook_node_update_index(). |
|
1335 */ |
|
1336 function comment_node_update_index($node) { |
|
1337 $index_comments = &drupal_static(__FUNCTION__); |
|
1338 |
|
1339 if ($index_comments === NULL) { |
|
1340 // Find and save roles that can 'access comments' or 'search content'. |
|
1341 $perms = array('access comments' => array(), 'search content' => array()); |
|
1342 $result = db_query("SELECT rid, permission FROM {role_permission} WHERE permission IN ('access comments', 'search content')"); |
|
1343 foreach ($result as $record) { |
|
1344 $perms[$record->permission][$record->rid] = $record->rid; |
|
1345 } |
|
1346 |
|
1347 // Prevent indexing of comments if there are any roles that can search but |
|
1348 // not view comments. |
|
1349 $index_comments = TRUE; |
|
1350 foreach ($perms['search content'] as $rid) { |
|
1351 if (!isset($perms['access comments'][$rid]) && ($rid <= DRUPAL_AUTHENTICATED_RID || !isset($perms['access comments'][DRUPAL_AUTHENTICATED_RID]))) { |
|
1352 $index_comments = FALSE; |
|
1353 break; |
|
1354 } |
|
1355 } |
|
1356 } |
|
1357 |
|
1358 if ($index_comments) { |
|
1359 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED); |
|
1360 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50); |
|
1361 if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) { |
|
1362 $comments = comment_load_multiple($cids); |
|
1363 comment_prepare_thread($comments); |
|
1364 $build = comment_view_multiple($comments, $node); |
|
1365 return drupal_render($build); |
|
1366 } |
|
1367 } |
|
1368 return ''; |
|
1369 } |
|
1370 |
|
1371 /** |
|
1372 * Implements hook_update_index(). |
|
1373 */ |
|
1374 function comment_update_index() { |
|
1375 // Store the maximum possible comments per thread (used for ranking by reply count) |
|
1376 variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField())); |
|
1377 } |
|
1378 |
|
1379 /** |
|
1380 * Implements hook_node_search_result(). |
|
1381 * |
|
1382 * Formats a comment count string and returns it, for display with search |
|
1383 * results. |
|
1384 */ |
|
1385 function comment_node_search_result($node) { |
|
1386 // Do not make a string if comments are hidden. |
|
1387 if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) { |
|
1388 $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField(); |
|
1389 // Do not make a string if comments are closed and there are currently |
|
1390 // zero comments. |
|
1391 if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) { |
|
1392 return array('comment' => format_plural($comments, '1 comment', '@count comments')); |
|
1393 } |
|
1394 } |
|
1395 } |
|
1396 |
|
1397 /** |
|
1398 * Implements hook_user_cancel(). |
|
1399 */ |
|
1400 function comment_user_cancel($edit, $account, $method) { |
|
1401 switch ($method) { |
|
1402 case 'user_cancel_block_unpublish': |
|
1403 $comments = comment_load_multiple(array(), array('uid' => $account->uid)); |
|
1404 foreach ($comments as $comment) { |
|
1405 $comment->status = 0; |
|
1406 comment_save($comment); |
|
1407 } |
|
1408 break; |
|
1409 |
|
1410 case 'user_cancel_reassign': |
|
1411 $comments = comment_load_multiple(array(), array('uid' => $account->uid)); |
|
1412 foreach ($comments as $comment) { |
|
1413 $comment->uid = 0; |
|
1414 comment_save($comment); |
|
1415 } |
|
1416 break; |
|
1417 } |
|
1418 } |
|
1419 |
|
1420 /** |
|
1421 * Implements hook_user_delete(). |
|
1422 */ |
|
1423 function comment_user_delete($account) { |
|
1424 $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol(); |
|
1425 comment_delete_multiple($cids); |
|
1426 } |
|
1427 |
|
1428 /** |
|
1429 * Determines whether the current user has access to a particular comment. |
|
1430 * |
|
1431 * Authenticated users can edit their comments as long they have not been |
|
1432 * replied to. This prevents people from changing or revising their statements |
|
1433 * based on the replies to their posts. |
|
1434 * |
|
1435 * @param $op |
|
1436 * The operation that is to be performed on the comment. Only 'edit' is |
|
1437 * recognized now. |
|
1438 * @param $comment |
|
1439 * The comment object. |
|
1440 * @return |
|
1441 * TRUE if the current user has acces to the comment, FALSE otherwise. |
|
1442 */ |
|
1443 function comment_access($op, $comment) { |
|
1444 global $user; |
|
1445 |
|
1446 if ($op == 'edit') { |
|
1447 return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments'); |
|
1448 } |
|
1449 } |
|
1450 |
|
1451 /** |
|
1452 * Accepts a submission of new or changed comment content. |
|
1453 * |
|
1454 * @param $comment |
|
1455 * A comment object. |
|
1456 */ |
|
1457 function comment_save($comment) { |
|
1458 global $user; |
|
1459 |
|
1460 $transaction = db_transaction(); |
|
1461 try { |
|
1462 $defaults = array( |
|
1463 'mail' => '', |
|
1464 'homepage' => '', |
|
1465 'name' => '', |
|
1466 'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, |
|
1467 ); |
|
1468 foreach ($defaults as $key => $default) { |
|
1469 if (!isset($comment->$key)) { |
|
1470 $comment->$key = $default; |
|
1471 } |
|
1472 } |
|
1473 // Make sure we have a bundle name. |
|
1474 if (!isset($comment->node_type)) { |
|
1475 $node = node_load($comment->nid); |
|
1476 $comment->node_type = 'comment_node_' . $node->type; |
|
1477 } |
|
1478 |
|
1479 // Load the stored entity, if any. |
|
1480 if (!empty($comment->cid) && !isset($comment->original)) { |
|
1481 $comment->original = entity_load_unchanged('comment', $comment->cid); |
|
1482 } |
|
1483 |
|
1484 field_attach_presave('comment', $comment); |
|
1485 |
|
1486 // Allow modules to alter the comment before saving. |
|
1487 module_invoke_all('comment_presave', $comment); |
|
1488 module_invoke_all('entity_presave', $comment, 'comment'); |
|
1489 |
|
1490 if ($comment->cid) { |
|
1491 |
|
1492 drupal_write_record('comment', $comment, 'cid'); |
|
1493 |
|
1494 // Ignore slave server temporarily to give time for the |
|
1495 // saved comment to be propagated to the slave. |
|
1496 db_ignore_slave(); |
|
1497 |
|
1498 // Update the {node_comment_statistics} table prior to executing hooks. |
|
1499 _comment_update_node_statistics($comment->nid); |
|
1500 |
|
1501 field_attach_update('comment', $comment); |
|
1502 // Allow modules to respond to the updating of a comment. |
|
1503 module_invoke_all('comment_update', $comment); |
|
1504 module_invoke_all('entity_update', $comment, 'comment'); |
|
1505 } |
|
1506 else { |
|
1507 // Add the comment to database. This next section builds the thread field. |
|
1508 // Also see the documentation for comment_view(). |
|
1509 if (!empty($comment->thread)) { |
|
1510 // Allow calling code to set thread itself. |
|
1511 $thread = $comment->thread; |
|
1512 } |
|
1513 elseif ($comment->pid == 0) { |
|
1514 // This is a comment with no parent comment (depth 0): we start |
|
1515 // by retrieving the maximum thread level. |
|
1516 $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); |
|
1517 // Strip the "/" from the end of the thread. |
|
1518 $max = rtrim($max, '/'); |
|
1519 // We need to get the value at the correct depth. |
|
1520 $parts = explode('.', $max); |
|
1521 $firstsegment = $parts[0]; |
|
1522 // Finally, build the thread field for this new comment. |
|
1523 $thread = int2vancode(vancode2int($firstsegment) + 1) . '/'; |
|
1524 } |
|
1525 else { |
|
1526 // This is a comment with a parent comment, so increase the part of the |
|
1527 // thread value at the proper depth. |
|
1528 |
|
1529 // Get the parent comment: |
|
1530 $parent = comment_load($comment->pid); |
|
1531 // Strip the "/" from the end of the parent thread. |
|
1532 $parent->thread = (string) rtrim((string) $parent->thread, '/'); |
|
1533 // Get the max value in *this* thread. |
|
1534 $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( |
|
1535 ':thread' => $parent->thread . '.%', |
|
1536 ':nid' => $comment->nid, |
|
1537 ))->fetchField(); |
|
1538 |
|
1539 if ($max == '') { |
|
1540 // First child of this parent. |
|
1541 $thread = $parent->thread . '.' . int2vancode(0) . '/'; |
|
1542 } |
|
1543 else { |
|
1544 // Strip the "/" at the end of the thread. |
|
1545 $max = rtrim($max, '/'); |
|
1546 // Get the value at the correct depth. |
|
1547 $parts = explode('.', $max); |
|
1548 $parent_depth = count(explode('.', $parent->thread)); |
|
1549 $last = $parts[$parent_depth]; |
|
1550 // Finally, build the thread field for this new comment. |
|
1551 $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/'; |
|
1552 } |
|
1553 } |
|
1554 |
|
1555 if (empty($comment->created)) { |
|
1556 $comment->created = REQUEST_TIME; |
|
1557 } |
|
1558 |
|
1559 if (empty($comment->changed)) { |
|
1560 $comment->changed = $comment->created; |
|
1561 } |
|
1562 |
|
1563 if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. |
|
1564 $comment->name = $user->name; |
|
1565 } |
|
1566 |
|
1567 // Ensure the parent id (pid) has a value set. |
|
1568 if (empty($comment->pid)) { |
|
1569 $comment->pid = 0; |
|
1570 } |
|
1571 |
|
1572 // Add the values which aren't passed into the function. |
|
1573 $comment->thread = $thread; |
|
1574 $comment->hostname = ip_address(); |
|
1575 |
|
1576 drupal_write_record('comment', $comment); |
|
1577 |
|
1578 // Ignore slave server temporarily to give time for the |
|
1579 // created comment to be propagated to the slave. |
|
1580 db_ignore_slave(); |
|
1581 |
|
1582 // Update the {node_comment_statistics} table prior to executing hooks. |
|
1583 _comment_update_node_statistics($comment->nid); |
|
1584 |
|
1585 field_attach_insert('comment', $comment); |
|
1586 |
|
1587 // Tell the other modules a new comment has been submitted. |
|
1588 module_invoke_all('comment_insert', $comment); |
|
1589 module_invoke_all('entity_insert', $comment, 'comment'); |
|
1590 } |
|
1591 if ($comment->status == COMMENT_PUBLISHED) { |
|
1592 module_invoke_all('comment_publish', $comment); |
|
1593 } |
|
1594 unset($comment->original); |
|
1595 } |
|
1596 catch (Exception $e) { |
|
1597 $transaction->rollback('comment'); |
|
1598 watchdog_exception('comment', $e); |
|
1599 throw $e; |
|
1600 } |
|
1601 |
|
1602 } |
|
1603 |
|
1604 /** |
|
1605 * Delete a comment and all its replies. |
|
1606 * |
|
1607 * @param $cid |
|
1608 * The comment to delete. |
|
1609 */ |
|
1610 function comment_delete($cid) { |
|
1611 comment_delete_multiple(array($cid)); |
|
1612 } |
|
1613 |
|
1614 /** |
|
1615 * Delete comments and all their replies. |
|
1616 * |
|
1617 * @param $cids |
|
1618 * The comment to delete. |
|
1619 */ |
|
1620 function comment_delete_multiple($cids) { |
|
1621 $comments = comment_load_multiple($cids); |
|
1622 if ($comments) { |
|
1623 $transaction = db_transaction(); |
|
1624 try { |
|
1625 // Delete the comments. |
|
1626 db_delete('comment') |
|
1627 ->condition('cid', array_keys($comments), 'IN') |
|
1628 ->execute(); |
|
1629 foreach ($comments as $comment) { |
|
1630 field_attach_delete('comment', $comment); |
|
1631 module_invoke_all('comment_delete', $comment); |
|
1632 module_invoke_all('entity_delete', $comment, 'comment'); |
|
1633 |
|
1634 // Delete the comment's replies. |
|
1635 $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol(); |
|
1636 comment_delete_multiple($child_cids); |
|
1637 _comment_update_node_statistics($comment->nid); |
|
1638 } |
|
1639 } |
|
1640 catch (Exception $e) { |
|
1641 $transaction->rollback(); |
|
1642 watchdog_exception('comment', $e); |
|
1643 throw $e; |
|
1644 } |
|
1645 } |
|
1646 } |
|
1647 |
|
1648 /** |
|
1649 * Load comments from the database. |
|
1650 * |
|
1651 * @param $cids |
|
1652 * An array of comment IDs. |
|
1653 * @param $conditions |
|
1654 * (deprecated) An associative array of conditions on the {comments} |
|
1655 * table, where the keys are the database fields and the values are the |
|
1656 * values those fields must have. Instead, it is preferable to use |
|
1657 * EntityFieldQuery to retrieve a list of entity IDs loadable by |
|
1658 * this function. |
|
1659 * @param $reset |
|
1660 * Whether to reset the internal static entity cache. Note that the static |
|
1661 * cache is disabled in comment_entity_info() by default. |
|
1662 * |
|
1663 * @return |
|
1664 * An array of comment objects, indexed by comment ID. |
|
1665 * |
|
1666 * @see entity_load() |
|
1667 * @see EntityFieldQuery |
|
1668 * |
|
1669 * @todo Remove $conditions in Drupal 8. |
|
1670 */ |
|
1671 function comment_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) { |
|
1672 return entity_load('comment', $cids, $conditions, $reset); |
|
1673 } |
|
1674 |
|
1675 /** |
|
1676 * Load the entire comment by cid. |
|
1677 * |
|
1678 * @param $cid |
|
1679 * The identifying comment id. |
|
1680 * @param $reset |
|
1681 * Whether to reset the internal static entity cache. Note that the static |
|
1682 * cache is disabled in comment_entity_info() by default. |
|
1683 * |
|
1684 * @return |
|
1685 * The comment object. |
|
1686 */ |
|
1687 function comment_load($cid, $reset = FALSE) { |
|
1688 $comment = comment_load_multiple(array($cid), array(), $reset); |
|
1689 return $comment ? $comment[$cid] : FALSE; |
|
1690 } |
|
1691 |
|
1692 /** |
|
1693 * Controller class for comments. |
|
1694 * |
|
1695 * This extends the DrupalDefaultEntityController class, adding required |
|
1696 * special handling for comment objects. |
|
1697 */ |
|
1698 class CommentController extends DrupalDefaultEntityController { |
|
1699 |
|
1700 protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { |
|
1701 $query = parent::buildQuery($ids, $conditions, $revision_id); |
|
1702 // Specify additional fields from the user and node tables. |
|
1703 $query->innerJoin('node', 'n', 'base.nid = n.nid'); |
|
1704 $query->addField('n', 'type', 'node_type'); |
|
1705 $query->innerJoin('users', 'u', 'base.uid = u.uid'); |
|
1706 $query->addField('u', 'name', 'registered_name'); |
|
1707 $query->fields('u', array('uid', 'signature', 'signature_format', 'picture')); |
|
1708 return $query; |
|
1709 } |
|
1710 |
|
1711 protected function attachLoad(&$comments, $revision_id = FALSE) { |
|
1712 // Setup standard comment properties. |
|
1713 foreach ($comments as $key => $comment) { |
|
1714 $comment->name = $comment->uid ? $comment->registered_name : $comment->name; |
|
1715 $comment->new = node_mark($comment->nid, $comment->changed); |
|
1716 $comment->node_type = 'comment_node_' . $comment->node_type; |
|
1717 $comments[$key] = $comment; |
|
1718 } |
|
1719 parent::attachLoad($comments, $revision_id); |
|
1720 } |
|
1721 } |
|
1722 |
|
1723 /** |
|
1724 * Get number of new comments for current user and specified node. |
|
1725 * |
|
1726 * @param $nid |
|
1727 * Node-id to count comments for. |
|
1728 * @param $timestamp |
|
1729 * Time to count from (defaults to time of last user access |
|
1730 * to node). |
|
1731 * @return The result or FALSE on error. |
|
1732 */ |
|
1733 function comment_num_new($nid, $timestamp = 0) { |
|
1734 global $user; |
|
1735 |
|
1736 if ($user->uid) { |
|
1737 // Retrieve the timestamp at which the current user last viewed this node. |
|
1738 if (!$timestamp) { |
|
1739 $timestamp = node_last_viewed($nid); |
|
1740 } |
|
1741 $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT); |
|
1742 |
|
1743 // Use the timestamp to retrieve the number of new comments. |
|
1744 return db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND created > :timestamp AND status = :status', array( |
|
1745 ':nid' => $nid, |
|
1746 ':timestamp' => $timestamp, |
|
1747 ':status' => COMMENT_PUBLISHED, |
|
1748 ))->fetchField(); |
|
1749 } |
|
1750 else { |
|
1751 return FALSE; |
|
1752 } |
|
1753 |
|
1754 } |
|
1755 |
|
1756 /** |
|
1757 * Get the display ordinal for a comment, starting from 0. |
|
1758 * |
|
1759 * Count the number of comments which appear before the comment we want to |
|
1760 * display, taking into account display settings and threading. |
|
1761 * |
|
1762 * @param $cid |
|
1763 * The comment ID. |
|
1764 * @param $node_type |
|
1765 * The node type of the comment's parent. |
|
1766 * @return |
|
1767 * The display ordinal for the comment. |
|
1768 * @see comment_get_display_page() |
|
1769 */ |
|
1770 function comment_get_display_ordinal($cid, $node_type) { |
|
1771 // Count how many comments (c1) are before $cid (c2) in display order. This is |
|
1772 // the 0-based display ordinal. |
|
1773 $query = db_select('comment', 'c1'); |
|
1774 $query->innerJoin('comment', 'c2', 'c2.nid = c1.nid'); |
|
1775 $query->addExpression('COUNT(*)', 'count'); |
|
1776 $query->condition('c2.cid', $cid); |
|
1777 if (!user_access('administer comments')) { |
|
1778 $query->condition('c1.status', COMMENT_PUBLISHED); |
|
1779 } |
|
1780 $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED); |
|
1781 |
|
1782 if ($mode == COMMENT_MODE_FLAT) { |
|
1783 // For flat comments, cid is used for ordering comments due to |
|
1784 // unpredicatable behavior with timestamp, so we make the same assumption |
|
1785 // here. |
|
1786 $query->condition('c1.cid', $cid, '<'); |
|
1787 } |
|
1788 else { |
|
1789 // For threaded comments, the c.thread column is used for ordering. We can |
|
1790 // use the vancode for comparison, but must remove the trailing slash. |
|
1791 // See comment_view_multiple(). |
|
1792 $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))'); |
|
1793 } |
|
1794 |
|
1795 return $query->execute()->fetchField(); |
|
1796 } |
|
1797 |
|
1798 /** |
|
1799 * Return the page number for a comment. |
|
1800 * |
|
1801 * Finds the correct page number for a comment taking into account display |
|
1802 * and paging settings. |
|
1803 * |
|
1804 * @param $cid |
|
1805 * The comment ID. |
|
1806 * @param $node_type |
|
1807 * The node type the comment is attached to. |
|
1808 * @return |
|
1809 * The page number. |
|
1810 */ |
|
1811 function comment_get_display_page($cid, $node_type) { |
|
1812 $ordinal = comment_get_display_ordinal($cid, $node_type); |
|
1813 $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50); |
|
1814 return floor($ordinal / $comments_per_page); |
|
1815 } |
|
1816 |
|
1817 /** |
|
1818 * Page callback for comment editing. |
|
1819 */ |
|
1820 function comment_edit_page($comment) { |
|
1821 drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH); |
|
1822 $node = node_load($comment->nid); |
|
1823 return drupal_get_form("comment_node_{$node->type}_form", $comment); |
|
1824 } |
|
1825 |
|
1826 /** |
|
1827 * Implements hook_forms(). |
|
1828 */ |
|
1829 function comment_forms() { |
|
1830 $forms = array(); |
|
1831 foreach (node_type_get_types() as $type) { |
|
1832 $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form'; |
|
1833 } |
|
1834 return $forms; |
|
1835 } |
|
1836 |
|
1837 /** |
|
1838 * Generate the basic commenting form, for appending to a node or display on a separate page. |
|
1839 * |
|
1840 * @see comment_form_validate() |
|
1841 * @see comment_form_submit() |
|
1842 * |
|
1843 * @ingroup forms |
|
1844 */ |
|
1845 function comment_form($form, &$form_state, $comment) { |
|
1846 global $user; |
|
1847 |
|
1848 // During initial form build, add the comment entity to the form state for |
|
1849 // use during form building and processing. During a rebuild, use what is in |
|
1850 // the form state. |
|
1851 if (!isset($form_state['comment'])) { |
|
1852 $defaults = array( |
|
1853 'name' => '', |
|
1854 'mail' => '', |
|
1855 'homepage' => '', |
|
1856 'subject' => '', |
|
1857 'comment' => '', |
|
1858 'cid' => NULL, |
|
1859 'pid' => NULL, |
|
1860 'language' => LANGUAGE_NONE, |
|
1861 'uid' => 0, |
|
1862 ); |
|
1863 foreach ($defaults as $key => $value) { |
|
1864 if (!isset($comment->$key)) { |
|
1865 $comment->$key = $value; |
|
1866 } |
|
1867 } |
|
1868 $form_state['comment'] = $comment; |
|
1869 } |
|
1870 else { |
|
1871 $comment = $form_state['comment']; |
|
1872 } |
|
1873 |
|
1874 $node = node_load($comment->nid); |
|
1875 $form['#node'] = $node; |
|
1876 |
|
1877 // Use #comment-form as unique jump target, regardless of node type. |
|
1878 $form['#id'] = drupal_html_id('comment_form'); |
|
1879 $form['#attributes']['class'][] = 'comment-form'; |
|
1880 $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form'); |
|
1881 |
|
1882 $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT); |
|
1883 $is_admin = (!empty($comment->cid) && user_access('administer comments')); |
|
1884 |
|
1885 if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) { |
|
1886 $form['#attached']['library'][] = array('system', 'jquery.cookie'); |
|
1887 $form['#attributes']['class'][] = 'user-info-from-cookie'; |
|
1888 } |
|
1889 |
|
1890 // If not replying to a comment, use our dedicated page callback for new |
|
1891 // comments on nodes. |
|
1892 if (empty($comment->cid) && empty($comment->pid)) { |
|
1893 $form['#action'] = url('comment/reply/' . $comment->nid); |
|
1894 } |
|
1895 |
|
1896 if (isset($form_state['comment_preview'])) { |
|
1897 $form += $form_state['comment_preview']; |
|
1898 } |
|
1899 |
|
1900 // Display author information in a fieldset for comment moderators. |
|
1901 if ($is_admin) { |
|
1902 $form['author'] = array( |
|
1903 '#type' => 'fieldset', |
|
1904 '#title' => t('Administration'), |
|
1905 '#collapsible' => TRUE, |
|
1906 '#collapsed' => TRUE, |
|
1907 '#weight' => -2, |
|
1908 ); |
|
1909 } |
|
1910 else { |
|
1911 // Sets the author form elements above the subject. |
|
1912 $form['author'] = array( |
|
1913 '#weight' => -2, |
|
1914 ); |
|
1915 } |
|
1916 |
|
1917 // Prepare default values for form elements. |
|
1918 if ($is_admin) { |
|
1919 $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name); |
|
1920 $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED); |
|
1921 $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O')); |
|
1922 } |
|
1923 else { |
|
1924 if ($user->uid) { |
|
1925 $author = $user->name; |
|
1926 } |
|
1927 else { |
|
1928 $author = ($comment->name ? $comment->name : ''); |
|
1929 } |
|
1930 $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED); |
|
1931 $date = ''; |
|
1932 } |
|
1933 |
|
1934 // Add the author name field depending on the current user. |
|
1935 if ($is_admin) { |
|
1936 $form['author']['name'] = array( |
|
1937 '#type' => 'textfield', |
|
1938 '#title' => t('Authored by'), |
|
1939 '#default_value' => $author, |
|
1940 '#maxlength' => 60, |
|
1941 '#size' => 30, |
|
1942 '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), |
|
1943 '#autocomplete_path' => 'user/autocomplete', |
|
1944 ); |
|
1945 } |
|
1946 elseif ($user->uid) { |
|
1947 $form['author']['_author'] = array( |
|
1948 '#type' => 'item', |
|
1949 '#title' => t('Your name'), |
|
1950 '#markup' => theme('username', array('account' => $user)), |
|
1951 ); |
|
1952 $form['author']['name'] = array( |
|
1953 '#type' => 'value', |
|
1954 '#value' => $author, |
|
1955 ); |
|
1956 } |
|
1957 else { |
|
1958 $form['author']['name'] = array( |
|
1959 '#type' => 'textfield', |
|
1960 '#title' => t('Your name'), |
|
1961 '#default_value' => $author, |
|
1962 '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), |
|
1963 '#maxlength' => 60, |
|
1964 '#size' => 30, |
|
1965 ); |
|
1966 } |
|
1967 |
|
1968 // Add author e-mail and homepage fields depending on the current user. |
|
1969 $form['author']['mail'] = array( |
|
1970 '#type' => 'textfield', |
|
1971 '#title' => t('E-mail'), |
|
1972 '#default_value' => $comment->mail, |
|
1973 '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), |
|
1974 '#maxlength' => 64, |
|
1975 '#size' => 30, |
|
1976 '#description' => t('The content of this field is kept private and will not be shown publicly.'), |
|
1977 '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), |
|
1978 ); |
|
1979 $form['author']['homepage'] = array( |
|
1980 '#type' => 'textfield', |
|
1981 '#title' => t('Homepage'), |
|
1982 '#default_value' => $comment->homepage, |
|
1983 '#maxlength' => 255, |
|
1984 '#size' => 30, |
|
1985 '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), |
|
1986 ); |
|
1987 |
|
1988 // Add administrative comment publishing options. |
|
1989 $form['author']['date'] = array( |
|
1990 '#type' => 'textfield', |
|
1991 '#title' => t('Authored on'), |
|
1992 '#default_value' => $date, |
|
1993 '#maxlength' => 25, |
|
1994 '#size' => 20, |
|
1995 '#access' => $is_admin, |
|
1996 ); |
|
1997 $form['author']['status'] = array( |
|
1998 '#type' => 'radios', |
|
1999 '#title' => t('Status'), |
|
2000 '#default_value' => $status, |
|
2001 '#options' => array( |
|
2002 COMMENT_PUBLISHED => t('Published'), |
|
2003 COMMENT_NOT_PUBLISHED => t('Not published'), |
|
2004 ), |
|
2005 '#access' => $is_admin, |
|
2006 ); |
|
2007 |
|
2008 $form['subject'] = array( |
|
2009 '#type' => 'textfield', |
|
2010 '#title' => t('Subject'), |
|
2011 '#maxlength' => 64, |
|
2012 '#default_value' => $comment->subject, |
|
2013 '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1, |
|
2014 '#weight' => -1, |
|
2015 ); |
|
2016 |
|
2017 // Used for conditional validation of author fields. |
|
2018 $form['is_anonymous'] = array( |
|
2019 '#type' => 'value', |
|
2020 '#value' => ($comment->cid ? !$comment->uid : !$user->uid), |
|
2021 ); |
|
2022 |
|
2023 // Add internal comment properties. |
|
2024 foreach (array('cid', 'pid', 'nid', 'language', 'uid') as $key) { |
|
2025 $form[$key] = array('#type' => 'value', '#value' => $comment->$key); |
|
2026 } |
|
2027 $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type); |
|
2028 |
|
2029 // Only show the save button if comment previews are optional or if we are |
|
2030 // already previewing the submission. |
|
2031 $form['actions'] = array('#type' => 'actions'); |
|
2032 $form['actions']['submit'] = array( |
|
2033 '#type' => 'submit', |
|
2034 '#value' => t('Save'), |
|
2035 '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']), |
|
2036 '#weight' => 19, |
|
2037 ); |
|
2038 $form['actions']['preview'] = array( |
|
2039 '#type' => 'submit', |
|
2040 '#value' => t('Preview'), |
|
2041 '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED), |
|
2042 '#weight' => 20, |
|
2043 '#submit' => array('comment_form_build_preview'), |
|
2044 ); |
|
2045 |
|
2046 // Attach fields. |
|
2047 $comment->node_type = 'comment_node_' . $node->type; |
|
2048 $langcode = entity_language('comment', $comment); |
|
2049 field_attach_form('comment', $comment, $form, $form_state, $langcode); |
|
2050 |
|
2051 return $form; |
|
2052 } |
|
2053 |
|
2054 /** |
|
2055 * Build a preview from submitted form values. |
|
2056 */ |
|
2057 function comment_form_build_preview($form, &$form_state) { |
|
2058 $comment = comment_form_submit_build_comment($form, $form_state); |
|
2059 $form_state['comment_preview'] = comment_preview($comment); |
|
2060 $form_state['rebuild'] = TRUE; |
|
2061 } |
|
2062 |
|
2063 /** |
|
2064 * Generate a comment preview. |
|
2065 */ |
|
2066 function comment_preview($comment) { |
|
2067 global $user; |
|
2068 |
|
2069 drupal_set_title(t('Preview comment'), PASS_THROUGH); |
|
2070 |
|
2071 $node = node_load($comment->nid); |
|
2072 |
|
2073 if (!form_get_errors()) { |
|
2074 $comment_body = field_get_items('comment', $comment, 'comment_body'); |
|
2075 $comment->format = $comment_body[0]['format']; |
|
2076 // Attach the user and time information. |
|
2077 if (!empty($comment->name)) { |
|
2078 $account = user_load_by_name($comment->name); |
|
2079 } |
|
2080 elseif ($user->uid && empty($comment->is_anonymous)) { |
|
2081 $account = $user; |
|
2082 } |
|
2083 |
|
2084 if (!empty($account->uid)) { |
|
2085 $comment->uid = $account->uid; |
|
2086 $comment->name = check_plain($account->name); |
|
2087 $comment->signature = $account->signature; |
|
2088 $comment->signature_format = $account->signature_format; |
|
2089 $comment->picture = $account->picture; |
|
2090 } |
|
2091 elseif (empty($comment->name)) { |
|
2092 $comment->name = variable_get('anonymous', t('Anonymous')); |
|
2093 } |
|
2094 |
|
2095 $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME; |
|
2096 $comment->changed = REQUEST_TIME; |
|
2097 $comment->in_preview = TRUE; |
|
2098 $comment_build = comment_view($comment, $node); |
|
2099 $comment_build['#weight'] = -100; |
|
2100 |
|
2101 $form['comment_preview'] = $comment_build; |
|
2102 } |
|
2103 |
|
2104 if ($comment->pid) { |
|
2105 $build = array(); |
|
2106 if ($comments = comment_load_multiple(array($comment->pid), array('status' => COMMENT_PUBLISHED))) { |
|
2107 $parent_comment = $comments[$comment->pid]; |
|
2108 $build = comment_view($parent_comment, $node); |
|
2109 } |
|
2110 } |
|
2111 else { |
|
2112 $build = node_view($node); |
|
2113 } |
|
2114 |
|
2115 $form['comment_output_below'] = $build; |
|
2116 $form['comment_output_below']['#weight'] = 100; |
|
2117 |
|
2118 return $form; |
|
2119 } |
|
2120 |
|
2121 /** |
|
2122 * Validate comment form submissions. |
|
2123 */ |
|
2124 function comment_form_validate($form, &$form_state) { |
|
2125 global $user; |
|
2126 |
|
2127 entity_form_field_validate('comment', $form, $form_state); |
|
2128 |
|
2129 if (!empty($form_state['values']['cid'])) { |
|
2130 // Verify the name in case it is being changed from being anonymous. |
|
2131 $account = user_load_by_name($form_state['values']['name']); |
|
2132 $form_state['values']['uid'] = $account ? $account->uid : 0; |
|
2133 |
|
2134 if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) { |
|
2135 form_set_error('date', t('You have to specify a valid date.')); |
|
2136 } |
|
2137 if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) { |
|
2138 form_set_error('name', t('You have to specify a valid author.')); |
|
2139 } |
|
2140 } |
|
2141 elseif ($form_state['values']['is_anonymous']) { |
|
2142 // Validate anonymous comment author fields (if given). If the (original) |
|
2143 // author of this comment was an anonymous user, verify that no registered |
|
2144 // user with this name exists. |
|
2145 if ($form_state['values']['name']) { |
|
2146 $query = db_select('users', 'u'); |
|
2147 $query->addField('u', 'uid', 'uid'); |
|
2148 $taken = $query |
|
2149 ->condition('name', db_like($form_state['values']['name']), 'LIKE') |
|
2150 ->countQuery() |
|
2151 ->execute() |
|
2152 ->fetchField(); |
|
2153 if ($taken) { |
|
2154 form_set_error('name', t('The name you used belongs to a registered user.')); |
|
2155 } |
|
2156 } |
|
2157 } |
|
2158 if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) { |
|
2159 form_set_error('mail', t('The e-mail address you specified is not valid.')); |
|
2160 } |
|
2161 if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) { |
|
2162 form_set_error('homepage', t('The URL of your homepage is not valid. Remember that it must be fully qualified, i.e. of the form <code>http://example.com/directory</code>.')); |
|
2163 } |
|
2164 } |
|
2165 |
|
2166 /** |
|
2167 * Prepare a comment for submission. |
|
2168 */ |
|
2169 function comment_submit($comment) { |
|
2170 // @todo Legacy support. Remove in Drupal 8. |
|
2171 if (is_array($comment)) { |
|
2172 $comment += array('subject' => ''); |
|
2173 $comment = (object) $comment; |
|
2174 } |
|
2175 |
|
2176 if (empty($comment->date)) { |
|
2177 $comment->date = 'now'; |
|
2178 } |
|
2179 $comment->created = strtotime($comment->date); |
|
2180 $comment->changed = REQUEST_TIME; |
|
2181 |
|
2182 // If the comment was posted by a registered user, assign the author's ID. |
|
2183 // @todo Too fragile. Should be prepared and stored in comment_form() already. |
|
2184 if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) { |
|
2185 $comment->uid = $account->uid; |
|
2186 } |
|
2187 // If the comment was posted by an anonymous user and no author name was |
|
2188 // required, use "Anonymous" by default. |
|
2189 if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) { |
|
2190 $comment->name = variable_get('anonymous', t('Anonymous')); |
|
2191 } |
|
2192 |
|
2193 // Validate the comment's subject. If not specified, extract from comment body. |
|
2194 if (trim($comment->subject) == '') { |
|
2195 // The body may be in any format, so: |
|
2196 // 1) Filter it into HTML |
|
2197 // 2) Strip out all HTML tags |
|
2198 // 3) Convert entities back to plain-text. |
|
2199 $field = field_info_field('comment_body'); |
|
2200 $langcode = field_is_translatable('comment', $field) ? entity_language('comment', $comment) : LANGUAGE_NONE; |
|
2201 $comment_body = $comment->comment_body[$langcode][0]; |
|
2202 if (isset($comment_body['format'])) { |
|
2203 $comment_text = check_markup($comment_body['value'], $comment_body['format']); |
|
2204 } |
|
2205 else { |
|
2206 $comment_text = check_plain($comment_body['value']); |
|
2207 } |
|
2208 $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE); |
|
2209 // Edge cases where the comment body is populated only by HTML tags will |
|
2210 // require a default subject. |
|
2211 if ($comment->subject == '') { |
|
2212 $comment->subject = t('(No subject)'); |
|
2213 } |
|
2214 } |
|
2215 return $comment; |
|
2216 } |
|
2217 |
|
2218 /** |
|
2219 * Updates the form state's comment entity by processing this submission's values. |
|
2220 * |
|
2221 * This is the default builder function for the comment form. It is called |
|
2222 * during the "Save" and "Preview" submit handlers to retrieve the entity to |
|
2223 * save or preview. This function can also be called by a "Next" button of a |
|
2224 * wizard to update the form state's entity with the current step's values |
|
2225 * before proceeding to the next step. |
|
2226 * |
|
2227 * @see comment_form() |
|
2228 */ |
|
2229 function comment_form_submit_build_comment($form, &$form_state) { |
|
2230 $comment = $form_state['comment']; |
|
2231 entity_form_submit_build_entity('comment', $comment, $form, $form_state); |
|
2232 comment_submit($comment); |
|
2233 return $comment; |
|
2234 } |
|
2235 |
|
2236 /** |
|
2237 * Process comment form submissions; prepare the comment, store it, and set a redirection target. |
|
2238 */ |
|
2239 function comment_form_submit($form, &$form_state) { |
|
2240 $node = node_load($form_state['values']['nid']); |
|
2241 $comment = comment_form_submit_build_comment($form, $form_state); |
|
2242 if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) { |
|
2243 // Save the anonymous user information to a cookie for reuse. |
|
2244 if (user_is_anonymous()) { |
|
2245 user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage')))); |
|
2246 } |
|
2247 |
|
2248 comment_save($comment); |
|
2249 $form_state['values']['cid'] = $comment->cid; |
|
2250 |
|
2251 // Add an entry to the watchdog log. |
|
2252 watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); |
|
2253 |
|
2254 // Explain the approval queue if necessary. |
|
2255 if ($comment->status == COMMENT_NOT_PUBLISHED) { |
|
2256 if (!user_access('administer comments')) { |
|
2257 drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.')); |
|
2258 } |
|
2259 } |
|
2260 else { |
|
2261 drupal_set_message(t('Your comment has been posted.')); |
|
2262 } |
|
2263 $query = array(); |
|
2264 // Find the current display page for this comment. |
|
2265 $page = comment_get_display_page($comment->cid, $node->type); |
|
2266 if ($page > 0) { |
|
2267 $query['page'] = $page; |
|
2268 } |
|
2269 // Redirect to the newly posted comment. |
|
2270 $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid)); |
|
2271 } |
|
2272 else { |
|
2273 watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING); |
|
2274 drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error'); |
|
2275 // Redirect the user to the node they are commenting on. |
|
2276 $redirect = 'node/' . $node->nid; |
|
2277 } |
|
2278 $form_state['redirect'] = $redirect; |
|
2279 // Clear the block and page caches so that anonymous users see the comment |
|
2280 // they have posted. |
|
2281 cache_clear_all(); |
|
2282 } |
|
2283 |
|
2284 /** |
|
2285 * Process variables for comment.tpl.php. |
|
2286 * |
|
2287 * @see comment.tpl.php |
|
2288 */ |
|
2289 function template_preprocess_comment(&$variables) { |
|
2290 $comment = $variables['elements']['#comment']; |
|
2291 $node = $variables['elements']['#node']; |
|
2292 $variables['comment'] = $comment; |
|
2293 $variables['node'] = $node; |
|
2294 $variables['author'] = theme('username', array('account' => $comment)); |
|
2295 |
|
2296 $variables['created'] = format_date($comment->created); |
|
2297 |
|
2298 // Avoid calling format_date() twice on the same timestamp. |
|
2299 if ($comment->changed == $comment->created) { |
|
2300 $variables['changed'] = $variables['created']; |
|
2301 } |
|
2302 else { |
|
2303 $variables['changed'] = format_date($comment->changed); |
|
2304 } |
|
2305 |
|
2306 $variables['new'] = !empty($comment->new) ? t('new') : ''; |
|
2307 $variables['picture'] = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : ''; |
|
2308 $variables['signature'] = $comment->signature; |
|
2309 |
|
2310 $uri = entity_uri('comment', $comment); |
|
2311 $uri['options'] += array('attributes' => array('class' => array('permalink'), 'rel' => 'bookmark')); |
|
2312 |
|
2313 $variables['title'] = l($comment->subject, $uri['path'], $uri['options']); |
|
2314 $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']); |
|
2315 $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created'])); |
|
2316 |
|
2317 // Preprocess fields. |
|
2318 field_attach_preprocess('comment', $comment, $variables['elements'], $variables); |
|
2319 |
|
2320 // Helpful $content variable for templates. |
|
2321 foreach (element_children($variables['elements']) as $key) { |
|
2322 $variables['content'][$key] = $variables['elements'][$key]; |
|
2323 } |
|
2324 |
|
2325 // Set status to a string representation of comment->status. |
|
2326 if (isset($comment->in_preview)) { |
|
2327 $variables['status'] = 'comment-preview'; |
|
2328 } |
|
2329 else { |
|
2330 $variables['status'] = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published'; |
|
2331 } |
|
2332 |
|
2333 // Gather comment classes. |
|
2334 // 'comment-published' class is not needed, it is either 'comment-preview' or |
|
2335 // 'comment-unpublished'. |
|
2336 if ($variables['status'] != 'comment-published') { |
|
2337 $variables['classes_array'][] = $variables['status']; |
|
2338 } |
|
2339 if ($variables['new']) { |
|
2340 $variables['classes_array'][] = 'comment-new'; |
|
2341 } |
|
2342 if (!$comment->uid) { |
|
2343 $variables['classes_array'][] = 'comment-by-anonymous'; |
|
2344 } |
|
2345 else { |
|
2346 if ($comment->uid == $variables['node']->uid) { |
|
2347 $variables['classes_array'][] = 'comment-by-node-author'; |
|
2348 } |
|
2349 if ($comment->uid == $variables['user']->uid) { |
|
2350 $variables['classes_array'][] = 'comment-by-viewer'; |
|
2351 } |
|
2352 } |
|
2353 } |
|
2354 |
|
2355 /** |
|
2356 * Returns HTML for a "you can't post comments" notice. |
|
2357 * |
|
2358 * @param $variables |
|
2359 * An associative array containing: |
|
2360 * - node: The comment node. |
|
2361 * |
|
2362 * @ingroup themeable |
|
2363 */ |
|
2364 function theme_comment_post_forbidden($variables) { |
|
2365 $node = $variables['node']; |
|
2366 global $user; |
|
2367 |
|
2368 // Since this is expensive to compute, we cache it so that a page with many |
|
2369 // comments only has to query the database once for all the links. |
|
2370 $authenticated_post_comments = &drupal_static(__FUNCTION__, NULL); |
|
2371 |
|
2372 if (!$user->uid) { |
|
2373 if (!isset($authenticated_post_comments)) { |
|
2374 // We only output a link if we are certain that users will get permission |
|
2375 // to post comments by logging in. |
|
2376 $comment_roles = user_roles(TRUE, 'post comments'); |
|
2377 $authenticated_post_comments = isset($comment_roles[DRUPAL_AUTHENTICATED_RID]); |
|
2378 } |
|
2379 |
|
2380 if ($authenticated_post_comments) { |
|
2381 // We cannot use drupal_get_destination() because these links |
|
2382 // sometimes appear on /node and taxonomy listing pages. |
|
2383 if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) { |
|
2384 $destination = array('destination' => "comment/reply/$node->nid#comment-form"); |
|
2385 } |
|
2386 else { |
|
2387 $destination = array('destination' => "node/$node->nid#comment-form"); |
|
2388 } |
|
2389 |
|
2390 if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) { |
|
2391 // Users can register themselves. |
|
2392 return t('<a href="@login">Log in</a> or <a href="@register">register</a> to post comments', array('@login' => url('user/login', array('query' => $destination)), '@register' => url('user/register', array('query' => $destination)))); |
|
2393 } |
|
2394 else { |
|
2395 // Only admins can add new users, no public registration. |
|
2396 return t('<a href="@login">Log in</a> to post comments', array('@login' => url('user/login', array('query' => $destination)))); |
|
2397 } |
|
2398 } |
|
2399 } |
|
2400 } |
|
2401 |
|
2402 /** |
|
2403 * Process variables for comment-wrapper.tpl.php. |
|
2404 * |
|
2405 * @see comment-wrapper.tpl.php |
|
2406 */ |
|
2407 function template_preprocess_comment_wrapper(&$variables) { |
|
2408 // Provide contextual information. |
|
2409 $variables['node'] = $variables['content']['#node']; |
|
2410 $variables['display_mode'] = variable_get('comment_default_mode_' . $variables['node']->type, COMMENT_MODE_THREADED); |
|
2411 // The comment form is optional and may not exist. |
|
2412 $variables['content'] += array('comment_form' => array()); |
|
2413 } |
|
2414 |
|
2415 /** |
|
2416 * Return an array of viewing modes for comment listings. |
|
2417 * |
|
2418 * We can't use a global variable array because the locale system |
|
2419 * is not initialized yet when the comment module is loaded. |
|
2420 */ |
|
2421 function _comment_get_modes() { |
|
2422 return array( |
|
2423 COMMENT_MODE_FLAT => t('Flat list'), |
|
2424 COMMENT_MODE_THREADED => t('Threaded list') |
|
2425 ); |
|
2426 } |
|
2427 |
|
2428 /** |
|
2429 * Return an array of "comments per page" settings from which the user |
|
2430 * can choose. |
|
2431 */ |
|
2432 function _comment_per_page() { |
|
2433 return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300)); |
|
2434 } |
|
2435 |
|
2436 /** |
|
2437 * Updates the comment statistics for a given node. This should be called any |
|
2438 * time a comment is added, deleted, or updated. |
|
2439 * |
|
2440 * The following fields are contained in the node_comment_statistics table. |
|
2441 * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node. |
|
2442 * - last_comment_name: the name of the anonymous poster for the last comment |
|
2443 * - last_comment_uid: the uid of the poster for the last comment for this node or the node authors uid if no comments exists for the node. |
|
2444 * - comment_count: the total number of approved/published comments on this node. |
|
2445 */ |
|
2446 function _comment_update_node_statistics($nid) { |
|
2447 // Allow bulk updates and inserts to temporarily disable the |
|
2448 // maintenance of the {node_comment_statistics} table. |
|
2449 if (!variable_get('comment_maintain_node_statistics', TRUE)) { |
|
2450 return; |
|
2451 } |
|
2452 |
|
2453 $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array( |
|
2454 ':nid' => $nid, |
|
2455 ':status' => COMMENT_PUBLISHED, |
|
2456 ))->fetchField(); |
|
2457 |
|
2458 if ($count > 0) { |
|
2459 // Comments exist. |
|
2460 $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array( |
|
2461 ':nid' => $nid, |
|
2462 ':status' => COMMENT_PUBLISHED, |
|
2463 ))->fetchObject(); |
|
2464 db_update('node_comment_statistics') |
|
2465 ->fields(array( |
|
2466 'cid' => $last_reply->cid, |
|
2467 'comment_count' => $count, |
|
2468 'last_comment_timestamp' => $last_reply->changed, |
|
2469 'last_comment_name' => $last_reply->uid ? '' : $last_reply->name, |
|
2470 'last_comment_uid' => $last_reply->uid, |
|
2471 )) |
|
2472 ->condition('nid', $nid) |
|
2473 ->execute(); |
|
2474 } |
|
2475 else { |
|
2476 // Comments do not exist. |
|
2477 $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); |
|
2478 db_update('node_comment_statistics') |
|
2479 ->fields(array( |
|
2480 'cid' => 0, |
|
2481 'comment_count' => 0, |
|
2482 'last_comment_timestamp' => $node->created, |
|
2483 'last_comment_name' => '', |
|
2484 'last_comment_uid' => $node->uid, |
|
2485 )) |
|
2486 ->condition('nid', $nid) |
|
2487 ->execute(); |
|
2488 } |
|
2489 } |
|
2490 |
|
2491 /** |
|
2492 * Generate vancode. |
|
2493 * |
|
2494 * Consists of a leading character indicating length, followed by N digits |
|
2495 * with a numerical value in base 36. Vancodes can be sorted as strings |
|
2496 * without messing up numerical order. |
|
2497 * |
|
2498 * It goes: |
|
2499 * 00, 01, 02, ..., 0y, 0z, |
|
2500 * 110, 111, ... , 1zy, 1zz, |
|
2501 * 2100, 2101, ..., 2zzy, 2zzz, |
|
2502 * 31000, 31001, ... |
|
2503 */ |
|
2504 function int2vancode($i = 0) { |
|
2505 $num = base_convert((int) $i, 10, 36); |
|
2506 $length = strlen($num); |
|
2507 |
|
2508 return chr($length + ord('0') - 1) . $num; |
|
2509 } |
|
2510 |
|
2511 /** |
|
2512 * Decode vancode back to an integer. |
|
2513 */ |
|
2514 function vancode2int($c = '00') { |
|
2515 return base_convert(substr($c, 1), 36, 10); |
|
2516 } |
|
2517 |
|
2518 /** |
|
2519 * Implements hook_action_info(). |
|
2520 */ |
|
2521 function comment_action_info() { |
|
2522 return array( |
|
2523 'comment_publish_action' => array( |
|
2524 'label' => t('Publish comment'), |
|
2525 'type' => 'comment', |
|
2526 'configurable' => FALSE, |
|
2527 'behavior' => array('changes_property'), |
|
2528 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'), |
|
2529 ), |
|
2530 'comment_unpublish_action' => array( |
|
2531 'label' => t('Unpublish comment'), |
|
2532 'type' => 'comment', |
|
2533 'configurable' => FALSE, |
|
2534 'behavior' => array('changes_property'), |
|
2535 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'), |
|
2536 ), |
|
2537 'comment_unpublish_by_keyword_action' => array( |
|
2538 'label' => t('Unpublish comment containing keyword(s)'), |
|
2539 'type' => 'comment', |
|
2540 'configurable' => TRUE, |
|
2541 'behavior' => array('changes_property'), |
|
2542 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'), |
|
2543 ), |
|
2544 'comment_save_action' => array( |
|
2545 'label' => t('Save comment'), |
|
2546 'type' => 'comment', |
|
2547 'configurable' => FALSE, |
|
2548 'triggers' => array('comment_insert', 'comment_update'), |
|
2549 ), |
|
2550 ); |
|
2551 } |
|
2552 |
|
2553 /** |
|
2554 * Publishes a comment. |
|
2555 * |
|
2556 * @param $comment |
|
2557 * An optional comment object. |
|
2558 * @param array $context |
|
2559 * Array with components: |
|
2560 * - 'cid': Comment ID. Required if $comment is not given. |
|
2561 * |
|
2562 * @ingroup actions |
|
2563 */ |
|
2564 function comment_publish_action($comment, $context = array()) { |
|
2565 if (isset($comment->subject)) { |
|
2566 $subject = $comment->subject; |
|
2567 $comment->status = COMMENT_PUBLISHED; |
|
2568 } |
|
2569 else { |
|
2570 $cid = $context['cid']; |
|
2571 $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField(); |
|
2572 db_update('comment') |
|
2573 ->fields(array('status' => COMMENT_PUBLISHED)) |
|
2574 ->condition('cid', $cid) |
|
2575 ->execute(); |
|
2576 } |
|
2577 watchdog('action', 'Published comment %subject.', array('%subject' => $subject)); |
|
2578 } |
|
2579 |
|
2580 /** |
|
2581 * Unpublishes a comment. |
|
2582 * |
|
2583 * @param $comment |
|
2584 * An optional comment object. |
|
2585 * @param array $context |
|
2586 * Array with components: |
|
2587 * - 'cid': Comment ID. Required if $comment is not given. |
|
2588 * |
|
2589 * @ingroup actions |
|
2590 */ |
|
2591 function comment_unpublish_action($comment, $context = array()) { |
|
2592 if (isset($comment->subject)) { |
|
2593 $subject = $comment->subject; |
|
2594 $comment->status = COMMENT_NOT_PUBLISHED; |
|
2595 } |
|
2596 else { |
|
2597 $cid = $context['cid']; |
|
2598 $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField(); |
|
2599 db_update('comment') |
|
2600 ->fields(array('status' => COMMENT_NOT_PUBLISHED)) |
|
2601 ->condition('cid', $cid) |
|
2602 ->execute(); |
|
2603 } |
|
2604 watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject)); |
|
2605 } |
|
2606 |
|
2607 /** |
|
2608 * Unpublishes a comment if it contains certain keywords. |
|
2609 * |
|
2610 * @param object $comment |
|
2611 * Comment object to modify. |
|
2612 * @param array $context |
|
2613 * Array with components: |
|
2614 * - 'keywords': Keywords to look for. If the comment contains at least one |
|
2615 * of the keywords, it is unpublished. |
|
2616 * |
|
2617 * @ingroup actions |
|
2618 * @see comment_unpublish_by_keyword_action_form() |
|
2619 * @see comment_unpublish_by_keyword_action_submit() |
|
2620 */ |
|
2621 function comment_unpublish_by_keyword_action($comment, $context) { |
|
2622 $node = node_load($comment->nid); |
|
2623 $build = comment_view($comment, $node); |
|
2624 $text = drupal_render($build); |
|
2625 foreach ($context['keywords'] as $keyword) { |
|
2626 if (strpos($text, $keyword) !== FALSE) { |
|
2627 $comment->status = COMMENT_NOT_PUBLISHED; |
|
2628 comment_save($comment); |
|
2629 watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject)); |
|
2630 break; |
|
2631 } |
|
2632 } |
|
2633 } |
|
2634 |
|
2635 /** |
|
2636 * Form builder; Prepare a form for blacklisted keywords. |
|
2637 * |
|
2638 * @ingroup forms |
|
2639 * @see comment_unpublish_by_keyword_action() |
|
2640 * @see comment_unpublish_by_keyword_action_submit() |
|
2641 */ |
|
2642 function comment_unpublish_by_keyword_action_form($context) { |
|
2643 $form['keywords'] = array( |
|
2644 '#title' => t('Keywords'), |
|
2645 '#type' => 'textarea', |
|
2646 '#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'), |
|
2647 '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '', |
|
2648 ); |
|
2649 |
|
2650 return $form; |
|
2651 } |
|
2652 |
|
2653 /** |
|
2654 * Process comment_unpublish_by_keyword_action_form form submissions. |
|
2655 * |
|
2656 * @see comment_unpublish_by_keyword_action() |
|
2657 */ |
|
2658 function comment_unpublish_by_keyword_action_submit($form, $form_state) { |
|
2659 return array('keywords' => drupal_explode_tags($form_state['values']['keywords'])); |
|
2660 } |
|
2661 |
|
2662 /** |
|
2663 * Saves a comment. |
|
2664 * |
|
2665 * @ingroup actions |
|
2666 */ |
|
2667 function comment_save_action($comment) { |
|
2668 comment_save($comment); |
|
2669 cache_clear_all(); |
|
2670 watchdog('action', 'Saved comment %title', array('%title' => $comment->subject)); |
|
2671 } |
|
2672 |
|
2673 /** |
|
2674 * Implements hook_ranking(). |
|
2675 */ |
|
2676 function comment_ranking() { |
|
2677 return array( |
|
2678 'comments' => array( |
|
2679 'title' => t('Number of comments'), |
|
2680 'join' => array( |
|
2681 'type' => 'LEFT', |
|
2682 'table' => 'node_comment_statistics', |
|
2683 'alias' => 'node_comment_statistics', |
|
2684 'on' => 'node_comment_statistics.nid = i.sid', |
|
2685 ), |
|
2686 // Inverse law that maps the highest reply count on the site to 1 and 0 to 0. |
|
2687 'score' => '2.0 - 2.0 / (1.0 + node_comment_statistics.comment_count * CAST(:scale AS DECIMAL))', |
|
2688 'arguments' => array(':scale' => variable_get('node_cron_comments_scale', 0)), |
|
2689 ), |
|
2690 ); |
|
2691 } |
|
2692 |
|
2693 /** |
|
2694 * Implements hook_rdf_mapping(). |
|
2695 */ |
|
2696 function comment_rdf_mapping() { |
|
2697 return array( |
|
2698 array( |
|
2699 'type' => 'comment', |
|
2700 'bundle' => RDF_DEFAULT_BUNDLE, |
|
2701 'mapping' => array( |
|
2702 'rdftype' => array('sioc:Post', 'sioct:Comment'), |
|
2703 'title' => array( |
|
2704 'predicates' => array('dc:title'), |
|
2705 ), |
|
2706 'created' => array( |
|
2707 'predicates' => array('dc:date', 'dc:created'), |
|
2708 'datatype' => 'xsd:dateTime', |
|
2709 'callback' => 'date_iso8601', |
|
2710 ), |
|
2711 'changed' => array( |
|
2712 'predicates' => array('dc:modified'), |
|
2713 'datatype' => 'xsd:dateTime', |
|
2714 'callback' => 'date_iso8601', |
|
2715 ), |
|
2716 'comment_body' => array( |
|
2717 'predicates' => array('content:encoded'), |
|
2718 ), |
|
2719 'pid' => array( |
|
2720 'predicates' => array('sioc:reply_of'), |
|
2721 'type' => 'rel', |
|
2722 ), |
|
2723 'uid' => array( |
|
2724 'predicates' => array('sioc:has_creator'), |
|
2725 'type' => 'rel', |
|
2726 ), |
|
2727 'name' => array( |
|
2728 'predicates' => array('foaf:name'), |
|
2729 ), |
|
2730 ), |
|
2731 ), |
|
2732 ); |
|
2733 } |
|
2734 |
|
2735 /** |
|
2736 * Implements hook_file_download_access(). |
|
2737 */ |
|
2738 function comment_file_download_access($field, $entity_type, $entity) { |
|
2739 if ($entity_type == 'comment') { |
|
2740 if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) { |
|
2741 $node = node_load($entity->nid); |
|
2742 return node_access('view', $node); |
|
2743 } |
|
2744 return FALSE; |
|
2745 } |
|
2746 } |