Projet

Général

Profil

Paste
Télécharger (47,3 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / feeds / plugins / FeedsProcessor.inc @ ed9a13f1

1
<?php
2

    
3
/**
4
 * @file
5
 * Contains FeedsProcessor and related classes.
6
 */
7

    
8
// Insert mode for new items.
9
define('FEEDS_SKIP_NEW', 0);
10
define('FEEDS_INSERT_NEW', 1);
11

    
12
// Update mode for existing items.
13
define('FEEDS_SKIP_EXISTING', 0);
14
define('FEEDS_REPLACE_EXISTING', 1);
15
define('FEEDS_UPDATE_EXISTING', 2);
16
// Options for handling content in Drupal but not in source data.
17
define('FEEDS_SKIP_NON_EXISTENT', 'skip');
18
define('FEEDS_DELETE_NON_EXISTENT', 'delete');
19

    
20
// Default limit for creating items on a page load, not respected by all
21
// processors.
22
define('FEEDS_PROCESS_LIMIT', 50);
23

    
24
/**
25
 * Thrown if a validation fails.
26
 */
27
class FeedsValidationException extends Exception {}
28

    
29
/**
30
 * Thrown if a an access check fails.
31
 */
32
class FeedsAccessException extends Exception {}
33

    
34
/**
35
 * Abstract class, defines interface for processors.
36
 */
37
abstract class FeedsProcessor extends FeedsPlugin {
38

    
39
  /**
40
   * Implements FeedsPlugin::pluginType().
41
   */
42
  public function pluginType() {
43
    return 'processor';
44
  }
45

    
46
  /**
47
   * @defgroup entity_api_wrapper Entity API wrapper.
48
   */
49

    
50
  /**
51
   * Entity type this processor operates on.
52
   */
53
  abstract public function entityType();
54

    
55
  /**
56
   * Bundle type this processor operates on.
57
   *
58
   * Defaults to the entity type for entities that do not define bundles.
59
   *
60
   * @return string|null
61
   *   The bundle type this processor operates on, or NULL if it is undefined.
62
   */
63
  public function bundle() {
64
    return $this->config['bundle'];
65
  }
66

    
67
  /**
68
   * Provides a list of bundle options for use in select lists.
69
   *
70
   * @return array
71
   *   A keyed array of bundle => label.
72
   */
73
  public function bundleOptions() {
74
    $options = array();
75
    foreach (field_info_bundles($this->entityType()) as $bundle => $info) {
76
      if (!empty($info['label'])) {
77
        $options[$bundle] = $info['label'];
78
      }
79
      else {
80
        $options[$bundle] = $bundle;
81
      }
82
    }
83
    return $options;
84
  }
85

    
86
  /**
87
   * Provides a list of languages available on the site.
88
   *
89
   * @return array
90
   *   A keyed array of language_key => language_name. For example:
91
   *   'en' => 'English'.
92
   */
93
  public function languageOptions() {
94
    $languages = array(
95
      LANGUAGE_NONE => t('Language neutral'),
96
    );
97
    $language_list = language_list('enabled');
98
    if (!empty($language_list[1])) {
99
      foreach ($language_list[1] as $language) {
100
        $languages[$language->language] = $language->name;
101
      }
102
    }
103
    return $languages;
104
  }
105

    
106
  /**
107
   * Create a new entity.
108
   *
109
   * @param FeedsSource $source
110
   *   The feeds source that spawns this entity.
111
   *
112
   * @return object
113
   *   A new entity object.
114
   */
115
  protected function newEntity(FeedsSource $source) {
116
    $entity = new stdClass();
117

    
118
    $info = $this->entityInfo();
119
    if (!empty($info['entity keys']['language'])) {
120
      $entity->{$info['entity keys']['language']} = $this->entityLanguage();
121
    }
122

    
123
    return $entity;
124
  }
125

    
126
  /**
127
   * Loads an existing entity.
128
   *
129
   * @param FeedsSource $source
130
   *   The feeds source that spawns this entity.
131
   * @param mixed $entity_id
132
   *   The id of the entity to load.
133
   *
134
   * @return object
135
   *   A new entity object.
136
   *
137
   * @todo We should be able to batch load these, if we found all of the
138
   *   existing ids first.
139
   */
140
  protected function entityLoad(FeedsSource $source, $entity_id) {
141
    $info = $this->entityInfo();
142

    
143
    if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
144
      $entities = entity_load($this->entityType(), array($entity_id));
145
      $entity = reset($entities);
146
    }
147
    else {
148
      $args = array(':entity_id' => $entity_id);
149
      $table = db_escape_table($info['base table']);
150
      $key = db_escape_field($info['entity keys']['id']);
151
      $entity = db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject();
152
    }
153

    
154
    if ($entity && !empty($info['entity keys']['language'])) {
155
      $entity->{$info['entity keys']['language']} = $this->entityLanguage();
156
    }
157

    
158
    return $entity;
159
  }
160

    
161
  /**
162
   * Validates an entity.
163
   *
164
   * @param object $entity
165
   *   The entity to validate.
166
   * @param FeedsSource $source
167
   *   (optional) The source to import from.
168
   *
169
   * @throws FeedsValidationException $e
170
   *   Thrown if validation fails.
171
   */
172
  protected function entityValidate($entity, FeedsSource $source = NULL) {
173
    $info = $this->entityInfo();
174

    
175
    if (!empty($info['entity keys']['language'])) {
176
      // Ensure that a valid language is always set.
177
      $key = $info['entity keys']['language'];
178
      $languages = $this->languageOptions();
179

    
180
      if (empty($entity->$key) || !isset($languages[$entity->$key])) {
181
        $entity->$key = $this->entityLanguage();
182
      }
183
    }
184

    
185
    // Perform field validation if entity is fieldable.
186
    if (!empty($info['fieldable'])) {
187
      try {
188
        $this->validateFields($entity);
189
      }
190
      catch (FieldValidationException $e) {
191
        $errors = array();
192

    
193
        // Unravel the errors inside the FieldValidationException.
194
        foreach ($e->errors as $field_errors) {
195
          $errors = array_merge($errors, $this->unravelFieldValidationExceptionErrors($field_errors));
196
        }
197

    
198
        // Compose error message. If available, use the entity label to indicate
199
        // which item failed. Fallback to the GUID value (if available) or else
200
        // no indication.
201
        $label = entity_label($this->entityType(), $entity);
202
        if ($label || $label === '0' || $label === 0) {
203
          $message = t("@error in item '@label':", array('@error' => $e->getMessage(), '@label' => $label));
204
        }
205
        elseif (!empty($entity->feeds_item->guid)) {
206
          $message = t("@error in item '@guid':", array('@error' => $e->getMessage(), '@guid' => $entity->feeds_item->guid));
207
        }
208
        else {
209
          $message = $e->getMessage();
210
        }
211
        // Compose the final error message and throw exception.
212
        $message .= theme('item_list', array('items' => $errors));
213
        throw new FeedsValidationException($message);
214
      }
215
    }
216
  }
217

    
218
  /**
219
   * Validates fields of an entity.
220
   *
221
   * This is mostly a copy of the field_attach_validate() function. The
222
   * difference is that field_attach_validate() validates *all* fields on an
223
   * entity, while Feeds only validates the fields where the user mapped to.
224
   *
225
   * @param object $entity
226
   *   The entity for which the field to validate.
227
   *
228
   * @throws FieldValidationException
229
   *   If validation errors are found, a FieldValidationException is thrown. The
230
   *   'errors' property contains the array of errors, keyed by field name,
231
   *   language and delta.
232
   */
233
  protected function validateFields($entity) {
234
    $entity_type = $this->entityType();
235

    
236
    // Get fields for the entity type we are mapping to.
237
    $fields = field_info_instances($entity_type, $this->bundle());
238

    
239
    // Get targets.
240
    $targets = $this->getCachedTargets();
241

    
242
    $errors = array();
243
    $null = NULL;
244

    
245
    // Validate all fields that we are mapping to.
246
    foreach ($this->getMappings() as $mapping) {
247
      // Get real target name.
248
      if (isset($targets[$mapping['target']]['real_target'])) {
249
        $target_name = $targets[$mapping['target']]['real_target'];
250
      }
251
      else {
252
        $target_name = $mapping['target'];
253
      }
254

    
255
      if (isset($fields[$target_name])) {
256
        // Validate this field.
257
        _field_invoke_default('validate', $entity_type, $entity, $errors, $null, array(
258
          'field_name' => $target_name,
259
        ));
260
        _field_invoke('validate', $entity_type, $entity, $errors, $null, array(
261
          'field_name' => $target_name,
262
        ));
263
      }
264
    }
265

    
266
    // Let other modules validate the entity.
267
    // Avoid module_invoke_all() to let $errors be taken by reference.
268
    foreach (module_implements('field_attach_validate') as $module) {
269
      $function = $module . '_field_attach_validate';
270
      $function($entity_type, $entity, $errors);
271
    }
272

    
273
    if ($errors) {
274
      throw new FieldValidationException($errors);
275
    }
276
  }
277

    
278
  /**
279
   * Helper function to unravel error messages hidden in a
280
   * FieldValidationException.
281
   *
282
   * @param array $field_item_errors
283
   *   The errors for a single field item.
284
   *
285
   * @return array
286
   *   The unraveled error messages.
287
   */
288
  protected function unravelFieldValidationExceptionErrors(array $field_item_errors) {
289
    $errors = array();
290

    
291
    foreach ($field_item_errors as $field_item_error) {
292
      if (isset($field_item_error['message'])) {
293
        // Found the error message!
294
        $errors[] = $field_item_error['message'];
295
      }
296
      elseif (is_array($field_item_error)) {
297
        // Error message is hidden deeper in the tree.
298
        $errors = array_merge($errors, $this->unravelFieldValidationExceptionErrors($field_item_error));
299
      }
300
    }
301

    
302
    return $errors;
303
  }
304

    
305
  /**
306
   * Access check for saving an entity.
307
   *
308
   * @param object $entity
309
   *   The entity to be saved.
310
   *
311
   * @throws FeedsAccessException $e
312
   *   If the access check fails.
313
   */
314
  protected function entitySaveAccess($entity) {}
315

    
316
  /**
317
   * Saves an entity.
318
   *
319
   * @param object $entity
320
   *   The entity to be saved.
321
   */
322
  abstract protected function entitySave($entity);
323

    
324
  /**
325
   * Deletes a series of entities.
326
   *
327
   * @param array $entity_ids
328
   *   Array of unique identity ids to be deleted.
329
   */
330
  abstract protected function entityDeleteMultiple($entity_ids);
331

    
332
  /**
333
   * Wrapper around entity_get_info().
334
   *
335
   * The entity_get_info() function is wrapped so that extending classes can
336
   * override it and add more entity information.
337
   *
338
   * Allowed additional keys:
339
   * - label plural
340
   *   the plural label of an entity type.
341
   */
342
  protected function entityInfo() {
343
    $info = entity_get_info($this->entityType());
344

    
345
    // Entity module has defined the plural label in "plural label" instead of
346
    // "label plural". So if "plural label" is defined, this will have priority
347
    // over "label plural".
348
    if (isset($info['plural label'])) {
349
      $info['label plural'] = $info['plural label'];
350
    }
351

    
352
    return $info;
353
  }
354

    
355
  /**
356
   * Returns the current language for entities.
357
   *
358
   * This checks if the configuration value is valid.
359
   *
360
   * @return string
361
   *   The current language code.
362
   */
363
  protected function entityLanguage() {
364
    if (!module_exists('locale')) {
365
      // language_list() may return languages even if the locale module is
366
      // disabled. See https://www.drupal.org/node/173227 why.
367
      // When the locale module is disabled, there are no selectable languages
368
      // in the UI, so the content should be imported in LANGUAGE_NONE.
369
      return LANGUAGE_NONE;
370
    }
371

    
372
    $languages = $this->languageOptions();
373

    
374
    return isset($languages[$this->config['language']]) ? $this->config['language'] : LANGUAGE_NONE;
375
  }
376

    
377
  /**
378
   * @}
379
   */
380

    
381
  /**
382
   * Process the result of the parsing stage.
383
   *
384
   * @param FeedsSource $source
385
   *   Source information about this import.
386
   * @param FeedsParserResult $parser_result
387
   *   The result of the parsing stage.
388
   */
389
  public function process(FeedsSource $source, FeedsParserResult $parser_result) {
390
    $state = $source->state(FEEDS_PROCESS);
391
    if (!isset($state->removeList) && $parser_result->items) {
392
      $this->initEntitiesToBeRemoved($source, $state);
393
    }
394

    
395
    $skip_new = $this->config['insert_new'] == FEEDS_SKIP_NEW;
396
    $skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING;
397

    
398
    while ($item = $parser_result->shiftItem()) {
399

    
400
      // Check if this item already exists.
401
      $entity_id = $this->existingEntityId($source, $parser_result);
402
      // If it's included in the feed, it must not be removed on clean.
403
      if ($entity_id) {
404
        unset($state->removeList[$entity_id]);
405
      }
406

    
407
      module_invoke_all('feeds_before_update', $source, $item, $entity_id);
408

    
409
      // If it exists, and we are not updating, or if it does not exist, and we
410
      // are not inserting, pass onto the next item.
411
      if (($entity_id && $skip_existing) || (!$entity_id && $skip_new)) {
412
        continue;
413
      }
414

    
415
      try {
416
        $hash = $this->hash($item);
417
        $changed = $hash !== $this->getHash($entity_id);
418

    
419
        // Do not proceed if the item exists, has not changed, and we're not
420
        // forcing the update.
421
        if ($entity_id && !$changed && !$this->config['skip_hash_check']) {
422
          continue;
423
        }
424

    
425
        // Load an existing entity.
426
        if ($entity_id) {
427
          $entity = $this->entityLoad($source, $entity_id);
428

    
429
          // The feeds_item table is always updated with the info for the most
430
          // recently processed entity. The only carryover is the entity_id.
431
          $this->newItemInfo($entity, $source->feed_nid, $hash);
432
          $entity->feeds_item->entity_id = $entity_id;
433
          $entity->feeds_item->is_new = FALSE;
434
        }
435

    
436
        // Build a new entity.
437
        else {
438
          $entity = $this->newEntity($source);
439
          $this->newItemInfo($entity, $source->feed_nid, $hash);
440
        }
441

    
442
        // Set property and field values.
443
        $this->map($source, $parser_result, $entity);
444

    
445
        // Allow modules to alter the entity before validating.
446
        module_invoke_all('feeds_prevalidate', $source, $entity, $item, $entity_id);
447
        $this->entityValidate($entity, $source);
448

    
449
        // Allow modules to alter the entity before saving.
450
        module_invoke_all('feeds_presave', $source, $entity, $item, $entity_id);
451
        if (module_exists('rules')) {
452
          rules_invoke_event('feeds_import_' . $source->importer()->id, $entity);
453
        }
454

    
455
        // Enable modules to skip saving at all.
456
        if (!empty($entity->feeds_item->skip)) {
457
          continue;
458
        }
459

    
460
        // This will throw an exception on failure.
461
        $this->entitySaveAccess($entity);
462
        $this->entitySave($entity);
463

    
464
        // Allow modules to perform operations using the saved entity data.
465
        // $entity contains the updated entity after saving.
466
        module_invoke_all('feeds_after_save', $source, $entity, $item, $entity_id);
467

    
468
        // Track progress.
469
        if (empty($entity_id)) {
470
          $state->created++;
471
        }
472
        else {
473
          $state->updated++;
474
        }
475
      }
476

    
477
      // Something bad happened, log it.
478
      catch (Exception $e) {
479
        $state->failed++;
480
        drupal_set_message($e->getMessage(), 'warning');
481
        list($message, $arguments) = $this->createLogEntry($e, $entity, $item);
482
        $source->log('import', $message, $arguments, WATCHDOG_ERROR);
483
      }
484
    }
485

    
486
    // Set messages if we're done.
487
    if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
488
      return;
489
    }
490
    // Remove not included items if needed.
491
    // It depends on the implementation of the clean() method what will happen
492
    // to items that were no longer in the source.
493
    $this->clean($state);
494
    $info = $this->entityInfo();
495
    $tokens = array(
496
      '@entity' => strtolower($info['label']),
497
      '@entities' => strtolower($info['label plural']),
498
    );
499
    $messages = array();
500
    if ($state->created) {
501
      $messages[] = array(
502
        'message' => format_plural(
503
          $state->created,
504
          'Created @number @entity.',
505
          'Created @number @entities.',
506
          array('@number' => $state->created) + $tokens
507
        ),
508
      );
509
    }
510
    if ($state->updated) {
511
      $messages[] = array(
512
        'message' => format_plural(
513
          $state->updated,
514
          'Updated @number @entity.',
515
          'Updated @number @entities.',
516
          array('@number' => $state->updated) + $tokens
517
        ),
518
      );
519
    }
520
    if ($state->unpublished) {
521
      $messages[] = array(
522
        'message' => format_plural(
523
            $state->unpublished,
524
            'Unpublished @number @entity.',
525
            'Unpublished @number @entities.',
526
            array('@number' => $state->unpublished) + $tokens
527
        ),
528
      );
529
    }
530
    if ($state->blocked) {
531
      $messages[] = array(
532
        'message' => format_plural(
533
          $state->blocked,
534
          'Blocked @number @entity.',
535
          'Blocked @number @entities.',
536
          array('@number' => $state->blocked) + $tokens
537
        ),
538
      );
539
    }
540
    if ($state->deleted) {
541
      $messages[] = array(
542
        'message' => format_plural(
543
          $state->deleted,
544
          'Removed @number @entity.',
545
          'Removed @number @entities.',
546
          array('@number' => $state->deleted) + $tokens
547
        ),
548
      );
549
    }
550
    if ($state->failed) {
551
      $messages[] = array(
552
        'message' => format_plural(
553
          $state->failed,
554
          'Failed importing @number @entity.',
555
          'Failed importing @number @entities.',
556
          array('@number' => $state->failed) + $tokens
557
        ),
558
        'level' => WATCHDOG_ERROR,
559
      );
560
    }
561
    if (empty($messages)) {
562
      $messages[] = array(
563
        'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
564
      );
565
    }
566
    foreach ($messages as $message) {
567
      drupal_set_message($message['message']);
568
      $source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
569
    }
570
  }
