Merge pull request #224 from Combodo/feature/dict-apcu-bug

Feature/dict apcu bug
This commit is contained in:
odain-cbd
2021-12-02 12:22:22 +01:00
committed by GitHub
7 changed files with 557 additions and 21 deletions

View File

@@ -0,0 +1,42 @@
<?php
/**
* Class ApcService
* @since 2.7.6 N°4125
*/
class ApcService {
public function __construct() {
}
/**
* @param string $function_name
* @return bool
* @see function_exists()
*/
public function function_exists($function_name) {
return function_exists($function_name);
}
/**
* @param string|array $key
* @return mixed
* @see apc_fetch()
*/
function apc_fetch($key)
{
return apc_fetch($key);
}
/**
* @param array|string $key
* @param $var
* @param int $ttl
* @return array|bool
* @see apc_store()
*/
function apc_store($key, $var = NULL, $ttl = 0)
{
return apc_store($key, $var, $ttl);
}
}
?>

View File

@@ -64,6 +64,8 @@ class Dict
protected static $m_aLanguages = array(); // array( code => array( 'description' => '...', 'localized_description' => '...') ...)
protected static $m_aData = array();
protected static $m_sApplicationPrefix = null;
/** @var \ApcService $m_oApcService */
protected static $m_oApcService = null;
/**
* @param $sLanguageCode
@@ -145,15 +147,17 @@ class Dict
{
// Attempt to find the string in the user language
//
self::InitLangIfNeeded(self::GetUserLanguage());
$sLangCode = self::GetUserLanguage();
self::InitLangIfNeeded($sLangCode);
if (!array_key_exists(self::GetUserLanguage(), self::$m_aData))
if (!array_key_exists($sLangCode, self::$m_aData))
{
IssueLog::Warning("Cannot find $sLangCode in dictionnaries. default labels displayed");
// It may happen, when something happens before the dictionaries get loaded
return $sStringCode;
}
$aCurrentDictionary = self::$m_aData[self::GetUserLanguage()];
if (array_key_exists($sStringCode, $aCurrentDictionary))
$aCurrentDictionary = self::$m_aData[$sLangCode];
if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary))
{
return $aCurrentDictionary[$sStringCode];
}
@@ -164,7 +168,7 @@ class Dict
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
if (array_key_exists($sStringCode, $aDefaultDictionary))
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
}
@@ -173,7 +177,7 @@ class Dict
self::InitLangIfNeeded('EN US');
$aDefaultDictionary = self::$m_aData['EN US'];
if (array_key_exists($sStringCode, $aDefaultDictionary))
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
}
@@ -232,7 +236,26 @@ class Dict
{
self::$m_aLanguages = $aLanguagesList;
}
/**
* @since 2.7.6 N°4125
* @return \ApcService
*/
public static function GetApcService() {
if (self::$m_oApcService === null){
self::$m_oApcService = new ApcService();
}
return self::$m_oApcService;
}
/**
* @since 2.7.6 N°4125
* @param \ApcService $m_oApcService
*/
public static function SetApcService($oApcService) {
self::$m_oApcService = $oApcService;
}
/**
* Load a language from the language dictionary, if not already loaded
* @param string $sLangCode Language code
@@ -241,20 +264,23 @@ class Dict
public static function InitLangIfNeeded($sLangCode)
{
if (array_key_exists($sLangCode, self::$m_aData)) return true;
$bResult = false;
if (function_exists('apc_fetch') && (self::$m_sApplicationPrefix !== null))
if (self::GetApcService()->function_exists('apc_fetch')
&& (self::$m_sApplicationPrefix !== null))
{
// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
//
self::$m_aData[$sLangCode] = apc_fetch(self::$m_sApplicationPrefix.'-dict-'.$sLangCode);
if (self::$m_aData[$sLangCode] === false)
{
self::$m_aData[$sLangCode] = self::GetApcService()->apc_fetch(self::$m_sApplicationPrefix.'-dict-'.$sLangCode);
if (self::$m_aData[$sLangCode] === false) {
unset(self::$m_aData[$sLangCode]);
}
else
{
} else if (! is_array(self::$m_aData[$sLangCode])) {
// N°4125: we dont fix dictionnary corrupted cache (on iTop side).
// but we log an error in a dedicated channel to let itop administrator be aware of a potential APCu issue to fix.
IssueLog::Error("APCu corrupted data (with $sLangCode dictionnary). APCu configuration and running version should be troubleshooted...", LogChannels::APC);
$bResult = true;
} else {
$bResult = true;
}
}
@@ -263,9 +289,10 @@ class Dict
$sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php';
require_once($sDictFile);
if (function_exists('apc_store') && (self::$m_sApplicationPrefix !== null))
if (self::GetApcService()->function_exists('apc_store')
&& (self::$m_sApplicationPrefix !== null))
{
apc_store(self::$m_sApplicationPrefix.'-dict-'.$sLangCode, self::$m_aData[$sLangCode]);
self::GetApcService()->apc_store(self::$m_sApplicationPrefix.'-dict-'.$sLangCode, self::$m_aData[$sLangCode]);
}
$bResult = true;
}

View File

@@ -542,6 +542,7 @@ class LogChannels
const DEADLOCK = 'DeadLock';
const INLINE_IMAGE = 'InlineImage';
const PORTAL = 'portal';
const APC = 'apc';
}

View File

@@ -17,6 +17,7 @@ return array(
'ActionChecker' => $baseDir . '/core/userrights.class.inc.php',
'ActionEmail' => $baseDir . '/core/action.class.inc.php',
'ActionNotification' => $baseDir . '/core/action.class.inc.php',
'ApcService' => $baseDir . '/core/apc-service.class.inc.php',
'ApplicationContext' => $baseDir . '/application/applicationcontext.class.inc.php',
'ApplicationException' => $baseDir . '/application/application.inc.php',
'ApplicationMenu' => $baseDir . '/application/menunode.class.inc.php',

View File

@@ -247,6 +247,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'ActionChecker' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
'ActionEmail' => __DIR__ . '/../..' . '/core/action.class.inc.php',
'ActionNotification' => __DIR__ . '/../..' . '/core/action.class.inc.php',
'ApcService' => __DIR__ . '/../..' . '/core/apc-service.class.inc.php',
'ApplicationContext' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
'ApplicationException' => __DIR__ . '/../..' . '/application/application.inc.php',
'ApplicationMenu' => __DIR__ . '/../..' . '/application/menunode.class.inc.php',

402
test/core/dictApcuTest.php Normal file
View File

@@ -0,0 +1,402 @@
<?php
// Copyright (c) 2010-2017 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
/**
* Created by PhpStorm.
* User: Eric
* Date: 30/10/2017
* Time: 13:43
*/
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Dict;
use Exception;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class dictApcuTest extends ItopTestCase
{
private $sEnvName;
private $oApcService;
private $sDictionaryFolder;
protected function setUp()
{
parent::setUp();
require_once (APPROOT.'core' . DIRECTORY_SEPARATOR . 'coreexception.class.inc.php');
require_once (APPROOT.'core' . DIRECTORY_SEPARATOR . 'dict.class.inc.php');
require_once (APPROOT.'core' . DIRECTORY_SEPARATOR . 'apc-service.class.inc.php');
$this->sEnvName = time();
$_SESSION['itop_env'] = $this->sEnvName;
$this->oApcService = $this->createMock(\ApcService::class);
Dict::SetApcService($this->oApcService);
Dict::EnableCache('toto');
Dict::SetLanguagesList(['FR FR' => 'fr', 'EN US' => 'en', 'DE DE' => 'de', 'RU RU' => 'de']);
$this->InitDictionnaries();
}
private function InitDictionnaries(){
clearstatcache();
$this->sDictionaryFolder = APPROOT."env-$this->sEnvName" . DIRECTORY_SEPARATOR . "dictionaries";
@mkdir($this->sDictionaryFolder, 0777, true);
$sLabels = <<<STR
'label1' => 'fr1',
STR;
$this->InitDictionnary($this->sDictionaryFolder, 'FR FR', 'fr-fr', $sLabels);
$sLabels = <<<STR
'label1' => 'ru1',
'label2' => 'ru2',
STR;
$this->InitDictionnary($this->sDictionaryFolder, 'RU RU', 'ru-ru', $sLabels);
$sLabels = <<<STR
'label1' => 'en1',
'label2' => 'en2',
'label3' => 'en3',
STR;
$this->InitDictionnary($this->sDictionaryFolder, 'EN US', 'en-us', $sLabels);
clearstatcache();
}
private function InitDictionnary($sDictionaryFolder, $sLanguageCode, $sLanguageCodeInFilename, $sLabels){
$sContent = <<<PHP
<?php
//
// Dictionary built by the compiler for the language "FR FR"
//
Dict::SetEntries('$sLanguageCode', array(
$sLabels
));
PHP;
file_put_contents($sDictionaryFolder . DIRECTORY_SEPARATOR . "$sLanguageCodeInFilename.dict.php", $sContent);
}
private function InitBrokenDictionnary($sDictionaryFolder, $sLanguageCode, $sLanguageCodeInFilename){
$sContent = <<<PHP
<?php
//
// Dictionary built by the compiler for the language "FR FR"
//
Dict::SetEntries('$sLanguageCode', 'stringinsteadofanarray');
PHP;
file_put_contents($sDictionaryFolder . DIRECTORY_SEPARATOR . "$sLanguageCodeInFilename.dict.php", $sContent);
}
protected function tearDown()
{
foreach (glob(APPROOT."env-$this->sEnvName" . DIRECTORY_SEPARATOR . "dictionaries" . DIRECTORY_SEPARATOR . "*") as $sFile){
unlink($sFile);
}
rmdir(APPROOT."env-$this->sEnvName" . DIRECTORY_SEPARATOR . "dictionaries");
rmdir(APPROOT."env-$this->sEnvName");
}
public function InitLangIfNeeded_NoApcProvider(){
return [
'apcu mocked' => [ 'bApcuMocked' => true ],
'integration test - apcu service like in production - install php-apcu before' => [ 'bApcuMocked' => false ],
];
}
/**
* @dataProvider InitLangIfNeeded_NoApcProvider
*/
public function testInitLangIfNeeded_NoApc($bApcuMocked){
if ($bApcuMocked) {
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(false);
$this->oApcService->expects($this->never())
->method('apc_fetch');
$this->oApcService->expects($this->never())
->method('apc_store');
} else {
Dict::SetApcService(null);
}
//EN US default language
$this->assertEquals('en1', Dict::S('label1'));
$this->assertEquals('en2', Dict::S('label2'));
$this->assertEquals('en3', Dict::S('label3'));
$this->assertEquals('not_defined_label', Dict::S('not_defined_label'));
//default language set to RU RU
Dict::SetDefaultLanguage('RU RU');
$this->assertEquals('ru1', Dict::S('label1'));
$this->assertEquals('ru2', Dict::S('label2'));
$this->assertEquals('en3', Dict::S('label3'));
$this->assertEquals('not_defined_label', Dict::S('not_defined_label'));
//user language set to FR FR
Dict::SetUserLanguage('FR FR');
$this->assertEquals('fr1', Dict::S('label1'));
$this->assertEquals('ru2', Dict::S('label2'));
$this->assertEquals('en3', Dict::S('label3'));
$this->assertEquals('not_defined_label', Dict::S('not_defined_label'));
}
public function testInitLangIfNeeded_Apc_LanguageMismatchDictionnary(){
//language mismatch!!
$sLabels = <<<STR
'label1' => 'de1',
STR;
$this->InitDictionnary($this->sDictionaryFolder, 'RU RU', 'de-de', $sLabels);
clearstatcache();
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(false);
$this->oApcService->expects($this->exactly(0))
->method('apc_fetch');
$this->oApcService->expects($this->never())
->method('apc_store');
Dict::SetUserLanguage('DE DE');
$this->assertEquals('label1', Dict::S('label1'));
}
public function testInitLangIfNeeded_Apc_BrokenUserDictionnary(){
$this->InitBrokenDictionnary($this->sDictionaryFolder, 'DE DE', 'de-de');
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(false);
$this->oApcService->expects($this->exactly(0))
->method('apc_fetch');
$this->oApcService->expects($this->never())
->method('apc_store');
Dict::SetUserLanguage('DE DE');
$this->assertEquals('en1', Dict::S('label1'));
Dict::SetDefaultLanguage('RU RU');
$this->assertEquals('ru1', Dict::S('label1'));
}
public function testInitLangIfNeeded_Apc_BrokenDictionnary_UserAndDefault(){
$this->InitBrokenDictionnary($this->sDictionaryFolder, 'DE DE', 'de-de');
$this->InitBrokenDictionnary($this->sDictionaryFolder, 'RU RU', 'ru-ru');
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(false);
$this->oApcService->expects($this->exactly(0))
->method('apc_fetch');
$this->oApcService->expects($this->never())
->method('apc_store');
Dict::SetUserLanguage('DE DE');
Dict::SetDefaultLanguage('RU RU');
$this->assertEquals('en1', Dict::S('label1'));
}
public function testInitLangIfNeeded_Apc_BrokenDictionnary_ALL(){
$this->InitBrokenDictionnary($this->sDictionaryFolder, 'DE DE', 'de-de');
$this->InitBrokenDictionnary($this->sDictionaryFolder, 'RU RU', 'ru-ru');
$this->InitBrokenDictionnary($this->sDictionaryFolder, 'EN US', 'en-us');
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(false);
$this->oApcService->expects($this->exactly(0))
->method('apc_fetch');
$this->oApcService->expects($this->never())
->method('apc_store');
Dict::SetUserLanguage('DE DE');
Dict::SetDefaultLanguage('RU RU');
$this->assertEquals('label1', Dict::S('label1'));
}
public function testInitLangIfNeeded_ApcFromCache_PropertyInUserDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(1))
->method('apc_fetch')
->with('toto-dict-FR FR')
->willReturn(['label1' => 'fr1']);
$this->oApcService->expects($this->exactly(0))
->method('apc_store');
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('fr1', Dict::S('label1'));
}
public function testInitLangIfNeeded_ApcStore_PropertyInUserDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(1))
->method('apc_fetch')
->with('toto-dict-FR FR')
->willReturn(false);
$this->oApcService->expects($this->exactly(1))
->method('apc_store')
->with('toto-dict-FR FR', ['label1' => 'fr1']);
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('fr1', Dict::S('label1'));
}
//corrupted data not fixed
//we will return label from another dictionary (defaut one => russian here)
public function testInitLangIfNeeded_Apc_CorruptedCache_PropertyInUserDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(2))
->method('apc_fetch')
->withConsecutive(['toto-dict-FR FR'], ['toto-dict-RU RU'])
->willReturnOnConsecutiveCalls('corrupteddata', ['label1' => 'ru1']);
$this->oApcService->expects($this->exactly(0))
->method('apc_store');
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('ru1', Dict::S('label1'));
}
public function testInitLangIfNeeded_Apc_PropertyInDefaultLanguageDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(2))
->method('apc_fetch')
->withConsecutive(['toto-dict-FR FR'], ['toto-dict-RU RU'])
->willReturnOnConsecutiveCalls([], false);
$this->oApcService->expects($this->exactly(1))
->method('apc_store')
->withConsecutive(['toto-dict-RU RU', ['label1' => 'ru1', 'label2' => 'ru2']]
);
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('ru2', Dict::S('label2'));
}
//corrupted data not fixed
//we will return label from default language dictionary (EN here)
public function testInitLangIfNeeded_ApcCorrupted_PropertyInDefaultLanguageDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(3))
->method('apc_fetch')
->withConsecutive(['toto-dict-FR FR'], ['toto-dict-RU RU'], ['toto-dict-EN US'])
->willReturnOnConsecutiveCalls([], 'corrupteddata', ['label1' => 'en1', 'label2' => 'en2', 'label3' => 'en3']);
$this->oApcService->expects($this->exactly(0))
->method('apc_store');
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('en2', Dict::S('label2'));
}
public function testInitLangIfNeeded_Apc_PropertyInDictDefaultLanguageDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(3))
->method('apc_fetch')
->withConsecutive(['toto-dict-FR FR'], ['toto-dict-RU RU'], ['toto-dict-EN US'])
->willReturnOnConsecutiveCalls([], [], false);
$this->oApcService->expects($this->exactly(1))
->method('apc_store')
->withConsecutive(
['toto-dict-EN US', ['label1' => 'en1', 'label2' => 'en2', 'label3' => 'en3']]
);
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('en3', Dict::S('label3'));
}
public function testInitLangIfNeeded_ApcCorrupted_PropertyInDictDefaultLanguageDictionnary(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(3))
->method('apc_fetch')
->withConsecutive(['toto-dict-FR FR'], ['toto-dict-RU RU'], ['toto-dict-EN US'])
->willReturnOnConsecutiveCalls([], [], 'corrupteddata');
$this->oApcService->expects($this->exactly(0))
->method('apc_store');
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('label3', Dict::S('label3'));
}
public function testInitLangIfNeeded_Apc_PropertyNotFound(){
$this->oApcService->expects($this->any())
->method('function_exists')
->willReturn(true);
$this->oApcService->expects($this->exactly(3))
->method('apc_fetch')
->withConsecutive(['toto-dict-FR FR'], ['toto-dict-RU RU'], ['toto-dict-EN US'])
->willReturnOnConsecutiveCalls([], [], []);
$this->oApcService->expects($this->exactly(0))
->method('apc_store');
Dict::SetDefaultLanguage('RU RU');
Dict::SetUserLanguage('FR FR');
$this->assertEquals('undefined_label', Dict::S('undefined_label'));
}
}

