Merge pull request #8657 from MrPetovan/bug/8653-catch-template-engine-errors

Catch template engine errors
This commit is contained in:
Michael Vogel 2020-05-19 00:22:45 +02:00 committed by GitHub
commit e315bc3eae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 201 additions and 114 deletions

View file

@ -23,8 +23,8 @@ namespace Friendica\Core;
use Exception; use Exception;
use Friendica\DI; use Friendica\DI;
use Friendica\Render\FriendicaSmarty; use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Render\ITemplateEngine; use Friendica\Render\TemplateEngine;
/** /**
* This class handles Renderer related functions. * This class handles Renderer related functions.
@ -66,28 +66,30 @@ class Renderer
]; ];
/** /**
* This is our template processor * Returns the rendered template output from the template string and variables
* *
* @param string|FriendicaSmarty $s The string requiring macro substitution or an instance of FriendicaSmarty * @param string $template
* @param array $vars Key value pairs (search => replace) * @param array $vars
* * @return string
* @return string substituted string * @throws InternalServerErrorException
* @throws Exception
*/ */
public static function replaceMacros($s, array $vars = []) public static function replaceMacros(string $template, array $vars = [])
{ {
$stamp1 = microtime(true); $stamp1 = microtime(true);
// pass $baseurl to all templates if it isn't set // pass $baseurl to all templates if it isn't set
$vars = array_merge(['$baseurl' => DI::baseUrl()->get()], $vars); $vars = array_merge(['$baseurl' => DI::baseUrl()->get(), '$APP' => DI::app()], $vars);
$t = self::getTemplateEngine(); $t = self::getTemplateEngine();
try { try {
$output = $t->replaceMacros($s, $vars); $output = $t->replaceMacros($template, $vars);
} catch (Exception $e) { } catch (Exception $e) {
echo "<pre><b>" . __FUNCTION__ . "</b>: " . $e->getMessage() . "</pre>"; DI::logger()->critical($e->getMessage(), ['template' => $template, 'vars' => $vars]);
exit(); $message = is_site_admin() ?
$e->getMessage() :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new InternalServerErrorException($message);
} }
DI::profiler()->saveTimestamp($stamp1, "rendering", System::callstack()); DI::profiler()->saveTimestamp($stamp1, "rendering", System::callstack());
@ -98,22 +100,25 @@ class Renderer
/** /**
* Load a given template $s * Load a given template $s
* *
* @param string $s Template to load. * @param string $file Template to load.
* @param string $subDir Subdirectory (Optional) * @param string $subDir Subdirectory (Optional)
* *
* @return string template. * @return string template.
* @throws Exception * @throws InternalServerErrorException
*/ */
public static function getMarkupTemplate($s, $subDir = '') public static function getMarkupTemplate($file, $subDir = '')
{ {
$stamp1 = microtime(true); $stamp1 = microtime(true);
$t = self::getTemplateEngine(); $t = self::getTemplateEngine();
try { try {
$template = $t->getTemplateFile($s, $subDir); $template = $t->getTemplateFile($file, $subDir);
} catch (Exception $e) { } catch (Exception $e) {
echo "<pre><b>" . __FUNCTION__ . "</b>: " . $e->getMessage() . "</pre>"; DI::logger()->critical($e->getMessage(), ['file' => $file, 'subDir' => $subDir]);
exit(); $message = is_site_admin() ?
$e->getMessage() :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new InternalServerErrorException($message);
} }
DI::profiler()->saveTimestamp($stamp1, "file", System::callstack()); DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
@ -125,18 +130,22 @@ class Renderer
* Register template engine class * Register template engine class
* *
* @param string $class * @param string $class
* @throws InternalServerErrorException
*/ */
public static function registerTemplateEngine($class) public static function registerTemplateEngine($class)
{ {
$v = get_class_vars($class); $v = get_class_vars($class);
if (!empty($v['name'])) if (!empty($v['name'])) {
{
$name = $v['name']; $name = $v['name'];
self::$template_engines[$name] = $class; self::$template_engines[$name] = $class;
} else { } else {
echo "template engine <tt>$class</tt> cannot be registered without a name.\n"; $admin_message = DI::l10n()->t('template engine cannot be registered without a name.');
die(); DI::logger()->critical($admin_message, ['class' => $class]);
$message = is_site_admin() ?
$admin_message :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new InternalServerErrorException($message);
} }
} }
@ -146,7 +155,8 @@ class Renderer
* If $name is not defined, return engine defined by theme, * If $name is not defined, return engine defined by theme,
* or default * or default
* *
* @return ITemplateEngine Template Engine instance * @return TemplateEngine Template Engine instance
* @throws InternalServerErrorException
*/ */
public static function getTemplateEngine() public static function getTemplateEngine()
{ {
@ -156,15 +166,20 @@ class Renderer
if (isset(self::$template_engine_instance[$template_engine])) { if (isset(self::$template_engine_instance[$template_engine])) {
return self::$template_engine_instance[$template_engine]; return self::$template_engine_instance[$template_engine];
} else { } else {
$a = DI::app();
$class = self::$template_engines[$template_engine]; $class = self::$template_engines[$template_engine];
$obj = new $class; $obj = new $class($a->getCurrentTheme(), $a->theme_info);
self::$template_engine_instance[$template_engine] = $obj; self::$template_engine_instance[$template_engine] = $obj;
return $obj; return $obj;
} }
} }
echo "template engine <tt>$template_engine</tt> is not registered!\n"; $admin_message = DI::l10n()->t('template engine is not registered!');
exit(); DI::logger()->critical($admin_message, ['template_engine' => $template_engine]);
$message = is_site_admin() ?
$admin_message :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new InternalServerErrorException($message);
} }
/** /**

View file

@ -535,6 +535,6 @@ class Tag
} }
} }
return Strings::startsWith($tag, $tag_chars); return Strings::startsWithChars($tag, $tag_chars);
} }
} }

View file

@ -31,7 +31,9 @@ use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Register; use Friendica\Model\Register;
use Friendica\Module\BaseAdmin; use Friendica\Module\BaseAdmin;
use Friendica\Module\Update\Profile;
use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Render\FriendicaSmarty;
use Friendica\Util\ConfigFileLoader; use Friendica\Util\ConfigFileLoader;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network; use Friendica\Util\Network;
@ -46,6 +48,14 @@ class Summary extends BaseAdmin
// are there MyISAM tables in the DB? If so, trigger a warning message // are there MyISAM tables in the DB? If so, trigger a warning message
$warningtext = []; $warningtext = [];
$templateEngine = Renderer::getTemplateEngine();
$errors = [];
$templateEngine->testInstall($errors);
foreach ($errors as $error) {
$warningtext[] = DI::l10n()->t('Template engine (%s) error: %s', $templateEngine::$name, $error);
}
if (DBA::count(['information_schema' => 'tables'], ['engine' => 'myisam', 'table_schema' => DBA::databaseName()])) { if (DBA::count(['information_schema' => 'tables'], ['engine' => 'myisam', 'table_schema' => DBA::databaseName()])) {
$warningtext[] = DI::l10n()->t('Your DB still runs with MyISAM tables. You should change the engine type to InnoDB. As Friendica will use InnoDB only features in the future, you should change this! See <a href="%s">here</a> for a guide that may be helpful converting the table engines. You may also use the command <tt>php bin/console.php dbstructure toinnodb</tt> of your Friendica installation for an automatic conversion.<br />', 'https://dev.mysql.com/doc/refman/5.7/en/converting-tables-to-innodb.html'); $warningtext[] = DI::l10n()->t('Your DB still runs with MyISAM tables. You should change the engine type to InnoDB. As Friendica will use InnoDB only features in the future, you should change this! See <a href="%s">here</a> for a guide that may be helpful converting the table engines. You may also use the command <tt>php bin/console.php dbstructure toinnodb</tt> of your Friendica installation for an automatic conversion.<br />', 'https://dev.mysql.com/doc/refman/5.7/en/converting-tables-to-innodb.html');
} }
@ -136,7 +146,6 @@ class Summary extends BaseAdmin
throw new InternalServerErrorException('Stream is null.'); throw new InternalServerErrorException('Stream is null.');
} }
} }
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
$warningtext[] = DI::l10n()->t('The debug logfile \'%s\' is not usable. No logging possible (error: \'%s\')', $file, $exception->getMessage()); $warningtext[] = DI::l10n()->t('The debug logfile \'%s\' is not usable. No logging possible (error: \'%s\')', $file, $exception->getMessage());
} }

View file

@ -21,7 +21,6 @@
namespace Friendica\Render; namespace Friendica\Render;
use Friendica\DI;
use Smarty; use Smarty;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
@ -34,26 +33,23 @@ class FriendicaSmarty extends Smarty
public $filename; public $filename;
function __construct() function __construct(string $theme, array $theme_info)
{ {
parent::__construct(); parent::__construct();
$a = DI::app();
$theme = $a->getCurrentTheme();
// setTemplateDir can be set to an array, which Smarty will parse in order. // setTemplateDir can be set to an array, which Smarty will parse in order.
// The order is thus very important here // The order is thus very important here
$template_dirs = ['theme' => "view/theme/$theme/" . self::SMARTY3_TEMPLATE_FOLDER . "/"]; $template_dirs = ['theme' => "view/theme/$theme/" . self::SMARTY3_TEMPLATE_FOLDER . "/"];
if (!empty($a->theme_info['extends'])) { if (!empty($theme_info['extends'])) {
$template_dirs = $template_dirs + ['extends' => "view/theme/" . $a->theme_info["extends"] . "/" . self::SMARTY3_TEMPLATE_FOLDER . "/"]; $template_dirs = $template_dirs + ['extends' => "view/theme/" . $theme_info["extends"] . "/" . self::SMARTY3_TEMPLATE_FOLDER . "/"];
} }
$template_dirs = $template_dirs + ['base' => "view/" . self::SMARTY3_TEMPLATE_FOLDER . "/"]; $template_dirs = $template_dirs + ['base' => "view/" . self::SMARTY3_TEMPLATE_FOLDER . "/"];
$this->setTemplateDir($template_dirs); $this->setTemplateDir($template_dirs);
$this->setCompileDir('view/smarty3/compiled/'); $this->setCompileDir('view/smarty3/compiled/');
$this->setConfigDir('view/smarty3/config/'); $this->setConfigDir('view/smarty3/');
$this->setCacheDir('view/smarty3/cache/'); $this->setCacheDir('view/smarty3/');
$this->left_delimiter = Renderer::getTemplateLeftDelimiter('smarty3'); $this->left_delimiter = Renderer::getTemplateLeftDelimiter('smarty3');
$this->right_delimiter = Renderer::getTemplateRightDelimiter('smarty3'); $this->right_delimiter = Renderer::getTemplateRightDelimiter('smarty3');
@ -63,13 +59,4 @@ class FriendicaSmarty extends Smarty
// Don't report errors so verbosely // Don't report errors so verbosely
$this->error_reporting = E_ALL & ~E_NOTICE; $this->error_reporting = E_ALL & ~E_NOTICE;
} }
}
function parsed($template = '')
{
if ($template) {
return $this->fetch('string:' . $template);
}
return $this->fetch('file:' . $this->filename);
}
}

View file

@ -23,56 +23,82 @@ namespace Friendica\Render;
use Friendica\Core\Hook; use Friendica\Core\Hook;
use Friendica\DI; use Friendica\DI;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Util\Strings;
/** /**
* Smarty implementation of the Friendica template engine interface * Smarty implementation of the Friendica template abstraction
*/ */
class FriendicaSmartyEngine implements ITemplateEngine final class FriendicaSmartyEngine extends TemplateEngine
{ {
static $name = "smarty3"; static $name = "smarty3";
public function __construct() const FILE_PREFIX = 'file:';
const STRING_PREFIX = 'string:';
/** @var FriendicaSmarty */
private $smarty;
/**
* @inheritDoc
*/
public function __construct(string $theme, array $theme_info)
{ {
if (!is_writable(__DIR__ . '/../../view/smarty3/')) { $this->theme = $theme;
echo "<b>ERROR:</b> folder <tt>view/smarty3/</tt> must be writable by webserver."; $this->theme_info = $theme_info;
exit(); $this->smarty = new FriendicaSmarty($this->theme, $this->theme_info);
if (!is_writable(DI::basePath() . '/view/smarty3')) {
$admin_message = DI::l10n()->t('The folder view/smarty3/ must be writable by webserver.');
DI::logger()->critical($admin_message);
$message = is_site_admin() ?
$admin_message :
DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.');
throw new InternalServerErrorException($message);
} }
} }
// ITemplateEngine interface /**
public function replaceMacros($s, $r) * @inheritDoc
*/
public function testInstall(array &$errors = null)
{ {
$template = ''; $this->smarty->testInstall($errors);
if (gettype($s) === 'string') { }
$template = $s;
$s = new FriendicaSmarty();
}
$r['$APP'] = DI::app(); /**
* @inheritDoc
*/
public function replaceMacros(string $template, array $vars)
{
if (!Strings::startsWith($template, self::FILE_PREFIX)) {
$template = self::STRING_PREFIX . $template;
}
// "middleware": inject variables into templates // "middleware": inject variables into templates
$arr = [ $arr = [
"template" => basename($s->filename), 'template' => basename($this->smarty->filename),
"vars" => $r 'vars' => $vars
]; ];
Hook::callAll("template_vars", $arr); Hook::callAll('template_vars', $arr);
$r = $arr['vars']; $vars = $arr['vars'];
foreach ($r as $key => $value) { foreach ($vars as $key => $value) {
if ($key[0] === '$') { if ($key[0] === '$') {
$key = substr($key, 1); $key = substr($key, 1);
} }
$s->assign($key, $value); $this->smarty->assign($key, $value);
} }
return $s->parsed($template);
return $this->smarty->fetch($template);
} }
public function getTemplateFile($file, $subDir = '') /**
* @inheritDoc
*/
public function getTemplateFile(string $file, string $subDir = '')
{ {
$a = DI::app();
$template = new FriendicaSmarty();
// Make sure $root ends with a slash / // Make sure $root ends with a slash /
if ($subDir !== '' && substr($subDir, -1, 1) !== '/') { if ($subDir !== '' && substr($subDir, -1, 1) !== '/') {
$subDir = $subDir . '/'; $subDir = $subDir . '/';
@ -80,21 +106,20 @@ class FriendicaSmartyEngine implements ITemplateEngine
$root = DI::basePath() . '/' . $subDir; $root = DI::basePath() . '/' . $subDir;
$theme = $a->getCurrentTheme(); $filename = $this->smarty::SMARTY3_TEMPLATE_FOLDER . '/' . $file;
$filename = $template::SMARTY3_TEMPLATE_FOLDER . '/' . $file;
if (file_exists("{$root}view/theme/$theme/$filename")) { if (file_exists("{$root}view/theme/$this->theme/$filename")) {
$template_file = "{$root}view/theme/$theme/$filename"; $template_file = "{$root}view/theme/$this->theme/$filename";
} elseif (!empty($a->theme_info['extends']) && file_exists(sprintf('%sview/theme/%s}/%s', $root, $a->theme_info['extends'], $filename))) { } elseif (!empty($this->theme_info['extends']) && file_exists(sprintf('%sview/theme/%s}/%s', $root, $this->theme_info['extends'], $filename))) {
$template_file = sprintf('%sview/theme/%s}/%s', $root, $a->theme_info['extends'], $filename); $template_file = sprintf('%sview/theme/%s}/%s', $root, $this->theme_info['extends'], $filename);
} elseif (file_exists("{$root}/$filename")) { } elseif (file_exists("{$root}/$filename")) {
$template_file = "{$root}/$filename"; $template_file = "{$root}/$filename";
} else { } else {
$template_file = "{$root}view/$filename"; $template_file = "{$root}view/$filename";
} }
$template->filename = $template_file; $this->smarty->filename = $template_file;
return $template; return self::FILE_PREFIX . $template_file;
} }
} }

View file

@ -1,31 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Render;
/**
* Interface for template engines
*/
interface ITemplateEngine
{
public function replaceMacros($s, $v);
public function getTemplateFile($file, $subDir = '');
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Render;
/**
* Interface for template engines
*/
abstract class TemplateEngine
{
/** @var string */
static $name;
/** @var string */
protected $theme;
/** @var array */
protected $theme_info;
/**
* @param string $theme The current theme name
* @param array $theme_info The current theme info array
*/
abstract public function __construct(string $theme, array $theme_info);
/**
* Checks the template engine is correctly installed and configured and reports error messages in the provided
* parameter or displays them directly if it's null.
*
* @param array|null $errors
*/
abstract public function testInstall(array &$errors = null);
/**
* Returns the rendered template output from the template string and variables
*
* @param string $template
* @param array $vars
* @return string
*/
abstract public function replaceMacros(string $template, array $vars);
/**
* Returns the template string from a file path and an optional sub-directory from the project root
*
* @param string $file
* @param string $subDir
* @return mixed
*/
abstract public function getTemplateFile(string $file, string $subDir = '');
}

View file

@ -369,13 +369,27 @@ class Strings
* @param array $chars * @param array $chars
* @return bool * @return bool
*/ */
public static function startsWith($string, array $chars) public static function startsWithChars($string, array $chars)
{ {
$return = in_array(substr(trim($string), 0, 1), $chars); $return = in_array(substr(trim($string), 0, 1), $chars);
return $return; return $return;
} }
/**
* Check if the first string starts with the second
*
* @param string $string
* @param string $start
* @return bool
*/
public static function startsWith(string $string, string $start)
{
$return = substr_compare($string, $start, 0, strlen($start)) === 0;
return $return;
}
/** /**
* Returns the regular expression string to match URLs in a given text * Returns the regular expression string to match URLs in a given text
* *

View file

@ -108,7 +108,7 @@ trait AppMockTrait
->andReturn($this->configMock); ->andReturn($this->configMock);
$this->app $this->app
->shouldReceive('getTemplateEngine') ->shouldReceive('getTemplateEngine')
->andReturn(new FriendicaSmartyEngine()); ->andReturn(new FriendicaSmartyEngine('frio', []));
$this->app $this->app
->shouldReceive('getCurrentTheme') ->shouldReceive('getCurrentTheme')
->andReturn('Smarty3'); ->andReturn('Smarty3');