Issues (102)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Ebi.php (16 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Ebi;
9
10
11
class Ebi {
12
    /**
13
     * @var string
14
     */
15
    protected $cachePath;
16
    /**
17
     * @var callable[]
18
     */
19
    protected $functions;
20
    /**
21
     * @var TemplateLoaderInterface
22
     */
23
    private $templateLoader;
24
    /**
25
     * @var CompilerInterface
26
     */
27
    private $compiler;
28
    /**
29
     * @var callable[]
30
     */
31
    private $components = [];
32
    /**
33
     * @var array
34
     */
35
    private $meta;
36
37
    /**
38
     * Ebi constructor.
39
     *
40
     * @param TemplateLoaderInterface $templateLoader Used to load template sources from component names.
41
     * @param string $cachePath The path to cache compiled templates.
42
     * @param CompilerInterface $compiler The compiler used to compile templates.
0 ignored issues
show
Should the type for parameter $compiler not be null|CompilerInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
43
     */
44 83
    public function __construct(TemplateLoaderInterface $templateLoader, $cachePath, CompilerInterface $compiler = null) {
45 83
        $this->templateLoader = $templateLoader;
46 83
        $this->cachePath = $cachePath;
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
47 83
        $this->compiler = $compiler ?: new Compiler();
0 ignored issues
show
Documentation Bug introduced by
It seems like $compiler ?: new \Ebi\Compiler() can also be of type object<Ebi\Compiler>. However, the property $compiler is declared as type object<Ebi\CompilerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
48
49 83
        $this->defineFunction('abs');
50 83
        $this->defineFunction('arrayColumn', 'array_column');
51 83
        $this->defineFunction('arrayKeyExists', 'array_key_exists');
52 83
        $this->defineFunction('arrayKeys', 'array_keys');
53 83
        $this->defineFunction('arrayMerge', 'array_merge');
54 83
        $this->defineFunction('arrayMergeRecursive', 'array_merge_recursive');
55 83
        $this->defineFunction('arrayReplace', 'array_replace');
56 83
        $this->defineFunction('arrayReplaceRecursive', 'array_replace_recursive');
57 83
        $this->defineFunction('arrayReverse', 'array_reverse');
58 83
        $this->defineFunction('arrayValues', 'array_values');
59 83
        $this->defineFunction('base64Encode', 'base64_encode');
60 83
        $this->defineFunction('ceil');
61 83
        $this->defineFunction('componentExists', [$this, 'componentExists']);
62 83
        $this->defineFunction('count');
63 83
        $this->defineFunction('empty');
64 83
        $this->defineFunction('floor');
65 83
        $this->defineFunction('formatDate', [$this, 'formatDate']);
66 83
        $this->defineFunction('formatNumber', 'number_format');
67 83
        $this->defineFunction('htmlEncode', 'htmlspecialchars');
68 83
        $this->defineFunction('isArray', 'is_array');
69 83
        $this->defineFunction('isBool', 'is_bool');
70 83
        $this->defineFunction('isInt', 'is_int');
71 83
        $this->defineFunction('isScalar', 'is_scalar');
72 83
        $this->defineFunction('isString', 'is_string');
73 83
        $this->defineFunction('join');
74 83
        $this->defineFunction('lcase', $this->mb('strtolower'));
75 83
        $this->defineFunction('lcfirst');
76 83
        $this->defineFunction('ltrim');
77 83
        $this->defineFunction('max');
78 83
        $this->defineFunction('min');
79 83
        $this->defineFunction('queryEncode', 'http_build_query');
80 83
        $this->defineFunction('round');
81 83
        $this->defineFunction('rtrim');
82 83
        $this->defineFunction('sprintf');
83 83
        $this->defineFunction('strlen', $this->mb('strlen'));
84 83
        $this->defineFunction('substr', $this->mb('substr'));
85 83
        $this->defineFunction('trim');
86 83
        $this->defineFunction('ucase', $this->mb('strtoupper'));
87 83
        $this->defineFunction('ucfirst');
88 83
        $this->defineFunction('ucwords');
89 83
        $this->defineFunction('urlencode', 'rawurlencode');
90
91 83
        $this->defineFunction('@class', [$this, 'attributeClass']);
92 83
        $this->defineFunction('@style', [$this, 'attributeStyle']);
93
94
        // Define a simple component not found component to help troubleshoot.
95
        $this->defineComponent('@component-not-found', function ($props) {
96 1
            echo '<!-- Ebi component "'.htmlspecialchars($props['component']).'" not found. -->';
97 83
        });
98
99
        // Define a simple component exception.
100
        $this->defineComponent('@exception', function ($props) {
101 1
            echo "\n<!--\nEbi exception in component \"".htmlspecialchars($props['component'])."\".\n".
102 1
                htmlspecialchars($props['message'])."\n-->\n";
103
104 83
        });
105
106 83
        $this->defineComponent('@compile-exception', [$this, 'writeCompileException']);
107 83
    }
108
109
    /**
110
     * Register a runtime function.
111
     *
112
     * @param string $name The name of the function.
113
     * @param callable $function The function callback.
0 ignored issues
show
Should the type for parameter $function not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
114
     */
115 83
    public function defineFunction($name, $function = null) {
116 83
        if ($function === null) {
117 83
            $function = $name;
118 83
        }
119
120 83
        $this->functions[strtolower($name)] = $function;
121 83
        $this->compiler->defineFunction($name, $function);
0 ignored issues
show
The method defineFunction() does not seem to exist on object<Ebi\CompilerInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
122 83
    }
123
124 83
    private function mb($func) {
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
125 83
        return function_exists("mb_$func") ? "mb_$func" : $func;
126
    }
127
128
    /**
129
     * Write a component to the output buffer.
130
     *
131
     * @param string $component The name of the component.
132
     * @param array ...$args
133
     */
134 23
    public function write($component, ...$args) {
135 23
        $component = strtolower($component);
136
137
        try {
138 23
            $callback = $this->lookup($component);
139
140 23
            if (is_callable($callback)) {
141 23
                call_user_func($callback, ...$args);
142 23
            } else {
143 1
                $this->write('@component-not-found', ['component' => $component]);
144
            }
145 23
        } catch (\Throwable $ex) {
0 ignored issues
show
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
146
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
147
            return;
148 1
        } catch (\Exception $ex) {
149 1
            $this->write('@exception', ['message' => $ex->getMessage(), 'code', $ex->getCode(), 'component' => $component]);
150 1
            return;
151
        }
152 23
    }
153
154
    /**
155
     * Lookup a component with a given name.
156
     *
157
     * @param string $component The component to lookup.
158
     * @return callable|null Returns the component function or **null** if the component is not found.
159
     */
160 78
    public function lookup($component) {
161 78
        $component = strtolower($component);
162 78
        $key = $this->componentKey($component);
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
163
164 78
        if (!array_key_exists($key, $this->components)) {
165 73
            $this->loadComponent($component);
166 73
        }
167
168 78
        if (isset($this->components[$key])) {
169 77
            return $this->components[$key];
170
        } else {
171
            // Mark a tombstone to the component array so it doesn't keep getting loaded.
172 2
            $this->components[$key] = null;
173 2
            return null;
174
        }
175
    }
176
177
    /**
178
     * Check to see if a component exists.
179
     *
180
     * @param string $component The name of the component.
181
     * @param bool $loader Whether or not to use the component loader or just look in the component cache.
182
     * @return bool Returns **true** if the component exists or **false** otherwise.
183
     */
184 2
    public function componentExists($component, $loader = true) {
185 2
        $componentKey = $this->componentKey($component);
186 2
        if (array_key_exists($componentKey, $this->components)) {
187 1
            return $this->components[$componentKey] !== null;
188 2
        } elseif ($loader) {
189 2
            return !empty($this->templateLoader->cacheKey($component));
190
        }
191 1
        return false;
192
    }
193
194
    /**
195
     * Strip the namespace off a component name to get the component key.
196
     *
197
     * @param string $component The full name of the component with a possible namespace.
198
     * @return string Returns the component key.
199
     */
200 80
    protected function componentKey($component) {
201 80
        if (false !== $pos = strpos($component, ':')) {
202 1
            $component = substr($component, $pos + 1);
203 1
        }
204 80
        return strtolower($component);
205
    }
206
207
    /**
208
     * Load a component.
209
     *
210
     * @param string $component The name of the component to load.
211
     * @return callable|null Returns the component or **null** if the component isn't found.
212
     */
213 73
    protected function loadComponent($component) {
214 73
        $cacheKey = $this->templateLoader->cacheKey($component);
215
        // The template loader can tell us a template doesn't exist when giving the cache key.
216 73
        if (empty($cacheKey)) {
217 2
            return null;
218
        }
219
220 71
        $cachePath = "{$this->cachePath}/$cacheKey.php";
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
221 71
        $componentKey = $this->componentKey($component);
222
223 71
        if (!file_exists($cachePath)) {
224 71
            $src = $this->templateLoader->load($component);
225
            try {
226 71
                return $this->compile($componentKey, $src, $cacheKey);
227 8
            } catch (CompileException $ex) {
228 8
                $props = ['message' => $ex->getMessage()] + $ex->getContext();
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 34 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
229 8
                return $this->components[$componentKey] = function() use ($props) {
230 8
                    $this->write('@compile-exception', $props);
231 8
                };
232
            }
233
        } else {
234
            return $this->includeComponent($componentKey, $cachePath);
235
        }
236
    }
237
238 8
    protected function writeCompileException($props) {
239 8
        echo "\n<section class=\"ebi-ex\">\n",
240 8
            '<h2>Error compiling '.htmlspecialchars($props['path'])." near line {$props['line']}.</h2>\n";
241
242 8
        echo '<p class="ebi-ex-message">'.htmlspecialchars($props['message'])."</p>\n";
243
244 8
        if (!empty($props['source'])) {
245 6
            $source = $props['source'];
246 6
            if (isset($props['sourcePosition'])) {
247 3
                $pos = $props['sourcePosition'];
248 3
                $len = isset($props['sourceLength']) ? $props['sourceLength'] : 1;
249
250 3
                if ($len === 1) {
251
                    // Small kludge to select a viewable character.
252 3
                    for (; $pos >= 0 && isset($source[$pos]) && in_array($source[$pos], [' ', "\n"], true); $pos--, $len++) {
0 ignored issues
show
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
253
                        // It's all in the loop.
254 1
                    }
255 3
                }
256
257 3
                $source = htmlspecialchars(substr($source, 0, $pos)).
258 3
                    '<mark class="ebi-ex-highlight">'.htmlspecialchars(substr($source, $pos, $len)).'</mark>'.
259 3
                    htmlspecialchars(substr($source, $pos + $len));
260 3
            } else {
261 3
                $source = htmlspecialchars($source);
262
            }
263
264 6
            echo '<pre class="ebi-ex-source ebi-ex-context"><code>',
265
                $source,
266
                "</code></pre>\n";
267 6
        }
268
269 8
        if (!empty($props['lines'])) {
270 8
            echo '<pre class="ebi-ex-source ebi-ex-lines">';
271
272 8
            foreach ($props['lines'] as $i => $line) {
273 8
                echo '<code class="ebi-ex-line">';
274
275 8
                $str = sprintf("%3d. %s", $i, htmlspecialchars($line));
276 8
                if ($i === $props['line']) {
277 8
                    echo "<mark class=\"ebi-ex-highlight\">$str</mark>";
278 8
                } else {
279 6
                    echo $str;
280
                }
281
282 8
                echo "</code>\n";
283 8
            }
284
285 8
            echo "</pre>\n";
286 8
        }
287
288 8
        echo "</section>\n";
289 8
    }
290
291
    /**
292
     * Check to see if a specific cache key exists in the cache.
293
     *
294
     * @param string $cacheKey The cache key to check.
295
     * @return bool Returns **true** if there is a cache key at the file or **false** otherwise.
296
     */
297 1
    public function cacheKeyExists($cacheKey) {
298 1
        $cachePath = "{$this->cachePath}/$cacheKey.php";
299 1
        return file_exists($cachePath);
300
    }
301
302
    /**
303
     * Compile a component from source, cache it and include it.
304
     *
305
     * @param string $component The name of the component.
306
     * @param string $src The component source.
307
     * @param string $cacheKey The cache key of the component.
308
     * @return callable|null Returns the compiled component closure.
309
     */
310 75
    public function compile($component, $src, $cacheKey) {
311 75
        $cachePath = "{$this->cachePath}/$cacheKey.php";
312 75
        $component = strtolower($component);
313
314 75
        $php = $this->compiler->compile($src, ['basename' => $component, 'path' => $cacheKey]);
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
315 67
        $comment = "/*\n".str_replace('*/', '❄/', trim($src))."\n*/";
316
317 67
        $this->filePutContents($cachePath, "<?php\n$comment\n$php");
318
319 67
        return $this->includeComponent($component, $cachePath);
320
    }
321
322
    /**
323
     * Include a cached component.
324
     *
325
     * @param string $component The component key.
326
     * @param string $cachePath The path to the component.
327
     * @return callable|null Returns the component function or **null** if the component wasn't properly defined.
328
     */
329 67
    private function includeComponent($component, $cachePath) {
330 67
        unset($this->components[$component]);
331 67
        $fn = $this->requireFile($cachePath);
332
333 67
        if (isset($this->components[$component])) {
334 67
            return $this->components[$component];
335
        } elseif (is_callable($fn)) {
336
            $this->defineComponent($component, $fn);
337
            return $fn;
338
        } else {
339
            $this->components[$component] = null;
340
            return null;
341
        }
342
    }
343
344
    /**
345
     * A safe version of {@link file_put_contents()} that also clears op caches.
346
     *
347
     * @param string $path The path to save to.
348
     * @param string $contents The contents of the file.
349
     * @return bool Returns **true** on success or **false** on failure.
350
     */
351 67
    private function filePutContents($path, $contents) {
352 67
        if (!file_exists(dirname($path))) {
353 4
            mkdir(dirname($path), 0777, true);
354 4
        }
355 67
        $tmpPath = tempnam(dirname($path), 'ebi-');
356 67
        $r = false;
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
357 67
        if (file_put_contents($tmpPath, $contents) !== false) {
358 67
            chmod($tmpPath, 0664);
359 67
            $r = rename($tmpPath, $path);
360 67
        }
361
362 67
        if (function_exists('apc_delete_file')) {
363
            // This fixes a bug with some configurations of apc.
364
            @apc_delete_file($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
365 67
        } elseif (function_exists('opcache_invalidate')) {
366 67
            @opcache_invalidate($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
367 67
        }
368
369 67
        return $r;
370
    }
371
372
    /**
373
     * Include a file.
374
     *
375
     * This is method is useful for including a file bound to this object instance.
376
     *
377
     * @param string $path The path to the file to include.
378
     * @return mixed Returns the result of the include.
379
     */
380 67
    public function requireFile($path) {
381 67
        return require $path;
382
    }
383
384
    /**
385
     * Register a component.
386
     *
387
     * @param string $name The name of the component to register.
388
     * @param callable $component The component function.
389
     */
390 83
    public function defineComponent($name, callable $component) {
391 83
        $this->components[$name] = $component;
392 83
    }
393
394
    /**
395
     * Render a component to a string.
396
     *
397
     * @param string $component The name of the component to render.
398
     * @param array ...$args Arguments to pass to the component.
399
     * @return string|null Returns the rendered component or **null** if the component was not found.
400
     */
401 74
    public function render($component, ...$args) {
402 74
        if ($callback = $this->lookup($component)) {
403 74
            ob_start();
404 74
            $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
405 74
            call_user_func($callback, ...$args);
406 74
            error_reporting($errs);
407 74
            $str = ob_get_clean();
408 74
            return $str;
409
        } else {
410
            trigger_error("Could not find component $component.", E_USER_NOTICE);
411
            return null;
412
        }
413
    }
414
415
    /**
416
     * Set the error reporting appropriate for template rendering.
417
     *
418
     * @return int Returns the previous error level.
419
     */
420
    public function setErrorReporting() {
421
        $errs = error_reporting(error_reporting() & ~E_NOTICE & ~E_WARNING);
422
        return $errs;
423
    }
424
425
    /**
426
     * Call a function registered with **defineFunction()**.
427
     *
428
     * If a static or global function is registered then it's simply rendered in the compiled template.
429
     * This method is for closures or callbacks.
430
     *
431
     * @param string $name The name of the registered function.
432
     * @param array ...$args The function's argument.
433
     * @return mixed Returns the result of the function
434
     * @throws RuntimeException Throws an exception when the function isn't found.
435
     */
436 4
    public function call($name, ...$args) {
437 4
        if (!isset($this->functions[$name])) {
438 1
            throw new RuntimeException("Call to undefined function $name.", 500);
439
        } else {
440 3
            return $this->functions[$name](...$args);
441
        }
442
    }
443
444
    /**
445
     * Render a variable appropriately for CSS.
446
     *
447
     * This is a convenience runtime function.
448
     *
449
     * @param string|array $expr A CSS class, an array of CSS classes, or an associative array where the keys are class
450
     * names and the values are truthy conditions to include the class (or not).
451
     * @return string Returns a space-delimited CSS class string.
452
     */
453 11
    public function attributeClass($expr) {
454 11
        if (is_array($expr)) {
455 4
            $classes = [];
456 4
            foreach ($expr as $i => $val) {
457 4
                if (is_array($val)) {
458 1
                    $classes[] = $this->attributeClass($val);
459 4
                } elseif (is_int($i)) {
460 1
                    $classes[] = $val;
461 4
                } elseif (!empty($val)) {
462 3
                    $classes[] = $i;
463 3
                }
464 4
            }
465 4
            return implode(' ', $classes);
466
        } else {
467 7
            return (string)$expr;
468
        }
469
    }
470
471
    /**
472
     * Render a variable appropriately for a style attribute.
473
     *
474
     * This function expects a string or an array. If an array is supplied then the keys represent style properties and
475
     * the values are style values.
476
     *
477
     * @param mixed $expr The expression to render.
478
     * @return string Returns a style attribute.
479
     */
480 4
    public function attributeStyle($expr) {
481 4
        static $false = ['display' => 'none', 'visibility' => 'hidden', 'border' => 'none', 'box-shadow' => 'none'];
482 4
        static $true = ['visibility' => 'visible'];
483
484 4
        if (is_array($expr)) {
485 4
            $style = [];
486 4
            foreach ($expr as $prop => $value) {
487 4
                if ($value === false && isset($false[$prop])) {
488 1
                    $value = $false[$prop];
489 4
                } elseif ($value === true && isset($true[$prop])) {
490 1
                    $value = $true[$prop];
491 3
                } elseif (in_array($value, [null, '', [], false], true)) {
492
                    continue;
493 2
                } elseif (is_array($value)) {
494 1
                    if ($prop === 'font-family') {
495 1
                        $value = "'".implode("','", $value)."'";
496 1
                    } else {
497 1
                        $value = implode(' ', $value);
498
                    }
499 1
                }
500
501 4
                $style[] = "$prop: $value";
502 4
            }
503 4
            return implode('; ', $style);
504
        } else {
505
            return (string)$expr;
506
        }
507
    }
508
509
    /**
510
     * Format a data.
511
     *
512
     * @param mixed $date The date to format. This can be a string data, a timestamp or an instance of **DateTimeInterface**.
513
     * @param string $format The format of the date.
514
     * @return string Returns the formatted data.
515
     * @see date_format()
516
     */
517 1
    public function formatDate($date, $format = 'c') {
518 1
        if (is_string($date)) {
519
            try {
520 1
                $date = new \DateTimeImmutable($date);
521 1
            } catch (\Exception $ex) {
522
                return '#error#';
523
            }
524 1
        } elseif (empty($date)) {
525
            return '';
526
        } elseif (is_int($date)) {
527
            try {
528
                $date = new \DateTimeImmutable('@'.$date);
529
            } catch (\Exception $ex) {
530
                return '#error#';
531
            }
532
        } elseif (!$date instanceof \DateTimeInterface) {
533
            return '#error#';
534
        }
535
536 1
        return $date->format($format);
537
    }
538
539
    /**
540
     * Get a single item from the meta array.
541
     *
542
     * @param string $name The key to get from.
543
     * @param mixed $default The default value if no item at the key exists.
544
     * @return mixed Returns the meta value.
545
     */
546
    public function getMeta($name, $default = null) {
547
        return isset($this->meta[$name]) ? $this->meta[$name] : $default;
548
    }
549
550
    /**
551
     * Set a single item to the meta array.
552
     *
553
     * @param string $name The key to set.
554
     * @param mixed $value The new value.
555
     * @return $this
556
     */
557 1
    public function setMeta($name, $value) {
558 1
        $this->meta[$name] = $value;
559 1
        return $this;
560
    }
561
562
    /**
563
     * Get the template loader.
564
     *
565
     * The template loader translates component names into template contents.
566
     *
567
     * @return TemplateLoaderInterface Returns the template loader.
568
     */
569 1
    public function getTemplateLoader() {
570 1
        return $this->templateLoader;
571
    }
572
573
    /**
574
     * Set the template loader.
575
     *
576
     * The template loader translates component names into template contents.
577
     *
578
     * @param TemplateLoaderInterface $templateLoader The new template loader.
579
     * @return $this
580
     */
581
    public function setTemplateLoader($templateLoader) {
582
        $this->templateLoader = $templateLoader;
583
        return $this;
584
    }
585
586
    /**
587
     * Get the entire meta array.
588
     *
589
     * @return array Returns the meta.
590
     */
591
    public function getMetaArray() {
592
        return $this->meta;
593
    }
594
595
    /**
596
     * Set the entire meta array.
597
     *
598
     * @param array $meta The new meta array.
599
     * @return $this
600
     */
601
    public function setMetaArray(array $meta) {
602
        $this->meta = $meta;
603
        return $this;
604
    }
605
606
    /**
607
     * Return a dynamic attribute.
608
     *
609
     * The attribute renders differently depending on the value.
610
     *
611
     * - If the value is **true** then it will render as an HTML5 boolean attribute.
612
     * - If the value is **false** or **null** then the attribute will not render.
613
     * - Other values render as attribute values.
614
     * - Attributes that start with **aria-** render **true** and **false** as values.
615
     *
616
     * @param string $name The name of the attribute.
617
     * @param mixed $value The value of the attribute.
618
     * @return string Returns the attribute definition or an empty string.
619
     */
620 24
    public function attribute($name, $value) {
621 24
        if (substr($name, 0, 5) === 'aria-' && is_bool($value)) {
622 2
            $value = $value ? 'true' : 'false';
623 2
        }
624
625 24
        if ($value === true) {
626 1
            return ' '.$name;
627 23
        } elseif (!in_array($value, [null, false], true)) {
628 20
            return " $name=\"".htmlspecialchars($value).'"';
629
        }
630 4
        return '';
631
    }
632
633
    /**
634
     * Escape a value for echoing to HTML with a bit of non-scalar checking.
635
     *
636
     * @param mixed $val The value to escape.
637
     * @return string The escaped value.
638
     */
639 27
    public function escape($val = null) {
640 27
        if (is_array($val)) {
641 1
            return '[array]';
642 27
        } elseif ($val instanceof \DateTimeInterface) {
643 1
            return htmlspecialchars($val->format(\DateTime::RFC3339));
644 27
        } elseif (is_object($val) && !method_exists($val, '__toString')) {
645 1
            return '{object}';
646
        } else {
647 27
            return htmlspecialchars($val);
648
        }
649
    }
650
651
    /**
652
     * Write children blocks.
653
     *
654
     * @param array|callable|null $children The children blocks to write.
655
     */
656 4
    public function writeChildren($children) {
657 4
        if (empty($children)) {
658
            return;
659 4
        } elseif (is_array($children)) {
660 1
            array_map([$this, 'writeChildren'], $children);
661 1
        } else {
662 4
            $children();
663
        }
664 4
    }
665
666
    /**
667
     * Massage a dynamic tag name.
668
     *
669
     * @param mixed $expr The result of the tag name expression.
670
     * @param string $tag The default tag name if **$expr** is true.
671
     * @return string Returns the expected tag name.
672
     */
673 5
    protected function tagName($expr, $tag) {
674 5
        if ($expr === true) {
675 3
            return $tag;
676
        }
677 4
        return $expr;
678
    }
679
}
680