comment.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <?php
  2. /**
  3. * @file
  4. * Support for comment destinations.
  5. */
  6. // TODO:
  7. // Make sure this works with updates, explicit destination keys
  8. /**
  9. * Destination class implementing migration into comments.
  10. */
  11. class MigrateDestinationComment extends MigrateDestinationEntity {
  12. static public function getKeySchema() {
  13. return array(
  14. 'cid' => array(
  15. 'type' => 'int',
  16. 'unsigned' => TRUE,
  17. 'description' => 'ID of destination entity',
  18. ),
  19. );
  20. }
  21. /**
  22. * Save the original setting of comment_maintain_node_statistics
  23. * @var boolean
  24. */
  25. protected $maintainNodeStatistics;
  26. /**
  27. * Return an options array for comment destinations.
  28. *
  29. * @param string $language
  30. * Default language for comments created via this destination class.
  31. * @param string $text_format
  32. * Default text format for comments created via this destination class.
  33. */
  34. static public function options($language, $text_format) {
  35. return compact('language', 'text_format');
  36. }
  37. /**
  38. * Basic initialization
  39. *
  40. * @param string $bundle
  41. * A.k.a. the content type (page, article, etc.) of the ... comment?.
  42. * @param array $options
  43. * Options applied to comments.
  44. */
  45. public function __construct($bundle, array $options = array()) {
  46. parent::__construct('comment', $bundle, $options);
  47. }
  48. /**
  49. * Returns a list of fields available to be mapped for comments attached to
  50. * a particular bundle (node type)
  51. *
  52. * @param Migration $migration
  53. * Optionally, the migration containing this destination.
  54. * @return array
  55. * Keys: machine names of the fields (to be passed to addFieldMapping)
  56. * Values: Human-friendly descriptions of the fields.
  57. */
  58. public function fields($migration = NULL) {
  59. $fields = array();
  60. // First the core (comment table) properties
  61. $fields['cid'] = t('<a href="@doc">Existing comment ID</a>',
  62. array('@doc' => 'http://drupal.org/node/1349714#cid'));
  63. $fields['nid'] = t('<a href="@doc">Node (by Drupal ID)</a>',
  64. array('@doc' => 'http://drupal.org/node/1349714#nid'));
  65. $fields['uid'] = t('<a href="@doc">User (by Drupal ID)</a>',
  66. array('@doc' => 'http://drupal.org/node/1349714#uid'));
  67. $fields['pid'] = t('<a href="@doc">Parent (by Drupal ID)</a>',
  68. array('@doc' => 'http://drupal.org/node/1349714#pid'));
  69. $fields['subject'] = t('<a href="@doc">Subject</a>',
  70. array('@doc' => 'http://drupal.org/node/1349714#subject'));
  71. $fields['created'] = t('<a href="@doc">Created timestamp</a>',
  72. array('@doc' => 'http://drupal.org/node/1349714#created'));
  73. $fields['changed'] = t('<a href="@doc">Modified timestamp</a>',
  74. array('@doc' => 'http://drupal.org/node/1349714#changed'));
  75. $fields['status'] = t('<a href="@doc">Status</a>',
  76. array('@doc' => 'http://drupal.org/node/1349714#status'));
  77. $fields['hostname'] = t('<a href="@doc">Hostname/IP address</a>',
  78. array('@doc' => 'http://drupal.org/node/1349714#hostname'));
  79. $fields['name'] = t('<a href="@doc">User name (not username)</a>',
  80. array('@doc' => 'http://drupal.org/node/1349714#name'));
  81. $fields['mail'] = t('<a href="@doc">Email address</a>',
  82. array('@doc' => 'http://drupal.org/node/1349714#mail'));
  83. $fields['homepage'] = t('<a href="@doc">Homepage</a>',
  84. array('@doc' => 'http://drupal.org/node/1349714#homepage'));
  85. $fields['language'] = t('<a href="@doc">Language</a>',
  86. array('@doc' => 'http://drupal.org/node/1349714#language'));
  87. $fields['thread'] = t('<a href="@doc">Thread</a>',
  88. array('@doc' => 'http://drupal.org/node/1349714#thread'));
  89. // Then add in anything provided by handlers
  90. $fields += migrate_handler_invoke_all('Entity', 'fields', $this->entityType, $this->bundle, $migration);
  91. $fields += migrate_handler_invoke_all('Comment', 'fields', $this->entityType, $this->bundle, $migration);
  92. return $fields;
  93. }
  94. /**
  95. * Delete a batch of comments at once.
  96. *
  97. * @param $cids
  98. * Array of comment IDs to be deleted.
  99. */
  100. public function bulkRollback(array $cids) {
  101. migrate_instrument_start('comment_delete_multiple');
  102. $this->prepareRollback($cids);
  103. $result = comment_delete_multiple($cids);
  104. $this->completeRollback($cids);
  105. migrate_instrument_stop('comment_delete_multiple');
  106. return $result;
  107. }
  108. /**
  109. * Import a single comment.
  110. *
  111. * @param $comment
  112. * Comment object to build. Prefilled with any fields mapped in the Migration.
  113. * @param $row
  114. * Raw source data object - passed through to prepare/complete handlers.
  115. * @return array
  116. * Array of key fields (cid only in this case) of the comment that was saved if
  117. * successful. FALSE on failure.
  118. */
  119. public function import(stdClass $comment, stdClass $row) {
  120. $migration = Migration::currentMigration();
  121. // Updating previously-migrated content?
  122. if (isset($row->migrate_map_destid1)) {
  123. if (isset($comment->cid)) {
  124. if ($comment->cid != $row->migrate_map_destid1) {
  125. throw new MigrateException(t("Incoming cid !cid and map destination nid !destid1 don't match",
  126. array('!cid' => $comment->cid, '!destid1' => $row->migrate_map_destid1)));
  127. }
  128. }
  129. else {
  130. $comment->cid = $row->migrate_map_destid1;
  131. }
  132. }
  133. // Fix up timestamps
  134. if (isset($comment->created)) {
  135. $comment->created = MigrationBase::timestamp($comment->created);
  136. }
  137. if (isset($comment->changed)) {
  138. $comment->changed = MigrationBase::timestamp($comment->changed);
  139. }
  140. if (!isset($comment->node_type)) {
  141. $comment->node_type = $this->bundle;
  142. }
  143. if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
  144. if (!isset($comment->cid)) {
  145. throw new MigrateException(t('System-of-record is DESTINATION, but no destination cid provided'));
  146. }
  147. $rawcomment = $comment;
  148. $old_comment = comment_load($comment->cid);
  149. if (empty($old_comment)) {
  150. throw new MigrateException(t('System-of-record is DESTINATION, but commend !cid does not exist',
  151. array('!cid' => $comment->cid)));
  152. }
  153. if (!isset($comment->nid)) {
  154. $comment->nid = $old_comment->nid;
  155. }
  156. if (!isset($comment->created)) {
  157. $comment->created = $old_comment->created;
  158. }
  159. if (!isset($comment->changed)) {
  160. $comment->changed = $old_comment->changed;
  161. }
  162. $this->prepare($comment, $row);
  163. foreach ($rawcomment as $field => $value) {
  164. $old_comment->$field = $comment->$field;
  165. }
  166. $comment = $old_comment;
  167. }
  168. else {
  169. // Set some default properties.
  170. $defaults = array(
  171. 'language' => $this->language,
  172. 'node_type' => $this->bundle,
  173. 'subject' => '',
  174. 'status' => COMMENT_PUBLISHED,
  175. 'uid' => 0,
  176. 'cid' => 0,
  177. 'pid' => 0,
  178. );
  179. foreach ($defaults as $field => $value) {
  180. if (!isset($comment->$field)) {
  181. $comment->$field = $value;
  182. }
  183. }
  184. $this->prepare($comment, $row);
  185. // Make sure we have a nid
  186. if (!isset($comment->nid) || !$comment->nid) {
  187. throw new MigrateException(t('No node ID provided for comment'));
  188. }
  189. // comment_save() hardcodes hostname, so if we're trying to set it we
  190. // need to save it and apply it after
  191. if (isset($comment->hostname)) {
  192. $hostname = $comment->hostname;
  193. }
  194. }
  195. if (isset($comment->cid) && $comment->cid) {
  196. $updating = TRUE;
  197. }
  198. else {
  199. $updating = FALSE;
  200. }
  201. // Validate field data prior to saving.
  202. MigrateDestinationEntity::fieldAttachValidate('comment', $comment);
  203. migrate_instrument_start('comment_save');
  204. comment_save($comment);
  205. migrate_instrument_stop('comment_save');
  206. if (isset($hostname) && isset($comment->cid) && $comment->cid > 0) {
  207. db_update('comment')
  208. ->fields(array('hostname' => $hostname))
  209. ->condition('cid', $comment->cid)
  210. ->execute();
  211. }
  212. $this->complete($comment, $row);
  213. if (isset($comment->cid) && $comment->cid > 0) {
  214. $return = array($comment->cid);
  215. if ($updating) {
  216. $this->numUpdated++;
  217. }
  218. else {
  219. $this->numCreated++;
  220. }
  221. }
  222. else {
  223. $return = FALSE;
  224. }
  225. return $return;
  226. }
  227. /**
  228. * Implements MigrateDestination::preImport().
  229. */
  230. public function preImport() {
  231. $this->disableStatistics();
  232. }
  233. /**
  234. * Implements MigrateDestination::postImport().
  235. */
  236. public function postImport() {
  237. $this->enableStatistics();
  238. }
  239. /**
  240. * Implements MigrateDestination::preRollback().
  241. */
  242. public function preRollback() {
  243. $this->disableStatistics();
  244. }
  245. /**
  246. * Implements MigrateDestination::postRollback().
  247. */
  248. public function postRollback() {
  249. $this->enableStatistics();
  250. }
  251. /**
  252. * Updating node statistics on every comment imported or rolled back is
  253. * expensive. We disable node statistics while performing imports and rollbacks,
  254. * then re-enable and compute them in bulk when done.
  255. */
  256. protected function disableStatistics() {
  257. // If maintaining node statistics is enabled, temporarily disable it
  258. $this->maintainNodeStatistics =
  259. variable_get('comment_maintain_node_statistics', TRUE);
  260. if ($this->maintainNodeStatistics) {
  261. $GLOBALS['conf']['comment_maintain_node_statistics'] = FALSE;
  262. }
  263. }
  264. /**
  265. * Re-enable and recompute node statistics after an import or rollback
  266. * operation.
  267. */
  268. protected function enableStatistics() {
  269. // If originally enabled, re-enable and rebuild the stats
  270. if ($this->maintainNodeStatistics) {
  271. $GLOBALS['conf']['comment_maintain_node_statistics'] = TRUE;
  272. // Copied from devel_rebuild_node_comment_statistics
  273. // Empty table
  274. db_truncate('node_comment_statistics')->execute();
  275. // DBTNG. IGNORE keyword is not compatible with Postgres. SQLite?
  276. switch (db_driver()) {
  277. case 'pgsql':
  278. // We still want to run this under Postgres. On the very rare occasion
  279. // when we have 2 comments on the same node with the same timestamp
  280. // we will lose data.
  281. $sql = "
  282. INSERT INTO {node_comment_statistics} (nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) (
  283. SELECT c.nid, c.cid, c.created, c.name, c.uid, c2.comment_count
  284. FROM {comment} c
  285. JOIN (
  286. SELECT c.nid, MAX(c.created) AS created, COUNT(*) AS comment_count
  287. FROM {comment} c
  288. WHERE status=:published
  289. GROUP BY c.nid
  290. ) AS c2 ON c.nid = c2.nid AND c.created=c2.created
  291. )";
  292. break;
  293. default:
  294. $sql = "
  295. INSERT IGNORE INTO {node_comment_statistics} (nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) (
  296. SELECT c.nid, c.cid, c.created, c.name, c.uid, c2.comment_count
  297. FROM {comment} c
  298. JOIN (
  299. SELECT c.nid, MAX(c.created) AS created, COUNT(*) AS comment_count
  300. FROM {comment} c
  301. WHERE status=:published
  302. GROUP BY c.nid
  303. ) AS c2 ON c.nid = c2.nid AND c.created=c2.created
  304. )";
  305. }
  306. try {
  307. db_query($sql, array(':published' => COMMENT_PUBLISHED));
  308. }
  309. catch (Exception $e) {
  310. // Our edge case has been hit. A Postgres migration has likely just
  311. // lost data. Let the user know.
  312. Migration::displayMessage(t('Failed to update node comment statistics: !message',
  313. array('!message' => $e->getMessage())
  314. ));
  315. }
  316. // Insert records into the node_comment_statistics for nodes that are missing.
  317. $query = db_select('node', 'n');
  318. $query->leftJoin('node_comment_statistics', 'ncs', 'ncs.nid = n.nid');
  319. $query->addField('n', 'created', 'last_comment_timestamp');
  320. $query->addField('n', 'uid', 'last_comment_uid');
  321. $query->addField('n', 'nid');
  322. $query->addExpression('0', 'comment_count');
  323. $query->addExpression('NULL', 'last_comment_name');
  324. $query->isNull('ncs.comment_count');
  325. db_insert('node_comment_statistics')
  326. ->from($query)
  327. ->execute();
  328. }
  329. }
  330. }
  331. class MigrateCommentNodeHandler extends MigrateDestinationHandler {
  332. public function __construct() {
  333. $this->registerTypes(array('node'));
  334. }
  335. /**
  336. * Implementation of MigrateDestinationHandler::fields().
  337. */
  338. public function fields($entity_type, $bundle, $migration = NULL) {
  339. $fields = array();
  340. $fields['comment'] = t('Whether comments may be posted to the node');
  341. return $fields;
  342. }
  343. }