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\Plugins\SitesManager;
10
11use DateTimeZone;
12use Exception;
13use Piwik\Access;
14use Piwik\CacheId;
15use Piwik\Common;
16use Piwik\Container\StaticContainer;
17use Piwik\Date;
18use Piwik\Intl\Data\Provider\CurrencyDataProvider;
19use Matomo\Network\IPUtils;
20use Piwik\Option;
21use Piwik\Piwik;
22use Piwik\Plugin\SettingsProvider;
23use Piwik\Plugins\CorePluginsAdmin\SettingsMetadata;
24use Piwik\Plugins\WebsiteMeasurable\Settings\Urls;
25use Piwik\Settings\Measurable\MeasurableProperty;
26use Piwik\Settings\Measurable\MeasurableSettings;
27use Piwik\ProxyHttp;
28use Piwik\Scheduler\Scheduler;
29use Piwik\SettingsPiwik;
30use Piwik\SettingsServer;
31use Piwik\Site;
32use Piwik\Tracker;
33use Piwik\Tracker\Cache;
34use Piwik\Tracker\TrackerCodeGenerator;
35use Piwik\Measurable\Type;
36use Piwik\Translation\Translator;
37use Piwik\Url;
38use Piwik\UrlHelper;
39use Piwik\DataAccess\Model as CoreModel;
40
41/**
42 * The SitesManager API gives you full control on Websites in Matomo (create, update and delete), and many methods to retrieve websites based on various attributes.
43 *
44 * This API lets you create websites via "addSite", update existing websites via "updateSite" and delete websites via "deleteSite".
45 * When creating websites, it can be useful to access internal codes used by Matomo for currencies via "getCurrencyList", or timezones via "getTimezonesList".
46 *
47 * There are also many ways to request a list of websites: from the website ID via "getSiteFromId" or the site URL via "getSitesIdFromSiteUrl".
48 * Often, the most useful technique is to list all websites that are known to a current user, based on the token_auth, via
49 * "getSitesWithAdminAccess", "getSitesWithViewAccess" or "getSitesWithAtLeastViewAccess" (which returns both).
50 *
51 * Some methods will affect all websites globally: "setGlobalExcludedIps" will set the list of IPs to be excluded on all websites,
52 * "setGlobalExcludedQueryParameters" will set the list of URL parameters to remove from URLs for all websites.
53 * The existing values can be fetched via "getExcludedIpsGlobal" and "getExcludedQueryParametersGlobal".
54 * See also the documentation about <a href='http://matomo.org/docs/manage-websites/' rel='noreferrer' target='_blank'>Managing Websites</a> in Matomo.
55 * @method static \Piwik\Plugins\SitesManager\API getInstance()
56 */
57class API extends \Piwik\Plugin\API
58{
59    const DEFAULT_SEARCH_KEYWORD_PARAMETERS = 'q,query,s,search,searchword,k,keyword';
60    const OPTION_EXCLUDED_IPS_GLOBAL = 'SitesManager_ExcludedIpsGlobal';
61    const OPTION_DEFAULT_TIMEZONE = 'SitesManager_DefaultTimezone';
62    const OPTION_DEFAULT_CURRENCY = 'SitesManager_DefaultCurrency';
63    const OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL = 'SitesManager_ExcludedQueryParameters';
64    const OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL = 'SitesManager_SearchKeywordParameters';
65    const OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL = 'SitesManager_SearchCategoryParameters';
66    const OPTION_EXCLUDED_USER_AGENTS_GLOBAL = 'SitesManager_ExcludedUserAgentsGlobal';
67    const OPTION_KEEP_URL_FRAGMENTS_GLOBAL = 'SitesManager_KeepURLFragmentsGlobal';
68
69    /**
70     * @var SettingsProvider
71     */
72    private $settingsProvider;
73
74    /**
75     * @var SettingsMetadata
76     */
77    private $settingsMetadata;
78
79    /**
80     * @var Translator
81     */
82    private $translator;
83
84    private $timezoneNameCache = [];
85
86    public function __construct(SettingsProvider $provider, SettingsMetadata $settingsMetadata, Translator $translator)
87    {
88        $this->settingsProvider = $provider;
89        $this->settingsMetadata = $settingsMetadata;
90        $this->translator = $translator;
91    }
92
93    /**
94     * Returns the javascript tag for the given idSite.
95     * This tag must be included on every page to be tracked by Matomo
96     *
97     * @param int    $idSite
98     * @param string $piwikUrl
99     * @param bool   $mergeSubdomains
100     * @param bool   $groupPageTitlesByDomain
101     * @param bool   $mergeAliasUrls
102     * @param bool   $visitorCustomVariables
103     * @param bool   $pageCustomVariables
104     * @param bool   $customCampaignNameQueryParam
105     * @param bool   $customCampaignKeywordParam
106     * @param bool   $doNotTrack
107     * @param bool   $disableCookies
108     * @param bool   $trackNoScript
109     * @param bool   $crossDomain
110     * @param bool   $forceMatomoEndpoint Whether the Matomo endpoint should be forced if Matomo was installed prior 3.7.0.
111     * @param bool   $excludedQueryParams
112     *
113     * @return string The Javascript tag ready to be included on the HTML pages
114     * @throws Exception
115     */
116    public function getJavascriptTag($idSite, $piwikUrl = '', $mergeSubdomains = false, $groupPageTitlesByDomain = false,
117                                     $mergeAliasUrls = false, $visitorCustomVariables = false, $pageCustomVariables = false,
118                                     $customCampaignNameQueryParam = false, $customCampaignKeywordParam = false,
119                                     $doNotTrack = false, $disableCookies = false, $trackNoScript = false,
120                                     $crossDomain = false, $forceMatomoEndpoint = false, $excludedQueryParams = false)
121    {
122        Piwik::checkUserHasViewAccess($idSite);
123
124        if (empty($piwikUrl)) {
125            $piwikUrl = SettingsPiwik::getPiwikUrl();
126        }
127
128        // Revert the automatic encoding
129        // TODO remove that when https://github.com/piwik/piwik/issues/4231 is fixed
130        $piwikUrl = Common::unsanitizeInputValue($piwikUrl);
131        $visitorCustomVariables = Common::unsanitizeInputValues($visitorCustomVariables);
132        $pageCustomVariables = Common::unsanitizeInputValues($pageCustomVariables);
133        $customCampaignNameQueryParam = Common::unsanitizeInputValue($customCampaignNameQueryParam);
134        $customCampaignKeywordParam = Common::unsanitizeInputValue($customCampaignKeywordParam);
135
136        if (is_array($excludedQueryParams)) {
137            $excludedQueryParams = implode(',', $excludedQueryParams);
138        }
139        $excludedQueryParams = Common::unsanitizeInputValue($excludedQueryParams);
140
141        $generator = new TrackerCodeGenerator();
142        if ($forceMatomoEndpoint) {
143            $generator->forceMatomoEndpoint();
144        }
145
146        $code = $generator->generate($idSite, $piwikUrl, $mergeSubdomains, $groupPageTitlesByDomain,
147                                     $mergeAliasUrls, $visitorCustomVariables, $pageCustomVariables,
148                                     $customCampaignNameQueryParam, $customCampaignKeywordParam,
149                                     $doNotTrack, $disableCookies, $trackNoScript, $crossDomain,
150                                     $excludedQueryParams);
151        $code = str_replace(array('<br>', '<br />', '<br/>'), '', $code);
152        return $code;
153    }
154
155    /**
156     * Returns image link tracking code for a given site with specified options.
157     *
158     * @param int $idSite The ID to generate tracking code for.
159     * @param string $piwikUrl The domain and URL path to the Matomo installation.
160     * @param int $idGoal An ID for a goal to trigger a conversion for.
161     * @param int $revenue The revenue of the goal conversion. Only used if $idGoal is supplied.
162     * @param bool $forceMatomoEndpoint Whether the Matomo endpoint should be forced if Matomo was installed prior 3.7.0.
163     * @return string The HTML tracking code.
164     */
165    public function getImageTrackingCode($idSite, $piwikUrl = '', $actionName = false, $idGoal = false, $revenue = false, $forceMatomoEndpoint = false)
166    {
167        $urlParams = array('idsite' => $idSite, 'rec' => 1);
168
169        if ($actionName !== false) {
170            $urlParams['action_name'] = urlencode(Common::unsanitizeInputValue($actionName));
171        }
172
173        if ($idGoal !== false) {
174            $urlParams['idgoal'] = $idGoal;
175            if ($revenue !== false) {
176                $urlParams['revenue'] = $revenue;
177            }
178        }
179
180        /**
181         * Triggered when generating image link tracking code server side. Plugins can use
182         * this event to customise the image tracking code that is displayed to the
183         * user.
184         *
185         * @param string &$piwikHost The domain and URL path to the Matomo installation, eg,
186         *                           `'examplepiwik.com/path/to/piwik'`.
187         * @param array &$urlParams The query parameters used in the <img> element's src
188         *                          URL. See Matomo's image tracking docs for more info.
189         */
190        Piwik::postEvent('SitesManager.getImageTrackingCode', array(&$piwikUrl, &$urlParams));
191
192        $trackerCodeGenerator = new TrackerCodeGenerator();
193        if ($forceMatomoEndpoint) {
194            $trackerCodeGenerator->forceMatomoEndpoint();
195        }
196        $matomoPhp = $trackerCodeGenerator->getPhpTrackerEndpoint();
197
198        $url = (ProxyHttp::isHttps() ? "https://" : "http://") . rtrim($piwikUrl, '/') . '/'.$matomoPhp.'?' . Url::getQueryStringFromParameters($urlParams);
199        $html = "<!-- Matomo Image Tracker-->
200<img referrerpolicy=\"no-referrer-when-downgrade\" src=\"" . htmlspecialchars($url, ENT_COMPAT, 'UTF-8') . "\" style=\"border:0\" alt=\"\" />
201<!-- End Matomo -->";
202        return htmlspecialchars($html, ENT_COMPAT, 'UTF-8');
203    }
204
205    /**
206     * Returns all websites belonging to the specified group
207     * @param string $group Group name
208     * @return array of sites
209     */
210    public function getSitesFromGroup($group = '')
211    {
212        Piwik::checkUserHasSuperUserAccess();
213
214        $group = trim($group);
215        $sites = $this->getModel()->getSitesFromGroup($group);
216
217        foreach ($sites as &$site) {
218            $this->enrichSite($site);
219        }
220
221        $sites = Site::setSitesFromArray($sites);
222        return $sites;
223    }
224
225    /**
226     * Returns the list of website groups, including the empty group
227     * if no group were specified for some websites
228     *
229     * @return array of group names strings
230     */
231    public function getSitesGroups()
232    {
233        Piwik::checkUserHasSuperUserAccess();
234
235        $groups = $this->getModel()->getSitesGroups();
236        $cleanedGroups = array_map('trim', $groups);
237
238        return $cleanedGroups;
239    }
240
241    /**
242     * Returns the website information : name, main_url
243     *
244     * @throws Exception if the site ID doesn't exist or the user doesn't have access to it
245     * @param int $idSite
246     * @return array
247     */
248    public function getSiteFromId($idSite)
249    {
250        Piwik::checkUserHasViewAccess($idSite);
251
252        $site = $this->getModel()->getSiteFromId($idSite);
253
254        if ($site) {
255            $this->enrichSite($site);
256        }
257
258        Site::setSiteFromArray($idSite, $site);
259
260        return $site;
261    }
262
263    private function getModel()
264    {
265        return new Model();
266    }
267
268    /**
269     * Returns the list of all URLs registered for the given idSite (main_url + alias URLs).
270     *
271     * @throws Exception if the website ID doesn't exist or the user doesn't have access to it
272     * @param int $idSite
273     * @return array list of URLs
274     */
275    public function getSiteUrlsFromId($idSite)
276    {
277        Piwik::checkUserHasViewAccess($idSite);
278        return $this->getModel()->getSiteUrlsFromId($idSite);
279    }
280
281    private function getSitesId()
282    {
283        return $this->getModel()->getSitesId();
284    }
285
286    /**
287     * Returns all websites, requires Super User access
288     *
289     * @return array The list of websites, indexed by idsite
290     */
291    public function getAllSites()
292    {
293        Piwik::checkUserHasSuperUserAccess();
294
295        $sites  = $this->getModel()->getAllSites();
296        $return = array();
297        foreach ($sites as $site) {
298            $this->enrichSite($site);
299            $return[$site['idsite']] = $site;
300        }
301
302        $return = Site::setSitesFromArray($return);
303
304        return $return;
305    }
306
307    /**
308     * Returns the list of all the website IDs registered.
309     * Requires Super User access.
310     *
311     * @return array The list of website IDs
312     */
313    public function getAllSitesId()
314    {
315        Piwik::checkUserHasSuperUserAccess();
316        try {
317            return $this->getSitesId();
318        } catch (Exception $e) {
319            // can be called before Matomo tables are created so return empty
320            return array();
321        }
322    }
323
324    /**
325     * Returns the list of websites with the 'admin' access for the current user.
326     * For the superUser it returns all the websites in the database.
327     *
328     * @param bool $fetchAliasUrls
329     * @param false|string $pattern
330     * @param false|int    $limit
331     * @return array for each site, an array of information (idsite, name, main_url, etc.)
332     */
333    public function getSitesWithAdminAccess($fetchAliasUrls = false, $pattern = false, $limit = false)
334    {
335        $sitesId = $this->getSitesIdWithAdminAccess();
336
337        if ($pattern === false) {
338            $sites = $this->getSitesFromIds($sitesId, $limit);
339        } else {
340            $sites = $this->getModel()->getPatternMatchSites($sitesId, $pattern, $limit);
341
342            foreach ($sites as &$site) {
343                $this->enrichSite($site);
344            }
345
346            $sites = Site::setSitesFromArray($sites);
347        }
348
349        if ($fetchAliasUrls) {
350            foreach ($sites as &$site) {
351                $site['alias_urls'] = $this->getSiteUrlsFromId($site['idsite']);
352            }
353        }
354
355        return $sites;
356    }
357
358    /**
359     * Returns the list of websites with the 'view' access for the current user.
360     * For the superUser it doesn't return any result because the superUser has admin access on all the websites (use getSitesWithAtLeastViewAccess() instead).
361     *
362     * @return array for each site, an array of information (idsite, name, main_url, etc.)
363     */
364    public function getSitesWithViewAccess()
365    {
366        $sitesId = $this->getSitesIdWithViewAccess();
367        return $this->getSitesFromIds($sitesId);
368    }
369
370    /**
371     * Returns the list of websites with the 'view' or 'admin' access for the current user.
372     * For the superUser it returns all the websites in the database.
373     *
374     * @param bool|int $limit Specify max number of sites to return
375     * @param bool $_restrictSitesToLogin Hack necessary when running scheduled tasks, where "Super User" is forced, but sometimes not desired, see #3017
376     * @return array array for each site, an array of information (idsite, name, main_url, etc.)
377     */
378    public function getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin = false)
379    {
380        $sitesId = $this->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin);
381        return $this->getSitesFromIds($sitesId, $limit);
382    }
383
384    /**
385     * Returns the list of websites ID with the 'admin' access for the current user.
386     * For the superUser it returns all the websites in the database.
387     *
388     * @return array list of websites ID
389     */
390    public function getSitesIdWithAdminAccess()
391    {
392        $sitesId = Access::getInstance()->getSitesIdWithAdminAccess();
393        return $sitesId;
394    }
395
396    /**
397     * Returns the list of websites ID with the 'view' access for the current user.
398     * For the superUser it doesn't return any result because the superUser has admin access on all the websites (use getSitesIdWithAtLeastViewAccess() instead).
399     *
400     * @return array list of websites ID
401     */
402    public function getSitesIdWithViewAccess()
403    {
404        return Access::getInstance()->getSitesIdWithViewAccess();
405    }
406
407    /**
408     * Returns the list of websites ID with the 'write' access for the current user.
409     * For the superUser it doesn't return any result because the superUser has write access on all the websites (use getSitesIdWithAtLeastWriteAccess() instead).
410     *
411     * @return array list of websites ID
412     */
413    public function getSitesIdWithWriteAccess()
414    {
415        return Access::getInstance()->getSitesIdWithWriteAccess();
416    }
417
418    /**
419     * Returns the list of websites ID with the 'view' or 'admin' access for the current user.
420     * For the superUser it returns all the websites in the database.
421     *
422     * @param bool $_restrictSitesToLogin
423     * @return array list of websites ID
424     */
425    public function getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin = false)
426    {
427        /** @var Scheduler $scheduler */
428        $scheduler = StaticContainer::getContainer()->get('Piwik\Scheduler\Scheduler');
429
430        if (Piwik::hasUserSuperUserAccess() && !$scheduler->isRunningTask()) {
431            return Access::getInstance()->getSitesIdWithAtLeastViewAccess();
432        }
433
434        if (!empty($_restrictSitesToLogin)
435            // Only Super User or logged in user can see viewable sites for a specific login,
436            // but during scheduled task execution, we sometimes want to restrict sites to
437            // a different login than the superuser.
438            && (Piwik::hasUserSuperUserAccessOrIsTheUser($_restrictSitesToLogin)
439                || $scheduler->isRunningTask())
440        ) {
441
442            if (Piwik::hasTheUserSuperUserAccess($_restrictSitesToLogin)) {
443                return Access::getInstance()->getSitesIdWithAtLeastViewAccess();
444            }
445
446            $accessRaw = Access::getInstance()->getRawSitesWithSomeViewAccess($_restrictSitesToLogin);
447            $sitesId   = array();
448
449            foreach ($accessRaw as $access) {
450                $sitesId[] = $access['idsite'];
451            }
452
453            return $sitesId;
454        } else {
455            return Access::getInstance()->getSitesIdWithAtLeastViewAccess();
456        }
457    }
458
459    /**
460     * Returns the list of websites from the ID array in parameters.
461     * The user access is not checked in this method so the ID have to be accessible by the user!
462     *
463     * @param array $idSites list of website ID
464     * @param bool $limit
465     * @return array
466     */
467    private function getSitesFromIds($idSites, $limit = false)
468    {
469        $sites = $this->getModel()->getSitesFromIds($idSites, $limit);
470
471        foreach ($sites as &$site) {
472            $this->enrichSite($site);
473        }
474
475        $sites = Site::setSitesFromArray($sites);
476
477        return $sites;
478    }
479
480    protected function getNormalizedUrls($url)
481    {
482        // if found, remove scheme and www. from URL
483        $hostname = str_replace('www.', '', $url);
484        $hostname = str_replace('http://', '', $hostname);
485        $hostname = str_replace('https://', '', $hostname);
486
487        // return all variations of the URL
488        return array(
489            $url,
490            "http://" . $hostname,
491            "http://www." . $hostname,
492            "https://" . $hostname,
493            "https://www." . $hostname
494        );
495    }
496
497    /**
498     * Returns the list of websites ID associated with a URL.
499     *
500     * @param string $url
501     * @return array list of websites ID
502     */
503    public function getSitesIdFromSiteUrl($url)
504    {
505        $url = $this->removeTrailingSlash($url);
506        $normalisedUrls = $this->getNormalizedUrls($url);
507
508        if (Piwik::hasUserSuperUserAccess()) {
509            $ids   = $this->getModel()->getAllSitesIdFromSiteUrl($normalisedUrls);
510        } else {
511            $login = Piwik::getCurrentUserLogin();
512            $ids   = $this->getModel()->getSitesIdFromSiteUrlHavingAccess($login, $normalisedUrls);
513        }
514
515        return $ids;
516    }
517
518    /**
519     * Returns all websites with a timezone matching one the specified timezones
520     *
521     * @param array $timezones
522     * @return array
523     * @ignore
524     */
525    public function getSitesIdFromTimezones($timezones)
526    {
527        Piwik::checkUserHasSuperUserAccess();
528
529        $timezones = Piwik::getArrayFromApiParameter($timezones);
530        $timezones = array_unique($timezones);
531
532        $ids = $this->getModel()->getSitesFromTimezones($timezones);
533
534        $return = array();
535        foreach ($ids as $id) {
536            $return[] = $id['idsite'];
537        }
538
539        return $return;
540    }
541
542    private function enrichSite(&$site)
543    {
544        $cacheKey = $site['timezone'] . $this->translator->getCurrentLanguage();
545        if (!isset($this->timezoneNameCache[$cacheKey])) {
546            //cached as this can be called VERY often and getTimezoneName is quite slow
547            $this->timezoneNameCache[$cacheKey] = $this->getTimezoneName($site['timezone']);
548        }
549        $site['timezone_name'] = $this->timezoneNameCache[$cacheKey];
550
551        $key = 'Intl_Currency_' . $site['currency'];
552        $name = $this->translator->translate($key);
553
554        $site['currency_name'] = ($key === $name) ? $site['currency'] : $name;
555
556        // don't want to expose other user logins here
557        if (!Piwik::hasUserSuperUserAccess()) {
558            unset($site['creator_login']);
559        }
560    }
561
562    /**
563     * Add a website.
564     * Requires Super User access.
565     *
566     * The website is defined by a name and an array of URLs.
567     * @param string $siteName Site name
568     * @param array|string $urls The URLs array must contain at least one URL called the 'main_url' ;
569     *                        if several URLs are provided in the array, they will be recorded
570     *                        as Alias URLs for this website.
571     *                        When calling API via HTTP specify multiple URLs via `&urls[]=http...&urls[]=http...`.
572     * @param int $ecommerce Is Ecommerce Reporting enabled for this website?
573     * @param null $siteSearch
574     * @param string $searchKeywordParameters Comma separated list of search keyword parameter names
575     * @param string $searchCategoryParameters Comma separated list of search category parameter names
576     * @param string $excludedIps Comma separated list of IPs to exclude from the reports (allows wildcards)
577     * @param null $excludedQueryParameters
578     * @param string $timezone Timezone string, eg. 'Europe/London'
579     * @param string $currency Currency, eg. 'EUR'
580     * @param string $group Website group identifier
581     * @param string $startDate Date at which the statistics for this website will start. Defaults to today's date in YYYY-MM-DD format
582     * @param null|string $excludedUserAgents
583     * @param int $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they
584     *                              will be removed. If 0, the default global behavior will be used.
585     * @param array|null $settingValues JSON serialized settings eg {settingName: settingValue, ...}
586     * @see getKeepURLFragmentsGlobal.
587     * @param string $type The website type, defaults to "website" if not set.
588     * @param bool|null $excludeUnknownUrls Track only URL matching one of website URLs
589     *
590     * @return int the website ID created
591     */
592    public function addSite($siteName,
593                            $urls = null,
594                            $ecommerce = null,
595                            $siteSearch = null,
596                            $searchKeywordParameters = null,
597                            $searchCategoryParameters = null,
598                            $excludedIps = null,
599                            $excludedQueryParameters = null,
600                            $timezone = null,
601                            $currency = null,
602                            $group = null,
603                            $startDate = null,
604                            $excludedUserAgents = null,
605                            $keepURLFragments = null,
606                            $type = null,
607                            $settingValues = null,
608                            $excludeUnknownUrls = null)
609    {
610        Piwik::checkUserHasSuperUserAccess();
611        SitesManager::dieIfSitesAdminIsDisabled();
612
613        $this->checkName($siteName);
614
615        if (!isset($settingValues)) {
616            $settingValues = array();
617        }
618
619        $coreProperties = array();
620        $coreProperties = $this->setSettingValue('urls', $urls, $coreProperties, $settingValues);
621        $coreProperties = $this->setSettingValue('ecommerce', $ecommerce, $coreProperties, $settingValues);
622        $coreProperties = $this->setSettingValue('group', $group, $coreProperties, $settingValues);
623        $coreProperties = $this->setSettingValue('sitesearch', $siteSearch, $coreProperties, $settingValues);
624        $coreProperties = $this->setSettingValue('sitesearch_keyword_parameters', explode(',', $searchKeywordParameters ?? ''), $coreProperties, $settingValues);
625        $coreProperties = $this->setSettingValue('sitesearch_category_parameters', explode(',', $searchCategoryParameters ?? ''), $coreProperties, $settingValues);
626        $coreProperties = $this->setSettingValue('keep_url_fragment', $keepURLFragments, $coreProperties, $settingValues);
627        $coreProperties = $this->setSettingValue('exclude_unknown_urls', $excludeUnknownUrls, $coreProperties, $settingValues);
628        $coreProperties = $this->setSettingValue('excluded_ips', explode(',', $excludedIps ?? ''), $coreProperties, $settingValues);
629        $coreProperties = $this->setSettingValue('excluded_parameters', explode(',', $excludedQueryParameters ?? ''), $coreProperties, $settingValues);
630        $coreProperties = $this->setSettingValue('excluded_user_agents', explode(',', $excludedUserAgents ?? ''), $coreProperties, $settingValues);
631
632        $timezone = trim($timezone ?? '');
633        if (empty($timezone)) {
634            $timezone = $this->getDefaultTimezone();
635        }
636        $this->checkValidTimezone($timezone);
637
638        if (empty($currency)) {
639            $currency = $this->getDefaultCurrency();
640        }
641        $this->checkValidCurrency($currency);
642
643        $bind = array('name' => $siteName);
644        $bind['timezone']   = $timezone;
645        $bind['currency']   = $currency;
646        $bind['main_url']   = '';
647
648        if (is_null($startDate)) {
649            $bind['ts_created'] = Date::now()->getDatetime();
650        } else {
651            $bind['ts_created'] = Date::factory($startDate)->getDatetime();
652        }
653
654        $bind['type'] = $this->checkAndReturnType($type);
655
656        if (!empty($group) && Piwik::hasUserSuperUserAccess()) {
657            $bind['group'] = trim($group);
658        } else {
659            $bind['group'] = "";
660        }
661
662        $bind['creator_login'] = Piwik::getCurrentUserLogin();
663
664        $allSettings = $this->setAndValidateMeasurableSettings(0, 'website', $coreProperties);
665
666        // any setting specified in setting values will overwrite other setting
667        if (!empty($settingValues)) {
668            $this->setAndValidateMeasurableSettings(0, $bind['type'], $settingValues);
669        }
670
671        foreach ($allSettings as $settings) {
672            foreach ($settings->getSettingsWritableByCurrentUser() as $setting) {
673                $name = $setting->getName();
674                if ($setting instanceof MeasurableProperty && $name !== 'urls') {
675                    $default = $setting->getDefaultValue();
676                    if (is_bool($default)) {
677                        $default = (int) $default;
678                    } elseif (is_array($default)) {
679                        $default = implode(',', $default);
680                    }
681
682                    $bind[$name] = $default;
683                }
684            }
685        }
686
687        $idSite = $this->getModel()->createSite($bind);
688
689        if (!empty($coreProperties)) {
690            $this->saveMeasurableSettings($idSite, 'website', $coreProperties);
691        }
692        if (!empty($settingValues)) {
693            $this->saveMeasurableSettings($idSite, $bind['type'], $settingValues);
694        }
695
696        // we reload the access list which doesn't yet take in consideration this new website
697        Access::getInstance()->reloadAccess();
698
699        $this->postUpdateWebsite($idSite);
700
701        /**
702         * Triggered after a site has been added.
703         *
704         * @param int $idSite The ID of the site that was added.
705         */
706        Piwik::postEvent('SitesManager.addSite.end', array($idSite));
707
708        return (int) $idSite;
709    }
710
711    private function setSettingValue($fieldName, $value, $coreProperties, $settingValues)
712    {
713        $pluginName = 'WebsiteMeasurable';
714
715        if (isset($value)) {
716
717            if (empty($coreProperties[$pluginName])) {
718                $coreProperties[$pluginName] = array();
719            }
720
721            $coreProperties[$pluginName][] = array('name' => $fieldName, 'value' => $value);
722
723        } elseif (!empty($settingValues[$pluginName])) {
724            // we check if the value is defined in the setting values instead
725            foreach ($settingValues[$pluginName] as $key => $setting) {
726                if ($setting['name'] === $fieldName) {
727
728                    if (empty($coreProperties[$pluginName])) {
729                        $coreProperties[$pluginName] = array();
730                    }
731
732                    $coreProperties[$pluginName][] = array('name' => $fieldName, 'value' => $setting['value']);
733                    return $coreProperties;
734                }
735            }
736        }
737
738        return $coreProperties;
739    }
740
741    public function getSiteSettings($idSite)
742    {
743        Piwik::checkUserHasAdminAccess($idSite);
744
745        $measurableSettings = $this->settingsProvider->getAllMeasurableSettings($idSite, $idMeasurableType = false);
746
747        return $this->settingsMetadata->formatSettings($measurableSettings);
748    }
749
750    private function setAndValidateMeasurableSettings($idSite, $idType, $settingValues)
751    {
752        $measurableSettings = $this->settingsProvider->getAllMeasurableSettings($idSite, $idType);
753
754        $this->settingsMetadata->setPluginSettings($measurableSettings, $settingValues);
755
756        return $measurableSettings;
757    }
758
759    /**
760     * @param MeasurableSettings[] $measurableSettings
761     */
762    private function saveMeasurableSettings($idSite, $idType, $settingValues)
763    {
764        $measurableSettings = $this->setAndValidateMeasurableSettings($idSite, $idType, $settingValues);
765
766        foreach ($measurableSettings as $measurableSetting) {
767            $measurableSetting->save();
768        }
769    }
770
771    private function postUpdateWebsite($idSite)
772    {
773        Site::clearCache();
774        Cache::regenerateCacheWebsiteAttributes($idSite);
775        Cache::clearCacheGeneral();
776        SiteUrls::clearSitesCache();
777    }
778
779    /**
780     * Delete a website from the database, given its Id. The method deletes the actual site as well as some associated
781     * data. However, it does not delete any logs or archives that belong to this website. You can delete logs and
782     * archives for a site manually as described in this FAQ: http://matomo.org/faq/how-to/faq_73/ .
783     *
784     * Requires Super User access.
785     *
786     * @param int $idSite
787     * @throws Exception
788     */
789    public function deleteSite($idSite)
790    {
791        Piwik::checkUserHasSuperUserAccess();
792        SitesManager::dieIfSitesAdminIsDisabled();
793
794        $idSites = $this->getSitesId();
795        if (!in_array($idSite, $idSites)) {
796            throw new Exception("website id = $idSite not found");
797        }
798        $nbSites = count($idSites);
799        if ($nbSites == 1) {
800            throw new Exception($this->translator->translate("SitesManager_ExceptionDeleteSite"));
801        }
802
803        $this->getModel()->deleteSite($idSite);
804
805        $coreModel = new CoreModel();
806        $coreModel->deleteInvalidationsForSites([$idSite]);
807
808        /**
809         * Triggered after a site has been deleted.
810         *
811         * Plugins can use this event to remove site specific values or settings, such as removing all
812         * goals that belong to a specific website. If you store any data related to a website you
813         * should clean up that information here.
814         *
815         * @param int $idSite The ID of the site being deleted.
816         */
817        Piwik::postEvent('SitesManager.deleteSite.end', array($idSite));
818    }
819
820    private function checkValidTimezone($timezone)
821    {
822        $timezones = $this->getTimezonesList();
823        foreach (array_values($timezones) as $cities) {
824            foreach ($cities as $timezoneId => $city) {
825                if ($timezoneId == $timezone) {
826                    return true;
827                }
828            }
829        }
830        throw new Exception($this->translator->translate('SitesManager_ExceptionInvalidTimezone', array($timezone)));
831    }
832
833    private function checkValidCurrency($currency)
834    {
835        if (!in_array($currency, array_keys($this->getCurrencyList()))) {
836            throw new Exception($this->translator->translate('SitesManager_ExceptionInvalidCurrency', array($currency, "USD, EUR, etc.")));
837        }
838    }
839
840    private function checkAndReturnType($type)
841    {
842        if (empty($type)) {
843            $type = Site::DEFAULT_SITE_TYPE;
844        }
845
846        if (!is_string($type)) {
847            throw new Exception("Invalid website type $type");
848        }
849
850        return $type;
851    }
852
853    /**
854     * Checks that the submitted IPs (comma separated list) are valid
855     * Returns the cleaned up IPs
856     *
857     * @param string $excludedIps Comma separated list of IP addresses
858     * @throws Exception
859     * @return array of IPs
860     */
861    private function checkAndReturnExcludedIps($excludedIps)
862    {
863        if (empty($excludedIps)) {
864            return '';
865        }
866
867        $ips = explode(',', $excludedIps);
868        $ips = array_map('trim', $ips);
869        $ips = array_filter($ips, 'strlen');
870
871        foreach ($ips as $ip) {
872            if (!$this->isValidIp($ip)) {
873                throw new Exception($this->translator->translate('SitesManager_ExceptionInvalidIPFormat', array($ip, "1.2.3.4, 1.2.3.*, or 1.2.3.4/5")));
874            }
875        }
876
877        $ips = implode(',', $ips);
878        return $ips;
879    }
880
881    /**
882     * Add a list of alias Urls to the given idSite
883     *
884     * If some URLs given in parameter are already recorded as alias URLs for this website,
885     * they won't be duplicated. The 'main_url' of the website won't be affected by this method.
886     *
887     * @param int $idSite
888     * @param array|string $urls When calling API via HTTP specify multiple URLs via `&urls[]=http...&urls[]=http...`.
889     * @return int the number of inserted URLs
890     */
891    public function addSiteAliasUrls($idSite, $urls)
892    {
893        Piwik::checkUserHasAdminAccess($idSite);
894
895        if (empty($urls)) {
896            return 0;
897        }
898
899        if (!is_array($urls)) {
900            $urls = array($urls);
901        }
902
903        $urlsInit = $this->getSiteUrlsFromId($idSite);
904        $toInsert = array_merge($urlsInit, $urls);
905
906        $urlsProperty = new Urls($idSite);
907        $urlsProperty->setValue($toInsert);
908        $urlsProperty->save();
909
910        $inserted = array_diff($urlsProperty->getValue(), $urlsInit);
911
912        $this->postUpdateWebsite($idSite);
913
914        return count($inserted);
915    }
916
917    /**
918     * Set the list of alias Urls for the given idSite
919     *
920     * Completely overwrites the current list of URLs with the provided list.
921     * The 'main_url' of the website won't be affected by this method.
922     *
923     * @return int the number of inserted URLs
924     */
925    public function setSiteAliasUrls($idSite, $urls = array())
926    {
927        Piwik::checkUserHasAdminAccess($idSite);
928
929        $mainUrl = Site::getMainUrlFor($idSite);
930        array_unshift($urls, $mainUrl);
931
932        $urlsProperty = new Urls($idSite);
933        $urlsProperty->setValue($urls);
934        $urlsProperty->save();
935
936        $inserted = array_diff($urlsProperty->getValue(), $urls);
937
938        $this->postUpdateWebsite($idSite);
939
940        return count($inserted);
941    }
942
943    /**
944     * Get the start and end IP addresses for an IP address range
945     *
946     * @param string $ipRange IP address range in presentation format
947     * @return array|false Array( low, high ) IP addresses in presentation format; or false if error
948     */
949    public function getIpsForRange($ipRange)
950    {
951        $range = IPUtils::getIPRangeBounds($ipRange);
952        if ($range === null) {
953            return false;
954        }
955
956        return array(IPUtils::binaryToStringIP($range[0]), IPUtils::binaryToStringIP($range[1]));
957    }
958
959    /**
960     * Sets IPs to be excluded from all websites. IPs can contain wildcards.
961     * Will also apply to websites created in the future.
962     *
963     * @param string $excludedIps Comma separated list of IPs to exclude from being tracked (allows wildcards)
964     * @return bool
965     */
966    public function setGlobalExcludedIps($excludedIps)
967    {
968        Piwik::checkUserHasSuperUserAccess();
969        $excludedIps = $this->checkAndReturnExcludedIps($excludedIps);
970        Option::set(self::OPTION_EXCLUDED_IPS_GLOBAL, $excludedIps);
971        Cache::deleteTrackerCache();
972        return true;
973    }
974
975    /**
976     * Sets Site Search keyword/category parameter names, to be used on websites which have not specified these values
977     * Expects Comma separated list of query params names
978     *
979     * @param string
980     * @param string
981     * @return bool
982     */
983    public function setGlobalSearchParameters($searchKeywordParameters, $searchCategoryParameters)
984    {
985        Piwik::checkUserHasSuperUserAccess();
986        Option::set(self::OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL, $searchKeywordParameters);
987        Option::set(self::OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL, $searchCategoryParameters);
988        Cache::deleteTrackerCache();
989        return true;
990    }
991
992    /**
993     * @return string Comma separated list of URL parameters
994     */
995    public function getSearchKeywordParametersGlobal()
996    {
997        Piwik::checkUserHasSomeAdminAccess();
998        $names = Option::get(self::OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL);
999        if ($names === false) {
1000            $names = self::DEFAULT_SEARCH_KEYWORD_PARAMETERS;
1001        }
1002        if (empty($names)) {
1003            $names = '';
1004        }
1005        return $names;
1006    }
1007
1008    /**
1009     * @return string Comma separated list of URL parameters
1010     */
1011    public function getSearchCategoryParametersGlobal()
1012    {
1013        Piwik::checkUserHasSomeAdminAccess();
1014        return Option::get(self::OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL);
1015    }
1016
1017    /**
1018     * Returns the list of URL query parameters that are excluded from all websites
1019     *
1020     * @return string Comma separated list of URL parameters
1021     */
1022    public function getExcludedQueryParametersGlobal()
1023    {
1024        Piwik::checkUserHasSomeViewAccess();
1025        return Option::get(self::OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL);
1026    }
1027
1028    /**
1029     * Returns the list of user agent substrings to look for when excluding visits for
1030     * all websites. If a visitor's user agent string contains one of these substrings,
1031     * their visits will not be included.
1032     *
1033     * @return string Comma separated list of strings.
1034     */
1035    public function getExcludedUserAgentsGlobal()
1036    {
1037        Piwik::checkUserHasSomeAdminAccess();
1038        return Option::get(self::OPTION_EXCLUDED_USER_AGENTS_GLOBAL);
1039    }
1040
1041    /**
1042     * Sets list of user agent substrings to look for when excluding visits. For more info,
1043     * @see getExcludedUserAgentsGlobal.
1044     *
1045     * @param string $excludedUserAgents Comma separated list of strings. Each element is trimmed,
1046     *                                   and empty strings are removed.
1047     */
1048    public function setGlobalExcludedUserAgents($excludedUserAgents)
1049    {
1050        Piwik::checkUserHasSuperUserAccess();
1051
1052        // update option
1053        $excludedUserAgents = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents);
1054        Option::set(self::OPTION_EXCLUDED_USER_AGENTS_GLOBAL, $excludedUserAgents);
1055
1056        // make sure tracker cache will reflect change
1057        Cache::deleteTrackerCache();
1058    }
1059
1060    /**
1061     * Returns true if the default behavior is to keep URL fragments when tracking,
1062     * false if otherwise.
1063     *
1064     * @return bool
1065     */
1066    public function getKeepURLFragmentsGlobal()
1067    {
1068        Piwik::checkUserHasSomeViewAccess();
1069        return (bool)Option::get(self::OPTION_KEEP_URL_FRAGMENTS_GLOBAL);
1070    }
1071
1072    /**
1073     * Sets whether the default behavior should be to keep URL fragments when
1074     * tracking or not.
1075     *
1076     * @param $enabled bool If true, the default behavior will be to keep URL
1077     *                      fragments when tracking. If false, the default
1078     *                      behavior will be to remove them.
1079     */
1080    public function setKeepURLFragmentsGlobal($enabled)
1081    {
1082        Piwik::checkUserHasSuperUserAccess();
1083
1084        // update option
1085        Option::set(self::OPTION_KEEP_URL_FRAGMENTS_GLOBAL, $enabled);
1086
1087        // make sure tracker cache will reflect change
1088        Cache::deleteTrackerCache();
1089    }
1090
1091    /**
1092     * Sets list of URL query parameters to be excluded on all websites.
1093     * Will also apply to websites created in the future.
1094     *
1095     * @param string $excludedQueryParameters Comma separated list of URL query parameters to exclude from URLs
1096     * @return bool
1097     */
1098    public function setGlobalExcludedQueryParameters($excludedQueryParameters)
1099    {
1100        Piwik::checkUserHasSuperUserAccess();
1101        $excludedQueryParameters = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters);
1102        Option::set(self::OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL, $excludedQueryParameters);
1103        Cache::deleteTrackerCache();
1104        return true;
1105    }
1106
1107    /**
1108     * Returns the list of IPs that are excluded from all websites
1109     *
1110     * @return string Comma separated list of IPs
1111     */
1112    public function getExcludedIpsGlobal()
1113    {
1114        Piwik::checkUserHasSomeAdminAccess();
1115        return Option::get(self::OPTION_EXCLUDED_IPS_GLOBAL);
1116    }
1117
1118    /**
1119     * Returns the default currency that will be set when creating a website through the API.
1120     *
1121     * @return string Currency ID eg. 'USD'
1122     */
1123    public function getDefaultCurrency()
1124    {
1125        Piwik::checkUserHasSomeAdminAccess();
1126        $defaultCurrency = Option::get(self::OPTION_DEFAULT_CURRENCY);
1127        if ($defaultCurrency) {
1128            return $defaultCurrency;
1129        }
1130        return 'USD';
1131    }
1132
1133    /**
1134     * Sets the default currency that will be used when creating websites
1135     *
1136     * @param string $defaultCurrency Currency code, eg. 'USD'
1137     * @return bool
1138     */
1139    public function setDefaultCurrency($defaultCurrency)
1140    {
1141        Piwik::checkUserHasSuperUserAccess();
1142        $this->checkValidCurrency($defaultCurrency);
1143        Option::set(self::OPTION_DEFAULT_CURRENCY, $defaultCurrency);
1144        return true;
1145    }
1146
1147    /**
1148     * Returns the default timezone that will be set when creating a website through the API.
1149     * Via the UI, if the default timezone is not UTC, it will be pre-selected in the drop down
1150     *
1151     * @return string Timezone eg. UTC+7 or Europe/Paris
1152     */
1153    public function getDefaultTimezone()
1154    {
1155        $defaultTimezone = Option::get(self::OPTION_DEFAULT_TIMEZONE);
1156        if ($defaultTimezone) {
1157            return $defaultTimezone;
1158        }
1159        return 'UTC';
1160    }
1161
1162    /**
1163     * Sets the default timezone that will be used when creating websites
1164     *
1165     * @param string $defaultTimezone Timezone string eg. Europe/Paris or UTC+8
1166     * @return bool
1167     */
1168    public function setDefaultTimezone($defaultTimezone)
1169    {
1170        Piwik::checkUserHasSuperUserAccess();
1171        $this->checkValidTimezone($defaultTimezone);
1172        Option::set(self::OPTION_DEFAULT_TIMEZONE, $defaultTimezone);
1173        return true;
1174    }
1175
1176    /**
1177     * Update an existing website.
1178     * If only one URL is specified then only the main url will be updated.
1179     * If several URLs are specified, both the main URL and the alias URLs will be updated.
1180     *
1181     * @param int $idSite website ID defining the website to edit
1182     * @param string $siteName website name
1183     * @param string|array $urls the website URLs
1184     *                           When calling API via HTTP specify multiple URLs via `&urls[]=http...&urls[]=http...`.
1185     * @param int $ecommerce Whether Ecommerce is enabled, 0 or 1
1186     * @param null|int $siteSearch Whether site search is enabled, 0 or 1
1187     * @param string $searchKeywordParameters Comma separated list of search keyword parameter names
1188     * @param string $searchCategoryParameters Comma separated list of search category parameter names
1189     * @param string $excludedIps Comma separated list of IPs to exclude from being tracked (allows wildcards)
1190     * @param null|string $excludedQueryParameters
1191     * @param string $timezone Timezone
1192     * @param string $currency Currency code
1193     * @param string $group Group name where this website belongs
1194     * @param string $startDate Date at which the statistics for this website will start. Defaults to today's date in YYYY-MM-DD format
1195     * @param null|string $excludedUserAgents
1196     * @param int|null $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they
1197     *                                   will be removed. If 0, the default global behavior will be used.
1198     * @param string $type The Website type, default value is "website"
1199     * @param array|null $settingValues JSON serialized settings eg {settingName: settingValue, ...}
1200     * @param bool|null $excludeUnknownUrls Track only URL matching one of website URLs
1201     * @throws Exception
1202     * @see getKeepURLFragmentsGlobal. If null, the existing value will
1203     *                                   not be modified.
1204     *
1205     * @return bool true on success
1206     */
1207    public function updateSite($idSite,
1208                               $siteName = null,
1209                               $urls = null,
1210                               $ecommerce = null,
1211                               $siteSearch = null,
1212                               $searchKeywordParameters = null,
1213                               $searchCategoryParameters = null,
1214                               $excludedIps = null,
1215                               $excludedQueryParameters = null,
1216                               $timezone = null,
1217                               $currency = null,
1218                               $group = null,
1219                               $startDate = null,
1220                               $excludedUserAgents = null,
1221                               $keepURLFragments = null,
1222                               $type = null,
1223                               $settingValues = null,
1224                               $excludeUnknownUrls = null)
1225    {
1226        Piwik::checkUserHasAdminAccess($idSite);
1227        SitesManager::dieIfSitesAdminIsDisabled();
1228
1229        $idSites = $this->getSitesId();
1230
1231        if (!in_array($idSite, $idSites)) {
1232            throw new Exception("website id = $idSite not found");
1233        }
1234
1235        // Build the SQL UPDATE based on specified updates to perform
1236        $bind = array();
1237
1238        if (!is_null($siteName)) {
1239            $this->checkName($siteName);
1240            $bind['name'] = $siteName;
1241        }
1242
1243        if (!isset($settingValues)) {
1244            $settingValues = array();
1245        }
1246
1247        if (empty($coreProperties)) {
1248            $coreProperties = array();
1249        }
1250
1251        $coreProperties = $this->setSettingValue('urls', $urls, $coreProperties, $settingValues);
1252        $coreProperties = $this->setSettingValue('group', $group, $coreProperties, $settingValues);
1253        $coreProperties = $this->setSettingValue('ecommerce', $ecommerce, $coreProperties, $settingValues);
1254        $coreProperties = $this->setSettingValue('sitesearch', $siteSearch, $coreProperties, $settingValues);
1255        $coreProperties = $this->setSettingValue('sitesearch_keyword_parameters', explode(',', $searchKeywordParameters ?? ''), $coreProperties, $settingValues);
1256        $coreProperties = $this->setSettingValue('sitesearch_category_parameters', explode(',', $searchCategoryParameters ?? ''), $coreProperties, $settingValues);
1257        $coreProperties = $this->setSettingValue('keep_url_fragment', $keepURLFragments, $coreProperties, $settingValues);
1258        $coreProperties = $this->setSettingValue('exclude_unknown_urls', $excludeUnknownUrls, $coreProperties, $settingValues);
1259        $coreProperties = $this->setSettingValue('excluded_ips', explode(',', $excludedIps ?? ''), $coreProperties, $settingValues);
1260        $coreProperties = $this->setSettingValue('excluded_parameters', explode(',', $excludedQueryParameters ?? ''), $coreProperties, $settingValues);
1261        $coreProperties = $this->setSettingValue('excluded_user_agents', explode(',', $excludedUserAgents ?? ''), $coreProperties, $settingValues);
1262
1263        if (isset($currency)) {
1264            $currency = trim($currency);
1265            $this->checkValidCurrency($currency);
1266            $bind['currency'] = $currency;
1267        }
1268        if (isset($timezone)) {
1269            $timezone = trim($timezone);
1270            $this->checkValidTimezone($timezone);
1271            $bind['timezone'] = $timezone;
1272        }
1273        if (isset($group)
1274            && Piwik::hasUserSuperUserAccess()
1275        ) {
1276            $bind['group'] = trim($group);
1277        }
1278        if (isset($startDate)) {
1279            $bind['ts_created'] = Date::factory($startDate)->getDatetime();
1280        }
1281
1282        if (isset($type)) {
1283            $bind['type'] = $this->checkAndReturnType($type);
1284        }
1285
1286        if (!empty($coreProperties)) {
1287            $this->setAndValidateMeasurableSettings($idSite, $idType = 'website', $coreProperties);
1288        }
1289
1290        if (!empty($settingValues)) {
1291            $this->setAndValidateMeasurableSettings($idSite, $idType = null, $settingValues);
1292        }
1293
1294        if (!empty($bind)) {
1295            $this->getModel()->updateSite($bind, $idSite);
1296        }
1297
1298        if (!empty($coreProperties)) {
1299            $this->saveMeasurableSettings($idSite, $idType = 'website', $coreProperties);
1300        }
1301
1302        if (!empty($settingValues)) {
1303            $this->saveMeasurableSettings($idSite, $idType = null, $settingValues);
1304        }
1305
1306        $this->postUpdateWebsite($idSite);
1307    }
1308
1309    /**
1310     * Updates the field ts_created for the specified websites.
1311     *
1312     * @param $idSites int Id Site to update ts_created
1313     * @param $minDate Date to set as creation date. To play it safe it will subtract one more day.
1314     *
1315     * @ignore
1316     */
1317    public function updateSiteCreatedTime($idSites, Date $minDate)
1318    {
1319        $idSites = Site::getIdSitesFromIdSitesString($idSites);
1320        Piwik::checkUserHasAdminAccess($idSites);
1321
1322        $minDateSql = $minDate->subDay(1)->getDatetime();
1323
1324        $this->getModel()->updateSiteCreatedTime($idSites, $minDateSql);
1325    }
1326
1327    private function checkAndReturnCommaSeparatedStringList($parameters)
1328    {
1329        $parameters = trim($parameters);
1330        if (empty($parameters)) {
1331            return '';
1332        }
1333
1334        $parameters = explode(',', $parameters);
1335        $parameters = array_map('trim', $parameters);
1336        $parameters = array_filter($parameters, 'strlen');
1337        $parameters = array_unique($parameters);
1338        return implode(',', $parameters);
1339    }
1340
1341    /**
1342     * Returns the list of supported currencies
1343     * @see getCurrencySymbols()
1344     * @return array ( currencyId => currencyName)
1345     */
1346    public function getCurrencyList()
1347    {
1348        /** @var CurrencyDataProvider $dataProvider */
1349        $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\CurrencyDataProvider');
1350        $currency = $dataProvider->getCurrencyList();
1351
1352        $return = array();
1353        foreach (array_keys($currency) as $currencyCode) {
1354            $return[$currencyCode] = $this->translator->translate('Intl_Currency_' . $currencyCode) .
1355              ' (' . $this->translator->translate('Intl_CurrencySymbol_' . $currencyCode) . ')';
1356        }
1357
1358        asort($return);
1359
1360        return $return;
1361    }
1362
1363    /**
1364     * Returns the list of currency symbols
1365     * @see getCurrencyList()
1366     * @return array( currencyId => currencySymbol )
1367     */
1368    public function getCurrencySymbols()
1369    {
1370        /** @var CurrencyDataProvider $dataProvider */
1371        $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\CurrencyDataProvider');
1372        $currencies =  $dataProvider->getCurrencyList();
1373
1374        return array_map(function ($a) {
1375            return $a[0];
1376        }, $currencies);
1377    }
1378
1379    /**
1380     * Return true if Timezone support is enabled on server
1381     *
1382     * @return bool
1383     */
1384    public function isTimezoneSupportEnabled()
1385    {
1386        Piwik::checkUserHasSomeViewAccess();
1387        return SettingsServer::isTimezoneSupportEnabled();
1388    }
1389
1390    /**
1391     * Returns the list of timezones supported.
1392     * Used for addSite and updateSite
1393     *
1394     * @return array of timezone strings
1395     */
1396    public function getTimezonesList()
1397    {
1398        if (!SettingsServer::isTimezoneSupportEnabled()) {
1399            return array('UTC' => $this->getTimezonesListUTCOffsets());
1400        }
1401
1402        $countries = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider')->getCountryList();
1403
1404        $return = array();
1405        $continents = array();
1406        foreach ($countries as $countryCode => $continentCode) {
1407            $countryCode = strtoupper($countryCode);
1408            $timezones = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $countryCode);
1409            foreach ($timezones as $timezone) {
1410                if (!isset($continents[$continentCode])) {
1411                    $continents[$continentCode] = $this->translator->translate('Intl_Continent_' . $continentCode);
1412                }
1413                $continent = $continents[$continentCode];
1414
1415                $return[$continent][$timezone] = $this->getTimezoneName($timezone, $countryCode, count($timezones) > 1);
1416            }
1417        }
1418
1419        // Sort by continent name and then by country name.
1420        ksort($return);
1421        foreach ($return as $continent => $countries) {
1422            asort($return[$continent]);
1423        }
1424
1425        $return['UTC'] = $this->getTimezonesListUTCOffsets();
1426        return $return;
1427    }
1428
1429    /**
1430     * Returns a user-friendly label for a timezone.
1431     * This is usually the country name of the timezone. For countries spanning multiple timezones,
1432     * a city/location name is added to avoid ambiguity.
1433     *
1434     * @param string $timezone a timezone, e.g. "Asia/Tokyo" or "America/Los_Angeles"
1435     * @param string $countryCode an upper-case country code (if not supplied, it will be looked up)
1436     * @param bool $multipleTimezonesInCountry whether there are multiple timezones in the country (if not supplied, it will be looked up)
1437     * @return string a timezone label, e.g. "Japan" or "United States - Los Angeles"
1438     */
1439    public function getTimezoneName($timezone, $countryCode = null, $multipleTimezonesInCountry = null)
1440    {
1441        if (substr($timezone, 0, 3) === 'UTC') {
1442            return $this->translator->translate('SitesManager_Format_Utc', str_replace(array('.25', '.5', '.75'), array(':15', ':30', ':45'), substr($timezone, 3)));
1443        }
1444
1445        if (!isset($countryCode)) {
1446            try {
1447                $zone = new DateTimeZone($timezone);
1448                $location = $zone->getLocation();
1449                if (isset($location['country_code']) && $location['country_code'] !== '??') {
1450                    $countryCode = $location['country_code'];
1451                }
1452            } catch (Exception $e) {
1453            }
1454        }
1455
1456        if (!$countryCode) {
1457            $timezoneExploded = explode('/', $timezone);
1458            return str_replace('_', ' ', end($timezoneExploded));
1459        }
1460
1461        if (!isset($multipleTimezonesInCountry)) {
1462            $timezonesInCountry = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $countryCode);
1463            $multipleTimezonesInCountry = (count($timezonesInCountry) > 1);
1464        }
1465
1466        $return = $this->translator->translate('Intl_Country_' . $countryCode);
1467
1468        if ($multipleTimezonesInCountry) {
1469            $translationId = 'Intl_Timezone_' . str_replace(array('_', '/'), array('', '_'), $timezone);
1470            $city = $this->translator->translate($translationId);
1471
1472            // Fall back to English identifier, if translation is missing due to differences in tzdata in different PHP versions.
1473            if ($city === $translationId) {
1474                $timezoneExploded = explode('/', $timezone);
1475                $city = str_replace('_', ' ', end($timezoneExploded));
1476            }
1477
1478            $return .= ' - ' . $city;
1479        }
1480
1481        return $return;
1482    }
1483
1484    private function getTimezonesListUTCOffsets()
1485    {
1486        // manually add the UTC offsets
1487        $GmtOffsets = array(-12, -11.5, -11, -10.5, -10, -9.5, -9, -8.5, -8, -7.5, -7, -6.5, -6, -5.5, -5, -4.5, -4, -3.5, -3, -2.5, -2, -1.5, -1, -0.5,
1488                            0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 5.75, 6, 6.5, 7, 7.5, 8, 8.5, 8.75, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.75, 13, 13.75, 14);
1489
1490        $return = array();
1491        foreach ($GmtOffsets as $offset) {
1492            $offset = Common::forceDotAsSeparatorForDecimalPoint($offset);
1493
1494            if ($offset > 0) {
1495                $offset = '+' . $offset;
1496            } elseif ($offset == 0) {
1497                $offset = '';
1498            }
1499            $timezone = 'UTC' . $offset;
1500            $return[$timezone] = $this->getTimezoneName($timezone);
1501        }
1502        return $return;
1503    }
1504
1505    /**
1506     * Returns the list of unique timezones from all configured sites.
1507     *
1508     * @return array ( string )
1509     */
1510    public function getUniqueSiteTimezones()
1511    {
1512        Piwik::checkUserHasSuperUserAccess();
1513
1514        return $this->getModel()->getUniqueSiteTimezones();
1515    }
1516
1517    /**
1518     * Remove the final slash in the URLs if found
1519     *
1520     * @param string $url
1521     * @return string the URL without the trailing slash
1522     */
1523    private function removeTrailingSlash($url)
1524    {
1525        // if there is a final slash, we take the URL without this slash (expected URL format)
1526        if (strlen($url) > 5
1527            && $url[strlen($url) - 1] == '/'
1528        ) {
1529            $url = substr($url, 0, strlen($url) - 1);
1530        }
1531
1532        return $url;
1533    }
1534
1535    /**
1536     * Tests if the URL is a valid URL
1537     *
1538     * @param string $url
1539     * @return bool
1540     */
1541    private function isValidUrl($url)
1542    {
1543        return UrlHelper::isLookLikeUrl($url);
1544    }
1545
1546    /**
1547     * Tests if the IP is a valid IP, allowing wildcards, except in the first octet.
1548     * Wildcards can only be used from right to left, ie. 1.1.*.* is allowed, but 1.1.*.1 is not.
1549     *
1550     * @param string $ip IP address
1551     * @return bool
1552     */
1553    private function isValidIp($ip)
1554    {
1555        return IPUtils::getIPRangeBounds($ip) !== null;
1556    }
1557
1558    /**
1559     * Check that the website name has a correct format.
1560     *
1561     * @param $siteName
1562     * @throws Exception
1563     */
1564    private function checkName($siteName)
1565    {
1566        if (empty($siteName)) {
1567            throw new Exception($this->translator->translate("SitesManager_ExceptionEmptyName"));
1568        }
1569    }
1570
1571    public function renameGroup($oldGroupName, $newGroupName)
1572    {
1573        Piwik::checkUserHasSuperUserAccess();
1574
1575        if ($oldGroupName == $newGroupName) {
1576            return true;
1577        }
1578
1579        $sitesHavingOldGroup = $this->getSitesFromGroup($oldGroupName);
1580
1581        foreach ($sitesHavingOldGroup as $site) {
1582            $this->updateSite($site['idsite'],
1583                              $siteName = null,
1584                              $urls = null,
1585                              $ecommerce = null,
1586                              $siteSearch = null,
1587                              $searchKeywordParameters = null,
1588                              $searchCategoryParameters = null,
1589                              $excludedIps = null,
1590                              $excludedQueryParameters = null,
1591                              $timezone = null,
1592                              $currency = null,
1593                              $newGroupName);
1594        }
1595
1596        return true;
1597    }
1598
1599    /**
1600     * Find websites matching the given pattern.
1601     *
1602     * Any website will be returned that matches the pattern in the name, URL or group.
1603     * To limit the number of returned sites you can either specify `filter_limit` as usual or `limit` which is
1604     * faster.
1605     *
1606     * @param string $pattern
1607     * @param int|false $limit
1608     * @return array
1609     */
1610    public function getPatternMatchSites($pattern, $limit = false)
1611    {
1612        $ids = $this->getSitesIdWithAtLeastViewAccess();
1613        if (empty($ids)) {
1614            return array();
1615        }
1616
1617        $sites = $this->getModel()->getPatternMatchSites($ids, $pattern, $limit);
1618
1619        foreach ($sites as &$site) {
1620            $this->enrichSite($site);
1621        }
1622
1623        $sites = Site::setSitesFromArray($sites);
1624
1625        return $sites;
1626    }
1627
1628    /**
1629     * Returns the number of websites to display per page.
1630     *
1631     * For example this is used in the All Websites Dashboard, in the Website Selector etc. If multiple websites are
1632     * shown somewhere, one should request this method to detect how many websites should be shown per page when
1633     * using paging. To use paging is always recommended since some installations have thousands of websites.
1634     *
1635     * @return int
1636     */
1637    public function getNumWebsitesToDisplayPerPage()
1638    {
1639        Piwik::checkUserHasSomeViewAccess();
1640
1641        return SettingsPiwik::getWebsitesCountToDisplay();
1642    }
1643
1644}
1645