Completed
Branch BUG-10911-php-7.2 (c0fc70)
by
unknown
22:59 queued 11:49
created

EEH_DTT_Helper::validate_timezone()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
nc 3
nop 2
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
1
<?php
2
defined('EVENT_ESPRESSO_VERSION') || exit('NO direct script access allowed');
3
4
5
6
/**
7
 * EEH_DTT_Helper
8
 * This is a helper utility class containing a variety for date time formatting helpers for Event Espresso.
9
 *
10
 * @package         Event Espresso
11
 * @subpackage      /helpers/EEH_DTT_Helper.helper.php
12
 * @author          Darren Ethier
13
 */
14
class EEH_DTT_Helper
15
{
16
17
18
    /**
19
     * return the timezone set for the WP install
20
     *
21
     * @return string valid timezone string for PHP DateTimeZone() class
22
     * @throws EE_Error
23
     */
24
    public static function get_timezone()
25
    {
26
        return EEH_DTT_Helper::get_valid_timezone_string();
27
    }
28
29
30
    /**
31
     * get_valid_timezone_string
32
     *    ensures that a valid timezone string is returned
33
     *
34
     * @param string $timezone_string
35
     * @return string
36
     * @throws EE_Error
37
     */
38
    public static function get_valid_timezone_string($timezone_string = '')
39
    {
40
        // if passed a value, then use that, else get WP option
41
        $timezone_string = ! empty($timezone_string) ? $timezone_string : (string) get_option('timezone_string');
42
        // value from above exists, use that, else get timezone string from gmt_offset
43
        $timezone_string = ! empty($timezone_string)
44
            ? $timezone_string
45
            : EEH_DTT_Helper::get_timezone_string_from_gmt_offset();
46
        EEH_DTT_Helper::validate_timezone($timezone_string);
47
        return $timezone_string;
48
    }
49
50
51
    /**
52
     * This only purpose for this static method is to validate that the incoming timezone is a valid php timezone.
53
     *
54
     * @static
55
     * @param  string $timezone_string Timezone string to check
56
     * @param bool    $throw_error
57
     * @return bool
58
     * @throws EE_Error
59
     */
60
    public static function validate_timezone($timezone_string, $throw_error = true)
61
    {
62
        // easiest way to test a timezone string is just see if it throws an error when you try to create a DateTimeZone object with it
63
        try {
64
            new DateTimeZone($timezone_string);
65
        } catch (Exception $e) {
66
            // sometimes we take exception to exceptions
67
            if (! $throw_error) {
68
                return false;
69
            }
70
            throw new EE_Error(
71
                sprintf(
72
                    esc_html__(
73
                        'The timezone given (%1$s), is invalid, please check with %2$sthis list%3$s for what valid timezones can be used',
74
                        'event_espresso'
75
                    ),
76
                    $timezone_string,
77
                    '<a href="http://www.php.net/manual/en/timezones.php">',
78
                    '</a>'
79
                )
80
            );
81
        }
82
        return true;
83
    }
84
85
86
    /**
87
     * _create_timezone_object_from_timezone_name
88
     *
89
     * @param float|string $gmt_offset
90
     * @return string
91
     * @throws EE_Error
92
     */
93
    public static function get_timezone_string_from_gmt_offset($gmt_offset = '')
94
    {
95
        $timezone_string = 'UTC';
96
        //if there is no incoming gmt_offset, then because WP hooks in on timezone_string, we need to see if that is
97
        //set because it will override `gmt_offset` via `pre_get_option` filter.  If that's set, then let's just use
98
        //that!  Otherwise we'll leave timezone_string at the default of 'UTC' before doing other logic.
99
        if ($gmt_offset === '') {
100
            //autoloaded so no need to set to a variable.  There will not be multiple hits to the db.
101
            if (get_option('timezone_string')) {
102
                return (string) get_option('timezone_string');
103
            }
104
        }
105
        $gmt_offset = $gmt_offset !== '' ? $gmt_offset : (string) get_option('gmt_offset');
106
        $gmt_offset = (float) $gmt_offset;
107
        //if $gmt_offset is 0, then just return UTC
108
        if ($gmt_offset === (float) 0) {
109
            return $timezone_string;
110
        }
111
        if ($gmt_offset !== '') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $gmt_offset (double) and '' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
112
            // convert GMT offset to seconds
113
            $gmt_offset *= HOUR_IN_SECONDS;
114
            // although we don't know the TZ abbreviation, we know the UTC offset
115
            $timezone_string = timezone_name_from_abbr(null, $gmt_offset);
116
            //only use this timezone_string IF it's current offset matches the given offset
117
            if(! empty($timezone_string)) {
118
                $offset  = null;
119
                try {
120
                    $offset = self::get_timezone_offset(new DateTimeZone($timezone_string));
121
                    if ($offset !== $gmt_offset) {
122
                        $timezone_string = false;
123
                    }
124
                } catch (Exception $e) {
125
                    $timezone_string = false;
126
                }
127
            }
128
        }
129
        // better have a valid timezone string by now, but if not, sigh... loop thru  the timezone_abbreviations_list()...
130
        $timezone_string = $timezone_string !== false
131
            ? $timezone_string
132
            : EEH_DTT_Helper::get_timezone_string_from_abbreviations_list($gmt_offset);
133
        return $timezone_string;
134
    }
135
136
137
    /**
138
     * Gets the site's GMT offset based on either the timezone string
139
     * (in which case teh gmt offset will vary depending on the location's
140
     * observance of daylight savings time) or the gmt_offset wp option
141
     *
142
     * @return int seconds offset
143
     */
144
    public static function get_site_timezone_gmt_offset()
145
    {
146
        $timezone_string = (string) get_option('timezone_string');
147
        if ($timezone_string) {
148
            try {
149
                $timezone = new DateTimeZone($timezone_string);
150
                return $timezone->getOffset(new DateTime()); //in WordPress DateTime defaults to UTC
151
            } catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
152
            }
153
        }
154
        $offset = get_option('gmt_offset');
155
        return (int) ($offset * HOUR_IN_SECONDS);
