Projet

Général

Profil

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

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

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 an element identified by $element_key of the given item.
124
   * The element key corresponds to the values in the array returned by
125
   * FeedsParser::getMappingSources().
126
   *
127
   * This method is invoked from FeedsProcessor::map() when a concrete item is
128
   * processed.
129
   *
130
   * @ingroup mappingapi
131
   *
132
   * @param $batch
133
   *   FeedsImportBatch object containing the sources to be mapped from.
134
   * @param $element_key
135
   *   The key identifying the element that should be retrieved from $source
136
   *
137
   * @return
138
   *   The source element from $item identified by $element_key.
139
   *
140
   * @see FeedsProcessor::map()
141
   * @see FeedsCSVParser::getSourceElement()
142
   */
143
  public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
144

    
145
    switch ($element_key) {
146

    
147
      case 'parent:uid':
148
        if ($source->feed_nid && $node = node_load($source->feed_nid)) {
149
          return $node->uid;
150
        }
151
        break;
152
      case 'parent:nid':
153
        return $source->feed_nid;
154
    }
155

    
156
    $item = $result->currentItem();
157
    return isset($item[$element_key]) ? $item[$element_key] : '';
158
  }
159
}
160

    
161
/**
162
 * Defines an element of a parsed result. Such an element can be a simple type,
163
 * a complex type (derived from FeedsElement) or an array of either.
164
 *
165
 * @see FeedsEnclosure
166
 */
167
class FeedsElement {
168
  // The standard value of this element. This value can contain be a simple type,
169
  // a FeedsElement or an array of either.
170
  protected $value;
171

    
172
  /**
173
   * Constructor.
174
   */
175
  public function __construct($value) {
176
    $this->value = $value;
177
  }
178

    
179
  /**
180
   * @todo Make value public and deprecate use of getValue().
181
   *
182
   * @return
183
   *   Value of this FeedsElement represented as a scalar.
184
   */
185
  public function getValue() {
186
    return $this->value;
187
  }
188

    
189
  /**
190
   * Magic method __toString() for printing and string conversion of this
191
   * object.
192
   *
193
   * @return
194
   *   A string representation of this element.
195
   */
196
  public function __toString() {
197
    if (is_array($this->value)) {
198
      return 'Array';
199
    }
200
    if (is_object($this->value)) {
201
      return 'Object';
202
    }
203
    return (string) $this->getValue();
204
  }
205
}
206

    
207
/**
208
 * Encapsulates a taxonomy style term object.
209
 *
210
 * Objects of this class can be turned into a taxonomy term style arrays by
211
 * casting them.
212
 *
213
 * @code
214
 *   $term_object = new FeedsTermElement($term_array);
215
 *   $term_array = (array)$term_object;
216
 * @endcode
217
 */
218
class FeedsTermElement extends FeedsElement {
219
  public $tid, $vid, $name;
220

    
221
  /**
222
   * @param $term
223
   *   An array or a stdClass object that is a Drupal taxonomy term.
224
   */
225
  public function __construct($term) {
226
    if (is_array($term)) {
227
      parent::__construct($term['name']);
228
      foreach ($this as $key => $value) {
229
        $this->$key = isset($term[$key]) ? $term[$key] : NULL;
230
      }
231
    }
232
    elseif (is_object($term)) {
233
      parent::__construct($term->name);
234
      foreach ($this as $key => $value) {
235
        $this->$key = isset($term->$key) ? $term->$key : NULL;
236
      }
237
    }
238
  }
239

    
240
  /**
241
   * Use $name as $value.
242
   */
243
  public function getValue() {
244
    return $this->name;
245
  }
246
}
247

    
248
/**
249
 * A geo term element.
250
 */