571

    
572
  /**
573
   * Initializes the list of entities to remove.
574
   *
575
   * This populates $state->removeList with all existing entities previously
576
   * imported from the source.
577
   *
578
   * @param FeedsSource $source
579
   *   Source information about this import.
580
   * @param FeedsState $state
581
   *   The FeedsState object for the given stage.
582
   */
583
  protected function initEntitiesToBeRemoved(FeedsSource $source, FeedsState $state) {
584
    $state->removeList = array();
585

    
586
    // We fill it only if needed.
587
    if ($this->config['update_non_existent'] == FEEDS_SKIP_NON_EXISTENT) {
588
      return;
589
    }
590

    
591
    // Get the full list of entities for this source.
592
    $entity_ids = db_select('feeds_item')
593
      ->fields('feeds_item', array('entity_id'))
594
      ->condition('entity_type', $this->entityType())
595
      ->condition('id', $this->id)
596
      ->condition('feed_nid', $source->feed_nid)
597
      ->condition('hash', $this->config['update_non_existent'], '<>')
598
      ->execute()
599
      ->fetchCol();
600

    
601
    if (!$entity_ids) {
602
      return;
603
    }
604

    
605
    $state->removeList = array_combine($entity_ids, $entity_ids);
606
  }
607

    
608
  /**
609
   * Deletes entities which were not found during processing.
610
   *
611
   * @todo batch delete?
612
   *
613
   * @param FeedsState $state
614
   *   The FeedsState object for the given stage.
615
   */
