1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\Period;
10
11use Exception;
12use Piwik\Container\StaticContainer;
13use Piwik\Date;
14use Piwik\Period;
15use Piwik\Piwik;
16use Piwik\Plugin;
17
18/**
19 * Creates Period instances using the values used for the 'period' and 'date'
20 * query parameters.
21 *
22 * ## Custom Periods
23 *
24 * Plugins can define their own period factories all plugins to define new period types, in addition
25 * to "day", "week", "month", "year" and "range".
26 *
27 * To define a new period type:
28 *
29 * 1. create a new period class that derives from {@see \Piwik\Period}.
30 * 2. extend this class in a new PeriodFactory class and put it in /path/to/piwik/plugins/MyPlugin/PeriodFactory.php
31 *
32 * Period name collisions:
33 *
34 * If two plugins try to handle the same period label, the first one encountered will
35 * be used. In other words, avoid using another plugin's period label.
36 */
37abstract class Factory
38{
39    public function __construct()
40    {
41        // empty
42    }
43
44    /**
45     * Returns true if this factory should handle the period/date string combination.
46     *
47     * @return bool
48     */
49    public abstract function shouldHandle($strPeriod, $strDate);
50
51    /**
52     * Creates a period using the value of the 'date' query parameter.
53     *
54     * @param string $strPeriod
55     * @param string|Date $date
56     * @param string $timezone
57     * @return Period
58     */
59    public abstract function make($strPeriod, $date, $timezone);
60
61    /**
62     * Creates a new Period instance with a period ID and {@link Date} instance.
63     *
64     * _Note: This method cannot create {@link Period\Range} periods._
65     *
66     * @param string $period `"day"`, `"week"`, `"month"`, `"year"`, `"range"`.
67     * @param Date|string $date A date within the period or the range of dates.
68     * @param Date|string $timezone Optional timezone that will be used only when $period is 'range' or $date is 'last|previous'
69     * @throws Exception If `$strPeriod` is invalid or $date is invalid.
70     * @return \Piwik\Period
71     */
72    public static function build($period, $date, $timezone = 'UTC')
73    {
74        self::checkPeriodIsEnabled($period);
75
76        if (is_string($date)) {
77            [$period, $date] = self::convertRangeToDateIfNeeded($period, $date);
78            if (Period::isMultiplePeriod($date, $period)
79                || $period == 'range'
80            ) {
81
82                return new Range($period, $date, $timezone);
83            }
84
85            $dateObject = Date::factory($date);
86        } else if ($date instanceof Date) {
87            $dateObject = $date;
88        } else {
89            throw new \Exception("Invalid date supplied to Period\Factory::build(): " . gettype($date));
90        }
91
92        switch ($period) {
93            case 'day':
94                return new Day($dateObject);
95            case 'week':
96                return new Week($dateObject);
97            case 'month':
98                return new Month($dateObject);
99            case 'year':
100                return new Year($dateObject);
101        }
102
103        /** @var string[] $customPeriodFactories */
104        $customPeriodFactories = Plugin\Manager::getInstance()->findComponents('PeriodFactory', self::class);
105        foreach ($customPeriodFactories as $customPeriodFactoryClass) {
106            $customPeriodFactory = StaticContainer::get($customPeriodFactoryClass);
107            if ($customPeriodFactory->shouldHandle($period, $date)) {
108                return $customPeriodFactory->make($period, $date, $timezone);
109            }
110        }
111
112        throw new \Exception("Don't know how to create a '$period' period! (date = $date)");
113    }
114
115    public static function checkPeriodIsEnabled($period)
116    {
117        if (!self::isPeriodEnabledForAPI($period)) {
118            self::throwExceptionInvalidPeriod($period);
119        }
120    }
121
122    /**
123     * @param $strPeriod
124     * @throws \Exception
125     */
126    private static function throwExceptionInvalidPeriod($strPeriod)
127    {
128        $periods = self::getPeriodsEnabledForAPI();
129        $periods = implode(", ", $periods);
130        $message = Piwik::translate('General_ExceptionInvalidPeriod', array($strPeriod, $periods));
131        throw new Exception($message);
132    }
133
134    private static function convertRangeToDateIfNeeded($period, $date)
135    {
136        if (is_string($period) && is_string($date) && $period === 'range') {
137            $dates = explode(',', $date);
138            if (count($dates) === 2 && $dates[0] === $dates[1]) {
139                $period = 'day';
140                $date = $dates[0];
141            }
142        }
143
144        return array($period, $date);
145    }
146
147    /**
148     * Creates a Period instance using a period, date and timezone.
149     *
150     * @param string $timezone The timezone of the date. Only used if `$date` is `'now'`, `'today'`,
151     *                         `'yesterday'` or `'yesterdaySameTime'`.
152     * @param string $period The period string: `"day"`, `"week"`, `"month"`, `"year"`, `"range"`.
153     * @param string $date The date or date range string. Can be a special value including
154     *                     `'now'`, `'today'`, `'yesterday'`, `'yesterdaySameTime'`.
155     * @return \Piwik\Period
156     */
157    public static function makePeriodFromQueryParams($timezone, $period, $date)
158    {
159        if (empty($timezone)) {
160            $timezone = 'UTC';
161        }
162
163        [$period, $date] = self::convertRangeToDateIfNeeded($period, $date);
164
165        if ($period == 'range') {
166            self::checkPeriodIsEnabled('range');
167            $oPeriod = new Range('range', $date, $timezone, Date::factory('today', $timezone));
168        } else {
169            if (!($date instanceof Date)) {
170                if (preg_match('/^(now|today|yesterday|yesterdaySameTime|last[ -]?(?:week|month|year))$/i', $date)) {
171                    $date = Date::factoryInTimezone($date, $timezone);
172                }
173                $date = Date::factory($date);
174            }
175            $oPeriod = Factory::build($period, $date);
176        }
177        return $oPeriod;
178    }
179
180    /**
181     * @param $period
182     * @return bool
183     */
184    public static function isPeriodEnabledForAPI($period)
185    {
186        $periodValidator = new PeriodValidator();
187        return $periodValidator->isPeriodAllowedForAPI($period);
188    }
189
190    /**
191     * @return array
192     */
193    public static function getPeriodsEnabledForAPI()
194    {
195        $periodValidator = new PeriodValidator();
196        return $periodValidator->getPeriodsAllowedForAPI();
197    }
198
199    public static function isAnyLowerPeriodDisabledForAPI($periodLabel)
200    {
201        $parentPeriod = null;
202        switch ($periodLabel) {
203            case 'week':
204                $parentPeriod = 'day';
205                break;
206            case 'month':
207                $parentPeriod = 'week';
208                break;
209            case 'year':
210                $parentPeriod = 'month';
211                break;
212            default:
213                break;
214        }
215
216        if ($parentPeriod === null) {
217            return false;
218        }
219
220        return !self::isPeriodEnabledForAPI($parentPeriod)
221            || self::isAnyLowerPeriodDisabledForAPI($parentPeriod);
222    }
223}
224