156
    }
157
158
159
    /**
160
     * Depending on PHP version,
161
     * there might not be valid current timezone strings to match these gmt_offsets in its timezone tables.
162
     * To get around that, for these fringe timezones we bump them to a known valid offset.
163
     * This method should ONLY be called after first verifying an timezone_string cannot be retrieved for the offset.
164
     *
165
     * @param int $gmt_offset
166
     * @return int
167
     */
168
    public static function adjust_invalid_gmt_offsets($gmt_offset = 0)
169
    {
170
        //make sure $gmt_offset is int
171
        $gmt_offset = (int) $gmt_offset;
172
        switch ($gmt_offset) {
173
            //-12
174
            case -43200:
175
                $gmt_offset = -39600;
176
                break;
177
            //-11.5
178
            case -41400:
179
                $gmt_offset = -39600;
180
                break;
181
            //-10.5
182
            case -37800:
183
                $gmt_offset = -39600;
184
                break;
185
            //-8.5
186
            case -30600:
187
                $gmt_offset = -28800;
188
                break;
189
            //-7.5
190
            case -27000:
191
                $gmt_offset = -25200;
192
                break;
193
            //-6.5
194
            case -23400:
195
                $gmt_offset = -21600;
196
                break;
197
            //-5.5
198
            case -19800:
199
                $gmt_offset = -18000;
200
                break;
201
            //-4.5
202
            case -16200:
203
                $gmt_offset = -14400;
204
                break;
205
            //-3.5
206
            case -12600:
207
                $gmt_offset = -10800;
208
                break;
209
            //-2.5
210
            case -9000:
211
                $gmt_offset = -7200;
212
                break;
213
            //-1.5
214
            case -5400:
215
                $gmt_offset = -3600;
216
                break;
217
            //-0.5
218
            case -1800:
219
                $gmt_offset = 0;
220
                break;
221
            //.5
222
            case 1800:
223
                $gmt_offset = 3600;
224
                break;
225
            //1.5
226
            case 5400:
227
                $gmt_offset = 7200;
228
                break;
229
            //2.5
230
            case 9000:
231
                $gmt_offset = 10800;
232
                break;
233
            //3.5
234
            case 12600:
235
                $gmt_offset = 14400;
236
                break;
237
            //7.5
238
            case 27000:
239
                $gmt_offset = 28800;
240
                break;
241
            //8.5
242
            case 30600:
243
                $gmt_offset = 31500;
244
                break;
245
            //10.5
246
            case 37800:
247
                $gmt_offset = 39600;
248
                break;
249
            //11.5
250
            case 41400:
251
                $gmt_offset = 43200;
252
                break;
253
            //12.75
254
            case 45900:
255
                $gmt_offset = 46800;
256
                break;
257
            //13.75
258
            case 49500:
259
                $gmt_offset = 50400;
260
                break;
261
        }
262
        return $gmt_offset;
263
    }
264
265
266
    /**
267
     * get_timezone_string_from_abbreviations_list
268
     *
269
     * @param int  $gmt_offset
270
     * @param bool $coerce If true, we attempt to coerce with our adjustment table @see self::adjust_invalid_gmt_offset.
271
     * @return string
272
     * @throws EE_Error
273
     */
274
    public static function get_timezone_string_from_abbreviations_list($gmt_offset = 0, $coerce = true)
275
    {
276
        $gmt_offset =  (int) $gmt_offset;
277
        /** @var array[] $abbreviations */
278
        $abbreviations = DateTimeZone::listAbbreviations();
279
        foreach ($abbreviations as $abbreviation) {
280
            foreach ($abbreviation as $timezone) {
281
                if ((int) $timezone['offset'] === $gmt_offset && (bool) $timezone['dst'] === false) {
282
                    try {
283
                        $offset = self::get_timezone_offset(new DateTimeZone($timezone['timezone_id']));
284
                        if ($offset !== $gmt_offset) {
285
                            continue;
286
                        }
287
                        return $timezone['timezone_id'];
288
                    } catch (Exception $e) {
289
                        continue;
290
                    }
291
                }
292
            }
293
        }
294
        //if $coerce is true, let's see if we can get a timezone string after the offset is adjusted
295
        if ($coerce === true) {
296
            $timezone_string = self::get_timezone_string_from_abbreviations_list(
297
                self::adjust_invalid_gmt_offsets($gmt_offset),
298
                false
299
            );
300
            if ($timezone_string) {
301
                return $timezone_string;
302
            }
303
        }
304
        throw new EE_Error(
305
            sprintf(
306
                esc_html__(
307
                    'The provided GMT offset (%1$s), is invalid, please check with %2$sthis list%3$s for what valid timezones can be used',
308
                    'event_espresso'
309
                ),
310
                $gmt_offset / HOUR_IN_SECONDS,
311
                '<a href="http://www.php.net/manual/en/timezones.php">',
312
                '</a>'
313
            )
314
        );
315
    }
316
317
318
319
    /**
320
     * Get Timezone Transitions
321
     *
322
     * @param DateTimeZone $date_time_zone
323
     * @param int|null     $time
324
     * @param bool         $first_only
325
     * @return array|mixed
326
     */
327
    public static function get_timezone_transitions(DateTimeZone $date_time_zone, $time = null, $first_only = true)
328
    {
329
        $time        = is_int($time) || $time === null ? $time : (int) strtotime($time);
330
        $time        = preg_match(EE_Datetime_Field::unix_timestamp_regex, $time) ? $time : time();
331
        $transitions = $date_time_zone->getTransitions($time);
332
        return $first_only && ! isset($transitions['ts']) ? reset($transitions) : $transitions;
333
    }
334
335
336
    /**
337
     * Get Timezone Offset for given timezone object.
338
     *
339
     * @param DateTimeZone $date_time_zone
340
     * @param null         $time
341
     * @return mixed
342
     * @throws DomainException
343
     */
344
    public static function get_timezone_offset(DateTimeZone $date_time_zone, $time = null)