616
  protected function clean(FeedsState $state) {
617
    // We clean only if needed.
618
    if ($this->config['update_non_existent'] != FEEDS_DELETE_NON_EXISTENT) {
619
      return;
620
    }
621

    
622
    if ($total = count($state->removeList)) {
623
      $this->entityDeleteMultiple($state->removeList);
624
      $state->deleted += $total;
625
    }
626
  }
627

    
628
  /**
629
   * Removes all imported items for a source.
630
   *
631
   * @param FeedsSource $source
632
   *   Source information for this expiry. Implementers should only delete items
633
   *   pertaining to this source. The preferred way of determining whether an
634
   *   item pertains to a certain souce is by using $source->feed_nid. It is the
635
   *   processor's responsibility to store the feed_nid of an imported item in
636
   *   the processing stage.
637
   */
638
  public function clear(FeedsSource $source) {
639
    $state = $source->state(FEEDS_PROCESS_CLEAR);
640

    
641
    // Build base select statement.
642
    $select = db_select('feeds_item')
643
      ->fields('feeds_item', array('entity_id'))
644
      ->condition('entity_type', $this->entityType())
645
      ->condition('id', $this->id)
646
      ->condition('feed_nid', $source->feed_nid);
647

    
648
    // If there is no total, query it.
649
    if (!$state->total) {
650
      $state->total = $select->countQuery()->execute()->fetchField();
651
    }
652

    
653
    // Delete a batch of entities.
654
    $entity_ids = $select->range(0, $this->getLimit())->execute()->fetchCol();
655
    $this->entityDeleteMultiple($entity_ids);
656

    
657
    // Report progress, take into account that we may not have deleted as many
658
    // items as we have counted at first.
659
    if ($deleted_count = count($entity_ids)) {
660
      $state->deleted += $deleted_count;
661
      $state->progress($state->total, $state->deleted);
662
    }
663
    else {
664
      $state->progress($state->total, $state->total);
665
    }
666

    
667
    // Report results when done.
668
    if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
669
      $info = $this->entityInfo();
670

    
671
      if ($state->deleted) {
672
        $message = format_plural(
673
          $state->deleted,
674
          'Deleted @number @entity',
675
          'Deleted @number @entities',
676
          array(
677
            '@number' => $state->deleted,
678
            '@entity' => strtolower($info['label']),
679
            '@entities' => strtolower($info['label plural']),
680
          )
681
        );
682
        $source->log('clear', $message, array(), WATCHDOG_INFO);
683
        drupal_set_message($message);
684
      }
685
      else {
686
        drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
687
      }
688
    }
