2017-02-14 13:05:26 +00:00
|
|
|
|
<?php
|
2020-12-21 10:40:19 +00:00
|
|
|
|
/**
|
|
|
|
|
* This file is part of the ForkBB <https://github.com/forkbb>.
|
|
|
|
|
*
|
|
|
|
|
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
|
|
|
|
|
* @license The MIT License (MIT)
|
|
|
|
|
*/
|
2023-09-16 11:00:45 +00:00
|
|
|
|
/**
|
|
|
|
|
* based on Dirk <https://github.com/artoodetoo/dirk>
|
|
|
|
|
*
|
|
|
|
|
* @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
|
|
|
|
|
* @license The MIT License (MIT)
|
|
|
|
|
*/
|
2017-02-14 13:05:26 +00:00
|
|
|
|
|
2020-10-14 13:01:43 +00:00
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2017-02-14 13:05:26 +00:00
|
|
|
|
namespace ForkBB\Core;
|
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
use ForkBB\Core\View\Compiler;
|
2017-11-03 13:06:22 +00:00
|
|
|
|
use ForkBB\Models\Page;
|
2017-02-14 13:05:26 +00:00
|
|
|
|
use RuntimeException;
|
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
class View
|
2017-02-14 13:05:26 +00:00
|
|
|
|
{
|
2023-09-16 11:00:45 +00:00
|
|
|
|
protected string $ext = '.forkbb.php';
|
2023-10-10 16:29:55 +00:00
|
|
|
|
protected string $preFile = '';
|
2017-02-14 13:05:26 +00:00
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
protected ?Compiler $compilerObj;
|
|
|
|
|
protected string $compilerClass = Compiler::class;
|
2020-08-24 11:31:46 +00:00
|
|
|
|
|
2023-10-04 12:28:30 +00:00
|
|
|
|
protected string $cacheDir;
|
2023-09-16 11:00:45 +00:00
|
|
|
|
protected string $defaultDir;
|
|
|
|
|
protected string $defaultHash;
|
2017-12-14 12:16:09 +00:00
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
protected array $other = [];
|
|
|
|
|
protected array $composers = [];
|
|
|
|
|
protected array $blocks = [];
|
|
|
|
|
protected array $blockStack = [];
|
|
|
|
|
protected array $templates = [];
|
2020-10-15 13:02:24 +00:00
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
public function __construct(string|array $config, mixed $views)
|
|
|
|
|
{
|
|
|
|
|
if (\is_array($config)) {
|
2023-10-04 12:28:30 +00:00
|
|
|
|
$this->cacheDir = $config['cache'];
|
2023-09-16 11:00:45 +00:00
|
|
|
|
$this->defaultDir = $config['defaultDir'];
|
2017-12-14 12:16:09 +00:00
|
|
|
|
|
2023-09-20 14:01:08 +00:00
|
|
|
|
if (! empty($config['userDir'])) {
|
2023-10-04 12:28:30 +00:00
|
|
|
|
$this->addTplDir($config['userDir'], 10);
|
2023-09-20 14:01:08 +00:00
|
|
|
|
}
|
2017-12-14 12:16:09 +00:00
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
if (! empty($config['composers'])) {
|
|
|
|
|
foreach ($config['composers'] as $name => $composer) {
|
|
|
|
|
$this->composer($name, $composer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! empty($config['compiler'])) {
|
|
|
|
|
$this->compilerClass = $config['compiler'];
|
|
|
|
|
}
|
2023-10-10 16:29:55 +00:00
|
|
|
|
|
|
|
|
|
if (! empty($config['preFile'])) {
|
|
|
|
|
$this->preFile = $config['preFile'];
|
|
|
|
|
}
|
2023-09-16 11:00:45 +00:00
|
|
|
|
} else {
|
|
|
|
|
// для rev. 68 и ниже
|
2023-10-04 12:28:30 +00:00
|
|
|
|
$this->cacheDir = $config;
|
|
|
|
|
$this->defaultDir = $views;
|
2017-02-14 13:05:26 +00:00
|
|
|
|
}
|
2020-07-03 08:28:10 +00:00
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
$this->defaultHash = \hash('md5', $this->defaultDir);
|
2017-02-14 13:05:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-04 12:28:30 +00:00
|
|
|
|
/**
|
|
|
|
|
* Добавляет новый каталог шаблонов $pathToDir.
|
|
|
|
|
* Сортирует список каталогов в соответствии с приоритетом $priority. По убыванию.
|
|
|
|
|
*/
|
|
|
|
|
public function addTplDir(string $pathToDir, int $priority): View
|
|
|
|
|
{
|
|
|
|
|
$this->other[\hash('md5', $pathToDir)] = [$pathToDir, $priority];
|
|
|
|
|
|
|
|
|
|
if (\count($this->other) > 1) {
|
|
|
|
|
\uasort($this->other, function (array $a, array $b) {
|
|
|
|
|
return $b[1] <=> $a[1];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 13:06:22 +00:00
|
|
|
|
/**
|
2023-09-16 11:00:45 +00:00
|
|
|
|
* Возвращает отображение страницы $p или null
|
2017-11-03 13:06:22 +00:00
|
|
|
|
*/
|
2020-05-24 11:30:48 +00:00
|
|
|
|
public function rendering(Page $p): ?string
|
2017-02-14 13:05:26 +00:00
|
|
|
|
{
|
2017-11-03 13:06:22 +00:00
|
|
|
|
if (null === $p->nameTpl) {
|
2023-06-16 18:21:25 +00:00
|
|
|
|
$this->sendHttpHeaders($p);
|
|
|
|
|
|
2017-02-14 13:05:26 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 13:06:22 +00:00
|
|
|
|
$p->prepare();
|
|
|
|
|
|
|
|
|
|
$this->templates[] = $p->nameTpl;
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
2018-03-04 05:30:07 +00:00
|
|
|
|
while ($_name = \array_shift($this->templates)) {
|
2017-11-03 13:06:22 +00:00
|
|
|
|
$this->beginBlock('content');
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
2017-11-03 13:06:22 +00:00
|
|
|
|
foreach ($this->composers as $_cname => $_cdata) {
|
2018-03-04 05:30:07 +00:00
|
|
|
|
if (\preg_match($_cname, $_name)) {
|
2017-11-03 13:06:22 +00:00
|
|
|
|
foreach ($_cdata as $_citem) {
|
2018-03-04 05:30:07 +00:00
|
|
|
|
\extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
|
2017-11-03 13:06:22 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
|
|
|
|
require $this->prepare($_name);
|
|
|
|
|
|
2017-11-03 13:06:22 +00:00
|
|
|
|
$this->endBlock(true);
|
|
|
|
|
}
|
2020-07-03 08:28:10 +00:00
|
|
|
|
|
2023-06-16 18:21:25 +00:00
|
|
|
|
$this->sendHttpHeaders($p);
|
|
|
|
|
|
2017-11-03 13:06:22 +00:00
|
|
|
|
return $this->block('content');
|
2017-02-14 13:05:26 +00:00
|
|
|
|
}
|
2023-06-16 18:21:25 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Отправляет HTTP заголовки
|
|
|
|
|
*/
|
|
|
|
|
protected function sendHttpHeaders(Page $p): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($p->httpHeaders as $catHeader) {
|
|
|
|
|
foreach ($catHeader as $header) {
|
|
|
|
|
\header($header[0], $header[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Возвращает отображение шаблона $name
|
|
|
|
|
*/
|
|
|
|
|
public function fetch(string $name, array $data = []): string
|
|
|
|
|
{
|
|
|
|
|
$this->templates[] = $name;
|
|
|
|
|
|
|
|
|
|
if (! empty($data)) {
|
|
|
|
|
\extract($data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while ($_name = \array_shift($this->templates)) {
|
|
|
|
|
$this->beginBlock('content');
|
|
|
|
|
|
|
|
|
|
foreach ($this->composers as $_cname => $_cdata) {
|
|
|
|
|
if (\preg_match($_cname, $_name)) {
|
|
|
|
|
foreach ($_cdata as $_citem) {
|
|
|
|
|
\extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require $this->prepare($_name);
|
|
|
|
|
|
|
|
|
|
$this->endBlock(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->block('content');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add view composer
|
|
|
|
|
* @param mixed $name template name or array of names
|
|
|
|
|
* @param mixed $composer data in the same meaning as for fetch() call, or callable returning such data
|
|
|
|
|
*/
|
|
|
|
|
public function composer(string|array $name, mixed $composer): void
|
|
|
|
|
{
|
|
|
|
|
if (\is_array($name)) {
|
|
|
|
|
foreach ($name as $n) {
|
|
|
|
|
$this->composer($n, $composer);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$p = '~^'
|
|
|
|
|
. \str_replace('\*', '[^' . $this->separator . ']+', \preg_quote($name, $this->separator . '~'))
|
|
|
|
|
. '$~';
|
|
|
|
|
$this->composers[$p][] = $composer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Подготавливает файл для подключения
|
|
|
|
|
*/
|
|
|
|
|
protected function prepare(string $name): string
|
|
|
|
|
{
|
|
|
|
|
$st = \preg_replace('%\W%', '-', $name);
|
|
|
|
|
|
|
|
|
|
foreach ($this->other as $hash => $cur) {
|
2023-09-20 13:51:21 +00:00
|
|
|
|
if (\file_exists($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
|
2023-10-04 12:28:30 +00:00
|
|
|
|
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
! \file_exists($php)
|
|
|
|
|
|| \filemtime($tpl) > \filemtime($php)
|
|
|
|
|
) {
|
2023-10-10 16:29:55 +00:00
|
|
|
|
$this->create($php, $tpl, $name);
|
2023-09-16 11:00:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $php;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$hash = $this->defaultHash;
|
|
|
|
|
$tpl = "{$this->defaultDir}/{$name}{$this->ext}";
|
2023-10-04 12:28:30 +00:00
|
|
|
|
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
! \file_exists($php)
|
|
|
|
|
|| \filemtime($tpl) > \filemtime($php)
|
|
|
|
|
) {
|
2023-10-10 16:29:55 +00:00
|
|
|
|
$this->create($php, $tpl, $name);
|
2023-09-16 11:00:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $php;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-10 09:32:59 +00:00
|
|
|
|
/**
|
|
|
|
|
* Удаляет файлы кэша для шаблона $name
|
|
|
|
|
*/
|
|
|
|
|
public function delete(string $name): void
|
|
|
|
|
{
|
|
|
|
|
$st = \preg_replace('%\W%', '-', $name);
|
|
|
|
|
|
|
|
|
|
\array_map('\\unlink', \glob("{$this->cacheDir}/_{$st}-*.php"));
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 11:00:45 +00:00
|
|
|
|
/**
|
|
|
|
|
* Генерирует $php файл на основе шаблона $tpl
|
|
|
|
|
*/
|
2023-10-10 16:29:55 +00:00
|
|
|
|
protected function create(string $php, string $tpl, string $name): void
|
2023-09-16 11:00:45 +00:00
|
|
|
|
{
|
|
|
|
|
if (empty($this->compilerObj)) {
|
2023-10-10 16:29:55 +00:00
|
|
|
|
$this->compilerObj = new $this->compilerClass($this->preFile);
|
2023-09-16 11:00:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-10 16:29:55 +00:00
|
|
|
|
$text = $this->compilerObj->create($name, \file_get_contents($tpl), \hash('fnv1a32', $tpl));
|
2023-09-16 11:00:45 +00:00
|
|
|
|
|
|
|
|
|
if (false === \file_put_contents($php, $text, \LOCK_EX)) {
|
|
|
|
|
throw new RuntimeException("Failed to write {$php} file");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (\function_exists('\\opcache_invalidate')) {
|
|
|
|
|
\opcache_invalidate($php, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Задает родительский шаблон
|
|
|
|
|
*/
|
|
|
|
|
protected function extend(string $name): void
|
|
|
|
|
{
|
|
|
|
|
$this->templates[] = $name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Возвращает содержимое блока или $default
|
|
|
|
|
*/
|
|
|
|
|
protected function block(string $name, string $default = ''): string
|
|
|
|
|
{
|
|
|
|
|
return \array_key_exists($name, $this->blocks)
|
|
|
|
|
? $this->blocks[$name]
|
|
|
|
|
: $default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Задает начало блока
|
|
|
|
|
*/
|
|
|
|
|
protected function beginBlock(string $name): void
|
|
|
|
|
{
|
|
|
|
|
$this->blockStack[] = $name;
|
|
|
|
|
|
|
|
|
|
\ob_start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Задает конец блока
|
|
|
|
|
*/
|
|
|
|
|
protected function endBlock(bool $overwrite = false): string
|
|
|
|
|
{
|
|
|
|
|
$name = \array_pop($this->blockStack);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
$overwrite
|
|
|
|
|
|| ! \array_key_exists($name, $this->blocks)
|
|
|
|
|
) {
|
|
|
|
|
$this->blocks[$name] = \ob_get_clean();
|
|
|
|
|
} else {
|
|
|
|
|
$this->blocks[$name] .= \ob_get_clean();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $name;
|
|
|
|
|
}
|
2017-02-14 13:05:26 +00:00
|
|
|
|
}
|