345
    {
346
        $transition = self::get_timezone_transitions($date_time_zone, $time);
347
        if (! isset($transition['offset'])) {
348
            $transition['offset'] = 0;
349
            if (! isset($transition['ts'])) {
350
                throw new DomainException(
351
                    sprintf(
352
                        esc_html__('An invalid timezone transition was received %1$s', 'event_espresso'),
353
                        print_r($transition, true)
354
                    )
355
                );
356
            }
357
        }
358
        return $transition['offset'];
359
    }
360
361
362
    /**
363
     * @param string $timezone_string
364
     * @throws EE_Error
365
     */
366
    public static function timezone_select_input($timezone_string = '')
367
    {
368
        // get WP date time format
369
        $datetime_format = get_option('date_format') . ' ' . get_option('time_format');
370
        // if passed a value, then use that, else get WP option
371
        $timezone_string = ! empty($timezone_string) ? $timezone_string : (string) get_option('timezone_string');
372
        // check if the timezone is valid but don't throw any errors if it isn't
373
        $timezone_string = EEH_DTT_Helper::validate_timezone($timezone_string, false)
374
            ? $timezone_string
375
            : '';
376
        $gmt_offset      = get_option('gmt_offset');
377
        $check_zone_info = true;
378
        if (empty($timezone_string)) {
379
            // Create a UTC+- zone if no timezone string exists
380
            $timezone_string = 'UTC';
381
            $check_zone_info = false;
382
            if ($gmt_offset > 0) {
383
                $timezone_string = 'UTC+' . $gmt_offset;
384
            } elseif ($gmt_offset < 0) {
385
                $timezone_string = 'UTC' . $gmt_offset;
386
            }
387
        }
388
        ?>
389
390
        <p>
391
            <label for="timezone_string"><?php _e('timezone'); ?></label>
392
            <select id="timezone_string" name="timezone_string">
393
                <?php echo wp_timezone_choice($timezone_string); ?>
394
            </select>
395
            <br/>
396
            <span class="description"><?php _e('Choose a city in the same timezone as the event.'); ?></span>
397
        </p>
398
399
        <p>
400
        <span><?php
401
            printf(
402
                __('%1$sUTC%2$s time is %3$s'),
403
                '<abbr title="Coordinated Universal Time">',
404
                '</abbr>',
405
                '<code>' . date_i18n($datetime_format, false, true) . '</code>'
406
            );
407
            ?></span>
408
        <?php if (! empty($timezone_string) || ! empty($gmt_offset)) : ?>
409
        <br/><span><?php printf(__('Local time is %1$s'), '<code>' . date_i18n($datetime_format) . '</code>'); ?></span>
410
    <?php endif; ?>
411
412
        <?php if ($check_zone_info && $timezone_string) : ?>
413
        <br/>
414
        <span>
415
					<?php
416
                    // Set TZ so localtime works.
417
                    date_default_timezone_set($timezone_string);
418
                    $now = localtime(time(), true);
419
                    if ($now['tm_isdst']) {
420
                        _e('This timezone is currently in daylight saving time.');
421
                    } else {
422
                        _e('This timezone is currently in standard time.');
423
                    }
424
                    ?>
425
            <br/>
426
            <?php
427
            if (function_exists('timezone_transitions_get')) {
428
                $found                   = false;
429
                $date_time_zone_selected = new DateTimeZone($timezone_string);
430
                $tz_offset               = timezone_offset_get($date_time_zone_selected, date_create());
431
                $right_now               = time();
432
                $tr['isdst']             = false;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$tr was never initialized. Although not strictly required by PHP, it is generally a good practice to add $tr = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
433
                foreach (timezone_transitions_get($date_time_zone_selected) as $tr) {
434
                    if ($tr['ts'] > $right_now) {
435
                        $found = true;
436
                        break;
437
                    }
438
                }
439
                if ($found) {
440
                    $message = $tr['isdst']
441
                        ?
442
                        __(' Daylight saving time begins on: %s.')
443
                        :
444
                        __(' Standard time begins  on: %s.');
445
                    // Add the difference between the current offset and the new offset to ts to get the correct transition time from date_i18n().
446
                    printf(
447
                        $message,
448
                        '<code >' . date_i18n($datetime_format, $tr['ts'] + ($tz_offset - $tr['offset'])) . '</code >'
449
                    );
450
                } else {
451
                    _e('This timezone does not observe daylight saving time.');
452
                }
453
            }
454
            // Set back to UTC.
455
            date_default_timezone_set('UTC');
456
            ?>
457
				</span></p>
458
        <?php
459
    endif;
460
    }
461
462
463
    /**
464
     * This method will take an incoming unix timestamp and add the offset to it for the given timezone_string.
465
     * If no unix timestamp is given then time() is used.  If no timezone is given then the set timezone string for
466
     * the site is used.
467
     * This is used typically when using a Unix timestamp any core WP functions that expect their specially
468
     * computed timestamp (i.e. date_i18n() )
469
     *
470
     * @param int    $unix_timestamp                  if 0, then time() will be used.
471
     * @param string $timezone_string                 timezone_string. If empty, then the current set timezone for the
472
     *                                                site will be used.
473
     * @return int $unix_timestamp with the offset applied for the given timezone.
474
     * @throws EE_Error
475
     */
476
    public static function get_timestamp_with_offset($unix_timestamp = 0, $timezone_string = '')
477
    {
478
        $unix_timestamp  = $unix_timestamp === 0 ? time() : (int) $unix_timestamp;
479
        $timezone_string = self::get_valid_timezone_string($timezone_string);
480
        $TimeZone        = new DateTimeZone($timezone_string);
481
        $DateTime        = new DateTime('@' . $unix_timestamp, $TimeZone);
482
        $offset          = timezone_offset_get($TimeZone, $DateTime);
483
        return (int) $DateTime->format('U') + (int) $offset;
484
    }
485
486
487
    /**
488
     *    _set_date_time_field
489
     *    modifies EE_Base_Class EE_Datetime_Field objects
490
     *
491
     * @param  EE_Base_Class $obj                 EE_Base_Class object
492
     * @param    DateTime    $DateTime            PHP DateTime object
493
     * @param  string        $datetime_field_name the datetime fieldname to be manipulated
494
     * @return EE_Base_Class
495
     * @throws EE_Error
496
     */