689
  }
690

    
691
  /**
692
   * Report number of items that can be processed per call.
693
   *
694
   * 0 means 'unlimited'.
695
   *
696
   * If a number other than 0 is given, Feeds parsers that support batching
697
   * will only deliver this limit to the processor.
698
   *
699
   * @see FeedsSource::getLimit()
700
   * @see FeedsCSVParser::parse()
701
   */
702
  public function getLimit() {
703
    return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
704
  }
705

    
706
  /**
707
   * Deletes feed items older than REQUEST_TIME - $time.
708
   *
709
   * Do not invoke expire on a processor directly, but use
710
   * FeedsSource::expire() instead.
711
   *
712
   * @param FeedsSource $source
713
   *   The source to expire entities for.
714
   * @param int|null $time
715
   *   (optional) All items produced by this configuration that are older than
716
   *   REQUEST_TIME - $time should be deleted. If NULL, processor should use
717
   *   internal configuration. Defaults to NULL.
718
   *
719
   * @return float|null
720
   *   FEEDS_BATCH_COMPLETE if all items have been processed, null if expiring
721
   *   is disabled, a float between 0 and 0.99* indicating progress otherwise.
722
   *
723
   * @see FeedsSource::expire()
724
   */
725
  public function expire(FeedsSource $source, $time = NULL) {
726
    $state = $source->state(FEEDS_PROCESS_EXPIRE);
727

    
728
    if ($time === NULL) {
729
      $time = $this->expiryTime();
730
    }
731
    if ($time == FEEDS_EXPIRE_NEVER) {
732
      return;
733
    }
734

    
735
    $select = $this->expiryQuery($source, $time);
736

    
737
    // If there is no total, query it.
738
    if (!$state->total) {
739
      $state->total = $select->countQuery()->execute()->fetchField();
740
    }
741

    
742
    // Delete a batch of entities.
743
    $entity_ids = $select->range(0, $this->getLimit())->execute()->fetchCol();
744
    if ($entity_ids) {
745
      $this->entityDeleteMultiple($entity_ids);
746
      $state->deleted += count($entity_ids);
747
      $state->progress($state->total, $state->deleted);
748
    }
749
    else {
750
      $state->progress($state->total, $state->total);
751
    }
752
  }
753

    
754
  /**
755
   * Returns a database query used to select entities to expire.
756
   *
757
   * Processor classes should override this method to set the age portion of the
758
   * query.
759
   *
760
   * @param FeedsSource $source
761
   *   The feed source.
762
   * @param int $time
763
   *   Delete entities older than this.
764
   *
765
   * @return SelectQuery
766
   *   A select query to execute.
767
   *
768
   * @see FeedsNodeProcessor::expiryQuery()
769
   */
770
  protected function expiryQuery(FeedsSource $source, $time) {
771
    // Build base select statement.
772
    $info = $this->entityInfo();
773
    $id_key = $info['entity keys']['id'];
774

    
775
    $select = db_select($info['base table'], 'e');
776
    $select->addField('e', $id_key);
777
    $select->join('feeds_item', 'fi', "e.$id_key = fi.entity_id");
778
    $select->condition('fi.entity_type', $this->entityType());
779
    $select->condition('fi.id', $this->id);
780
    $select->condition('fi.feed_nid', $source->feed_nid);
781

    
782
    return $select;
783
  }
784

    
785
  /**
786
   * Counts the number of items imported by this processor.
787
   *
788
   * @return int
789
   *   The number of items imported by this processor.
790
   */
791
  public function itemCount(FeedsSource $source) {
792
    return db_query("SELECT count(*) FROM {feeds_item} WHERE id = :id AND entity_type = :entity_type AND feed_nid = :feed_nid", array(
793
      ':id' => $this->id,
794
      ':entity_type' => $this->entityType(),
795
      ':feed_nid' => $source->feed_nid,
796
    ))->fetchField();
797
  }
798

    
799
  /**
800
   * Returns a statically cached version of the target mappings.
801
   *
802
   * @return array
803
   *   The targets for this importer.
804
   */
