From 37cd12fb21b4bbb8813ab38e18edb822b9cb5c3e Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Fri, 23 Aug 2024 16:35:52 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B07662=20Fix=20the=20handling=20of=20PHP?= =?UTF-8?q?=20native=20deprecations=20(e.g.=20call=20strftime=20on=20PHP?= =?UTF-8?q?=208.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/log.class.inc.php | 176 ++++----- .../core/Log/DeprecatedCallsLogTest.php | 355 ++++++++++++------ 2 files changed, 323 insertions(+), 208 deletions(-) diff --git a/core/log.class.inc.php b/core/log.class.inc.php index 3eb3013e1..a1688afdc 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -1173,7 +1173,7 @@ class DeprecatedCallsLog extends LogAPI /** * This will catch a message for all E_DEPRECATED and E_USER_DEPRECATED errors. - * This handler is set in DeprecatedCallsLog::Enable + * This handler is set in {@see DeprecatedCallsLog::Enable} * * @param int $errno * @param string $errstr @@ -1193,52 +1193,22 @@ class DeprecatedCallsLog extends LogAPI return false; } + $aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4); + if (isset($aStack[2]['function']) && ($aStack[2]['function'] == 'ForwardToTriggerError')) { + // Let the notice bubble up + return false; + } + if (false === static::IsLogLevelEnabledSafe(self::LEVEL_WARNING, self::ENUM_CHANNEL_PHP_LIBMETHOD)) { - // returns true so that nothing is throwned ! + // returns true so that nothing is thrown! return true; } - $aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4); - $iStackDeprecatedMethodLevel = 2; // level 0 = current method, level 1 = @trigger_error, level 2 = method containing the `trigger_error` call (can be either 'trigger_deprecation' or the faulty method), level 3 = In some cases, method containing the 'trigger_deprecation' call - // In case current level is actually a 'trigger_deprecation' call, try to go one level further to get the real deprecated method - if (array_key_exists($iStackDeprecatedMethodLevel, $aStack) && ($aStack[$iStackDeprecatedMethodLevel]['function'] === 'trigger_deprecation') && array_key_exists($iStackDeprecatedMethodLevel + 1, $aStack)) { - $iStackDeprecatedMethodLevel++; - } - - $sDeprecatedObject = $aStack[$iStackDeprecatedMethodLevel]['class']; - $sDeprecatedMethod = $aStack[$iStackDeprecatedMethodLevel]['function']; - if (($sDeprecatedObject === __CLASS__) && ($sDeprecatedMethod === 'Log')) { - // We are generating a trigger_error ourselves, we don't want to trace them ! - return false; - } - $sCallerFile = $aStack[$iStackDeprecatedMethodLevel]['file']; - $sCallerLine = $aStack[$iStackDeprecatedMethodLevel]['line']; - $sMessage = "Call to {$sDeprecatedObject}::{$sDeprecatedMethod} in {$sCallerFile}#L{$sCallerLine}"; - - $iStackCallerMethodLevel = $iStackDeprecatedMethodLevel + 1; // level 3 = caller of the deprecated method - if (array_key_exists($iStackCallerMethodLevel, $aStack)) { - $sCallerObject = $aStack[$iStackCallerMethodLevel]['class'] ?? null; - $sCallerMethod = $aStack[$iStackCallerMethodLevel]['function'] ?? null; - $sMessage .= ' ('; - if (!is_null($sCallerObject)) { - $sMessage .= "{$sCallerObject}::{$sCallerMethod}"; - } else { - $sCallerMethodFile = $aStack[$iStackCallerMethodLevel]['file']; - $sCallerMethodLine = $aStack[$iStackCallerMethodLevel]['line']; - if (!is_null($sCallerMethod)) { - $sMessage .= "call to {$sCallerMethod}() in {$sCallerMethodFile}#L{$sCallerMethodLine}"; - } else { - $sMessage .= "{$sCallerMethodFile}#L{$sCallerMethodLine}"; - } - } - $sMessage .= ')'; - } - - if (!empty($errstr)) { - $sMessage .= ' : '.$errstr; - } + $aStack = static::StripCallStack($aStack); + $sMessage = "$errstr, called from ".static::SummarizeCallStack($aStack); static::Warning($sMessage, self::ENUM_CHANNEL_PHP_LIBMETHOD); + static::ForwardToTriggerError($sMessage); return true; } @@ -1264,6 +1234,8 @@ class DeprecatedCallsLog extends LogAPI } /** + * Call this helper at the beginning of a deprecated file (in its global scope) + * * @since 3.0.1 3.1.0 N°4725 silently handles ConfigException * @since 3.0.4 3.1.0 N°4725 remove forgotten throw PHPDoc annotation * @@ -1298,9 +1270,12 @@ class DeprecatedCallsLog extends LogAPI } static::Warning($sMessage, static::ENUM_CHANNEL_FILE); + static::ForwardToTriggerError($sMessage); } /** + * Call this helper when calling a deprecated extension method + * * @param string $sImplementationClass Class implementing the deprecated API * @param string $sDeprecatedApi Class name of the deprecated API * @param string $sDeprecatedMethod Method name of the deprecated API @@ -1327,9 +1302,12 @@ class DeprecatedCallsLog extends LogAPI } static::Warning($sMessage, self::ENUM_CHANNEL_PHP_API); + static::ForwardToTriggerError($sMessage); } /** + * Call this helper within deprecated methods + * * @param string|null $sAdditionalMessage * * @link https://www.php.net/debug_backtrace @@ -1347,52 +1325,24 @@ class DeprecatedCallsLog extends LogAPI } $aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - $sMessage = self::GetMessageFromStack($aStack); - if (!is_null($sAdditionalMessage)) { - $sMessage .= ' : '.$sAdditionalMessage; + if (isset($aStack[1]['class'])) { + $sFunctionDesc = $aStack[1]['class'].$aStack[1]['type'].$aStack[1]['function']; } + else { + $sFunctionDesc = $aStack[1]['function']; + } + + $sMessage = "Function $sFunctionDesc() is deprecated"; + if (!is_null($sAdditionalMessage)) { + $sMessage .= ': '.$sAdditionalMessage; + } + + $sMessage .= '. Caller: '.self::SummarizeCallStack(array_slice($aStack, 1)); + static::Warning($sMessage, self::ENUM_CHANNEL_PHP_METHOD); - } - - /** - * @param array $aDebugBacktrace data from {@see debug_backtrace()} - * - * @return string message to print to the log - */ - private static function GetMessageFromStack(array $aDebugBacktrace): string - { - // level 0 = current method - // level 1 = deprecated method, containing the `NotifyDeprecatedPhpMethod` call - $sMessage = 'Call'.self::GetMessageForCurrentStackLevel($aDebugBacktrace[1], " to "); - - // level 2 = caller of the deprecated method - if (array_key_exists(2, $aDebugBacktrace)) { - $sMessage .= ' (from '; - $sMessage .= self::GetMessageForCurrentStackLevel($aDebugBacktrace[2]); - $sMessage .= ')'; - } - - return $sMessage; - } - - private static function GetMessageForCurrentStackLevel(array $aCurrentLevelDebugTrace, ?string $sPrefix = ""): string - { - $sMessage = ""; - if (array_key_exists('class', $aCurrentLevelDebugTrace)) { - $sDeprecatedObject = $aCurrentLevelDebugTrace['class']; - $sDeprecatedMethod = $aCurrentLevelDebugTrace['function'] ?? ""; - $sMessage = "{$sPrefix}{$sDeprecatedObject}::{$sDeprecatedMethod} in "; - } - - if (array_key_exists('file', $aCurrentLevelDebugTrace)) { - $sCallerFile = $aCurrentLevelDebugTrace['file']; - $sCallerLine = $aCurrentLevelDebugTrace['line'] ?? ""; - $sMessage .= "{$sCallerFile}#L{$sCallerLine}"; - } - - return $sMessage; + static::ForwardToTriggerError($sMessage); } /** @@ -1422,14 +1372,11 @@ class DeprecatedCallsLog extends LogAPI } static::Warning($sMessage, self::ENUM_CHANNEL_PHP_ENDPOINT); + static::ForwardToTriggerError($sMessage); } public static function Log($sLevel, $sMessage, $sChannel = null, $aContext = array()): void { - if (true === utils::IsDevelopmentEnvironment()) { - trigger_error($sMessage, E_USER_DEPRECATED); - } - try { parent::Log($sLevel, $sMessage, $sChannel, $aContext); } @@ -1437,6 +1384,61 @@ class DeprecatedCallsLog extends LogAPI // nothing much we can do... and we don't want to crash the caller ! } } + + /** + * Strips some elements from the top of the call stack to skip calls that are not relevant to report the deprecated call + * @param array $aCallStack Call stack as returned by {@see debug_backtrace()} + */ + protected static function StripCallStack($aCallStack): array + { + if (!isset($aCallStack[0]['line'])) { + $aCallStack = array_slice($aCallStack, 1); + } + if (isset($aCallStack[1]['function']) && $aCallStack[1]['function'] === 'trigger_deprecation') { + $aCallStack = array_slice($aCallStack, 1); + } + + return $aCallStack; + } + + protected static function SummarizeCallStack($aCallStack, $bRecurse = true) + { + if (count($aCallStack) == 0) { + return null; + } + $sFileLine = $aCallStack[0]['file'].'#'.$aCallStack[0]['line']; + $sSummary = $sFileLine; + + // If possible and meaningful, add the class and method + if (isset($aCallStack[1]['class'])) { + $sSummary = $aCallStack[1]['class'].$aCallStack[1]['type'].$aCallStack[1]['function']." ($sFileLine)"; + } + elseif (isset($aCallStack[1]['function'])) { + if (in_array($aCallStack[1]['function'], ['include', 'require', 'include_once', 'require_once'])) { + // No need to show the generic mechanism of inclusion + $bRecurse = false; + } + else { + $sSummary = $aCallStack[1]['function']." ($sFileLine)"; + } + } + + if ($bRecurse) { + $sUpperSummary = static::SummarizeCallStack(array_slice($aCallStack, 1), false); + if (!is_null($sUpperSummary)) { + $sSummary .= ', itself called from '.$sUpperSummary; + } + } + + return $sSummary; + } + + private static function ForwardToTriggerError(string $sMessage): void + { + if (true === utils::IsDevelopmentEnvironment()) { + trigger_error($sMessage, E_USER_DEPRECATED); + } + } } diff --git a/tests/php-unit-tests/unitary-tests/core/Log/DeprecatedCallsLogTest.php b/tests/php-unit-tests/unitary-tests/core/Log/DeprecatedCallsLogTest.php index 3c602c07b..5f948be1d 100644 --- a/tests/php-unit-tests/unitary-tests/core/Log/DeprecatedCallsLogTest.php +++ b/tests/php-unit-tests/unitary-tests/core/Log/DeprecatedCallsLogTest.php @@ -13,143 +13,256 @@ use DeprecatedCallsLog; class DeprecatedCallsLogTest extends ItopTestCase { /** - * We are testing for a undefined offset error. This was throwing a Notice, but starting with PHP 8.0 it was converted to a Warning ! Also the message was changed :( - * - * @link https://www.php.net/manual/en/migration80.incompatible.php check "A number of notices have been converted into warnings:" + * @dataProvider StripCallStackProvider */ - private function SetUndefinedOffsetExceptionToExpect(): void + public function testStripCallStack($sInputStack, $sExpectedStack) { - /** @noinspection ConstantCanBeUsedInspection Preferring the function call as it is easier to read and won't cost that much in this PHPUnit context */ - if (version_compare(PHP_VERSION, '8.0', '>=')) { - $this->expectWarning(); - $sUndefinedOffsetExceptionMessage = 'Undefined array key "tutu"'; - } else { - $this->expectNotice(); - $sUndefinedOffsetExceptionMessage = 'Undefined index: tutu'; - } - $this->expectExceptionMessage($sUndefinedOffsetExceptionMessage); + $this->assertEquals( + $sExpectedStack, + $this->InvokeNonPublicStaticMethod(DeprecatedCallsLog::class, 'StripCallStack', [$sInputStack]), + 'The top item of the call stack should be the first item meaningful to track deprecated calls, not the intermediate layers of the PHP engine' + ); } - public function testPhpNoticeWithoutDeprecatedCallsLog(): void - { - $this->SetUndefinedOffsetExceptionToExpect(); - - $aArray = []; - if ('toto' === $aArray['tutu']) { - //Do nothing, just raising a undefined offset warning - } - } - - /** - * @runInSeparateProcess Necessary, due to the DeprecatedCallsLog being enabled (no mean to reset) - * - * The error handler set by DeprecatedCallsLog during startup was causing PHPUnit to miss PHP notices like "undefined offset" - * - * The error handler is now disabled when running PHPUnit - * - * @since 3.0.4 N°6274 - * @covers DeprecatedCallsLog::DeprecatedNoticesErrorHandler - */ - public function testPhpNoticeWithDeprecatedCallsLog(): void - { - $this->RequireOnceItopFile('core/log.class.inc.php'); - DeprecatedCallsLog::Enable(); // will set error handler - $this->SetUndefinedOffsetExceptionToExpect(); - - $aArray = []; - if ('toto' === $aArray['tutu']) { - //Do nothing, just raising a undefined offset warning - } - } - - /** - * @dataProvider GetMessageFromStackProvider - */ - public function testGetMessageFromStack($aDebugBacktrace, $sExpectedMessage): void - { - $sActualMessage = $this->InvokeNonPublicStaticMethod(DeprecatedCallsLog::class, 'GetMessageFromStack', [$aDebugBacktrace]); - $this->assertEquals($sExpectedMessage, $sActualMessage); - } - - public function GetMessageFromStackProvider() + public function StripCallStackProvider() { return [ - 'Call in a file outside of a function or class' => [ - [ + /* A deprecated PHP method is invoked from scratch.php, at line 25 (note: the name of the method is not present in the callstack) */ + 'Should preserve the handler when the notice is fired by PHP itself' => [ + 'in' => [ + [ + 'file' => 'whateverfolder/scratch.php', + 'line' => 25, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + ], + 'out' => [ + [ + 'file' => 'whateverfolder/scratch.php', + 'line' => 25, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + ], + ], + 'Should skip the handler when the notice is fired by a call to trigger_error' => [ + 'in' => [ [ - 'file' => 'C:\Dev\wamp64\www\itop-32\sources\Application\WebPage\WebPage.php', - 'line' => '866', - 'function' => 'NotifyDeprecatedPhpMethod', - 'class' => 'DeprecatedCallsLog', - 'type' => '::', + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', ], [ - 'file' => 'C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php', - 'line' => '130', - 'function' => 'add_linked_script', - 'class' => 'Combodo\iTop\Application\WebPage\WebPage', - 'type' => '->', + 'file' => 'whateverfolder/scratch.php', + 'line' => 25, + 'function' => 'trigger_error', + ], + ], + 'out' => [ + [ + 'file' => 'whateverfolder/scratch.php', + 'line' => 25, + 'function' => 'trigger_error', + ], + ], + ], + 'Should skip two levels when the notice is fired by trigger_deprecation (Symfony helper)' => [ + 'in' => [ + [ + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', ], [ - 'file' => 'C:\Dev\wamp64\www\itop-32\pages\exec.php', - 'line' => '102', - 'args' => ['C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php'], + 'file' => 'symfony/deprecation.php', + 'line' => 12, + 'function' => 'trigger_error', + ], + [ + 'file' => 'symfony/service.php', + 'line' => 25, + 'function' => 'trigger_deprecation', + ], + ], + 'out' => [ + [ + 'file' => 'symfony/service.php', + 'line' => 25, + 'function' => 'trigger_deprecation', + ], + ], + ], + ]; + } + + /** + * @dataProvider SummarizeCallStackProvider + */ + public function testSummarizeCallStack($sInputStackStripped, $sExpectedSummary) + { + $this->assertEquals( + $sExpectedSummary, + $this->InvokeNonPublicStaticMethod(DeprecatedCallsLog::class, 'SummarizeCallStack', [$sInputStackStripped]) + ); + } + + public function SummarizeCallStackProvider() + { + // All tests are based on a call stack issued from a deprecated PHP function + // Other cases are similar: what counts on the top level item is the file and line number of the deprecated call + return [ + 'From the main page (deprecated PHP function)' => [ + 'in:stripped call stack' => [ + [ + 'file' => 'whateverfolder/scratch.php', + 'line' => 25, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + ], + 'out' => 'whateverfolder/scratch.php#25', + ], + 'From an iTop method (deprecated PHP function)' => [ + 'in:stripped call stack' => [ + [ + 'file' => 'whateverfolder/someclass.php', + 'line' => 18, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + [ + 'file' => 'whateverfolder/index.php', + 'line' => 25, + 'function' => 'SomeMethod', + 'class' => 'SomeClass', + 'type' => '->', + ], + ], + 'out' => 'SomeClass->SomeMethod (whateverfolder/someclass.php#18), itself called from whateverfolder/index.php#25', + ], + 'From an iTop static method (deprecated PHP function)' => [ + 'in:stripped call stack' => [ + [ + 'file' => 'whateverfolder/someclass.php', + 'line' => 18, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + [ + 'file' => 'whateverfolder/index.php', + 'line' => 25, + 'function' => 'SomeMethod', + 'class' => 'SomeClass', + 'type' => '::', + ], + ], + 'out' => 'SomeClass::SomeMethod (whateverfolder/someclass.php#18), itself called from whateverfolder/index.php#25', + ], + 'From an iTop function (deprecated PHP function)' => [ + 'in:stripped call stack' => [ + [ + 'file' => 'whateverfolder/someclass.php', + 'line' => 18, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + [ + 'file' => 'whateverfolder/index.php', + 'line' => 25, + 'function' => 'SomeFunction', + ], + ], + 'out' => 'SomeFunction (whateverfolder/someclass.php#18), itself called from whateverfolder/index.php#25', + ], + 'From a code snippet (deprecated PHP function)' => [ + 'in:stripped call stack' => [ + [ + 'file' => 'itop-root/env-production/core/main.php', + 'line' => 1290, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + [ + 'file' => 'itop-root/core/metamodel.class.php', + 'line' => 6698, + 'function' => 'require_once', + ], + [ + 'file' => 'itop-root/env-production/autoload.php', + 'line' => 6, + 'function' => 'IncludeModule', + 'class' => 'MetaModel', + 'type' => '::', + ], + [ + 'file' => 'itop-root/core/metamodel.class.php', + 'line' => 6487, 'function' => 'require_once', ], ], - 'Call to Combodo\iTop\Application\WebPage\WebPage::add_linked_script in C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php#L130 (from C:\Dev\wamp64\www\itop-32\pages\exec.php#L102)', + 'out' => 'itop-root/env-production/core/main.php#1290' ], - - 'Call in a file function, outside of a class' => [ - [ - [ - 'file' => 'C:\\Dev\\wamp64\\www\\itop-32\\sources\\Application\\WebPage\\WebPage.php', - 'line' => 866, - 'function' => 'NotifyDeprecatedPhpMethod', - 'class' => 'DeprecatedCallsLog', - 'type' => '::', - ], - [ - 'file' => 'C:\\Dev\\wamp64\\www\\itop-32\\extensions\\itop-object-copier\\copy.php', - 'line' => 81, - 'function' => 'add_linked_script', - 'class' => 'Combodo\\iTop\\Application\\WebPage\\WebPage', - 'type' => '->', - ], - [ - 'file' => 'C:\\Dev\\wamp64\\www\\itop-32\\extensions\\itop-object-copier\\copy.php', - 'line' => 123, - 'function' => 'myFunction', - ], + 'From a persistent object method (deprecated PHP function)' => [ + 'in:stripped call stack' => [ + [ + 'file' => 'itop-root/env-production/itop-tickets/model.itop-tickets.php', + 'line' => 165, + 'function' => 'DeprecatedNoticesErrorHandler', + 'class' => 'DeprecatedCallsLog', + 'type' => '::', + ], + [ + 'file' => 'itop-root/core/dbobject.class.php', + 'line' => 6575, + 'function' => 'OnBeforeWriteTicket', + 'class' => 'Ticket', + 'type' => '->', + ], + [ + 'file' => 'itop-root/application/cmdbabstract.class.inc.php', + 'line' => 5933, + 'function' => 'FireEvent', + 'class' => 'DBObject', + 'type' => '->', + ], + [ + 'file' => 'itop-root/core/dbobject.class.php', + 'line' => 3643, + 'function' => 'FireEventBeforeWrite', + 'class' => 'cmdbAbstractObject', + 'type' => '->', + ], + [ + 'file' => 'itop-root/application/cmdbabstract.class.inc.php', + 'line' => 4593, + 'function' => 'DBUpdate', + 'class' => 'DBObject', + 'type' => '->', + ], + [ + 'file' => 'itop-root/sources/Controller/Base/Layout/ObjectController.php', + 'line' => 649, + 'function' => 'DBUpdate', + 'class' => 'cmdbAbstractObject', + 'type' => '->', + ], + [ + 'file' => 'itop-root/pages/UI.php', + 'line' => 720, + 'function' => 'OperationApplyModify', + 'class' => 'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController', + 'type' => '->', + ], ], - 'Call to Combodo\iTop\Application\WebPage\WebPage::add_linked_script in C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php#L81 (from C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php#L123)', - ], - - 'Call from a class method' => [ - [ - [ - 'file' => 'C:\\Dev\\wamp64\\www\\itop-32\\sources\\Application\\WebPage\\WebPage.php', - 'line' => 866, - 'function' => 'NotifyDeprecatedPhpMethod', - 'class' => 'DeprecatedCallsLog', - 'type' => '::', - ], - [ - 'file' => 'C:\\Dev\\wamp64\\www\\itop-32\\extensions\\itop-object-copier\\copy.php', - 'line' => 82, - 'function' => 'add_linked_script', - 'class' => 'Combodo\\iTop\\Application\\WebPage\\WebPage', - 'type' => '->', - ], - [ - 'file' => 'C:\\Dev\\wamp64\\www\\itop-32\\extensions\\itop-object-copier\\copy.php', - 'line' => 125, - 'function' => 'MyMethod', - 'class' => 'MyClass', - 'type' => '::', - ], - ], - 'Call to Combodo\iTop\Application\WebPage\WebPage::add_linked_script in C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php#L82 (from MyClass::MyMethod in C:\Dev\wamp64\www\itop-32\extensions\itop-object-copier\copy.php#L125)', + 'out' => 'Ticket->OnBeforeWriteTicket (itop-root/env-production/itop-tickets/model.itop-tickets.php#165), itself called from DBObject->FireEvent (itop-root/core/dbobject.class.php#6575)' ], ]; }