Projet

Général

Profil

Paste
Télécharger (26 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / feeds / includes / FeedsSource.inc @ 651307cd

1
<?php
2

    
3
/**
4
 * @file
5
 * Definition of FeedsSourceInterface and FeedsSource class.
6
 */
7

    
8
/**
9
 * Distinguish exceptions occurring when handling locks.
10
 */
11
class FeedsLockException extends Exception {}
12

    
13
/**
14
 * Denote a import or clearing stage. Used for multi page processing.
15
 */
16
define('FEEDS_START', 'start_time');
17
define('FEEDS_FETCH', 'fetch');
18
define('FEEDS_PARSE', 'parse');
19
define('FEEDS_PROCESS', 'process');
20
define('FEEDS_PROCESS_CLEAR', 'process_clear');
21
define('FEEDS_PROCESS_EXPIRE', 'process_expire');
22

    
23
/**
24
 * Declares an interface for a class that defines default values and form
25
 * descriptions for a FeedSource.
26
 */
27
interface FeedsSourceInterface {
28

    
29
  /**
30
   * Crutch: for ease of use, we implement FeedsSourceInterface for every
31
   * plugin, but then we need to have a handle which plugin actually implements
32
   * a source.
33
   *
34
   * @see FeedsPlugin class.
35
   *
36
   * @return
37
   *   TRUE if a plugin handles source specific configuration, FALSE otherwise.
38
   */
39
  public function hasSourceConfig();
40

    
41
  /**
42
   * Return an associative array of default values.
43
   */
44
  public function sourceDefaults();
45

    
46
  /**
47
   * Return a Form API form array that defines a form configuring values. Keys
48
   * correspond to the keys of the return value of sourceDefaults().
49
   */
50
  public function sourceForm($source_config);
51

    
52
  /**
53
   * Validate user entered values submitted by sourceForm().
54
   */
55
  public function sourceFormValidate(&$source_config);
56

    
57
  /**
58
   * A source is being saved.
59
   */
60
  public function sourceSave(FeedsSource $source);
61

    
62
  /**
63
   * A source is being deleted.
64
   */
65
  public function sourceDelete(FeedsSource $source);
66
}
67

    
68
/**
69
 * Status of an import or clearing operation on a source.
70
 */
71
class FeedsState {
72
  /**
73
   * Floating point number denoting the progress made. 0.0 meaning no progress
74
   * 1.0 = FEEDS_BATCH_COMPLETE meaning finished.
75
   */
76
  public $progress;
77

    
78
  /**
79
   * Used as a pointer to store where left off. Must be serializable.
80
   */
81
  public $pointer;
82

    
83
  /**
84
   * Natural numbers denoting more details about the progress being made.
85
   */
86
  public $total;
87
  public $created;
88
  public $updated;
89
  public $deleted;
90
  public $unpublished;
91
  public $blocked;
92
  public $skipped;
93
  public $failed;
94

    
95
  /**
96
   * IDs of entities to be removed.
97
   */
98
  public $removeList;
99

    
100
  /**
101
   * Constructor, initialize variables.
102
   */
103
  public function __construct() {
104
    $this->progress = FEEDS_BATCH_COMPLETE;
105
    $this->total =
106
    $this->created =
107
    $this->updated =
108
    $this->deleted =
109
    $this->unpublished =
110
    $this->blocked =
111
    $this->skipped =
112
    $this->failed = 0;
113
  }
114

    
115
  /**
116
   * Safely report progress.
117
   *
118
   * When $total == $progress, the state of the task tracked by this state is
119
   * regarded to be complete.
120
   *
121
   * Handles the following cases gracefully:
122
   *
123
   * - $total is 0
124
   * - $progress is larger than $total
125
   * - $progress approximates $total so that $finished rounds to 1.0
126
   *
127
   * @param $total
128
   *   A natural number that is the total to be worked off.
129
   * @param $progress
130
   *   A natural number that is the progress made on $total.
131
   */
132
  public function progress($total, $progress) {
133
    if ($progress > $total) {
134
      $this->progress = FEEDS_BATCH_COMPLETE;
135
    }
136
    elseif ($total) {
137
      $this->progress = (float) $progress / $total;
138
      if ($this->progress == FEEDS_BATCH_COMPLETE && $total != $progress) {
139
        $this->progress = 0.99;
140
      }
141
    }
142
    else {
143
      $this->progress = FEEDS_BATCH_COMPLETE;
144
    }
145
  }
146
}
147

    
148
/**
149
 * This class encapsulates a source of a feed. It stores where the feed can be
150
 * found and how to import it.
151
 *
152
 * Information on how to import a feed is encapsulated in a FeedsImporter object
153
 * which is identified by the common id of the FeedsSource and the
154
 * FeedsImporter. More than one FeedsSource can use the same FeedsImporter
155
 * therefore a FeedsImporter never holds a pointer to a FeedsSource object, nor
156
 * does it hold any other information for a particular FeedsSource object.
157
 *
158
 * Classes extending FeedsPlugin can implement a sourceForm to expose
159
 * configuration for a FeedsSource object. This is for instance how FeedsFetcher
160
 * exposes a text field for a feed URL or how FeedsCSVParser exposes a select
161
 * field for choosing between colon or semicolon delimiters.
162
 *
163
 * It is important that a FeedsPlugin does not directly hold information about
164
 * a source but leave all storage up to FeedsSource. An instance of a
165
 * FeedsPlugin class only exists once per FeedsImporter configuration, while an
166
 * instance of a FeedsSource class exists once per feed_nid to be imported.
167
 *
168
 * As with FeedsImporter, the idea with FeedsSource is that it can be used
169
 * without actually saving the object to the database.
170
 */
171
class FeedsSource extends FeedsConfigurable {
172

    
173
  // Contains the node id of the feed this source info object is attached to.
174
  // Equals 0 if not attached to any node - i. e. if used on a
175
  // standalone import form within Feeds or by other API users.
176
  protected $feed_nid;
177

    
178
  // The FeedsImporter object that this source is expected to be used with.
179
  protected $importer;
180

    
181
  // A FeedsSourceState object holding the current import/clearing state of this
182
  // source.
183
  protected $state;
184

    
185
  // Fetcher result, used to cache fetcher result when batching.
186
  protected $fetcher_result;
187

    
188
  // Timestamp when this source was imported the last time.
189
  protected $imported;
190

    
191
  // Holds an exception object in case an exception occurs during importing.
192
  protected $exception;
193

    
194
  /**
195
   * Instantiate a unique object per class/id/feed_nid. Don't use
196
   * directly, use feeds_source() instead.
197
   */
198
  public static function instance($importer_id, $feed_nid) {
199
    $class = variable_get('feeds_source_class', 'FeedsSource');
200

    
201
    $instances = &drupal_static(__METHOD__, array());
202

    
203
    if (!isset($instances[$class][$importer_id][$feed_nid])) {
204
      $instances[$class][$importer_id][$feed_nid] = new $class($importer_id, $feed_nid);
205
    }
206
    return $instances[$class][$importer_id][$feed_nid];
207
  }
208

    
209
  /**
210
   * Constructor.
211
   */
212
  protected function __construct($importer_id, $feed_nid) {
213
    $this->feed_nid = $feed_nid;
214
    $this->importer = feeds_importer($importer_id);
215
    parent::__construct($importer_id);
216
    $this->load();
217
  }
218

    
219
  /**
220
   * Returns the FeedsImporter object that this source is expected to be used with.
221
   */
222
  public function importer() {
223
    return $this->importer;
224
  }
225

    
226
  /**
227
   * Preview = fetch and parse a feed.
228
   *
229
   * @return
230
   *   FeedsParserResult object.
231
   *
232
   * @throws
233
   *   Throws Exception if an error occurs when fetching or parsing.
234
   */
235
  public function preview() {
236
    $result = $this->importer->fetcher->fetch($this);
237
    $result = $this->importer->parser->parse($this, $result);
238
    module_invoke_all('feeds_after_parse', $this, $result);
239
    return $result;
240
  }
241

    
242
  /**
243
   * Start importing a source.
244
   *
245
   * This method starts an import job. Depending on the configuration of the
246
   * importer of this source, a Batch API job or a background job with Job
247
   * Scheduler will be created.
248
   *
249
   * @throws Exception
250
   *   If processing in background is enabled, the first batch chunk of the
251
   *   import will be executed on the current page request. This means that this
252
   *   method may throw the same exceptions as FeedsSource::import().
253
   */
254
  public function startImport() {
255
    $config = $this->importer->getConfig();
256
    if ($config['process_in_background']) {
257
      $this->startBackgroundJob('import');
258
    }
259
    else {
260
      $this->startBatchAPIJob(t('Importing'), 'import');
261
    }
262
  }
263

    
264
  /**
265
   * Start deleting all imported items of a source.
266
   *
267
   * This method starts a clear job. Depending on the configuration of the
268
   * importer of this source, a Batch API job or a background job with Job
269
   * Scheduler will be created.
270
   *
271
   * @throws Exception
272
   *   If processing in background is enabled, the first batch chunk of the
273
   *   clear task will be executed on the current page request. This means that
274
   *   this method may throw the same exceptions as FeedsSource::clear().
275
   */
276
  public function startClear() {
277
    $config = $this->importer->getConfig();
278
    if ($config['process_in_background']) {
279
      $this->startBackgroundJob('clear');
280
    }
281
    else {
282
      $this->startBatchAPIJob(t('Deleting'), 'clear');
283
    }
284
  }
285

    
286
  /**
287
   * Schedule all periodic tasks for this source.
288
   */
289
  public function schedule() {
290
    $this->scheduleImport();
291
    $this->scheduleExpire();
292
  }
293

    
294
  /**
295
   * Schedule periodic or background import tasks.
296
   */
297
  public function scheduleImport() {
298
    // Check whether any fetcher is overriding the import period.
299
    $period = $this->importer->config['import_period'];
300
    $fetcher_period = $this->importer->fetcher->importPeriod($this);
301
    if (is_numeric($fetcher_period)) {
302
      $period = $fetcher_period;
303
    }
304
    $job = array(
305
      'type' => $this->id,
306
      'id' => $this->feed_nid,
307
      'period' => $period,
308
      'periodic' => TRUE,
309
    );
310
    if ($period == FEEDS_SCHEDULE_NEVER && $this->progressImporting() === FEEDS_BATCH_COMPLETE) {
311
      JobScheduler::get('feeds_source_import')->remove($job);
312
    }
313
    elseif ($this->progressImporting() === FEEDS_BATCH_COMPLETE) {
314
      JobScheduler::get('feeds_source_import')->set($job);
315
    }
316
    else {
317
      // Feed is not fully imported yet, so we put this job back in the queue
318
      // immediately for further processing.
319
      $queue = DrupalQueue::get('feeds_source_import');
320
      $queue->createItem($job);
321
    }
322
  }
323

    
324
  /**
325
   * Schedule background expire tasks.
326
   */
327
  public function scheduleExpire() {
328
    // Schedule as soon as possible if a batch is active.
329
    $period = $this->progressExpiring() === FEEDS_BATCH_COMPLETE ? 3600 : 0;
330

    
331
    $job = array(
332
      'type' => $this->id,
333
      'id' => $this->feed_nid,
334
      'period' => $period,
335
      'periodic' => TRUE,
336
    );
337
    if ($this->importer->processor->expiryTime() == FEEDS_EXPIRE_NEVER) {
338
      JobScheduler::get('feeds_source_expire')->remove($job);
339
    }
340
    else {
341
      JobScheduler::get('feeds_source_expire')->set($job);
342
    }
343
  }
344

    
345
  /**
346
   * Schedule background clearing tasks.
347
   */
348
  public function scheduleClear() {
349
    $job = array(
350
      'type' => $this->id,
351
      'id' => $this->feed_nid,
352
      'period' => 0,
353
      'periodic' => TRUE,
354
    );
355
    // Remove job if batch is complete.
356
    if ($this->progressClearing() === FEEDS_BATCH_COMPLETE) {
357
      JobScheduler::get('feeds_source_clear')->remove($job);
358
    }
359
    // Schedule as soon as possible if batch is not complete.
360
    else {
361
      JobScheduler::get('feeds_source_clear')->set($job);
362
    }
363
  }
364

    
365
  /**
366
   * Import a source: execute fetching, parsing and processing stage.
367
   *
368
   * This method only executes the current batch chunk, then returns. If you are
369
   * looking to import an entire source, use FeedsSource::startImport() instead.
370
   *
371
   * @return
372
   *   FEEDS_BATCH_COMPLETE if the import process finished. A decimal between
373
   *   0.0 and 0.9 periodic if import is still in progress.
374
   *
375
   * @throws
376
   *   Throws Exception if an error occurs when importing.
377
   */
378
  public function import() {
379
    $this->acquireLock();
380
    try {
381
      // If fetcher result is empty, we are starting a new import, log.
382
      if (empty($this->fetcher_result)) {
383
        module_invoke_all('feeds_before_import', $this);
384
        if (module_exists('rules')) {
385
          rules_invoke_event('feeds_before_import', $this);
386
        }
387
        $this->state[FEEDS_START] = time();
388
      }
389

    
390
      // Fetch.
391
      if (empty($this->fetcher_result) || FEEDS_BATCH_COMPLETE == $this->progressParsing()) {
392
        $this->fetcher_result = $this->importer->fetcher->fetch($this);
393
        // Clean the parser's state, we are parsing an entirely new file.
394
        unset($this->state[FEEDS_PARSE]);
395
      }
396

    
397
      // Parse.
398
      $parser_result = $this->importer->parser->parse($this, $this->fetcher_result);
399
      module_invoke_all('feeds_after_parse', $this, $parser_result);
400

    
401
      // Process.
402
      $this->importer->processor->process($this, $parser_result);
403

    
404
      // Import finished without exceptions, so unset any potentially previously
405
      // recorded exceptions.
406
      unset($this->exception);
407
    }
408
    catch (Exception $e) {
409
      // $e is stored and re-thrown once we've had a chance to log our progress.
410
      // Set the exception so that other modules can check if an exception
411
      // occurred in hook_feeds_after_import().
412
      $this->exception = $e;
413
    }
414

    
415
    // Clean up.
416
    $result = $this->progressImporting();
417
    if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
418
      $this->imported = time();
419
      $this->log('import', 'Imported in @s seconds.', array('@s' => $this->imported - $this->state[FEEDS_START]), WATCHDOG_INFO);
420
      $this->importer->fetcher->afterImport($this);
421
      module_invoke_all('feeds_after_import', $this);
422
      if (module_exists('rules')) {
423
        rules_invoke_event('feeds_after_import', $this);
424
      }
425
      unset($this->fetcher_result, $this->state);
426
    }
427
    $this->save();
428

    
429
    $this->releaseLock();
430

    
431
    if (isset($e)) {
432
      throw $e;
433
    }
434

    
435
    return $result;
436
  }
437

    
438
  /**
439
   * Imports a fetcher result all at once in memory.
440
   *
441
   * @param FeedsFetcherResult $fetcher_result
442
   *   The fetcher result to process.
443
   *
444
   * @throws Exception
445
   *   Thrown if an error occurs when importing.
446
   */
447
  public function pushImport(FeedsFetcherResult $fetcher_result) {
448
    // Since locks only work during a request, check if an import is active.
449
    if (!empty($this->fetcher_result) || !empty($this->state)) {
450
      throw new RuntimeException('The feed is currently importing.');
451
    }
452

    
453
    $this->acquireLock();
454
    $start = time();
455

    
456
    try {
457
      module_invoke_all('feeds_before_import', $this);
458

    
459
      // Parse.
460
      do {
461
        $parser_result = $this->importer->parser->parse($this, $fetcher_result);
462
        module_invoke_all('feeds_after_parse', $this, $parser_result);
463

    
464
        // Process.
465
        $this->importer->processor->process($this, $parser_result);
466

    
467
      } while ($this->progressParsing() !== FEEDS_BATCH_COMPLETE);
468
    }
469
    catch (Exception $e) {
470
      // $e is stored and re-thrown once we've had a chance to log our progress.
471
      // Set the exception so that other modules can check if an exception
472
      // occurred in hook_feeds_after_import().
473
      $this->exception = $e;
474
    }
475

    
476
    module_invoke_all('feeds_after_import', $this);
477

    
478
    $this->imported = time();
479
    $this->log('import', 'Imported in @s seconds.', array('@s' => $this->imported - $start), WATCHDOG_INFO);
480

    
481
    unset($this->fetcher_result, $this->state);
482

    
483
    $this->save();
484

    
485
    $this->releaseLock();
486

    
487
    if (isset($e)) {
488
      throw $e;
489
    }
490
  }
491

    
492
  /**
493
   * Remove all items from a feed.
494
   *
495
   * This method only executes the current batch chunk, then returns. If you are
496
   * looking to delete all items of a source, use FeedsSource::startClear()
497
   * instead.
498
   *
499
   * @return
500
   *   FEEDS_BATCH_COMPLETE if the clearing process finished. A decimal between
501
   *   0.0 and 0.9 periodic if clearing is still in progress.
502
   *
503
   * @throws
504
   *   Throws Exception if an error occurs when clearing.
505
   */
506
  public function clear() {
507
    $this->acquireLock();
508
    try {
509
      $this->importer->fetcher->clear($this);
510
      $this->importer->parser->clear($this);
511
      $this->importer->processor->clear($this);
512
    }
513
    catch (Exception $e) {
514
      // $e is stored and re-thrown once we've had a chance to log our progress.
515
    }
516
    $this->releaseLock();
517

    
518
    // Clean up.
519
    $result = $this->progressClearing();
520
    if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
521
      module_invoke_all('feeds_after_clear', $this);
522
      unset($this->state);
523
    }
524
    $this->save();
525
    if (isset($e)) {
526
      throw $e;
527
    }
528
    return $result;
529
  }
