Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <tsteur@users.noreply.github.com>2019-08-04 23:59:04 +0300
committerdiosmosis <diosmosis@users.noreply.github.com>2019-08-04 23:59:04 +0300
commit9877f0b86da4d004bcadc0d046ac0ae40ad82e94 (patch)
treedbdc49996ae617364cfe0dafc7e6437c171daf8a
parentc9c7a96e35952e3808c07e6325ffab0989dd5c84 (diff)
Support to cache the config file (#14617)
* support config cache * make cache work * minor tweak * add tests * update test
-rw-r--r--core/Config/Cache.php89
-rw-r--r--core/Config/IniFileChain.php33
-rw-r--r--tests/PHPUnit/Integration/Config/CacheTest.php113
-rw-r--r--tests/PHPUnit/Unit/Config/IniFileChainCacheTest.php161
4 files changed, 396 insertions, 0 deletions
diff --git a/core/Config/Cache.php b/core/Config/Cache.php
new file mode 100644
index 0000000000..aa0bf80e9e
--- /dev/null
+++ b/core/Config/Cache.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Config;
+
+use Piwik\Cache\Backend\File;
+use Piwik\Common;
+use Piwik\Filesystem;
+use Piwik\Piwik;
+use Piwik\Url;
+
+/**
+ * Exception thrown when the config file doesn't exist.
+ */
+class Cache extends File
+{
+ private $host = '';
+
+ public function __construct()
+ {
+ $this->host = $this->getHost();
+
+ // because the config is not yet loaded we cannot identify the instanceId...
+ // need to use the hostname
+ $dir = $this->makeCacheDir($this->host);
+
+ parent::__construct($dir);
+ }
+
+ private function makeCacheDir($host)
+ {
+ return PIWIK_INCLUDE_PATH . '/tmp/' . $host . '/cache/tracker';
+ }
+
+ public function isValidHost($mergedConfigSettings)
+ {
+ if (!isset($mergedConfigSettings['General']['trusted_hosts']) || !is_array($mergedConfigSettings['General']['trusted_hosts'])) {
+ return false;
+ }
+ // note: we do not support "enable_trusted_host_check" to keep things secure
+ return in_array($this->host, $mergedConfigSettings['General']['trusted_hosts'], true);
+ }
+
+ private function getHost()
+ {
+ $host = Url::getHost($checkIfTrusted = false);
+ $host = Url::getHostSanitized($host); // Remove any port number to get actual hostname
+ $host = Common::sanitizeInputValue($host);
+
+ if (empty($host)
+ || strpos($host, '..') !== false
+ || strpos($host, '\\') !== false
+ || strpos($host, '/') !== false) {
+ throw new \Exception('Unsupported host');
+ }
+
+ $this->host = $host;
+
+ return $host;
+ }
+
+ public function doDelete($id)
+ {
+ // when the config changes, we need to invalidate the config caches for all configured hosts as well, not only
+ // the currently trusted host
+ $hosts = Url::getTrustedHosts();
+ $initialDir = $this->directory;
+
+ foreach ($hosts as $host)
+ {
+ $dir = $this->makeCacheDir($host);
+ if (@is_dir($dir)) {
+ $this->directory = $dir;
+ $success = parent::doDelete($id);
+ if ($success) {
+ Piwik::postEvent('Core.configFileDeleted', array($this->getFilename($id)));
+ }
+ }
+ }
+
+ $this->directory = $initialDir;
+ }
+
+}
diff --git a/core/Config/IniFileChain.php b/core/Config/IniFileChain.php
index 1058f7016b..db087edca1 100644
--- a/core/Config/IniFileChain.php
+++ b/core/Config/IniFileChain.php
@@ -11,6 +11,7 @@ use Piwik\Common;
use Piwik\Ini\IniReader;
use Piwik\Ini\IniReadingException;
use Piwik\Ini\IniWriter;
+use Piwik\Url;
/**
* Manages a list of INI files where the settings in each INI file merge with or override the
@@ -34,6 +35,7 @@ use Piwik\Ini\IniWriter;
*/
class IniFileChain
{
+ const CONFIG_CACHE_KEY = 'config.ini';
/**
* Maps INI file names with their parsed contents. The order of the files signifies the order
* in the chain. Files with lower index are overwritten/merged with files w/ a higher index.
@@ -208,6 +210,18 @@ class IniFileChain
$this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile);
}
+ if (!empty($userSettingsFile) && !empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) {
+ $cache = new Cache();
+ $values = $cache->doFetch(self::CONFIG_CACHE_KEY);
+ if (!empty($values)
+ && isset($values['mergedSettings'])
+ && isset($values['settingsChain'])) {
+ $this->mergedSettings = $values['mergedSettings'];
+ $this->settingsChain = $values['settingsChain'];
+ return;
+ }
+ }
+
$reader = new IniReader();
foreach ($this->settingsChain as $file => $ignore) {
if (is_readable($file)) {
@@ -226,6 +240,20 @@ class IniFileChain
// remove reference to $this->settingsChain... otherwise dump() or compareElements() will never notice a difference
// on PHP 7+ as they would be always equal
$this->mergedSettings = $this->copy($merged);
+
+ if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])
+ && !empty($userSettingsFile)
+ && !empty($this->mergedSettings)
+ && !empty($this->settingsChain)) {
+
+ $ttlOneHour = 3600;
+ $cache = new Cache();
+ if ($cache->isValidHost($this->mergedSettings)) {
+ // we make sure to save the config only if the host is valid...
+ $data = array('mergedSettings' => $this->mergedSettings, 'settingsChain' => $this->settingsChain);
+ $cache->doSave(self::CONFIG_CACHE_KEY, $data, $ttlOneHour);
+ }
+ }
}
private function copy($merged)
@@ -474,6 +502,11 @@ class IniFileChain
private function dumpSettings($values, $header)
{
+ if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) {
+ $cache = new Cache();
+ $cache->doDelete(self::CONFIG_CACHE_KEY);
+ }
+
$values = $this->encodeValues($values);
$writer = new IniWriter();
diff --git a/tests/PHPUnit/Integration/Config/CacheTest.php b/tests/PHPUnit/Integration/Config/CacheTest.php
new file mode 100644
index 0000000000..34ecd9bc3f
--- /dev/null
+++ b/tests/PHPUnit/Integration/Config/CacheTest.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Integration\Config\Cache;
+
+use PHPUnit_Framework_TestCase;
+use Piwik\Config;
+use Piwik\Config\Cache;
+use Piwik\Config\IniFileChain;
+use Piwik\Tests\Integration\Settings\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class CacheTest extends IntegrationTestCase
+{
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ private $testHost = 'analytics.test.matomo.org';
+
+ public function setUp()
+ {
+ unset($GLOBALS['ENABLE_CONFIG_PHP_CACHE']);
+ $this->setTrustedHosts();
+ $_SERVER['HTTP_HOST'] = $this->testHost;
+ $this->cache = new Cache();
+ $this->cache->doDelete(IniFileChain::CONFIG_CACHE_KEY);
+ parent::setUp();
+ }
+
+ private function setTrustedHosts()
+ {
+ Config::setSetting('General', 'trusted_hosts', array($this->testHost, 'foonot.exists'));
+ }
+
+ public function tearDown()
+ {
+ $this->setTrustedHosts();
+ $this->cache->doDelete(IniFileChain::CONFIG_CACHE_KEY);
+ unset($_SERVER['HTTP_HOST']);
+ parent::tearDown();
+ }
+
+ public function test_doFetch_noValueSaved_shouldReturnFalse()
+ {
+ $noValue = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertFalse($noValue);
+ }
+
+ /**
+ * @dataProvider getRandmHosts
+ * @expectedException \Exception
+ * @expectedExceptionMessage Unsupported host
+ */
+ public function test_construct_failsWhenUsingRandomHost($host)
+ {
+ $_SERVER['HTTP_HOST'] = $host;
+ new Cache();
+ }
+
+ public function getRandmHosts()
+ {
+ return [
+ ['foo..test'],
+ ['foo\test'],
+ ['']
+ ];
+ }
+
+ public function test_doSave_doFetch_savesAndReadsData()
+ {
+ $value = array('mergedSettings' => 'foobar', 'settingsChain' => array('bar' => 'baz'));
+ $this->cache->doSave(IniFileChain::CONFIG_CACHE_KEY, $value, 60);
+ $this->assertEquals($value, $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY));
+
+ // also works when creating new instance to ensure it's read from file
+ $this->cache = new Cache();
+ $this->assertEquals($value, $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY));
+ }
+
+ public function test_doDelete()
+ {
+ $value = array('mergedSettings' => 'foobar', 'settingsChain' => array('bar' => 'baz'));
+ $this->cache->doSave(IniFileChain::CONFIG_CACHE_KEY, $value, 60);
+
+ $this->setTrustedHosts();
+
+ $this->assertEquals($value, $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY));
+
+ $this->cache->doDelete(IniFileChain::CONFIG_CACHE_KEY);
+
+ $this->assertFalse($this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY));
+
+ $cache = new Cache();
+ $this->assertFalse($cache->doFetch(IniFileChain::CONFIG_CACHE_KEY));
+ }
+
+ public function test_isValidHost()
+ {
+ $this->assertTrue($this->cache->isValidHost(array('General' => array('trusted_hosts' => array('foo.com', $this->testHost, 'bar.baz')))));
+ $this->assertFalse($this->cache->isValidHost(array('General' => array('trusted_hosts' => array('foo.com', 'bar.baz')))));
+ $this->assertFalse($this->cache->isValidHost(array('General' => array('trusted_hosts' => array()))));
+ $this->assertFalse($this->cache->isValidHost(array()));
+ }
+
+} \ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Config/IniFileChainCacheTest.php b/tests/PHPUnit/Unit/Config/IniFileChainCacheTest.php
new file mode 100644
index 0000000000..8b0d0de1c0
--- /dev/null
+++ b/tests/PHPUnit/Unit/Config/IniFileChainCacheTest.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Unit\Config;
+
+use Piwik\Config;
+use Piwik\Config\Cache;
+use Piwik\Config\IniFileChain;
+
+class TestIniFileChain extends IniFileChain
+{
+ public $addHostInfo = '';
+
+ public function __construct(array $defaultSettingsFiles = array(), $userSettingsFile = null, $addhostInfo = '')
+ {
+ $this->addHostInfo = $addhostInfo;
+ parent::__construct($defaultSettingsFiles, $userSettingsFile);
+ }
+
+ protected function mergeFileSettings()
+ {
+ $settings = parent::mergeFileSettings();
+
+ if (!empty($this->addHostInfo)) {
+ $settings['General'] = ['trusted_hosts'=> [$this->addHostInfo]];
+ }
+
+ return $settings;
+ }
+}
+
+/**
+ * @group Core
+ */
+class IniFileChainCacheTest extends IniFileChainTest
+{
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ private $testHost = 'mytest.matomo.org';
+
+ public function setUp()
+ {
+ $GLOBALS['ENABLE_CONFIG_PHP_CACHE'] = true;
+ $_SERVER['HTTP_HOST'] = $this->testHost;
+ $this->cache = new Cache();
+ parent::setUp();
+ $this->setTrustedHosts();
+ }
+
+ private function setTrustedHosts()
+ {
+ Config::setSetting('General', 'trusted_hosts', array($this->testHost, 'foonot.exists'));
+ }
+
+ public function tearDown()
+ {
+ $this->cache->doDelete(IniFileChain::CONFIG_CACHE_KEY);
+ unset($GLOBALS['ENABLE_CONFIG_PHP_CACHE']);
+ unset($_SERVER['HTTP_HOST']);
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider getMergingTestData
+ */
+ public function test_reload_shouldNotPopulateCacheWhenNoTrustedHostIsConfigured($testDescription, $defaultSettingFiles, $userSettingsFile, $expected)
+ {
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+
+ // reading the chain should populate the cache
+ $fileChain = new TestIniFileChain($defaultSettingFiles, $userSettingsFile);
+ $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed");
+
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+ }
+
+ /**
+ * @dataProvider getMergingTestData
+ */
+ public function test_reload_shouldNotPopulateCacheWhenTrustedHostIsNotValid($testDescription, $defaultSettingFiles, $userSettingsFile, $expected)
+ {
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+
+ // reading the chain should populate the cache
+ $fileChain = new TestIniFileChain($defaultSettingFiles, $userSettingsFile, 'foo.bar.com');
+ $expected['General'] = array('trusted_hosts' => array('foo.bar.com'));
+ $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed");
+
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+ }
+
+ /**
+ * @dataProvider getMergingTestData
+ */
+ public function test_reload_shoulPopulateCacheWhenTrustedHostIsValid($testDescription, $defaultSettingFiles, $userSettingsFile, $expected)
+ {
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+
+ // reading the chain should populate the cache
+ $fileChain = new TestIniFileChain($defaultSettingFiles, $userSettingsFile, $this->testHost);
+ $expected['General'] = array('trusted_hosts' => array($this->testHost));
+ $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed");
+
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $settingsChain = $value['settingsChain'];
+ unset($value['settingsChain']);
+ $this->assertEquals(array('mergedSettings' => $expected), $value);
+
+ $this->assertArraySubset($defaultSettingFiles, array_keys($settingsChain));
+ $this->assertNotEmpty(array_keys($settingsChain));
+ }
+
+ /**
+ * @dataProvider getMergingTestData
+ */
+ public function test_reload_canReadFromCache($testDescription, $defaultSettingFiles, $userSettingsFile, $expected)
+ {
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+
+ // reading the chain should populate the cache
+ $fileChain = new TestIniFileChain($defaultSettingFiles, $userSettingsFile, $this->testHost);
+ $expected['General'] = array('trusted_hosts' => array($this->testHost));
+ $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed");
+
+ // even though the passed config files don't exist it still returns the same result as it is fetched from
+ // cache
+ $testChain = new TestIniFileChain(array('foo'), 'bar');
+ $this->assertEquals($expected, $testChain->getAll(), "'$testDescription' failed");
+ }
+
+ /**
+ * @dataProvider getMergingTestData
+ */
+ public function test_populateCache_DeleteCache($testDescription, $defaultSettingFiles, $userSettingsFile, $expected)
+ {
+ $this->test_reload_shoulPopulateCacheWhenTrustedHostIsValid($testDescription, $defaultSettingFiles, $userSettingsFile, $expected);
+
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertNotEmpty($value);
+
+ // dumping the cache should delete it
+ $fileChain = new TestIniFileChain($defaultSettingFiles, $userSettingsFile);
+ $fileChain->dump('');
+
+ $value = $this->cache->doFetch(IniFileChain::CONFIG_CACHE_KEY);
+ $this->assertEquals(false, $value);
+ }
+} \ No newline at end of file