497
    protected static function _set_date_time_field(EE_Base_Class $obj, DateTime $DateTime, $datetime_field_name)
498
    {
499
        // grab current datetime format
500
        $current_format = $obj->get_format();
501
        // set new full timestamp format
502
        $obj->set_date_format(EE_Datetime_Field::mysql_date_format);
503
        $obj->set_time_format(EE_Datetime_Field::mysql_time_format);
504
        // set the new date value using a full timestamp format so that no data is lost
505
        $obj->set($datetime_field_name, $DateTime->format(EE_Datetime_Field::mysql_timestamp_format));
506
        // reset datetime formats
507
        $obj->set_date_format($current_format[0]);
508
        $obj->set_time_format($current_format[1]);
509
        return $obj;
510
    }
511
512
513
    /**
514
     *    date_time_add
515
     *    helper for doing simple datetime calculations on a given datetime from EE_Base_Class
516
     *    and modifying it IN the EE_Base_Class so you don't have to do anything else.
517
     *
518
     * @param  EE_Base_Class $obj                 EE_Base_Class object
519
     * @param  string        $datetime_field_name name of the EE_Datetime_Filed datatype db column to be manipulated
520
     * @param  string        $period              what you are adding. The options are (years, months, days, hours,
521
     *                                            minutes, seconds) defaults to years
522
     * @param  integer       $value               what you want to increment the time by
523
     * @return EE_Base_Class return the EE_Base_Class object so right away you can do something with it
524
     *                                            (chaining)
525
     * @throws EE_Error
526
     * @throws Exception
527
     */
528 View Code Duplication
    public static function date_time_add(EE_Base_Class $obj, $datetime_field_name, $period = 'years', $value = 1)
529
    {
530
        //get the raw UTC date.
531
        $DateTime = $obj->get_DateTime_object($datetime_field_name);
532
        $DateTime = EEH_DTT_Helper::calc_date($DateTime, $period, $value);
533
        return EEH_DTT_Helper::_set_date_time_field($obj, $DateTime, $datetime_field_name);
534
    }
535
536
537
    /**
538
     *    date_time_subtract
539
     *    same as date_time_add except subtracting value instead of adding.
540
     *
541
     * @param EE_Base_Class $obj
542
     * @param  string       $datetime_field_name name of the EE_Datetime_Filed datatype db column to be manipulated
543
     * @param string        $period
544
     * @param int           $value
545
     * @return EE_Base_Class
546
     * @throws EE_Error
547
     * @throws Exception
548
     */
549 View Code Duplication
    public static function date_time_subtract(EE_Base_Class $obj, $datetime_field_name, $period = 'years', $value = 1)
550
    {
551
        //get the raw UTC date
552
        $DateTime = $obj->get_DateTime_object($datetime_field_name);
553
        $DateTime = EEH_DTT_Helper::calc_date($DateTime, $period, $value, '-');
554
        return EEH_DTT_Helper::_set_date_time_field($obj, $DateTime, $datetime_field_name);
555
    }
556
557
558
    /**
559
     * Simply takes an incoming DateTime object and does calculations on it based on the incoming parameters
560
     *
561
     * @param  DateTime   $DateTime DateTime object
562
     * @param  string     $period   a value to indicate what interval is being used in the calculation. The options are
563
     *                              'years', 'months', 'days', 'hours', 'minutes', 'seconds'. Defaults to years.
564
     * @param  int|string $value    What you want to increment the date by
565
     * @param  string     $operand  What operand you wish to use for the calculation
566
     * @return DateTime return whatever type came in.
567
     * @throws Exception
568
     * @throws EE_Error
569
     */
570
    protected static function _modify_datetime_object(DateTime $DateTime, $period = 'years', $value = 1, $operand = '+')
571
    {
572
        if (! $DateTime instanceof DateTime) {
573
            throw new EE_Error(
574
                sprintf(
575
                    esc_html__('Expected a PHP DateTime object, but instead received %1$s', 'event_espresso'),
576
                    print_r($DateTime, true)
577
                )
578
            );
579
        }
580
        switch ($period) {
581
            case 'years' :
582
                $value = 'P' . $value . 'Y';
583
                break;
584
            case 'months' :
585
                $value = 'P' . $value . 'M';
586
                break;
587
            case 'weeks' :
588
                $value = 'P' . $value . 'W';
589
                break;
590
            case 'days' :
591
                $value = 'P' . $value . 'D';
592
                break;
593
            case 'hours' :
594
                $value = 'PT' . $value . 'H';
595
                break;
596
            case 'minutes' :
597
                $value = 'PT' . $value . 'M';
598
                break;
599
            case 'seconds' :
600
                $value = 'PT' . $value . 'S';
601
                break;
602
        }
603
        switch ($operand) {
604
            case '+':
605
                $DateTime->add(new DateInterval($value));
606
                break;
607
            case '-':
608
                $DateTime->sub(new DateInterval($value));
609
                break;
610
        }
611
        return $DateTime;
612
    }
613
614
615
    /**
616
     * Simply takes an incoming Unix timestamp and does calculations on it based on the incoming parameters
617
     *
618
     * @param  int     $timestamp Unix timestamp
619
     * @param  string  $period    a value to indicate what interval is being used in the calculation. The options are
620
     *                            'years', 'months', 'days', 'hours', 'minutes', 'seconds'. Defaults to years.
621
     * @param  integer $value     What you want to increment the date by
622
     * @param  string  $operand   What operand you wish to use for the calculation
623
     * @return int
624
     * @throws EE_Error
625
     */
626
    protected static function _modify_timestamp($timestamp, $period = 'years', $value = 1, $operand = '+')
