Added a ->setPHP7CompatMode() that will fix undefined array keys etc from triggering E_WARNINGS.

This commit is contained in:
Simon Wisselink
2021-01-15 17:39:52 +01:00
parent 992ba3d828
commit 1a052b6a77
6 changed files with 157 additions and 109 deletions

View File

@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- You can now use `$smarty->setPHP7CompatMode()` to activate php7 compatibility mode when running PHP8
### Changed ### Changed
- Switch CI from Travis to Github CI - Switch CI from Travis to Github CI
- Updated unit tests to avoid skipped and risky test warnings - Updated unit tests to avoid skipped and risky test warnings

View File

@@ -640,6 +640,12 @@ class Smarty extends Smarty_Internal_TemplateBase
'cache_dir' => 'CacheDir', 'cache_dir' => 'CacheDir',
); );
/**
* PHP7 Compatibility mode
* @var bool
*/
private $php7CompatMode = false;
/** /**
* Initialize new Smarty object * Initialize new Smarty object
*/ */
@@ -1369,4 +1375,23 @@ class Smarty extends Smarty_Internal_TemplateBase
$isConfig ? $this->_joined_config_dir = join('#', $this->config_dir) : $isConfig ? $this->_joined_config_dir = join('#', $this->config_dir) :
$this->_joined_template_dir = join('#', $this->template_dir); $this->_joined_template_dir = join('#', $this->template_dir);
} }
/**
* Activates PHP7 compatibility mode:
* - converts E_WARNINGS for "undefined array key" and "trying to read property of null" errors to E_NOTICE
*
* @void
*/
public function setPHP7CompatMode(): void {
$this->php7CompatMode = true;
}
/**
* Indicates if PHP7 compatibility mode is set.
* @bool
*/
public function getPHP7CompatMode(): bool {
return $this->php7CompatMode;
}
} }

View File

