Comment.php 17 KB

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