Projet

Général

Profil

Paste
Télécharger (25,5 ko) Statistiques
| Branche: | Révision:

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

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
/**
177
 * Defines an element of a parsed result. Such an element can be a simple type,
178
 * a complex type (derived from FeedsElement) or an array of either.
179
 *
180
 * @see FeedsEnclosure
181
 */
182
class FeedsElement {
183
  // The standard value of this element. This value can contain be a simple type,
184
  // a FeedsElement or an array of either.
185
  protected $value;
186

    
187
  /**
188
   * Constructor.
189
   */
190
  public function __construct($value) {
191
    $this->value = $value;
192
  }
193

    
194
  /**
195
   * @todo Make value public and deprecate use of getValue().
196
   *
197
   * @return
198
   *   Value of this FeedsElement represented as a scalar.
199
   */
200
  public function getValue() {
201
    return $this->value;
202
  }
203

    
204
  /**
205
   * Magic method __toString() for printing and string conversion of this
206
   * object.
207
   *
208
   * @return
209
   *   A string representation of this element.
210
   */
211
  public function __toString() {
212
    if (is_array($this->value)) {
213
      return 'Array';
214
    }
215
    if (is_object($this->value)) {
216
      return 'Object';
217
    }
218
    return (string) $this->getValue();
219
  }
220
}
221

    
222
/**
223
 * Encapsulates a taxonomy style term object.
224
 *
225
 * Objects of this class can be turned into a taxonomy term style arrays by
226
 * casting them.
227
 *
228
 * @code
229
 *   $term_object = new FeedsTermElement($term_array);
230
 *   $term_array = (array)$term_object;
231
 * @endcode
232
 */
233
class FeedsTermElement extends FeedsElement {
234
  public $tid, $vid, $name;
235

    
236
  /**
237
   * @param $term
238
   *   An array or a stdClass object that is a Drupal taxonomy term.
239
   */
240
  public function __construct($term) {
241
    if (is_array($term)) {
242
      parent::__construct($term['name']);
243
      foreach ($this as $key => $value) {
244
        $this->$key = isset($term[$key]) ? $term[$key] : NULL;
245
      }
246
    }
247
    elseif (is_object($term)) {
248
      parent::__construct($term->name);
249
      foreach ($this as $key => $value) {
250
        $this->$key = isset($term->$key) ? $term->$key : NULL;
251
      }
252
    }
253
  }
254

    
255
  /**
256
   * Use $name as $value.
257
   */
258
  public function getValue() {
259
    return $this->name;
260
  }
261
}
262

    
263
/**
264
 * A geo term element.
265
 */
266
class FeedsGeoTermElement extends FeedsTermElement {
267
  public $lat, $lon, $bound_top, $bound_right, $bound_bottom, $bound_left, $geometry;
268
  /**
269
   * @param $term
270
   *   An array or a stdClass object that is a Drupal taxonomy term. Can include
271
   *   geo extensions.
272
   */
273
  public function __construct($term) {
274
    parent::__construct($term);
275
  }
276
}
277

    
278
/**
279
 * Enclosure element, can be part of the result array.
280
 */
