* @copyright Copyright (c) 2016-2022 Jorge Patricio Castro Castillo MIT License.
* Don't delete this comment, its part of the license.
* Part of this code is based in the work of Laravel PHP Components.
* @version 4.8.2
* @link https://github.com/EFTEC/BladeOne
*/
class BladeOne
{
//
public const VERSION = '4.8.2';
/** @var int BladeOne reads if the compiled file has changed. If it has changed,then the file is replaced. */
public const MODE_AUTO = 0;
/** @var int Then compiled file is always replaced. It's slow and it's useful for development. */
public const MODE_SLOW = 1;
/** @var int The compiled file is never replaced. It's fast and it's useful for production. */
public const MODE_FAST = 2;
/** @var int DEBUG MODE, the file is always compiled and the filename is identifiable. */
public const MODE_DEBUG = 5;
/** @var array Hold dictionary of translations */
public static $dictionary = [];
/** @var string PHP tag. You could use < ?php or < ? (if shorttag is active in php.ini) */
public $phpTag = '
* If false (default value), then the variables defined in the "include" as arguments are defined globally.
* Example: (includeScope=false)
*
* @include("template",['a1'=>'abc']) // a1 is equals to abc
* @include("template",[]) // a1 is equals to abc
*
* Example: (includeScope=true)
*
* @include("template",['a1'=>'abc']) // a1 is equals to abc
* @include("template",[]) // a1 is not defined
*
*/
public $includeScope = false;
/**
* @var callable[] It allows to parse the compiled output using a function.
* This function doesn't require to return a value
* Example: this converts all compiled result in uppercase (note, content is a ref)
*
*/
public $compileCallbacks = [];
/** @var array All the registered extensions. */
protected $extensions = [];
/** @var array All the finished, captured sections. */
protected $sections = [];
/** @var string The template currently being compiled. For example "folder.template" */
protected $fileName;
protected $currentView;
protected $notFoundPath;
/** @var string File extension for the template files. */
protected $fileExtension = '.blade.php';
/** @var array The stack of in-progress sections. */
protected $sectionStack = [];
/** @var array The stack of in-progress loops. */
protected $loopsStack = [];
/** @var array Dictionary of variables */
protected $variables = [];
/** @var array Dictionary of global variables */
protected $variablesGlobal = [];
/** @var array All the available compiler functions. */
protected $compilers = [
'Extensions',
'Statements',
'Comments',
'Echos',
];
/** @var string|null it allows to set the stack */
protected $viewStack;
/** @var array used by $this->composer() */
protected $composerStack = [];
/** @var array The stack of in-progress push sections. */
protected $pushStack = [];
/** @var array All the finished, captured push sections. */
protected $pushes = [];
/** @var int The number of active rendering operations. */
protected $renderCount = 0;
/** @var string[] Get the template path for the compiled views. */
protected $templatePath;
/** @var string Get the compiled path for the compiled views. If null then it uses the default path */
protected $compiledPath;
/** @var string the extension of the compiled file. */
protected $compileExtension = '.bladec';
/**
* @var string=['auto','sha1','md5'][$i] It determines how the compiled filename will be called.
* auto (default mode) the mode is "sha1"
* sha1 the filename is converted into a sha1 hash
* md5 the filename is converted into a md5 hash
*/
protected $compileTypeFileName='auto';
/** @var array Custom "directive" dictionary. Those directives run at compile time. */
protected $customDirectives = [];
/** @var bool[] Custom directive dictionary. Those directives run at runtime. */
protected $customDirectivesRT = [];
/** @var callable Function used for resolving injected classes. */
protected $injectResolver;
/** @var array Used for conditional if. */
protected $conditions = [];
/** @var int Unique counter. It's used for extends */
protected $uidCounter = 0;
/** @var string The main url of the system. Don't use raw $_SERVER values unless the value is sanitized */
protected $baseUrl = '.';
/** @var string|null The base domain of the system */
protected $baseDomain;
/** @var string|null It stores the current canonical url. */
protected $canonicalUrl;
/** @var string|null It stores the current url including arguments */
protected $currentUrl;
/** @var string it is a relative path calculated between baseUrl and the current url. Example ../../ */
protected $relativePath = '';
/** @var string[] Dictionary of assets */
protected $assetDict;
/** @var bool if true then it removes tabs and unneeded spaces */
protected $optimize = true;
/** @var bool if false, then the template is not compiled (but executed on memory). */
protected $isCompiled = true;
/** @var bool */
protected $isRunFast = false; // stored for historical purpose.
/** @var array Array of opening and closing tags for raw echos. */
protected $rawTags = ['{!!', '!!}'];
/** @var array Array of opening and closing tags for regular echos. */
protected $contentTags = ['{{', '}}'];
/** @var array Array of opening and closing tags for escaped echos. */
protected $escapedTags = ['{{{', '}}}'];
/** @var string The "regular" / legacy echo string format. */
protected $echoFormat = '\htmlentities(%s??\'\', ENT_QUOTES, \'UTF-8\', false)';
/** @var string */
protected $echoFormatOld = 'static::e(%s)';
/** @var array Lines that will be added at the footer of the template */
protected $footer = [];
/** @var string Placeholder to temporary mark the position of verbatim blocks. */
protected $verbatimPlaceholder = '$__verbatim__$';
/** @var array Array to temporary store the verbatim blocks found in the template. */
protected $verbatimBlocks = [];
/** @var int Counter to keep track of nested forelse statements. */
protected $forelseCounter = 0;
/** @var array The components being rendered. */
protected $componentStack = [];
/** @var array The original data passed to the component. */
protected $componentData = [];
/** @var array The slot contents for the component. */
protected $slots = [];
/** @var array The names of the slots being rendered. */
protected $slotStack = [];
/** @var string tag unique */
protected $PARENTKEY = '@parentXYZABC';
/**
* Indicates the compile mode.
* if the constant BLADEONE_MODE is defined, then it is used instead of this field.
*
* @var int=[BladeOne::MODE_AUTO,BladeOne::MODE_DEBUG,BladeOne::MODE_SLOW,BladeOne::MODE_FAST][$i]
*/
protected $mode;
/** @var int Indicates the number of open switches */
protected $switchCount = 0;
/** @var bool Indicates if the switch is recently open */
protected $firstCaseInSwitch = true;
//
//
/**
* Bob the constructor.
* The folder at $compiledPath is created in case it doesn't exist.
*
* @param string|array $templatePath If null then it uses (caller_folder)/views
* @param string $compiledPath If null then it uses (caller_folder)/compiles
* @param int $mode =[BladeOne::MODE_AUTO,BladeOne::MODE_DEBUG,BladeOne::MODE_FAST,BladeOne::MODE_SLOW][$i]
*/
public function __construct($templatePath = null, $compiledPath = null, $mode = 0)
{
if ($templatePath === null) {
$templatePath = \getcwd() . '/views';
}
if ($compiledPath === null) {
$compiledPath = \getcwd() . '/compiles';
}
$this->templatePath = (is_array($templatePath)) ? $templatePath : [$templatePath];
$this->compiledPath = $compiledPath;
$this->setMode($mode);
$this->authCallBack = function (
$action = null,
/** @noinspection PhpUnusedParameterInspection */
$subject = null
) {
return \in_array($action, $this->currentPermission, true);
};
$this->authAnyCallBack = function ($array = []) {
foreach ($array as $permission) {
if (\in_array($permission, $this->currentPermission ?? [], true)) {
return true;
}
}
return false;
};
$this->errorCallBack = static function (
/** @noinspection PhpUnusedParameterInspection */
$key = null
) {
return false;
};
// If the "traits" has "Constructors", then we call them.
// Requisites.
// 1- the method must be public or protected
// 2- it must don't have arguments
// 3- It must have the name of the trait. i.e. trait=MyTrait, method=MyTrait()
$traits = get_declared_traits();
foreach ($traits as $trait) {
$r = explode('\\', $trait);
$name = end($r);
if (is_callable([$this, $name]) && method_exists($this, $name)) {
$this->{$name}();
}
}
}
//
//
/**
* Show an error in the web.
*
* @param string $id Title of the error
* @param string $text Message of the error
* @param bool $critic if true then the compilation is ended, otherwise it continues
* @param bool $alwaysThrow if true then it always throws a runtime exception.
* @return string
* @throws \RuntimeException
*/
public function showError($id, $text, $critic = false, $alwaysThrow = false): string
{
\ob_get_clean();
if ($this->throwOnError || $alwaysThrow || $critic === true) {
throw new \RuntimeException("BladeOne Error [$id] $text");
}
$msg = "
*
* @param ?string $input The input value
* @param string $quote The quote used (to quote the result)
* @param bool $parse If the result will be parsed or not. If false then it's returned without $this->e
* @return string
*/
public function wrapPHP($input, $quote = '"', $parse = true): string
{
if($input===null) {
return 'null';
}
if (strpos($input, '(') !== false && !$this->isQuoted($input)) {
if ($parse) {
return $quote . $this->phpTagEcho . '$this->e(' . $input . ');?>' . $quote;
}
return $quote . $this->phpTagEcho . $input . ';?>' . $quote;
}
if (strpos($input, '$') === false) {
if ($parse) {
return self::enq($input);
}
return $input;
}
if ($parse) {
return $quote . $this->phpTagEcho . '$this->e(' . $input . ');?>' . $quote;
}
return $quote . $this->phpTagEcho . $input . ';?>' . $quote;
}
/**
* Returns true if the text is surrounded by quotes (double or single quote)
*
* @param string|null $text
* @return bool
*/
public function isQuoted($text): bool
{
if (!$text || strlen($text) < 2) {
return false;
}
if ($text[0] === '"' && substr($text, -1) === '"') {
return true;
}
return ($text[0] === "'" && substr($text, -1) === "'");
}
/**
* Escape HTML entities in a string.
*
* @param string $value
* @return string
*/
public static function enq($value): string
{
if (\is_array($value) || \is_object($value)) {
return \htmlentities(\print_r($value, true), ENT_NOQUOTES, 'UTF-8', false);
}
return \htmlentities($value??'', ENT_NOQUOTES, 'UTF-8', false);
}
/**
* @param string $view example "folder.template"
* @param string|null $alias example "mynewop". If null then it uses the name of the template.
*/
public function addInclude($view, $alias = null): void
{
if (!isset($alias)) {
$alias = \explode('.', $view);
$alias = \end($alias);
}
$this->directive($alias, function ($expression) use ($view) {
$expression = $this->stripParentheses($expression) ?: '[]';
return "$this->phpTag echo \$this->runChild('$view', $expression); ?>";
});
}
/**
* Register a handler for custom directives.
*
* @param string $name
* @param callable $handler
* @return void
*/
public function directive($name, callable $handler): void
{
$this->customDirectives[$name] = $handler;
$this->customDirectivesRT[$name] = false;
}
/**
* Strip the parentheses from the given expression.
*
* @param string|null $expression
* @return string
*/
public function stripParentheses($expression): string
{
if (\is_null($expression)) {
return '';
}
if (static::startsWith($expression, '(')) {
$expression = \substr($expression, 1, -1);
}
return $expression;
}
/**
* Determine if a given string starts with a given substring.
*
* @param string $haystack
* @param string|array $needles
* @return bool
*/
public static function startsWith($haystack, $needles): bool
{
foreach ((array)$needles as $needle) {
if ($needle != '') {
if (\function_exists('mb_strpos')) {
if ($haystack !== null && \mb_strpos($haystack, $needle) === 0) {
return true;
}
} elseif ($haystack !== null && \strpos($haystack, $needle) === 0) {
return true;
}
}
}
return false;
}
/**
* If false then the file is not compiled, and it is executed directly from the memory.
* By default the value is true
* It also sets the mode to MODE_SLOW
*
* @param bool $bool
* @return BladeOne
* @see BladeOne::setMode
*/
public function setIsCompiled($bool = false): BladeOne
{
$this->isCompiled = $bool;
if (!$bool) {
$this->setMode(self::MODE_SLOW);
}
return $this;
}
/**
* It sets the template and compile path (without trailing slash).
*
Example:setPath("somefolder","otherfolder");
*
* @param null|string|string[] $templatePath If null then it uses the current path /views folder
* @param null|string $compiledPath If null then it uses the current path /views folder
*/
public function setPath($templatePath, $compiledPath): void
{
if ($templatePath === null) {
$templatePath = \getcwd() . '/views';
}
if ($compiledPath === null) {
$compiledPath = \getcwd() . '/compiles';
}
$this->templatePath = (is_array($templatePath)) ? $templatePath : [$templatePath];
$this->compiledPath = $compiledPath;
}
/**
* @return array
*/
public function getAliasClasses(): array
{
return $this->aliasClasses;
}
/**
* @param array $aliasClasses
*/
public function setAliasClasses($aliasClasses): void
{
$this->aliasClasses = $aliasClasses;
}
/**
* @param string $aliasName
* @param string $classWithNS
*/
public function addAliasClasses($aliasName, $classWithNS): void
{
$this->aliasClasses[$aliasName] = $classWithNS;
}
//
//
/**
* Authentication. Sets with a user,role and permission
*
* @param string $user
* @param null $role
* @param array $permission
*/
public function setAuth($user = '', $role = null, $permission = []): void
{
$this->currentUser = $user;
$this->currentRole = $role;
$this->currentPermission = $permission;
}
/**
* run the blade engine. It returns the result of the code.
*
* @param string $string HTML to parse
* @param array $data It is an associative array with the datas to display.
* @return string It returns a parsed string
* @throws Exception
*/
public function runString($string, $data = []): string
{
$php = $this->compileString($string);
$obLevel = \ob_get_level();
\ob_start();
\extract($data, EXTR_SKIP);
$previousError = \error_get_last();
try {
@eval('?' . '>' . $php);
} catch (Exception $e) {
while (\ob_get_level() > $obLevel) {
\ob_end_clean();
}
throw $e;
} catch (\ParseError $e) { // PHP >= 7
while (\ob_get_level() > $obLevel) {
\ob_end_clean();
}
$this->showError('runString', $e->getMessage() . ' ' . $e->getCode(), true);
return '';
}
$lastError = \error_get_last(); // PHP 5.6
if ($previousError != $lastError && $lastError['type'] == E_PARSE) {
while (\ob_get_level() > $obLevel) {
\ob_end_clean();
}
$this->showError('runString', $lastError['message'] . ' ' . $lastError['type'], true);
return '';
}
return \ob_get_clean();
}
/**
* Compile the given Blade template contents.
*
* @param string $value
* @return string
*/
public function compileString($value): string
{
$result = '';
if (\strpos($value, '@verbatim') !== false) {
$value = $this->storeVerbatimBlocks($value);
}
$this->footer = [];
// Here we will loop through all the tokens returned by the Zend lexer and
// parse each one into the corresponding valid PHP. We will then have this
// template as the correctly rendered PHP that can be rendered natively.
foreach (\token_get_all($value) as $token) {
$result .= \is_array($token) ? $this->parseToken($token) : $token;
}
if (!empty($this->verbatimBlocks)) {
$result = $this->restoreVerbatimBlocks($result);
}
// If there are any footer lines that need to get added to a template we will
// add them here at the end of the template. This gets used mainly for the
// template inheritance via the extends keyword that should be appended.
if (\count($this->footer) > 0) {
$result = \ltrim($result, PHP_EOL)
. PHP_EOL . \implode(PHP_EOL, \array_reverse($this->footer));
}
return $result;
}
/**
* Store the verbatim blocks and replace them with a temporary placeholder.
*
* @param string $value
* @return string
*/
protected function storeVerbatimBlocks($value): string
{
return \preg_replace_callback('/(?verbatimBlocks[] = $matches[1];
return $this->verbatimPlaceholder;
}, $value);
}
/**
* Parse the tokens from the template.
*
* @param array $token
*
* @return string
*
* @see BladeOne::compileStatements
* @see BladeOne::compileExtends
* @see BladeOne::compileComments
* @see BladeOne::compileEchos
*/
protected function parseToken($token): string
{
[$id, $content] = $token;
if ($id == T_INLINE_HTML) {
foreach ($this->compilers as $type) {
$content = $this->{"compile$type"}($content);
}
}
return $content;
}
/**
* Replace the raw placeholders with the original code stored in the raw blocks.
*
* @param string $result
* @return string
*/
protected function restoreVerbatimBlocks($result): string
{
$result = \preg_replace_callback('/' . \preg_quote($this->verbatimPlaceholder) . '/', function () {
return \array_shift($this->verbatimBlocks);
}, $result);
$this->verbatimBlocks = [];
return $result;
}
/**
* it calculates the relative path of a web.
* This function uses the current url and the baseurl
*
* @param string $relativeWeb . Example img/images.jpg
* @return string Example ../../img/images.jpg
*/
public function relative($relativeWeb): string
{
return $this->assetDict[$relativeWeb] ?? ($this->relativePath . $relativeWeb);
}
/**
* It adds an alias to the link of the resources.
* addAssetDict('name','url/res.jpg')
* addAssetDict(['name'=>'url/res.jpg','name2'=>'url/res2.jpg']);
*
* @param string|array $name example 'css/style.css', you could also add an array
* @param string $url example https://www.web.com/style.css'
*/
public function addAssetDict($name, $url = ''): void
{
if (\is_array($name)) {
if ($this->assetDict === null) {
$this->assetDict = $name;
} else {
$this->assetDict = \array_merge($this->assetDict, $name);
}
} else {
$this->assetDict[$name] = $url;
}
}
/**
* Compile the push statements into valid PHP.
*
* @param string $expression
* @return string
*/
public function compilePush($expression): string
{
return $this->phpTag . "\$this->startPush$expression; ?>";
}
/**
* Compile the push statements into valid PHP.
*
* @param string $expression
* @return string
*/
public function compilePushOnce($expression): string
{
$key = '$__pushonce__' . \trim(\substr($expression, 2, -2));
return $this->phpTag . "if(!isset($key)): $key=1; \$this->startPush$expression; ?>";
}
/**
* Compile the push statements into valid PHP.
*
* @param string $expression
* @return string
*/
public function compilePrepend($expression): string
{
return $this->phpTag . "\$this->startPush$expression; ?>";
}
/**
* Start injecting content into a push section.
*
* @param string $section
* @param string $content
* @return void
*/
public function startPush($section, $content = ''): void
{
if ($content === '') {
if (\ob_start()) {
$this->pushStack[] = $section;
}
} else {
$this->extendPush($section, $content);
}
}
/*
* endswitch tag
*/
/**
* Append content to a given push section.
*
* @param string $section
* @param string $content
* @return void
*/
protected function extendPush($section, $content): void
{
if (!isset($this->pushes[$section])) {
$this->pushes[$section] = []; // start an empty section
}
if (!isset($this->pushes[$section][$this->renderCount])) {
$this->pushes[$section][$this->renderCount] = $content;
} else {
$this->pushes[$section][$this->renderCount] .= $content;
}
}
/**
* Start injecting content into a push section.
*
* @param string $section
* @param string $content
* @return void
*/
public function startPrepend($section, $content = ''): void
{
if ($content === '') {
if (\ob_start()) {
\array_unshift($this->pushStack[], $section);
}
} else {
$this->extendPush($section, $content);
}
}
/**
* Stop injecting content into a push section.
*
* @return string
*/
public function stopPush(): string
{
if (empty($this->pushStack)) {
$this->showError('stopPush', 'Cannot end a section without first starting one', true);
}
$last = \array_pop($this->pushStack);
$this->extendPush($last, \ob_get_clean());
return $last;
}
/**
* Stop injecting content into a push section.
*
* @return string
*/
public function stopPrepend(): string
{
if (empty($this->pushStack)) {
$this->showError('stopPrepend', 'Cannot end a section without first starting one', true);
}
$last = \array_shift($this->pushStack);
$this->extendStartPush($last, \ob_get_clean());
return $last;
}
/**
* Append content to a given push section.
*
* @param string $section
* @param string $content
* @return void
*/
protected function extendStartPush($section, $content): void
{
if (!isset($this->pushes[$section])) {
$this->pushes[$section] = []; // start an empty section
}
if (!isset($this->pushes[$section][$this->renderCount])) {
$this->pushes[$section][$this->renderCount] = $content;
} else {
$this->pushes[$section][$this->renderCount] = $content . $this->pushes[$section][$this->renderCount];
}
}
/**
* Get the string contents of a push section.
*
* @param string $section
* @param string $default
* @return string
*/
public function yieldPushContent($section, $default = ''): string
{
if (!isset($this->pushes[$section])) {
return $default;
}
return \implode(\array_reverse($this->pushes[$section]));
}
/**
* Get the string contents of a push section.
*
* @param int|string $each if "int", then it split the foreach every $each numbers.
* if "string" or "c3", then it means that it will split in 3 columns
* @param string $splitText
* @param string $splitEnd
* @return string
*/
public function splitForeach($each = 1, $splitText = ',', $splitEnd = ''): string
{
$loopStack = static::last($this->loopsStack); // array(7) { ["index"]=> int(0) ["remaining"]=> int(6) ["count"]=> int(5) ["first"]=> bool(true) ["last"]=> bool(false) ["depth"]=> int(1) ["parent"]=> NULL }
if (($loopStack['index']) == $loopStack['count'] - 1) {
return $splitEnd;
}
$eachN = 0;
if (is_numeric($each)) {
$eachN = $each;
} elseif (strlen($each) > 1) {
if ($each[0] === 'c') {
$eachN = $loopStack['count'] / substr($each, 1);
}
} else {
$eachN = PHP_INT_MAX;
}
if (($loopStack['index'] + 1) % $eachN === 0) {
return $splitText;
}
return '';
}
/**
* Return the last element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
* @return mixed
*/
public static function last($array, callable $callback = null, $default = null)
{
if (\is_null($callback)) {
return empty($array) ? static::value($default) : \end($array);
}
return static::first(\array_reverse($array), $callback, $default);
}
/**
* Return the default value of the given value.
*
* @param mixed $value
* @return mixed
*/
public static function value($value)
{
return $value instanceof Closure ? $value() : $value;
}
/**
* Return the first element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
* @return mixed
*/
public static function first($array, callable $callback = null, $default = null)
{
if (\is_null($callback)) {
return empty($array) ? static::value($default) : \reset($array);
}
foreach ($array as $key => $value) {
if ($callback($key, $value)) {
return $value;
}
}
return static::value($default);
}
/**
* @param string $name
* @param $args []
* @return string
* @throws BadMethodCallException
*/
public function __call($name, $args)
{
if ($name === 'if') {
return $this->registerIfStatement($args[0] ?? null, $args[1] ?? null);
}
$this->showError('call', "function $name is not defined ", true, true);
return '';
}
/**
* Register an "if" statement directive.
*
* @param string $name
* @param callable $callback
* @return string
*/
public function registerIfStatement($name, callable $callback): string
{
$this->conditions[$name] = $callback;
$this->directive($name, function ($expression) use ($name) {
$tmp = $this->stripParentheses($expression);
return $expression !== ''
? $this->phpTag . " if (\$this->check('$name', $tmp)): ?>"
: $this->phpTag . " if (\$this->check('$name')): ?>";
});
$this->directive('else' . $name, function ($expression) use ($name) {
$tmp = $this->stripParentheses($expression);
return $expression !== ''
? $this->phpTag . " elseif (\$this->check('$name', $tmp)): ?>"
: $this->phpTag . " elseif (\$this->check('$name')): ?>";
});
$this->directive('end' . $name, function () {
return $this->phpTag . ' endif; ?>';
});
return '';
}
/**
* Check the result of a condition.
*
* @param string $name
* @param array $parameters
* @return bool
*/
public function check($name, ...$parameters): bool
{
return \call_user_func($this->conditions[$name], ...$parameters);
}
/**
* @param bool $bool
* @param string $view name of the view
* @param array $value arrays of values
* @return string
* @throws Exception
*/
public function includeWhen($bool = false, $view = '', $value = []): string
{
if ($bool) {
return $this->runChild($view, $value);
}
return '';
}
/**
* Macro of function run
*
* @param $view
* @param array $variables
* @return string
* @throws Exception
*/
public function runChild($view, $variables = []): string
{
if (\is_array($variables)) {
if ($this->includeScope) {
$backup = $this->variables;
} else {
$backup = null;
}
$newVariables = \array_merge($this->variables, $variables);
} else {
if ($variables === null) {
$newVariables = $this->variables;
var_dump($newVariables);
die(1);
}
$this->showError('run/include', "RunChild: Include/run variables should be defined as array ['idx'=>'value']", true);
return '';
}
$r = $this->runInternal($view, $newVariables, false, $this->isRunFast);
if ($backup !== null) {
$this->variables = $backup;
}
return $r;
}
/**
* run the blade engine. It returns the result of the code.
*
* @param string $view
* @param array $variables
* @param bool $forced if true then it recompiles no matter if the compiled file exists or not.
* @param bool $runFast if true then the code is not compiled neither checked, and it runs directly the compiled
* version.
* @return string
* @throws Exception
* @noinspection PhpUnusedParameterInspection
*/
protected function runInternal($view, $variables = [], $forced = false, $runFast = false): string
{
$this->currentView = $view;
if (@\count($this->composerStack)) {
$this->evalComposer($view);
}
if (@\count($this->variablesGlobal) > 0) {
$this->variables = \array_merge($variables, $this->variablesGlobal);
//$this->variablesGlobal = []; // used so we delete it.
} else {
$this->variables = $variables;
}
if (!$runFast) {
// a) if the "compile" is forced then we compile the original file, then save the file.
// b) if the "compile" is not forced then we read the datetime of both file, and we compared.
// c) in both cases, if the compiled doesn't exist then we compile.
if ($view) {
$this->fileName = $view;
}
$result = $this->compile($view, $forced);
if (!$this->isCompiled) {
return $this->evaluateText($result, $this->variables);
}
} elseif ($view) {
$this->fileName = $view;
}
$this->isRunFast = $runFast;
return $this->evaluatePath($this->getCompiledFile(), $this->variables);
}
protected function evalComposer($view): void
{
foreach ($this->composerStack as $viewKey => $fn) {
if ($this->wildCardComparison($view, $viewKey)) {
if (is_callable($fn)) {
$fn($this);
} elseif ($this->methodExistsStatic($fn, 'composer')) {
// if the method exists statically then $fn is the class and 'composer' is the name of the method
$fn::composer($this);
} elseif (is_object($fn) || class_exists($fn)) {
// if $fn is an object, or it is a class and the class exists.
$instance = (is_object($fn)) ? $fn : new $fn();
if (method_exists($instance, 'composer')) {
// and the method exists inside the instance.
$instance->composer($this);
} else {
if ($this->mode === self::MODE_DEBUG) {
$this->showError('evalComposer', "BladeOne: composer() added an incorrect method [$fn]", true, true);
return;
}
$this->showError('evalComposer', 'BladeOne: composer() added an incorrect method', true, true);
return;
}
} else {
$this->showError('evalComposer', 'BladeOne: composer() added an incorrect method', true, true);
}
}
}
}
/**
* It compares with wildcards (*) and returns true if both strings are equals
* The wildcards only works at the beginning and/or at the end of the string.
* Example:
*
*
* @param string $text
* @param string|null $textWithWildcard
*
* @return bool
*/
protected function wildCardComparison($text, $textWithWildcard): bool
{
if (($textWithWildcard === null || $textWithWildcard === '')
|| strpos($textWithWildcard, '*') === false
) {
// if the text with wildcard is null or empty, or it contains two ** or it contains no * then..
return $text == $textWithWildcard;
}
if ($textWithWildcard === '*' || $textWithWildcard === '**') {
return true;
}
$c0 = $textWithWildcard[0];
$c1 = substr($textWithWildcard, -1);
$textWithWildcardClean = str_replace('*', '', $textWithWildcard);
$p0 = strpos($text, $textWithWildcardClean);
if ($p0 === false) {
// no matches.
return false;
}
if ($c0 === '*' && $c1 === '*') {
// $textWithWildcard='*asasasas*'
return true;
}
if ($c1 === '*') {
// $textWithWildcard='asasasas*'
return $p0 === 0;
}
// $textWithWildcard='*asasasas'
$len = strlen($textWithWildcardClean);
return (substr($text, -$len) === $textWithWildcardClean);
}
protected function methodExistsStatic($class, $method): bool
{
try {
return (new \ReflectionMethod($class, $method))->isStatic();
} catch (\ReflectionException $e) {
return false;
}
}
/**
* Compile the view at the given path.
*
* @param string $templateName The name of the template. Example folder.template
* @param bool $forced If the compilation will be forced (always compile) or not.
* @return boolean|string True if the operation was correct, or false (if not exception)
* if it fails. It returns a string (the content compiled) if isCompiled=false
* @throws Exception
*/
public function compile($templateName = null, $forced = false)
{
$compiled = $this->getCompiledFile($templateName);
$template = $this->getTemplateFile($templateName);
if (!$this->isCompiled) {
$contents = $this->compileString($this->getFile($template));
$this->compileCallBacks($contents, $templateName);
return $contents;
}
if ($forced || $this->isExpired($templateName)) {
// compile the original file
$contents = $this->compileString($this->getFile($template));
$this->compileCallBacks($contents, $templateName);
if ($this->optimize) {
// removes space and tabs and replaces by a single space
$contents = \preg_replace('/^ {2,}/m', ' ', $contents);
$contents = \preg_replace('/^\t{2,}/m', ' ', $contents);
}
$ok = @\file_put_contents($compiled, $contents);
if ($ok === false) {
$this->showError(
'Compiling',
"Unable to save the file [$compiled]. Check the compile folder is defined and has the right permission"
);
return false;
}
}
return true;
}
/**
* Get the full path of the compiled file.
*
* @param string $templateName
* @return string
*/
public function getCompiledFile($templateName = ''): string
{
$templateName = (empty($templateName)) ? $this->fileName : $templateName;
$fullPath = $this->getTemplateFile($templateName);
if($fullPath == '') {
throw new \RuntimeException('Template not found: ' . $templateName);
}
$style=$this->compileTypeFileName;
if ($style==='auto') {
$style='sha1';
}
$hash = $style === 'md5' ? \md5($fullPath) : \sha1($fullPath);
return $this->compiledPath . '/' . basename($templateName) . '_' . $hash . $this->compileExtension;
}
/**
* Get the mode of the engine.See BladeOne::MODE_* constants
*
* @return int=[self::MODE_AUTO,self::MODE_DEBUG,self::MODE_FAST,self::MODE_SLOW][$i]
*/
public function getMode(): int
{
if (\defined('BLADEONE_MODE')) {
$this->mode = BLADEONE_MODE;
}
return $this->mode;
}
/**
* Set the compile mode
*
* @param $mode int=[self::MODE_AUTO,self::MODE_DEBUG,self::MODE_FAST,self::MODE_SLOW][$i]
* @return void
*/
public function setMode($mode): void
{
$this->mode = $mode;
}
/**
* Get the full path of the template file.
*
Example: getTemplateFile('.abc.def')
*
* @param string $templateName template name. If not template is set then it uses the base template.
* @return string
*/
public function getTemplateFile($templateName = ''): string
{
$templateName = (empty($templateName)) ? $this->fileName : $templateName;
if (\strpos($templateName, '/') !== false) {
return $this->locateTemplate($templateName); // it's a literal
}
$arr = \explode('.', $templateName);
$c = \count($arr);
if ($c == 1) {
// it's in the root of the template folder.
return $this->locateTemplate($templateName . $this->fileExtension);
}
$file = $arr[$c - 1];
\array_splice($arr, $c - 1, $c - 1); // delete the last element
$path = \implode('/', $arr);
return $this->locateTemplate($path . '/' . $file . $this->fileExtension);
}
/**
* Find template file with the given name in all template paths in the order the paths were written
*
* @param string $name Filename of the template (without path)
* @return string template file
*/
protected function locateTemplate($name): string
{
$this->notFoundPath = '';
foreach ($this->templatePath as $dir) {
$path = $dir . '/' . $name;
if (\is_file($path)) {
return $path;
}
$this->notFoundPath .= $path . ",";
}
return '';
}
/**
* Get the contents of a file.
*
* @param string $fullFileName It gets the content of a filename or returns ''.
*
* @return string
*/
public function getFile($fullFileName): string
{
if (\is_file($fullFileName)) {
return \file_get_contents($fullFileName);
}
$this->showError('getFile', "File does not exist at paths (separated by comma) [$this->notFoundPath] or permission denied");
return '';
}
protected function compileCallBacks(&$contents, $templateName): void
{
if (!empty($this->compileCallbacks)) {
foreach ($this->compileCallbacks as $callback) {
if (is_callable($callback)) {
$callback($contents, $templateName);
}
}
}
}
/**
* Determine if the view has expired.
*
* @param string|null $fileName
* @return bool
*/
public function isExpired($fileName): bool
{
$compiled = $this->getCompiledFile($fileName);
$template = $this->getTemplateFile($fileName);
if (!\is_file($template)) {
if ($this->mode == self::MODE_DEBUG) {
$this->showError('Read file', 'Template not found :' . $this->fileName . " on file: $template", true);
} else {
$this->showError('Read file', 'Template not found :' . $this->fileName, true);
}
}
// If the compiled file doesn't exist we will indicate that the view is expired
// so that it can be re-compiled. Else, we will verify the last modification
// of the views is less than the modification times of the compiled views.
if (!$this->compiledPath || !\is_file($compiled)) {
return true;
}
return \filemtime($compiled) < \filemtime($template);
}
/**
* Evaluates a text (string) using the current variables
*
* @param string $content
* @param array $variables
* @return string
* @throws Exception
*/
protected function evaluateText($content, $variables): string
{
\ob_start();
\extract($variables);
// We'll evaluate the contents of the view inside a try/catch block, so we can
// flush out any stray output that might get out before an error occurs or
// an exception is thrown. This prevents any partial views from leaking.
try {
eval(' ?>' . $content . $this->phpTag);
} catch (Exception $e) {
$this->handleViewException($e);
}
return \ltrim(\ob_get_clean());
}
/**
* Handle a view exception.
*
* @param Exception $e
* @return void
* @throws $e
*/
protected function handleViewException($e): void
{
\ob_get_clean();
throw $e;
}
/**
* Evaluates a compiled file using the current variables
*
* @param string $compiledFile full path of the compile file.
* @param array $variables
* @return string
* @throws Exception
*/
protected function evaluatePath($compiledFile, $variables): string
{
\ob_start();
// note, the variables are extracted locally inside this method,
// they are not global variables :-3
\extract($variables);
// We'll evaluate the contents of the view inside a try/catch block, so we can
// flush out any stray output that might get out before an error occurs or
// an exception is thrown. This prevents any partial views from leaking.
try {
include $compiledFile;
} catch (Exception $e) {
$this->handleViewException($e);
}
return \ltrim(\ob_get_clean());
}
/**
* @param array $views array of views
* @param array $value
* @return string
* @throws Exception
*/
public function includeFirst($views = [], $value = []): string
{
foreach ($views as $view) {
if ($this->templateExist($view)) {
return $this->runChild($view, $value);
}
}
return '';
}
/**
* Returns true if the template exists. Otherwise, it returns false
*
* @param $templateName
* @return bool
*/
protected function templateExist($templateName): bool
{
$file = $this->getTemplateFile($templateName);
return \is_file($file);
}
/**
* Convert an array such as ["class1"=>"myclass","style="mystyle"] to class1='myclass' style='mystyle' string
*
* @param array|string $array array to convert
* @return string
*/
public function convertArg($array): string
{
if (!\is_array($array)) {
return $array; // nothing to convert.
}
return \implode(' ', \array_map('static::convertArgCallBack', \array_keys($array), $array));
}
/**
* Returns the current token. if there is not a token then it generates a new one.
* It could require an open session.
*
* @param bool $fullToken It returns a token with the current ip.
* @param string $tokenId [optional] Name of the token.
*
* @return string
*/
public function getCsrfToken($fullToken = false, $tokenId = '_token'): string
{
if ($this->csrf_token == '') {
$this->regenerateToken($tokenId);
}
if ($fullToken) {
return $this->csrf_token . '|' . $this->ipClient();
}
return $this->csrf_token;
}
/**
* Regenerates the csrf token and stores in the session.
* It requires an open session.
*
* @param string $tokenId [optional] Name of the token.
*/
public function regenerateToken($tokenId = '_token'): void
{
try {
$this->csrf_token = \bin2hex(\random_bytes(10));
} catch (Exception $e) {
$this->csrf_token = '123456789012345678901234567890'; // unable to generates a random token.
}
@$_SESSION[$tokenId] = $this->csrf_token . '|' . $this->ipClient();
}
public function ipClient()
{
if (
isset($_SERVER['HTTP_X_FORWARDED_FOR'])
&& \preg_match('/^(d{1,3}).(d{1,3}).(d{1,3}).(d{1,3})$/', $_SERVER['HTTP_X_FORWARDED_FOR'])
) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/**
* Validates if the csrf token is valid or not.
* It requires an open session.
*
* @param bool $alwaysRegenerate [optional] Default is false.
* If true then it will generate a new token regardless
* of the method.
* If false, then it will generate only if the method is POST.
* Note: You must not use true if you want to use csrf with AJAX.
*
* @param string $tokenId [optional] Name of the token.
*
* @return bool It returns true if the token is valid, or it is generated. Otherwise, false.
*/
public function csrfIsValid($alwaysRegenerate = false, $tokenId = '_token'): bool
{
if (@$_SERVER['REQUEST_METHOD'] === 'POST' && $alwaysRegenerate === false) {
$this->csrf_token = $_POST[$tokenId] ?? null; // ping pong the token.
return $this->csrf_token . '|' . $this->ipClient() === ($_SESSION[$tokenId] ?? null);
}
if ($this->csrf_token == '' || $alwaysRegenerate) {
// if not token then we generate a new one
$this->regenerateToken($tokenId);
}
return true;
}
/**
* Stop injecting content into a section and return its contents.
*
* @return string
*/
public function yieldSection(): ?string
{
$sc = $this->stopSection();
return $this->sections[$sc] ?? null;
}
/**
* Stop injecting content into a section.
*
* @param bool $overwrite
* @return string
*/
public function stopSection($overwrite = false): string
{
if (empty($this->sectionStack)) {
$this->showError('stopSection', 'Cannot end a section without first starting one.', true, true);
}
$last = \array_pop($this->sectionStack);
if ($overwrite) {
$this->sections[$last] = \ob_get_clean();
} else {
$this->extendSection($last, \ob_get_clean());
}
return $last;
}
/**
* Append content to a given section.
*
* @param string $section
* @param string $content
* @return void
*/
protected function extendSection($section, $content): void
{
if (isset($this->sections[$section])) {
$content = \str_replace($this->PARENTKEY, $content, $this->sections[$section]);
}
$this->sections[$section] = $content;
}
public function dump($object, $jsconsole = false): void
{
if (!$jsconsole) {
echo '
';
\var_dump($object);
echo '
';
} else {
/** @noinspection BadExpressionStatementJS */
/** @noinspection JSVoidFunctionReturnValueUsed */
echo '';
}
}
/**
* Start injecting content into a section.
*
* @param string $section
* @param string $content
* @return void
*/
public function startSection($section, $content = ''): void
{
if ($content === '') {
\ob_start() && $this->sectionStack[] = $section;
} else {
$this->extendSection($section, $content);
}
}
/**
* Stop injecting content into a section and append it.
*
* @return string
* @throws InvalidArgumentException
*/
public function appendSection(): string
{
if (empty($this->sectionStack)) {
$this->showError('appendSection', 'Cannot end a section without first starting one.', true, true);
}
$last = \array_pop($this->sectionStack);
if (isset($this->sections[$last])) {
$this->sections[$last] .= \ob_get_clean();
} else {
$this->sections[$last] = \ob_get_clean();
}
return $last;
}
/**
* Adds a global variable. If $varname is an array then it merges all the values.
* Example:
*
* $this->share('variable',10.5);
* $this->share('variable2','hello');
* // or we could add the two variables as:
* $this->share(['variable'=>10.5,'variable2'=>'hello']);
*
*
* @param string|array $varname It is the name of the variable or, it is an associative array
* @param mixed $value
* @return $this
* @see BladeOne::share
*/
public function with($varname, $value = null): BladeOne
{
return $this->share($varname, $value);
}
/**
* Adds a global variable. If $varname is an array then it merges all the values.
* Example:
*
* $this->share('variable',10.5);
* $this->share('variable2','hello');
* // or we could add the two variables as:
* $this->share(['variable'=>10.5,'variable2'=>'hello']);
*
*
* @param string|array $varname It is the name of the variable, or it is an associative array
* @param mixed $value
* @return $this
*/
public function share($varname, $value = null): BladeOne
{
if (is_array($varname)) {
$this->variablesGlobal = \array_merge($this->variablesGlobal, $varname);
} else {
$this->variablesGlobal[$varname] = $value;
}
return $this;
}
/**
* Get the string contents of a section.
*
* @param string $section
* @param string $default
* @return string
*/
public function yieldContent($section, $default = ''): string
{
if (isset($this->sections[$section])) {
return \str_replace($this->PARENTKEY, $default, $this->sections[$section]);
}
return $default;
}
/**
* Register a custom Blade compiler.
*
* @param callable $compiler
* @return void
*/
public function extend(callable $compiler): void
{
$this->extensions[] = $compiler;
}
/**
* Register a handler for custom directives for run at runtime
*
* @param string $name
* @param callable $handler
* @return void
*/
public function directiveRT($name, callable $handler): void
{
$this->customDirectives[$name] = $handler;
$this->customDirectivesRT[$name] = true;
}
/**
* Sets the escaped content tags used for the compiler.
*
* @param string $openTag
* @param string $closeTag
* @return void
*/
public function setEscapedContentTags($openTag, $closeTag): void
{
$this->setContentTags($openTag, $closeTag, true);
}
/**
* Gets the content tags used for the compiler.
*
* @return array
*/
public function getContentTags(): array
{
return $this->getTags();
}
/**
* Sets the content tags used for the compiler.
*
* @param string $openTag
* @param string $closeTag
* @param bool $escaped
* @return void
*/
public function setContentTags($openTag, $closeTag, $escaped = false): void
{
$property = ($escaped === true) ? 'escapedTags' : 'contentTags';
$this->{$property} = [\preg_quote($openTag), \preg_quote($closeTag)];
}
/**
* Gets the tags used for the compiler.
*
* @param bool $escaped
* @return array
*/
protected function getTags($escaped = false): array
{
$tags = $escaped ? $this->escapedTags : $this->contentTags;
return \array_map('stripcslashes', $tags);
}
/**
* Gets the escaped content tags used for the compiler.
*
* @return array
*/
public function getEscapedContentTags(): array
{
return $this->getTags(true);
}
/**
* Sets the function used for resolving classes with inject.
*
* @param callable $function
*/
public function setInjectResolver(callable $function): void
{
$this->injectResolver = $function;
}
/**
* Get the file extension for template files.
*
* @return string
*/
public function getFileExtension(): string
{
return $this->fileExtension;
}
/**
* Set the file extension for the template files.
* It must include the leading dot e.g. ".blade.php"
*
* @param string $fileExtension Example: .prefix.ext
*/
public function setFileExtension($fileExtension): void
{
$this->fileExtension = $fileExtension;
}
/**
* Get the file extension for template files.
*
* @return string
*/
public function getCompiledExtension(): string
{
return $this->compileExtension;
}
/**
* Set the file extension for the compiled files.
* Including the leading dot for the extension is required, e.g. ".bladec"
*
* @param $fileExtension
*/
public function setCompiledExtension($fileExtension): void
{
$this->compileExtension = $fileExtension;
}
/**
* @return string
* @see BladeOne::setCompileTypeFileName
*/
public function getCompileTypeFileName(): string
{
return $this->compileTypeFileName;
}
/**
* It determines how the compiled filename will be called.
* auto (default mode) the mode is "sha1"
* sha1 the filename is converted into a sha1 hash (it's the slow method, but it is safest)
* md5 the filename is converted into a md5 hash (it's faster than sha1, and it uses less space)
* @param string $compileTypeFileName=['auto','sha1','md5'][$i]
* @return BladeOne
*/
public function setCompileTypeFileName(string $compileTypeFileName): BladeOne
{
$this->compileTypeFileName = $compileTypeFileName;
return $this;
}
/**
* Add new loop to the stack.
*
* @param array|Countable $data
* @return void
*/
public function addLoop($data): void
{
$length = \is_array($data) || $data instanceof Countable ? \count($data) : null;
$parent = static::last($this->loopsStack);
$this->loopsStack[] = [
'index' => -1,
'iteration' => 0,
'remaining' => isset($length) ? $length + 1 : null,
'count' => $length,
'first' => true,
'even' => true,
'odd' => false,
'last' => isset($length) ? $length == 1 : null,
'depth' => \count($this->loopsStack) + 1,
'parent' => $parent ? (object)$parent : null,
];
}
/**
* Increment the top loop's indices.
*
* @return object
*/
public function incrementLoopIndices(): object
{
$c = \count($this->loopsStack) - 1;
$loop = &$this->loopsStack[$c];
$loop['index']++;
$loop['iteration']++;
$loop['first'] = $loop['index'] == 0;
$loop['even'] = $loop['index'] % 2 == 0;
$loop['odd'] = !$loop['even'];
if (isset($loop['count'])) {
$loop['remaining']--;
$loop['last'] = $loop['index'] == $loop['count'] - 1;
}
return (object)$loop;
}
/**
* Pop a loop from the top of the loop stack.
*
* @return void
*/
public function popLoop(): void
{
\array_pop($this->loopsStack);
}
/**
* Get an instance of the first loop in the stack.
*
* @return object|null
*/
public function getFirstLoop(): ?object
{
return ($last = static::last($this->loopsStack)) ? (object)$last : null;
}
/**
* Get the rendered contents of a partial from a loop.
*
* @param string $view
* @param array $data
* @param string $iterator
* @param string $empty
* @return string
* @throws Exception
*/
public function renderEach($view, $data, $iterator, $empty = 'raw|'): string
{
$result = '';
if (\count($data) > 0) {
// If is actually data in the array, we will loop through the data and append
// an instance of the partial view to the final result HTML passing in the
// iterated value of this data array, allowing the views to access them.
foreach ($data as $key => $value) {
$data = ['key' => $key, $iterator => $value];
$result .= $this->runChild($view, $data);
}
} elseif (static::startsWith($empty, 'raw|')) {
$result = \substr($empty, 4);
} else {
$result = $this->run($empty);
}
return $result;
}
/**
* Run the blade engine. It returns the result of the code.
*
* @param string|null $view The name of the cache. Ex: "folder.folder.view" ("/folder/folder/view.blade")
* @param array $variables An associative arrays with the values to display.
* @return string
* @throws Exception
*/
public function run($view = null, $variables = []): string
{
$mode = $this->getMode();
if ($view === null) {
$view = $this->viewStack;
}
$this->viewStack = null;
if ($view === null) {
$this->showError('run', 'BladeOne: view not set', true);
return '';
}
$forced = ($mode & 1)!==0; // mode=1 forced:it recompiles no matter if the compiled file exists or not.
$runFast = ($mode & 2)!==0; // mode=2 runfast: the code is not compiled neither checked, and it runs directly the compiled
$this->sections = [];
if ($mode == 3) {
$this->showError('run', "we can't force and run fast at the same time", true);
}
return $this->runInternal($view, $variables, $forced, $runFast);
}
/**
* It sets the current view
* This value is cleared when it is used (method run).
* Example:
*
* $this->setView('folder.view')->share(['var1'=>20])->run(); // or $this->run('folder.view',['var1'=>20]);
*
*
* @param string $view
* @return BladeOne
*/
public function setView($view): BladeOne
{
$this->viewStack = $view;
return $this;
}
/**
* It injects a function, an instance, or a method class when a view is called.
* It could be stacked. If it sets null then it clears all definitions.
* Example:
*
* $this->composer('folder.view',function($bladeOne) { $bladeOne->share('newvalue','hi there'); });
* $this->composer('folder.view','namespace1\namespace2\SomeClass'); // SomeClass must exist, and it must have the
* // method 'composer'
* $this->composer('folder.*',$instance); // $instance must have the method called 'composer'
* $this->composer(); // clear all composer.
*
*
* @param string|array|null $view It could contain wildcards (*). Example: 'aa.bb.cc','*.bb.cc','aa.bb.*','*.bb.*'
*
* @param callable|string|null $functionOrClass
* @return BladeOne
*/
public function composer($view = null, $functionOrClass = null): BladeOne
{
if ($view === null && $functionOrClass === null) {
$this->composerStack = [];
return $this;
}
if (is_array($view)) {
foreach ($view as $v) {
$this->composerStack[$v] = $functionOrClass;
}
} else {
$this->composerStack[$view] = $functionOrClass;
}
return $this;
}
/**
* Start a component rendering process.
*
* @param string $name
* @param array $data
* @return void
*/
public function startComponent($name, array $data = []): void
{
if (\ob_start()) {
$this->componentStack[] = $name;
$this->componentData[$this->currentComponent()] = $data;
$this->slots[$this->currentComponent()] = [];
}
}
/**
* Get the index for the current component.
*
* @return int
*/
protected function currentComponent(): int
{
return \count($this->componentStack) - 1;
}
/**
* Render the current component.
*
* @return string
* @throws Exception
*/
public function renderComponent(): string
{
//echo "render ";
$name = \array_pop($this->componentStack);
//return $this->runChild($name, $this->componentData());
$cd = $this->componentData();
$clean = array_keys($cd);
$r = $this->runChild($name, $cd);
// we clean variables defined inside the component (so they are garbaged when the component is used)
foreach ($clean as $key) {
unset($this->variables[$key]);
}
return $r;
}
/**
* Get the data for the given component.
*
* @return array
*/
protected function componentData(): array
{
$cs = count($this->componentStack);
//echo "";
//echo " data: ";
//var_dump($this->componentData);
//echo " datac: ";
//var_dump(count($this->componentStack));
return array_merge(
$this->componentData[$cs],
['slot' => trim(ob_get_clean())],
$this->slots[$cs]
);
}
/**
* Start the slot rendering process.
*
* @param string $name
* @param string|null $content
* @return void
*/
public function slot($name, $content = null): void
{
if (\count(\func_get_args()) === 2) {
$this->slots[$this->currentComponent()][$name] = $content;
} elseif (\ob_start()) {
$this->slots[$this->currentComponent()][$name] = '';
$this->slotStack[$this->currentComponent()][] = $name;
}
}
/**
* Save the slot content for rendering.
*
* @return void
*/
public function endSlot(): void
{
static::last($this->componentStack);
$currentSlot = \array_pop(
$this->slotStack[$this->currentComponent()]
);
$this->slots[$this->currentComponent()][$currentSlot] = \trim(\ob_get_clean());
}
/**
* @return string
*/
public function getPhpTag(): string
{
return $this->phpTag;
}
/**
* @param string $phpTag
*/
public function setPhpTag($phpTag): void
{
$this->phpTag = $phpTag;
}
/**
* @return string
*/
public function getCurrentUser(): string
{
return $this->currentUser;
}
/**
* @param string $currentUser
*/
public function setCurrentUser($currentUser): void
{
$this->currentUser = $currentUser;
}
/**
* @return string
*/
public function getCurrentRole(): string
{
return $this->currentRole;
}
/**
* @param string $currentRole
*/
public function setCurrentRole($currentRole): void
{
$this->currentRole = $currentRole;
}
/**
* @return string[]
*/
public function getCurrentPermission(): array
{
return $this->currentPermission;
}
/**
* @param string[] $currentPermission
*/
public function setCurrentPermission($currentPermission): void
{
$this->currentPermission = $currentPermission;
}
/**
* Returns the current base url without trailing slash.
*
* @return string
*/
public function getBaseUrl(): string
{
return $this->baseUrl;
}
/**
* It sets the base url and, it also calculates the relative path.
* The base url defines the "root" of the project, not always the level of the domain, but it could be
* any folder.
* This value is used to calculate the relativity of the resources, but it is also used to set the domain.
* Note: The trailing slash is removed automatically if it's present.
* Note: We should not use arguments or name of the script.
* Examples:
*
*
* @param string $baseUrl Example http://www.web.com/folder https://www.web.com/folder/anotherfolder
* @return BladeOne
*/
public function setBaseUrl($baseUrl): BladeOne
{
$this->baseUrl = \rtrim($baseUrl, '/'); // base with the url trimmed
$this->baseDomain = @parse_url($this->baseUrl)['host'];
$currentUrl = $this->getCurrentUrlCalculated();
if ($currentUrl === '') {
$this->relativePath = '';
return $this;
}
if (\strpos($currentUrl, $this->baseUrl) === 0) {
$part = \str_replace($this->baseUrl, '', $currentUrl);
$numf = \substr_count($part, '/') - 1;
$numf = ($numf > 10) ? 10 : $numf; // avoid overflow
$this->relativePath = ($numf < 0) ? '' : \str_repeat('../', $numf);
} else {
$this->relativePath = '';
}
return $this;
}
/**
* It gets the full current url calculated with the information sends by the user.
* Note: If we set baseurl, then it always uses the baseurl as domain (it's safe).
* Note: This information could be forged/faked by the end-user.
* Note: It returns empty '' if it is called in a command line interface / non-web.
* Note: It doesn't return the user and password.
* @param bool $noArgs if true then it excludes the arguments.
* @return string
*/
public function getCurrentUrlCalculated($noArgs = false): string
{
if (!isset($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'])) {
return '';
}
$host = $this->baseDomain ?? $_SERVER['HTTP_HOST']; // <-- it could be forged!
$link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http');
$port = $_SERVER['SERVER_PORT'];
$port2 = (($link === 'http' && $port === '80') || ($link === 'https' && $port === '443')) ? '' : ':' . $port;
$link .= "://$host$port2$_SERVER[REQUEST_URI]";
if ($noArgs) {
$link = @explode('?', $link)[0];
}
return $link;
}
/**
* It returns the relative path to the base url or empty if not set
* Example:
*
* Note:The relative path is calculated when we set the base url.
*
* @return string
* @see BladeOne::setBaseUrl
*/
public function getRelativePath(): string
{
return $this->relativePath;
}
/**
* It gets the full current canonical url.
* Example: https://www.mysite.com/aaa/bb/php.php?aa=bb
*
*
It returns the $this->canonicalUrl value if is not null
*
Otherwise, it returns the $this->currentUrl if not null
*
Otherwise, the url is calculated with the information sends by the user
*
*
* @return string|null
*/
public function getCanonicalUrl(): ?string
{
return $this->canonicalUrl ?? $this->getCurrentUrl();
}
/**
* It sets the full canonical url.
* Example: https://www.mysite.com/aaa/bb/php.php?aa=bb
*
* @param string|null $canonUrl
* @return BladeOne
*/
public function setCanonicalUrl($canonUrl = null): BladeOne
{
$this->canonicalUrl = $canonUrl;
return $this;
}
/**
* It gets the full current url
* Example: https://www.mysite.com/aaa/bb/php.php?aa=bb
*
*
It returns the $this->currentUrl if not null
*
Otherwise, the url is calculated with the information sends by the user
*
*
* @param bool $noArgs if true then it ignores the arguments.
* @return string|null
*/
public function getCurrentUrl($noArgs = false): ?string
{
$link = $this->currentUrl ?? $this->getCurrentUrlCalculated();
if ($noArgs) {
$link = @explode('?', $link)[0];
}
return $link;
}
/**
* It sets the full current url.
* Example: https://www.mysite.com/aaa/bb/php.php?aa=bb
* Note: If the current url is not set, then the system could calculate the current url.
*
* @param string|null $currentUrl
* @return BladeOne
*/
public function setCurrentUrl($currentUrl = null): BladeOne
{
$this->currentUrl = $currentUrl;
return $this;
}
/**
* If true then it optimizes the result (it removes tab and extra spaces).
*
* @param bool $bool
* @return BladeOne
*/
public function setOptimize($bool = false): BladeOne
{
$this->optimize = $bool;
return $this;
}
/**
* It sets the callback function for authentication. It is used by @can and @cannot
*
* @param callable $fn
*/
public function setCanFunction(callable $fn): void
{
$this->authCallBack = $fn;
}
/**
* It sets the callback function for authentication. It is used by @canany
*
* @param callable $fn
*/
public function setAnyFunction(callable $fn): void
{
$this->authAnyCallBack = $fn;
}
/**
* It sets the callback function for errors. It is used by @error
*
* @param callable $fn
*/
public function setErrorFunction(callable $fn): void
{
$this->errorCallBack = $fn;
}
//
//
/**
* Get the entire loop stack.
*
* @return array
*/
public function getLoopStack(): array
{
return $this->loopsStack;
}
/**
* It adds a string inside a quoted string
* example:
*
*
* @param $quoted
* @param $newFragment
* @return string
*/
public function addInsideQuote($quoted, $newFragment): string
{
if ($this->isQuoted($quoted)) {
return substr($quoted, 0, -1) . $newFragment . substr($quoted, -1);
}
return $quoted . $newFragment;
}
/**
* Return true if the string is a php variable (it starts with $)
*
* @param string|null $text
* @return bool
*/
public function isVariablePHP($text): bool
{
if (!$text || strlen($text) < 2) {
return false;
}
return $text[0] === '$';
}
/**
* It's the same as "@_e", however it parses the text (using sprintf).
* If the operation fails then, it returns the original expression without translation.
*
* @param $phrase
*
* @return string
*/
public function _ef($phrase): string
{
$argv = \func_get_args();
$r = $this->_e($phrase);
$argv[0] = $r; // replace the first argument with the translation.
$result = @sprintf(...$argv);
return !$result ? $r : $result;
}
/**
* Tries to translate the word if it's in the array defined by BladeOneLang::$dictionary
* If the operation fails then, it returns the original expression without translation.
*
* @param $phrase
*
* @return string
*/
public function _e($phrase): string
{
if ((!\array_key_exists($phrase, static::$dictionary))) {
$this->missingTranslation($phrase);
return $phrase;
}
return static::$dictionary[$phrase];
}
/**
* Log a missing translation into the file $this->missingLog.
* If the file is not defined, then it doesn't write the log.
*
* @param string $txt Message to write on.
*/
protected function missingTranslation($txt): void
{
if (!$this->missingLog) {
return; // if there is not a file assigned then it skips saving.
}
$fz = @\filesize($this->missingLog);
if (\is_object($txt) || \is_array($txt)) {
$txt = \print_r($txt, true);
}
// Rewrite file if more than 100000 bytes
$mode = ($fz > 100000) ? 'w' : 'a';
$fp = \fopen($this->missingLog, $mode);
\fwrite($fp, $txt . "\n");
\fclose($fp);
}
/**
* if num is more than one then it returns the phrase in plural, otherwise the phrase in singular.
* Note: the translation should be as follows: $msg['Person']='Person' $msg=['Person']['p']='People'
*
* @param string $phrase
* @param string $phrases
* @param int $num
*
* @return string
*/
public function _n($phrase, $phrases, $num = 0): string
{
if ((!\array_key_exists($phrase, static::$dictionary))) {
$this->missingTranslation($phrase);
return ($num <= 1) ? $phrase : $phrases;
}
return ($num <= 1) ? $this->_e($phrase) : $this->_e($phrases);
}
/**
* @param $expression
* @return string
* @see BladeOne::getCanonicalUrl
*/
public function compileCanonical($expression = null): string
{
return '';
}
/**
* @param $expression
* @return string
* @see BladeOne::getBaseUrl
*/
public function compileBase($expression = null): string
{
return '';
}
protected function compileUse($expression): string
{
return $this->phpTag . 'use ' . $this->stripParentheses($expression) . '; ?>';
}
protected function compileSwitch($expression): string
{
$this->switchCount++;
$this->firstCaseInSwitch = true;
return $this->phpTag . "switch $expression {";
}
//
//
protected function compileDump($expression): string
{
return $this->phpTagEcho . "\$this->dump$expression;?>";
}
protected function compileRelative($expression): string
{
return $this->phpTagEcho . "\$this->relative$expression;?>";
}
protected function compileMethod($expression): string
{
$v = $this->stripParentheses($expression);
return "";
}
protected function compilecsrf($expression = null): string
{
$expression = $expression ?? "'_token'";
return "";
}
protected function compileDd($expression): string
{
return $this->phpTagEcho . "'
'; var_dump$expression; echo '
';?>";
}
/**
* Execute the case tag.
*
* @param $expression
* @return string
*/
protected function compileCase($expression): string
{
if ($this->firstCaseInSwitch) {
$this->firstCaseInSwitch = false;
return 'case ' . $expression . ': ?>';
}
return $this->phpTag . "case $expression: ?>";
}
/**
* Compile the while statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileWhile($expression): string
{
return $this->phpTag . "while$expression: ?>";
}
/**
* default tag used for switch/case
*
* @return string
*/
protected function compileDefault(): string
{
if ($this->firstCaseInSwitch) {
return $this->showError('@default', '@switch without any @case', true);
}
return $this->phpTag . 'default: ?>';
}
protected function compileEndSwitch(): string
{
--$this->switchCount;
if ($this->switchCount < 0) {
return $this->showError('@endswitch', 'Missing @switch', true);
}
return $this->phpTag . '} // end switch ?>';
}
/**
* Compile while statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileInject($expression): string
{
$ex = $this->stripParentheses($expression);
$p0 = \strpos($ex, ',');
if (!$p0) {
$var = $this->stripQuotes($ex);
$namespace = '';
} else {
$var = $this->stripQuotes(\substr($ex, 0, $p0));
$namespace = $this->stripQuotes(\substr($ex, $p0 + 1));
}
return $this->phpTag . "\$$var = \$this->injectClass('$namespace', '$var'); ?>";
}
/**
* Remove first and end quote from a quoted string of text
*
* @param mixed $text
* @return null|string|string[]
*/
public function stripQuotes($text)
{
if (!$text || strlen($text) < 2) {
return $text;
}
$text = trim($text);
$p0 = $text[0];
$p1 = \substr($text, -1);
if ($p0 === $p1 && ($p0 === '"' || $p0 === "'")) {
return \substr($text, 1, -1);
}
return $text;
}
/**
* Execute the user defined extensions.
*
* @param string $value
* @return string
*/
protected function compileExtensions($value): string
{
foreach ($this->extensions as $compiler) {
$value = $compiler($value, $this);
}
return $value;
}
/**
* Compile Blade comments into valid PHP.
*
* @param string $value
* @return string
*/
protected function compileComments($value): string
{
$pattern = \sprintf('/%s--(.*?)--%s/s', $this->contentTags[0], $this->contentTags[1]);
return \preg_replace($pattern, $this->phpTag . '/*$1*/ ?>', $value);
}
/**
* Compile Blade echos into valid PHP.
*
* @param string $value
* @return string
* @throws Exception
*/
protected function compileEchos($value): string
{
foreach ($this->getEchoMethods() as $method => $length) {
$value = $this->$method($value);
}
return $value;
}
/**
* Get the echo methods in the proper order for compilation.
*
* @return array
*/
protected function getEchoMethods(): array
{
$methods = [
'compileRawEchos' => \strlen(\stripcslashes($this->rawTags[0])),
'compileEscapedEchos' => \strlen(\stripcslashes($this->escapedTags[0])),
'compileRegularEchos' => \strlen(\stripcslashes($this->contentTags[0])),
];
\uksort($methods, static function ($method1, $method2) use ($methods) {
// Ensure the longest tags are processed first
if ($methods[$method1] > $methods[$method2]) {
return -1;
}
if ($methods[$method1] < $methods[$method2]) {
return 1;
}
// Otherwise, give preference to raw tags (assuming they've overridden)
if ($method1 === 'compileRawEchos') {
return -1;
}
if ($method2 === 'compileRawEchos') {
return 1;
}
if ($method1 === 'compileEscapedEchos') {
return -1;
}
if ($method2 === 'compileEscapedEchos') {
return 1;
}
throw new BadMethodCallException("Method [$method1] not defined");
});
return $methods;
}
/**
* Compile Blade statements that start with "@".
*
* @param string $value
*
* @return array|string|string[]|null
*/
protected function compileStatements($value)
{
/**
* @param array $match
* [0]=full expression with @ and parenthesis
* [1]=expression without @ and argument
* [2]=????
* [3]=argument with parenthesis and without the first @
* [4]=argument without parenthesis.
*
* @return mixed|string
*/
$callback = function ($match) {
if (static::contains($match[1], '@')) {
// @@escaped tag
$match[0] = isset($match[3]) ? $match[1] . $match[3] : $match[1];
} else {
if (strpos($match[1], '::') !== false) {
// Someclass::method
return $this->compileStatementClass($match);
}
if (isset($this->customDirectivesRT[$match[1]])) {
if ($this->customDirectivesRT[$match[1]]) {
$match[0] = $this->compileStatementCustom($match);
} else {
$match[0] = \call_user_func(
$this->customDirectives[$match[1]],
$this->stripParentheses(static::get($match, 3))
);
}
} elseif (\method_exists($this, $method = 'compile' . \ucfirst($match[1]))) {
// it calls the function compile
$match[0] = $this->$method(static::get($match, 3));
} else {
return $match[0];
}
}
return isset($match[3]) ? $match[0] : $match[0] . $match[2];
};
/* return \preg_replace_callback('/\B@(@?\w+)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', $callback, $value); */
return preg_replace_callback('/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', $callback, $value);
}
/**
* Determine if a given string contains a given substring.
*
* @param string $haystack
* @param string|array $needles
* @return bool
*/
public static function contains($haystack, $needles): bool
{
foreach ((array)$needles as $needle) {
if ($needle != '') {
if (\function_exists('mb_strpos')) {
if (\mb_strpos($haystack, $needle) !== false) {
return true;
}
} elseif (\strpos($haystack, $needle) !== false) {
return true;
}
}
}
return false;
}
protected function compileStatementClass($match): string
{
if (isset($match[3])) {
return $this->phpTagEcho . $this->fixNamespaceClass($match[1]) . $match[3] . '; ?>';
}
return $this->phpTagEcho . $this->fixNamespaceClass($match[1]) . '(); ?>';
}
/**
* Util method to fix namespace of a class
* Example: "SomeClass::method()" -> "\namespace\SomeClass::method()"
*
* @param string $text
*
* @return string
* @see BladeOne
*/
protected function fixNamespaceClass($text): string
{
if (strpos($text, '::') === false) {
return $text;
}
$classPart = explode('::', $text, 2);
if (isset($this->aliasClasses[$classPart[0]])) {
$classPart[0] = $this->aliasClasses[$classPart[0]];
}
return $classPart[0] . '::' . $classPart[1];
}
/**
* For compile custom directive at runtime.
*
* @param $match
* @return string
*/
protected function compileStatementCustom($match): string
{
$v = $this->stripParentheses(static::get($match, 3));
$v = ($v == '') ? '' : ',' . $v;
return $this->phpTag . 'call_user_func($this->customDirectives[\'' . $match[1] . '\']' . $v . '); ?>';
}
/**
* Get an item from an array using "dot" notation.
*
* @param ArrayAccess|array $array
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function get($array, $key, $default = null)
{
$accesible = \is_array($array) || $array instanceof ArrayAccess;
if (!$accesible) {
return static::value($default);
}
if (\is_null($key)) {
return $array;
}
if (static::exists($array, $key)) {
return $array[$key];
}
foreach (\explode('.', $key) as $segment) {
if (static::exists($array, $segment)) {
$array = $array[$segment];
} else {
return static::value($default);
}
}
return $array;
}
/**
* Determine if the given key exists in the provided array.
*
* @param ArrayAccess|array $array
* @param string|int $key
* @return bool
*/
public static function exists($array, $key): bool
{
if ($array instanceof ArrayAccess) {
return $array->offsetExists($key);
}
return \array_key_exists($key, $array);
}
/**
* This method removes the parenthesis of the expression and parse the arguments.
* @param string $expression
* @return array
*/
protected function getArgs($expression): array
{
return $this->parseArgs($this->stripParentheses($expression), ' ');
}
/**
* It separates a string using a separator and an identifier
* It excludes quotes,double quotes and the "¬" symbol.
* Example
*