diff options
author | Thomas Steur <tsteur@users.noreply.github.com> | 2019-08-04 23:59:04 +0300 |
---|---|---|
committer | diosmosis <diosmosis@users.noreply.github.com> | 2019-08-04 23:59:04 +0300 |
commit | 9877f0b86da4d004bcadc0d046ac0ae40ad82e94 (patch) | |
tree | dbdc49996ae617364cfe0dafc7e6437c171daf8a | |
parent | c9c7a96e35952e3808c07e6325ffab0989dd5c84 (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.php | 89 | ||||
-rw-r--r-- | core/Config/IniFileChain.php | 33 | ||||
-rw-r--r-- | tests/PHPUnit/Integration/Config/CacheTest.php | 113 | ||||
-rw-r--r-- | tests/PHPUnit/Unit/Config/IniFileChainCacheTest.php | 161 |
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 |