627
    {
628
        if (! preg_match(EE_Datetime_Field::unix_timestamp_regex, $timestamp)) {
629
            throw new EE_Error(
630
                sprintf(
631
                    esc_html__('Expected a Unix timestamp, but instead received %1$s', 'event_espresso'),
632
                    print_r($timestamp, true)
633
                )
634
            );
635
        }
636
        switch ($period) {
637
            case 'years' :
638
                $value = YEAR_IN_SECONDS * $value;
639
                break;
640
            case 'months' :
641
                $value = YEAR_IN_SECONDS / 12 * $value;
642
                break;
643
            case 'weeks' :
644
                $value = WEEK_IN_SECONDS * $value;
645
                break;
646
            case 'days' :
647
                $value = DAY_IN_SECONDS * $value;
648
                break;
649
            case 'hours' :
650
                $value = HOUR_IN_SECONDS * $value;
651
                break;
652
            case 'minutes' :
653
                $value = MINUTE_IN_SECONDS * $value;
654
                break;
655
        }
656
        switch ($operand) {
657
            case '+':
658
                $timestamp += $value;
659
                break;
660
            case '-':
661
                $timestamp -= $value;
662
                break;
663
        }
664
        return $timestamp;
665
    }
666
667
668
    /**
669
     * Simply takes an incoming UTC timestamp or DateTime object and does calculations on it based on the incoming
670
     * parameters and returns the new timestamp or DateTime.
671
     *
672
     * @param  int | DateTime $DateTime_or_timestamp DateTime object or Unix timestamp
673
     * @param  string         $period                a value to indicate what interval is being used in the
674
     *                                               calculation. The options are 'years', 'months', 'days', 'hours',
675
     *                                               'minutes', 'seconds'. Defaults to years.
676
     * @param  integer        $value                 What you want to increment the date by
677
     * @param  string         $operand               What operand you wish to use for the calculation
678
     * @return mixed string|DateTime          return whatever type came in.
679
     * @throws Exception
680
     * @throws EE_Error
681
     */
682
    public static function calc_date($DateTime_or_timestamp, $period = 'years', $value = 1, $operand = '+')
683
    {
684
        if ($DateTime_or_timestamp instanceof DateTime) {
685
            return EEH_DTT_Helper::_modify_datetime_object(
686
                $DateTime_or_timestamp,
687
                $period,
688
                $value,
689
                $operand
690
            );
691
        }
692
        if (preg_match(EE_Datetime_Field::unix_timestamp_regex, $DateTime_or_timestamp)) {
693
            return EEH_DTT_Helper::_modify_timestamp(
694
                $DateTime_or_timestamp,
695
                $period,
696
                $value,
697
                $operand
698
            );
699
        }
700
        //error
701
        return $DateTime_or_timestamp;
702
    }
703
704
705
    /**
706
     * The purpose of this helper method is to receive an incoming format string in php date/time format
707
     * and spit out the js and moment.js equivalent formats.
708
     * Note, if no format string is given, then it is assumed the user wants what is set for WP.
709
     * Note, js date and time formats are those used by the jquery-ui datepicker and the jquery-ui date-
710
     * time picker.
711
     *
712
     * @see http://stackoverflow.com/posts/16725290/ for the code inspiration.
713
     * @param string $date_format_string
714
     * @param string $time_format_string
715
     * @return array
716
     *              array(
717
     *              'js' => array (
718
     *              'date' => //date format
719
     *              'time' => //time format
720
     *              ),
721
     *              'moment' => //date and time format.
722
     *              )
723
     */
724
    public static function convert_php_to_js_and_moment_date_formats(
725
        $date_format_string = null,
726
        $time_format_string = null
727
    ) {
728
        if ($date_format_string === null) {
729
            $date_format_string = (string) get_option('date_format');
730
        }
731
        if ($time_format_string === null) {
732
            $time_format_string = (string) get_option('time_format');
733
        }
734
        $date_format = self::_php_to_js_moment_converter($date_format_string);
735
        $time_format = self::_php_to_js_moment_converter($time_format_string);
736
        return array(
737
            'js'     => array(
738
                'date' => $date_format['js'],
739
                'time' => $time_format['js'],
740
            ),
741
            'moment' => $date_format['moment'] . ' ' . $time_format['moment'],
742
        );
743
    }
744
745
746
    /**
747
     * This converts incoming format string into js and moment variations.
748
     *
749
     * @param string $format_string incoming php format string
750
     * @return array js and moment formats.
751
     */
752
    protected static function _php_to_js_moment_converter($format_string)