281
class FeedsEnclosure extends FeedsElement {
282

    
283
  /**
284
   * The mime type of the enclosure.
285
   *
286
   * @param string
287
   */
288
   protected $mime_type;
289

    
290
   /**
291
   * The default list of allowed extensions.
292
   *
293
   * @param string
294
   */
295
  protected $allowedExtensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
296

    
297
  /**
298
   * The sanitized local file name.
299
   *
300
   * @var string
301
   */
302
  protected $safeFilename;
303

    
304
  /**
305
   * Constructor, requires MIME type.
306
   *
307
   * @param $value
308
   *   A path to a local file or a URL to a remote document.
309
   * @param $mimetype
310
   *   The mime type of the resource.
311
   */
312
  public function __construct($value, $mime_type) {
313
    parent::__construct($value);
314
    $this->mime_type = $mime_type;
315
  }
316

    
317
  /**
318
   * @return
319
   *   MIME type of return value of getValue().
320
   */
321
  public function getMIMEType() {
322
    return $this->mime_type;
323
  }
324

    
325
  /**
326
   * Sets the list of allowed extensions.
327
   *
328
   * @param string $extensions
329
   *   The list of allowed extensions separated by a space.
330
   */
331
  public function setAllowedExtensions($extensions) {
332
    // Normalize whitespace so that empty extensions are not allowed.
333
    $this->allowedExtensions = drupal_strtolower(trim(preg_replace('/\s+/', ' ', $extensions)));
334
  }
335

    
336
  /**
337
   * Use this method instead of FeedsElement::getValue() when fetching the file
338
   * from the URL.
339
   *
340
   * @return
341
   *   Value with encoded space characters to safely fetch the file from the URL.
342
   *
343
   * @see FeedsElement::getValue()
344
   */
345
  public function getUrlEncodedValue() {
346
    return str_replace(' ', '%20', $this->getValue());
347
  }
348

    
349
  /**
350
   * Returns the full path to the file URI with a safe file name.
351
   *
352
   * @return string
353
   *   The safe file URI.
354
   *
355
   * @throws RuntimeException
356
   *   Thrown if the file extension is invalid.
357
   */
358
  public function getSanitizedUri() {
359
    return drupal_dirname($this->getValue()) . '/' . $this->getSafeFilename();
360
  }
361

    
362
  /**
363
   * Returns the file name transformed for better local saving.
364
   *
365
   * @return string
366
   *   Value with space characters changed to underscores.
367
   *
368
   * @throws RuntimeException
369
   *   Thrown if the file extension is invalid.
370
   */
371
  public function getLocalValue() {
372
    return str_replace(' ', '_', $this->getSafeFilename());
373
  }
374

    
375
  /**
376
   * Returns the safe file name.
377
   *
378
   * @return string
379
   *   A filename that is safe to save to the filesystem.
380
   *
381
   * @throws RuntimeException
382
   *   Thrown if the file extension is invalid.
383
   */
384
  protected function getSafeFilename() {
385
    if (isset($this->safeFilename)) {
386
      return $this->safeFilename;
387
    }
388

    
389
    // Strip any query string or fragment from file name.
390
    list($filename) = explode('?', $this->getValue());
391
    list($filename) = explode('#', $filename);
392

    
393
    $filename = rawurldecode(drupal_basename($filename));
394

    
395
    // Remove leading and trailing whitespace and periods.
396
    $filename = trim($filename, " \t\n\r\0\x0B.");
397

    
398
    if (strpos($filename, '.') === FALSE) {
399
      $extension = FALSE;
400
    }
401
    else {
402
      $extension = drupal_strtolower(substr($filename, strrpos($filename, '.') + 1));
403
    }
404

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

    
409
    $this->safeFilename = file_munge_filename($filename, $this->allowedExtensions, FALSE);
410

    
411
    return $this->safeFilename;
412
  }
413

    
414
  /**
415
   * Downloads the content from the file URL.
416
   *
417
   * @return string
418
   *   The content of the referenced resource.
419
   */
420
  public function getContent() {
421
    feeds_include_library('http_request.inc', 'http_request');
422
    $result = http_request_get($this->getUrlEncodedValue());
423
    if ($result->code != 200) {
424
      throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->getUrlEncodedValue(), '!code' => $result->code)));
425
    }
426
    return $result->data;
427
  }
428

    
429
  /**
430
   * Get a Drupal file object of the enclosed resource, download if necessary.
431
   *
432
   * @param $destination
433
   *   The path or uri specifying the target directory in which the file is
434
   *   expected. Don't use trailing slashes unless it's a streamwrapper scheme.
435
   *
436
   * @return
437
   *   A Drupal temporary file object of the enclosed resource.
438
   *
439
   * @throws Exception
440
   *   If file object could not be created.
441
   */
442
  public function getFile($destination) {
443
    $file = NULL;
444
    if ($this->getValue()) {
445
      // Prepare destination directory.
446
      file_prepare_directory($destination, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
447
      // Copy or save file depending on whether it is remote or local.
448
      if (drupal_realpath($this->getSanitizedUri())) {
449
        $file           = new stdClass();
450
        $file->uid      = 0;
451
        $file->uri      = $this->getSanitizedUri();
452
        $file->filemime = $this->getMIMEType();
453
        $file->filename = $this->getSafeFilename();
454

    
455
        if (drupal_dirname($file->uri) !== $destination) {
456
          $file = file_copy($file, $destination);
457
        }
458
        else {
459
          // If file is not to be copied, check whether file already exists,
460
          // as file_save() won't do that for us (compare file_copy() and
461
          // file_save())
462
          $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
463
          if (count($existing_files)) {
464
            $existing = reset($existing_files);
465
            $file->fid = $existing->fid;
466
            $file->filename = $existing->filename;
467
          }
468
          file_save($file);
469
        }
470
      }
471
      else {
472
        if (file_uri_target($destination)) {
473
          $destination = trim($destination, '/') . '/';
474
        }
475
        try {
476
          $filename = $this->getLocalValue();
477

    
478
          if (module_exists('transliteration')) {
479
            require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
480
            $filename = transliteration_clean_filename($filename);
481
          }
482

    
483
          $file = file_save_data($this->getContent(), $destination . $filename);
484
        }
485
        catch (Exception $e) {
486
          watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
487
        }
488
      }
489

    
490
      // We couldn't make sense of this enclosure, throw an exception.
491
      if (!$file) {
492
        throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
493
      }
494

    
495
      return $file;
496
    }
497
  }
498
}
499

    
500
/**
501
 * Defines a date element of a parsed result (including ranges, repeat).
502
 */
