Projet

Général

Profil

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

root / drupal7 / sites / all / modules / feeds / plugins / FeedsParser.inc @ b5aa1857

1
<?php
2

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

    
8
/**
9
 * A result of a parsing stage.
10
 */
11
class FeedsParserResult extends FeedsResult {
12
  public $title;
13
  public $description;
14
  public $link;
15
  public $items;
16
  public $current_item;
17

    
18
  /**
19
   * Constructor.
20
   */
21
  public function __construct($items = array()) {
22
    $this->title = '';
23
    $this->description = '';
24
    $this->link = '';
25
    $this->items = $items;
26
  }
27

    
28
  /**
29
   * @todo Move to a nextItem() based approach, not consuming the item array.
30
   *   Can only be done once we don't cache the entire batch object between page
31
   *   loads for batching anymore.
32
   *
33
   * @return
34
   *   Next available item or NULL if there is none. Every returned item is
35
   *   removed from the internal array.
36
   */
37
  public function shiftItem() {
38
    $this->current_item = array_shift($this->items);
39
    return $this->current_item;
40
  }
41

    
42
  /**
43
   * @return
44
   *   Current result item.
45
   */
46
  public function currentItem() {
47
    return empty($this->current_item) ? NULL : $this->current_item;
48
  }
49
}
50

    
51
/**
52
 * Abstract class, defines interface for parsers.
53
 */
54
abstract class FeedsParser extends FeedsPlugin {
55

    
56
  /**
57
   * Implements FeedsPlugin::pluginType().
58
   */
59
  public function pluginType() {
60
    return 'parser';
61
  }
62

    
63
  /**
64
   * Parse content fetched by fetcher.
65
   *
66
   * Extending classes must implement this method.
67
   *
68
   * @param FeedsSource $source
69
   *   Source information.
70
   * @param $fetcher_result
71
   *   FeedsFetcherResult returned by fetcher.
72
   */
73
  public abstract function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result);
74

    
75
  /**
76
   * Clear all caches for results for given source.
77
   *
78
   * @param FeedsSource $source
79
   *   Source information for this expiry. Implementers can choose to only clear
80
   *   caches pertaining to this source.
81
   */
82
  public function clear(FeedsSource $source) {}
83

    
84
  /**
85
   * Declare the possible mapping sources that this parser produces.
86
   *
87
   * @ingroup mappingapi
88
   *
89
   * @return
90
   *   An array of mapping sources, or FALSE if the sources can be defined by
91
   *   typing a value in a text field.
92
   *
93
   *   Example:
94
   *   @code
95
   *   array(
96
   *     'title' => t('Title'),
97
   *     'created' => t('Published date'),
98
   *     'url' => t('Feed item URL'),
99
   *     'guid' => t('Feed item GUID'),
100
   *   )
101
   *   @endcode
102
   */
103
  public function getMappingSources() {
104
    self::loadMappers();
105
    $sources = array();
106
    $content_type = feeds_importer($this->id)->config['content_type'];
107
    drupal_alter('feeds_parser_sources', $sources, $content_type);
108
    if (!feeds_importer($this->id)->config['content_type']) {
109
      return $sources;
110
    }
111
    $sources['parent:uid'] = array(
112
      'name' => t('Feed node: User ID'),
113
      'description' => t('The feed node author uid.'),
114
    );
115
    $sources['parent:nid'] = array(
116
      'name' => t('Feed node: Node ID'),
117
      'description' => t('The feed node nid.'),
118
    );
119
    return $sources;
120
  }
121

    
122
  /**
123
   * Get list of mapped sources.
124
   *
125
   * @return array
126
   *   List of mapped source names in an array.
127
   */
128
  public function getMappingSourceList() {
129
    $mappings = feeds_importer($this->id)->processor->config['mappings'];
130
    $sources = array();
131
    foreach ($mappings as $mapping) {
132
      $sources[] = $mapping['source'];
133
    }
134
    return $sources;
135
  }
