node.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <?php
  2. /**
  3. * @file
  4. * Support for node destinations.
  5. */
  6. // TODO:
  7. // Make sure this works with updates, explicit destination keys
  8. /**
  9. * Destination class implementing migration into nodes.
  10. */
  11. class MigrateDestinationNode extends MigrateDestinationEntity {
  12. protected $bypassDestIdCheck = FALSE;
  13. static public function getKeySchema() {
  14. return array(
  15. 'nid' => array(
  16. 'type' => 'int',
  17. 'unsigned' => TRUE,
  18. 'description' => 'ID of destination node',
  19. ),
  20. );
  21. }
  22. /**
  23. * Return an options array for node destinations.
  24. *
  25. * @param string $language
  26. * Default language for nodes created via this destination class.
  27. * @param string $text_format
  28. * Default text format for nodes created via this destination class.
  29. */
  30. static public function options($language, $text_format) {
  31. return compact('language', 'text_format');
  32. }
  33. /**
  34. * Basic initialization
  35. *
  36. * @param string $bundle
  37. * A.k.a. the content type (page, article, etc.) of the node.
  38. * @param array $options
  39. * Options applied to nodes.
  40. */
  41. public function __construct($bundle, array $options = array()) {
  42. parent::__construct('node', $bundle, $options);
  43. }
  44. /**
  45. * Returns a list of fields available to be mapped for the node type (bundle)
  46. *
  47. * @param Migration $migration
  48. * Optionally, the migration containing this destination.
  49. * @return array
  50. * Keys: machine names of the fields (to be passed to addFieldMapping)
  51. * Values: Human-friendly descriptions of the fields.
  52. */
  53. public function fields($migration = NULL) {
  54. $fields = array();
  55. // First the core (node table) properties
  56. $fields['nid'] = t('Node: <a href="@doc">Existing node ID</a>',
  57. array('@doc' => 'http://drupal.org/node/1349696#nid'));
  58. $node_type = node_type_load($this->bundle);
  59. if ($node_type->has_title) {
  60. $fields['title'] = t('Node: <a href="@doc">',
  61. array('@doc' => 'http://drupal.org/node/1349696#title'))
  62. . $node_type->title_label . '</a>';
  63. }
  64. $fields['uid'] = t('<a href="@doc">Authored by (uid)</a>',
  65. array('@doc' => 'http://drupal.org/node/1349696#uid'));
  66. $fields['created'] = t('<a href="@doc">Created timestamp</a>',
  67. array('@doc' => 'http://drupal.org/node/1349696#created'));
  68. $fields['changed'] = t('<a href="@doc">Modified timestamp</a>',
  69. array('@doc' => 'http://drupal.org/node/1349696#changed'));
  70. $fields['status'] = t('<a href="@doc">Published</a>',
  71. array('@doc' => 'http://drupal.org/node/1349696#status'));
  72. $fields['promote'] = t('<a href="@doc">Promoted to front page</a>',
  73. array('@doc' => 'http://drupal.org/node/1349696#promote'));
  74. $fields['sticky'] = t('<a href="@doc">Sticky at top of lists</a>',
  75. array('@doc' => 'http://drupal.org/node/1349696#sticky'));
  76. $fields['revision'] = t('<a href="@doc">Create new revision</a>',
  77. array('@doc' => 'http://drupal.org/node/1349696#revision'));
  78. $fields['log'] = t('<a href="@doc">Revision Log message</a>',
  79. array('@doc' => 'http://drupal.org/node/1349696#log'));
  80. $fields['language'] = t('<a href="@doc">Language (fr, en, ...)</a>',
  81. array('@doc' => 'http://drupal.org/node/1349696#language'));
  82. $fields['tnid'] = t('<a href="@doc">The translation set id for this node</a>',
  83. array('@doc' => 'http://drupal.org/node/1349696#tnid'));
  84. $fields['translate'] = t('<a href="@doc">A boolean indicating whether this translation page needs to be updated</a>',
  85. array('@doc' => 'http://drupal.org/node/1349696#translate'));
  86. $fields['revision_uid'] = t('<a href="@doc">Modified (uid)</a>',
  87. array('@doc' => 'http://drupal.org/node/1349696#revision_uid'));
  88. $fields['is_new'] = t('Option: <a href="@doc">Indicates a new node with the specified nid should be created</a>',
  89. array('@doc' => 'http://drupal.org/node/1349696#is_new'));
  90. // Then add in anything provided by handlers
  91. $fields += migrate_handler_invoke_all('Entity', 'fields', $this->entityType, $this->bundle, $migration);
  92. $fields += migrate_handler_invoke_all('Node', 'fields', $this->entityType, $this->bundle, $migration);
  93. return $fields;
  94. }
  95. /**
  96. * Delete a batch of nodes at once.
  97. *
  98. * @param $nids
  99. * Array of node IDs to be deleted.
  100. */
  101. public function bulkRollback(array $nids) {
  102. migrate_instrument_start('node_delete_multiple');
  103. $this->prepareRollback($nids);
  104. node_delete_multiple($nids);
  105. $this->completeRollback($nids);
  106. migrate_instrument_stop('node_delete_multiple');
  107. }
  108. /**
  109. * Import a single node.
  110. *
  111. * @param $node
  112. * Node 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 (nid only in this case) of the node that was saved if
  117. * successful. FALSE on failure.
  118. */
  119. public function import(stdClass $node, stdClass $row) {
  120. // Updating previously-migrated content?
  121. $migration = Migration::currentMigration();
  122. if (isset($row->migrate_map_destid1) && !$this->bypassDestIdCheck) {
  123. // Make sure is_new is off
  124. $node->is_new = FALSE;
  125. if (isset($node->nid)) {
  126. if ($node->nid != $row->migrate_map_destid1) {
  127. throw new MigrateException(t("Incoming nid !nid and map destination nid !destid1 don't match",
  128. array('!nid' => $node->nid, '!destid1' => $row->migrate_map_destid1)));
  129. }
  130. }
  131. else {
  132. $node->nid = $row->migrate_map_destid1;
  133. }
  134. // Get the existing vid, tnid so updates don't generate notices
  135. $values = db_select('node', 'n')
  136. ->fields('n', array('vid', 'tnid'))
  137. ->condition('nid', $node->nid)
  138. ->execute()
  139. ->fetchAssoc();
  140. if (empty($values)) {
  141. throw new MigrateException(t("Incoming node ID !nid no longer exists",
  142. array('!nid' => $node->nid)));
  143. }
  144. $node->vid = $values['vid'];
  145. if (empty($node->tnid)) {
  146. $node->tnid = $values['tnid'];
  147. }
  148. }
  149. if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
  150. if (!isset($node->nid)) {
  151. throw new MigrateException(t('System-of-record is DESTINATION, but no destination nid provided'));
  152. }
  153. $old_node = node_load($node->nid);
  154. if (empty($old_node)) {
  155. throw new MigrateException(t('System-of-record is DESTINATION, but node !nid does not exist',
  156. array('!nid' => $node->nid)));
  157. }
  158. if (!isset($node->created)) {
  159. $node->created = $old_node->created;
  160. }
  161. if (!isset($node->vid)) {
  162. $node->vid = $old_node->vid;
  163. }
  164. if (!isset($node->status)) {
  165. $node->status = $old_node->status;
  166. }
  167. if (!isset($node->uid)) {
  168. $node->uid = $old_node->uid;
  169. }
  170. }
  171. if (!isset($node->type)) {
  172. // Default the type to our designated destination bundle (by doing this
  173. // conditionally, we permit some flexibility in terms of implementing
  174. // migrations which can affect more than one type).
  175. $node->type = $this->bundle;
  176. }
  177. // Set some required properties.
  178. if ($migration->getSystemOfRecord() == Migration::SOURCE) {
  179. if (empty($node->language)) {
  180. $node->language = $this->language;
  181. }
  182. // Apply defaults, allow standard node prepare hooks to fire.
  183. // node_object_prepare() will blow these away, so save them here and
  184. // stuff them in later if need be.
  185. if (isset($node->created)) {
  186. $created = MigrationBase::timestamp($node->created);
  187. }
  188. else {
  189. // To keep node_object_prepare() from choking
  190. $node->created = REQUEST_TIME;
  191. }
  192. if (isset($node->changed)) {
  193. $changed = MigrationBase::timestamp($node->changed);
  194. }
  195. if (isset($node->uid)) {
  196. $uid = $node->uid;
  197. }
  198. if (isset($node->revision)) {
  199. $revision = $node->revision;
  200. }
  201. node_object_prepare($node);
  202. if (isset($created)) {
  203. $node->created = $created;
  204. }
  205. // No point to resetting $node->changed here, node_save() will overwrite it
  206. if (isset($uid)) {
  207. $node->uid = $uid;
  208. }
  209. if (isset($revision)) {
  210. $node->revision = $revision;
  211. }
  212. }
  213. // Invoke migration prepare handlers
  214. $this->prepare($node, $row);
  215. if (!isset($node->revision)) {
  216. $node->revision = 0; // Saves disk space and writes. Can be overridden.
  217. }
  218. // Trying to update an existing node
  219. if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
  220. // Incoming data overrides existing data, so only copy non-existent fields
  221. foreach ($old_node as $field => $value) {
  222. // An explicit NULL in the source data means to wipe to old value (i.e.,
  223. // don't copy it over from $old_node)
  224. if (property_exists($node, $field) && $node->$field === NULL) {
  225. // Ignore this field
  226. }
  227. elseif (!isset($node->$field)) {
  228. $node->$field = $old_node->$field;
  229. }
  230. }
  231. }
  232. if (isset($node->nid) && !(isset($node->is_new) && $node->is_new)) {
  233. $updating = TRUE;
  234. }
  235. else {
  236. $updating = FALSE;
  237. }
  238. // Make sure that if is_new is not TRUE, it is not present.
  239. if (isset($node->is_new) && empty($node->is_new)) {
  240. unset($node->is_new);
  241. }
  242. // Validate field data prior to saving.
  243. MigrateDestinationEntity::fieldAttachValidate('node', $node);
  244. migrate_instrument_start('node_save');
  245. node_save($node);
  246. migrate_instrument_stop('node_save');
  247. if (isset($node->nid)) {
  248. if ($updating) {
  249. $this->numUpdated++;
  250. }
  251. else {
  252. $this->numCreated++;
  253. }
  254. // Unfortunately, http://drupal.org/node/722688 was not accepted, so fix
  255. // the changed timestamp
  256. if (isset($changed)) {
  257. db_update('node')
  258. ->fields(array('changed' => $changed))
  259. ->condition('nid', $node->nid)
  260. ->execute();
  261. $node->changed = $changed;
  262. }
  263. // Potentially fix uid and timestamp in node_revisions.
  264. $query = db_update('node_revision')
  265. ->condition('vid', $node->vid);
  266. if (isset($changed)) {
  267. $fields['timestamp'] = $changed;
  268. }
  269. $revision_uid = isset($node->revision_uid) ? $node->revision_uid : $node->uid;
  270. if ($revision_uid != $GLOBALS['user']->uid) {
  271. $fields['uid'] = $revision_uid;
  272. }
  273. if (!empty($fields)) {
  274. // We actually have something to update.
  275. $query->fields($fields);
  276. $query->execute();
  277. if (isset($changed)) {
  278. $node->timestamp = $changed;
  279. }
  280. }
  281. $return = array($node->nid);
  282. }
  283. else {
  284. $return = FALSE;
  285. }
  286. $this->complete($node, $row);
  287. return $return;
  288. }
  289. }
  290. /**
  291. * Allows you to import revisions.
  292. *
  293. * Adapted from http://www.darrenmothersele.com/blog/2012/07/16/migrating-node-revisions-drupal-7/
  294. *
  295. * Class MigrateDestinationNodeRevision
  296. *
  297. * @author darrenmothersele
  298. * @author cthos
  299. */
  300. class MigrateDestinationNodeRevision extends MigrateDestinationNode {
  301. /**
  302. * Basic initialization.
  303. *
  304. * @see parent::__construct
  305. *
  306. * @param string $bundle
  307. * A.k.a. the content type (page, article, etc.) of the node.
  308. * @param array $options
  309. * Options applied to nodes.
  310. */
  311. public function __construct($bundle, array $options = array()) {
  312. parent::__construct($bundle, $options);
  313. $this->bypassDestIdCheck = TRUE;
  314. }
  315. /**
  316. * Get key schema for the node revision destination.
  317. *
  318. * @see MigrateDestination::getKeySchema
  319. *
  320. * @return array
  321. * Returns the key schema.
  322. */
  323. static public function getKeySchema() {
  324. return array(
  325. 'vid' => array(
  326. 'type' => 'int',
  327. 'unsigned' => TRUE,
  328. 'description' => 'ID of destination node revision',
  329. ),
  330. );
  331. }
  332. /**
  333. * Returns additional fields on top of node destinations.
  334. *
  335. * @param string $migration
  336. * Active migration
  337. *
  338. * @return array
  339. * Fields.
  340. */
  341. public function fields($migration = NULL) {
  342. $fields = parent::fields($migration);
  343. $fields['vid'] = t('Node: <a href="@doc">Revision (vid)</a>', array('@doc' => 'http://drupal.org/node/1298724'));
  344. return $fields;
  345. }
  346. /**
  347. * Rolls back any versions that have been created.
  348. *
  349. * @param array $vids
  350. * Version ids to roll back.
  351. */
  352. public function bulkRollback(array $vids) {
  353. migrate_instrument_start('revision_delete_multiple');
  354. $this->prepareRollback($vids);
  355. $nids = array();
  356. foreach ($vids as $vid) {
  357. if ($revision = node_load(NULL, $vid)) {
  358. db_delete('node_revision')
  359. ->condition('vid', $revision->vid)
  360. ->execute();
  361. module_invoke_all('node_revision_delete', $revision);
  362. field_attach_delete_revision('node', $revision);
  363. $nids[$revision->nid] = $revision->nid;
  364. }
  365. }
  366. $this->completeRollback($vids);
  367. foreach ($nids as $nid) {
  368. $vid = db_select('node_revision', 'nr')->fields('nr', array('vid'))->condition('nid', $nid, '=')->execute()->fetchField();
  369. if (!empty($vid)) {
  370. db_update('node')->fields(array('vid' => $vid))->condition('nid', $nid, '=')->execute();
  371. }
  372. }
  373. migrate_instrument_stop('revision_delete_multiple');
  374. }
  375. /**
  376. * Overridden import method.
  377. *
  378. * This is done because parent::import will return the nid of the newly
  379. * created nodes. This is bad since the migrate_map_* table will have
  380. * nids instead of vids, which could cause a nightmare explosion on
  381. * rollback.
  382. *
  383. * @param stdClass $node
  384. * Populated entity.
  385. *
  386. * @param stdClass $row
  387. * Source information in object format.
  388. *
  389. * @return array|bool
  390. * Array with newly created vid, or FALSE on error.
  391. *
  392. * @throws MigrateException
  393. */
  394. public function import(stdClass $node, stdClass $row) {
  395. // We're importing revisions, this should be set.
  396. $node->revision = 1;
  397. if (empty($node->nid)) {
  398. throw new MigrateException(t('Missing incoming nid.'));
  399. }
  400. $original_updated = $this->numUpdated;
  401. parent::import($node, $row);
  402. // Reset num updated and increment created since new revision is always an update.
  403. $this->numUpdated = $original_updated;
  404. $this->numCreated++;
  405. if (empty($node->vid)) {
  406. return FALSE;
  407. }
  408. return array($node->vid);
  409. }
  410. }