VvebOIDC/system/component/component.php

537 lines
16 KiB
PHP

<?php
/**
* Vvveb
*
* Copyright (C) 2022 Ziadin Givan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Vvveb\System\Component;
use function Vvveb\dashesToCamelCase;
use function Vvveb\removeJsonComments;
use Vvveb\System\Cache;
use Vvveb\System\Core\View;
if (! defined('COMPONENT_CACHE_FLAG_LOCK')) {
define('COMPONENT_CACHE_FLAG_LOCK', PHP_INT_MIN + 1);
define('COMPONENT_CACHE_FLAG_REGENERATE', PHP_INT_MIN + 2);
//time
define('COMPONENT_CACHE_EXPIRE_DELAY', 5); //real expiration time +5 seconds
define('COMPONENT_CACHE_WAIT', 1); //wait for cache generation
define('COMPONENT_CACHE_MAX_WAIT_RETRY', 3); //wait for cache generation
define('COMPONENT_CACHE_LOCK_EXPIRE', 20); //lock can not be set more than COMPONENT_CACHE_LOCK_EXPIRE seconds
define('COMPONENT_CACHE_EXPIRE', 20);
}
class Component {
static private $instance;
private $queue;
private $components = [];
private $componentsFile;
private $loaded;
private $content;
private $view;
private $app;
private $documentType = 'html';
private $cache = true;
static function getInstance($view = false, $regenerate = false, $content = false, $app = null) {
if (self :: $instance === NULL) {
if (! $view) {
$view = View::getInstance();
}
self :: $instance = new self($view, $regenerate, $content);
}
return self :: $instance;
}
function __construct($view, $regenerate = false, $content = false, $app = null) {
if ($this->loaded) {
return true;
}
$this->app = $app ?? APP;
if (! $view) {
$view = View::getInstance();
}
$this->view = $view;
$this->componentsFile = $view->serviceTemplate() . '.component';
$this->content = $content;
if ((! file_exists($this->componentsFile)) || $regenerate) {
$this->generateRequiredComponents();
$this->saveTemplateComponents();
$this->loadComponents();
$this->loaded = true;
} else {
}
if ($this->loaded) {
return true;
}
$this->loadTemplateComponents();
$this->loadComponents();
$this->loaded = true;
}
function isComponent($name) {
return isset($this->components[$name]);
}
function getComponent($name) {
return $this->components[$name] ?? [];
}
function loadComponents() {
$view = $this->view;
$cache = [];
$objs = [];
$namespace = 'component';
$notFound404 = false;
if (is_array($this->components)) {
foreach ($this->components as $component => $instances) {
if (strpos($component, 'plugin') === 0) {
$template = str_replace('plugin', '', $component);
$p = strrpos($template, '-');
$pluginName = substr($template, 1, $p - 1);
$nameSpace = substr($template, $p + 1);
$app = '';
$pluginClassName = dashesToCamelCase($pluginName);
$class = "Vvveb\Plugins\\$pluginClassName\Component\\$nameSpace";
$file = DIR_PLUGINS . "$pluginName/component/" . str_replace('-', '/', $nameSpace) . '.php';
} else {
$class = '\Vvveb\Component\\' . str_replace('-', '\\', $component);
$file = DIR_ROOT . $this->app . DS . 'component' . DS . str_replace('-', '/', $component) . '.php';
}
$component = str_replace('-', '_', $component);
if (file_exists($file)) {
include_once $file;
foreach ($instances as $instance => $options) {
$obj = new $class($options);
if ($obj->cacheExpire && $this->cache && ($cacheKey = $obj->cacheKey())) {
$cacheExpireKey = $cacheKey . '_expire';
$cache[] = $cacheKey;
$cache[] = $cacheExpireKey;
$obj->component = $component;
$objs[$obj->cacheKey][$instance] = $obj;
} else {
$results = $obj->results();
if ($results !== false && $results !== null) {
$results['_instance'] = $obj;
}
$comp = &$view->_component[$component];
$comp[$instance] = $results;
if (isset($results['404']) && $results['404'] == true) {
$notFound404 = true;
}
}
}
}
}
if ($this->cache) {
//get cached data
$cacheDriver = Cache::getInstance();
//$cacheDriver = new Memcached();
$null = [];
$data = $cacheDriver->getMulti($namespace, $cache, SITE_ID) ?? [];
foreach ($objs as $cacheKey => $instances) {
foreach ($instances as $index => $instance) {
if (! isset($data[$cacheKey]) || ! is_array($data[$cacheKey])) {
$data[$cacheKey] = [];
}
$component = $instance->component;
$component = str_replace('-', '_', $component);
$data[$cacheKey]['_instance'] = $instance;
$comp = &$view->_component[$component];
$comp[$index] = $data[$cacheKey];
if (isset($comp[$index]['404']) && $comp[$index]['404'] == true) {
$notFound404 = true;
}
}
//cache hit, remove from sql regeneration queue
$cacheExpireKey = $cacheKey . '_expire';
//if no lock set (! -1) and cache is expiring then set lock and set for regeneration
if (! isset($data[$cacheExpireKey]) || ! isset($data[$cacheKey]) ||
($data[$cacheExpireKey] && ($data[$cacheExpireKey] > 0) &&
($data[$cacheExpireKey] + COMPONENT_CACHE_EXPIRE_DELAY) < $_SERVER['REQUEST_TIME'])) {
$cacheDriver->set($namespace, $cacheExpireKey, COMPONENT_CACHE_FLAG_LOCK, COMPONENT_CACHE_LOCK_EXPIRE); //set lock
$data[$cacheExpireKey] = COMPONENT_CACHE_FLAG_REGENERATE; //set regeneration flag
}
if ($data[$cacheExpireKey] > 0) {
unset($objs[$cacheKey], $cache[$cacheKey], $cache[$cacheExpireKey], $data[$cacheKey], $data[$cacheExpireKey]);
}
}
$wait = true;
$retry = 0;
//run sql queries for uncached components
$saveCache = [];
while ($wait && $retry < COMPONENT_CACHE_MAX_WAIT_RETRY) {
$wait = false;
$retry++;
foreach ($objs as $key => $objects) {
$cacheExpireKey = $key . '_expire';
//check lock
if ((! isset($data[$cacheExpireKey]) || ! ($data[$cacheExpireKey])) ||
! isset($data[$key]) ||
$data[$cacheExpireKey] == COMPONENT_CACHE_FLAG_REGENERATE) {
//lock set by this script, regenerate content
$id = key($objects);
$results = $objects[$id]->results();
if ($results !== null) {
$saveCache[$key] = $results;
$saveCache[$cacheExpireKey] = $_SERVER['REQUEST_TIME'] + $objects[$id]->cacheExpire;
}
if (isset($results['404']) && $results['404'] == true) {
$notFound404 = true;
}
$data[$cacheExpireKey] = 0;
foreach ($objects as $index => $instance) {
$results['_instance'] = $instance;
$component = $instance->component;
$component = str_replace('-', '_', $component);
$comp = &$view->_component[$component];
$comp[$index] = $results;
}
} else {
if ($data[$cacheExpireKey] == COMPONENT_CACHE_FLAG_LOCK) {
//error_log("wait for $cacheExpireKey");
//item is locked, some other script is generating content
$wait = true;
}
}
}
if ($wait) {
error_log('wait cache ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . print_r($cache,1));
//get
@sleep(COMPONENT_CACHE_WAIT);
$data = $cacheDriver->getMulti($namespace, $cache);
}
}
if ($retry >= COMPONENT_CACHE_MAX_WAIT_RETRY) {
error_log('error:CACHE max retry reached for ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
}
}
if (! empty($saveCache)) {
$cacheDriver->setMulti($namespace, $saveCache, /*$_SERVER['REQUEST_TIME'] + */COMPONENT_CACHE_EXPIRE * 3600, SITE_ID);
}
}
//call request for each component
foreach ($this->components as $component => $instances) {
foreach ($instances as $index => $options) {
$component = str_replace('-', '_', $component);
if (isset($view->_component[$component])) {
$comp = &$view->_component[$component];
$results = &$comp[$index];
$object = $results['_instance'] ?? false;
if ($object && method_exists($object, 'request')) {
$object->request($results, $index);
}
}
}
}
unset($data, $cache);
$this->components = NULL;
if ($notFound404) {
\Vvveb\System\Core\FrontController::notFound(false);
}
}
/* static function isLoaded($component)
{
return isset($this->queue[$component]);
}
*/
function results() {
}
function generateRequiredComponents() {
//get components from html page
$document = new \DomDocument();
$document->preserveWhiteSpace = false;
$document->recover = true;
$document->strictErrorChecking = false;
$document->formatOutput = false;
$document->resolveExternals = false;
$document->validateOnParse = false;
$document->xmlStandalone = true;
$view = view::getInstance();
libxml_use_internal_errors(true);
$this->documentType = $view->getDocumentType();
if ($this->content) {
if ($this->documentType == 'html') {
@$document->loadHTML($this->content);
} else {
@$document->loadXML($this->content);
}
} else {
$view = $this->view;
$template = $view->template();
$extension = strtolower(trim(substr($template, -4), '.'));
$this->documentType = $extension;
if ($template[0] == '/') {
} else {
if (strpos($template, 'plugins/') === 0) {
$template = str_replace('plugins/', '', $template);
$p = strpos($template, '/');
$pluginName = substr($template, 0, $p);
$nameSpace = substr($template, $p + 1);
$app = '';
if ($this->app != 'app') {
$app = $this->app . DS;
}
$template = DIR_PLUGINS . $pluginName . DS . 'public' . DS . $app . $nameSpace;
} else {
$template = $view->getTemplatePath() . $template;
}
}
if ($this->documentType == 'html') {
@$document->loadHTMLFile($template,
LIBXML_NOWARNING | LIBXML_NOERROR);
} else {
$content = file_get_contents($template);
if ($this->documentType == 'json') {
//remove json comments from line start
$content = removeJsonComments($content);
$json = json_decode($content, true);
$json = \Vvveb\prepareJson($json);
$xml = \Vvveb\array2xml($json);
@$document->loadXML($xml);
} else {
@$document->loadXML($content,
LIBXML_NOWARNING | LIBXML_NOERROR);
}
}
}
$xpath = new \DOMXpath($document);
$i = 0;
//include froms in case any component_ is included
while (($elements = $xpath->query('//*[ @data-v-copy-from or @data-v-save-global ]')) && $elements->length && $i++ < 2) {
$fromDocument = new \DomDocument();
$fromDocument->preserveWhiteSpace = false;
$fromDocument->recover = true;
$fromDocument->strictErrorChecking = false;
$fromDocument->formatOutput = false;
$fromDocument->resolveExternals = false;
$fromDocument->validateOnParse = false;
$fromDocument->xmlStandalone = true;
foreach ($elements as $element) {
$attribute = $element->getAttribute('data-v-copy-from') ?: $element->getAttribute('data-v-save-global');
$element->removeAttribute('data-v-copy-from');
$element->removeAttribute('data-v-save-global');
if (preg_match('/([^\,]+)\,([^$,]+)/', $attribute , $from)) {
$file = html_entity_decode(trim($from[1]));
$selector = html_entity_decode(trim($from[2]));
if ($this->documentType == 'html') {
$fromDocument->loadHTMLFile($view->getTemplatePath() . $file);
} else {
$fromDocument->loadXML($view->getTemplatePath() . $file);
}
$fromXpath = new \DOMXpath($fromDocument);
$fromElements = $fromXpath->query(\Vvveb\cssToXpath($selector));
$count = 0;
$parent = $element->parentNode;
foreach ($fromElements as $externalNode) {
$importedNode = $document->importNode($externalNode, true);
if ($parent) {
if ($count) {
$parent->appendChild($importedNode);
} else {
$parent->replaceChild($importedNode, $element);
}
$element = $importedNode;
//$parent = $element->parentNode;
$count++;
}
}
}
}
}
//search for elements that have an attribute starting with data-v-component-
$elements = $xpath->query('//*[@*[starts-with(name(), "data-v-component-")]]');
foreach ($elements as $element) {
$component = '';
$opts = [];
foreach ($element->attributes as $attr) {
$nodeName = $attr->nodeName;
if (strpos($nodeName, 'data-v-component-') === 0) {
$component = str_replace('data-v-component-', '', $nodeName);
//$classes = explode(' ', trim($attr->nodeValue));
} else {
if (strpos($nodeName, 'data-v-') === 0) {
$option = str_replace('data-v-', '', $nodeName);
$opts[$option] = $attr->nodeValue;
}
}
}
//get all classes
//search for options
$options = null;
//validate options
$validOptions = [];
if (strpos($component, 'plugin') === 0) {
$template = str_replace('plugin', '', $component);
$p = strrpos($template, '-');
$pluginName = substr($template, 1, $p - 1);
$nameSpace = substr($template, $p + 1);
$app = '';
if ($this->app != 'app') {
$app = $this->app . DS;
}
$pluginClassName = dashesToCamelCase($pluginName);
$componentClass = "Vvveb\Plugins\\$pluginClassName\Component\\$nameSpace";
$file = DIR_PLUGINS . "$pluginName/component/" . str_replace('-', '/', $nameSpace) . '.php';
} else {
$componentClass = '\Vvveb\Component\\' . ucfirst(str_replace('-', '\\', $component));
$file = DIR_ROOT . $this->app . DS . 'component/' . str_replace('-', '/', $component) . '.php';
}
if (file_exists($file)) {
include_once $file;
//$componentClass = new $componentClass;
//do not add design only components
if (isset($componentClass::$designOnly) && $componentClass::$designOnly == true) {
continue;
}
$validOptions = array_keys($componentClass::$defaultOptions);
} else {
if (defined('DEBUG') && \DEBUG) {
error_log("Component does not exist $componentClass => $file");
//die("Component does not exist $componentClass => $file");
}
continue;
}
//save options
foreach ($opts as $name => $option) {
if (in_array($name, $validOptions) && isset($option) !== false) {
if ((isset($option[0]) && ($option[0] == '{' || $option[0] == '[')) || (strpos($option, ',') !== false)) {
$options[$name] = json_decode($option, 1);
} else {
$options[$name] = $option;
}
}
}
$options['_hash'] = md5($component . serialize($options));
$components[$component][] = $options;
}
if (isset($components)) {
$this->components = $components;
}
//get fields for component
//load components and feed fields
}
function saveTemplateComponents() {
$php = var_export($this->components, true);
$php = preg_replace('/\s+/', ' ', $php);
//repeating end lines
$php = preg_replace('/\n+/', '', $php);
return file_put_contents($this->componentsFile, '<?php $components=' . $php . ';');
}
function loadTemplateComponents() {
include_once $this->componentsFile;
if (isset($components)) {
$this->components = $components;
}
//keep only requested service
if (isset($_GET['component_ajax'])) {
}
return true;
}
}