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 @ ed9a13f1

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
/**
53
 * Abstract class, defines interface for parsers.
54
 */
55
abstract class FeedsParser extends FeedsPlugin {
56

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

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

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

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

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

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

    
159
    switch ($element_key) {
160

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

    
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
/**
192
 * Defines an element of a parsed result. Such an element can be a simple type,
193
 * a complex type (derived from FeedsElement) or an array of either.
194
 *
195
 * @see FeedsEnclosure
196
 */
197
class FeedsElement {
198

    
199
  /**
200
   * The standard value of this element. This value can contain be a simple
201
   * type, a FeedsElement or an array of either.
202
   */
203
  protected $value;
204

    
205
  /**
206
   * Constructor.
207
   */
208
  public function __construct($value) {
209
    $this->value = $value;
210
  }
211

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

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

    
239
}
240

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

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

    
274
  /**
275
   * Use $name as $value.
276
   */
277
  public function getValue() {
278
    return $this->name;
279
  }
280

    
281
}
282

    
283
/**
284
 * A geo term element.
285
 */
286
class FeedsGeoTermElement extends FeedsTermElement {
287
  public $lat, $lon, $bound_top, $bound_right, $bound_bottom, $bound_left, $geometry;
288

    
289
  /**
290
   * @param $term
291
   *   An array or a stdClass object that is a Drupal taxonomy term. Can include
292
   *   geo extensions.
293
   */
294
  public function __construct($term) {
295
    parent::__construct($term);
296
  }
297

    
298
}
299

    
300
/**
301
 * Enclosure element, can be part of the result array.
302
 */
303
class FeedsEnclosure extends FeedsElement {
304

    
305
  /**
306
   * The mime type of the enclosure.
307
   *
308
   * @param string
309
   */
310
  protected $mime_type;
311

    
312
  /**
313
   * The default list of allowed extensions.
314
   *
315
   * @param string
316
   */
317
  protected $allowedExtensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
318

    
319
  /**
320
   * The sanitized local file name.
321
   *
322
   * @var string
323
   */
324
  protected $safeFilename;
325

    
326
  /**
327
   * Constructor, requires MIME type.
328
   *
329
   * @param $value
330
   *   A path to a local file or a URL to a remote document.
331
   * @param $mimetype
332
   *   The mime type of the resource.
333
   */
334
  public function __construct($value, $mime_type) {
335
    parent::__construct($value);
336
    $this->mime_type = $mime_type;
337
  }
338

    
339
  /**
340
   * @return
341
   *   MIME type of return value of getValue().
342
   */
343
  public function getMIMEType() {
344
    return $this->mime_type;
345
  }
346

    
347
  /**
348
   * Sets the list of allowed extensions.
349
   *
350
   * @param string $extensions
351
   *   The list of allowed extensions separated by a space.
352
   */
353
  public function setAllowedExtensions($extensions) {
354
    // Normalize whitespace so that empty extensions are not allowed.
355
    $this->allowedExtensions = drupal_strtolower(trim(preg_replace('/\s+/', ' ', $extensions)));
356
  }
357

    
358
  /**
359
   * Use this method instead of FeedsElement::getValue() when fetching the file
360
   * from the URL.
361
   *
362
   * @return
363
   *   Value with encoded space characters to safely fetch the file from the URL.
364
   *
365
   * @see FeedsElement::getValue()
366
   */
367
  public function getUrlEncodedValue() {
368
    return str_replace(' ', '%20', $this->getValue());
369
  }
370

    
371
  /**
372
   * Returns the full path to the file URI with a safe file name.
373
   *
374
   * @return string
375
   *   The safe file URI.
376
   *
377
   * @throws RuntimeException
378
   *   Thrown if the file extension is invalid.
379
   */
380
  public function getSanitizedUri() {
381
    return drupal_dirname($this->getValue()) . '/' . $this->getSafeFilename();
382
  }
383

    
384
  /**
385
   * Returns the file name transformed for better local saving.
386
   *
387
   * @return string
388
   *   Value with space characters changed to underscores.
389
   *
390
   * @throws RuntimeException
391
   *   Thrown if the file extension is invalid.
392
   */
393
  public function getLocalValue() {
394
    return str_replace(' ', '_', $this->getSafeFilename());
395
  }
396

    
397
  /**
398
   * Returns the safe file name.
399
   *
400
   * @return string
401
   *   A filename that is safe to save to the filesystem.
402
   *
403
   * @throws RuntimeException
404
   *   Thrown if the file extension is invalid.
405
   */
406
  protected function getSafeFilename() {
407
    if (isset($this->safeFilename)) {
408
      return $this->safeFilename;
409
    }
410

    
411
    // Strip any query string or fragment from file name.
412
    list($filename) = explode('?', $this->getValue());
413
    list($filename) = explode('#', $filename);
414

    
415
    $filename = rawurldecode(drupal_basename($filename));
416

    
417
    // Remove leading and trailing whitespace and periods.
418
    $filename = trim($filename, " \t\n\r\0\x0B.");
419

    
420
    if (strpos($filename, '.') === FALSE) {
421
      $extension = FALSE;
422
    }
423
    else {
424
      $extension = drupal_strtolower(substr($filename, strrpos($filename, '.') + 1));
425
    }
426

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

    
431
    $this->safeFilename = file_munge_filename($filename, $this->allowedExtensions, FALSE);
432

    
433
    return $this->safeFilename;
434
  }
435

    
436
  /**
437
   * Downloads the content from the file URL.
438
   *
439
   * @return string
440
   *   The content of the referenced resource.
441
   *
442
   * @throws FeedsHTTPRequestException
443
   *   In case the result code of the HTTP request is not in the 2xx series.
444
   */
445
  public function getContent() {
446
    feeds_include_library('http_request.inc', 'http_request');
447
    $result = feeds_http_request($this->getUrlEncodedValue());
448
    http_request_check_result($this->getUrlEncodedValue(), $result);
449
    return $result->data;
450
  }
451

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

    
483
        if (drupal_dirname($file->uri) !== $destination) {
484
          $file = file_copy($file, $destination, $replace);
485
        }
486
        else {
487
          // If file is not to be copied, check whether file already exists,
488
          // as file_save() won't do that for us (compare file_copy() and
489
          // file_save())
490
          $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
491
          if (count($existing_files)) {
492
            $existing = reset($existing_files);
493
            if ($replace == FEEDS_FILE_EXISTS_SKIP) {
494
              return $existing;
495
            }
496
            $file->fid = $existing->fid;
497
            $file->filename = $existing->filename;
498
          }
499
          file_save($file);
500
        }
501
      }
502
      else {
503
        if (file_uri_target($destination)) {
504
          $destination = trim($destination, '/') . '/';
505
        }
506
        try {
507
          $filename = $this->getLocalValue();
508

    
509
          if (module_exists('transliteration')) {
510
            require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
511
            $filename = transliteration_clean_filename($filename);
512
          }
513

    
514
          $file = file_save_data($this->getContent(), $destination . $filename, $replace);
515
        }
516
        catch (Exception $e) {
517
          watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
518
        }
519
      }
520

    
521
      // We couldn't make sense of this enclosure, throw an exception.
522
      if (!$file) {
523
        throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
524
      }
525

    
526
      return $file;
527
    }