805
  protected function getCachedTargets() {
806
    $targets = &drupal_static('FeedsProcessor::getCachedTargets', array());
807

    
808
    if (!isset($targets[$this->id])) {
809
      $targets[$this->id] = $this->getMappingTargets();
810
    }
811

    
812
    return $targets[$this->id];
813
  }
814

    
815
  /**
816
   * Returns a statically cached version of the source mappings.
817
   *
818
   * @return array
819
   *   The sources for this importer.
820
   */
821
  protected function getCachedSources() {
822
    $sources = &drupal_static('FeedsProcessor::getCachedSources', array());
823

    
824
    if (!isset($sources[$this->id])) {
825

    
826
      $sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
827

    
828
      if (is_array($sources[$this->id])) {
829
        foreach ($sources[$this->id] as $source_key => $source) {
830
          if (empty($source['callback']) || !is_callable($source['callback'])) {
831
            unset($sources[$this->id][$source_key]['callback']);
832
          }
833
        }
834
      }
835
    }
836

    
837
    return $sources[$this->id];
838
  }
839

    
840
  /**
841
   * Execute mapping on an item.
842
   *
843
   * This method encapsulates the central mapping functionality. When an item is
844
   * processed, it is passed through map() where the properties of $source_item
845
   * are mapped onto $target_item following the processor's mapping
846
   * configuration.
847
   *
848
   * For each mapping FeedsParser::getSourceElement() is executed to retrieve
849
   * the source element, then FeedsProcessor::setTargetElement() is invoked
850
   * to populate the target item properly. Alternatively a
851
   * hook_x_targets_alter() may have specified a callback for a mapping target
852
   * in which case the callback is asked to populate the target item instead of
853
   * FeedsProcessor::setTargetElement().
854
   *
855
   * @ingroup mappingapi
856
   *
857
   * @see hook_feeds_parser_sources_alter()
858
   * @see hook_feeds_processor_targets()
859
   * @see hook_feeds_processor_targets_alter()
860
   */
861
  protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
862
    $targets = $this->getCachedTargets();
863
    // Get fields for the entity type we are mapping to.
864
    $fields = field_info_instances($this->entityType(), $this->bundle());
865

    
866
    if (empty($target_item)) {
867
      $target_item = array();
868
    }
869

    
870
    // Many mappers add to existing fields rather than replacing them. Hence we
871
    // need to clear target elements of each item before mapping in case we are
872
    // mapping on a prepopulated item such as an existing node.
873
    foreach ($this->getMappings() as $mapping) {
874
      if (isset($targets[$mapping['target']]['real_target'])) {
875
        $target_name = $targets[$mapping['target']]['real_target'];
876
      }
877
      else {
878
        $target_name = $mapping['target'];
879
      }
880

    
881
      // If the target is a field empty the value for the targeted language
882
      // only.
883
      // In all other cases, just empty the target completely.
884
      if (isset($fields[$target_name])) {
885
        // Empty the target for the specified language.
886
        $target_item->{$target_name}[$mapping['language']] = array();
887
      }
888
      else {
889
        // Empty the whole target.
890
        $target_item->{$target_name} = NULL;
891
      }
892
    }
893

    
894
    // This is where the actual mapping happens: For every mapping we invoke
895
    // the parser's getSourceElement() method to retrieve the value of the
896
    // source element and pass it to the processor's setTargetElement() to stick
897
    // it on the right place of the target item.
898
    foreach ($this->getMappings() as $mapping) {
899
      $value = $this->getSourceValue($source, $result, $mapping['source']);
900

    
901
      $this->mapToTarget($source, $mapping['target'], $target_item, $value, $mapping);
902
    }
903

    
904
    return $target_item;
905
  }
906

    
907
  /**
908
   * Returns the values from the parser, or callback.
909
   *
910
   * @param FeedsSource $source
911
   *   The feed source.
912
   * @param FeedsParserResult $result
913
   *   The parser result.
914
   * @param string $source_key
915
   *   The current key being processed.
916
   *
917
   * @return mixed
918
   *   A value, or a list of values.
919
   */
920
  protected function getSourceValue(FeedsSource $source, FeedsParserResult $result, $source_key) {
921
    $sources = $this->getCachedSources();
922

    
923
    if (isset($sources[$source_key]['callback'])) {
924
      return call_user_func($sources[$source_key]['callback'], $source, $result, $source_key);
925
    }
926

    
927
    return feeds_importer($this->id)->parser->getSourceElement($source, $result, $source_key);
928
  }
929

    
930
  /**
931
   * Maps values onto the target item.
932
   *
933
   * @param FeedsSource $source
934
   *   The feed source.
935
   * @param mixed &$target_item
936
   *   The target item to apply values into.
937
   * @param mixed $value
938
   *   A value, or a list of values.
939
   * @param array $mapping
940
   *   The mapping configuration.
941
   */
942
  protected function mapToTarget(FeedsSource $source, $target, &$target_item, $value, array $mapping) {
943
    $targets = $this->getCachedTargets();
944

    
945
    // Map the source element's value to the target.
946
    // If the mapping specifies a callback method, use the callback instead of
947
    // setTargetElement().
948
    if (isset($targets[$target]['callback'])) {
949

    
950
      // All target callbacks expect an array.
951
      if (!is_array($value)) {
952
        $value = array($value);
953
      }
954

    
955
      call_user_func($targets[$target]['callback'], $source, $target_item, $target, $value, $mapping);
956
    }
957

    
958
    else {
959
      $this->setTargetElement($source, $target_item, $target, $value, $mapping);
960
    }
961
  }
962

    
963
  /**
964
   * Returns the time in seconds after which items should be removed.
965
   *
966
   * If expiry of items isn't supported, FEEDS_EXPIRE_NEVER is returned.
967
   *
968
   * By default, expiring items isn't supported. Processors that want to support
969
   * this feature, should override this method.
970
   *
971
   * @return int
972
   *   The expiry time or FEEDS_EXPIRE_NEVER if expiry isn't supported.
973
   */
974
  public function expiryTime() {
975
    return FEEDS_EXPIRE_NEVER;
976
  }
977

    
978
  /**
979
   * {@inheritdoc}
980
   */
981
  public function configDefaults() {
982
    $info = $this->entityInfo();
983
    $bundle = NULL;
984
    if (empty($info['entity keys']['bundle'])) {
985
      $bundle = $this->entityType();
986
    }
987
    return array(
988
      'mappings' => array(),
989
      'insert_new' => FEEDS_INSERT_NEW,
990
      'update_existing' => FEEDS_SKIP_EXISTING,
991
      'update_non_existent' => FEEDS_SKIP_NON_EXISTENT,
992
      'input_format' => NULL,
993
      'skip_hash_check' => FALSE,
994
      'bundle' => $bundle,
995
      'language' => LANGUAGE_NONE,
996
    ) + parent::configDefaults();
997
  }
