FeedsNodeProcessor.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. /**
  3. * @file
  4. * Class definition of FeedsNodeProcessor.
  5. */
  6. /**
  7. * Option for handling content in Drupal but not in source data (unpublish
  8. * instead of skip/delete).
  9. */
  10. define('FEEDS_UNPUBLISH_NON_EXISTENT', 'unpublish');
  11. /**
  12. * Creates nodes from feed items.
  13. */
  14. class FeedsNodeProcessor extends FeedsProcessor {
  15. /**
  16. * Define entity type.
  17. */
  18. public function entityType() {
  19. return 'node';
  20. }
  21. /**
  22. * Implements parent::entityInfo().
  23. */
  24. protected function entityInfo() {
  25. $info = parent::entityInfo();
  26. $info['label plural'] = t('Nodes');
  27. return $info;
  28. }
  29. /**
  30. * Creates a new node in memory and returns it.
  31. */
  32. protected function newEntity(FeedsSource $source) {
  33. $node = parent::newEntity($source);
  34. $node->type = $this->bundle();
  35. $node->changed = REQUEST_TIME;
  36. $node->created = REQUEST_TIME;
  37. $node->is_new = TRUE;
  38. node_object_prepare($node);
  39. // Populate properties that are set by node_object_prepare().
  40. $node->log = 'Created by FeedsNodeProcessor';
  41. $node->uid = $this->config['author'];
  42. return $node;
  43. }
  44. /**
  45. * Loads an existing node.
  46. *
  47. * If the update existing method is not FEEDS_UPDATE_EXISTING, only the node
  48. * table will be loaded, foregoing the node_load API for better performance.
  49. *
  50. * @todo Reevaluate the use of node_object_prepare().
  51. */
  52. protected function entityLoad(FeedsSource $source, $nid) {
  53. $node = parent::entityLoad($source, $nid);
  54. if ($this->config['update_existing'] != FEEDS_UPDATE_EXISTING) {
  55. $node->uid = $this->config['author'];
  56. }
  57. node_object_prepare($node);
  58. // Workaround for issue #1247506. See #1245094 for backstory.
  59. if (!empty($node->menu)) {
  60. // If the node has a menu item(with a valid mlid) it must be flagged
  61. // 'enabled'.
  62. $node->menu['enabled'] = (int) (bool) $node->menu['mlid'];
  63. }
  64. // Populate properties that are set by node_object_prepare().
  65. if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
  66. $node->log = 'Updated by FeedsNodeProcessor';
  67. }
  68. else {
  69. $node->log = 'Replaced by FeedsNodeProcessor';
  70. }
  71. return $node;
  72. }
  73. /**
  74. * Check that the user has permission to save a node.
  75. */
  76. protected function entitySaveAccess($entity) {
  77. // The check will be skipped for anonymous nodes.
  78. if ($this->config['authorize'] && !empty($entity->uid)) {
  79. $author = user_load($entity->uid);
  80. // If the uid was mapped directly, rather than by email or username, it
  81. // could be invalid.
  82. if (!$author) {
  83. $message = 'User %uid is not a valid user.';
  84. throw new FeedsAccessException(t($message, array('%uid' => $entity->uid)));
  85. }
  86. if (empty($entity->nid) || !empty($entity->is_new)) {
  87. $op = 'create';
  88. $access = node_access($op, $entity->type, $author);
  89. }
  90. else {
  91. $op = 'update';
  92. $access = node_access($op, $entity, $author);
  93. }
  94. if (!$access) {
  95. $message = t('The user %name is not authorized to %op content of type %content_type. To import this item, either the user "@name" (author of the item) must be given the permission to @op content of type @content_type, or the option "Authorize" on the Node processor settings must be turned off.', array(
  96. '%name' => $author->name,
  97. '%op' => $op,
  98. '%content_type' => $entity->type,
  99. '@name' => $author->name,
  100. '@op' => $op,
  101. '@content_type' => $entity->type,
  102. ));
  103. throw new FeedsAccessException($message);
  104. }
  105. }
  106. }
  107. /**
  108. * Validates a node.
  109. */
  110. protected function entityValidate($entity) {
  111. parent::entityValidate($entity);
  112. if (!isset($entity->uid) || !is_numeric($entity->uid)) {
  113. $entity->uid = $this->config['author'];
  114. }
  115. }
  116. /**
  117. * Save a node.
  118. */
  119. public function entitySave($entity) {
  120. node_save($entity);
  121. }
  122. /**
  123. * Delete a series of nodes.
  124. */
  125. protected function entityDeleteMultiple($nids) {
  126. node_delete_multiple($nids);
  127. }
  128. /**
  129. * Overrides parent::expiryQuery().
  130. */
  131. protected function expiryQuery(FeedsSource $source, $time) {
  132. $select = parent::expiryQuery($source, $time);
  133. $select->condition('e.created', REQUEST_TIME - $time, '<');
  134. return $select;
  135. }
  136. /**
  137. * Return expiry time.
  138. */
  139. public function expiryTime() {
  140. return $this->config['expire'];
  141. }
  142. /**
  143. * Override parent::configDefaults().
  144. */
  145. public function configDefaults() {
  146. return array(
  147. 'expire' => FEEDS_EXPIRE_NEVER,
  148. 'author' => 0,
  149. 'authorize' => TRUE,
  150. ) + parent::configDefaults();
  151. }
  152. /**
  153. * Override parent::configForm().
  154. */
  155. public function configForm(&$form_state) {
  156. $form = parent::configForm($form_state);
  157. $author = user_load($this->config['author']);
  158. $form['author'] = array(
  159. '#type' => 'textfield',
  160. '#title' => t('Author'),
  161. '#description' => t('Select the author of the nodes to be created - leave empty to assign "anonymous".'),
  162. '#autocomplete_path' => 'user/autocomplete',
  163. '#default_value' => empty($author->name) ? 'anonymous' : check_plain($author->name),
  164. );
  165. $form['authorize'] = array(
  166. '#type' => 'checkbox',
  167. '#title' => t('Authorize'),
  168. '#description' => t('Check that the author has permission to create the node.'),
  169. '#default_value' => $this->config['authorize'],
  170. );
  171. $period = drupal_map_assoc(array(FEEDS_EXPIRE_NEVER, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2592000, 2592000 * 3, 2592000 * 6, 31536000), 'feeds_format_expire');
  172. $form['expire'] = array(
  173. '#type' => 'select',
  174. '#title' => t('Expire nodes'),
  175. '#options' => $period,
  176. '#description' => t("Select after how much time nodes should be deleted. The node's published date will be used for determining the node's age, see Mapping settings."),
  177. '#default_value' => $this->config['expire'],
  178. );
  179. // Add on the "Unpublish" option for nodes, update wording.
  180. if (isset($form['update_non_existent'])) {
  181. $form['update_non_existent']['#options'][FEEDS_UNPUBLISH_NON_EXISTENT] = t('Unpublish non-existent nodes');
  182. }
  183. return $form;
  184. }
  185. /**
  186. * Override parent::configFormValidate().
  187. */
  188. public function configFormValidate(&$values) {
  189. if ($author = user_load_by_name($values['author'])) {
  190. $values['author'] = $author->uid;
  191. }
  192. else {
  193. $values['author'] = 0;
  194. }
  195. }
  196. /**
  197. * Reschedule if expiry time changes.
  198. */
  199. public function configFormSubmit(&$values) {
  200. if ($this->config['expire'] != $values['expire']) {
  201. feeds_reschedule($this->id);
  202. }
  203. parent::configFormSubmit($values);
  204. }
  205. /**
  206. * Override setTargetElement to operate on a target item that is a node.
  207. */
  208. public function setTargetElement(FeedsSource $source, $target_node, $target_element, $value) {
  209. switch ($target_element) {
  210. case 'created':
  211. $target_node->created = feeds_to_unixtime($value, REQUEST_TIME);
  212. break;
  213. case 'changed':
  214. // The 'changed' value will be set on the node in feeds_node_presave().
  215. // This is because node_save() always overwrites this value (though
  216. // before invoking hook_node_presave()).
  217. $target_node->feeds_item->node_changed = feeds_to_unixtime($value, REQUEST_TIME);
  218. break;
  219. case 'feeds_source':
  220. // Get the class of the feed node importer's fetcher and set the source
  221. // property. See feeds_node_update() how $node->feeds gets stored.
  222. if ($id = feeds_get_importer_id($this->bundle())) {
  223. $class = get_class(feeds_importer($id)->fetcher);
  224. $target_node->feeds[$class]['source'] = $value;
  225. // This effectively suppresses 'import on submission' feature.
  226. // See feeds_node_insert().
  227. $target_node->feeds['suppress_import'] = TRUE;
  228. }
  229. break;
  230. case 'user_name':
  231. if ($user = user_load_by_name($value)) {
  232. $target_node->uid = $user->uid;
  233. }
  234. break;
  235. case 'user_mail':
  236. if ($user = user_load_by_mail($value)) {
  237. $target_node->uid = $user->uid;
  238. }
  239. break;
  240. default:
  241. parent::setTargetElement($source, $target_node, $target_element, $value);
  242. break;
  243. }
  244. }
  245. /**
  246. * Return available mapping targets.
  247. */
  248. public function getMappingTargets() {
  249. $type = node_type_get_type($this->bundle());
  250. $targets = parent::getMappingTargets();
  251. if ($type && $type->has_title) {
  252. $targets['title'] = array(
  253. 'name' => t('Title'),
  254. 'description' => t('The title of the node.'),
  255. 'optional_unique' => TRUE,
  256. );
  257. }
  258. $targets['nid'] = array(
  259. 'name' => t('Node ID'),
  260. 'description' => t('The nid of the node. NOTE: use this feature with care, node ids are usually assigned by Drupal.'),
  261. 'optional_unique' => TRUE,
  262. );
  263. $targets['uid'] = array(
  264. 'name' => t('User ID'),
  265. 'description' => t('The Drupal user ID of the node author.'),
  266. );
  267. $targets['user_name'] = array(
  268. 'name' => t('Username'),
  269. 'description' => t('The Drupal username of the node author.'),
  270. );
  271. $targets['user_mail'] = array(
  272. 'name' => t('User email'),
  273. 'description' => t('The email address of the node author.'),
  274. );
  275. $targets['status'] = array(
  276. 'name' => t('Published status'),
  277. 'description' => t('Whether a node is published or not. 1 stands for published, 0 for not published.'),
  278. );
  279. $targets['created'] = array(
  280. 'name' => t('Published date'),
  281. 'description' => t('The UNIX time when a node has been published.'),
  282. );
  283. $targets['changed'] = array(
  284. 'name' => t('Updated date'),
  285. 'description' => t('The Unix timestamp when a node has been last updated.'),
  286. );
  287. $targets['promote'] = array(
  288. 'name' => t('Promoted to front page'),
  289. 'description' => t('Boolean value, whether or not node is promoted to front page. (1 = promoted, 0 = not promoted)'),
  290. );
  291. $targets['sticky'] = array(
  292. 'name' => t('Sticky'),
  293. 'description' => t('Boolean value, whether or not node is sticky at top of lists. (1 = sticky, 0 = not sticky)'),
  294. );
  295. // Include language field if Locale module is enabled.
  296. if (module_exists('locale')) {
  297. $targets['language'] = array(
  298. 'name' => t('Language'),
  299. 'description' => t('The two-character language code of the node.'),
  300. );
  301. }
  302. // Include comment field if Comment module is enabled.
  303. if (module_exists('comment')) {
  304. $targets['comment'] = array(
  305. 'name' => t('Comments'),
  306. 'description' => t('Whether comments are allowed on this node: 0 = no, 1 = read only, 2 = read/write.'),
  307. );
  308. }
  309. // If the target content type is a Feed node, expose its source field.
  310. if ($id = feeds_get_importer_id($this->bundle())) {
  311. $name = feeds_importer($id)->config['name'];
  312. $targets['feeds_source'] = array(
  313. 'name' => t('Feed source'),
  314. 'description' => t('The content type created by this processor is a Feed Node, it represents a source itself. Depending on the fetcher selected on the importer "@importer", this field is expected to be for example a URL or a path to a file.', array('@importer' => $name)),
  315. 'optional_unique' => TRUE,
  316. );
  317. }
  318. $this->getHookTargets($targets);
  319. return $targets;
  320. }
  321. /**
  322. * Get nid of an existing feed item node if available.
  323. */
  324. protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
  325. if ($nid = parent::existingEntityId($source, $result)) {
  326. return $nid;
  327. }
  328. // Iterate through all unique targets and test whether they do already
  329. // exist in the database.
  330. foreach ($this->uniqueTargets($source, $result) as $target => $value) {
  331. switch ($target) {
  332. case 'nid':
  333. $nid = db_query("SELECT nid FROM {node} WHERE nid = :nid", array(':nid' => $value))->fetchField();
  334. break;
  335. case 'title':
  336. $nid = db_query("SELECT nid FROM {node} WHERE title = :title AND type = :type", array(':title' => $value, ':type' => $this->bundle()))->fetchField();
  337. break;
  338. case 'feeds_source':
  339. if ($id = feeds_get_importer_id($this->bundle())) {
  340. $nid = db_query("SELECT fs.feed_nid FROM {node} n JOIN {feeds_source} fs ON n.nid = fs.feed_nid WHERE fs.id = :id AND fs.source = :source", array(':id' => $id, ':source' => $value))->fetchField();
  341. }
  342. break;
  343. }
  344. if ($nid) {
  345. // Return with the first nid found.
  346. return $nid;
  347. }
  348. }
  349. return 0;
  350. }
  351. /**
  352. * Overrides FeedsProcessor::clean().
  353. *
  354. * Allow unpublish instead of delete.
  355. *
  356. * @param FeedsState $state
  357. * The FeedsState object for the given stage.
  358. */
  359. protected function clean(FeedsState $state) {
  360. // Delegate to parent if not unpublishing or option not set.
  361. if (!isset($this->config['update_non_existent']) || $this->config['update_non_existent'] != FEEDS_UNPUBLISH_NON_EXISTENT) {
  362. return parent::clean($state);
  363. }
  364. $total = count($state->removeList);
  365. if ($total) {
  366. $nodes = node_load_multiple($state->removeList);
  367. foreach ($nodes as &$node) {
  368. $this->loadItemInfo($node);
  369. // Update the hash value of the feed item to ensure that the item gets
  370. // updated in case it reappears in the feed.
  371. $node->feeds_item->hash = $this->config['update_non_existent'];
  372. node_unpublish_action($node);
  373. node_save($node);
  374. $state->unpublished++;
  375. }
  376. }
  377. }
  378. }