@@ -1,55 +1,54 @@
<?php <?php
/** /**
* Smarty error handler * Smarty error handler to fix new error levels in PHP8 for backwards compatibility
* *
* @package Smarty * @package Smarty
* @subpackage PluginsInternal * @subpackage PluginsInternal
* @author Uwe Tews * @author Simon Wisselink
* *
* @deprecated
Smarty does no longer use @filemtime()
*/ */
class Smarty_Internal_ErrorHandler class Smarty_Internal_ErrorHandler
{ {
/**
* contains directories outside of SMARTY_DIR that are to be muted by muteExpectedErrors()
*/
public static $mutedDirectories = array();
/** /**
* error handler returned by set_error_handler() in self::muteExpectedErrors() * Allows {$foo} where foo is unset.
* @var bool
*/ */
private static $previousErrorHandler = null; public $allowUndefinedVars = true;
/** /**
* Enable error handler to mute expected messages * Allows {$foo.bar} where bar is unset and {$foo.bar1.bar2} where either bar1 or bar2 is unset.
* * @var bool
*/ */
public static function muteExpectedErrors() public $allowUndefinedArrayKeys = true;
{
private $previousErrorHandler = null;
/**
* Enable error handler to intercept errors
*/
public function activate() {
/* /*
error muting is done because some people implemented custom error_handlers using Error muting is done because some people implemented custom error_handlers using
http://php.net/set_error_handler and for some reason did not understand the following paragraph: http://php.net/set_error_handler and for some reason did not understand the following paragraph:
It is important to remember that the standard PHP error handler is completely bypassed for the It is important to remember that the standard PHP error handler is completely bypassed for the
error types specified by error_types unless the callback function returns FALSE. error types specified by error_types unless the callback function returns FALSE.
error_reporting() settings will have no effect and your error handler will be called regardless - error_reporting() settings will have no effect and your error handler will be called regardless -
however you are still able to read the current value of error_reporting and act appropriately. however you are still able to read the current value of error_reporting and act appropriately.
Of particular note is that this value will be 0 if the statement that caused the error was Of particular note is that this value will be 0 if the statement that caused the error was
prepended by the @ error-control operator. prepended by the @ error-control operator.
Smarty deliberately uses @filemtime() over file_exists() and filemtime() in some places. Reasons include
- @filemtime() is almost twice as fast as using an additional file_exists()
- between file_exists() and filemtime() a possible race condition is opened,
which does not exist using the simple @filemtime() approach.
*/ */
$error_handler = array('Smarty_Internal_ErrorHandler', 'mutingErrorHandler'); $this->previousErrorHandler = set_error_handler([$this, 'handleError']);
$previous = set_error_handler($error_handler); }
// avoid dead loops
if ($previous !== $error_handler) { /**
self::$previousErrorHandler = $previous; * Disable error handler
} */
public function deactivate() {
restore_error_handler();
$this->previousErrorHandler = null;
} }
/** /**
@@ -65,49 +64,21 @@ class Smarty_Internal_ErrorHandler
* *
* @return bool * @return bool
*/ */
public static function mutingErrorHandler($errno, $errstr, $errfile, $errline, $errcontext = array()) public function handleError($errno, $errstr, $errfile, $errline, $errcontext = [])
{ {
$_is_muted_directory = false; if ($this->allowUndefinedVars && $errstr == 'Attempt to read property "value" on null') {
// add the SMARTY_DIR to the list of muted directories return; // suppresses this error
if (!isset(self::$mutedDirectories[ SMARTY_DIR ])) {
$smarty_dir = realpath(SMARTY_DIR);
if ($smarty_dir !== false) {
self::$mutedDirectories[ SMARTY_DIR ] =
array('file' => $smarty_dir, 'length' => strlen($smarty_dir),);
}
} }
// walk the muted directories and test against $errfile
foreach (self::$mutedDirectories as $key => &$dir) { if ($this->allowUndefinedArrayKeys && preg_match(
if (!$dir) { '/^(Undefined array key|Trying to access array offset on value of type null)/',
// resolve directory and length for speedy comparisons $errstr
$file = realpath($key); )) {
if ($file === false) { return; // suppresses this error
// this directory does not exist, remove and skip it
unset(self::$mutedDirectories[ $key ]);
continue;
}
$dir = array('file' => $file, 'length' => strlen($file),);
}
if (!strncmp($errfile, $dir[ 'file' ], $dir[ 'length' ])) {
$_is_muted_directory = true;
break;
}
}
// pass to next error handler if this error did not occur inside SMARTY_DIR
// or the error was within smarty but masked to be ignored
if (!$_is_muted_directory || ($errno && $errno & error_reporting())) {
if (self::$previousErrorHandler) {
return call_user_func(
self::$previousErrorHandler,
$errno,
$errstr,
$errfile,
$errline,
$errcontext
);
} else {
return false;
}
} }
// pass all other errors through to the previous error handler or to the default PHP error handler
return $this->previousErrorHandler ?
call_user_func($this->previousErrorHandler, $errno, $errstr, $errfile, $errline, $errcontext) : false;
} }
} }

View File

@@ -199,6 +199,12 @@ abstract class Smarty_Internal_TemplateBase extends Smarty_Internal_Data
try { try {
$_smarty_old_error_level = $_smarty_old_error_level =
isset($smarty->error_reporting) ? error_reporting($smarty->error_reporting) : null; isset($smarty->error_reporting) ? error_reporting($smarty->error_reporting) : null;
if ($smarty->getPHP7CompatMode()) {
$errorHandler = new Smarty_Internal_ErrorHandler();
$errorHandler->activate();
}
if ($this->_objType === 2) { if ($this->_objType === 2) {
/* @var Smarty_Internal_Template $this */ /* @var Smarty_Internal_Template $this */
$template->tplFunctions = $this->tplFunctions; $template->tplFunctions = $this->tplFunctions;
@@ -242,6 +248,11 @@ abstract class Smarty_Internal_TemplateBase extends Smarty_Internal_Data
} }
} }
} }
if (isset($errorHandler)) {
$errorHandler->deactivate();
}
if (isset($_smarty_old_error_level)) { if (isset($_smarty_old_error_level)) {
error_reporting($_smarty_old_error_level); error_reporting($_smarty_old_error_level);
} }
@@ -250,9 +261,13 @@ abstract class Smarty_Internal_TemplateBase extends Smarty_Internal_Data
while (ob_get_level() > $level) { while (ob_get_level() > $level) {
ob_end_clean(); ob_end_clean();
} }
if (isset($_smarty_old_error_level)) { if (isset($errorHandler)) {
error_reporting($_smarty_old_error_level); $errorHandler->deactivate();
} }
if (isset($_smarty_old_error_level)) {
error_reporting($_smarty_old_error_level);
}
throw $e; throw $e;
} }
} }

View File