998

    
999
  /**
1000
   * Validates the configuration.
1001
   *
1002
   * @return array
1003
   *   A list of errors.
1004
   */
1005
  public function validateConfig() {
1006
    $errors = parent::validateConfig();
1007
    $info = $this->entityInfo();
1008
    $config = $this->getConfig();
1009

    
1010
    // Check configured bundle if the bundle is configurable.
1011
    if (isset($config['bundle']) && !empty($info['entity keys']['bundle'])) {
1012
      $bundles = $this->bundleOptions();
1013
      if (!in_array($config['bundle'], array_keys($bundles))) {
1014
        $errors[] = t('Invalid value %value for config option %key.', array(
1015
          '%value' => $config['bundle'],
1016
          '%key' => !empty($info['bundle name']) ? $info['bundle name'] : t('Bundle'),
1017
        ));
1018
      }
1019
    }
1020

    
1021
    // Check configured language.
1022
    if (module_exists('locale') && !empty($info['entity keys']['language']) && isset($config['language'])) {
1023
      $languages = $this->languageOptions();
1024
      if (!isset($languages[$config['language']])) {
1025
        $errors[] = t('Invalid value %value for config option %key.', array(
1026
          '%value' => $config['language'],
1027
          '%key' => t('Language'),
1028
        ));
1029
      }
1030
    }
1031

    
1032
    return $errors;
1033
  }
1034

    
1035
  /**
1036
   * {@inheritdoc}
1037
   */
1038
  public function configForm(&$form_state) {
1039
    $info = $this->entityInfo();
1040
    $form = array();
1041

    
1042
    if (!empty($info['entity keys']['bundle'])) {
1043
      $bundle = $this->bundle();
1044
      $form['bundle'] = array(
1045
        '#type' => 'select',
1046
        '#options' => $this->bundleOptions(),
1047
        '#title' => !empty($info['bundle name']) ? $info['bundle name'] : t('Bundle'),
1048
        '#required' => TRUE,
1049
        '#default_value' => $bundle,
1050
      );
1051

    
1052
      // Add default value as one of the options if not yet available.
1053
      if (!isset($form['bundle']['#options'][$bundle])) {
1054
        $form['bundle']['#options'][$bundle] = t('Unknown bundle: @bundle', array(
1055
          '@bundle' => $bundle,
1056
        ));
1057
      }
1058
    }
1059
    else {
1060
      $form['bundle'] = array(
1061
        '#type' => 'value',
1062
        '#value' => $this->entityType(),
1063
      );
1064
    }
1065

    
1066
    if (module_exists('locale') && !empty($info['entity keys']['language'])) {
1067
      $form['language'] = array(
1068
        '#type' => 'select',
1069
        '#options' => $this->languageOptions(),
1070
        '#title' => t('Language'),
1071
        '#required' => TRUE,
1072
        '#default_value' => $this->config['language'],
1073
      );
1074

    
1075
      // Add default value as one of the options if not yet available.
1076
      if (!isset($form['language']['#options'][$this->config['language']])) {
1077
        $form['language']['#options'][$this->config['language']] = t('Unknown language: @language', array(
1078
          '@language' => $this->config['language'],
1079
        ));
1080
      }
1081
    }
1082

    
1083
    $tokens = array('@entities' => strtolower($info['label plural']));
1084

    
1085
    $form['insert_new'] = array(
1086
      '#type' => 'radios',
1087
      '#title' => t('Insert new @entities', $tokens),
1088
      '#description' => t('New @entities will be determined using mappings that are a "unique target".', $tokens),
1089
      '#options' => array(
1090
        FEEDS_INSERT_NEW => t('Insert new @entities', $tokens),
1091
        FEEDS_SKIP_NEW => t('Do not insert new @entities', $tokens),
1092
      ),
1093
      '#default_value' => $this->config['insert_new'],
1094
    );
1095

    
1096
    $form['update_existing'] = array(
1097
      '#type' => 'radios',
1098
      '#title' => t('Update existing @entities', $tokens),
1099
      '#description' => t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
1100
      '#options' => array(
1101
        FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
1102
        FEEDS_REPLACE_EXISTING => t('Replace existing @entities', $tokens),
1103
        FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
1104
      ),
1105
      '#default_value' => $this->config['update_existing'],
1106
      '#after_build' => array('feeds_processor_config_form_update_existing_after_build'),
1107
      '#tokens' => $tokens,
1108
    );
1109
    global $user;
1110
    $formats = filter_formats($user);
1111
    foreach ($formats as $format) {
1112
      $format_options[$format->format] = $format->name;
1113
    }
1114
    $form['skip_hash_check'] = array(
1115
      '#type' => 'checkbox',
1116
      '#title' => t('Skip hash check'),
1117
      '#description' => t('Force update of items even if item source data did not change.'),
1118
      '#default_value' => $this->config['skip_hash_check'],
1119
    );
1120
    $form['input_format'] = array(
1121
      '#type' => 'select',
1122
      '#title' => t('Text format'),
1123
      '#description' => t('Select the default input format for the text fields of the nodes to be created.'),
1124
      '#options' => $format_options,
1125
      '#default_value' => isset($this->config['input_format']) ? $this->config['input_format'] : 'plain_text',
1126
      '#required' => TRUE,
1127
    );
1128

    
1129
    $form['update_non_existent'] = array(
1130
      '#type' => 'radios',
1131
      '#title' => t('Action to take when previously imported @entities are missing in the feed', $tokens),
1132
      '#description' => t('Select how @entities previously imported and now missing in the feed should be updated.', $tokens),
1133
      '#options' => array(
1134
        FEEDS_SKIP_NON_EXISTENT => t('Skip non-existent @entities', $tokens),
1135
        FEEDS_DELETE_NON_EXISTENT => t('Delete non-existent @entities', $tokens),
1136
      ),
1137
      '#default_value' => $this->config['update_non_existent'],
1138
    );
1139

    
1140
    return $form;
1141
  }
1142

    
1143
  /**
1144
   * Get mappings.
1145
   *
1146
   * @return array
1147
   *   A list of configured mappings.
1148
   */