528
  }
529

    
530
}
531

    
532
/**
533
 * Defines a date element of a parsed result (including ranges, repeat).
534
 *
535
 * @deprecated This is no longer in use and will not be maintained.
536
 */
537
class FeedsDateTimeElement extends FeedsElement {
538

    
539
  /**
540
   * Start date and end date.
541
   */
542
  public $start;
543
  public $end;
544

    
545
  /**
546
   * Constructor.
547
   *
548
   * @param $start
549
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
550
   * @param $end
551
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
552
   * @param $tz
553
   *   A PHP DateTimeZone object.
554
   */
555
  public function __construct($start = NULL, $end = NULL, $tz = NULL) {
556
    $this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
557
    $this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
558
  }
559

    
560
  /**
561
   * Override FeedsElement::getValue().
562
   *
563
   * @return
564
   *   The UNIX timestamp of this object's start date. Return value is
565
   *   technically a string but will only contain numeric values.
566
   */
567
  public function getValue() {
568
    if ($this->start) {
569
      return $this->start->format('U');
570
    }
571
    return '0';
572
  }
573

    
574
  /**
575
   * Merge this field with another. Most stuff goes down when merging the two
576
   * sub-dates.
577
   *
578
   * @see FeedsDateTime
579
   */
580
  public function merge(FeedsDateTimeElement $other) {
581
    $this2 = clone $this;
582
    if ($this->start && $other->start) {
583
      $this2->start = $this->start->merge($other->start);
584
    }
585
    elseif ($other->start) {
586
      $this2->start = clone $other->start;
587
    }
588
    elseif ($this->start) {
589
      $this2->start = clone $this->start;
590
    }
591

    
592
    if ($this->end && $other->end) {
593
      $this2->end = $this->end->merge($other->end);
594
    }
595
    elseif ($other->end) {
596
      $this2->end = clone $other->end;
597
    }
598
    elseif ($this->end) {
599
      $this2->end = clone $this->end;
600
    }
601
    return $this2;
602
  }
603

    
604
  /**
605
   * Helper method for buildDateField(). Build a FeedsDateTimeElement object
606
   * from a standard formatted node.
607
   */
608
  protected static function readDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
609
    $ret = new FeedsDateTimeElement();
610
    if (isset($entity->{$field_name}[$language][$delta]['date']) && $entity->{$field_name}[$language][$delta]['date'] instanceof FeedsDateTime) {
611
      $ret->start = $entity->{$field_name}[$language][$delta]['date'];
612
    }
613
    if (isset($entity->{$field_name}[$language][$delta]['date2']) && $entity->{$field_name}[$language][$delta]['date2'] instanceof FeedsDateTime) {
614
      $ret->end = $entity->{$field_name}[$language][$delta]['date2'];
615
    }
616
    return $ret;
617
  }