503
class FeedsDateTimeElement extends FeedsElement {
504

    
505
  // Start date and end date.
506
  public $start;
507
  public $end;
508

    
509
  /**
510
   * Constructor.
511
   *
512
   * @param $start
513
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
514
   * @param $end
515
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
516
   * @param $tz
517
   *   A PHP DateTimeZone object.
518
   */
519
  public function __construct($start = NULL, $end = NULL, $tz = NULL) {
520
    $this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
521
    $this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
522
  }
523

    
524
  /**
525
   * Override FeedsElement::getValue().
526
   *
527
   * @return
528
   *   The UNIX timestamp of this object's start date. Return value is
529
   *   technically a string but will only contain numeric values.
530
   */
531
  public function getValue() {
532
    if ($this->start) {
533
      return $this->start->format('U');
534
    }
535
    return '0';
536
  }
537

    
538
  /**
539
   * Merge this field with another. Most stuff goes down when merging the two
540
   * sub-dates.
541
   *
542
   * @see FeedsDateTime
543
   */
544
  public function merge(FeedsDateTimeElement $other) {
545
    $this2 = clone $this;
546
    if ($this->start && $other->start) {
547
      $this2->start = $this->start->merge($other->start);
548
    }
549
    elseif ($other->start) {
550
      $this2->start = clone $other->start;
551
    }
552
    elseif ($this->start) {
553
      $this2->start = clone $this->start;
554
    }
555

    
556
    if ($this->end && $other->end) {
557
      $this2->end = $this->end->merge($other->end);
558
    }
559
    elseif ($other->end) {
560
      $this2->end = clone $other->end;
561
    }
562
    elseif ($this->end) {
563
      $this2->end = clone $this->end;
564
    }
565
    return $this2;
566
  }
567

    
568
  /**
569
   * Helper method for buildDateField(). Build a FeedsDateTimeElement object
570
   * from a standard formatted node.
571
   */
572
  protected static function readDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
573
    $ret = new FeedsDateTimeElement();
574
    if (isset($entity->{$field_name}[$language][$delta]['date']) && $entity->{$field_name}[$language][$delta]['date'] instanceof FeedsDateTime) {
575
      $ret->start = $entity->{$field_name}[$language][$delta]['date'];
576
    }
577
    if (isset($entity->{$field_name}[$language][$delta]['date2']) && $entity->{$field_name}[$language][$delta]['date2'] instanceof FeedsDateTime) {
578
      $ret->end = $entity->{$field_name}[$language][$delta]['date2'];
579
    }
580
    return $ret;
581
  }
582

    
583
  /**
584
   * Build a entity's date field from our object.
585
   *
586
   * @param object $entity
587
   *   The entity to build the date field on.
588
   * @param str $field_name
589
   *   The name of the field to build.
590
   * @param int $delta
591
   *   The delta in the field.
592
   */
593
  public function buildDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
594
    $info = field_info_field($field_name);
595

    
596
    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta, $language);
597
    // Merge with any preexisting objects on the field; we take precedence.
598
    $oldfield = $this->merge($oldfield);
599
    $use_start = $oldfield->start;
600
    $use_end = $oldfield->end;
601

    
602
    // Set timezone if not already in the FeedsDateTime object
603
    $to_tz = date_get_timezone($info['settings']['tz_handling'], date_default_timezone());
604
    $temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
605

    
606
    $db_tz = '';
607
    if ($use_start) {
608
      $use_start = $use_start->merge($temp);
609
      if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
610
        $use_start->setTimezone(new DateTimeZone("UTC"));
611
      }
612
      $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_start->getTimezone()->getName());