1149
  public function getMappings() {
1150
    $cache = &drupal_static('FeedsProcessor::getMappings', array());
1151

    
1152
    if (!isset($cache[$this->id])) {
1153
      $mappings = $this->config['mappings'];
1154
      $targets = $this->getCachedTargets();
1155
      $languages = $this->languageOptions();
1156

    
1157
      foreach ($mappings as &$mapping) {
1158

    
1159
        if (isset($targets[$mapping['target']]['preprocess_callbacks'])) {
1160
          foreach ($targets[$mapping['target']]['preprocess_callbacks'] as $callback) {
1161
            call_user_func_array($callback, array($targets[$mapping['target']], &$mapping));
1162
          }
1163
        }
1164

    
1165
        // Ensure there's always a language set.
1166
        if (empty($mapping['language'])) {
1167
          $mapping['language'] = LANGUAGE_NONE;
1168
        }
1169
        else {
1170
          // Check if the configured language is available. If not, fallback to
1171
          // LANGUAGE_NONE.
1172
          if (!isset($languages[$mapping['language']])) {
1173
            $mapping['language'] = LANGUAGE_NONE;
1174
          }
1175
        }
1176
      }
1177

    
1178
      $cache[$this->id] = $mappings;
1179
    }
1180

    
1181
    return $cache[$this->id];
1182
  }
1183

    
1184
  /**
1185
   * Declare possible mapping targets that this processor exposes.
1186
   *
1187
   * @ingroup mappingapi
1188
   *
1189
   * @return array
1190
   *   An array of mapping targets. Keys are paths to targets
1191
   *   separated by ->, values are TRUE if target can be unique,
1192
   *   FALSE otherwise.
1193
   */
1194
  public function getMappingTargets() {
1195

    
1196
    // The bundle has not been selected.
1197
    if (!$this->bundle()) {
1198
      $info = $this->entityInfo();
1199
      $bundle_name = !empty($info['bundle name']) ? drupal_strtolower($info['bundle name']) : t('bundle');
1200
      $plugin_key = feeds_importer($this->id)->config['processor']['plugin_key'];
1201
      $url = url('admin/structure/feeds/' . $this->id . '/settings/' . $plugin_key);
1202
      drupal_set_message(t('Please <a href="@url">select a @bundle_name</a>.', array('@url' => $url, '@bundle_name' => $bundle_name)), 'warning', FALSE);
1203
    }
1204

    
1205
    return array(
1206
      'url' => array(
1207
        'name' => t('URL'),
1208
        'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
1209
        'optional_unique' => TRUE,
1210
      ),
1211
      'guid' => array(
1212
        'name' => t('GUID'),
1213
        '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.'),
1214
        'optional_unique' => TRUE,
1215
      ),
1216
    );
1217
  }
1218

    
1219
  /**
1220
   * Allows other modules to expose targets.
1221
   *
1222
   * @param array &$targets
1223
   *   The existing target array.
1224
   */
1225
  protected function getHookTargets(array &$targets) {
1226
    self::loadMappers();
1227

    
1228
    $entity_type = $this->entityType();
1229
    $bundle = $this->bundle();
1230
    $targets += module_invoke_all('feeds_processor_targets', $entity_type, $bundle);
1231

    
1232
    drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
1233
  }
1234

    
1235
  /**
1236
   * Set a concrete target element. Invoked from FeedsProcessor::map().
1237
   *
1238
   * @ingroup mappingapi
1239
   */
1240
  public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
1241
    switch ($target_element) {
1242
      case 'url':
1243
      case 'guid':
1244
        $target_item->feeds_item->$target_element = $value;
1245
        break;
1246

    
1247
      default:
1248
        $target_item->$target_element = $value;
1249
        break;
1250
    }
1251
  }
1252

    
1253
  /**
1254
   * Retrieve the target entity's existing id if available. Otherwise return 0.
1255
   *
1256
   * @param FeedsSource $source
1257
   *   The source information about this import.
1258
   * @param FeedsParserResult $result
1259
   *   A FeedsParserResult object.
1260
   *
1261
   * @return int
1262
   *   The serial id of an entity if found, 0 otherwise.
1263
   *
1264
   * @ingroup mappingapi
1265
   */
1266
  protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
1267
    $targets = $this->getCachedTargets();
1268

    
1269
    $entity_id = 0;
1270

    
1271
    // Iterate through all unique targets and test whether they already exist in
1272
    // the database.
1273
    foreach ($this->uniqueTargets($source, $result) as $target => $value) {
1274
      if ($target === 'guid' || $target === 'url') {
1275
        $entity_id = db_select('feeds_item')
1276
          ->fields('feeds_item', array('entity_id'))
1277
          ->condition('feed_nid', $source->feed_nid)
1278
          ->condition('entity_type', $this->entityType())
1279
          ->condition('id', $source->id)
1280
          ->condition($target, $value)
1281
          ->execute()
1282
          ->fetchField();
1283
      }
1284

    
1285
      if (!$entity_id && !empty($targets[$target]['unique_callbacks'])) {
1286
        if (!is_array($value)) {
1287
          $value = array($value);
1288
        }
1289

    
1290
        foreach ($targets[$target]['unique_callbacks'] as $callback) {
1291
          if ($entity_id = call_user_func($callback, $source, $this->entityType(), $this->bundle(), $target, $value)) {
1292
            // Stop at the first unique ID returned by a callback.
1293
            break;
1294
          }
1295
        }
1296
      }
1297

    
1298
      // Return with the content id found.
1299
      if ($entity_id) {
1300
        return $entity_id;
1301
      }
1302
    }
1303

    
1304
    return $entity_id;
1305
  }
1306

    
1307
  /**
1308
   * Returns all mapping targets that are marked as unique.
1309
   *
1310
   * @param FeedsSource $source
1311
   *   The source information about this import.
1312
   * @param FeedsParserResult $result
1313
   *   A FeedsParserResult object.
1314
   *
1315
   * @return array
1316
   *   An array where the keys are target field names and the values are the
1317
   *   elements from the source item mapped to these targets.
1318
   */
1319
  protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
1320
    $parser = feeds_importer($this->id)->parser;
1321
    $targets = array();
1322
    foreach ($this->getMappings() as $mapping) {
1323
      if (!empty($mapping['unique'])) {
1324
        // Invoke the parser's getSourceElement to retrieve the value for this
1325
        // mapping's source.
1326
        $targets[$mapping['target']] = $parser->getSourceElement($source, $result, $mapping['source']);
1327
      }
1328
    }
1329
    return $targets;
1330
  }
1331

    
1332
  /**
1333
   * Adds Feeds specific information on $entity->feeds_item.
1334
   *
1335
   * @param object $entity
1336
   *   The entity object to be populated with new item info.
1337
   * @param int $feed_nid
1338
   *   The feed nid of the source that produces this entity.
1339
   * @param string $hash
1340
   *   The fingerprint of the source item.
1341
   */