251
class FeedsGeoTermElement extends FeedsTermElement {
252
  public $lat, $lon, $bound_top, $bound_right, $bound_bottom, $bound_left, $geometry;
253
  /**
254
   * @param $term
255
   *   An array or a stdClass object that is a Drupal taxonomy term. Can include
256
   *   geo extensions.
257
   */
258
  public function __construct($term) {
259
    parent::__construct($term);
260
  }
261
}
262

    
263
/**
264
 * Enclosure element, can be part of the result array.
265
 */
266
class FeedsEnclosure extends FeedsElement {
267

    
268
  /**
269
   * The mime type of the enclosure.
270
   *
271
   * @param string
272
   */
273
   protected $mime_type;
274

    
275
   /**
276
   * The default list of allowed extensions.
277
   *
278
   * @param string
279
   */
280
  protected $allowedExtensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
281

    
282
  /**
283
   * The sanitized local file name.
284
   *
285
   * @var string
286
   */
287
  protected $safeFilename;
288

    
289
  /**
290
   * Constructor, requires MIME type.
291
   *
292
   * @param $value
293
   *   A path to a local file or a URL to a remote document.
294
   * @param $mimetype
295
   *   The mime type of the resource.
296
   */
297
  public function __construct($value, $mime_type) {
298
    parent::__construct($value);
299
    $this->mime_type = $mime_type;
300
  }
301

    
302
  /**
303
   * @return
304
   *   MIME type of return value of getValue().
305
   */
306
  public function getMIMEType() {
307
    return $this->mime_type;
308
  }
309

    
310
  /**
311
   * Sets the list of allowed extensions.
312
   *
313
   * @param string $extensions
314
   *   The list of allowed extensions separated by a space.
315
   */
316
  public function setAllowedExtensions($extensions) {
317
    // Normalize whitespace so that empty extensions are not allowed.
318
    $this->allowedExtensions = drupal_strtolower(trim(preg_replace('/\s+/', ' ', $extensions)));
319
  }
320

    
321
  /**
322
   * Use this method instead of FeedsElement::getValue() when fetching the file
323
   * from the URL.
324
   *
325
   * @return
326
   *   Value with encoded space characters to safely fetch the file from the URL.
327
   *
328
   * @see FeedsElement::getValue()
329
   */
330
  public function getUrlEncodedValue() {
331
    return str_replace(' ', '%20', $this->getValue());
332
  }
333

    
334
  /**
335
   * Returns the full path to the file URI with a safe file name.
336
   *
337
   * @return string
338
   *   The safe file URI.
339
   *
340
   * @throws RuntimeException
341
   *   Thrown if the file extension is invalid.
342
   */
343
  public function getSanitizedUri() {
344
    return drupal_dirname($this->getValue()) . '/' . $this->getSafeFilename();
345
  }
346

    
347
  /**
348
   * Returns the file name transformed for better local saving.
349
   *
350
   * @return string
351
   *   Value with space characters changed to underscores.
352
   *
353
   * @throws RuntimeException
354
   *   Thrown if the file extension is invalid.
355
   */
356
  public function getLocalValue() {
357
    return str_replace(' ', '_', $this->getSafeFilename());
358
  }
359

    
360
  /**
361
   * Returns the safe file name.
362
   *
363
   * @return string
364
   *   A filename that is safe to save to the filesystem.
365
   *
366
   * @throws RuntimeException
367
   *   Thrown if the file extension is invalid.
368
   */
369
  protected function getSafeFilename() {
370
    if (isset($this->safeFilename)) {
371
      return $this->safeFilename;
372
    }
373

    
374
    // Strip any query string or fragment from file name.
375
    list($filename) = explode('?', $this->getValue());
376
    list($filename) = explode('#', $filename);
377

    
378
    $filename = rawurldecode(drupal_basename($filename));
379

    
380
    // Remove leading and trailing whitespace and periods.
381
    $filename = trim($filename, " \t\n\r\0\x0B.");
382

    
383
    if (strpos($filename, '.') === FALSE) {
384
      $extension = FALSE;
385
    }
386
    else {
387
      $extension = drupal_strtolower(substr($filename, strrpos($filename, '.') + 1));
388
    }
389

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

    
394
    $this->safeFilename = file_munge_filename($filename, $this->allowedExtensions, FALSE);
395

    
396
    return $this->safeFilename;
397
  }
398

    
399
  /**
400
   * Downloads the content from the file URL.
401
   *
402
   * @return string
403
   *   The content of the referenced resource.
404
   */
405
  public function getContent() {
406
    feeds_include_library('http_request.inc', 'http_request');
407
    $result = http_request_get($this->getUrlEncodedValue());
408
    if ($result->code != 200) {
409
      throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->getUrlEncodedValue(), '!code' => $result->code)));