530

    
531
  /**
532
   * Removes all expired items from a feed.
533
   */
534
  public function expire() {
535
    $this->acquireLock();
536
    try {
537
      $result = $this->importer->processor->expire($this);
538
    }
539
    catch (Exception $e) {
540
      // Will throw after the lock is released.
541
    }
542
    $this->releaseLock();
543

    
544
    if (isset($e)) {
545
      throw $e;
546
    }
547

    
548
    return $result;
549
  }
550

    
551
  /**
552
   * Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
553
   */
554
  public function progressParsing() {
555
    return $this->state(FEEDS_PARSE)->progress;
556
  }
557

    
558
  /**
559
   * Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
560
   */
561
  public function progressImporting() {
562
    $fetcher = $this->state(FEEDS_FETCH);
563
    $parser = $this->state(FEEDS_PARSE);
564
    if ($fetcher->progress == FEEDS_BATCH_COMPLETE && $parser->progress == FEEDS_BATCH_COMPLETE) {
565
      return FEEDS_BATCH_COMPLETE;
566
    }
567
    // Fetching envelops parsing.
568
    // @todo: this assumes all fetchers neatly use total. May not be the case.
569
    $fetcher_fraction = $fetcher->total ? 1.0 / $fetcher->total : 1.0;
570
    $parser_progress = $parser->progress * $fetcher_fraction;
571
    $result = $fetcher->progress - $fetcher_fraction + $parser_progress;
572
    if ($result == FEEDS_BATCH_COMPLETE) {
573
      return 0.99;
574
    }
575
    return $result;
576
  }
