Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Config.php 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. /**
  12. * Singleton that provides read & write access to Piwik's INI configuration.
  13. *
  14. * This class reads and writes to the `config/config.ini.php` file. If config
  15. * options are missing from that file, this class will look for their default
  16. * values in `config/global.ini.php`.
  17. *
  18. * ### Examples
  19. *
  20. * **Getting a value:**
  21. *
  22. * // read the minimum_memory_limit option under the [General] section
  23. * $minValue = Config::getInstance()->General['minimum_memory_limit'];
  24. *
  25. * **Setting a value:**
  26. *
  27. * // set the minimum_memory_limit option
  28. * Config::getInstance()->General['minimum_memory_limit'] = 256;
  29. * Config::getInstance()->forceSave();
  30. *
  31. * **Setting an entire section:**
  32. *
  33. * Config::getInstance()->MySection = array('myoption' => 1);
  34. * Config::getInstance()->forceSave();
  35. *
  36. * @method static \Piwik\Config getInstance()
  37. */
  38. class Config extends Singleton
  39. {
  40. const DEFAULT_LOCAL_CONFIG_PATH = '/config/config.ini.php';
  41. const DEFAULT_COMMON_CONFIG_PATH = '/config/common.config.ini.php';
  42. const DEFAULT_GLOBAL_CONFIG_PATH = '/config/global.ini.php';
  43. /**
  44. * Contains configuration files values
  45. *
  46. * @var array
  47. */
  48. protected $initialized = false;
  49. protected $configGlobal = array();
  50. protected $configLocal = array();
  51. protected $configCommon = array();
  52. protected $configCache = array();
  53. protected $pathGlobal = null;
  54. protected $pathCommon = null;
  55. protected $pathLocal = null;
  56. /**
  57. * @var boolean
  58. */
  59. protected $isTest = false;
  60. /**
  61. * Constructor
  62. */
  63. public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
  64. {
  65. $this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath();
  66. $this->pathCommon = $pathCommon ?: self::getCommonConfigPath();
  67. $this->pathLocal = $pathLocal ?: self::getLocalConfigPath();
  68. }
  69. /**
  70. * Returns the path to the local config file used by this instance.
  71. *
  72. * @return string
  73. */
  74. public function getLocalPath()
  75. {
  76. return $this->pathLocal;
  77. }
  78. /**
  79. * Returns the path to the global config file used by this instance.
  80. *
  81. * @return string
  82. */
  83. public function getGlobalPath()
  84. {
  85. return $this->pathGlobal;
  86. }
  87. /**
  88. * Returns the path to the common config file used by this instance.
  89. *
  90. * @return string
  91. */
  92. public function getCommonPath()
  93. {
  94. return $this->pathCommon;
  95. }
  96. /**
  97. * Enable test environment
  98. *
  99. * @param string $pathLocal
  100. * @param string $pathGlobal
  101. * @param string $pathCommon
  102. */
  103. public function setTestEnvironment($pathLocal = null, $pathGlobal = null, $pathCommon = null, $allowSaving = false)
  104. {
  105. if (!$allowSaving) {
  106. $this->isTest = true;
  107. }
  108. $this->clear();
  109. $this->pathLocal = $pathLocal ?: Config::getLocalConfigPath();
  110. $this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
  111. $this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
  112. $this->init();
  113. // this proxy will not record any data in the production database.
  114. // this provides security for Piwik installs and tests were setup.
  115. if (isset($this->configGlobal['database_tests'])
  116. || isset($this->configLocal['database_tests'])
  117. ) {
  118. $this->__get('database_tests');
  119. $this->configCache['database'] = $this->configCache['database_tests'];
  120. }
  121. // Ensure local mods do not affect tests
  122. if (empty($pathGlobal)) {
  123. $this->configCache['Debug'] = $this->configGlobal['Debug'];
  124. $this->configCache['mail'] = $this->configGlobal['mail'];
  125. $this->configCache['General'] = $this->configGlobal['General'];
  126. $this->configCache['Segments'] = $this->configGlobal['Segments'];
  127. $this->configCache['Tracker'] = $this->configGlobal['Tracker'];
  128. $this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs'];
  129. $this->configCache['Deletereports'] = $this->configGlobal['Deletereports'];
  130. $this->configCache['Development'] = $this->configGlobal['Development'];
  131. }
  132. // for unit tests, we set that no plugin is installed. This will force
  133. // the test initialization to create the plugins tables, execute ALTER queries, etc.
  134. $this->configCache['PluginsInstalled'] = array('PluginsInstalled' => array());
  135. }
  136. /**
  137. * Returns absolute path to the global configuration file
  138. *
  139. * @return string
  140. */
  141. protected static function getGlobalConfigPath()
  142. {
  143. return PIWIK_USER_PATH . self::DEFAULT_GLOBAL_CONFIG_PATH;
  144. }
  145. /**
  146. * Returns absolute path to the common configuration file.
  147. *
  148. * @return string
  149. */
  150. public static function getCommonConfigPath()
  151. {
  152. return PIWIK_USER_PATH . self::DEFAULT_COMMON_CONFIG_PATH;
  153. }
  154. /**
  155. * Returns absolute path to the local configuration file
  156. *
  157. * @return string
  158. */
  159. public static function getLocalConfigPath()
  160. {
  161. $path = self::getByDomainConfigPath();
  162. if ($path) {
  163. return $path;
  164. }
  165. return PIWIK_USER_PATH . self::DEFAULT_LOCAL_CONFIG_PATH;
  166. }
  167. private static function getLocalConfigInfoForHostname($hostname)
  168. {
  169. // Remove any port number to get actual hostname
  170. $hostname = Url::getHostSanitized($hostname);
  171. $perHostFilename = $hostname . '.config.ini.php';
  172. $pathDomainConfig = PIWIK_USER_PATH . '/config/' . $perHostFilename;
  173. return array('file' => $perHostFilename, 'path' => $pathDomainConfig);
  174. }
  175. public function getConfigHostnameIfSet()
  176. {
  177. if ($this->getByDomainConfigPath() === false) {
  178. return false;
  179. }
  180. return $this->getHostname();
  181. }
  182. public function getClientSideOptions()
  183. {
  184. $general = $this->General;
  185. return array(
  186. 'action_url_category_delimiter' => $general['action_url_category_delimiter'],
  187. 'autocomplete_min_sites' => $general['autocomplete_min_sites'],
  188. 'datatable_export_range_as_day' => $general['datatable_export_range_as_day'],
  189. 'datatable_row_limits' => $this->getDatatableRowLimits()
  190. );
  191. }
  192. /**
  193. * @param $general
  194. * @return mixed
  195. */
  196. private function getDatatableRowLimits()
  197. {
  198. $limits = $this->General['datatable_row_limits'];
  199. $limits = explode(",", $limits);
  200. $limits = array_map('trim', $limits);
  201. return $limits;
  202. }
  203. protected static function getByDomainConfigPath()
  204. {
  205. $host = self::getHostname();
  206. $hostConfig = self::getLocalConfigInfoForHostname($host);
  207. if (Filesystem::isValidFilename($hostConfig['file'])
  208. && file_exists($hostConfig['path'])
  209. ) {
  210. return $hostConfig['path'];
  211. }
  212. return false;
  213. }
  214. /**
  215. * Returns the hostname of the current request (without port number)
  216. *
  217. * @return string
  218. */
  219. public static function getHostname()
  220. {
  221. // Check trusted requires config file which is not ready yet
  222. $host = Url::getHost($checkIfTrusted = false);
  223. // Remove any port number to get actual hostname
  224. $host = Url::getHostSanitized($host);
  225. return $host;
  226. }
  227. /**
  228. * If set, Piwik will use the hostname config no matter if it exists or not. Useful for instance if you want to
  229. * create a new hostname config:
  230. *
  231. * $config = Config::getInstance();
  232. * $config->forceUsageOfHostnameConfig('piwik.example.com');
  233. * $config->save();
  234. *
  235. * @param string $hostname eg piwik.example.com
  236. * @return string
  237. * @throws \Exception In case the domain contains not allowed characters
  238. */
  239. public function forceUsageOfLocalHostnameConfig($hostname)
  240. {
  241. $hostConfig = static::getLocalConfigInfoForHostname($hostname);
  242. if (!Filesystem::isValidFilename($hostConfig['file'])) {
  243. throw new Exception('Hostname is not valid');
  244. }
  245. $this->pathLocal = $hostConfig['path'];
  246. $this->configLocal = array();
  247. $this->initialized = false;
  248. return $this->pathLocal;
  249. }
  250. /**
  251. * Returns `true` if the local configuration file is writable.
  252. *
  253. * @return bool
  254. */
  255. public function isFileWritable()
  256. {
  257. return is_writable($this->pathLocal);
  258. }
  259. /**
  260. * Clear in-memory configuration so it can be reloaded
  261. */
  262. public function clear()
  263. {
  264. $this->configGlobal = array();
  265. $this->configLocal = array();
  266. $this->configCache = array();
  267. $this->initialized = false;
  268. }
  269. /**
  270. * Read configuration from files into memory
  271. *
  272. * @throws Exception if local config file is not readable; exits for other errors
  273. */
  274. public function init()
  275. {
  276. $this->initialized = true;
  277. $reportError = SettingsServer::isTrackerApiRequest();
  278. // read defaults from global.ini.php
  279. if (!is_readable($this->pathGlobal) && $reportError) {
  280. Piwik_ExitWithMessage(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal)));
  281. }
  282. $this->configGlobal = _parse_ini_file($this->pathGlobal, true);
  283. if (empty($this->configGlobal) && $reportError) {
  284. Piwik_ExitWithMessage(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
  285. }
  286. $this->configCommon = _parse_ini_file($this->pathCommon, true);
  287. // Check config.ini.php last
  288. $this->checkLocalConfigFound();
  289. $this->configLocal = _parse_ini_file($this->pathLocal, true);
  290. if (empty($this->configLocal) && $reportError) {
  291. Piwik_ExitWithMessage(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
  292. }
  293. }
  294. public function existsLocalConfig()
  295. {
  296. return is_readable($this->pathLocal);
  297. }
  298. public function deleteLocalConfig()
  299. {
  300. $configLocal = $this->getLocalPath();
  301. unlink($configLocal);
  302. }
  303. public function checkLocalConfigFound()
  304. {
  305. if (!$this->existsLocalConfig()) {
  306. throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathLocal)));
  307. }
  308. }
  309. /**
  310. * Decode HTML entities
  311. *
  312. * @param mixed $values
  313. * @return mixed
  314. */
  315. protected function decodeValues($values)
  316. {
  317. if (is_array($values)) {
  318. foreach ($values as &$value) {
  319. $value = $this->decodeValues($value);
  320. }
  321. return $values;
  322. }
  323. return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
  324. }
  325. /**
  326. * Encode HTML entities
  327. *
  328. * @param mixed $values
  329. * @return mixed
  330. */
  331. protected function encodeValues($values)
  332. {
  333. if (is_array($values)) {
  334. foreach ($values as &$value) {
  335. $value = $this->encodeValues($value);
  336. }
  337. } else {
  338. $values = htmlentities($values, ENT_COMPAT, 'UTF-8');
  339. $values = str_replace('$', '&#36;', $values);
  340. }
  341. return $values;
  342. }
  343. /**
  344. * Returns a configuration value or section by name.
  345. *
  346. * @param string $name The value or section name.
  347. * @return string|array The requested value requested. Returned by reference.
  348. * @throws Exception If the value requested not found in either `config.ini.php` or
  349. * `global.ini.php`.
  350. * @api
  351. */
  352. public function &__get($name)
  353. {
  354. if (!$this->initialized) {
  355. $this->init();
  356. // must be called here, not in init(), since setTestEnvironment() calls init(). (this avoids
  357. // infinite recursion)
  358. Piwik::postTestEvent('Config.createConfigSingleton',
  359. array($this, &$this->configCache, &$this->configLocal));
  360. }
  361. // check cache for merged section
  362. if (isset($this->configCache[$name])) {
  363. $tmp =& $this->configCache[$name];
  364. return $tmp;
  365. }
  366. $section = $this->getFromGlobalConfig($name);
  367. $sectionCommon = $this->getFromCommonConfig($name);
  368. if(empty($section) && !empty($sectionCommon)) {
  369. $section = $sectionCommon;
  370. } elseif(!empty($section) && !empty($sectionCommon)) {
  371. $section = $this->array_merge_recursive_distinct($section, $sectionCommon);
  372. }
  373. if (isset($this->configLocal[$name])) {
  374. // local settings override the global defaults
  375. $section = $section
  376. ? array_merge($section, $this->configLocal[$name])
  377. : $this->configLocal[$name];
  378. }
  379. if ($section === null && $name = 'superuser') {
  380. $user = $this->getConfigSuperUserForBackwardCompatibility();
  381. return $user;
  382. } else if ($section === null) {
  383. throw new Exception("Error while trying to read a specific config file entry <strong>'$name'</strong> from your configuration files.</b>If you just completed a Piwik upgrade, please check that the file config/global.ini.php was overwritten by the latest Piwik version.");
  384. }
  385. // cache merged section for later
  386. $this->configCache[$name] = $this->decodeValues($section);
  387. $tmp =& $this->configCache[$name];
  388. return $tmp;
  389. }
  390. /**
  391. * @deprecated since version 2.0.4
  392. */
  393. public function getConfigSuperUserForBackwardCompatibility()
  394. {
  395. try {
  396. $db = Db::get();
  397. $user = $db->fetchRow("SELECT login, email, password
  398. FROM " . Common::prefixTable("user") . "
  399. WHERE superuser_access = 1
  400. ORDER BY date_registered ASC LIMIT 1");
  401. if (!empty($user)) {
  402. $user['bridge'] = 1;
  403. return $user;
  404. }
  405. } catch (Exception $e) {}
  406. return array();
  407. }
  408. public function getFromGlobalConfig($name)
  409. {
  410. if (isset($this->configGlobal[$name])) {
  411. return $this->configGlobal[$name];
  412. }
  413. return null;
  414. }
  415. public function getFromCommonConfig($name)
  416. {
  417. if (isset($this->configCommon[$name])) {
  418. return $this->configCommon[$name];
  419. }
  420. return null;
  421. }
  422. /**
  423. * Sets a configuration value or section.
  424. *
  425. * @param string $name This section name or value name to set.
  426. * @param mixed $value
  427. * @api
  428. */
  429. public function __set($name, $value)
  430. {
  431. $this->configCache[$name] = $value;
  432. }
  433. /**
  434. * Comparison function
  435. *
  436. * @param mixed $elem1
  437. * @param mixed $elem2
  438. * @return int;
  439. */
  440. public static function compareElements($elem1, $elem2)
  441. {
  442. if (is_array($elem1)) {
  443. if (is_array($elem2)) {
  444. return strcmp(serialize($elem1), serialize($elem2));
  445. }
  446. return 1;
  447. }
  448. if (is_array($elem2)) {
  449. return -1;
  450. }
  451. if ((string)$elem1 === (string)$elem2) {
  452. return 0;
  453. }
  454. return ((string)$elem1 > (string)$elem2) ? 1 : -1;
  455. }
  456. /**
  457. * Compare arrays and return difference, such that:
  458. *
  459. * $modified = array_merge($original, $difference);
  460. *
  461. * @param array $original original array
  462. * @param array $modified modified array
  463. * @return array differences between original and modified
  464. */
  465. public function array_unmerge($original, $modified)
  466. {
  467. // return key/value pairs for keys in $modified but not in $original
  468. // return key/value pairs for keys in both $modified and $original, but values differ
  469. // ignore keys that are in $original but not in $modified
  470. return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements'));
  471. }
  472. /**
  473. * Dump config
  474. *
  475. * @param array $configLocal
  476. * @param array $configGlobal
  477. * @param array $configCommon
  478. * @param array $configCache
  479. * @return string
  480. */
  481. public function dumpConfig($configLocal, $configGlobal, $configCommon, $configCache)
  482. {
  483. $dirty = false;
  484. $output = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
  485. $output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
  486. if (!$configCache) {
  487. return false;
  488. }
  489. // If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values
  490. if(!empty($configCommon)) {
  491. $configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon);
  492. }
  493. if ($configLocal) {
  494. foreach ($configLocal as $name => $section) {
  495. if (!isset($configCache[$name])) {
  496. $configCache[$name] = $this->decodeValues($section);
  497. }
  498. }
  499. }
  500. $sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache)));
  501. foreach ($sectionNames as $section) {
  502. if (!isset($configCache[$section])) {
  503. continue;
  504. }
  505. // Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php)
  506. // get local and cached config
  507. $local = isset($configLocal[$section]) ? $configLocal[$section] : array();
  508. $config = $configCache[$section];
  509. // remove default values from both (they should not get written to local)
  510. if (isset($configGlobal[$section])) {
  511. $config = $this->array_unmerge($configGlobal[$section], $configCache[$section]);
  512. $local = $this->array_unmerge($configGlobal[$section], $local);
  513. }
  514. // if either local/config have non-default values and the other doesn't,
  515. // OR both have values, but different values, we must write to config.ini.php
  516. if (empty($local) xor empty($config)
  517. || (!empty($local)
  518. && !empty($config)
  519. && self::compareElements($config, $configLocal[$section]))
  520. ) {
  521. $dirty = true;
  522. }
  523. // no point in writing empty sections, so skip if the cached section is empty
  524. if (empty($config)) {
  525. continue;
  526. }
  527. $output .= "[$section]\n";
  528. foreach ($config as $name => $value) {
  529. $value = $this->encodeValues($value);
  530. if (is_numeric($name)) {
  531. $name = $section;
  532. $value = array($value);
  533. }
  534. if (is_array($value)) {
  535. foreach ($value as $currentValue) {
  536. $output .= $name . "[] = \"$currentValue\"\n";
  537. }
  538. } else {
  539. if (!is_numeric($value)) {
  540. $value = "\"$value\"";
  541. }
  542. $output .= $name . ' = ' . $value . "\n";
  543. }
  544. }
  545. $output .= "\n";
  546. }
  547. if ($dirty) {
  548. return $output;
  549. }
  550. return false;
  551. }
  552. /**
  553. * Write user configuration file
  554. *
  555. * @param array $configLocal
  556. * @param array $configGlobal
  557. * @param array $configCommon
  558. * @param array $configCache
  559. * @param string $pathLocal
  560. * @param bool $clear
  561. *
  562. * @throws \Exception if config file not writable
  563. */
  564. protected function writeConfig($configLocal, $configGlobal, $configCommon, $configCache, $pathLocal, $clear = true)
  565. {
  566. if ($this->isTest) {
  567. return;
  568. }
  569. $output = $this->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache);
  570. if ($output !== false) {
  571. $success = @file_put_contents($pathLocal, $output);
  572. if (!$success) {
  573. throw $this->getConfigNotWritableException();
  574. }
  575. }
  576. if ($clear) {
  577. $this->clear();
  578. }
  579. }
  580. /**
  581. * Writes the current configuration to the **config.ini.php** file. Only writes options whose
  582. * values are different from the default.
  583. *
  584. * @api
  585. */
  586. public function forceSave()
  587. {
  588. $this->writeConfig($this->configLocal, $this->configGlobal, $this->configCommon, $this->configCache, $this->pathLocal);
  589. }
  590. /**
  591. * @throws \Exception
  592. */
  593. public function getConfigNotWritableException()
  594. {
  595. $path = "config/" . basename($this->pathLocal);
  596. return new Exception(Piwik::translate('General_ConfigFileIsNotWritable', array("(" . $path . ")", "")));
  597. }
  598. /**
  599. * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
  600. * keys to arrays rather than overwriting the value in the first array with the duplicate
  601. * value in the second array, as array_merge does. I.e., with array_merge_recursive,
  602. * this happens (documented behavior):
  603. *
  604. * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
  605. * => array('key' => array('org value', 'new value'));
  606. *
  607. * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
  608. * Matching keys' values in the second array overwrite those in the first array, as is the
  609. * case with array_merge, i.e.:
  610. *
  611. * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
  612. * => array('key' => array('new value'));
  613. *
  614. * Parameters are passed by reference, though only for performance reasons. They're not
  615. * altered by this function.
  616. *
  617. * @param array $array1
  618. * @param array $array2
  619. * @return array
  620. * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
  621. * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
  622. */
  623. function array_merge_recursive_distinct ( array &$array1, array &$array2 )
  624. {
  625. $merged = $array1;
  626. foreach ( $array2 as $key => &$value ) {
  627. if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) ) {
  628. $merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value );
  629. } else {
  630. $merged [$key] = $value;
  631. }
  632. }
  633. return $merged;
  634. }
  635. }