View File

@@ -38,12 +38,49 @@ use Exception;
*/
class dictTest extends ItopTestCase
{
private $sEnvName;
protected function setUp()
{
parent::setUp();
require_once (APPROOT.'core/coreexception.class.inc.php');
require_once (APPROOT.'core/dict.class.inc.php');
require_once 'mockDict.incphp';
require_once (APPROOT.'core'. DIRECTORY_SEPARATOR . 'coreexception.class.inc.php');
require_once (APPROOT.'core'. DIRECTORY_SEPARATOR . 'dict.class.inc.php');
require_once (APPROOT.'core'. DIRECTORY_SEPARATOR . 'apc-service.class.inc.php');
$this->sEnvName = time();
$sDictionaryFolder = APPROOT."env-$this->sEnvName" . DIRECTORY_SEPARATOR . "dictionaries";
@mkdir($sDictionaryFolder, 0777, true);
$sContent = <<<PHP
<?php
//
// Dictionary built by the compiler for the language "FR FR"
//
Dict::SetEntries('FR FR', array(
'label1' => 'gabu',
));
PHP;
file_put_contents($sDictionaryFolder . DIRECTORY_SEPARATOR . "fr-fr.dict.php", $sContent);
$sContent = <<<PHP
<?php
//
// Dictionary built by the compiler for the language "FR FR"
//
Dict::SetEntries('EN EN', array(
'label1' => 'zomeu',
));
PHP;
file_put_contents($sDictionaryFolder . DIRECTORY_SEPARATOR . "en-en.dict.php", $sContent);
$_SESSION['itop_env'] = $this->sEnvName;
//require_once 'mockDict.incphp';
}
protected function tearDown()
{
foreach (glob(APPROOT."env-$this->sEnvName" . DIRECTORY_SEPARATOR . "dictionaries" . DIRECTORY_SEPARATOR . "*") as $sFile){
unlink($sFile);
}
rmdir(APPROOT."env-$this->sEnvName" . DIRECTORY_SEPARATOR . "dictionaries");
rmdir(APPROOT."env-$this->sEnvName");
}
/**
@@ -51,7 +88,32 @@ class dictTest extends ItopTestCase
*/
public function testType()
{
$_SESSION['itop_env'] = 'production';
$this->assertInternalType('string', Dict::S('Core:AttributeURL'));
$this->assertInternalType('string', Dict::Format('Change:AttName_SetTo', '1', '2'));
}
public function testInitLangIfNeeded_NoApc(){
$oApcService = $this->createMock(\ApcService::class);
Dict::SetApcService($oApcService);
Dict::EnableCache('toto');
$oApcService->expects($this->any())
->method('function_exists')
->willReturn(false);
$oApcService->expects($this->never())
->method('apc_fetch')
->willReturn(false);
$oApcService->expects($this->never())
->method('apc_store')
->willReturn(false);
Dict::SetLanguagesList(['FR FR' => 'fr', 'EN EN' => 'en']);
Dict::SetUserLanguage('FR FR');
$this->assertEquals('gabu', Dict::S('label1'));
Dict::SetUserLanguage('EN EN');
$this->assertEquals('zomeu', Dict::S('label1'));
}
}