613
    }
614
    if ($use_end) {
615
      $use_end = $use_end->merge($temp);
616
      if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
617
        $use_end->setTimezone(new DateTimeZone("UTC"));
618
      }
619
      if (!$db_tz) {
620
        $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_end->getTimezone()->getName());
621
      }
622
    }
623
    if (!$db_tz) {
624
      return;
625
    }
626

    
627
    $db_tz = new DateTimeZone($db_tz);
628
    if (!isset($entity->{$field_name})) {
629
      $entity->{$field_name} = array($language => array());
630
    }
631
    if ($use_start) {
632
      $entity->{$field_name}[$language][$delta]['timezone'] = $use_start->getTimezone()->getName();
633
      $entity->{$field_name}[$language][$delta]['offset'] = $use_start->getOffset();
634
      $use_start->setTimezone($db_tz);
635
      $entity->{$field_name}[$language][$delta]['date'] = $use_start;
636
      /**
637
       * @todo the date_type_format line could be simplified based upon a patch
638
       *   DO issue #259308 could affect this, follow up on at some point.
639
       *   Without this, all granularity info is lost.
640
       *   $use_start->format(date_type_format($field['type'], $use_start->granularity));
641
       */
642
      $entity->{$field_name}[$language][$delta]['value'] = $use_start->format(date_type_format($info['type']));
643
    }
644
    if ($use_end) {
645
      // Don't ever use end to set timezone (for now)
646
      $entity->{$field_name}[$language][$delta]['offset2'] = $use_end->getOffset();
647
      $use_end->setTimezone($db_tz);
648
      $entity->{$field_name}[$language][$delta]['date2'] = $use_end;
649
      $entity->{$field_name}[$language][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
650
    }
651
  }
652
}
653

    
654
/**
655
 * Extend PHP DateTime class with granularity handling, merge functionality and
656
 * slightly more flexible initialization parameters.
657
 *
658
 * This class is a Drupal independent extension of the >= PHP 5.2 DateTime
659
 * class.
660
 *
661
 * @see FeedsDateTimeElement
662
 */
663
class FeedsDateTime extends DateTime {
664
  public $granularity = array();
665
  protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
666
  private $_serialized_time;
667
  private $_serialized_timezone;
668

    
669
  /**
670
   * Helper function to prepare the object during serialization.
671
   *
672
   * We are extending a core class and core classes cannot be serialized.
673
   *
674
   * Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
675
   */
676
  public function __sleep() {
677
    $this->_serialized_time = $this->format('c');
678
    $this->_serialized_timezone = $this->getTimezone()->getName();
679
    return array('_serialized_time', '_serialized_timezone');
680
  }
681

    
682
  /**
683
   * Upon unserializing, we must re-build ourselves using local variables.
684
   */
685
  public function __wakeup() {
686
    $this->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
687
  }
688

    
689
  /**
690
   * Overridden constructor.
691
   *
692
   * @param $time
693
   *   time string, flexible format including timestamp. Invalid formats will
694
   *   fall back to 'now'.
695
   * @param $tz
696
   *   PHP DateTimeZone object, NULL allowed
697
   */
698
  public function __construct($time = '', $tz = NULL) {
699
    if (is_numeric($time)) {
700
      // Assume UNIX timestamp if it doesn't look like a simple year.
701
      if (strlen($time) > 4) {
702
        $time = "@" . $time;
703
      }
704
      // If it's a year, add a default month too, because PHP's date functions
705
      // won't parse standalone years after 2000 correctly (see explanation at
706
      // http://aaronsaray.com/blog/2007/07/11/helpful-strtotime-reminders/#comment-47).
707
      else {
708
        $time = 'January ' . $time;
709
      }
710
    }
711

    
712
    // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
713
    $time = str_replace("GMT-", "-", $time);
714
    $time = str_replace("GMT+", "+", $time);
715

    
716
    // Some PHP 5.2 version's DateTime class chokes on invalid dates.
717
    if (!date_create($time)) {
718
      $time = 'now';
719
    }
720

    
721
    // Create and set time zone separately, PHP 5.2.6 does not respect time zone
722
    // argument in __construct().
723
    parent::__construct($time);
724
    $tz = $tz ? $tz : new DateTimeZone("UTC");
725
    $this->setTimeZone($tz);
726

    
727
    // Verify that timezone has not been specified as an offset.
728
    if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
729
      $this->setTimezone(new DateTimeZone("UTC"));
730
    }
731

    
732
    // Finally set granularity.
733
    $this->setGranularityFromTime($time, $tz);
734
  }