136

    
137
  /**
138
   * Get an element identified by $element_key of the given item.
139
   * The element key corresponds to the values in the array returned by
140
   * FeedsParser::getMappingSources().
141
   *
142
   * This method is invoked from FeedsProcessor::map() when a concrete item is
143
   * processed.
144
   *
145
   * @ingroup mappingapi
146
   *
147
   * @param $batch
148
   *   FeedsImportBatch object containing the sources to be mapped from.
149
   * @param $element_key
150
   *   The key identifying the element that should be retrieved from $source
151
   *
152
   * @return
153
   *   The source element from $item identified by $element_key.
154
   *
155
   * @see FeedsProcessor::map()
156
   * @see FeedsCSVParser::getSourceElement()
157
   */
158
  public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
159

    
160
    switch ($element_key) {
161

    
162
      case 'parent:uid':
163
        if ($source->feed_nid && $node = node_load($source->feed_nid)) {
164
          return $node->uid;
165
        }
166
        break;
167
      case 'parent:nid':
168
        return $source->feed_nid;
169
    }
170

    
171
    $item = $result->currentItem();
172
    return isset($item[$element_key]) ? $item[$element_key] : '';
173
  }
174

    
175
  /**
176
   * Returns if the parsed result can have a title.
177
   *
178
   * Parser classes should override this method in case they support a source
179
   * title.
180
   *
181
   * @return bool
182
   *   TRUE if the parsed result can have a title.
183
   *   FALSE otherwise.
184
   */
185
  public function providesSourceTitle() {
186
    return FALSE;
187
  }
188
}
189

    
190
/**
191
 * Defines an element of a parsed result. Such an element can be a simple type,
192
 * a complex type (derived from FeedsElement) or an array of either.
193
 *
194
 * @see FeedsEnclosure
195
 */
196
class FeedsElement {
197
  // The standard value of this element. This value can contain be a simple type,
198
  // a FeedsElement or an array of either.
199
  protected $value;
200

    
201
  /**
202
   * Constructor.
203
   */
204
  public function __construct($value) {
205
    $this->value = $value;
206
  }
207

    
208
  /**
209
   * @todo Make value public and deprecate use of getValue().
210
   *
211
   * @return
212
   *   Value of this FeedsElement represented as a scalar.
213
   */
214
  public function getValue() {
215
    return $this->value;
216
  }
217

    
218
  /**
219
   * Magic method __toString() for printing and string conversion of this
220
   * object.
221
   *
222
   * @return
223
   *   A string representation of this element.
224
   */
225
  public function __toString() {
226
    if (is_array($this->value)) {
227
      return 'Array';
228
    }
229
    if (is_object($this->value)) {
230
      return 'Object';
231
    }
232
    return (string) $this->getValue();
233
  }
234
}
235

    
236
/**
237
 * Encapsulates a taxonomy style term object.
238
 *
239
 * Objects of this class can be turned into a taxonomy term style arrays by
240
 * casting them.
241
 *
242
 * @code
243
 *   $term_object = new FeedsTermElement($term_array);
244
 *   $term_array = (array)$term_object;
245
 * @endcode
246
 */
247
class FeedsTermElement extends FeedsElement {
248
  public $tid, $vid, $name;
249

    
250
  /**
251
   * @param $term
252
   *   An array or a stdClass object that is a Drupal taxonomy term.
253
   */
254
  public function __construct($term) {
255
    if (is_array($term)) {
256
      parent::__construct($term['name']);
257
      foreach ($this as $key => $value) {
258
        $this->$key = isset($term[$key]) ? $term[$key] : NULL;
259
      }
260
    }
261
    elseif (is_object($term)) {
262
      parent::__construct($term->name);
263
      foreach ($this as $key => $value) {
264
        $this->$key = isset($term->$key) ? $term->$key : NULL;
265
      }
266
    }
267
  }
268

    
269
  /**
270
   * Use $name as $value.
271
   */
272
  public function getValue() {
273
    return $this->name;
274
  }
275
}
276

    
277
/**
278
 * A geo term element.
279
 */
280
class FeedsGeoTermElement extends FeedsTermElement {
281
  public $lat, $lon, $bound_top, $bound_right, $bound_bottom, $bound_left, $geometry;
282
  /**
283
   * @param $term
284
   *   An array or a stdClass object that is a Drupal taxonomy term. Can include
285
   *   geo extensions.
286
   */
287
  public function __construct($term) {
288
    parent::__construct($term);
289
  }
290
}
291

    
292
/**
293
 * Enclosure element, can be part of the result array.
294
 */
