|
Server : Apache System : Linux s1230 5.15.0-139-generic #149~20.04.1 SMP Tue Jul 14 11:21:49 UTC 2026 x86_64 User : p141464 ( 418825) PHP Version : 7.4.33.12 Disable Function : NONE Directory : /html/relaunch-kmu/wp-content/plugins/real-cookie-banner/inc/templates/ |
Upload File : |
<?php
namespace DevOwl\RealCookieBanner\templates;
use DevOwl\RealCookieBanner\base\UtilsProvider;
use DevOwl\RealCookieBanner\scanner\Persist;
use DevOwl\RealCookieBanner\Vendor\DevOwl\ServiceCloudConsumer\storages\AbstractStorage;
use DevOwl\RealCookieBanner\Vendor\DevOwl\ServiceCloudConsumer\templates\AbstractTemplate;
use DevOwl\RealCookieBanner\Vendor\DevOwl\ServiceCloudConsumer\templates\BlockerTemplate;
use DevOwl\RealCookieBanner\Vendor\DevOwl\ServiceCloudConsumer\templates\ServiceTemplate;
use DevOwl\RealCookieBanner\Vendor\MatthiasWeb\Utils\ExpireOption;
// @codeCoverageIgnoreStart
\defined('ABSPATH') or die('No script kiddies please!');
// Avoid direct file request
// @codeCoverageIgnoreEnd
/**
* Helper functionality for common storage operations for a given consumer.
* @internal
*/
class StorageHelper
{
use UtilsProvider;
const TABLE_NAME = 'template';
const TRANSIENT_CACHE_KEY = RCB_OPT_PREFIX . '-%s-templates-%s';
const TRANSIENT_CACHE_EXPIRE = 24 * 60 * 60;
const TYPE_SERVICE = 'service';
const TYPE_BLOCKER = 'blocker';
private $storage;
/**
* Cache of templates as they can be time-consuming to generate.
* So, they are recalculated each x hours.
*
* @var ExpireOption
*/
private $expireOption;
/**
* C'tor.
*
* @param AbstractStorage $storage
*/
public function __construct($storage)
{
$this->storage = $storage;
}
/**
* Retrieve all templates (`field=null`) or by criteria.
*
* @param string $field
* @param mixed $value
* @param string $middlewareRoutine Can be `before` or `after`
* @param boolean|string $forceInvalidate
* @return AbstractTemplate[]|false
*/
public function retrieveBy($field = null, $value = null, $middlewareRoutine = 'after', $forceInvalidate = \false)
{
global $wpdb;
$table_name = $this->getTableName(self::TABLE_NAME);
$type = $this->getType();
$context = $this->getContext();
$middlewareRoutine = \in_array($middlewareRoutine, ['before', 'after'], \true) ? $middlewareRoutine : 'after';
// Also try to use the `identifier` field to make the database use the index
$where = [];
$where[] = "t.identifier <> ''";
$where[] = $wpdb->prepare('t.type = %s', $type);
$where[] = $wpdb->prepare('t.context = %s', $context);
$where['is_outdated'] = 't.is_outdated = 0';
if ($field === 'identifier') {
if (\is_array($value)) {
$where[] = \count($value) > 0 ? \sprintf('t.identifier IN (%s)', \join(',', \array_map(function ($identifier) use($wpdb) {
return $wpdb->prepare('%s', $identifier);
}, $value))) : '1 = 0';
} else {
$where[] = $wpdb->prepare('t.identifier = %s', $value);
}
} elseif ($field === 'scanned' && $value) {
$table_name_scan = $this->getTableName(Persist::TABLE_NAME);
$where[] = $wpdb->prepare('t.is_disabled = %d', 0);
// phpcs:disable WordPress.DB
$where[] = $wpdb->prepare("((t.type = %s AND EXISTS(SELECT 1 FROM {$table_name_scan} WHERE preset = t.identifier)) OR t.is_recommended = 1)", self::TYPE_BLOCKER);
// phpcs:enable WordPress.DB
} elseif ($field === 'recommended' && $value) {
$where[] = $wpdb->prepare('t.is_hidden = %d', 0);
$where[] = $wpdb->prepare('t.is_disabled = %d', 0);
$where[] = $wpdb->prepare('t.is_recommended = %d', $value);
} elseif ($field === 'versions' && \is_array($value) && \count($value) > 0) {
$identifier = $value[0];
$versions = \array_slice($value, 1);
unset($where['is_outdated']);
$where[] = $wpdb->prepare('t.identifier = %s', $identifier);
$where[] = \sprintf('t.version IN (%s)', \join(',', \array_map(function ($version) use($wpdb) {
return $wpdb->prepare('%d', $version);
}, $versions)));
} elseif (\is_string($field)) {
return [];
}
// phpcs:disable WordPress.DB
$result = $wpdb->get_results(\sprintf("SELECT t2.versions, %s AS response, is_invalidate_needed, t2.context\n FROM {$table_name} t\n LEFT JOIN (\n SELECT identifier, context, GROUP_CONCAT(DISTINCT version) AS versions\n FROM {$table_name}\n GROUP BY identifier, context\n ) t2 ON t.identifier = t2.identifier AND t.context = t2.context\n WHERE %s\n ORDER BY headline ASC, version DESC", $middlewareRoutine . '_middleware', \join(' AND ', $where)), ARRAY_A);
// phpcs:enable WordPress.DB
if ($result === null) {
// Error happened, do not try to refetch from data sources
return [];
}
if ($type === self::TYPE_SERVICE && $field === null && \count($result) === 0 && $middlewareRoutine === 'after' && $forceInvalidate !== 'never') {
// No results in database but there must be at least one, refetch from data source
// Force also the fetch from service cloud via API (e.g. new language in WPML added)
$this->getExpireOption()->delete();
return \false;
}
foreach ($result as &$row) {
if (isset($where['is_outdated']) && $middlewareRoutine === 'after' && \intval($row['is_invalidate_needed']) > 0 && $forceInvalidate !== 'never') {
// So, in this state we know that we should recalculate middlewares.
return \false;
}
$versions = \array_filter(\array_map('intval', \explode(',', $row['versions'])));
$context = $row['context'];
$row = $this->fromArray(\json_decode($row['response'], ARRAY_A));
$row->consumerData['context'] = $context;
if ($field !== 'versions') {
$row->consumerData['versions'] = $versions;
}
}
return $result;
}
/**
* Convert JSON to `ServiceTemplate` or `BlockerTemplate` depending on current type.
*
* @param array $array
*/
public function fromArray($array)
{
$consumer = $this->storage->getConsumer();
$row = $this->getType() === self::TYPE_BLOCKER ? new BlockerTemplate($consumer) : new ServiceTemplate($consumer);
$row->fromArray($array);
return $row;
}
/**
* Persist a set of templates in chunks into the database.
*
* @param AbstractTemplate[] $templates
* @param closre $otherMetaKeysResolver
*/
public function persist($templates, $otherMetaKeysResolver = null)
{
global $wpdb;
/**
* Chunks.
*
* @var AbstractTemplate[][]
*/
$chunks = \array_chunk($templates, \DevOwl\RealCookieBanner\templates\TemplateConsumers::PERSIST_CHUNK_SIZE_TEMPLATES);
$table_name = $this->getTableName(self::TABLE_NAME);
$inserted = 0;
$context = $this->getContext();
$type = $this->getType();
$persistedIdentifierInSql = [];
foreach ($chunks as $chunk) {
$values = [];
$templateToVersionMap = [];
foreach ($chunk as $template) {
$otherMeta = \is_callable($otherMetaKeysResolver) ? $otherMetaKeysResolver($template) : [];
$values[] = \str_ireplace("'NULL'", 'NULL', $wpdb->prepare('(%s, %s, %s, %d, %s,
%s, %s, %s,
%d, %d, %d, %d, %s, %d, %d, %d,
%s, %s, %s, %s, %s, %s)', $template->identifier, $context, $type, $template->version, \mysql2date('c', \gmdate('Y-m-d H:i:s', $template->createdAt), \false), $template->headline ?? '', $template->subHeadline ?? '', $template->logoUrl ?? '', 0, $template->consumerData['isDisabled'] ? 1 : 0, 0, $template->consumerData['isUntranslated'] ?? \false, $template->machineTranslationStatus ?? 'no-translation', $template->isHidden ? 1 : 0, $template->consumerData['isRecommended'] ? 1 : 0, isset($template->consumerData['isCloud']) && $template->consumerData['isCloud'] ? 1 : 0, $template->tier, \count($template->consumerData['tags']) > 0 ? \json_encode($template->consumerData['tags']) : \json_encode((object) []), \json_encode(AbstractTemplate::toArray($template->getBeforeMiddleware())), \json_encode(AbstractTemplate::toArray($template)), \count($otherMeta) > 0 ? \json_encode($otherMeta) : \json_encode((object) []), \count($template->successorOfIdentifierInfo) > 0 ? \json_encode($template->successorOfIdentifierInfo) : 'NULL'));
$persistedIdentifierInSql[] = $wpdb->prepare('%s', $template->identifier);
$templateToVersionMap[$template->identifier] = $template->version;
}
// phpcs:disable WordPress.DB.PreparedSQL
$result = $wpdb->query(\sprintf('INSERT INTO %s (
`identifier`, `context`, `type`, `version`, `created_at`,
`headline`, `sub_headline`, `logo_url`,
`is_outdated`, `is_disabled`, `is_invalidate_needed`, `is_untranslated`, `machine_translation_status`, `is_hidden`, `is_recommended`, `is_cloud`,
`tier`, `tags`, `before_middleware`, `after_middleware`, `other_meta`, `successor_of_identifiers`
)
VALUES %s ON DUPLICATE KEY UPDATE
`version` = VALUES(`version`),
`created_at` = VALUES(`created_at`),
`headline` = VALUES(`headline`),
`sub_headline` = VALUES(`sub_headline`),
`logo_url` = VALUES(`logo_url`),
`is_outdated` = VALUES(`is_outdated`),
`is_disabled` = VALUES(`is_disabled`),
`is_invalidate_needed` = VALUES(`is_invalidate_needed`),
`is_untranslated` = VALUES(`is_untranslated`),
`machine_translation_status` = VALUES(`machine_translation_status`),
`is_hidden` = VALUES(`is_hidden`),
`is_recommended` = VALUES(`is_recommended`),
`is_cloud` = VALUES(`is_cloud`),
`tier` = VALUES(`tier`),
`tags` = VALUES(`tags`),
`before_middleware` = VALUES(`before_middleware`),
`after_middleware` = VALUES(`after_middleware`),
`other_meta` = VALUES(`other_meta`),
`successor_of_identifiers` = VALUES(`successor_of_identifiers`)', $table_name, \join(',', $values)));
// phpcs:enable WordPress.DB.PreparedSQL
// When $result is zero, the query did not fail but no new row where added, we need to respect `ON DUPLICATE KEY UPDATE`
$inserted += $result === \false ? 0 : \count($values);
// Create a SQL statement which removes prerelease versions when we are no longer allowed to download them as this could lead to problems with `is_outdated`
$sql = [];
foreach ($templateToVersionMap as $identifier => $version) {
$sql[] = $wpdb->prepare('(identifier = %s AND version > %d)', $identifier, $version);
}
if (\count($sql) > 0) {
$sql = \sprintf("DELETE FROM {$table_name} WHERE %s AND (%s)", $wpdb->prepare('context = %s AND type = %s', $context, $type), \join(' OR ', $sql));
// phpcs:disable WordPress.DB.PreparedSQL
$wpdb->query($sql);
// phpcs:enable WordPress.DB.PreparedSQL
}
}
$this->updateOutdated();
// Delete no-longer known templates
if (\count($persistedIdentifierInSql) > 0) {
// phpcs:disable WordPress.DB.PreparedSQL
$wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE context = %s AND type = %s AND identifier NOT IN (" . \join(',', $persistedIdentifierInSql) . ')', $context, $type));
// phpcs:enable WordPress.DB.PreparedSQL
}
return $inserted;
}
/**
* Update all `is_outdated` for all known services.
*/
public function updateOutdated()
{
global $wpdb;
$table_name = $this->getTableName(self::TABLE_NAME);
// phpcs:disable WordPress.DB.PreparedSQL
$wpdb->query("UPDATE {$table_name} t1\n INNER JOIN {$table_name} t2\n ON t1.identifier = t2.identifier AND t1.context = t2.context AND t1.type = t2.type\n SET t1.is_outdated = 1\n WHERE t1.version < t2.version AND t1.is_outdated = 0");
// phpcs:enable WordPress.DB.PreparedSQL
}
// Documented in AbstractStorage
public function shouldInvalidate()
{
$option = $this->getExpireOption();
$cache = $option->get();
$result = $cache !== $this->getCacheInvalidateKey() && $option->set($this->getCacheInvalidateKey());
return $result;
}
/**
* Get type as string from template type class.
*/
public function getType()
{
$typeClass = $this->storage->getConsumer()->getTypeClass();
return $typeClass === ServiceTemplate::class ? self::TYPE_SERVICE : self::TYPE_BLOCKER;
}
/**
* Get the `ExpireOption` instance.
*/
public function getExpireOption()
{
return $this->expireOption === null ? $this->expireOption = new ExpireOption(\sprintf(self::TRANSIENT_CACHE_KEY, $this->getType(), $this->getContext()), \false, self::TRANSIENT_CACHE_EXPIRE) : $this->expireOption;
}
/**
* Automatically invalidate storage when Real Cookie Banner verion changes.
*/
public function getCacheInvalidateKey()
{
return $this->storage->getConsumer()->getVariableResolver()->resolveRequired('cache.invalidate.key');
}
/**
* Get context from variable resolver.
*
* @return string
*/
public function getContext()
{
return $this->storage->getConsumer()->getVariableResolver()->resolveRequired('context');
}
}