@@ -16,7 +16,6 @@ class UndefinedTemplateVarTest extends PHPUnit_Smarty
public function setUp(): void public function setUp(): void
{ {
$this->setUpSmarty(dirname(__FILE__)); $this->setUpSmarty(dirname(__FILE__));
error_reporting(E_ALL | E_STRICT);
} }
public function testInit() public function testInit()
@@ -48,9 +47,9 @@ class UndefinedTemplateVarTest extends PHPUnit_Smarty
$this->assertEquals($e1, $e2); $this->assertEquals($e1, $e2);
} }
/** /**
* Test Error suppression template object fetched by Smarty object * Test Error suppression template object fetched by Smarty object
*/ */
public function testErrorDisabledTplObject_2() public function testErrorDisabledTplObject_2()
{ {
$e1 = error_reporting(); $e1 = error_reporting();
@@ -66,27 +65,73 @@ class UndefinedTemplateVarTest extends PHPUnit_Smarty
*/ */
public function testError() public function testError()
{ {
$exceptionThrown = false; $exceptionThrown = false;
try { try {
$e1 = error_reporting(); $e1 = error_reporting();
$this->assertEquals('undefined = ', $this->smarty->fetch('001_main.tpl')); $this->assertEquals('undefined = ', $this->smarty->fetch('001_main.tpl'));
$e2 = error_reporting(); $e2 = error_reporting();
$this->assertEquals($e1, $e2); $this->assertEquals($e1, $e2);
} catch (Exception $e) { } catch (Exception $e) {
$exceptionThrown = true; $exceptionThrown = true;
$this->assertStringStartsWith('Undefined ', $e->getMessage()); $this->assertStringStartsWith('Undefined ', $e->getMessage());
$this->assertTrue(in_array( $this->assertTrue(in_array(
get_class($e), get_class($e),
array( [
'PHPUnit_Framework_Error_Warning', 'PHPUnit\Framework\Error\Warning',
'PHPUnit_Framework_Error_Notice', 'PHPUnit\Framework\Error\Notice',
'PHPUnit\Framework\Error\Warning', ]
'PHPUnit\Framework\Error\Notice', ));
)
));
} }
$this->assertTrue($exceptionThrown); $this->assertTrue($exceptionThrown);
} }
public function testUndefinedSimpleVar() {
$this->smarty->setErrorReporting(E_ALL & ~E_NOTICE);
$this->smarty->setPHP7CompatMode();
$tpl = $this->smarty->createTemplate('string:a{if $undef}def{/if}b');
$this->assertEquals("ab", $this->smarty->fetch($tpl));
}
public function testUndefinedArrayIndex() {
$this->smarty->setErrorReporting(E_ALL & ~E_NOTICE);
$this->smarty->setPHP7CompatMode();
$tpl = $this->smarty->createTemplate('string:a{if $ar.undef}def{/if}b');
$tpl->assign('ar', []);
$this->assertEquals("ab", $this->smarty->fetch($tpl));
}
public function testUndefinedArrayIndexDeep() {
$this->smarty->setErrorReporting(E_ALL & ~E_NOTICE);
$this->smarty->setPHP7CompatMode();
$tpl = $this->smarty->createTemplate('string:a{if $ar.undef.nope.neither}def{/if}b');
$tpl->assign('ar', []);
$this->assertEquals("ab", $this->smarty->fetch($tpl));
}
public function testUndefinedArrayIndexError()
{
$exceptionThrown = false;
try {
$tpl = $this->smarty->createTemplate('string:a{if $ar.undef}def{/if}b');
$tpl->assign('ar', []);
$this->smarty->fetch($tpl);
} catch (Exception $e) {
$exceptionThrown = true;
$this->assertStringStartsWith('Undefined ', $e->getMessage());
$this->assertTrue(in_array(
get_class($e),
[
'PHPUnit\Framework\Error\Warning',
'PHPUnit\Framework\Error\Notice',
]
));
}
$this->assertTrue($exceptionThrown);
}
} }

View File

@@ -285,12 +285,7 @@ class PluginFunctionHtmlCheckboxesTest extends PHPUnit_Smarty
$this->_errors = array(); $this->_errors = array();
set_error_handler(array($this, 'error_handler')); set_error_handler(array($this, 'error_handler'));
$n = "\n"; $this->smarty->setPHP7CompatMode();
$expected = '<label><input type="checkbox" name="id[]" value="1000" />Joe Schmoe</label><br />'
. $n . '<label><input type="checkbox" name="id[]" value="1001" checked="checked" />Jack Smith</label><br />'
. $n . '<label><input type="checkbox" name="id[]" value="1002" />Jane Johnson</label><br />'
. $n . '<label><input type="checkbox" name="id[]" value="1003" />Charlie Brown</label><br />';
$tpl = $this->smarty->createTemplate('eval:{html_checkboxes name="id" options=$cust_radios selected=$customer_id separator="<br />"}'); $tpl = $this->smarty->createTemplate('eval:{html_checkboxes name="id" options=$cust_radios selected=$customer_id separator="<br />"}');
$tpl->assign('customer_id', new _object_noString(1001)); $tpl->assign('customer_id', new _object_noString(1001));
$tpl->assign('cust_radios', array( $tpl->assign('cust_radios', array(
@@ -312,12 +307,6 @@ class PluginFunctionHtmlCheckboxesTest extends PHPUnit_Smarty
$this->_errors = array(); $this->_errors = array();
set_error_handler(array($this, 'error_handler')); set_error_handler(array($this, 'error_handler'));
$n = "\n";
$expected = '<label><input type="checkbox" name="id[]" value="1000" />Joe Schmoe</label><br />'
. $n . '<label><input type="checkbox" name="id[]" value="1001" checked="checked" />Jack Smith</label><br />'
. $n . '<label><input type="checkbox" name="id[]" value="1002" />Jane Johnson</label><br />'
. $n . '<label><input type="checkbox" name="id[]" value="1003" />Charlie Brown</label><br />';
$tpl = $this->smarty->createTemplate('eval:{html_checkboxes name="id" options=$cust_radios selected=$customer_id separator="<br />"}'); $tpl = $this->smarty->createTemplate('eval:{html_checkboxes name="id" options=$cust_radios selected=$customer_id separator="<br />"}');
$tpl->assign('customer_id', 1001); $tpl->assign('customer_id', 1001);
$tpl->assign('cust_radios', array( $tpl->assign('cust_radios', array(