295
class FeedsEnclosure extends FeedsElement {
296

    
297
  /**
298
   * The mime type of the enclosure.
299
   *
300
   * @param string
301
   */
302
  protected $mime_type;
303

    
304
  /**
305
   * The default list of allowed extensions.
306
   *
307
   * @param string
308
   */
309
  protected $allowedExtensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
310

    
311
  /**
312
   * The sanitized local file name.
313
   *
314
   * @var string
315
   */
316
  protected $safeFilename;
317

    
318
  /**
319
   * Constructor, requires MIME type.
320
   *
321
   * @param $value
322
   *   A path to a local file or a URL to a remote document.
323
   * @param $mimetype
324
   *   The mime type of the resource.
325
   */
326
  public function __construct($value, $mime_type) {
327
    parent::__construct($value);
328
    $this->mime_type = $mime_type;
329
  }
330

    
331
  /**
332
   * @return
333
   *   MIME type of return value of getValue().
334
   */
335
  public function getMIMEType() {
336
    return $this->mime_type;
337
  }
338

    
339
  /**
340
   * Sets the list of allowed extensions.
341
   *
342
   * @param string $extensions
343
   *   The list of allowed extensions separated by a space.
344
   */
345
  public function setAllowedExtensions($extensions) {
346
    // Normalize whitespace so that empty extensions are not allowed.
347
    $this->allowedExtensions = drupal_strtolower(trim(preg_replace('/\s+/', ' ', $extensions)));
348
  }
349

    
350
  /**
351
   * Use this method instead of FeedsElement::getValue() when fetching the file
352
   * from the URL.
353
   *
354
   * @return
355
   *   Value with encoded space characters to safely fetch the file from the URL.
356
   *
357
   * @see FeedsElement::getValue()
358
   */
359
  public function getUrlEncodedValue() {
360
    return str_replace(' ', '%20', $this->getValue());
361
  }
362

    
363
  /**
364
   * Returns the full path to the file URI with a safe file name.
365
   *
366
   * @return string
367
   *   The safe file URI.
368
   *
369
   * @throws RuntimeException
370
   *   Thrown if the file extension is invalid.
371
   */
372
  public function getSanitizedUri() {
373
    return drupal_dirname($this->getValue()) . '/' . $this->getSafeFilename();
374
  }
375

    
376
  /**
377
   * Returns the file name transformed for better local saving.
378
   *
379
   * @return string
380
   *   Value with space characters changed to underscores.
381
   *
382
   * @throws RuntimeException
383
   *   Thrown if the file extension is invalid.
384
   */
385
  public function getLocalValue() {
386
    return str_replace(' ', '_', $this->getSafeFilename());
387
  }
388

    
389
  /**
390
   * Returns the safe file name.
391
   *
392
   * @return string
393
   *   A filename that is safe to save to the filesystem.
394
   *
395
   * @throws RuntimeException
396
   *   Thrown if the file extension is invalid.
397
   */
398
  protected function getSafeFilename() {
399
    if (isset($this->safeFilename)) {
400
      return $this->safeFilename;
401
    }
402

    
403
    // Strip any query string or fragment from file name.
404
    list($filename) = explode('?', $this->getValue());
405
    list($filename) = explode('#', $filename);
406

    
407
    $filename = rawurldecode(drupal_basename($filename));
408

    
409
    // Remove leading and trailing whitespace and periods.
410
    $filename = trim($filename, " \t\n\r\0\x0B.");
411

    
412
    if (strpos($filename, '.') === FALSE) {
413
      $extension = FALSE;
414
    }
415
    else {
416
      $extension = drupal_strtolower(substr($filename, strrpos($filename, '.') + 1));
417
    }
418

    
419
    if (!$extension || !in_array($extension, explode(' ', $this->allowedExtensions), TRUE)) {
420
      throw new RuntimeException(t('The file @file has an invalid extension.', array('@file' => $filename)));
421
    }
422

    
423
    $this->safeFilename = file_munge_filename($filename, $this->allowedExtensions, FALSE);
424

    
425
    return $this->safeFilename;
426
  }
427

    
428
  /**
429
   * Downloads the content from the file URL.
430
   *
431
   * @return string
432
   *   The content of the referenced resource.
433
   */
434
  public function getContent() {
435
    feeds_include_library('http_request.inc', 'http_request');
436
    $result = http_request_get($this->getUrlEncodedValue());
437
    if ($result->code != 200) {
438
      throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->getUrlEncodedValue(), '!code' => $result->code)));
