a5f196646974da48c9e71ba4b35f1cc12d338630
[yaffs-website] / web / core / modules / comment / src / Entity / Comment.php
1 <?php
2
3 namespace Drupal\comment\Entity;
4
5 use Drupal\Component\Utility\Number;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Entity\ContentEntityBase;
8 use Drupal\comment\CommentInterface;
9 use Drupal\Core\Entity\EntityChangedTrait;
10 use Drupal\Core\Entity\EntityPublishedTrait;
11 use Drupal\Core\Entity\EntityStorageInterface;
12 use Drupal\Core\Entity\EntityTypeInterface;
13 use Drupal\Core\Field\BaseFieldDefinition;
14 use Drupal\field\Entity\FieldStorageConfig;
15 use Drupal\user\Entity\User;
16 use Drupal\user\UserInterface;
17
18 /**
19  * Defines the comment entity class.
20  *
21  * @ContentEntityType(
22  *   id = "comment",
23  *   label = @Translation("Comment"),
24  *   label_singular = @Translation("comment"),
25  *   label_plural = @Translation("comments"),
26  *   label_count = @PluralTranslation(
27  *     singular = "@count comment",
28  *     plural = "@count comments",
29  *   ),
30  *   bundle_label = @Translation("Comment type"),
31  *   handlers = {
32  *     "storage" = "Drupal\comment\CommentStorage",
33  *     "storage_schema" = "Drupal\comment\CommentStorageSchema",
34  *     "access" = "Drupal\comment\CommentAccessControlHandler",
35  *     "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
36  *     "view_builder" = "Drupal\comment\CommentViewBuilder",
37  *     "views_data" = "Drupal\comment\CommentViewsData",
38  *     "form" = {
39  *       "default" = "Drupal\comment\CommentForm",
40  *       "delete" = "Drupal\comment\Form\DeleteForm"
41  *     },
42  *     "translation" = "Drupal\comment\CommentTranslationHandler"
43  *   },
44  *   base_table = "comment",
45  *   data_table = "comment_field_data",
46  *   uri_callback = "comment_uri",
47  *   translatable = TRUE,
48  *   entity_keys = {
49  *     "id" = "cid",
50  *     "bundle" = "comment_type",
51  *     "label" = "subject",
52  *     "langcode" = "langcode",
53  *     "uuid" = "uuid",
54  *     "published" = "status",
55  *   },
56  *   links = {
57  *     "canonical" = "/comment/{comment}",
58  *     "delete-form" = "/comment/{comment}/delete",
59  *     "edit-form" = "/comment/{comment}/edit",
60  *   },
61  *   bundle_entity_type = "comment_type",
62  *   field_ui_base_route  = "entity.comment_type.edit_form",
63  *   constraints = {
64  *     "CommentName" = {}
65  *   }
66  * )
67  */
68 class Comment extends ContentEntityBase implements CommentInterface {
69
70   use EntityChangedTrait;
71   use EntityPublishedTrait;
72
73   /**
74    * The thread for which a lock was acquired.
75    */
76   protected $threadLock = '';
77
78   /**
79    * {@inheritdoc}
80    */
81   public function preSave(EntityStorageInterface $storage) {
82     parent::preSave($storage);
83
84     if (is_null($this->get('status')->value)) {
85       if (\Drupal::currentUser()->hasPermission('skip comment approval')) {
86         $this->setPublished();
87       }
88       else {
89         $this->setUnpublished();
90       }
91     }
92     if ($this->isNew()) {
93       // Add the comment to database. This next section builds the thread field.
94       // @see \Drupal\comment\CommentViewBuilder::buildComponents()
95       $thread = $this->getThread();
96       if (empty($thread)) {
97         if ($this->threadLock) {
98           // Thread lock was not released after being set previously.
99           // This suggests there's a bug in code using this class.
100           throw new \LogicException('preSave() is called again without calling postSave() or releaseThreadLock()');
101         }
102         if (!$this->hasParentComment()) {
103           // This is a comment with no parent comment (depth 0): we start
104           // by retrieving the maximum thread level.
105           $max = $storage->getMaxThread($this);
106           // Strip the "/" from the end of the thread.
107           $max = rtrim($max, '/');
108           // We need to get the value at the correct depth.
109           $parts = explode('.', $max);
110           $n = Number::alphadecimalToInt($parts[0]);
111           $prefix = '';
112         }
113         else {
114           // This is a comment with a parent comment, so increase the part of
115           // the thread value at the proper depth.
116
117           // Get the parent comment:
118           $parent = $this->getParentComment();
119           // Strip the "/" from the end of the parent thread.
120           $parent->setThread((string) rtrim((string) $parent->getThread(), '/'));
121           $prefix = $parent->getThread() . '.';
122           // Get the max value in *this* thread.
123           $max = $storage->getMaxThreadPerThread($this);
124
125           if ($max == '') {
126             // First child of this parent. As the other two cases do an
127             // increment of the thread number before creating the thread
128             // string set this to -1 so it requires an increment too.
129             $n = -1;
130           }
131           else {
132             // Strip the "/" at the end of the thread.
133             $max = rtrim($max, '/');
134             // Get the value at the correct depth.
135             $parts = explode('.', $max);
136             $parent_depth = count(explode('.', $parent->getThread()));
137             $n = Number::alphadecimalToInt($parts[$parent_depth]);
138           }
139         }
140         // Finally, build the thread field for this new comment. To avoid
141         // race conditions, get a lock on the thread. If another process already
142         // has the lock, just move to the next integer.
143         do {
144           $thread = $prefix . Number::intToAlphadecimal(++$n) . '/';
145           $lock_name = "comment:{$this->getCommentedEntityId()}:$thread";
146         } while (!\Drupal::lock()->acquire($lock_name));
147         $this->threadLock = $lock_name;
148       }
149       // We test the value with '===' because we need to modify anonymous
150       // users as well.
151       if ($this->getOwnerId() === \Drupal::currentUser()->id() && \Drupal::currentUser()->isAuthenticated()) {
152         $this->setAuthorName(\Drupal::currentUser()->getUsername());
153       }
154       $this->setThread($thread);
155       if (!$this->getHostname()) {
156         // Ensure a client host from the current request.
157         $this->setHostname(\Drupal::request()->getClientIP());
158       }
159     }
160   }
161
162   /**
163    * {@inheritdoc}
164    */
165   public function postSave(EntityStorageInterface $storage, $update = TRUE) {
166     parent::postSave($storage, $update);
167
168     // Always invalidate the cache tag for the commented entity.
169     if ($commented_entity = $this->getCommentedEntity()) {
170       Cache::invalidateTags($commented_entity->getCacheTagsToInvalidate());
171     }
172
173     $this->releaseThreadLock();
174     // Update the {comment_entity_statistics} table prior to executing the hook.
175     \Drupal::service('comment.statistics')->update($this);
176   }
177
178   /**
179    * Release the lock acquired for the thread in preSave().
180    */
181   protected function releaseThreadLock() {
182     if ($this->threadLock) {
183       \Drupal::lock()->release($this->threadLock);
184       $this->threadLock = '';
185     }
186   }
187
188   /**
189    * {@inheritdoc}
190    */
191   public static function postDelete(EntityStorageInterface $storage, array $entities) {
192     parent::postDelete($storage, $entities);
193
194     $child_cids = $storage->getChildCids($entities);
195     entity_delete_multiple('comment', $child_cids);
196
197     foreach ($entities as $id => $entity) {
198       \Drupal::service('comment.statistics')->update($entity);
199     }
200   }
201
202   /**
203    * {@inheritdoc}
204    */
205   public function referencedEntities() {
206     $referenced_entities = parent::referencedEntities();
207     if ($this->getCommentedEntityId()) {
208       $referenced_entities[] = $this->getCommentedEntity();
209     }
210     return $referenced_entities;
211   }
212
213   /**
214    * {@inheritdoc}
215    */
216   public function permalink() {
217     $uri = $this->urlInfo();
218     $uri->setOption('fragment', 'comment-' . $this->id());
219     return $uri;
220   }
221
222   /**
223    * {@inheritdoc}
224    */
225   public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
226     /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
227     $fields = parent::baseFieldDefinitions($entity_type);
228     $fields += static::publishedBaseFieldDefinitions($entity_type);
229
230     $fields['cid']->setLabel(t('Comment ID'))
231       ->setDescription(t('The comment ID.'));
232
233     $fields['uuid']->setDescription(t('The comment UUID.'));
234
235     $fields['comment_type']->setLabel(t('Comment Type'))
236       ->setDescription(t('The comment type.'));
237
238     $fields['langcode']->setDescription(t('The comment language code.'));
239
240     $fields['pid'] = BaseFieldDefinition::create('entity_reference')
241       ->setLabel(t('Parent ID'))
242       ->setDescription(t('The parent comment ID if this is a reply to a comment.'))
243       ->setSetting('target_type', 'comment');
244
245     $fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
246       ->setLabel(t('Entity ID'))
247       ->setDescription(t('The ID of the entity of which this comment is a reply.'))
248       ->setRequired(TRUE);
249
250     $fields['subject'] = BaseFieldDefinition::create('string')
251       ->setLabel(t('Subject'))
252       ->setTranslatable(TRUE)
253       ->setSetting('max_length', 64)
254       ->setDisplayOptions('form', [
255         'type' => 'string_textfield',
256         // Default comment body field has weight 20.
257         'weight' => 10,
258       ])
259       ->setDisplayConfigurable('form', TRUE);
260
261     $fields['uid'] = BaseFieldDefinition::create('entity_reference')
262       ->setLabel(t('User ID'))
263       ->setDescription(t('The user ID of the comment author.'))
264       ->setTranslatable(TRUE)
265       ->setSetting('target_type', 'user')
266       ->setDefaultValue(0);
267
268     $fields['name'] = BaseFieldDefinition::create('string')
269       ->setLabel(t('Name'))
270       ->setDescription(t("The comment author's name."))
271       ->setTranslatable(TRUE)
272       ->setSetting('max_length', 60)
273       ->setDefaultValue('');
274
275     $fields['mail'] = BaseFieldDefinition::create('email')
276       ->setLabel(t('Email'))
277       ->setDescription(t("The comment author's email address."))
278       ->setTranslatable(TRUE);
279
280     $fields['homepage'] = BaseFieldDefinition::create('uri')
281       ->setLabel(t('Homepage'))
282       ->setDescription(t("The comment author's home page address."))
283       ->setTranslatable(TRUE)
284       // URIs are not length limited by RFC 2616, but we can only store 255
285       // characters in our comment DB schema.
286       ->setSetting('max_length', 255);
287
288     $fields['hostname'] = BaseFieldDefinition::create('string')
289       ->setLabel(t('Hostname'))
290       ->setDescription(t("The comment author's hostname."))
291       ->setTranslatable(TRUE)
292       ->setSetting('max_length', 128);
293
294     $fields['created'] = BaseFieldDefinition::create('created')
295       ->setLabel(t('Created'))
296       ->setDescription(t('The time that the comment was created.'))
297       ->setTranslatable(TRUE);
298
299     $fields['changed'] = BaseFieldDefinition::create('changed')
300       ->setLabel(t('Changed'))
301       ->setDescription(t('The time that the comment was last edited.'))
302       ->setTranslatable(TRUE);
303
304     $fields['thread'] = BaseFieldDefinition::create('string')
305       ->setLabel(t('Thread place'))
306       ->setDescription(t("The alphadecimal representation of the comment's place in a thread, consisting of a base 36 string prefixed by an integer indicating its length."))
307       ->setSetting('max_length', 255);
308
309     $fields['entity_type'] = BaseFieldDefinition::create('string')
310       ->setLabel(t('Entity type'))
311       ->setDescription(t('The entity type to which this comment is attached.'))
312       ->setSetting('is_ascii', TRUE)
313       ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);
314
315     $fields['field_name'] = BaseFieldDefinition::create('string')
316       ->setLabel(t('Comment field name'))
317       ->setDescription(t('The field name through which this comment was added.'))
318       ->setSetting('is_ascii', TRUE)
319       ->setSetting('max_length', FieldStorageConfig::NAME_MAX_LENGTH);
320
321     return $fields;
322   }
323
324   /**
325    * {@inheritdoc}
326    */
327   public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
328     if ($comment_type = CommentType::load($bundle)) {
329       $fields['entity_id'] = clone $base_field_definitions['entity_id'];
330       $fields['entity_id']->setSetting('target_type', $comment_type->getTargetEntityTypeId());
331       return $fields;
332     }
333     return [];
334   }
335
336   /**
337    * {@inheritdoc}
338    */
339   public function hasParentComment() {
340     return (bool) $this->get('pid')->target_id;
341   }
342
343   /**
344    * {@inheritdoc}
345    */
346   public function getParentComment() {
347     return $this->get('pid')->entity;
348   }
349
350   /**
351    * {@inheritdoc}
352    */
353   public function getCommentedEntity() {
354     return $this->get('entity_id')->entity;
355   }
356
357   /**
358    * {@inheritdoc}
359    */
360   public function getCommentedEntityId() {
361     return $this->get('entity_id')->target_id;
362   }
363
364   /**
365    * {@inheritdoc}
366    */
367   public function getCommentedEntityTypeId() {
368     return $this->get('entity_type')->value;
369   }
370
371   /**
372    * {@inheritdoc}
373    */
374   public function setFieldName($field_name) {
375     $this->set('field_name', $field_name);
376     return $this;
377   }
378
379   /**
380    * {@inheritdoc}
381    */
382   public function getFieldName() {
383     return $this->get('field_name')->value;
384   }
385
386   /**
387    * {@inheritdoc}
388    */
389   public function getSubject() {
390     return $this->get('subject')->value;
391   }
392
393   /**
394    * {@inheritdoc}
395    */
396   public function setSubject($subject) {
397     $this->set('subject', $subject);
398     return $this;
399   }
400
401   /**
402    * {@inheritdoc}
403    */
404   public function getAuthorName() {
405     if ($this->get('uid')->target_id) {
406       return $this->get('uid')->entity->label();
407     }
408     return $this->get('name')->value ?: \Drupal::config('user.settings')->get('anonymous');
409   }
410
411   /**
412    * {@inheritdoc}
413    */
414   public function setAuthorName($name) {
415     $this->set('name', $name);
416     return $this;
417   }
418
419   /**
420    * {@inheritdoc}
421    */
422   public function getAuthorEmail() {
423     $mail = $this->get('mail')->value;
424
425     if ($this->get('uid')->target_id != 0) {
426       $mail = $this->get('uid')->entity->getEmail();
427     }
428
429     return $mail;
430   }
431
432   /**
433    * {@inheritdoc}
434    */
435   public function getHomepage() {
436     return $this->get('homepage')->value;
437   }
438
439   /**
440    * {@inheritdoc}
441    */
442   public function setHomepage($homepage) {
443     $this->set('homepage', $homepage);
444     return $this;
445   }
446
447   /**
448    * {@inheritdoc}
449    */
450   public function getHostname() {
451     return $this->get('hostname')->value;
452   }
453
454   /**
455    * {@inheritdoc}
456    */
457   public function setHostname($hostname) {
458     $this->set('hostname', $hostname);
459     return $this;
460   }
461
462   /**
463    * {@inheritdoc}
464    */
465   public function getCreatedTime() {
466     if (isset($this->get('created')->value)) {
467       return $this->get('created')->value;
468     }
469     return NULL;
470   }
471
472   /**
473    * {@inheritdoc}
474    */
475   public function setCreatedTime($created) {
476     $this->set('created', $created);
477     return $this;
478   }
479
480   /**
481    * {@inheritdoc}
482    */
483   public function getStatus() {
484     return $this->get('status')->value;
485   }
486
487   /**
488    * {@inheritdoc}
489    */
490   public function getThread() {
491     $thread = $this->get('thread');
492     if (!empty($thread->value)) {
493       return $thread->value;
494     }
495   }
496
497   /**
498    * {@inheritdoc}
499    */
500   public function setThread($thread) {
501     $this->set('thread', $thread);
502     return $this;
503   }
504
505   /**
506    * {@inheritdoc}
507    */
508   public static function preCreate(EntityStorageInterface $storage, array &$values) {
509     if (empty($values['comment_type']) && !empty($values['field_name']) && !empty($values['entity_type'])) {
510       $field_storage = FieldStorageConfig::loadByName($values['entity_type'], $values['field_name']);
511       $values['comment_type'] = $field_storage->getSetting('comment_type');
512     }
513   }
514
515   /**
516    * {@inheritdoc}
517    */
518   public function getOwner() {
519     $user = $this->get('uid')->entity;
520     if (!$user || $user->isAnonymous()) {
521       $user = User::getAnonymousUser();
522       $user->name = $this->getAuthorName();
523       $user->homepage = $this->getHomepage();
524     }
525     return $user;
526   }
527
528   /**
529    * {@inheritdoc}
530    */
531   public function getOwnerId() {
532     return $this->get('uid')->target_id;
533   }
534
535   /**
536    * {@inheritdoc}
537    */
538   public function setOwnerId($uid) {
539     $this->set('uid', $uid);
540     return $this;
541   }
542
543   /**
544    * {@inheritdoc}
545    */
546   public function setOwner(UserInterface $account) {
547     $this->set('uid', $account->id());
548     return $this;
549   }
550
551   /**
552    * Get the comment type ID for this comment.
553    *
554    * @return string
555    *   The ID of the comment type.
556    */
557   public function getTypeId() {
558     return $this->bundle();
559   }
560
561 }