753
    {
754
        /**
755
         * This is a map of symbols for formats.
756
         * The index is the php symbol, the equivalent values are in the array.
757
         *
758
         * @var array
759
         */
760
        $symbols_map          = array(
761
            // Day
762
            //01
763
            'd' => array(
764
                'js'     => 'dd',
765
                'moment' => 'DD',
766
            ),
767
            //Mon
768
            'D' => array(
769
                'js'     => 'D',
770
                'moment' => 'ddd',
771
            ),
772
            //1,2,...31
773
            'j' => array(
774
                'js'     => 'd',
775
                'moment' => 'D',
776
            ),
777
            //Monday
778
            'l' => array(
779
                'js'     => 'DD',
780
                'moment' => 'dddd',
781
            ),
782
            //ISO numeric representation of the day of the week (1-6)
783
            'N' => array(
784
                'js'     => '',
785
                'moment' => 'E',
786
            ),
787
            //st,nd.rd
788
            'S' => array(
789
                'js'     => '',
790
                'moment' => 'o',
791
            ),
792
            //numeric representation of day of week (0-6)
793
            'w' => array(
794
                'js'     => '',
795
                'moment' => 'd',
796
            ),
797
            //day of year starting from 0 (0-365)
798
            'z' => array(
799
                'js'     => 'o',
800
                'moment' => 'DDD' //note moment does not start with 0 so will need to modify by subtracting 1
801
            ),
802
            // Week
803
            //ISO-8601 week number of year (weeks starting on monday)
804
            'W' => array(
805
                'js'     => '',
806
                'moment' => 'w',
807
            ),
808
            // Month
809
            // January...December
810
            'F' => array(
811
                'js'     => 'MM',
812
                'moment' => 'MMMM',
813
            ),
814
            //01...12
815
            'm' => array(
816
                'js'     => 'mm',
817
                'moment' => 'MM',
818
            ),
819
            //Jan...Dec
820
            'M' => array(
821
                'js'     => 'M',
822
                'moment' => 'MMM',
823
            ),
824
            //1-12
825
            'n' => array(
826
                'js'     => 'm',
827
                'moment' => 'M',
828
            ),
829
            //number of days in given month
830
            't' => array(
831
                'js'     => '',
832
                'moment' => '',
833
            ),
834
            // Year
835
            //whether leap year or not 1/0
836
            'L' => array(
837
                'js'     => '',
838
                'moment' => '',
839
            ),
840
            //ISO-8601 year number
841
            'o' => array(
842
                'js'     => '',
843
                'moment' => 'GGGG',
844
            ),
845
            //1999...2003
846
            'Y' => array(
847
                'js'     => 'yy',
848
                'moment' => 'YYYY',
849
            ),
850
            //99...03
851
            'y' => array(
852
                'js'     => 'y',
853
                'moment' => 'YY',
854
            ),
855
            // Time
856
            // am/pm
857
            'a' => array(
858
                'js'     => 'tt',
859
                'moment' => 'a',
860
            ),
861
            // AM/PM
862
            'A' => array(
863
                'js'     => 'TT',
864
                'moment' => 'A',
865
            ),
866
            // Swatch Internet Time?!?
867
            'B' => array(
868
                'js'     => '',
869
                'moment' => '',
870
            ),
871
            //1...12
872
            'g' => array(
873
                'js'     => 'h',
874
                'moment' => 'h',
875
            ),
876
            //0...23
877
            'G' => array(
878
                'js'     => 'H',
879
                'moment' => 'H',
880
            ),
881
            //01...12
882
            'h' => array(
883
                'js'     => 'hh',
884
                'moment' => 'hh',
885
            ),
886
            //00...23
887
            'H' => array(
888
                'js'     => 'HH',
889
                'moment' => 'HH',
890
            ),
891
            //00..59
892
            'i' => array(
893
                'js'     => 'mm',
894
                'moment' => 'mm',
895
            ),
896
            //seconds... 00...59
897
            's' => array(
898
                'js'     => 'ss',
899
                'moment' => 'ss',
900
            ),
901
            //microseconds
902
            'u' => array(
903
                'js'     => '',
904
                'moment' => '',
905
            ),
906
        );
907
        $jquery_ui_format     = '';
908
        $moment_format        = '';
909
        $escaping             = false;
910
        $format_string_length = strlen($format_string);
911
        for ($i = 0; $i < $format_string_length; $i++) {
912
            $char = $format_string[ $i ];
913
            if ($char === '\\') { // PHP date format escaping character
914
                $i++;
915
                if ($escaping) {
916
                    $jquery_ui_format .= $format_string[ $i ];
917
                    $moment_format    .= $format_string[ $i ];
918
                } else {
919
                    $jquery_ui_format .= '\'' . $format_string[ $i ];
920
                    $moment_format    .= $format_string[ $i ];
921
                }
922
                $escaping = true;
923
            } else {
924
                if ($escaping) {
925
                    $jquery_ui_format .= "'";
926
                    $moment_format    .= "'";
927
                    $escaping         = false;
928
                }
929
                if (isset($symbols_map[ $char ])) {
930
                    $jquery_ui_format .= $symbols_map[ $char ]['js'];
931
                    $moment_format    .= $symbols_map[ $char ]['moment'];
932
                } else {
933
                    $jquery_ui_format .= $char;
934
                    $moment_format    .= $char;
935
                }
936
            }
937
        }
938
        return array('js' => $jquery_ui_format, 'moment' => $moment_format);
939
    }
940
941
942
    /**
943
     * This takes an incoming format string and validates it to ensure it will work fine with PHP.
944
     *
945
     * @param string $format_string   Incoming format string for php date().
946
     * @return mixed bool|array  If all is okay then TRUE is returned.  Otherwise an array of validation
947
     *                                errors is returned.  So for client code calling, check for is_array() to
948
     *                                indicate failed validations.
949
     */
950
    public static function validate_format_string($format_string)
951
    {
952
        $error_msg = array();
953
        //time format checks
954
        switch (true) {
955
            case   strpos($format_string, 'h') !== false  :
956
            case   strpos($format_string, 'g') !== false :
957
                /**
958
                 * if the time string has a lowercase 'h' which == 12 hour time format and there
959
                 * is not any ante meridiem format ('a' or 'A').  Then throw an error because its
960
                 * too ambiguous and PHP won't be able to figure out whether 1 = 1pm or 1am.
961
                 */
962
                if (stripos($format_string, 'A') === false) {
963
                    $error_msg[] = esc_html__(
964
                        'There is a  time format for 12 hour time but no  "a" or "A" to indicate am/pm.  Without this distinction, PHP is unable to determine if a "1" for the hour value equals "1pm" or "1am".',
965
                        'event_espresso'
966
                    );
967
                }
968
                break;
969
        }
970
        return empty($error_msg) ? true : $error_msg;
971
    }
972
973
974
    /**
975
     *     If the the first date starts at midnight on one day, and the next date ends at midnight on the
976
     *     very next day then this method will return true.
977
     *    If $date_1 = 2015-12-15 00:00:00 and $date_2 = 2015-12-16 00:00:00 then this function will return true.
978
     *    If $date_1 = 2015-12-15 03:00:00 and $date_2 = 2015-12_16 03:00:00 then this function will return false.
979
     *    If $date_1 = 2015-12-15 00:00:00 and $date_2 = 2015-12-15 00:00:00 then this function will return true.
980
     *
981
     * @param mixed $date_1
982
     * @param mixed $date_2
983
     * @return bool
984
     */
985
    public static function dates_represent_one_24_hour_date($date_1, $date_2)