439
    }
440
    return $result->data;
441
  }
442

    
443
  /**
444
   * Get a Drupal file object of the enclosed resource, download if necessary.
445
   *
446
   * @param string $destination
447
   *   The path or uri specifying the target directory in which the file is
448
   *   expected. Don't use trailing slashes unless it's a streamwrapper scheme.
449
   * @param int $replace
450
   *   Replace behavior when the destination file already exists.
451
   *   @see file_save_data()
452
   *
453
   * @return stdClass|FALSE
454
   *   A Drupal temporary file object of the enclosed resource or FALSE if the
455
   *   value is empty.
456
   *
457
   * @throws Exception
458
   *   If file object could not be created.
459
   */
460
  public function getFile($destination, $replace = FILE_EXISTS_RENAME) {
461
    $file = FALSE;
462
    if ($this->getValue()) {
463
      // Prepare destination directory.
464
      file_prepare_directory($destination, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
465
      // Copy or save file depending on whether it is remote or local.
466
      if (drupal_realpath($this->getSanitizedUri())) {
467
        $file = new stdClass();
468
        $file->uid = 0;
469
        $file->uri = $this->getSanitizedUri();
470
        $file->filemime = $this->getMIMEType();
471
        $file->filename = $this->getSafeFilename();
472

    
473
        if (drupal_dirname($file->uri) !== $destination) {
474
          $file = file_copy($file, $destination, $replace);
475
        }
476
        else {
477
          // If file is not to be copied, check whether file already exists,
478
          // as file_save() won't do that for us (compare file_copy() and
479
          // file_save())
480
          $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
481
          if (count($existing_files)) {
482
            $existing = reset($existing_files);
483
            if ($replace == FEEDS_FILE_EXISTS_SKIP) {
484
              return $existing;
485
            }
486
            $file->fid = $existing->fid;
487
            $file->filename = $existing->filename;
488
          }
489
          file_save($file);
490
        }
491
      }
492
      else {
493
        if (file_uri_target($destination)) {
494
          $destination = trim($destination, '/') . '/';
495
        }
496
        try {
497
          $filename = $this->getLocalValue();
498

    
499
          if (module_exists('transliteration')) {
500
            require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
501
            $filename = transliteration_clean_filename($filename);
502
          }
503

    
504
          $file = file_save_data($this->getContent(), $destination . $filename, $replace);
505
        }
506
        catch (Exception $e) {
507
          watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
508
        }
509
      }
510

    
511
      // We couldn't make sense of this enclosure, throw an exception.
512
      if (!$file) {
513
        throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
514
      }
515

    
516
      return $file;
517
    }
518
  }
519
}
520

    
521
/**
522
 * Defines a date element of a parsed result (including ranges, repeat).
523
 *
524
 * @deprecated This is no longer in use and will not be maintained.
525
 */
