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