986
    {
987
988
        if (
989
            (! $date_1 instanceof DateTime || ! $date_2 instanceof DateTime)
990
            || ($date_1->format(EE_Datetime_Field::mysql_time_format) !== '00:00:00'
991
                || $date_2->format(
992
                    EE_Datetime_Field::mysql_time_format
993
                ) !== '00:00:00')
994
        ) {
995
            return false;
996
        }
997
        return $date_2->format('U') - $date_1->format('U') === 86400;
998
    }
999
1000
1001
    /**
1002
     * This returns the appropriate query interval string that can be used in sql queries involving mysql Date
1003
     * Functions.
1004
     *
1005
     * @param string $timezone_string    A timezone string in a valid format to instantiate a DateTimeZone object.
1006
     * @param string $field_for_interval The Database field that is the interval is applied to in the query.
1007
     * @return string
1008
     */
1009
    public static function get_sql_query_interval_for_offset($timezone_string, $field_for_interval)
1010
    {
1011
        try {
1012
            /** need to account for timezone offset on the selects */
1013
            $DateTimeZone = new DateTimeZone($timezone_string);
1014
        } catch (Exception $e) {
1015
            $DateTimeZone = null;
1016
        }
1017
        /**
1018
         * Note get_option( 'gmt_offset') returns a value in hours, whereas DateTimeZone::getOffset returns values in seconds.
1019
         * Hence we do the calc for DateTimeZone::getOffset.
1020
         */
1021
        $offset         = $DateTimeZone instanceof DateTimeZone
1022
            ? $DateTimeZone->getOffset(new DateTime('now')) / HOUR_IN_SECONDS
1023
            : (float) get_option('gmt_offset');
1024
        $query_interval = $offset < 0
1025
            ? 'DATE_SUB(' . $field_for_interval . ', INTERVAL ' . $offset * -1 . ' HOUR)'
1026
            : 'DATE_ADD(' . $field_for_interval . ', INTERVAL ' . $offset . ' HOUR)';
1027
        return $query_interval;
1028
    }
1029
1030
1031
    /**
1032
     * Retrieves the site's default timezone and returns it formatted so it's ready for display
1033
     * to users. If you want to customize how its displayed feel free to fetch the 'timezone_string'
1034
     * and 'gmt_offset' WordPress options directly; or use the filter
1035
     * FHEE__EEH_DTT_Helper__get_timezone_string_for_display
1036
     * (although note that we remove any HTML that may be added)
1037
     *
1038
     * @return string
1039
     */
1040
    public static function get_timezone_string_for_display()
1041
    {
1042
        $pretty_timezone = apply_filters('FHEE__EEH_DTT_Helper__get_timezone_string_for_display', '');
1043
        if (! empty($pretty_timezone)) {
1044
            return esc_html($pretty_timezone);
1045
        }
1046
        $timezone_string = get_option('timezone_string');
1047
        if ($timezone_string) {
1048
            static $mo_loaded = false;
1049
            // Load translations for continents and cities just like wp_timezone_choice does
1050
            if (! $mo_loaded) {
1051
                $locale = get_locale();
1052
                $mofile = WP_LANG_DIR . '/continents-cities-' . $locale . '.mo';
1053
                load_textdomain('continents-cities', $mofile);
1054
                $mo_loaded = true;
1055
            }
1056
            //well that was easy.
1057
            $parts = explode('/', $timezone_string);
1058
            //remove the continent
1059
            unset($parts[0]);
1060
            $t_parts = array();
1061
            foreach ($parts as $part) {
1062
                $t_parts[] = translate(str_replace('_', ' ', $part), 'continents-cities');
1063
            }
1064
            return implode(' - ', $t_parts);
1065
        }
1066
        //they haven't set the timezone string, so let's return a string like "UTC+1"
1067
        $gmt_offset = get_option('gmt_offset');
1068
        $prefix     = (int) $gmt_offset >= 0 ? '+' : '';
1069
        $parts      = explode('.', (string) $gmt_offset);
1070
        if (count($parts) === 1) {
1071
            $parts[1] = '00';
1072
        } else {
1073
            //convert the part after the decimal, eg "5" (from x.5) or "25" (from x.25)
1074
            //to minutes, eg 30 or 15, respectively
1075
            $hour_fraction = (float) ('0.' . $parts[1]);
1076
            $parts[1]      = (string) $hour_fraction * 60;
1077
        }
1078
        return sprintf(__('UTC%1$s', 'event_espresso'), $prefix . implode(':', $parts));
1079
    }
1080
1081
1082
1083
    /**
1084
     * So PHP does this awesome thing where if you are trying to get a timestamp
1085
     * for a month using a string like "February" or "February 2017",
1086
     * and you don't specify a day as part of your string,
1087
     * then PHP will use whatever the current day of the month is.
1088
     * IF the current day of the month happens to be the 30th or 31st,
1089
     * then PHP gets really confused by a date like February 30,
1090
     * so instead of saying
1091
     *      "Hey February only has 28 days (this year)...
1092
     *      ...you must have meant the last day of the month!"
1093
     * PHP does the next most logical thing, and bumps the date up to March 2nd,
1094
     * because someone requesting February 30th obviously meant March 1st!
1095
     * The way around this is to always set the day to the first,
1096
     * so that the month will stay on the month you wanted.
1097
     * this method will add that "1" into your date regardless of the format.
1098
     *
1099
     * @param string $month
1100
     * @return string
1101
     */
1102
    public static function first_of_month_timestamp($month = '')
1103
    {
1104
        $month = (string) $month;
1105
        $year  = '';
1106
        // check if the incoming string has a year in it or not
1107
        if (preg_match('/\b\d{4}\b/', $month, $matches)) {
1108
            $year = $matches[0];
1109
            // ten remove that from the month string as well as any spaces
1110
            $month = trim(str_replace($year, '', $month));
1111
            // add a space before the year
1112
            $year = " {$year}";
1113
        }
1114
        // return timestamp for something like "February 1 2017"
1115
        return strtotime("{$month} 1{$year}");
1116
    }
1117
1118
1119
    /**
1120
     * This simply returns the timestamp for tomorrow (midnight next day) in this sites timezone.  So it may be midnight
1121
     * for this sites timezone, but the timestamp could be some other time GMT.
1122
     */