526
class FeedsDateTimeElement extends FeedsElement {
527

    
528
  // Start date and end date.
529
  public $start;
530
  public $end;
531

    
532
  /**
533
   * Constructor.
534
   *
535
   * @param $start
536
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
537
   * @param $end
538
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
539
   * @param $tz
540
   *   A PHP DateTimeZone object.
541
   */
542
  public function __construct($start = NULL, $end = NULL, $tz = NULL) {
543
    $this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
544
    $this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
545
  }
546

    
547
  /**
548
   * Override FeedsElement::getValue().
549
   *
550
   * @return
551
   *   The UNIX timestamp of this object's start date. Return value is
552
   *   technically a string but will only contain numeric values.
553
   */
554
  public function getValue() {
555
    if ($this->start) {
556
      return $this->start->format('U');
557
    }
558
    return '0';
559
  }
560

    
561
  /**
562
   * Merge this field with another. Most stuff goes down when merging the two
563
   * sub-dates.
564
   *
565
   * @see FeedsDateTime
566
   */
567
  public function merge(FeedsDateTimeElement $other) {
568
    $this2 = clone $this;
569
    if ($this->start && $other->start) {
570
      $this2->start = $this->start->merge($other->start);
571
    }
572
    elseif ($other->start) {
573
      $this2->start = clone $other->start;
574
    }
575
    elseif ($this->start) {
576
      $this2->start = clone $this->start;
577
    }
578

    
579
    if ($this->end && $other->end) {
580
      $this2->end = $this->end->merge($other->end);
581
    }
582
    elseif ($other->end) {
583
      $this2->end = clone $other->end;
584
    }
585
    elseif ($this->end) {
586
      $this2->end = clone $this->end;
587
    }
588
    return $this2;
589
  }
590

    
591
  /**
592
   * Helper method for buildDateField(). Build a FeedsDateTimeElement object
593
   * from a standard formatted node.
594
   */
595
  protected static function readDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
596
    $ret = new FeedsDateTimeElement();
597
    if (isset($entity->{$field_name}[$language][$delta]['date']) && $entity->{$field_name}[$language][$delta]['date'] instanceof FeedsDateTime) {
598
      $ret->start = $entity->{$field_name}[$language][$delta]['date'];
599
    }
600
    if (isset($entity->{$field_name}[$language][$delta]['date2']) && $entity->{$field_name}[$language][$delta]['date2'] instanceof FeedsDateTime) {
601
      $ret->end = $entity->{$field_name}[$language][$delta]['date2'];
602
    }
603
    return $ret;
604
  }
605

    
606
  /**
607
   * Build a entity's date field from our object.
608
   *
609
   * @param object $entity
610
   *   The entity to build the date field on.
611
   * @param str $field_name
612
   *   The name of the field to build.
613
   * @param int $delta
614
   *   The delta in the field.
615
   */
616
  public function buildDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
617
    $info = field_info_field($field_name);
618

    
619
    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta, $language);
620
    // Merge with any preexisting objects on the field; we take precedence.
621
    $oldfield = $this->merge($oldfield);
622
    $use_start = $oldfield->start;
623
    $use_end = $oldfield->end;
624

    
625
    // Set timezone if not already in the FeedsDateTime object
626
    $to_tz = date_get_timezone($info['settings']['tz_handling'], date_default_timezone());
627
    $temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
628

    
629
    $db_tz = '';
630
    if ($use_start) {
631
      $use_start = $use_start->merge($temp);
632
      if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
633
        $use_start->setTimezone(new DateTimeZone("UTC"));
634
      }
635
      $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_start->getTimezone()->getName());
636
    }
637
    if ($use_end) {
638
      $use_end = $use_end->merge($temp);
639
      if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
640
        $use_end->setTimezone(new DateTimeZone("UTC"));
641
      }
642
      if (!$db_tz) {
643
        $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_end->getTimezone()->getName());
644
      }
645
    }
646
    if (!$db_tz) {
647
      return;
648
    }
649

    
650
    $db_tz = new DateTimeZone($db_tz);
651
    if (!isset($entity->{$field_name})) {
652
      $entity->{$field_name} = array($language => array());
653
    }
654
    if ($use_start) {
655
      $entity->{$field_name}[$language][$delta]['timezone'] = $use_start->getTimezone()->getName();
656
      $entity->{$field_name}[$language][$delta]['offset'] = $use_start->getOffset();
657
      $use_start->setTimezone($db_tz);
658
      $entity->{$field_name}[$language][$delta]['date'] = $use_start;
659
      /**
660
       * @todo the date_type_format line could be simplified based upon a patch
661
       *   DO issue #259308 could affect this, follow up on at some point.
662
       *   Without this, all granularity info is lost.
663
       *   $use_start->format(date_type_format($field['type'], $use_start->granularity));
664
       */
665
      $entity->{$field_name}[$language][$delta]['value'] = $use_start->format(date_type_format($info['type']));
666
    }
667
    if ($use_end) {
668
      // Don't ever use end to set timezone (for now)
669
      $entity->{$field_name}[$language][$delta]['offset2'] = $use_end->getOffset();
670
      $use_end->setTimezone($db_tz);
671
      $entity->{$field_name}[$language][$delta]['date2'] = $use_end;
672
      $entity->{$field_name}[$language][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
673
    }
674
  }
