FeedsProcessor.inc 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294
  1. <?php
  2. /**
  3. * @file
  4. * Contains FeedsProcessor and related classes.
  5. */
  6. // Insert mode for new items.
  7. define('FEEDS_SKIP_NEW', 0);
  8. define('FEEDS_INSERT_NEW', 1);
  9. // Update mode for existing items.
  10. define('FEEDS_SKIP_EXISTING', 0);
  11. define('FEEDS_REPLACE_EXISTING', 1);
  12. define('FEEDS_UPDATE_EXISTING', 2);
  13. // Options for handling content in Drupal but not in source data.
  14. define('FEEDS_SKIP_NON_EXISTENT', 'skip');
  15. define('FEEDS_DELETE_NON_EXISTENT', 'delete');
  16. // Default limit for creating items on a page load, not respected by all
  17. // processors.
  18. define('FEEDS_PROCESS_LIMIT', 50);
  19. /**
  20. * Thrown if a validation fails.
  21. */
  22. class FeedsValidationException extends Exception {}
  23. /**
  24. * Thrown if a an access check fails.
  25. */
  26. class FeedsAccessException extends Exception {}
  27. /**
  28. * Abstract class, defines interface for processors.
  29. */
  30. abstract class FeedsProcessor extends FeedsPlugin {
  31. /**
  32. * Implements FeedsPlugin::pluginType().
  33. */
  34. public function pluginType() {
  35. return 'processor';
  36. }
  37. /**
  38. * @defgroup entity_api_wrapper Entity API wrapper.
  39. */
  40. /**
  41. * Entity type this processor operates on.
  42. */
  43. public abstract function entityType();
  44. /**
  45. * Bundle type this processor operates on.
  46. *
  47. * Defaults to the entity type for entities that do not define bundles.
  48. *
  49. * @return string|NULL
  50. * The bundle type this processor operates on, or NULL if it is undefined.
  51. */
  52. public function bundle() {
  53. return $this->config['bundle'];
  54. }
  55. /**
  56. * Provides a list of bundle options for use in select lists.
  57. *
  58. * @return array
  59. * A keyed array of bundle => label.
  60. */
  61. public function bundleOptions() {
  62. $options = array();
  63. foreach (field_info_bundles($this->entityType()) as $bundle => $info) {
  64. if (!empty($info['label'])) {
  65. $options[$bundle] = $info['label'];
  66. }
  67. else {
  68. $options[$bundle] = $bundle;
  69. }
  70. }
  71. return $options;
  72. }
  73. /**
  74. * Create a new entity.
  75. *
  76. * @param FeedsSource $source
  77. * The feeds source that spawns this entity.
  78. *
  79. * @return object
  80. * A new entity object.
  81. */
  82. protected function newEntity(FeedsSource $source) {
  83. $entity = new stdClass();
  84. $info = $this->entityInfo();
  85. if (!empty($info['entity keys']['language'])) {
  86. $entity->{$info['entity keys']['language']} = $this->entityLanguage();
  87. }
  88. return $entity;
  89. }
  90. /**
  91. * Load an existing entity.
  92. *
  93. * @param $source
  94. * The feeds source that spawns this entity.
  95. * @param $entity_id
  96. * The unique id of the entity that should be loaded.
  97. *
  98. * @return
  99. * A new entity object.
  100. *
  101. * @todo We should be able to batch load these, if we found all of the
  102. * existing ids first.
  103. */
  104. protected function entityLoad(FeedsSource $source, $entity_id) {
  105. $info = $this->entityInfo();
  106. if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
  107. $entities = entity_load($this->entityType(), array($entity_id));
  108. $entity = reset($entities);
  109. }
  110. else {
  111. $args = array(':entity_id' => $entity_id);
  112. $table = db_escape_table($info['base table']);
  113. $key = db_escape_field($info['entity keys']['id']);
  114. $entity = db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject();
  115. }
  116. if ($entity && !empty($info['entity keys']['language'])) {
  117. $entity->{$info['entity keys']['language']} = $this->entityLanguage();
  118. }
  119. return $entity;
  120. }
  121. /**
  122. * Validates an entity.
  123. *
  124. * @throws FeedsValidationException $e
  125. * Thrown if validation fails.
  126. */
  127. protected function entityValidate($entity) {
  128. $info = $this->entityInfo();
  129. if (empty($info['entity keys']['language'])) {
  130. return;
  131. }
  132. // Ensure that a valid language is always set.
  133. $key = $info['entity keys']['language'];
  134. $languages = language_list('enabled');
  135. if (empty($entity->$key) || !isset($languages[1][$entity->$key])) {
  136. $entity->$key = $this->entityLanguage();
  137. }
  138. }
  139. /**
  140. * Access check for saving an enity.
  141. *
  142. * @param $entity
  143. * Entity to be saved.
  144. *
  145. * @throws FeedsAccessException $e
  146. * If the access check fails.
  147. */
  148. protected function entitySaveAccess($entity) {}
  149. /**
  150. * Save an entity.
  151. *
  152. * @param $entity
  153. * Entity to be saved.
  154. */
  155. protected abstract function entitySave($entity);
  156. /**
  157. * Delete a series of entities.
  158. *
  159. * @param $entity_ids
  160. * Array of unique identity ids to be deleted.
  161. */
  162. protected abstract function entityDeleteMultiple($entity_ids);
  163. /**
  164. * Wrap entity_get_info() into a method so that extending classes can override
  165. * it and more entity information. Allowed additional keys:
  166. *
  167. * 'label plural' ... the plural label of an entity type.
  168. */
  169. protected function entityInfo() {
  170. $info = entity_get_info($this->entityType());
  171. // Entity module has defined the plural label in "plural label" instead of
  172. // "label plural". So if "plural label" is defined, this will have priority
  173. // over "label plural".
  174. if (isset($info['plural label'])) {
  175. $info['label plural'] = $info['plural label'];
  176. }
  177. return $info;
  178. }
  179. /**
  180. * Returns the current language for entities.
  181. *
  182. * This checks if the configuration value is valid.
  183. *
  184. * @return string
  185. * The current language code.
  186. */
  187. protected function entityLanguage() {
  188. if (!module_exists('locale')) {
  189. // language_list() may return languages even if the locale module is
  190. // disabled. See https://www.drupal.org/node/173227 why.
  191. // When the locale module is disabled, there are no selectable languages
  192. // in the UI, so the content should be imported in LANGUAGE_NONE.
  193. return LANGUAGE_NONE;
  194. }
  195. $languages = language_list('enabled');
  196. return isset($languages[1][$this->config['language']]) ? $this->config['language'] : LANGUAGE_NONE;
  197. }
  198. /**
  199. * @}
  200. */
  201. /**
  202. * Process the result of the parsing stage.
  203. *
  204. * @param FeedsSource $source
  205. * Source information about this import.
  206. * @param FeedsParserResult $parser_result
  207. * The result of the parsing stage.
  208. */
  209. public function process(FeedsSource $source, FeedsParserResult $parser_result) {
  210. $state = $source->state(FEEDS_PROCESS);
  211. if (!isset($state->removeList) && $parser_result->items) {
  212. $this->initEntitiesToBeRemoved($source, $state);
  213. }
  214. $skip_new = $this->config['insert_new'] == FEEDS_SKIP_NEW;
  215. $skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING;
  216. while ($item = $parser_result->shiftItem()) {
  217. // Check if this item already exists.
  218. $entity_id = $this->existingEntityId($source, $parser_result);
  219. // If it's included in the feed, it must not be removed on clean.
  220. if ($entity_id) {
  221. unset($state->removeList[$entity_id]);
  222. }
  223. module_invoke_all('feeds_before_update', $source, $item, $entity_id);
  224. // If it exists, and we are not updating, or if it does not exist, and we
  225. // are not inserting, pass onto the next item.
  226. if (($entity_id && $skip_existing) || (!$entity_id && $skip_new)) {
  227. continue;
  228. }
  229. try {
  230. $hash = $this->hash($item);
  231. $changed = $hash !== $this->getHash($entity_id);
  232. // Do not proceed if the item exists, has not changed, and we're not
  233. // forcing the update.
  234. if ($entity_id && !$changed && !$this->config['skip_hash_check']) {
  235. continue;
  236. }
  237. // Load an existing entity.
  238. if ($entity_id) {
  239. $entity = $this->entityLoad($source, $entity_id);
  240. // The feeds_item table is always updated with the info for the most
  241. // recently processed entity. The only carryover is the entity_id.
  242. $this->newItemInfo($entity, $source->feed_nid, $hash);
  243. $entity->feeds_item->entity_id = $entity_id;
  244. $entity->feeds_item->is_new = FALSE;
  245. }
  246. // Build a new entity.
  247. else {
  248. $entity = $this->newEntity($source);
  249. $this->newItemInfo($entity, $source->feed_nid, $hash);
  250. }
  251. // Set property and field values.
  252. $this->map($source, $parser_result, $entity);
  253. $this->entityValidate($entity);
  254. // Allow modules to alter the entity before saving.
  255. module_invoke_all('feeds_presave', $source, $entity, $item, $entity_id);
  256. if (module_exists('rules')) {
  257. rules_invoke_event('feeds_import_'. $source->importer()->id, $entity);
  258. }
  259. // Enable modules to skip saving at all.
  260. if (!empty($entity->feeds_item->skip)) {
  261. continue;
  262. }
  263. // This will throw an exception on failure.
  264. $this->entitySaveAccess($entity);
  265. $this->entitySave($entity);
  266. // Allow modules to perform operations using the saved entity data.
  267. // $entity contains the updated entity after saving.
  268. module_invoke_all('feeds_after_save', $source, $entity, $item, $entity_id);
  269. // Track progress.
  270. if (empty($entity_id)) {
  271. $state->created++;
  272. }
  273. else {
  274. $state->updated++;
  275. }
  276. }
  277. // Something bad happened, log it.
  278. catch (Exception $e) {
  279. $state->failed++;
  280. drupal_set_message($e->getMessage(), 'warning');
  281. list($message, $arguments) = $this->createLogEntry($e, $entity, $item);
  282. $source->log('import', $message, $arguments, WATCHDOG_ERROR);
  283. }
  284. }
  285. // Set messages if we're done.
  286. if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
  287. return;
  288. }
  289. // Remove not included items if needed.
  290. // It depends on the implementation of the clean() method what will happen
  291. // to items that were no longer in the source.
  292. $this->clean($state);
  293. $info = $this->entityInfo();
  294. $tokens = array(
  295. '@entity' => strtolower($info['label']),
  296. '@entities' => strtolower($info['label plural']),
  297. );
  298. $messages = array();
  299. if ($state->created) {
  300. $messages[] = array(
  301. 'message' => format_plural(
  302. $state->created,
  303. 'Created @number @entity.',
  304. 'Created @number @entities.',
  305. array('@number' => $state->created) + $tokens
  306. ),
  307. );
  308. }
  309. if ($state->updated) {
  310. $messages[] = array(
  311. 'message' => format_plural(
  312. $state->updated,
  313. 'Updated @number @entity.',
  314. 'Updated @number @entities.',
  315. array('@number' => $state->updated) + $tokens
  316. ),
  317. );
  318. }
  319. if ($state->unpublished) {
  320. $messages[] = array(
  321. 'message' => format_plural(
  322. $state->unpublished,
  323. 'Unpublished @number @entity.',
  324. 'Unpublished @number @entities.',
  325. array('@number' => $state->unpublished) + $tokens
  326. ),
  327. );
  328. }
  329. if ($state->blocked) {
  330. $messages[] = array(
  331. 'message' => format_plural(
  332. $state->blocked,
  333. 'Blocked @number @entity.',
  334. 'Blocked @number @entities.',
  335. array('@number' => $state->blocked) + $tokens
  336. ),
  337. );
  338. }
  339. if ($state->deleted) {
  340. $messages[] = array(
  341. 'message' => format_plural(
  342. $state->deleted,
  343. 'Removed @number @entity.',
  344. 'Removed @number @entities.',
  345. array('@number' => $state->deleted) + $tokens
  346. ),
  347. );
  348. }
  349. if ($state->failed) {
  350. $messages[] = array(
  351. 'message' => format_plural(
  352. $state->failed,
  353. 'Failed importing @number @entity.',
  354. 'Failed importing @number @entities.',
  355. array('@number' => $state->failed) + $tokens
  356. ),
  357. 'level' => WATCHDOG_ERROR,
  358. );
  359. }
  360. if (empty($messages)) {
  361. $messages[] = array(
  362. 'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
  363. );
  364. }
  365. foreach ($messages as $message) {
  366. drupal_set_message($message['message']);
  367. $source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
  368. }
  369. }
  370. /**
  371. * Initializes the list of entities to remove.
  372. *
  373. * This populates $state->removeList with all existing entities previously
  374. * imported from the source.
  375. *
  376. * @param FeedsSource $source
  377. * Source information about this import.
  378. * @param FeedsState $state
  379. * The FeedsState object for the given stage.
  380. */
  381. protected function initEntitiesToBeRemoved(FeedsSource $source, FeedsState $state) {
  382. $state->removeList = array();
  383. // We fill it only if needed.
  384. if ($this->config['update_non_existent'] == FEEDS_SKIP_NON_EXISTENT) {
  385. return;
  386. }
  387. // Get the full list of entities for this source.
  388. $entity_ids = db_select('feeds_item')
  389. ->fields('feeds_item', array('entity_id'))
  390. ->condition('entity_type', $this->entityType())
  391. ->condition('id', $this->id)
  392. ->condition('feed_nid', $source->feed_nid)
  393. ->condition('hash', $this->config['update_non_existent'], '<>')
  394. ->execute()
  395. ->fetchCol();
  396. if (!$entity_ids) {
  397. return;
  398. }
  399. $state->removeList = array_combine($entity_ids, $entity_ids);
  400. }
  401. /**
  402. * Deletes entities which were not found during processing.
  403. *
  404. * @todo batch delete?
  405. *
  406. * @param FeedsState $state
  407. * The FeedsState object for the given stage.
  408. */
  409. protected function clean(FeedsState $state) {
  410. // We clean only if needed.
  411. if ($this->config['update_non_existent'] == FEEDS_SKIP_NON_EXISTENT) {
  412. return;
  413. }
  414. if ($total = count($state->removeList)) {
  415. $this->entityDeleteMultiple($state->removeList);
  416. $state->deleted += $total;
  417. }
  418. }
  419. /**
  420. * Remove all stored results or stored results up to a certain time for a
  421. * source.
  422. *
  423. * @param FeedsSource $source
  424. * Source information for this expiry. Implementers should only delete items
  425. * pertaining to this source. The preferred way of determining whether an
  426. * item pertains to a certain souce is by using $source->feed_nid. It is the
  427. * processor's responsibility to store the feed_nid of an imported item in
  428. * the processing stage.
  429. */
  430. public function clear(FeedsSource $source) {
  431. $state = $source->state(FEEDS_PROCESS_CLEAR);
  432. // Build base select statement.
  433. $select = db_select('feeds_item')
  434. ->fields('feeds_item', array('entity_id'))
  435. ->condition('entity_type', $this->entityType())
  436. ->condition('id', $this->id)
  437. ->condition('feed_nid', $source->feed_nid);
  438. // If there is no total, query it.
  439. if (!$state->total) {
  440. $state->total = $select->countQuery()->execute()->fetchField();
  441. }
  442. // Delete a batch of entities.
  443. $entity_ids = $select->range(0, $this->getLimit())->execute()->fetchCol();
  444. $this->entityDeleteMultiple($entity_ids);
  445. // Report progress, take into account that we may not have deleted as many
  446. // items as we have counted at first.
  447. if ($deleted_count = count($entity_ids)) {
  448. $state->deleted += $deleted_count;
  449. $state->progress($state->total, $state->deleted);
  450. }
  451. else {
  452. $state->progress($state->total, $state->total);
  453. }
  454. // Report results when done.
  455. if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
  456. $info = $this->entityInfo();
  457. if ($state->deleted) {
  458. $message = format_plural(
  459. $state->deleted,
  460. 'Deleted @number @entity',
  461. 'Deleted @number @entities',
  462. array(
  463. '@number' => $state->deleted,
  464. '@entity' => strtolower($info['label']),
  465. '@entities' => strtolower($info['label plural']),
  466. )
  467. );
  468. $source->log('clear', $message, array(), WATCHDOG_INFO);
  469. drupal_set_message($message);
  470. }
  471. else {
  472. drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
  473. }
  474. }
  475. }
  476. /*
  477. * Report number of items that can be processed per call.
  478. *
  479. * 0 means 'unlimited'.
  480. *
  481. * If a number other than 0 is given, Feeds parsers that support batching
  482. * will only deliver this limit to the processor.
  483. *
  484. * @see FeedsSource::getLimit()
  485. * @see FeedsCSVParser::parse()
  486. */
  487. public function getLimit() {
  488. return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
  489. }
  490. /**
  491. * Deletes feed items older than REQUEST_TIME - $time.
  492. *
  493. * Do not invoke expire on a processor directly, but use
  494. * FeedsSource::expire() instead.
  495. *
  496. * @param FeedsSource $source
  497. * The source to expire entities for.
  498. *
  499. * @param $time
  500. * (optional) All items produced by this configuration that are older than
  501. * REQUEST_TIME - $time should be deleted. If NULL, processor should use
  502. * internal configuration. Defaults to NULL.
  503. *
  504. * @return float
  505. * FEEDS_BATCH_COMPLETE if all items have been processed, a float between 0
  506. * and 0.99* indicating progress otherwise.
  507. *
  508. * @see FeedsSource::expire()
  509. */
  510. public function expire(FeedsSource $source, $time = NULL) {
  511. $state = $source->state(FEEDS_PROCESS_EXPIRE);
  512. if ($time === NULL) {
  513. $time = $this->expiryTime();
  514. }
  515. if ($time == FEEDS_EXPIRE_NEVER) {
  516. return;
  517. }
  518. $select = $this->expiryQuery($source, $time);
  519. // If there is no total, query it.
  520. if (!$state->total) {
  521. $state->total = $select->countQuery()->execute()->fetchField();
  522. }
  523. // Delete a batch of entities.
  524. $entity_ids = $select->range(0, $this->getLimit())->execute()->fetchCol();
  525. if ($entity_ids) {
  526. $this->entityDeleteMultiple($entity_ids);
  527. $state->deleted += count($entity_ids);
  528. $state->progress($state->total, $state->deleted);
  529. }
  530. else {
  531. $state->progress($state->total, $state->total);
  532. }
  533. }
  534. /**
  535. * Returns a database query used to select entities to expire.
  536. *
  537. * Processor classes should override this method to set the age portion of the
  538. * query.
  539. *
  540. * @param FeedsSource $source
  541. * The feed source.
  542. * @param int $time
  543. * Delete entities older than this.
  544. *
  545. * @return SelectQuery
  546. * A select query to execute.
  547. *
  548. * @see FeedsNodeProcessor::expiryQuery()
  549. */
  550. protected function expiryQuery(FeedsSource $source, $time) {
  551. // Build base select statement.
  552. $info = $this->entityInfo();
  553. $id_key = $info['entity keys']['id'];
  554. $select = db_select($info['base table'], 'e');
  555. $select->addField('e', $id_key);
  556. $select->join('feeds_item', 'fi', "e.$id_key = fi.entity_id");
  557. $select->condition('fi.entity_type', $this->entityType());
  558. $select->condition('fi.id', $this->id);
  559. $select->condition('fi.feed_nid', $source->feed_nid);
  560. return $select;
  561. }
  562. /**
  563. * Counts the number of items imported by this processor.
  564. */
  565. public function itemCount(FeedsSource $source) {
  566. return db_query("SELECT count(*) FROM {feeds_item} WHERE id = :id AND entity_type = :entity_type AND feed_nid = :feed_nid", array(':id' => $this->id, ':entity_type' => $this->entityType(), ':feed_nid' => $source->feed_nid))->fetchField();
  567. }
  568. /**
  569. * Returns a statically cached version of the target mappings.
  570. *
  571. * @return array
  572. * The targets for this importer.
  573. */
  574. protected function getCachedTargets() {
  575. $targets = &drupal_static('FeedsProcessor::getCachedTargets', array());
  576. if (!isset($targets[$this->id])) {
  577. $targets[$this->id] = $this->getMappingTargets();
  578. }
  579. return $targets[$this->id];
  580. }
  581. /**
  582. * Returns a statically cached version of the source mappings.
  583. *
  584. * @return array
  585. * The sources for this importer.
  586. */
  587. protected function getCachedSources() {
  588. $sources = &drupal_static('FeedsProcessor::getCachedSources', array());
  589. if (!isset($sources[$this->id])) {
  590. $sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
  591. if (is_array($sources[$this->id])) {
  592. foreach ($sources[$this->id] as $source_key => $source) {
  593. if (empty($source['callback']) || !is_callable($source['callback'])) {
  594. unset($sources[$this->id][$source_key]['callback']);
  595. }
  596. }
  597. }
  598. }
  599. return $sources[$this->id];
  600. }
  601. /**
  602. * Execute mapping on an item.
  603. *
  604. * This method encapsulates the central mapping functionality. When an item is
  605. * processed, it is passed through map() where the properties of $source_item
  606. * are mapped onto $target_item following the processor's mapping
  607. * configuration.
  608. *
  609. * For each mapping FeedsParser::getSourceElement() is executed to retrieve
  610. * the source element, then FeedsProcessor::setTargetElement() is invoked
  611. * to populate the target item properly. Alternatively a
  612. * hook_x_targets_alter() may have specified a callback for a mapping target
  613. * in which case the callback is asked to populate the target item instead of
  614. * FeedsProcessor::setTargetElement().
  615. *
  616. * @ingroup mappingapi
  617. *
  618. * @see hook_feeds_parser_sources_alter()
  619. * @see hook_feeds_processor_targets()
  620. * @see hook_feeds_processor_targets_alter()
  621. */
  622. protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
  623. $targets = $this->getCachedTargets();
  624. // Get fields for the entity type we are mapping to.
  625. $fields = field_info_instances($this->entityType(), $this->bundle());
  626. if (empty($target_item)) {
  627. $target_item = array();
  628. }
  629. // Many mappers add to existing fields rather than replacing them. Hence we
  630. // need to clear target elements of each item before mapping in case we are
  631. // mapping on a prepopulated item such as an existing node.
  632. foreach ($this->getMappings() as $mapping) {
  633. if (isset($targets[$mapping['target']]['real_target'])) {
  634. $target_name = $targets[$mapping['target']]['real_target'];
  635. }
  636. else {
  637. $target_name = $mapping['target'];
  638. }
  639. // If the target is a field empty the value for the targeted language
  640. // only.
  641. // In all other cases, just empty the target completely.
  642. if (isset($fields[$target_name])) {
  643. // Empty the target for the specified language.
  644. $target_item->{$target_name}[$mapping['language']] = array();
  645. }
  646. else {
  647. // Empty the whole target.
  648. $target_item->{$target_name} = NULL;
  649. }
  650. }
  651. // This is where the actual mapping happens: For every mapping we invoke
  652. // the parser's getSourceElement() method to retrieve the value of the
  653. // source element and pass it to the processor's setTargetElement() to stick
  654. // it on the right place of the target item.
  655. foreach ($this->getMappings() as $mapping) {
  656. $value = $this->getSourceValue($source, $result, $mapping['source']);
  657. $this->mapToTarget($source, $mapping['target'], $target_item, $value, $mapping);
  658. }
  659. return $target_item;
  660. }
  661. /**
  662. * Returns the values from the parser, or callback.
  663. *
  664. * @param FeedsSource $source
  665. * The feed source.
  666. * @param FeedsParserResult $result
  667. * The parser result.
  668. * @param string $source_key
  669. * The current key being processed.
  670. *
  671. * @return mixed
  672. * A value, or a list of values.
  673. */
  674. protected function getSourceValue(FeedsSource $source, FeedsParserResult $result, $source_key) {
  675. $sources = $this->getCachedSources();
  676. if (isset($sources[$source_key]['callback'])) {
  677. return call_user_func($sources[$source_key]['callback'], $source, $result, $source_key);
  678. }
  679. return feeds_importer($this->id)->parser->getSourceElement($source, $result, $source_key);
  680. }
  681. /**
  682. * Maps values onto the target item.
  683. *
  684. * @param FeedsSource $source
  685. * The feed source.
  686. * @param mixed &$target_item
  687. * The target item to apply values into.
  688. * @param mixed $value
  689. * A value, or a list of values.
  690. * @param array $mapping
  691. * The mapping configuration.
  692. */
  693. protected function mapToTarget(FeedsSource $source, $target, &$target_item, $value, array $mapping) {
  694. $targets = $this->getCachedTargets();
  695. // Map the source element's value to the target.
  696. // If the mapping specifies a callback method, use the callback instead of
  697. // setTargetElement().
  698. if (isset($targets[$target]['callback'])) {
  699. // All target callbacks expect an array.
  700. if (!is_array($value)) {
  701. $value = array($value);
  702. }
  703. call_user_func($targets[$target]['callback'], $source, $target_item, $target, $value, $mapping);
  704. }
  705. else {
  706. $this->setTargetElement($source, $target_item, $target, $value, $mapping);
  707. }
  708. }
  709. /**
  710. * Per default, don't support expiry. If processor supports expiry of imported
  711. * items, return the time after which items should be removed.
  712. */
  713. public function expiryTime() {
  714. return FEEDS_EXPIRE_NEVER;
  715. }
  716. /**
  717. * Declare default configuration.
  718. */
  719. public function configDefaults() {
  720. $info = $this->entityInfo();
  721. $bundle = NULL;
  722. if (empty($info['entity keys']['bundle'])) {
  723. $bundle = $this->entityType();
  724. }
  725. return array(
  726. 'mappings' => array(),
  727. 'insert_new' => FEEDS_INSERT_NEW,
  728. 'update_existing' => FEEDS_SKIP_EXISTING,
  729. 'update_non_existent' => FEEDS_SKIP_NON_EXISTENT,
  730. 'input_format' => NULL,
  731. 'skip_hash_check' => FALSE,
  732. 'bundle' => $bundle,
  733. 'language' => LANGUAGE_NONE,
  734. );
  735. }
  736. /**
  737. * Overrides parent::configForm().
  738. */
  739. public function configForm(&$form_state) {
  740. $info = $this->entityInfo();
  741. $form = array();
  742. if (!empty($info['entity keys']['bundle'])) {
  743. $form['bundle'] = array(
  744. '#type' => 'select',
  745. '#options' => $this->bundleOptions(),
  746. '#title' => !empty($info['bundle name']) ? $info['bundle name'] : t('Bundle'),
  747. '#required' => TRUE,
  748. '#default_value' => $this->bundle(),
  749. );
  750. }
  751. else {
  752. $form['bundle'] = array(
  753. '#type' => 'value',
  754. '#value' => $this->entityType(),
  755. );
  756. }
  757. if (module_exists('locale') && !empty($info['entity keys']['language'])) {
  758. $form['language'] = array(
  759. '#type' => 'select',
  760. '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
  761. '#title' => t('Language'),
  762. '#required' => TRUE,
  763. '#default_value' => $this->config['language'],
  764. );
  765. }
  766. $tokens = array('@entities' => strtolower($info['label plural']));
  767. $form['insert_new'] = array(
  768. '#type' => 'radios',
  769. '#title' => t('Insert new @entities', $tokens),
  770. '#description' => t('New @entities will be determined using mappings that are a "unique target".', $tokens),
  771. '#options' => array(
  772. FEEDS_INSERT_NEW => t('Insert new @entities', $tokens),
  773. FEEDS_SKIP_NEW => t('Do not insert new @entities', $tokens),
  774. ),
  775. '#default_value' => $this->config['insert_new'],
  776. );
  777. $form['update_existing'] = array(
  778. '#type' => 'radios',
  779. '#title' => t('Update existing @entities', $tokens),
  780. '#description' =>
  781. t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
  782. '#options' => array(
  783. FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
  784. FEEDS_REPLACE_EXISTING => t('Replace existing @entities', $tokens),
  785. FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
  786. ),
  787. '#default_value' => $this->config['update_existing'],
  788. );
  789. global $user;
  790. $formats = filter_formats($user);
  791. foreach ($formats as $format) {
  792. $format_options[$format->format] = $format->name;
  793. }
  794. $form['skip_hash_check'] = array(
  795. '#type' => 'checkbox',
  796. '#title' => t('Skip hash check'),
  797. '#description' => t('Force update of items even if item source data did not change.'),
  798. '#default_value' => $this->config['skip_hash_check'],
  799. );
  800. $form['input_format'] = array(
  801. '#type' => 'select',
  802. '#title' => t('Text format'),
  803. '#description' => t('Select the default input format for the text fields of the nodes to be created.'),
  804. '#options' => $format_options,
  805. '#default_value' => isset($this->config['input_format']) ? $this->config['input_format'] : 'plain_text',
  806. '#required' => TRUE,
  807. );
  808. $form['update_non_existent'] = array(
  809. '#type' => 'radios',
  810. '#title' => t('Action to take when previously imported @entities are missing in the feed', $tokens),
  811. '#description' => t('Select how @entities previously imported and now missing in the feed should be updated.', $tokens),
  812. '#options' => array(
  813. FEEDS_SKIP_NON_EXISTENT => t('Skip non-existent @entities', $tokens),
  814. FEEDS_DELETE_NON_EXISTENT => t('Delete non-existent @entities', $tokens),
  815. ),
  816. '#default_value' => $this->config['update_non_existent'],
  817. );
  818. return $form;
  819. }
  820. /**
  821. * Get mappings.
  822. */
  823. public function getMappings() {
  824. $cache = &drupal_static('FeedsProcessor::getMappings', array());
  825. if (!isset($cache[$this->id])) {
  826. $mappings = $this->config['mappings'];
  827. $targets = $this->getCachedTargets();
  828. $languages = language_list('enabled');
  829. foreach ($mappings as &$mapping) {
  830. if (isset($targets[$mapping['target']]['preprocess_callbacks'])) {
  831. foreach ($targets[$mapping['target']]['preprocess_callbacks'] as $callback) {
  832. call_user_func_array($callback, array($targets[$mapping['target']], &$mapping));
  833. }
  834. }
  835. // Ensure there's always a language set.
  836. if (empty($mapping['language'])) {
  837. $mapping['language'] = LANGUAGE_NONE;
  838. }
  839. else {
  840. // Check if the configured language is available. If not, fallback to
  841. // LANGUAGE_NONE.
  842. if (!isset($languages[1][$mapping['language']])) {
  843. $mapping['language'] = LANGUAGE_NONE;
  844. }
  845. }
  846. }
  847. $cache[$this->id] = $mappings;
  848. }
  849. return $cache[$this->id];
  850. }
  851. /**
  852. * Declare possible mapping targets that this processor exposes.
  853. *
  854. * @ingroup mappingapi
  855. *
  856. * @return
  857. * An array of mapping targets. Keys are paths to targets
  858. * separated by ->, values are TRUE if target can be unique,
  859. * FALSE otherwise.
  860. */
  861. public function getMappingTargets() {
  862. // The bundle has not been selected.
  863. if (!$this->bundle()) {
  864. $info = $this->entityInfo();
  865. $bundle_name = !empty($info['bundle name']) ? drupal_strtolower($info['bundle name']) : t('bundle');
  866. $plugin_key = feeds_importer($this->id)->config['processor']['plugin_key'];
  867. $url = url('admin/structure/feeds/' . $this->id . '/settings/' . $plugin_key);
  868. drupal_set_message(t('Please <a href="@url">select a @bundle_name</a>.', array('@url' => $url, '@bundle_name' => $bundle_name)), 'warning', FALSE);
  869. }
  870. return array(
  871. 'url' => array(
  872. 'name' => t('URL'),
  873. 'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
  874. 'optional_unique' => TRUE,
  875. ),
  876. 'guid' => array(
  877. 'name' => t('GUID'),
  878. 'description' => t('The globally unique identifier of the item. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
  879. 'optional_unique' => TRUE,
  880. ),
  881. );
  882. }
  883. /**
  884. * Allows other modules to expose targets.
  885. *
  886. * @param array &$targets
  887. * The existing target array.
  888. */
  889. protected function getHookTargets(array &$targets) {
  890. self::loadMappers();
  891. $entity_type = $this->entityType();
  892. $bundle = $this->bundle();
  893. $targets += module_invoke_all('feeds_processor_targets', $entity_type, $bundle);
  894. drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
  895. }
  896. /**
  897. * Set a concrete target element. Invoked from FeedsProcessor::map().
  898. *
  899. * @ingroup mappingapi
  900. */
  901. public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
  902. switch ($target_element) {
  903. case 'url':
  904. case 'guid':
  905. $target_item->feeds_item->$target_element = $value;
  906. break;
  907. default:
  908. $target_item->$target_element = $value;
  909. break;
  910. }
  911. }
  912. /**
  913. * Retrieve the target entity's existing id if available. Otherwise return 0.
  914. *
  915. * @ingroup mappingapi
  916. *
  917. * @param FeedsSource $source
  918. * The source information about this import.
  919. * @param FeedsParserResult $result
  920. * A FeedsParserResult object.
  921. *
  922. * @return int
  923. * The serial id of an entity if found, 0 otherwise.
  924. */
  925. protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
  926. $targets = $this->getCachedTargets();
  927. $entity_id = 0;
  928. // Iterate through all unique targets and test whether they already exist in
  929. // the database.
  930. foreach ($this->uniqueTargets($source, $result) as $target => $value) {
  931. if ($target === 'guid' || $target === 'url') {
  932. $entity_id = db_select('feeds_item')
  933. ->fields('feeds_item', array('entity_id'))
  934. ->condition('feed_nid', $source->feed_nid)
  935. ->condition('entity_type', $this->entityType())
  936. ->condition('id', $source->id)
  937. ->condition($target, $value)
  938. ->execute()
  939. ->fetchField();
  940. }
  941. if (!$entity_id && !empty($targets[$target]['unique_callbacks'])) {
  942. if (!is_array($value)) {
  943. $value = array($value);
  944. }
  945. foreach ($targets[$target]['unique_callbacks'] as $callback) {
  946. if ($entity_id = call_user_func($callback, $source, $this->entityType(), $this->bundle(), $target, $value)) {
  947. // Stop at the first unique ID returned by a callback.
  948. break;
  949. }
  950. }
  951. }
  952. // Return with the content id found.
  953. if ($entity_id) {
  954. return $entity_id;
  955. }
  956. }
  957. return $entity_id;
  958. }
  959. /**
  960. * Utility function that iterates over a target array and retrieves all
  961. * sources that are unique.
  962. *
  963. * @param $batch
  964. * A FeedsImportBatch.
  965. *
  966. * @return
  967. * An array where the keys are target field names and the values are the
  968. * elements from the source item mapped to these targets.
  969. */
  970. protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
  971. $parser = feeds_importer($this->id)->parser;
  972. $targets = array();
  973. foreach ($this->getMappings() as $mapping) {
  974. if (!empty($mapping['unique'])) {
  975. // Invoke the parser's getSourceElement to retrieve the value for this
  976. // mapping's source.
  977. $targets[$mapping['target']] = $parser->getSourceElement($source, $result, $mapping['source']);
  978. }
  979. }
  980. return $targets;
  981. }
  982. /**
  983. * Adds Feeds specific information on $entity->feeds_item.
  984. *
  985. * @param $entity
  986. * The entity object to be populated with new item info.
  987. * @param $feed_nid
  988. * The feed nid of the source that produces this entity.
  989. * @param $hash
  990. * The fingerprint of the source item.
  991. */
  992. protected function newItemInfo($entity, $feed_nid, $hash = '') {
  993. $entity->feeds_item = new stdClass();
  994. $entity->feeds_item->is_new = TRUE;
  995. $entity->feeds_item->entity_id = 0;
  996. $entity->feeds_item->entity_type = $this->entityType();
  997. $entity->feeds_item->id = $this->id;
  998. $entity->feeds_item->feed_nid = $feed_nid;
  999. $entity->feeds_item->imported = REQUEST_TIME;
  1000. $entity->feeds_item->hash = $hash;
  1001. $entity->feeds_item->url = '';
  1002. $entity->feeds_item->guid = '';
  1003. }
  1004. /**
  1005. * Loads existing entity information and places it on $entity->feeds_item.
  1006. *
  1007. * @param $entity
  1008. * The entity object to load item info for. Id key must be present.
  1009. *
  1010. * @return
  1011. * TRUE if item info could be loaded, false if not.
  1012. */
  1013. protected function loadItemInfo($entity) {
  1014. $entity_info = entity_get_info($this->entityType());
  1015. $key = $entity_info['entity keys']['id'];
  1016. if ($item_info = feeds_item_info_load($this->entityType(), $entity->$key)) {
  1017. $entity->feeds_item = $item_info;
  1018. return TRUE;
  1019. }
  1020. return FALSE;
  1021. }
  1022. /**
  1023. * Create MD5 hash of item and mappings array.
  1024. *
  1025. * Include mappings as a change in mappings may have an affect on the item
  1026. * produced.
  1027. *
  1028. * @return string
  1029. * A hash is always returned, even when the item is empty, NULL or FALSE.
  1030. */
  1031. protected function hash($item) {
  1032. $sources = feeds_importer($this->id)->parser->getMappingSourceList();
  1033. $mapped_item = array_intersect_key($item, array_flip($sources));
  1034. return hash('md5', serialize($mapped_item) . serialize($this->getMappings()));
  1035. }
  1036. /**
  1037. * Retrieves the MD5 hash of $entity_id from the database.
  1038. *
  1039. * @return string
  1040. * Empty string if no item is found, hash otherwise.
  1041. */
  1042. protected function getHash($entity_id) {
  1043. if ($hash = db_query("SELECT hash FROM {feeds_item} WHERE entity_type = :type AND entity_id = :id", array(':type' => $this->entityType(), ':id' => $entity_id))->fetchField()) {
  1044. // Return with the hash.
  1045. return $hash;
  1046. }
  1047. return '';
  1048. }
  1049. /**
  1050. * DEPRECATED: Creates a log message for exceptions during import.
  1051. *
  1052. * Don't use this method as it concatenates user variables into the log
  1053. * message, which will pollute the locales_source table when the log message
  1054. * is translated. Use ::createLogEntry instead.
  1055. *
  1056. * @param Exception $e
  1057. * The exception that was throwned during processing the item.
  1058. * @param $entity
  1059. * The entity object.
  1060. * @param $item
  1061. * The parser result for this entity.
  1062. *
  1063. * @return string
  1064. * The message to log.
  1065. *
  1066. * @deprecated
  1067. * Use ::createLogEntry instead.
  1068. */
  1069. protected function createLogMessage(Exception $e, $entity, $item) {
  1070. $message = $e->getMessage();
  1071. $message .= '<h3>Original item</h3>';
  1072. // $this->exportObjectVars() already runs check_plain() for us, so we can
  1073. // concatenate here as is.
  1074. $message .= '<pre>' . $this->exportObjectVars($item) . '</pre>';
  1075. $message .= '<h3>Entity</h3>';
  1076. $message .= '<pre>' . $this->exportObjectVars($entity) . '</pre>';
  1077. return $message;
  1078. }
  1079. /**
  1080. * Creates a log entry for when an exception occurred during import.
  1081. *
  1082. * @param Exception $e
  1083. * The exception that was throwned during processing the item.
  1084. * @param object $entity
  1085. * The entity object.
  1086. * @param array $item
  1087. * The parser result for this entity.
  1088. *
  1089. * @return array
  1090. * The message and arguments to log.
  1091. */
  1092. protected function createLogEntry(Exception $e, $entity, $item) {
  1093. $message = '@exception';
  1094. $message .= '<h3>Original item</h3>';
  1095. $message .= '<pre>!item</pre>';
  1096. $message .= '<h3>Entity</h3>';
  1097. $message .= '<pre>!entity</pre>';
  1098. $arguments = array(
  1099. '@exception' => $e->getMessage(),
  1100. // $this->exportObjectVars() already runs check_plain() for us, so we can
  1101. // use the "!" placeholder.
  1102. '!item' => $this->exportObjectVars($item),
  1103. '!entity' => $this->exportObjectVars($entity),
  1104. );
  1105. return array($message, $arguments);
  1106. }
  1107. /**
  1108. * Returns a string representation of an object or array for log messages.
  1109. *
  1110. * @param object|array $object
  1111. * The object to convert.
  1112. *
  1113. * @return string
  1114. * The sanitized string representation of the object.
  1115. */
  1116. protected function exportObjectVars($object) {
  1117. include_once DRUPAL_ROOT . '/includes/utility.inc';
  1118. $out = is_array($object) ? $object : get_object_vars($object);
  1119. $out = array_filter($out, 'is_scalar');
  1120. foreach ($out as $key => $value) {
  1121. if (is_string($value)) {
  1122. $out[$key] = truncate_utf8($value, 100, FALSE, TRUE);
  1123. }
  1124. }
  1125. if (is_array($object)) {
  1126. return check_plain(drupal_var_export($out));
  1127. }
  1128. return check_plain(drupal_var_export((object) $out));
  1129. }
  1130. /**
  1131. * Overrides FeedsPlugin::dependencies().
  1132. */
  1133. public function dependencies() {
  1134. $dependencies = parent::dependencies();
  1135. // Find out which module defined the entity type.
  1136. $info = $this->entityInfo();
  1137. if (isset($info['module'])) {
  1138. $dependencies[$info['module']] = $info['module'];
  1139. }
  1140. return $dependencies;
  1141. }
  1142. }
  1143. class FeedsProcessorBundleNotDefined extends Exception {}