618

    
619
  /**
620
   * Build a entity's date field from our object.
621
   *
622
   * @param object $entity
623
   *   The entity to build the date field on.
624
   * @param string $field_name
625
   *   The name of the field to build.
626
   * @param int $delta
627
   *   The delta in the field.
628
   */
629
  public function buildDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
630
    $info = field_info_field($field_name);
631

    
632
    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta, $language);
633
    // Merge with any preexisting objects on the field; we take precedence.
634
    $oldfield = $this->merge($oldfield);
635
    $use_start = $oldfield->start;
636
    $use_end = $oldfield->end;
637

    
638
    // Set timezone if not already in the FeedsDateTime object.
639
    $to_tz = date_get_timezone($info['settings']['tz_handling'], date_default_timezone());
640
    $temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
641

    
642
    $db_tz = '';
643
    if ($use_start) {
644
      $use_start = $use_start->merge($temp);
645
      if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
646
        $use_start->setTimezone(new DateTimeZone("UTC"));
647
      }
648
      $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_start->getTimezone()->getName());
649
    }
650
    if ($use_end) {
651
      $use_end = $use_end->merge($temp);
652
      if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
653
        $use_end->setTimezone(new DateTimeZone("UTC"));
654
      }
655
      if (!$db_tz) {
656
        $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_end->getTimezone()->getName());
657
      }
658
    }
659
    if (!$db_tz) {
660
      return;
661
    }
662

    
663
    $db_tz = new DateTimeZone($db_tz);
664
    if (!isset($entity->{$field_name})) {
665
      $entity->{$field_name} = array($language => array());
666
    }
667
    if ($use_start) {
668
      $entity->{$field_name}[$language][$delta]['timezone'] = $use_start->getTimezone()->getName();
669
      $entity->{$field_name}[$language][$delta]['offset'] = $use_start->getOffset();
670
      $use_start->setTimezone($db_tz);
671
      $entity->{$field_name}[$language][$delta]['date'] = $use_start;
672
      /**
673
       * @todo the date_type_format line could be simplified based upon a patch
674
       *   DO issue #259308 could affect this, follow up on at some point.
675
       *   Without this, all granularity info is lost.
676
       *   $use_start->format(date_type_format($field['type'], $use_start->granularity));
677
       */
678
      $entity->{$field_name}[$language][$delta]['value'] = $use_start->format(date_type_format($info['type']));
679
    }