675
}
676

    
677
/**
678
 * Extend PHP DateTime class with granularity handling, merge functionality and
679
 * slightly more flexible initialization parameters.
680
 *
681
 * This class is a Drupal independent extension of the >= PHP 5.2 DateTime
682
 * class.
683
 *
684
 * @see FeedsDateTimeElement
685
 *
686
 * @deprecated Use DateObject instead.
687
 */
688
class FeedsDateTime extends DateTime {
689
  public $granularity = array();
690
  protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
691
  private $_serialized_time;
692
  private $_serialized_timezone;
693

    
694
  /**
695
   * The original time value passed into the constructor.
696
   *
697
   * @var mixed
698
   */
699
  protected $originalValue;
700

    
701
  /**
702
   * Overridden constructor.
703
   *
704
   * @param $time
705
   *   time string, flexible format including timestamp. Invalid formats will
706
   *   fall back to 'now'.
707
   * @param $tz
708
   *   PHP DateTimeZone object, NULL allowed
709
   */
710
  public function __construct($time = '', $tz = NULL) {
711
    $this->originalValue = $time;
712

    
713
    if (is_numeric($time)) {
714
      // Assume UNIX timestamp if it doesn't look like a simple year.
715
      if (strlen($time) > 4) {
716
        $time = "@" . $time;
717
      }
718
      // If it's a year, add a default month too, because PHP's date functions
719
      // won't parse standalone years after 2000 correctly (see explanation at
720
      // http://aaronsaray.com/blog/2007/07/11/helpful-strtotime-reminders/#comment-47).
721
      else {
722
        $time = 'January ' . $time;
723
      }
724
    }
725

    
726
    // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
727
    $time = str_replace("GMT-", "-", $time);
728
    $time = str_replace("GMT+", "+", $time);
729

    
730
    // Some PHP 5.2 version's DateTime class chokes on invalid dates.
731
    if (!date_create($time)) {
732
      $time = 'now';
733
    }
734

    
735
    // Create and set time zone separately, PHP 5.2.6 does not respect time zone
736
    // argument in __construct().
737
    parent::__construct($time);
738
    $tz = $tz ? $tz : new DateTimeZone("UTC");
739
    $this->setTimeZone($tz);
740

    
741
    // Verify that timezone has not been specified as an offset.
742
    if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
743
      $this->setTimezone(new DateTimeZone("UTC"));
744
    }
745

    
746
    // Finally set granularity.
747
    $this->setGranularityFromTime($time, $tz);
748
  }
749

    
750
  /**
751
   * Helper function to prepare the object during serialization.
752
   *
753
   * We are extending a core class and core classes cannot be serialized.
754
   *
755
   * Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
756
   */
757
  public function __sleep() {
758
    $this->_serialized_time = $this->format('c');
759
    $this->_serialized_timezone = $this->getTimezone()->getName();
760
    return array('_serialized_time', '_serialized_timezone');
761
  }
762

    
763
  /**
764
   * Upon unserializing, we must re-build ourselves using local variables.
765
   */
766
  public function __wakeup() {
767
    $this->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
768
  }
769

    
770
  /**
771
   * Returns the string representation.
772
   *
773
   * Will try to use the literal input, if that is a string. Fallsback to
774
   * ISO-8601.
775
   *
776
   * @return string
777
   *   The string version of this DateTime object.
778
   */
779
  public function __toString() {
780
    if (is_scalar($this->originalValue)) {
781
      return (string) $this->originalValue;
782
    }
783

    
784
    return $this->format('Y-m-d\TH:i:sO');
785
  }
786

    
787
  /**
788
   * This function will keep this object's values by default.
789
   */
790
  public function merge(FeedsDateTime $other) {
791
    $other_tz = $other->getTimezone();
792
    $this_tz = $this->getTimezone();
793
    // Figure out which timezone to use for combination.
794
    $use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
795

    
796
    $this2 = clone $this;
797
    $this2->setTimezone($use_tz);
798
    $other->setTimezone($use_tz);
799
    $val = $this2->toArray();
800
    $otherval = $other->toArray();
801
    foreach (self::$allgranularity as $g) {
802
      if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
803
        // The other class has a property we don't; steal it.
804
        $this2->addGranularity($g);
805
        $val[$g] = $otherval[$g];
806
      }
807
    }