577

    
578
  /**
579
   * Report progress on clearing.
580
   */
581
  public function progressClearing() {
582
    return $this->state(FEEDS_PROCESS_CLEAR)->progress;
583
  }
584

    
585
  /**
586
   * Report progress on expiry.
587
   */
588
  public function progressExpiring() {
589
    return $this->state(FEEDS_PROCESS_EXPIRE)->progress;
590
  }
591

    
592
  /**
593
   * Return a state object for a given stage. Lazy instantiates new states.
594
   *
595
   * @todo Rename getConfigFor() accordingly to config().
596
   *
597
   * @param $stage
598
   *   One of FEEDS_FETCH, FEEDS_PARSE, FEEDS_PROCESS or FEEDS_PROCESS_CLEAR.
599
   *
600
   * @return
601
   *   The FeedsState object for the given stage.
602
   */
603
  public function state($stage) {
604
    if (!is_array($this->state)) {
605
      $this->state = array();
606
    }
607
    if (!isset($this->state[$stage])) {
608
      $this->state[$stage] = new FeedsState();
609
    }
610
    return $this->state[$stage];
611
  }
612

    
613
  /**
614
   * Count items imported by this source.
615
   */
616
  public function itemCount() {
617
    return $this->importer->processor->itemCount($this);
618
  }