680
    if ($use_end) {
681
      // Don't ever use end to set timezone (for now)
682
      $entity->{$field_name}[$language][$delta]['offset2'] = $use_end->getOffset();
683
      $use_end->setTimezone($db_tz);
684
      $entity->{$field_name}[$language][$delta]['date2'] = $use_end;
685
      $entity->{$field_name}[$language][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
686
    }
687
  }
688

    
689
}
690

    
691
/**
692
 * Extend PHP DateTime class with granularity handling, merge functionality and
693
 * slightly more flexible initialization parameters.
694
 *
695
 * This class is a Drupal independent extension of the >= PHP 5.2 DateTime
696
 * class.
697
 *
698
 * @see FeedsDateTimeElement
699
 *
700
 * @deprecated Use DateObject instead.
701
 */
702
class FeedsDateTime extends DateTime {
703
  public $granularity = array();
704
  protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
705
  private $_serialized_time;
706
  private $_serialized_timezone;
707

    
708
  /**
709
   * The original time value passed into the constructor.
710
   *
711
   * @var mixed
712
   */
713
  protected $originalValue;
714

    
715
  /**
716
   * Overridden constructor.
717
   *
718
   * @param $time
719
   *   time string, flexible format including timestamp. Invalid formats will
720
   *   fall back to 'now'.
721
   * @param $tz
722
   *   PHP DateTimeZone object, NULL allowed
723
   */
724
  public function __construct($time = '', $tz = NULL) {
725
    $this->originalValue = $time;
726

    
727
    if (is_numeric($time)) {
728
      // Assume UNIX timestamp if it doesn't look like a simple year.
729
      if (strlen($time) > 4) {
730
        $time = "@" . $time;
731
      }
732
      // If it's a year, add a default month too, because PHP's date functions
733
      // won't parse standalone years after 2000 correctly (see explanation at
734
      // http://aaronsaray.com/blog/2007/07/11/helpful-strtotime-reminders/#comment-47).
735
      else {
736
        $time = 'January ' . $time;
737
      }
738
    }
739

    
740
    // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
741
    $time = str_replace("GMT-", "-", $time);
742
    $time = str_replace("GMT+", "+", $time);
743

    
744
    // Some PHP 5.2 version's DateTime class chokes on invalid dates.
745
    if (!date_create($time)) {
746
      $time = 'now';
747
    }
748

    
749
    // Create and set time zone separately, PHP 5.2.6 does not respect time zone
750
    // argument in __construct().
751
    parent::__construct($time);
752
    $tz = $tz ? $tz : new DateTimeZone("UTC");
753
    $this->setTimeZone($tz);
754

    
755
    // Verify that timezone has not been specified as an offset.
756
    if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
757
      $this->setTimezone(new DateTimeZone("UTC"));
758
    }
759

    
760
    // Finally set granularity.
761
    $this->setGranularityFromTime($time, $tz);
762
  }
763

    
764
  /**
765
   * Helper function to prepare the object during serialization.
766
   *
767
   * We are extending a core class and core classes cannot be serialized.
768
   *
769
   * Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
770
   */
771
  public function __sleep() {
772
    $this->_serialized_time = $this->format('c');
773
    $this->_serialized_timezone = $this->getTimezone()->getName();
774
    return array('_serialized_time', '_serialized_timezone');
775
  }
776

    
777
  /**
778
   * Upon unserializing, we must re-build ourselves using local variables.
779
   */
780
  public function __wakeup() {
781
    $this->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
782
  }
783

    
784
  /**
785
   * Returns the string representation.
786
   *
787
   * Will try to use the literal input, if that is a string. Fallsback to
788
   * ISO-8601.
789
   *
790
   * @return string
791
   *   The string version of this DateTime object.
792
   */
793
  public function __toString() {
794
    if (is_scalar($this->originalValue)) {
795
      return (string) $this->originalValue;
796
    }
797

    
798
    return $this->format('Y-m-d\TH:i:sO');
799
  }
800

    
801
  /**
802
   * This function will keep this object's values by default.
803
   */
