Commit d89dfb02 authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski

Check cache integrity across execution contexts

(#5465 adapted to master)
parent 512895a8
......@@ -5,6 +5,7 @@
/config/parameters.yaml
/tests/config_db*
/tests/db.yaml
/tests/parameters.yaml
/plugins/
/files/
/.buildpath
......
......@@ -1516,7 +1516,7 @@ class Config extends CommonDBTM {
function showPerformanceInformations() {
global $CONTAINER;
$cache_storage = $CONTAINER->get('application_cache_storage');
$cache_storage = $CONTAINER->get('application_cache')->getStorage();
if (!Config::canUpdate()) {
return false;
......
......@@ -725,7 +725,7 @@ class Html {
static function displayDebugInfos($with_session = true, $ajax = false) {
global $CFG_GLPI, $CONTAINER, $DEBUG_SQL, $SQL_TOTAL_REQUEST, $SQL_TOTAL_TIMER, $DEBUG_AUTOLOAD;
$cache_storage = $CONTAINER->get('application_cache_storage');
$cache_storage = $CONTAINER->get('application_cache')->getStorage();
// Only for debug mode so not need to be translated
if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) { // mode debug
......
......@@ -624,7 +624,7 @@ class Session {
$_SESSION['glpipluralnumber'] = $CFG_GLPI["languages"][$trytoload][5];
}
$TRANSLATE = new Zend\I18n\Translator\Translator;
$cache_storage = $CONTAINER->get('translation_cache_storage');
$cache_storage = $CONTAINER->get('translation_cache')->getStorage();
$TRANSLATE->setCache($cache_storage);
$TRANSLATE->addTranslationFile('gettext', GLPI_ROOT.$newfile, 'glpi', $trytoload);
......
......@@ -56,8 +56,8 @@ class CacheStorageFactory extends StorageFactory
private static $cacheUniqId;
/**
* @param string $cacheDir Cache directory.
* @param string $cacheUniqId Cache unique identifier.
* @param string $cacheDir Cache directory.
* @param string $cacheUniqId Cache unique identifier.
*/
public function __construct(string $cacheDir, string $cacheUniqId)
{
......@@ -67,36 +67,19 @@ class CacheStorageFactory extends StorageFactory
public static function factory($cfg)
{
// Compute prefered adapter if 'auto' value or no value is used
if (!isset($cfg['adapter']) || 'auto' === $cfg['adapter']) {
if (function_exists('wincache_ucache_add')) {
$cfg['adapter'] = 'wincache';
} elseif (function_exists('apcu_fetch')) {
$cfg['adapter'] = 'apcu';
} else {
$cfg['adapter'] = 'filesystem';
if (!array_key_exists('plugins', $cfg)) {
$cfg['plugins'] = [
'serializer'
];
} elseif (!in_array('serializer', $cfg['plugins'])) {
$cfg['plugins'][] = 'serializer';
}
}
}
// Add unique id to namespace
if (!array_key_exists('options', $cfg) || !is_array($cfg['options'])) {
$cfg['options'] = [];
}
$baseNamespace = isset($cfg['options']['namespace']) ? $cfg['options']['namespace'] : '_default';
$cfg['options']['namespace'] = $baseNamespace . (empty(self::$cacheUniqId) ? '' : '_' . self::$cacheUniqId);
// Add unique id to namespace
$namespace = isset($cfg['options']['namespace']) ? $cfg['options']['namespace'] : '_default';
$namespace .= (empty(self::$cacheUniqId) ? '' : '_' . self::$cacheUniqId);
$cfg['options']['namespace'] = $namespace;
// Handle pathname for dba adapter
if ('dba' === $cfg['adapter']) {
// Assign default value for pathname
if (!isset($cfg['options']['pathname'])) {
$namespace = $cfg['options']['namespace'];
$cfg['options']['pathname'] = self::$cacheDir . '/' . $namespace . '.data';
}
}
......@@ -105,7 +88,6 @@ class CacheStorageFactory extends StorageFactory
if ('filesystem' === $cfg['adapter']) {
// Assign default value for cache dir
if (!isset($cfg['options']['cache_dir'])) {
$namespace = $cfg['options']['namespace'];
$cfg['options']['cache_dir'] = self::$cacheDir . '/' . $namespace;
}
......@@ -130,44 +112,6 @@ class CacheStorageFactory extends StorageFactory
}
}
try {
return parent::factory($cfg);
} catch (\Exception $e) {
if ('filesystem' !== $cfg['adapter']) {
// Fallback to 'filesystem' adapter
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "filesystem" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
return self::factory(
[
'adapter' => 'filesystem',
'options' => [
'namespace' => $baseNamespace . '_fallback'
],
]
);
} else {
// Fallback to 'memory' adapter
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "memory" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
return self::factory(
[
'adapter' => 'memory',
'options' => [
'namespace' => $baseNamespace . '_fallback'
],
]
);
}
}
return parent::factory($cfg);
}
}
This diff is collapsed.
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Cache;
use Zend\Cache\Exception\ExceptionInterface;
/**
* Glpi simple cache factory.
*
* @since 10.0.0
*/
class SimpleCacheFactory
{
/**
* Cache directory.
*
* @var string
*/
private $cacheDir;
/**
* Force skipping of integrity checks.
*
* @var bool
*/
private $disableIntegrityChecks;
/**
* Cache storage factory class.
*
* @var CacheStorageFactory
*/
private $storageFactory;
/**
* @param string $cacheDir Cache directory.
* @param bool $disableIntegrityChecks Force skipping of integrity checks.
* @param CacheStorageFactory $storageFactory Cache storage factory class.
*/
public function __construct(string $cacheDir, bool $disableIntegrityChecks, CacheStorageFactory $storageFactory)
{
$this->cacheDir = $cacheDir;
$this->disableIntegrityChecks = $disableIntegrityChecks;
$this->storageFactory = $storageFactory;
}
/**
* Create a simple cache instance.
*
* @param array $cfg Cache storage configuration, see Zend\Cache\StorageFactory::factory()
*
* @return SimpleCache
*/
public function factory($cfg): SimpleCache
{
$isAdapterComputed = !isset($cfg['adapter']) || 'auto' === $cfg['adapter'];
$skipIntegrityChecks = $this->disableIntegrityChecks
|| (!$isAdapterComputed && $this->canSkipIntegrityChecks($cfg['adapter']));
// Compute prefered adapter if 'auto' value or no value is used
if ($isAdapterComputed) {
if (function_exists('wincache_ucache_add')) {
$cfg['adapter'] = 'wincache';
} elseif (function_exists('apcu_fetch')) {
$cfg['adapter'] = 'apcu';
} else {
$cfg['adapter'] = 'filesystem';
}
}
$namespace = isset($cfg['options']) && isset($cfg['options']['namespace'])
? $cfg['options']['namespace']
: '_default';
try {
$storage = $this->storageFactory->factory($cfg);
} catch (ExceptionInterface $e) {
if ($isAdapterComputed && 'filesystem' !== $cfg['adapter']) {
// Fallback to 'filesystem' adapter if adapter was not explicitely defined in config
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "filesystem" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
$storage = $this->storageFactory->factory(
[
'adapter' => 'filesystem',
'options' => [
'namespace' => $namespace . '_fallback'
],
]
);
} else {
// Fallback to 'memory' adapter
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "memory" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
$storage = $this->storageFactory->factory(
[
'adapter' => 'memory',
'options' => [
'namespace' => $namespace . '_fallback'
],
]
);
}
}
return new SimpleCache($storage, $this->cacheDir, !$skipIntegrityChecks);
}
/**
* Check if adapter can be used without integrity checks.
*
* @param string $adapter
*
* @return boolean
*/
private function canSkipIntegrityChecks(string $adapter)
{
// Adapter names can be written using case variations.
// see Zend\Cache\Storage\AdapterPluginManager::$aliases
$adapter = strtolower($adapter);
switch ($adapter) {
// Cache adapters that can share their data accross processes
case 'dba':
case 'ext_mongo_db':
case 'extmongodb':
case 'filesystem':
case 'memcache':
case 'memcached':
case 'mongo_db':
case 'mongodb':
case 'redis':
return true;
break;
// Cache adapters that cannot share their data accross processes
case 'apc':
case 'apcu':
case 'memory':
case 'session':
// wincache activation uses different configuration variable for CLI and web server
// so it may not be available for all contexts
case 'win_cache':
case 'wincache':
// zend server adapters are not available for CLI context
case 'zend_server_disk':
case 'zendserverdisk':
case 'zend_server_shm':
case 'zendservershm':
default:
return false;
break;
}
}
}
......@@ -39,6 +39,7 @@ parameters:
# with cache adapter that shares a common space (apcu for example).
# It will be generated on installation.
cache_uniq_id: ''
cache_disable_integrity_checks: false
application_cache:
adapter: auto
options:
......@@ -58,7 +59,7 @@ services:
# this creates a service per class whose id is the fully-qualified class name
Glpi\:
resource: '%kernel.project_dir%/src/Glpi/*'
exclude: '%kernel.project_dir%/src/Glpi/{Annotation,DependencyInjection,Event,Kernel.php}'
exclude: '%kernel.project_dir%/src/Glpi/{Annotation,Cache,DependencyInjection,Event,Kernel.php}'
#### Glpi services below
......@@ -94,19 +95,16 @@ services:
Glpi\Cache\CacheStorageFactory:
$cacheDir: '%cache_directory%'
$cacheUniqId: '%cache_uniq_id%'
Glpi\Cache\SimpleCacheFactory:
$cacheDir: '%cache_directory%'
$disableIntegrityChecks: '%cache_disable_integrity_checks%'
application_cache:
class: 'Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator'
arguments: ['@application_cache_storage']
application_cache_storage:
class: 'Zend\Cache\Storage\StorageInterface'
factory: 'Glpi\Cache\CacheStorageFactory:factory'
class: 'Glpi\Cache\SimpleCache'
factory: 'Glpi\Cache\SimpleCacheFactory:factory'
arguments: ['%application_cache%']
translation_cache:
class: 'Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator'
arguments: ['@translation_cache_storage']
translation_cache_storage:
class: 'Zend\Cache\Storage\StorageInterface'
factory: 'Glpi\Cache\CacheStorageFactory:factory'
class: 'Glpi\Cache\SimpleCache'
factory: 'Glpi\Cache\SimpleCacheFactory:factory'
arguments: ['%translation_cache%']
# Glpi\ConfigParams service is synthetic as it requires loading of data inside DB
......
......@@ -70,4 +70,13 @@ class GLPITestCase extends atoum {
}
return $this->int++;
}
/**
* Returns cache namespace.
*/
protected function getCacheNamespace(): string {
global $CONTAINER;
$cacheUniq = $CONTAINER->getParameter('cache_uniq_id');
return 'app' . (empty($cacheUniq) ? '' : '_' . $cacheUniq);
}
}
......@@ -40,6 +40,9 @@ define('TU_USER', '_test_user');
define('TU_PASS', 'PhpUnit_4');
define('GLPI_ROOT', __DIR__ . '/../');
is_dir(GLPI_LOG_DIR) or mkdir(GLPI_LOG_DIR, 0755, true);
is_dir(GLPI_CACHE_DIR) or mkdir(GLPI_CACHE_DIR, 0755, true);
if (!file_exists(GLPI_CONFIG_DIR . '/db.yaml')) {
die("\nConfiguration file for tests not found\n\nrun: bin/console glpi:database:install --config-dir=./tests ...\n\n");
}
......@@ -509,9 +512,6 @@ function loadDataset() {
$CFG_GLPI['url_base'] = GLPI_URI;
$CFG_GLPI['url_base_api'] = GLPI_URI . '/apirest.php';
is_dir(GLPI_LOG_DIR) or mkdir(GLPI_LOG_DIR, 0755, true);
is_dir(GLPI_CACHE_DIR) or mkdir(GLPI_CACHE_DIR, 0755, true);
$conf = Config::getConfigurationValues('phpunit');
if (isset($conf['dataset']) && $conf['dataset']==$data['_version']) {
printf("\nGLPI dataset version %s already loaded\n\n", $data['_version']);
......
......@@ -30,6 +30,7 @@
#
parameters:
cache_uniq_id: tests
cache_disable_integrity_checks: true
application_cache:
adapter: apcu
options:
......
......@@ -616,7 +616,7 @@ class DbUtils extends DbTestCase {
//- if $cache === 1; we expect cache to be empty before call, and populated after
//- if $hit === 1; we expect cache to be populated
$ckey = 'app_tests:glpi_entities_ancestors_cache_';
$ckey = $this->getCacheNamespace() . ':glpi_entities_ancestors_cache_';
//test on ent0
$expected = [0 => '0'];
......@@ -775,7 +775,7 @@ class DbUtils extends DbTestCase {
//- if $cache === 1; we expect cache to be empty before call, and populated after
//- if $hit === 1; we expect cache to be populated
$ckey = 'app_tests:glpi_entities_sons_cache_';
$ckey = $this->getCacheNamespace() . ':glpi_entities_sons_cache_';
//test on ent0
$expected = [$ent0 => "$ent0", $ent1 => "$ent1", $ent2 => "$ent2"];
......
......@@ -118,8 +118,8 @@ class Entity extends DbTestCase {
$ent1 = getItemByTypeName('Entity', '_test_child_1', true);
$ent2 = getItemByTypeName('Entity', '_test_child_2', true);
$ackey = 'app_tests:glpi_entities_ancestors_cache_';
$sckey = 'app_tests:glpi_entities_sons_cache_';
$ackey = $this->getCacheNamespace() . ':glpi_entities_ancestors_cache_';
$sckey = $this->getCacheNamespace() . ':glpi_entities_sons_cache_';
$entity = new \Entity();
$new_id = (int)$entity->add([
......
#
# ---------------------------------------------------------------------
# GLPI - Gestionnaire Libre de Parc Informatique
# Copyright (C) 2015-2019 Teclib' and contributors.
#
# http://glpi-project.org
#
# based on GLPI - Gestionnaire Libre de Parc Informatique
# Copyright (C) 2003-2014 by the INDEPNET Development Team.
#
# ---------------------------------------------------------------------
#
# LICENSE
#
# This file is part of GLPI.
#
# GLPI is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# GLPI is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GLPI. If not, see <http://www.gnu.org/licenses/>.
# ---------------------------------------------------------------------
#
parameters:
cache_uniq_id: tests
application_cache:
adapter: apcu
options:
namespace: app
translation_cache:
adapter: filesystem
options:
namespace: trans
......@@ -30,6 +30,7 @@
#
parameters:
cache_uniq_id: tests
cache_disable_integrity_checks: true
application_cache:
adapter: filesystem
options:
......
......@@ -44,36 +44,6 @@ class CacheStorageFactory extends \GLPITestCase {
*/
protected function factoryProvider(): array {
return [
// Case: auto adapter without options
[
'config' => [
'adapter' => 'auto',
],
'expected_adapter' => [
'class' => \Zend\Cache\Storage\Adapter\Apcu::class,
'options' => [],
'plugins' => [],
],
],
// Case: auto adapter with options
[
'config' => [
'adapter' => 'auto',
'options' => [
'namespace' => 'app_cache',
],
],
'expected_adapter' => [
'class' => \Zend\Cache\Storage\Adapter\Apcu::class,
'options' => [
'namespace' => 'app_cache',
],
'plugins' => [
],
],
],
// Case: dba adapter using default options
/* Cannot test without extension loaded
[
......@@ -200,77 +170,4 @@ class CacheStorageFactory extends \GLPITestCase {
}
}
}
/**
* Test that factory fallback to filesystem adapter if requested adapter not working.
*/
public function testFactoryFallbackToFilesystem() {
$uniqId = uniqid();
$this->newTestedInstance(GLPI_CACHE_DIR, $uniqId);
$self = $this;
$adapter = null;
$this->when(
function() use ($self, &$adapter) {
$adapter = $self->testedInstance->factory(
[
'adapter' => 'invalid'
]
);
}
)->error()
->withType(E_USER_WARNING)
->withPattern('/^Cache adapter instantiation failed, fallback to "filesystem" adapter./')
->exists();
$this->object($adapter)->isInstanceOf(\Zend\Cache\Storage\Adapter\Filesystem::class);
$adapterOptions = $adapter->getOptions()->toArray();
$this->array($adapterOptions)->hasKey('namespace');
$this->variable($adapterOptions['namespace'])->isEqualTo('_default_fallback_' . $uniqId);
$this->array($adapterOptions)->hasKey('cache_dir');
$this->variable($adapterOptions['cache_dir'])->isEqualTo(GLPI_CACHE_DIR . '/_default_fallback_' . $uniqId);
}
/**
* Test that factory fallback to memory adapter if filesystem adapter not working.
*/
public function testFactoryFallbackToMemory() {
$uniqId = uniqid();
$this->newTestedInstance(GLPI_CACHE_DIR, $uniqId);
$self = $this;
$adapter = null;
$this->when(
function() use ($self, &$adapter) {
$adapter = $self->testedInstance->factory(
[
'adapter' => 'filesystem',
'options' => [
'cache_dir' => '/this/directory/cannot/be/created',
],
]
);
}
)->error()
->withType(E_USER_WARNING)
->withMessage('Cannot create "/this/directory/cannot/be/created" cache directory.')
->exists()
->error
->withType(E_USER_WARNING)
->withPattern('/^Cache adapter instantiation failed, fallback to "memory" adapter./')
->exists();
$this->object($adapter)->isInstanceOf(\Zend\Cache\Storage\Adapter\Memory::class);
$adapterOptions = $adapter->getOptions()->toArray();
$this->array($adapterOptions)->hasKey('namespace');
$this->variable($adapterOptions['namespace'])->isEqualTo('_default_fallback_' . $uniqId);
}
}
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace tests\units\Glpi\Cache;
use org\bovigo\vfs\vfsStream;
/* Test for src/Glpi/Cache/SimpleCache.php */
class SimpleCache extends \GLPITestCase {
/**
* Test case: cache dir is empty and writable, footprint file should be created and used.
*/
public function testCacheWithEmptyWritableCacheDir() {
$cache_dir = vfsStream::url('glpi/cache');
$cache_namespace = uniqid(true);
vfsStream::setup(
'glpi',
null,
[
'cache' => [],
]
);
$footprint_file = vfsStream::url('glpi/cache/' . $cache_namespace . '.json');
$this->newTestedInstance(
new \mock\Zend\Cache\Storage\Adapter\Memory(['namespace' => $cache_namespace]),
$cache_dir
);
// File has been initialized
$this->string(file_get_contents($footprint_file))->isEqualTo('[]');
$this->testOperationsOnCache($footprint_file);
}
/**
* Test case: footprint file exists, is writable and empty, it should be initialized and used.
*/
public function testCacheWithEmptyFootprintFile() {
$cache_dir = vfsStream::url('glpi/cache');
$cache_namespace = uniqid(true);
vfsStream::setup(
'glpi',
null,
[
'cache' => [
$cache_namespace . '.json' => ''
],
]
);
$footprint_file = vfsStream::url('glpi/cache/' . $cache_namespace . '.json');
$this->newTestedInstance(
new \mock\Zend\Cache\Storage\Adapter\Memory(['namespace' => $cache_namespace]),
$cache_dir
);
// File has initialized
$this->string(file_get_contents($footprint_file))->isEqualTo('[]');
$this->testOperationsOnCache($footprint_file);
}
/**
* Test case: footprint file exists and is writable, it should be used.
*/
public function testCacheWithExistingFootprintFile() {
$cache_dir = vfsStream::url('glpi/cache');
$cache_namespace = uniqid(true);
$existing_footprint = '{"existing_key":"752c14ea195c460bac3c3b7896975ee9fd15eeb7"}';
vfsStream::setup(
'glpi',
null,
[