808
    $other->setTimezone($other_tz);
809

    
810
    $this2->setDate($val['year'], $val['month'], $val['day']);
811
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
812
    return $this2;
813
  }
814

    
815
  /**
816
   * Overrides default DateTime function. Only changes output values if
817
   * actually had time granularity. This should be used as a "converter" for
818
   * output, to switch tzs.
819
   *
820
   * In order to set a timezone for a datetime that doesn't have such
821
   * granularity, merge() it with one that does.
822
   */
823
  public function setTimezone($tz, $force = FALSE) {
824
    // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
825
    // http://bugs.php.net/bug.php?id=45038
826
    if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
827
      $tz = new DateTimeZone($tz->getName());
828
    }
829

    
830
    if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
831
      // this has no time or timezone granularity, so timezone doesn't mean much
832
      // We set the timezone using the method, which will change the day/hour, but then we switch back
833
      $arr = $this->toArray();
834
      parent::setTimezone($tz);
835
      $this->setDate($arr['year'], $arr['month'], $arr['day']);
836
      $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
837
      return;
838
    }
839
    parent::setTimezone($tz);
840
  }
841

    
842
  /**
843
   * Safely adds a granularity entry to the array.
844
   */
845
  public function addGranularity($g) {
846
    $this->granularity[] = $g;
847
    $this->granularity = array_unique($this->granularity);
848
  }
849

    
850
  /**
851
   * Removes a granularity entry from the array.
852
   */
853
  public function removeGranularity($g) {
854
    if ($key = array_search($g, $this->granularity)) {
855
      unset($this->granularity[$key]);
856
    }
857
  }
858

    
859
  /**
860
   * Checks granularity array for a given entry.
861
   */
862
  public function hasGranularity($g) {
863
    return in_array($g, $this->granularity);
864
  }
865

    
866
  /**
867
   * Returns whether this object has time set. Used primarily for timezone
868
   * conversion and fomratting.
869
   *
870
   * @todo currently very simplistic, but effective, see usage
871
   */
872
  public function hasTime() {
873
    return $this->hasGranularity('hour');
874
  }
875

    
876
  /**
877
   * Protected function to find the granularity given by the arguments to the
878
   * constructor.
879
   */
880
  protected function setGranularityFromTime($time, $tz) {
881
    $this->granularity = array();
882
    $temp = date_parse($time);
883
    // This PHP method currently doesn't have resolution down to seconds, so if
884
    // there is some time, all will be set.
885
    foreach (self::$allgranularity AS $g) {
886
      if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'zone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
887
        $this->granularity[] = $g;
888
      }
889
    }
890
    if ($tz) {
891
      $this->addGranularity('zone');
892
    }
893
  }
894

    
895
  /**
896
   * Helper to return all standard date parts in an array.
897
   */
898
  protected function toArray() {
899
    return array('year' => $this->format('Y'), 'month' => $this->format('m'), 'day' => $this->format('d'), 'hour' => $this->format('H'), 'minute' => $this->format('i'), 'second' => $this->format('s'), 'zone' => $this->format('e'));
900
  }
901

    
902
}
903

    
904
/**
905
 * Converts to UNIX time.
906
 *
907
 * @param $date
908
 *   A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
909
 * @param $default_value
910
 *   A default UNIX timestamp to return if $date could not be parsed.
911
 *
912
 * @return
913
 *   $date as UNIX time if conversion was successful, $dfeault_value otherwise.
914
 */
915
function feeds_to_unixtime($date, $default_value) {
916
  if (is_numeric($date)) {
917
    return $date;
918
  }
919

    
920
  if ($date instanceof FeedsDateTimeElement) {
921
    return $date->getValue();
922
  }
923

    
924
  if (is_string($date) || is_object($date) && method_exists($date, '__toString')) {
925
    if ($date_object = date_create(trim($date))) {
926
      return $date_object->format('U');
927
    }
928
  }
929

    
930
  return $default_value;
931
}