410
    }
411
    return $result->data;
412
  }
413

    
414
  /**
415
   * Get a Drupal file object of the enclosed resource, download if necessary.
416
   *
417
   * @param $destination
418
   *   The path or uri specifying the target directory in which the file is
419
   *   expected. Don't use trailing slashes unless it's a streamwrapper scheme.
420
   *
421
   * @return
422
   *   A Drupal temporary file object of the enclosed resource.
423
   *
424
   * @throws Exception
425
   *   If file object could not be created.
426
   */
427
  public function getFile($destination) {
428
    $file = NULL;
429
    if ($this->getValue()) {
430
      // Prepare destination directory.
431
      file_prepare_directory($destination, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
432
      // Copy or save file depending on whether it is remote or local.
433
      if (drupal_realpath($this->getSanitizedUri())) {
434
        $file           = new stdClass();
435
        $file->uid      = 0;
436
        $file->uri      = $this->getSanitizedUri();
437
        $file->filemime = $this->getMIMEType();
438
        $file->filename = $this->getSafeFilename();
439

    
440
        if (drupal_dirname($file->uri) !== $destination) {
441
          $file = file_copy($file, $destination);
442
        }
443
        else {
444
          // If file is not to be copied, check whether file already exists,
445
          // as file_save() won't do that for us (compare file_copy() and
446
          // file_save())
447
          $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
448
          if (count($existing_files)) {
449
            $existing = reset($existing_files);
450
            $file->fid = $existing->fid;
451
            $file->filename = $existing->filename;
452
          }
453
          file_save($file);
454
        }
455
      }
456
      else {
457
        if (file_uri_target($destination)) {
458
          $destination = trim($destination, '/') . '/';
459
        }
460
        try {
461
          $filename = $this->getLocalValue();
462

    
463
          if (module_exists('transliteration')) {
464
            require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
465
            $filename = transliteration_clean_filename($filename);
466
          }
467

    
468
          $file = file_save_data($this->getContent(), $destination . $filename);
469
        }
470
        catch (Exception $e) {
471
          watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
472
        }
473
      }
474

    
475
      // We couldn't make sense of this enclosure, throw an exception.
476
      if (!$file) {
477
        throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
478
      }
479

    
480
      return $file;
481
    }
482
  }
483
}
484

    
485
/**
486
 * Defines a date element of a parsed result (including ranges, repeat).
487
 */