735

    
736
  /**
737
   * This function will keep this object's values by default.
738
   */
739
  public function merge(FeedsDateTime $other) {
740
    $other_tz = $other->getTimezone();
741
    $this_tz = $this->getTimezone();
742
    // Figure out which timezone to use for combination.
743
    $use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
744

    
745
    $this2 = clone $this;
746
    $this2->setTimezone($use_tz);
747
    $other->setTimezone($use_tz);
748
    $val = $this2->toArray();
749
    $otherval = $other->toArray();
750
    foreach (self::$allgranularity as $g) {
751
      if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
752
        // The other class has a property we don't; steal it.
753
        $this2->addGranularity($g);
754
        $val[$g] = $otherval[$g];
755
      }
756
    }
757
    $other->setTimezone($other_tz);
758

    
759
    $this2->setDate($val['year'], $val['month'], $val['day']);
760
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
761
    return $this2;
762
  }
763

    
764
  /**
765
   * Overrides default DateTime function. Only changes output values if
766
   * actually had time granularity. This should be used as a "converter" for
767
   * output, to switch tzs.
768
   *
769
   * In order to set a timezone for a datetime that doesn't have such
770
   * granularity, merge() it with one that does.
771
   */
772
  public function setTimezone($tz, $force = FALSE) {
773
    // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
774
    // http://bugs.php.net/bug.php?id=45038
775
    if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
776
      $tz = new DateTimeZone($tz->getName());
777
    }
778

    
779
    if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
780
      // this has no time or timezone granularity, so timezone doesn't mean much
781
      // We set the timezone using the method, which will change the day/hour, but then we switch back
782
      $arr = $this->toArray();
783
      parent::setTimezone($tz);
784
      $this->setDate($arr['year'], $arr['month'], $arr['day']);
785
      $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
786
      return;
787
    }
788
    parent::setTimezone($tz);
789
  }
790

    
791
  /**
792
   * Safely adds a granularity entry to the array.
793
   */
794
  public function addGranularity($g) {
795
    $this->granularity[] = $g;
796
    $this->granularity = array_unique($this->granularity);
797
  }
798

    
799
  /**
800
   * Removes a granularity entry from the array.
801
   */
802
  public function removeGranularity($g) {
803
    if ($key = array_search($g, $this->granularity)) {
804
      unset($this->granularity[$key]);
805
    }
806
  }
807

    
808
  /**
809
   * Checks granularity array for a given entry.
810
   */
811
  public function hasGranularity($g) {
812
    return in_array($g, $this->granularity);
813
  }
814

    
815
  /**
816
   * Returns whether this object has time set. Used primarily for timezone
817
   * conversion and fomratting.
818
   *
819
   * @todo currently very simplistic, but effective, see usage
820
   */
821
  public function hasTime() {
822
    return $this->hasGranularity('hour');
823
  }
824

    
825
  /**
826
   * Protected function to find the granularity given by the arguments to the
827
   * constructor.
828
   */
829
  protected function setGranularityFromTime($time, $tz) {
830
    $this->granularity = array();
831
    $temp = date_parse($time);
832
    // This PHP method currently doesn't have resolution down to seconds, so if
833
    // there is some time, all will be set.
834
    foreach (self::$allgranularity AS $g) {
835
      if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'zone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
836
        $this->granularity[] = $g;
837
      }
838
    }
839
    if ($tz) {
840
      $this->addGranularity('zone');
841
    }
842
  }
843

    
844
  /**
845
   * Helper to return all standard date parts in an array.
846
   */
847
  protected function toArray() {
848
    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'));
849
  }
850
}
851

    
852
/**
853
 * Converts to UNIX time.
854
 *
855
 * @param $date
856
 *   A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
857
 * @param $default_value
858
 *   A default UNIX timestamp to return if $date could not be parsed.
859
 *
860
 * @return
861
 *   $date as UNIX time if conversion was successful, $dfeault_value otherwise.
862
 */
863
function feeds_to_unixtime($date, $default_value) {
864
  if (is_numeric($date)) {
865
    return $date;
866
  }
867
  elseif (is_string($date) && !empty($date)) {
868
    $date = new FeedsDateTimeElement($date);
869
    return $date->getValue();
870
  }
871
  elseif ($date instanceof FeedsDateTimeElement) {
872
    return $date->getValue();
873
  }
874
  return $default_value;
875
}