Project

General

Profile

Paste
Download (24.6 KB) Statistics
| Branch: | Revision:

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

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 = 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 filen ame.
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
   * @see FeedsElement::getValue()
357
   */
358
  public function getLocalValue() {
359
    return str_replace(' ', '_', $this->getSafeFilename());
360
  }
361

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

    
378
    $filename = rawurldecode(drupal_basename($this->getValue()));
379

    
380
    if (module_exists('transliteration')) {
381
      require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
382
      $filename = transliteration_clean_filename($filename);
383
    }
384

    
385
    // Remove leading and trailing whitespace and periods.
386
    $filename = trim($filename, " \t\n\r\0\x0B.");
387

    
388
    if (strpos($filename, '.') === FALSE) {
389
      $extension = FALSE;
390
    }
391
    else {
392
      $extension = substr($filename, strrpos($filename, '.') + 1);
393
    }
394

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

    
399
    $this->safeFilename = file_munge_filename($filename, $this->allowedExtensions, FALSE);
400

    
401
    return $this->safeFilename;
402
  }
403

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

    
419
  /**
420
   * Get a Drupal file object of the enclosed resource, download if necessary.
421
   *
422
   * @param $destination
423
   *   The path or uri specifying the target directory in which the file is
424
   *   expected. Don't use trailing slashes unless it's a streamwrapper scheme.
425
   *
426
   * @return
427
   *   A Drupal temporary file object of the enclosed resource.
428
   *
429
   * @throws Exception
430
   *   If file object could not be created.
431
   */
432
  public function getFile($destination) {
433

    
434
    if ($this->getValue()) {
435
      // Prepare destination directory.
436
      file_prepare_directory($destination, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
437
      // Copy or save file depending on whether it is remote or local.
438
      if (drupal_realpath($this->getSanitizedUri())) {
439
        $file           = new stdClass();
440
        $file->uid      = 0;
441
        $file->uri      = $this->getSanitizedUri();
442
        $file->filemime = $this->mime_type;
443
        $file->filename = $this->getSafeFilename();
444

    
445
        if (drupal_dirname($file->uri) !== $destination) {
446
          $file = file_copy($file, $destination);
447
        }
448
        else {
449
          // If file is not to be copied, check whether file already exists,
450
          // as file_save() won't do that for us (compare file_copy() and
451
          // file_save())
452
          $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
453
          if (count($existing_files)) {
454
            $existing = reset($existing_files);
455
            $file->fid = $existing->fid;
456
            $file->filename = $existing->filename;
457
          }
458
          file_save($file);
459
        }
460
      }
461
      else {
462
        if (file_uri_target($destination)) {
463
          $destination = trim($destination, '/') . '/';
464
        }
465
        try {
466
          $file = file_save_data($this->getContent(), $destination . $this->getLocalValue());
467
        }
468
        catch (Exception $e) {
469
          watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
470
        }
471
      }
472

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

    
478
      return $file;
479
    }
480
  }
481

    
482
}
483

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
673
  /**
674
   * Overridden constructor.
675
   *
676
   * @param $time
677
   *   time string, flexible format including timestamp. Invalid formats will
678
   *   fall back to 'now'.
679
   * @param $tz
680
   *   PHP DateTimeZone object, NULL allowed
681
   */
682
  public function __construct($time = '', $tz = NULL) {
683
    // Assume UNIX timestamp if numeric.
684
    if (is_numeric($time)) {
685
      // Make sure it's not a simple year
686
      if ((is_string($time) && strlen($time) > 4) || is_int($time)) {
687
        $time = "@" . $time;
688
      }
689
    }
690

    
691
    // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
692
    $time = str_replace("GMT-", "-", $time);
693
    $time = str_replace("GMT+", "+", $time);
694

    
695
    // Some PHP 5.2 version's DateTime class chokes on invalid dates.
696
    if (!strtotime($time)) {
697
      $time = 'now';
698
    }
699

    
700
    // Create and set time zone separately, PHP 5.2.6 does not respect time zone
701
    // argument in __construct().
702
    parent::__construct($time);
703
    $tz = $tz ? $tz : new DateTimeZone("UTC");
704
    $this->setTimeZone($tz);
705

    
706
    // Verify that timezone has not been specified as an offset.
707
    if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
708
      $this->setTimezone(new DateTimeZone("UTC"));
709
    }
710

    
711
    // Finally set granularity.
712
    $this->setGranularityFromTime($time, $tz);
713
  }
714

    
715
  /**
716
   * This function will keep this object's values by default.
717
   */
718
  public function merge(FeedsDateTime $other) {
719
    $other_tz = $other->getTimezone();
720
    $this_tz = $this->getTimezone();
721
    // Figure out which timezone to use for combination.
722
    $use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
723

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

    
738
    $this2->setDate($val['year'], $val['month'], $val['day']);
739
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
740
    return $this2;
741
  }
742

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

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

    
770
  /**
771
   * Safely adds a granularity entry to the array.
772
   */
773
  public function addGranularity($g) {
774
    $this->granularity[] = $g;
775
    $this->granularity = array_unique($this->granularity);
776
  }
777

    
778
  /**
779
   * Removes a granularity entry from the array.
780
   */
781
  public function removeGranularity($g) {
782
    if ($key = array_search($g, $this->granularity)) {
783
      unset($this->granularity[$key]);
784
    }
785
  }
786

    
787
  /**
788
   * Checks granularity array for a given entry.
789
   */
790
  public function hasGranularity($g) {
791
    return in_array($g, $this->granularity);
792
  }
793

    
794
  /**
795
   * Returns whether this object has time set. Used primarily for timezone
796
   * conversion and fomratting.
797
   *
798
   * @todo currently very simplistic, but effective, see usage
799
   */
800
  public function hasTime() {
801
    return $this->hasGranularity('hour');
802
  }
803

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

    
823
  /**
824
   * Helper to return all standard date parts in an array.
825
   */
826
  protected function toArray() {
827
    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'));
828
  }
829
}
830

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