488
class FeedsDateTimeElement extends FeedsElement {
489

    
490
  // Start date and end date.
491
  public $start;
492
  public $end;
493

    
494
  /**
495
   * Constructor.
496
   *
497
   * @param $start
498
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
499
   * @param $end
500
   *   A FeedsDateTime object or a date as accepted by FeedsDateTime.
501
   * @param $tz
502
   *   A PHP DateTimeZone object.
503
   */
504
  public function __construct($start = NULL, $end = NULL, $tz = NULL) {
505
    $this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
506
    $this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
507
  }
508

    
509
  /**
510
   * Override FeedsElement::getValue().
511
   *
512
   * @return
513
   *   The UNIX timestamp of this object's start date. Return value is
514
   *   technically a string but will only contain numeric values.
515
   */
516
  public function getValue() {
517
    if ($this->start) {
518
      return $this->start->format('U');
519
    }
520
    return '0';
521
  }
522

    
523
  /**
524
   * Merge this field with another. Most stuff goes down when merging the two
525
   * sub-dates.
526
   *
527
   * @see FeedsDateTime
528
   */
529
  public function merge(FeedsDateTimeElement $other) {
530
    $this2 = clone $this;
531
    if ($this->start && $other->start) {
532
      $this2->start = $this->start->merge($other->start);
533
    }
534
    elseif ($other->start) {
535
      $this2->start = clone $other->start;
536
    }
537
    elseif ($this->start) {
538
      $this2->start = clone $this->start;
539
    }
540

    
541
    if ($this->end && $other->end) {
542
      $this2->end = $this->end->merge($other->end);
543
    }
544
    elseif ($other->end) {
545
      $this2->end = clone $other->end;
546
    }
547
    elseif ($this->end) {
548
      $this2->end = clone $this->end;
549
    }
550
    return $this2;
551
  }
552

    
553
  /**
554
   * Helper method for buildDateField(). Build a FeedsDateTimeElement object
555
   * from a standard formatted node.
556
   */
557
  protected static function readDateField($entity, $field_name, $delta = 0) {
558
    $ret = new FeedsDateTimeElement();
559
    if (isset($entity->{$field_name}['und'][$delta]['date']) && $entity->{$field_name}['und'][$delta]['date'] instanceof FeedsDateTime) {
560
      $ret->start = $entity->{$field_name}['und'][$delta]['date'];
561
    }
562
    if (isset($entity->{$field_name}['und'][$delta]['date2']) && $entity->{$field_name}['und'][$delta]['date2'] instanceof FeedsDateTime) {
563
      $ret->end = $entity->{$field_name}['und'][$delta]['date2'];
564
    }
565
    return $ret;
566
  }
567

    
568
  /**
569
   * Build a entity's date field from our object.
570
   *
571
   * @param object $entity
572
   *   The entity to build the date field on.
573
   * @param str $field_name
574
   *   The name of the field to build.
575
   * @param int $delta
576
   *   The delta in the field.
577
   */
578
  public function buildDateField($entity, $field_name, $delta = 0) {
579
    $info = field_info_field($field_name);
580

    
581
    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta);
582
    // Merge with any preexisting objects on the field; we take precedence.
583
    $oldfield = $this->merge($oldfield);
584
    $use_start = $oldfield->start;
585
    $use_end = $oldfield->end;
586

    
587
    // Set timezone if not already in the FeedsDateTime object
588
    $to_tz = date_get_timezone($info['settings']['tz_handling'], date_default_timezone());
589
    $temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
590

    
591
    $db_tz = '';
592
    if ($use_start) {
593
      $use_start = $use_start->merge($temp);
594
      if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
595
        $use_start->setTimezone(new DateTimeZone("UTC"));
596
      }
597
      $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_start->getTimezone()->getName());
598
    }
599
    if ($use_end) {
600
      $use_end = $use_end->merge($temp);
601
      if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
602
        $use_end->setTimezone(new DateTimeZone("UTC"));
603
      }
604
      if (!$db_tz) {
605
        $db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_end->getTimezone()->getName());
606
      }
607
    }
608
    if (!$db_tz) {
609
      return;
610
    }
611

    
612
    $db_tz = new DateTimeZone($db_tz);
613
    if (!isset($entity->{$field_name})) {
614
      $entity->{$field_name} = array('und' => array());
615
    }
616
    if ($use_start) {
617
      $entity->{$field_name}['und'][$delta]['timezone'] = $use_start->getTimezone()->getName();
618
      $entity->{$field_name}['und'][$delta]['offset'] = $use_start->getOffset();
619
      $use_start->setTimezone($db_tz);
620
      $entity->{$field_name}['und'][$delta]['date'] = $use_start;
621
      /**
622
       * @todo the date_type_format line could be simplified based upon a patch
623
       *   DO issue #259308 could affect this, follow up on at some point.
624
       *   Without this, all granularity info is lost.
625
       *   $use_start->format(date_type_format($field['type'], $use_start->granularity));
626
       */
627
      $entity->{$field_name}['und'][$delta]['value'] = $use_start->format(date_type_format($info['type']));
628
    }