619

    
620
  /**
621
   * Save configuration.
622
   */
623
  public function save() {
624
    // Alert implementers of FeedsSourceInterface to the fact that we're saving.
625
    foreach ($this->importer->plugin_types as $type) {
626
      $this->importer->$type->sourceSave($this);
627
    }
628
    $config = $this->getConfig();
629

    
630
    // Store the source property of the fetcher in a separate column so that we
631
    // can do fast lookups on it.
632
    $source = '';
633
    if (isset($config[get_class($this->importer->fetcher)]['source'])) {
634
      $source = $config[get_class($this->importer->fetcher)]['source'];
635
    }
636
    $object = array(
637
      'id' => $this->id,
638
      'feed_nid' => $this->feed_nid,
639
      'imported' => $this->imported,
640
      'config' => $config,
641
      'source' => $source,
642
      'state' => isset($this->state) ? $this->state : FALSE,
643
      'fetcher_result' => isset($this->fetcher_result) ? $this->fetcher_result : FALSE,
644
    );
645
    if (db_query_range("SELECT 1 FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", 0, 1, array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchField()) {
646
      drupal_write_record('feeds_source', $object, array('id', 'feed_nid'));
647
    }
648
    else {
649
      drupal_write_record('feeds_source', $object);
650
    }
651
  }
652

    
653
  /**
654
   * Load configuration and unpack.
655
   *
656
   * @todo Patch CTools to move constants from export.inc to ctools.module.
657
   */
658
  public function load() {
659
    if ($record = db_query("SELECT imported, config, state, fetcher_result FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchObject()) {
660
      // While FeedsSource cannot be exported, we still use CTool's export.inc
661
      // export definitions.
662
      ctools_include('export');
663
      $this->export_type = EXPORT_IN_DATABASE;
664
      $this->imported = $record->imported;
665
      $this->config = unserialize($record->config);
666
      if (!empty($record->state)) {
667
        $this->state = unserialize($record->state);
668
      }
669
      if (!is_array($this->state)) {
670
        $this->state = array();
671
      }
672
      if (!empty($record->fetcher_result)) {
673
        $this->fetcher_result = unserialize($record->fetcher_result);
674
      }
675
    }
676
  }
677

    
678
  /**
679
   * Delete configuration. Removes configuration information
680
   * from database, does not delete configuration itself.
681
   */
682
  public function delete() {
683
    // Alert implementers of FeedsSourceInterface to the fact that we're
684
    // deleting.
685
    foreach ($this->importer->plugin_types as $type) {
686
      $this->importer->$type->sourceDelete($this);
687
    }
688
    db_delete('feeds_source')
689
      ->condition('id', $this->id)
690
      ->condition('feed_nid', $this->feed_nid)
691
      ->execute();
692
    // Remove from schedule.
693
    $job = array(
694
      'type' => $this->id,
695
      'id' => $this->feed_nid,
696
    );
697
    JobScheduler::get('feeds_source_import')->remove($job);
698
    JobScheduler::get('feeds_source_expire')->remove($job);
699
  }
700

    
701
  /**
702
   * Only return source if configuration is persistent and valid.
703
   *
704
   * @see FeedsConfigurable::existing().
705
   */
706
  public function existing() {
707
    // If there is no feed nid given, there must be no content type specified.
708
    // If there is a feed nid given, there must be a content type specified.
709
    // Ensure that importer is persistent (= defined in code or DB).
710
    // Ensure that source is persistent (= defined in DB).
711
    if ((empty($this->feed_nid) && empty($this->importer->config['content_type'])) ||
712
        (!empty($this->feed_nid) && !empty($this->importer->config['content_type']))) {
713
      $this->importer->existing();
714
      return parent::existing();
715
    }
716
    throw new FeedsNotExistingException(t('Source configuration not valid.'));
717
  }
718

    
719
  /**
720
   * Returns the configuration for a specific client class.
721
   *
722
   * @param FeedsSourceInterface $client
723
   *   An object that is an implementer of FeedsSourceInterface.
724
   *
725
   * @return
726
   *   An array stored for $client.
727
   */
728
  public function getConfigFor(FeedsSourceInterface $client) {
729
    $class = get_class($client);
730
    return isset($this->config[$class]) ? $this->config[$class] : $client->sourceDefaults();
731
  }
732

    
733
  /**
734
   * Sets the configuration for a specific client class.
735
   *
736
   * @param FeedsSourceInterface $client
737
   *   An object that is an implementer of FeedsSourceInterface.
738
   * @param $config
739
   *   The configuration for $client.
740
   *
741
   * @return
742
   *   An array stored for $client.
743
   */
744
  public function setConfigFor(FeedsSourceInterface $client, $config) {
745
    $this->config[get_class($client)] = $config;
746
  }
747

    
748
  /**
749
   * Return defaults for feed configuration.
750
   */
751
  public function configDefaults() {
752
    // Collect information from plugins.
753
    $defaults = array();
754
    foreach ($this->importer->plugin_types as $type) {
755
      if ($this->importer->$type->hasSourceConfig()) {
756
        $defaults[get_class($this->importer->$type)] = $this->importer->$type->sourceDefaults();
757
      }
758
    }
759
    return $defaults;
760
  }
761

    
762
  /**
763
   * Override parent::configForm().
764
   */
765
  public function configForm(&$form_state) {
766
    // Collect information from plugins.
767
    $form = array();
768
    foreach ($this->importer->plugin_types as $type) {
769
      if ($this->importer->$type->hasSourceConfig()) {
770
        $class = get_class($this->importer->$type);
771
        $config = isset($this->config[$class]) ? $this->config[$class] : array();
772
        $form[$class] = $this->importer->$type->sourceForm($config);
773
        $form[$class]['#tree'] = TRUE;
774
      }
775
    }
776
    return $form;
777
  }
778

    
779
  /**
780
   * Override parent::configFormValidate().
781
   */
782
  public function configFormValidate(&$values) {
783
    foreach ($this->importer->plugin_types as $type) {
784
      $class = get_class($this->importer->$type);
785
      if (isset($values[$class]) && $this->importer->$type->hasSourceConfig()) {
786
        $this->importer->$type->sourceFormValidate($values[$class]);
787
      }
788
    }
789
  }
790

    
791
  /**
792
   * Writes to feeds log.
793
   */
794
  public function log($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE) {
795
    feeds_log($this->id, $this->feed_nid, $type, $message, $variables, $severity);
796
  }
797

    
798
  /**
799
   * Background job helper. Starts a background job using Job Scheduler.
800
   *
801
   * Execute the first batch chunk of a background job on the current page load,
802
   * moves the rest of the job processing to a cron powered background job.
803
   *
804
   * Executing the first batch chunk is important, otherwise, when a user
805
   * submits a source for import or clearing, we will leave her without any
806
   * visual indicators of an ongoing job.
807
   *
808
   * @see FeedsSource::startImport().
809
   * @see FeedsSource::startClear().
810
   *
811
   * @param $method
812
   *   Method to execute on importer; one of 'import' or 'clear'.
813
   *
814
   * @throws Exception $e
815
   */
816
  protected function startBackgroundJob($method) {
817
    if (FEEDS_BATCH_COMPLETE != $this->$method()) {
818
      $job = array(
819
        'type' => $this->id,
820
        'id' => $this->feed_nid,
821
        'period' => 0,
822
        'periodic' => FALSE,
823
      );
824
      JobScheduler::get("feeds_source_{$method}")->set($job);
825
    }
826
  }
827

    
828
  /**
829
   * Batch API helper. Starts a Batch API job.
830
   *
831
   * @see FeedsSource::startImport().
832
   * @see FeedsSource::startClear().
833
   * @see feeds_batch()
834
   *
835
   * @param $title
836
   *   Title to show to user when executing batch.
837
   * @param $method
838
   *   Method to execute on importer; one of 'import' or 'clear'.
839
   */
840
  protected function startBatchAPIJob($title, $method) {
841
    $batch = array(
842
      'title' => $title,
843
      'operations' => array(
844
        array('feeds_batch', array($method, $this->id, $this->feed_nid)),
845
      ),
846
      'progress_message' => '',
847
    );
848
    batch_set($batch);
849
  }
850

    
851
  /**
852
   * Acquires a lock for this source.
853
   *
854
   * @throws FeedsLockException
855
   *   If a lock for the requested job could not be acquired.
856
   */
857
  protected function acquireLock() {
858
    if (!lock_acquire("feeds_source_{$this->id}_{$this->feed_nid}", 60.0)) {
859
      throw new FeedsLockException(t('Cannot acquire lock for source @id / @feed_nid.', array('@id' => $this->id, '@feed_nid' => $this->feed_nid)));
860
    }
861
  }
862

    
863
  /**
864
   * Releases a lock for this source.
865
   */
866
  protected function releaseLock() {
867
    lock_release("feeds_source_{$this->id}_{$this->feed_nid}");
868
  }
869

    
870
  /**
871
   * Implements FeedsConfigurable::dependencies().
872
   */
873
  public function dependencies() {
874
    $dependencies = parent::dependencies();
875
    return array_merge($dependencies, $this->importer()->dependencies());
876
  }
877

    
878
}