1342
  protected function newItemInfo($entity, $feed_nid, $hash = '') {
1343
    $entity->feeds_item = new stdClass();
1344
    $entity->feeds_item->is_new = TRUE;
1345
    $entity->feeds_item->entity_id = 0;
1346
    $entity->feeds_item->entity_type = $this->entityType();
1347
    $entity->feeds_item->id = $this->id;
1348
    $entity->feeds_item->feed_nid = $feed_nid;
1349
    $entity->feeds_item->imported = REQUEST_TIME;
1350
    $entity->feeds_item->hash = $hash;
1351
    $entity->feeds_item->url = '';
1352
    $entity->feeds_item->guid = '';
1353
  }
1354

    
1355
  /**
1356
   * Loads existing entity information and places it on $entity->feeds_item.
1357
   *
1358
   * @param object $entity
1359
   *   The entity object to load item info for. Id key must be present.
1360
   *
1361
   * @return bool
1362
   *   TRUE if item info could be loaded, false if not.
1363
   */
1364
  protected function loadItemInfo($entity) {
1365
    $entity_info = entity_get_info($this->entityType());
1366
    $key = $entity_info['entity keys']['id'];
1367
    if ($item_info = feeds_item_info_load($this->entityType(), $entity->$key)) {
1368
      $entity->feeds_item = $item_info;
1369
      return TRUE;
1370
    }
1371
    return FALSE;
1372
  }
1373

    
1374
  /**
1375
   * Create MD5 hash of item and mappings array.
1376
   *
1377
   * Include mappings as a change in mappings may have an affect on the item
1378
   * produced.
1379
   *
1380
   * @return string
1381
   *   A hash is always returned, even when the item is empty, NULL or FALSE.
1382
   */
1383
  protected function hash($item) {
1384
    $sources = feeds_importer($this->id)->parser->getMappingSourceList();
1385
    $mapped_item = array_intersect_key($item, array_flip($sources));
1386
    return hash('md5', serialize($mapped_item) . serialize($this->getMappings()));
1387
  }
1388

    
1389
  /**
1390
   * Retrieves the MD5 hash of $entity_id from the database.
1391
   *
1392
   * @return string
1393
   *   Empty string if no item is found, hash otherwise.
1394
   */
1395
  protected function getHash($entity_id) {
1396

    
1397
    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()) {
1398
      // Return with the hash.
1399
      return $hash;
1400
    }
1401
    return '';
1402
  }
1403

    
1404
  /**
1405
   * DEPRECATED: Creates a log message for exceptions during import.
1406
   *
1407
   * Don't use this method as it concatenates user variables into the log
1408
   * message, which will pollute the locales_source table when the log message
1409
   * is translated. Use ::createLogEntry instead.
1410
   *
1411
   * @param Exception $e
1412
   *   The exception that was throwned during processing the item.
1413
   * @param object $entity
1414
   *   The entity object.
1415
   * @param object $item
1416
   *   The parser result for this entity.
1417
   *
1418
   * @return string
1419
   *   The message to log.
1420
   *
1421
   * @deprecated
1422
   *   Use ::createLogEntry instead.
1423
   */
1424
  protected function createLogMessage(Exception $e, $entity, $item) {
1425
    $message = $e->getMessage();
1426
    $message .= '<h3>Original item</h3>';
1427
    // $this->exportObjectVars() already runs check_plain() for us, so we can
1428
    // concatenate here as is.
1429
    $message .= '<pre>' . $this->exportObjectVars($item) . '</pre>';
1430
    $message .= '<h3>Entity</h3>';
1431
    $message .= '<pre>' . $this->exportObjectVars($entity) . '</pre>';
1432

    
1433
    return $message;
1434
  }
1435

    
1436
  /**
1437
   * Creates a log entry for when an exception occurred during import.
1438
   *
1439
   * @param Exception $e
1440
   *   The exception that was throwned during processing the item.
1441
   * @param object $entity
1442
   *   The entity object.
1443
   * @param array $item
1444
   *   The parser result for this entity.
1445
   *
1446
   * @return array
1447
   *   The message and arguments to log.
1448
   */
1449
  protected function createLogEntry(Exception $e, $entity, $item) {
1450
    $message = '@exception';
1451
    $message .= '<h3>Original item</h3>';
1452
    $message .= '<pre>!item</pre>';
1453
    $message .= '<h3>Entity</h3>';
1454
    $message .= '<pre>!entity</pre>';
1455
    $arguments = array(
1456
      '@exception' => $e->getMessage(),
1457
      // $this->exportObjectVars() already runs check_plain() for us, so we can
1458
      // use the "!" placeholder.
1459
      '!item' => $this->exportObjectVars($item),
1460
      '!entity' => $this->exportObjectVars($entity),
1461
    );
1462

    
1463
    return array($message, $arguments);
1464
  }
1465

    
1466
  /**
1467
   * Returns a string representation of an object or array for log messages.
1468
   *
1469
   * @param object|array $object
1470
   *   The object to convert.
1471
   *
1472
   * @return string
1473
   *   The sanitized string representation of the object.
1474
   */
1475
  protected function exportObjectVars($object) {
1476
    include_once DRUPAL_ROOT . '/includes/utility.inc';
1477

    
1478
    $out = is_array($object) ? $object : get_object_vars($object);
1479
    $out = array_filter($out, 'is_scalar');
1480

    
1481
    foreach ($out as $key => $value) {
1482
      if (is_string($value)) {
1483
        if (function_exists('mb_check_encoding') && !mb_check_encoding($value, 'UTF-8')) {
1484
          $value = utf8_encode($value);
1485
        }
1486
        $out[$key] = truncate_utf8($value, 100, FALSE, TRUE);
1487
      }
1488
    }
1489

    
1490
    if (is_array($object)) {
1491
      return check_plain(drupal_var_export($out));
1492
    }
1493

    
1494
    return check_plain(drupal_var_export((object) $out));
1495
  }
1496

    
1497
  /**
1498
   * Overrides FeedsPlugin::dependencies().
1499
   */
1500
  public function dependencies() {
1501
    $dependencies = parent::dependencies();
1502

    
1503
    // Find out which module defined the entity type.
1504
    $info = $this->entityInfo();
1505
    if (isset($info['module'])) {
1506
      $dependencies[$info['module']] = $info['module'];
1507
    }
1508

    
1509
    return $dependencies;
1510
  }
1511

    
1512
}
1513

    
1514
/**
1515
 * Form after build callback for the field "update_existing".
1516
 *
1517
 * Adds descriptions to options of this field.
1518
 */
1519
function feeds_processor_config_form_update_existing_after_build($field) {
1520
  $field[FEEDS_REPLACE_EXISTING]['#description'] = t('Loads records directly from the database, bypassing the Entity API. Faster, but use with caution.');
1521
  $field[FEEDS_UPDATE_EXISTING]['#description'] = t('Loads complete @entities using the Entity API for full integration with other modules. Slower, but most reliable.', $field['#tokens']);
1522
  return $field;
1523
}