629
    if ($use_end) {
630
      // Don't ever use end to set timezone (for now)
631
      $entity->{$field_name}['und'][$delta]['offset2'] = $use_end->getOffset();
632
      $use_end->setTimezone($db_tz);
633
      $entity->{$field_name}['und'][$delta]['date2'] = $use_end;
634
      $entity->{$field_name}['und'][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
635
    }
636
  }
637
}
638

    
639
/**
640
 * Extend PHP DateTime class with granularity handling, merge functionality and
641
 * slightly more flexible initialization parameters.
642
 *
643
 * This class is a Drupal independent extension of the >= PHP 5.2 DateTime
644
 * class.
645
 *
646
 * @see FeedsDateTimeElement
647
 */
648
class FeedsDateTime extends DateTime {
649
  public $granularity = array();
650
  protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
651
  private $_serialized_time;
652
  private $_serialized_timezone;
653

    
654
  /**
655
   * Helper function to prepare the object during serialization.
656
   *
657
   * We are extending a core class and core classes cannot be serialized.
658
   *
659
   * Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
660
   */
661
  public function __sleep() {
662
    $this->_serialized_time = $this->format('c');
663
    $this->_serialized_timezone = $this->getTimezone()->getName();
664
    return array('_serialized_time', '_serialized_timezone');
665
  }
666

    
667
  /**
668
   * Upon unserializing, we must re-build ourselves using local variables.
669
   */
670
  public function __wakeup() {
671
    $this->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
672
  }
673

    
674
  /**
675
   * Overridden constructor.
676
   *
677
   * @param $time
678
   *   time string, flexible format including timestamp. Invalid formats will
679
   *   fall back to 'now'.
680
   * @param $tz
681
   *   PHP DateTimeZone object, NULL allowed
682
   */
683
  public function __construct($time = '', $tz = NULL) {
684
    if (is_numeric($time)) {
685
      // Assume UNIX timestamp if it doesn't look like a simple year.
686
      if (strlen($time) > 4) {
687
        $time = "@" . $time;
688
      }
689
      // If it's a year, add a default month too, because PHP's date functions
690
      // won't parse standalone years after 2000 correctly (see explanation at
691
      // http://aaronsaray.com/blog/2007/07/11/helpful-strtotime-reminders/#comment-47).
692
      else {
693
        $time = 'January ' . $time;
694
      }
695
    }
696

    
697
    // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
698
    $time = str_replace("GMT-", "-", $time);
699
    $time = str_replace("GMT+", "+", $time);
700

    
701
    // Some PHP 5.2 version's DateTime class chokes on invalid dates.
702
    if (!date_create($time)) {
703
      $time = 'now';
704
    }
705

    
706
    // Create and set time zone separately, PHP 5.2.6 does not respect time zone
707
    // argument in __construct().
708
    parent::__construct($time);
709
    $tz = $tz ? $tz : new DateTimeZone("UTC");
710
    $this->setTimeZone($tz);
711

    
712
    // Verify that timezone has not been specified as an offset.
713
    if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
714
      $this->setTimezone(new DateTimeZone("UTC"));
715
    }
716

    
717
    // Finally set granularity.
718
    $this->setGranularityFromTime($time, $tz);
719
  }
720

    
721
  /**
722
   * This function will keep this object's values by default.
723
   */
724
  public function merge(FeedsDateTime $other) {
725
    $other_tz = $other->getTimezone();
726
    $this_tz = $this->getTimezone();
727
    // Figure out which timezone to use for combination.
728
    $use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
729

    
730
    $this2 = clone $this;
731
    $this2->setTimezone($use_tz);
732
    $other->setTimezone($use_tz);
733
    $val = $this2->toArray();
734
    $otherval = $other->toArray();
735
    foreach (self::$allgranularity as $g) {
736
      if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
737
        // The other class has a property we don't; steal it.
738
        $this2->addGranularity($g);
739
        $val[$g] = $otherval[$g];
740
      }
741
    }
