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