1123
    public static function tomorrow()
1124
    {
1125
        //The multiplication of -1 ensures that we switch positive offsets to negative and negative offsets to positive
1126
        //before adding to the timestamp.  Why? Because we want tomorrow to be for midnight the next day in THIS timezone
1127
        //not an offset from midnight in UTC.  So if we're starting with UTC 00:00:00, then we want to make sure the
1128
        //final timestamp is equivalent to midnight in this timezone as represented in GMT.
1129
        return strtotime('tomorrow') + (self::get_site_timezone_gmt_offset() * -1);
1130
    }
1131
1132
1133
    /**
1134
     * **
1135
     * Gives a nicely-formatted list of timezone strings.
1136
     * Copied from the core wp function by the same name so we could customize to remove UTC offsets.
1137
     *
1138
     * @since     4.9.40.rc.008
1139
     * @staticvar bool $mo_loaded
1140
     * @staticvar string $locale_loaded
1141
     * @param string $selected_zone Selected timezone.
1142
     * @param string $locale        Optional. Locale to load the timezones in. Default current site locale.
1143
     * @return string
1144
     */
1145
    public static function wp_timezone_choice($selected_zone, $locale = null)
1146
    {
1147
        static $mo_loaded = false, $locale_loaded = null;
1148
        $continents = array(
1149
            'Africa',
1150
            'America',
1151
            'Antarctica',
1152
            'Arctic',
1153
            'Asia',
1154
            'Atlantic',
1155
            'Australia',
1156
            'Europe',
1157
            'Indian',
1158
            'Pacific',
1159
        );
1160
        // Load translations for continents and cities.
1161
        if (! $mo_loaded || $locale !== $locale_loaded) {
1162
            $locale_loaded = $locale ? $locale : get_locale();
1163
            $mofile        = WP_LANG_DIR . '/continents-cities-' . $locale_loaded . '.mo';
1164
            unload_textdomain('continents-cities');
1165
            load_textdomain('continents-cities', $mofile);
1166
            $mo_loaded = true;
1167
        }
1168
        $zone_data = array();
1169
        foreach (timezone_identifiers_list() as $zone) {
1170
            $zone = explode('/', $zone);
1171
            if (! in_array($zone[0], $continents, true)) {
1172
                continue;
1173
            }
1174
            // This determines what gets set and translated - we don't translate Etc/* strings here, they are done later
1175
            $exists      = array(
1176
                0 => isset($zone[0]) && $zone[0],
1177
                1 => isset($zone[1]) && $zone[1],
1178
                2 => isset($zone[2]) && $zone[2],
1179
            );
1180
            $exists[3]   = $exists[0] && $zone[0] !== 'Etc';
1181
            $exists[4]   = $exists[1] && $exists[3];
1182
            $exists[5]   = $exists[2] && $exists[3];
1183
            $zone_data[] = array(
1184
                'continent'   => $exists[0] ? $zone[0] : '',
1185
                'city'        => $exists[1] ? $zone[1] : '',
1186
                'subcity'     => $exists[2] ? $zone[2] : '',
1187
                't_continent' => $exists[3]
1188
                    ? translate(str_replace('_', ' ', $zone[0]), 'continents-cities')
1189
                    : '',
1190
                't_city'      => $exists[4]
1191
                    ? translate(str_replace('_', ' ', $zone[1]), 'continents-cities')
1192
                    : '',
1193
                't_subcity'   => $exists[5]
1194
                    ? translate(str_replace('_', ' ', $zone[2]), 'continents-cities')
1195
                    : '',
1196
            );
1197
        }
1198
        usort($zone_data, '_wp_timezone_choice_usort_callback');
1199
        $structure = array();
1200
        if (empty($selected_zone)) {
1201
            $structure[] = '<option selected="selected" value="">' . __('Select a city') . '</option>';
1202
        }
1203
        foreach ($zone_data as $key => $zone) {
1204
            // Build value in an array to join later
1205
            $value = array($zone['continent']);
1206
            if (empty($zone['city'])) {
1207
                // It's at the continent level (generally won't happen)
1208
                $display = $zone['t_continent'];
1209
            } else {
1210
                // It's inside a continent group
1211
                // Continent optgroup
1212
                if (! isset($zone_data[ $key - 1 ]) || $zone_data[ $key - 1 ]['continent'] !== $zone['continent']) {
1213
                    $label       = $zone['t_continent'];
1214
                    $structure[] = '<optgroup label="' . esc_attr($label) . '">';
1215
                }
1216
                // Add the city to the value
1217
                $value[] = $zone['city'];
1218
                $display = $zone['t_city'];
1219
                if (! empty($zone['subcity'])) {
1220
                    // Add the subcity to the value
1221
                    $value[] = $zone['subcity'];
1222
                    $display .= ' - ' . $zone['t_subcity'];
1223
                }
1224
            }
1225
            // Build the value
1226
            $value       = implode('/', $value);
1227
            $selected    = $value === $selected_zone ? ' selected="selected"' : '';
1228
            $structure[] = '<option value="' . esc_attr($value) . '"' . $selected . '>'
1229
                           . esc_html($display)
1230
                           . '</option>';
1231
            // Close continent optgroup
1232
            if (! empty($zone['city'])
1233
                && (
1234
                    ! isset($zone_data[ $key + 1 ])
1235
                    || (isset($zone_data[ $key + 1 ]) && $zone_data[ $key + 1 ]['continent'] !== $zone['continent'])
1236
                )
1237
            ) {
1238
                $structure[] = '</optgroup>';
1239
            }
1240
        }
1241
        return implode("\n", $structure);
1242
    }
1243
1244
1245
    /**
1246
     * Shim for the WP function `get_user_locale` that was added in WordPress 4.7.0
1247
     *
1248
     * @param int|WP_User $user_id
1249
     * @return string
1250
     */
1251
    public static function get_user_locale($user_id = 0)
1252
    {
1253
        if (function_exists('get_user_locale')) {
1254
            return get_user_locale($user_id);
1255
        }
1256
        return get_locale();
1257
    }
1258
1259
}
1260