WingStorageController.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <?php
  2. /**
  3. * @file
  4. * Definition of Drupal\comment\WingStorageController.
  5. */
  6. namespace Drupal\comment;
  7. use Drupal\Core\Entity\EntityInterface;
  8. use Drupal\Core\Entity\DatabaseStorageControllerNG;
  9. use Drupal\Component\Uuid\Uuid;
  10. use LogicException;
  11. /**
  12. * Defines the controller class for comments.
  13. *
  14. * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
  15. * required special handling for comment entities.
  16. */
  17. class WingStorageController extends DatabaseStorageControllerNG {
  18. /**
  19. * The thread for which a lock was acquired.
  20. */
  21. protected $threadLock = '';
  22. /**
  23. * Overrides Drupal\Core\Entity\DatabaseStorageController::buildQuery().
  24. */
  25. protected function buildQuery($ids, $revision_id = FALSE) {
  26. $query = parent::buildQuery($ids, $revision_id);
  27. // Specify additional fields from the user and node tables.
  28. $query->innerJoin('node', 'n', 'base.nid = n.nid');
  29. $query->addField('n', 'type', 'node_type');
  30. $query->innerJoin('users', 'u', 'base.uid = u.uid');
  31. // @todo: Move to a computed 'name' field instead.
  32. $query->addField('u', 'name', 'registered_name');
  33. return $query;
  34. }
  35. /**
  36. * Overrides Drupal\Core\Entity\DatabaseStorageController::attachLoad().
  37. */
  38. protected function attachLoad(&$records, $load_revision = FALSE) {
  39. // Prepare standard comment fields.
  40. foreach ($records as $key => $record) {
  41. $record->name = $record->uid ? $record->registered_name : $record->name;
  42. $record->node_type = 'comment_node_' . $record->node_type;
  43. $records[$key] = $record;
  44. }
  45. parent::attachLoad($records, $load_revision);
  46. }
  47. /**
  48. * Overrides Drupal\Core\Entity\DatabaseStorageControllerNG::create().
  49. */
  50. public function create(array $values) {
  51. if (empty($values['node_type']) && !empty($values['nid'])) {
  52. $node = node_load(is_object($values['nid']) ? $values['nid']->value : $values['nid']);
  53. $values['node_type'] = 'comment_node_' . $node->type;
  54. }
  55. return parent::create($values);
  56. }
  57. /**
  58. * Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
  59. *
  60. * @see comment_int_to_alphadecimal()
  61. * @see comment_alphadecimal_to_int()
  62. */
  63. protected function preSave(EntityInterface $comment) {
  64. global $user;
  65. if (!isset($comment->status->value)) {
  66. $comment->status->value = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
  67. }
  68. // Make sure we have a proper bundle name.
  69. if (!isset($comment->node_type->value)) {
  70. $comment->node_type->value = 'comment_node_' . $comment->nid->entity->type;
  71. }
  72. if ($comment->isNew()) {
  73. // Add the comment to database. This next section builds the thread field.
  74. // Also see the documentation for comment_view().
  75. if (!empty($comment->thread->value)) {
  76. // Allow calling code to set thread itself.
  77. $thread = $comment->thread->value;
  78. }
  79. else {
  80. if ($this->threadLock) {
  81. // As preSave() is protected, this can only happen when this class
  82. // is extended in a faulty manner.
  83. throw new LogicException('preSave is called again without calling postSave() or releaseThreadLock()');
  84. }
  85. if ($comment->pid->target_id == 0) {
  86. // This is a comment with no parent comment (depth 0): we start
  87. // by retrieving the maximum thread level.
  88. $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid->target_id))->fetchField();
  89. // Strip the "/" from the end of the thread.
  90. $max = rtrim($max, '/');
  91. // We need to get the value at the correct depth.
  92. $parts = explode('.', $max);
  93. $n = comment_alphadecimal_to_int($parts[0]);
  94. $prefix = '';
  95. }
  96. else {
  97. // This is a comment with a parent comment, so increase the part of
  98. // the thread value at the proper depth.
  99. // Get the parent comment:
  100. $parent = $comment->pid->entity;
  101. // Strip the "/" from the end of the parent thread.
  102. $parent->thread->value = (string) rtrim((string) $parent->thread->value, '/');
  103. $prefix = $parent->thread->value . '.';
  104. // Get the max value in *this* thread.
  105. $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
  106. ':thread' => $parent->thread->value . '.%',
  107. ':nid' => $comment->nid->target_id,
  108. ))->fetchField();
  109. if ($max == '') {
  110. // First child of this parent. As the other two cases do an
  111. // increment of the thread number before creating the thread
  112. // string set this to -1 so it requires an increment too.
  113. $n = -1;
  114. }
  115. else {
  116. // Strip the "/" at the end of the thread.
  117. $max = rtrim($max, '/');
  118. // Get the value at the correct depth.
  119. $parts = explode('.', $max);
  120. $parent_depth = count(explode('.', $parent->thread->value));
  121. $n = comment_alphadecimal_to_int($parts[$parent_depth]);
  122. }
  123. }
  124. // Finally, build the thread field for this new comment. To avoid
  125. // race conditions, get a lock on the thread. If aother process already
  126. // has the lock, just move to the next integer.
  127. do {
  128. $thread = $prefix . comment_int_to_alphadecimal(++$n) . '/';
  129. } while (!lock()->acquire("comment:{$comment->nid->target_id}:$thread"));
  130. $this->threadLock = $thread;
  131. }
  132. if (empty($comment->created->value)) {
  133. $comment->created->value = REQUEST_TIME;
  134. }
  135. if (empty($comment->changed->value)) {
  136. $comment->changed->value = $comment->created->value;
  137. }
  138. // We test the value with '===' because we need to modify anonymous
  139. // users as well.
  140. if ($comment->uid->target_id === $user->uid && $user->uid) {
  141. $comment->name->value = $user->name;
  142. }
  143. // Add the values which aren't passed into the function.
  144. $comment->thread->value = $thread;
  145. $comment->hostname->value = ip_address();
  146. }
  147. }
  148. /**
  149. * Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
  150. */
  151. protected function postSave(EntityInterface $comment, $update) {
  152. $this->releaseThreadLock();
  153. // Update the {node_comment_statistics} table prior to executing the hook.
  154. $this->updateNodeStatistics($comment->nid->target_id);
  155. if ($comment->status->value == COMMENT_PUBLISHED) {
  156. module_invoke_all('comment_publish', $comment);
  157. }
  158. }
  159. /**
  160. * Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
  161. */
  162. protected function postDelete($comments) {
  163. // Delete the comments' replies.
  164. $query = db_select('comment', 'c')
  165. ->fields('c', array('cid'))
  166. ->condition('pid', array(array_keys($comments)), 'IN');
  167. $child_cids = $query->execute()->fetchCol();
  168. comment_delete_multiple($child_cids);
  169. foreach ($comments as $comment) {
  170. $this->updateNodeStatistics($comment->nid->target_id);
  171. }
  172. }
  173. /**
  174. * Updates the comment statistics for a given node.
  175. *
  176. * The {node_comment_statistics} table has the following fields:
  177. * - last_comment_timestamp: The timestamp of the last comment for this node,
  178. * or the node created timestamp if no comments exist for the node.
  179. * - last_comment_name: The name of the anonymous poster for the last comment.
  180. * - last_comment_uid: The user ID of the poster for the last comment for
  181. * this node, or the node author's user ID if no comments exist for the
  182. * node.
  183. * - comment_count: The total number of approved/published comments on this
  184. * node.
  185. *
  186. * @param $nid
  187. * The node ID.
  188. */
  189. protected function updateNodeStatistics($nid) {
  190. // Allow bulk updates and inserts to temporarily disable the
  191. // maintenance of the {node_comment_statistics} table.
  192. if (!variable_get('comment_maintain_node_statistics', TRUE)) {
  193. return;
  194. }
  195. $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
  196. ':nid' => $nid,
  197. ':status' => COMMENT_PUBLISHED,
  198. ))->fetchField();
  199. if ($count > 0) {
  200. // Comments exist.
  201. $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(
  202. ':nid' => $nid,
  203. ':status' => COMMENT_PUBLISHED,
  204. ))->fetchObject();
  205. db_update('node_comment_statistics')
  206. ->fields(array(
  207. 'cid' => $last_reply->cid,
  208. 'comment_count' => $count,
  209. 'last_comment_timestamp' => $last_reply->changed,
  210. 'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
  211. 'last_comment_uid' => $last_reply->uid,
  212. ))
  213. ->condition('nid', $nid)
  214. ->execute();
  215. }
  216. else {
  217. // Comments do not exist.
  218. $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
  219. db_update('node_comment_statistics')
  220. ->fields(array(
  221. 'cid' => 0,
  222. 'comment_count' => 0,
  223. 'last_comment_timestamp' => $node->created,
  224. 'last_comment_name' => '',
  225. 'last_comment_uid' => $node->uid,
  226. ))
  227. ->condition('nid', $nid)
  228. ->execute();
  229. }
  230. }
  231. /**
  232. * Release the lock acquired for the thread in preSave().
  233. */
  234. protected function releaseThreadLock() {
  235. if ($this->threadLock) {
  236. lock()->release($this->threadLock);
  237. $this->threadLock = '';
  238. }
  239. }
  240. /**
  241. * Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::basePropertyDefinitions().
  242. */
  243. public function baseFieldDefinitions() {
  244. $properties['cid'] = array(
  245. 'label' => t('ID'),
  246. 'description' => t('The comment ID.'),
  247. 'type' => 'integer_field',
  248. 'read-only' => TRUE,
  249. );
  250. $properties['uuid'] = array(
  251. 'label' => t('UUID'),
  252. 'description' => t('The comment UUID.'),
  253. 'type' => 'string_field',
  254. );
  255. $properties['pid'] = array(
  256. 'label' => t('Parent ID'),
  257. 'description' => t('The parent comment ID if this is a reply to a comment.'),
  258. 'type' => 'entity_reference_field',
  259. 'settings' => array('target_type' => 'comment'),
  260. );
  261. $properties['nid'] = array(
  262. 'label' => t('Node ID'),
  263. 'description' => t('The ID of the node of which this comment is a reply.'),
  264. 'type' => 'entity_reference_field',
  265. 'settings' => array('target_type' => 'node'),
  266. 'required' => TRUE,
  267. );
  268. $properties['langcode'] = array(
  269. 'label' => t('Language code'),
  270. 'description' => t('The comment language code.'),
  271. 'type' => 'language_field',
  272. );
  273. $properties['subject'] = array(
  274. 'label' => t('Subject'),
  275. 'description' => t('The comment title or subject.'),
  276. 'type' => 'string_field',
  277. );
  278. $properties['uid'] = array(
  279. 'label' => t('User ID'),
  280. 'description' => t('The user ID of the comment author.'),
  281. 'type' => 'entity_reference_field',
  282. 'settings' => array('target_type' => 'user'),
  283. );
  284. $properties['name'] = array(
  285. 'label' => t('Name'),
  286. 'description' => t("The comment author's name."),
  287. 'type' => 'string_field',
  288. );
  289. $properties['mail'] = array(
  290. 'label' => t('e-mail'),
  291. 'description' => t("The comment author's e-mail address."),
  292. 'type' => 'string_field',
  293. );
  294. $properties['homepage'] = array(
  295. 'label' => t('Homepage'),
  296. 'description' => t("The comment author's home page address."),
  297. 'type' => 'string_field',
  298. );
  299. $properties['hostname'] = array(
  300. 'label' => t('Hostname'),
  301. 'description' => t("The comment author's hostname."),
  302. 'type' => 'string_field',
  303. );
  304. $properties['created'] = array(
  305. 'label' => t('Created'),
  306. 'description' => t('The time that the comment was created.'),
  307. 'type' => 'integer_field',
  308. );
  309. $properties['changed'] = array(
  310. 'label' => t('Changed'),
  311. 'description' => t('The time that the comment was last edited.'),
  312. 'type' => 'integer_field',
  313. );
  314. $properties['status'] = array(
  315. 'label' => t('Publishing status'),
  316. 'description' => t('A boolean indicating whether the comment is published.'),
  317. 'type' => 'boolean_field',
  318. );
  319. $properties['thread'] = array(
  320. 'label' => t('Thread place'),
  321. 'description' => 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."),
  322. 'type' => 'string_field',
  323. );
  324. $properties['node_type'] = array(
  325. // @todo: The bundle property should be stored so it's queryable.
  326. 'label' => t('Node type'),
  327. 'description' => t("The comment node type."),
  328. 'type' => 'string_field',
  329. 'queryable' => FALSE,
  330. );
  331. $properties['new'] = array(
  332. 'label' => t('Comment new marker'),
  333. 'description' => t("The comment 'new' marker for the current user (0 read, 1 new, 2 updated)."),
  334. 'type' => 'integer_field',
  335. 'computed' => TRUE,
  336. 'class' => '\Drupal\comment\FieldNewItem',
  337. );
  338. return $properties;
  339. }
  340. }