diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index a007a3ab3..4576e4fa0 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -2246,3 +2246,25 @@ interface iModuleExtension */ public function __construct(); } + +/** + * Interface to manipulate ormCaseLog objects after initialization/edition + * + * @api + * @private + * @since 3.1.0 N°6275 + */ +interface iOrmCaseLogExtension +{ + public function __construct(); + + /** + * Rebuild API to be able manipulate ormCaseLog after its initialization/modifications + * Examples of use: fix ormcase log broken index/shrink huge histories/.... + * @param string $sLog: ormcaselog description + * @param array|null $aIndex: ormcaselog index + * + * @return bool: indicate whether current ormCaseLog fields were touched + */ + public function Rebuild(&$sLog, &$aIndex) : bool; +} diff --git a/core/ormcaselog.class.inc.php b/core/ormcaselog.class.inc.php index 52e1629d9..5646faf13 100644 --- a/core/ormcaselog.class.inc.php +++ b/core/ormcaselog.class.inc.php @@ -3,7 +3,7 @@ // // This file is part of iTop. // -// iTop is free software; you can redistribute it and/or modify +// 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. @@ -25,10 +25,11 @@ use Combodo\iTop\Renderer\BlockRenderer; define('CASELOG_VISIBLE_ITEMS', 2); define('CASELOG_SEPARATOR', "\n".'========== %1$s : %2$s (%3$d) ============'."\n\n"); +require_once('ormcaselogservice.inc.php'); /** * Class to store a "case log" in a structured way, keeping track of its successive entries - * + * * @copyright Copyright (C) 2010-2023 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ @@ -47,19 +48,22 @@ class ormCaseLog { protected $m_sLog; protected $m_aIndex; protected $m_bModified; - + protected \ormCaseLogService $oOrmCaseLogService; + /** * Initializes the log with the first (initial) entry * @param $sLog string The text of the whole case log * @param $aIndex array The case log index */ - public function __construct($sLog = '', $aIndex = array()) + public function __construct($sLog = '', $aIndex = [], \ormCaseLogService $oOrmCaseLogService=null) { $this->m_sLog = $sLog; $this->m_aIndex = $aIndex; $this->m_bModified = false; + $this->oOrmCaseLogService = (is_null($oOrmCaseLogService)) ? new \ormCaseLogService() : $oOrmCaseLogService; + $this->RebuildIndex(); } - + public function GetText($bConvertToPlainText = false) { if ($bConvertToPlainText) @@ -72,7 +76,7 @@ class ormCaseLog { return $this->m_sLog; } } - + public static function FromJSON($oJson) { if (!isset($oJson->items)) @@ -88,8 +92,8 @@ class ormCaseLog { } /** - * Return a value that will be further JSON encoded - */ + * Return a value that will be further JSON encoded + */ public function GetForJSON() { // Order by ascending date @@ -199,9 +203,9 @@ class ormCaseLog { $sSeparator = sprintf(CASELOG_SEPARATOR, $aData['date'], $aData['user_login'], $aData['user_id']); $sPlainText .= $sSeparator.$aData['message']; } - return $sPlainText; + return $sPlainText; } - + public function GetIndex() { return $this->m_aIndex; @@ -227,7 +231,7 @@ class ormCaseLog { { return count($this->m_aIndex); } - + public function ClearModifiedFlag() { $this->m_bModified = false; @@ -235,7 +239,7 @@ class ormCaseLog { /** * Produces an HTML representation, aimed at being used within an email - */ + */ public function GetAsEmailHtml() { $sStyleCaseLogHeader = ''; @@ -312,10 +316,10 @@ class ormCaseLog { $sHtml .= ''; return $sHtml; } - + /** * Produces an HTML representation, aimed at being used to produce a PDF with TCPDF (no table) - */ + */ public function GetAsSimpleHtml($aTransfoHandler = null) { $sStyleCaseLogEntry = ''; @@ -338,7 +342,7 @@ class ormCaseLog { $sTextEntry = call_user_func($aTransfoHandler, $sTextEntry, true /* wiki "links" only */); } $sTextEntry = InlineImage::FixUrls($sTextEntry); - } + } $iPos += $aIndex[$index]['text_length']; $sEntry = '
  • '; @@ -396,7 +400,7 @@ class ormCaseLog { /** * Produces an HTML representation, aimed at being used within the iTop framework - */ + */ public function GetAsHTML(WebPage $oP = null, $bEditMode = false, $aTransfoHandler = null) { $bPrintableVersion = (utils::ReadParam('printable', '0') == '1'); @@ -519,7 +523,7 @@ class ormCaseLog { } } } - + return $sHtml; } @@ -534,7 +538,7 @@ class ormCaseLog { * @throws \ArchivedObjectException * @throws \CoreException * @throws \OQLException - * + * * @since 3.0.0 New $iOnBehalfOfId parameter * @since 3.0.0 May throw \ArchivedObjectException exception */ @@ -585,6 +589,7 @@ class ormCaseLog { 'separator_length' => $iSepLength, 'format' => static::ENUM_FORMAT_HTML, ); + $this->RebuildIndex(); $this->m_bModified = true; } @@ -620,7 +625,7 @@ class ormCaseLog { $iUserId = UserRights::GetUserId(); $sOnBehalfOf = UserRights::GetUserFriendlyName(); } - + if (isset($oJson->date)) { $oDate = new DateTime($oJson->date); @@ -653,14 +658,14 @@ class ormCaseLog { $iTextlength = strlen($sText); $this->m_sLog = $sSeparator.$sText.$this->m_sLog; // Latest entry printed first $this->m_aIndex[] = array( - 'user_name' => $sOnBehalfOf, - 'user_id' => $iUserId, - 'date' => $iDate, - 'text_length' => $iTextlength, + 'user_name' => $sOnBehalfOf, + 'user_id' => $iUserId, + 'date' => $iDate, + 'text_length' => $iTextlength, 'separator_length' => $iSepLength, 'format' => $sFormat, ); - + $this->RebuildIndex(); $this->m_bModified = true; } @@ -716,7 +721,7 @@ class ormCaseLog { $iLast = end($aKeys); // Strict standards: the parameter passed to 'end' must be a variable since it is passed by reference return $iLast; } - + /** * Get the text string corresponding to the given entry in the log (zero based index, older entries first) * @param integer $iIndex @@ -736,4 +741,16 @@ class ormCaseLog { $sText = substr($this->m_sLog, $iPos, $this->m_aIndex[$index]['text_length']); return InlineImage::FixUrls($sText); } + + /** + * @since 3.1.0 N°6275 + */ + public function RebuildIndex(): void + { + $oNewOrmCaseLog = $this->oOrmCaseLogService->Rebuild($this->m_sLog, $this->m_aIndex); + if (! is_null($oNewOrmCaseLog)) { + $this->m_aIndex = $oNewOrmCaseLog->m_aIndex; + $this->m_sLog = $oNewOrmCaseLog->m_sLog; + } + } } diff --git a/core/ormcaselogservice.inc.php b/core/ormcaselogservice.inc.php new file mode 100644 index 000000000..d1e933e3c --- /dev/null +++ b/core/ormcaselogservice.inc.php @@ -0,0 +1,58 @@ +aOrmCaseLogExtension !== null) return; + + $aOrmCaseLogExtension = []; + $aProviderClasses = \utils::GetClassesForInterface(iOrmCaseLogExtension::class, '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]', '[\\\\/]tests[\\\\/]')); + foreach($aProviderClasses as $sProviderClass) { + $aOrmCaseLogExtension[] = new $sProviderClass(); + } + $this->aOrmCaseLogExtension = $aOrmCaseLogExtension; + } + + /** + * @param string|null $sLog + * @param array|null $aIndex + * + * @return \ormCaseLog|null: returns rebuilt ormCaseLog. null if not touched + */ + public function Rebuild($sLog, $aIndex) : ?\ormCaseLog + { + $this->LoadCaseLogExtensions(); + + $bTouched = false; + foreach ($this->aOrmCaseLogExtension as $oOrmCaseLogExtension){ + /** var iOrmCaseLogExtension $oOrmCaseLogExtension */ + $bTouched = $bTouched || $oOrmCaseLogExtension->Rebuild($sLog, $aIndex); + } + + if ($bTouched){ + return new \ormCaseLog($sLog, $aIndex); + } + + return null; + } +} diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index b85485524..b15367dce 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -2970,6 +2970,7 @@ return array( 'lnkAuditCategoryToAuditDomain' => $baseDir . '/application/audit.domain.class.inc.php', 'lnkTriggerAction' => $baseDir . '/core/trigger.class.inc.php', 'ormCaseLog' => $baseDir . '/core/ormcaselog.class.inc.php', + 'ormCaseLogService' => $baseDir . '/core/ormcaselogservice.inc.php', 'ormCustomFieldsValue' => $baseDir . '/core/ormcustomfieldsvalue.class.inc.php', 'ormDocument' => $baseDir . '/core/ormdocument.class.inc.php', 'ormLinkSet' => $baseDir . '/core/ormlinkset.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index c10b2e716..15e466bb4 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -3335,6 +3335,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'lnkAuditCategoryToAuditDomain' => __DIR__ . '/../..' . '/application/audit.domain.class.inc.php', 'lnkTriggerAction' => __DIR__ . '/../..' . '/core/trigger.class.inc.php', 'ormCaseLog' => __DIR__ . '/../..' . '/core/ormcaselog.class.inc.php', + 'ormCaseLogService' => __DIR__ . '/../..' . '/core/ormcaselogservice.inc.php', 'ormCustomFieldsValue' => __DIR__ . '/../..' . '/core/ormcustomfieldsvalue.class.inc.php', 'ormDocument' => __DIR__ . '/../..' . '/core/ormdocument.class.inc.php', 'ormLinkSet' => __DIR__ . '/../..' . '/core/ormlinkset.class.inc.php', diff --git a/tests/php-unit-tests/unitary-tests/core/ormCaseLogTest.php b/tests/php-unit-tests/unitary-tests/core/ormCaseLogTest.php index 484be7951..18e3e3c6a 100644 --- a/tests/php-unit-tests/unitary-tests/core/ormCaseLogTest.php +++ b/tests/php-unit-tests/unitary-tests/core/ormCaseLogTest.php @@ -22,6 +22,32 @@ use ormCaseLog; */ class ormCaseLogTest extends ItopDataTestCase { + const USE_TRANSACTION = false; + private $sLogin; + private $sPassword = "Iuytrez9876543ç_è-("; + + public function setUp() :void{ + parent::setUp(); // TODO: Change the autogenerated stub + //require_once APPROOT . "core/CaseLogService.php"; + + $oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true); + + if (is_object($oAdminProfile)) { + $this->sLogin = "admin-".date('dmYHis'); + + $this->CreateTestOrganization(); + + /** @var \Person $oPerson */ + $oPerson = $this->createObject('Person', array( + 'name' => $this->sLogin, + 'first_name' => 'Test', + 'org_id' => $this->getTestOrgId(), + )); + + $this->CreateUser($this->sLogin, $oAdminProfile->GetKey(), $this->sPassword, $oPerson->GetKey()); + } + } + /** * @covers \ormCaseLog::GetEntryCount() * @throws \ArchivedObjectException @@ -38,4 +64,141 @@ class ormCaseLogTest extends ItopDataTestCase $oLog->AddLogEntry('First entry'); $this->assertEquals($oLog->GetEntryCount(), 1, 'Should be 1 entry, returned '.$oLog->GetEntryCount()); } + + public function testConstructorWithoutRebuild(){ + $oOrmCaseLogService = $this->createMock(\ormCaseLogService::class); + $sLog = "aaaaa"; + $aInitialIndex = ['a' => 'b']; + + $oOrmCaseLogService->expects($this->exactly(1)) + ->method('Rebuild') + ->with($sLog, $aInitialIndex) + ->willReturn(null); + + $oLog = new ormCaseLog($sLog, $aInitialIndex, $oOrmCaseLogService); + $this->assertEquals($aInitialIndex, $oLog->GetIndex()); + $this->assertEquals($sLog, $oLog->GetText()); + } + + public function testConstructorWithRebuild(){ + $oOrmCaseLogService = $this->createMock(\ormCaseLogService::class); + $sLog = "aaaaa"; + $sRebuiltLog = "bbbb"; + $aInitialIndex = ['a' => 'b']; + $aRebuiltIndex = ['c' => 'd']; + + $oOrmCaseLogService->expects($this->exactly(1)) + ->method('Rebuild') + ->with($sLog, $aInitialIndex) + ->willReturn(new ormCaseLog($sRebuiltLog, $aRebuiltIndex)); + + $oLog = new ormCaseLog($sLog, $aInitialIndex, $oOrmCaseLogService); + $this->assertEquals($aRebuiltIndex, $oLog->GetIndex()); + $this->assertEquals($sRebuiltLog, $oLog->GetText()); + } + + public function AddLogEntryProvider(){ + return [ + 'AddLogEntry' => [ + 'bTestAddLogEntry' => 'true' + ], + 'AddLogEntryFromJSON' => [ + 'bTestAddLogEntry' => 'false' + ] + ]; + } + + + /** + * @dataProvider AddLogEntryProvider + */ + public function testAddLogEntryNoRebuild($bTestAddLogEntry=true){ + $_SESSION = array(); + $this->assertTrue(\UserRights::Login($this->sLogin)); + + $sLog = "aaaaa"; + $sDate = date(\AttributeDateTime::GetInternalFormat()); + $iUserId = \UserRights::GetUserId(); + $sOnBehalfOf = \UserRights::GetUserFriendlyName(); + $sSeparator = sprintf(CASELOG_SEPARATOR, $sDate, $sOnBehalfOf, $iUserId); + $sExpectedLog = $sSeparator."

    $sLog

    "; + + $oOrmCaseLogService = $this->createMock(\ormCaseLogService::class); + $aExpectedIndex = [ + [ + 'user_name' => $sOnBehalfOf, + 'user_id' => $iUserId, + 'date' => time(), + 'text_length' => 12, + 'separator_length' => strlen($sSeparator), + 'format' => 'html', + ] + ]; + + $oOrmCaseLogService->expects($this->exactly(2)) + ->method('Rebuild') + ->withConsecutive(['', []], [$sExpectedLog, $aExpectedIndex]) + ->willReturnOnConsecutiveCalls(null, null); + + $oLog = new ormCaseLog('', [], $oOrmCaseLogService); + if ($bTestAddLogEntry){ + $oLog->AddLogEntry($sLog); + } else { + $sJson = json_encode( + [ + 'user_login' => 'gabuzmeu', + 'message' => $sLog, + ] + ); + $oJson = json_decode($sJson); + $oLog->AddLogEntryFromJSON($oJson, false); + } + + $this->assertEquals($sExpectedLog, $oLog->GetText()); + $this->assertEquals($aExpectedIndex, $oLog->GetIndex()); + } + + /** + * @dataProvider AddLogEntryProvider + */ + public function testAddLogEntry($bTestAddLogEntry=true){ + $_SESSION = array(); + $this->assertTrue(\UserRights::Login($this->sLogin)); + + $sLog = "aaaaa"; + $sDate = date(\AttributeDateTime::GetInternalFormat()); + $iUserId = \UserRights::GetUserId(); + $sOnBehalfOf = \UserRights::GetUserFriendlyName(); + $sSeparator = sprintf(CASELOG_SEPARATOR, $sDate, $sOnBehalfOf, $iUserId); + $sExpectedLog = $sSeparator."

    $sLog

    "; + + $aRebuiltIndex = ['c' => 'd']; + $sRebuiltLog = "bbbb"; + + $oOrmCaseLogService = $this->createMock(\ormCaseLogService::class); + + $aExpectedIndex = [ + [ + 'user_name' => $sOnBehalfOf, + 'user_id' => $iUserId, + 'date' => time(), + 'text_length' => 12, + 'separator_length' => strlen($sSeparator), + 'format' => 'html', + ] + ]; + $oOrmCaseLogService->expects($this->exactly(2)) + ->method('Rebuild') + ->withConsecutive(['', []], [$sExpectedLog, $aExpectedIndex]) + ->willReturnOnConsecutiveCalls( + null, + new ormCaseLog($sRebuiltLog, $aRebuiltIndex) + ); + + $oLog = new ormCaseLog('', [], $oOrmCaseLogService); + $oLog->AddLogEntry($sLog); + + $this->assertEquals($sRebuiltLog, $oLog->GetText()); + $this->assertEquals($aRebuiltIndex, $oLog->GetIndex()); + } }