742
    $other->setTimezone($other_tz);
743

    
744
    $this2->setDate($val['year'], $val['month'], $val['day']);
745
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
746
    return $this2;
747
  }
748

    
749
  /**
750
   * Overrides default DateTime function. Only changes output values if
751
   * actually had time granularity. This should be used as a "converter" for
752
   * output, to switch tzs.
753
   *
754
   * In order to set a timezone for a datetime that doesn't have such
755
   * granularity, merge() it with one that does.
756
   */
757
  public function setTimezone($tz, $force = FALSE) {
758
    // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
759
    // http://bugs.php.net/bug.php?id=45038
760
    if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
761
      $tz = new DateTimeZone($tz->getName());
762
    }
763

    
764
    if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
765
      // this has no time or timezone granularity, so timezone doesn't mean much
766
      // We set the timezone using the method, which will change the day/hour, but then we switch back
767
      $arr = $this->toArray();
768
      parent::setTimezone($tz);
769
      $this->setDate($arr['year'], $arr['month'], $arr['day']);
770
      $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
771
      return;
772
    }
773
    parent::setTimezone($tz);
774
  }
775

    
776
  /**
777
   * Safely adds a granularity entry to the array.
778
   */
779
  public function addGranularity($g) {
780
    $this->granularity[] = $g;
781
    $this->granularity = array_unique($this->granularity);
782
  }
783

    
784
  /**
785
   * Removes a granularity entry from the array.
786
   */
787
  public function removeGranularity($g) {
788
    if ($key = array_search($g, $this->granularity)) {
789
      unset($this->granularity[$key]);
790
    }
791
  }
792

    
793
  /**
794
   * Checks granularity array for a given entry.
795
   */
796
  public function hasGranularity($g) {
797
    return in_array($g, $this->granularity);
798
  }
799

    
800
  /**
801
   * Returns whether this object has time set. Used primarily for timezone
802
   * conversion and fomratting.
803
   *
804
   * @todo currently very simplistic, but effective, see usage
805
   */
806
  public function hasTime() {
807
    return $this->hasGranularity('hour');
808
  }
809

    
810
  /**
811
   * Protected function to find the granularity given by the arguments to the
812
   * constructor.
813
   */
814
  protected function setGranularityFromTime($time, $tz) {
815
    $this->granularity = array();
816
    $temp = date_parse($time);
817
    // This PHP method currently doesn't have resolution down to seconds, so if
818
    // there is some time, all will be set.
819
    foreach (self::$allgranularity AS $g) {
820
      if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'zone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
821
        $this->granularity[] = $g;
822
      }
823
    }
824
    if ($tz) {
825
      $this->addGranularity('zone');
826
    }
827
  }
828

    
829
  /**
830
   * Helper to return all standard date parts in an array.
831
   */
832
  protected function toArray() {
833
    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'));
834
  }
835
}
836

    
837
/**
838
 * Converts to UNIX time.
839
 *
840
 * @param $date
841
 *   A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
842
 * @param $default_value
843
 *   A default UNIX timestamp to return if $date could not be parsed.
844
 *
845
 * @return
846
 *   $date as UNIX time if conversion was successful, $dfeault_value otherwise.
847
 */
848
function feeds_to_unixtime($date, $default_value) {
849
  if (is_numeric($date)) {
850
    return $date;
851
  }
852
  elseif (is_string($date) && !empty($date)) {
853
    $date = new FeedsDateTimeElement($date);
854
    return $date->getValue();
855
  }
856
  elseif ($date instanceof FeedsDateTimeElement) {
857
    return $date->getValue();
858
  }
859
  return $default_value;
860
}