804
  public function merge(FeedsDateTime $other) {
805
    $other_tz = $other->getTimezone();
806
    $this_tz = $this->getTimezone();
807
    // Figure out which timezone to use for combination.
808
    $use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
809

    
810
    $this2 = clone $this;
811
    $this2->setTimezone($use_tz);
812
    $other->setTimezone($use_tz);
813
    $val = $this2->toArray();
814
    $otherval = $other->toArray();
815
    foreach (self::$allgranularity as $g) {
816
      if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
817
        // The other class has a property we don't; steal it.
818
        $this2->addGranularity($g);
819
        $val[$g] = $otherval[$g];
820
      }
821
    }
822
    $other->setTimezone($other_tz);
823

    
824
    $this2->setDate($val['year'], $val['month'], $val['day']);
825
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
826
    return $this2;
827
  }
828

    
829
  /**
830
   * Overrides default DateTime function. Only changes output values if
831
   * actually had time granularity. This should be used as a "converter" for
832
   * output, to switch tzs.
833
   *
834
   * In order to set a timezone for a datetime that doesn't have such
835
   * granularity, merge() it with one that does.
836
   */
837
  public function setTimezone($tz, $force = FALSE) {
838
    // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
839
    // http://bugs.php.net/bug.php?id=45038
840
    if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
841
      $tz = new DateTimeZone($tz->getName());
842
    }
843

    
844
    if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
845
      // This has no time or timezone granularity, so timezone doesn't mean much
846
      // We set the timezone using the method, which will change the day/hour, but then we switch back.
847
      $arr = $this->toArray();
848
      parent::setTimezone($tz);
849
      $this->setDate($arr['year'], $arr['month'], $arr['day']);
850
      $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
851
      return;
852
    }
853
    parent::setTimezone($tz);
854
  }
855

    
856
  /**
857
   * Safely adds a granularity entry to the array.
858
   */
859
  public function addGranularity($g) {
860
    $this->granularity[] = $g;
861
    $this->granularity = array_unique($this->granularity);
862
  }
863

    
864
  /**
865
   * Removes a granularity entry from the array.
866
   */
867
  public function removeGranularity($g) {
868
    if ($key = array_search($g, $this->granularity)) {
869
      unset($this->granularity[$key]);
870
    }
871
  }
872

    
873
  /**
874
   * Checks granularity array for a given entry.
875
   */
876
  public function hasGranularity($g) {
877
    return in_array($g, $this->granularity);
878
  }
879

    
880
  /**
881
   * Returns whether this object has time set. Used primarily for timezone
882
   * conversion and fomratting.
883
   *
884
   * @todo currently very simplistic, but effective, see usage
885
   */
886
  public function hasTime() {
887
    return $this->hasGranularity('hour');
888
  }
889

    
890
  /**
891
   * Protected function to find the granularity given by the arguments to the
892
   * constructor.
893
   */
894
  protected function setGranularityFromTime($time, $tz) {
895
    $this->granularity = array();
896
    $temp = date_parse($time);
897
    // This PHP method currently doesn't have resolution down to seconds, so if
898
    // there is some time, all will be set.
899
    foreach (self::$allgranularity as $g) {
900
      if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'zone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
901
        $this->granularity[] = $g;
902
      }
903
    }
904
    if ($tz) {
905
      $this->addGranularity('zone');
906
    }
907
  }
908

    
909
  /**
910
   * Helper to return all standard date parts in an array.
911
   */
912
  protected function toArray() {
913
    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'));
914
  }
915

    
916
}
917

    
918
/**
919
 * Converts to UNIX time.
920
 *
921
 * @param $date
922
 *   A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
923
 * @param $default_value
924
 *   A default UNIX timestamp to return if $date could not be parsed.
925
 *
926
 * @return
927
 *   $date as UNIX time if conversion was successful, $dfeault_value otherwise.
928
 */
929
function feeds_to_unixtime($date, $default_value) {
930
  if (is_numeric($date)) {
931
    return $date;
932
  }
933

    
934
  if ($date instanceof FeedsDateTimeElement) {
935
    return $date->getValue();
936
  }
937

    
938
  if (is_string($date) || is_object($date) && method_exists($date, '__toString')) {
939
    if ($date_object = date_create(trim($date))) {
940
      return $date_object->format('U');
941
    }
942
  }
943

    
944
  return $default_value;
945
}