Your IP : 18.225.56.80
<?php
namespace Bitrix\Landing;
use Bitrix\Landing\Hook\Page;
use \Bitrix\Landing\Internals\HookDataTable as HookData;
use \Bitrix\Main\Event;
use \Bitrix\Main\EventResult;
class Hook
{
/**
* If true, hook work in edit mode (form settings).
* @var boolean
*/
protected static $editMode = false;
/**
* Entity type site.
*/
const ENTITY_TYPE_SITE = 'S';
/**
* Entity type landing.
*/
const ENTITY_TYPE_LANDING = 'L';
/**
* Dir of repository of common hooks.
*/
const HOOKS_PAGE_DIR = '/bitrix/modules/landing/lib/hook/page';
/**
* Namespace of repoitory of common hooks (relative current).
*/
const HOOKS_NAMESPACE = '\\Hook\\Page\\';
/**
* Handler for copy event
*/
protected const HOOKS_ON_COPY_HANDLER = 'onCopy';
/**
* Hook codes which contains file ids.
*/
const HOOKS_CODES_FILES = [
'METAOG_IMAGE',
'BACKGROUND_PICTURE'
];
/**
* Hook codes which have visual effect
*/
const HOOKS_CODES_DESIGN = [
'BACKGROUND_USE',
'BACKGROUND_PICTURE',
'BACKGROUND_POSITION',
'BACKGROUND_COLOR',
'FONTS_CODE',
'THEME_CODE',
'THEME_USE',
'THEME_COLOR',
'THEMEFONTS_USE',
'THEMEFONTS_CODE_H',
'THEMEFONTS_CODE',
'THEMEFONTS_SIZE',
'THEMEFONTS_COLOR',
'THEMEFONTS_COLOR_H',
'THEMEFONTS_LINE_HEIGHT',
'THEMEFONTS_FONT_WEIGHT',
'THEMEFONTS_FONT_WEIGHT_H',
];
/**
* Get classes from dir.
* @param string $dir Relative dir.
* @return array
*/
protected static function getClassesFromDir($dir)
{
$classes = array();
$path = Manager::getDocRoot() . $dir;
if (($handle = opendir($path)))
{
while ((($entry = readdir($handle)) !== false))
{
if ($entry != '.' && $entry != '..')
{
$classes[] = mb_strtoupper(pathinfo($entry, PATHINFO_FILENAME));
}
}
}
return $classes;
}
/**
* Get data by entity id ant type.
* @param int $id Entity id.
* @param string $type Entity type.
* @param boolean $asIs Return row as is.
* @return array
*/
public static function getData($id, $type, $asIs = false): array
{
$data = [];
$id = (int)$id;
if (!is_string($type))
{
return $data;
}
$res = HookData::getList([
'select' => [
'ID', 'HOOK', 'CODE', 'VALUE'
],
'filter' => [
'ENTITY_ID' => $id,
'=ENTITY_TYPE' => $type,
'=PUBLIC' => self::$editMode ? 'N' : 'Y'
],
'order' => [
'ID' => 'asc'
]
]);
while ($row = $res->fetch())
{
if (!isset($data[$row['HOOK']]))
{
$data[$row['HOOK']] = [];
}
if (mb_strpos($row['VALUE'], 'serialized#') === 0)
{
$row['VALUE'] = unserialize(mb_substr($row['VALUE'], 11), ['allowed_classes' => false]);
}
$data[$row['HOOK']][$row['CODE']] = $asIs ? $row : $row['VALUE'];
}
return $data;
}
/**
* Get available hooks for this landing.
* @param int $id Entity id.
* @param string $type Entity type.
* @param array $data Data array (optional).
* @return Page[]
*/
protected static function getList($id, $type, array $data = array())
{
$hooks = array();
$classDir = self::HOOKS_PAGE_DIR;
$classNamespace = self::HOOKS_NAMESPACE;
$excludedHooks = \Bitrix\Landing\Site\Type::getExcludedHooks();
// first read all hooks in base dir
foreach (self::getClassesFromDir($classDir) as $class)
{
if (in_array($class, $excludedHooks))
{
continue;
}
$classFull = __NAMESPACE__ . $classNamespace . $class;
if (class_exists($classFull))
{
$hooks[$class] = new $classFull(
self::$editMode,
!($type == self::ENTITY_TYPE_SITE)
);
}
}
// sort hooks
uasort($hooks, function($a, $b)
{
if ($a->getSort() == $b->getSort())
{
return 0;
}
return ($a->getSort() < $b->getSort()) ? -1 : 1;
});
// check custom exec
$event = new Event('landing', 'onHookExec');
$event->send();
foreach ($event->getResults() as $result)
{
if ($result->getType() != EventResult::ERROR)
{
if ($customExec = $result->getModified())
{
foreach ((array)$customExec as $code => $itemExec)
{
$code = mb_strtoupper($code);
if (isset($hooks[$code]) && is_callable($itemExec))
{
$hooks[$code]->setCustomExec($itemExec);
}
}
unset($code, $itemExec);
}
unset($customExec);
}
}
unset($event, $result);
// then fill hook with data
if (!empty($hooks) && $id > 0)
{
if (empty($data))
{
$data = self::getData($id, $type);
}
foreach ($hooks as $code => $hook)
{
if (isset($data[$code]))
{
$hook->setData($data[$code]);
}
}
}
return $hooks;
}
/**
* Set edit mode to true.
* @param bool $mode Edit mode (true by default).
* @return void
*/
public static function setEditMode(bool $mode = true): void
{
self::$editMode = $mode;
}
/**
* Returns edit mode state.
* @return bool
*/
public static function getEditMode(): bool
{
return self::$editMode;
}
/**
* Get hooks for site.
* @param int $id Site id.
* @return Page[]
*/
public static function getForSite($id)
{
if (!Landing::getEditMode())
{
static $hooks = [];
}
else
{
$hooks = [];
}
if (!array_key_exists($id, $hooks))
{
$hooks[$id] = self::getList($id, self::ENTITY_TYPE_SITE);
}
return $hooks[$id];
}
/**
* Get hooks for landing.
* @param int $id Landing id.
* @return Page[]
*/
public static function getForLanding($id)
{
if (!Landing::getEditMode())
{
static $hooks = [];
}
else
{
$hooks = [];
}
if (!array_key_exists($id, $hooks))
{
$hooks[$id] = self::getList($id, self::ENTITY_TYPE_LANDING);
}
return $hooks[$id];
}
/**
* Get row hooks for landing.
* @param int $id Landing id.
* @return array
*/
public static function getForLandingRow($id)
{
return self::getData($id, self::ENTITY_TYPE_LANDING);
}
/**
* Copy data for entity.
* @param int $from From entity id.
* @param int $to To entity id.
* @param string $type Entity type.
* @param bool $publication It's not copy, but publication.
* @return void
*/
protected static function copy($from, $to, $type, $publication = false)
{
$from = (int)$from;
$to = (int)$to;
$data = self::getData($from, $type);
$existData = [];
$classDir = self::HOOKS_PAGE_DIR;
$classNamespace = self::HOOKS_NAMESPACE;
$excludedHooks = \Bitrix\Landing\Site\Type::getExcludedHooks();
// first read all hooks in base dir
foreach (self::getClassesFromDir($classDir) as $class)
{
if (in_array($class, $excludedHooks, true))
{
continue;
}
$classFull = __NAMESPACE__ . $classNamespace . $class;
if (
isset($data[$class])
&& method_exists($classFull, self::HOOKS_ON_COPY_HANDLER)
)
{
$handler = self::HOOKS_ON_COPY_HANDLER;
if ($preparedData = $classFull::$handler($data[$class], $from, $type, $publication))
{
$data[$class] = $preparedData;
}
}
}
// collect exist data
if ($data)
{
$res = HookData::getList([
'select' => [
'ID', 'HOOK', 'CODE'
],
'filter' => [
'ENTITY_ID' => $to,
'=ENTITY_TYPE' => $type,
'=PUBLIC' => $publication ? 'Y' : 'N'
]
]);
while ($row = $res->fetch())
{
$existData[$row['HOOK'] . '_' . $row['CODE']] = $row['ID'];
}
}
// update existing keys or add new
foreach ($data as $hookCode => $items)
{
foreach ($items as $code => $value)
{
$existKey = $hookCode . '_' . $code;
if (is_array($value))
{
$value = 'serialized#' . serialize($value);
}
if (array_key_exists($existKey, $existData))
{
HookData::update($existData[$existKey], [
'VALUE' => $value
]);
unset($existData[$existKey]);
}
else
{
HookData::add([
'ENTITY_ID' => $to,
'ENTITY_TYPE' => $type,
'HOOK' => $hookCode,
'CODE' => $code,
'VALUE' => $value,
'PUBLIC' => $publication ? 'Y' : 'N'
]);
}
}
}
// delete unused data
if ($existData)
{
foreach ($existData as $delId)
{
HookData::delete($delId);
}
}
}
/**
* Copy data for site.
* @param int $from From site id.
* @param int $to To site id.
* @return void
*/
public static function copySite($from, $to)
{
$originalEditMode = self::$editMode;
if (!self::$editMode)
{
self::$editMode = true;
}
self::copy($from, $to, self::ENTITY_TYPE_SITE);
self::$editMode = $originalEditMode;
}
/**
* Copy data for landing.
* @param int $from From landing id.
* @param int $to To landing id.
* @return void
*/
public static function copyLanding($from, $to)
{
$originalEditMode = self::$editMode;
if (!self::$editMode)
{
self::$editMode = true;
}
self::copy($from, $to, self::ENTITY_TYPE_LANDING);
self::$editMode = $originalEditMode;
}
/**
* Publication data for site.
* @param int $siteId Site id.
* @return void
*/
public static function publicationSite($siteId)
{
self::copy($siteId, $siteId, self::ENTITY_TYPE_SITE, true);
}
/**
* Publication data for landing.
* @param int $lid Landing id.
* @return void
*/
public static function publicationLanding($lid)
{
self::copy($lid, $lid, self::ENTITY_TYPE_LANDING, true);
}
/**
* In disable autobulication option we must skip hooks, then required page publication.
* @param $siteId
* @return void
*/
public static function publicationSiteWithSkipNeededPublication($siteId): void
{
self::publicationWithSkipNeededPublication($siteId, self::ENTITY_TYPE_SITE);
}
/**
* In disable autobulication option we must skip hooks, then required page publication.
* @param $landingId
* @return void
*/
public static function publicationLandingWithSkipNeededPublication($landingId): void
{
self::publicationWithSkipNeededPublication($landingId, self::ENTITY_TYPE_LANDING);
}
protected static function publicationWithSkipNeededPublication($id, $type): void
{
$editModeBack = self::$editMode;
self::$editMode = false;
$publicData = self::getData($id, $type, true);
self::$editMode = $editModeBack;
if ($type === self::ENTITY_TYPE_SITE)
{
self::publicationSite($id);
}
if ($type === self::ENTITY_TYPE_LANDING)
{
self::publicationLanding($id);
}
$data = self::getData($id, $type, true);
// return previously public values
$needClearCache = false;
foreach (self::getList($id, $type) as $hook)
{
if ($hook->isNeedPublication())
{
$fieldsToDelete = [];
if (isset($publicData[$hook->getCode()]))
{
foreach ($data[$hook->getCode()] as $fieldCode => $field)
{
if (!isset($publicData[$hook->getCode()][$fieldCode]))
{
$fieldsToDelete[$fieldCode] = $field;
}
elseif ($publicData[$hook->getCode()][$fieldCode]['VALUE'] !== $field['VALUE'])
{
$needClearCache = true;
HookData::update($field['ID'],
[
'VALUE' => $field['VALUE'],
]
);
}
}
}
else
{
$fieldsToDelete = $data[$hook->getCode()] ?? [];
}
// del if not exists in public
if (!empty($fieldsToDelete))
{
$needClearCache = true;
foreach ($fieldsToDelete as $fieldCode => $field)
{
$res = HookData::getList([
'select' => ['ID'],
'filter' => [
'ENTITY_ID' => $id,
'=ENTITY_TYPE' => $type,
'=HOOK' => $hook->getCode(),
'=CODE' => $fieldCode,
'=PUBLIC' => 'Y'
]
]);
if ($row = $res->fetch())
{
HookData::delete($row['ID']);
}
}
}
}
}
// drop public cache
if ($needClearCache)
{
if ($type === self::ENTITY_TYPE_SITE)
{
$landings = Landing::getList([
'select' => ['ID'],
'filter' => [
'SITE_ID' => $id,
'=PUBLIC' => 'Y',
'=DELETED' => 'N',
],
]);
while ($landing = $landings->fetch())
{
Landing::update($landing['ID'], ['PUBLIC' => 'N']);
}
}
if ($type === self::ENTITY_TYPE_LANDING)
{
Landing::update($id, ['PUBLIC' => 'N']);
}
}
}
/**
* Prepare data for save in hooks.
* @param array $data Input data.
* @return array
*/
protected static function prepareData(array $data)
{
$newData = array();
foreach ($data as $code => $val)
{
if (mb_strpos($code, '_') !== false)
{
$codeHook = mb_substr($code, 0, mb_strpos($code, '_'));
$codeVal = mb_substr($code, mb_strpos($code, '_') + 1);
if (!isset($newData[$codeHook]))
{
$newData[$codeHook] = array();
}
$newData[$codeHook][$codeVal] = $val;
}
}
return $newData;
}
/**
* Set data hooks for entity.
* @param int $id Entity id.
* @param string $type Entity type.
* @param array $data Data array.
* @return void
*/
protected static function saveData($id, $type, array $data)
{
$data = self::prepareData($data);
$hooks = self::getList($id, $type, $data);
$dataSave = self::getData($id, $type, true);
// get hooks with new new data (not saved yet)
foreach ($hooks as $hook)
{
$hookLocked = $hook->isLocked();
$codeHook = $hook->getCode();
// modify $dataSave ...
foreach ($hook->getFields() as $field)
{
$codeVal = $field->getCode();
if ($hookLocked && !$field->isEmptyValue())
{
continue;
}
if (!isset($data[$codeHook][$codeVal]))
{
continue;
}
// ... for changed
if (isset($dataSave[$codeHook][$codeVal]))
{
$dataSave[$codeHook][$codeVal]['CHANGED'] = true;
$dataSave[$codeHook][$codeVal]['VALUE'] = $field->getValue();
}
// ... and new fields
else
{
if (!isset($dataSave[$codeHook]))
{
$dataSave[$codeHook] = array();
}
$dataSave[$codeHook][$codeVal] = array(
'HOOK' => $codeHook,
'CODE' => $codeVal,
'VALUE' => $field->getValue()
);
}
if (is_array($dataSave[$codeHook][$codeVal]['VALUE']))
{
$dataSave[$codeHook][$codeVal]['VALUE'] = 'serialized#' . serialize(
$dataSave[$codeHook][$codeVal]['VALUE']
);
}
}
}
// now save the data
foreach ($dataSave as $codeHook => $dataHook)
{
foreach ($dataHook as $code => $row)
{
if (
is_array($row['VALUE']) && empty($row['VALUE'])
||
!is_array($row['VALUE']) && trim($row['VALUE']) == ''
)
{
if (isset($row['ID']))
{
HookData::delete($row['ID']);
}
}
else
{
if (!isset($row['ID']))
{
$row['ENTITY_ID'] = $id;
$row['ENTITY_TYPE'] = $type;
HookData::add($row);
}
elseif (isset($row['CHANGED']) && $row['CHANGED'])
{
$updId = $row['ID'];
unset($row['ID'], $row['CHANGED']);
HookData::update($updId, $row);
}
}
}
}
}
/**
* Index hook's content for entities.
* @param int $id Entity id.
* @param string $type Entity type.
* @return void
*/
protected static function indexContent($id, $type)
{
$id = intval($id);
if ($type == self::ENTITY_TYPE_LANDING)
{
$class = '\Bitrix\Landing\Landing';
}
if (!isset($class))
{
return;
}
// base fields
$searchContent = $class::getList([
'select' => [
'TITLE', 'DESCRIPTION'
],
'filter' => [
'ID' => $id,
'=DELETED' => ['Y', 'N'],
'=SITE.DELETED' => ['Y', 'N']
]
])->fetch();
if (!$searchContent)
{
return;
}
$searchContent = array_values($searchContent);
// hook fields
foreach (self::getList($id, $type) as $hook)
{
foreach ($hook->getFields() as $field)
{
if ($field->isSearchable())
{
$searchContent[] = $field->getValue();
}
}
}
$searchContent = array_unique($searchContent);
$searchContent = $searchContent ? implode(' ', $searchContent) : '';
$searchContent = trim($searchContent);
if ($searchContent)
{
$res = $class::update($id, [
'SEARCH_CONTENT' => $searchContent
]);
$res->isSuccess();
}
}
/**
* Set data hooks for site.
* @param int $id Site id.
* @param array $data Data array.
* @return void
*/
public static function saveForSite(int $id, array $data): void
{
$check = Site::getList([
'select' => [
'ID'
],
'filter' => [
'ID' => $id
]
])->fetch();
if ($check)
{
$editModeBack = self::$editMode;
self::$editMode = true;
self::saveData($id, self::ENTITY_TYPE_SITE, $data);
if (Manager::getOption('public_hook_on_save') === 'Y')
{
self::publicationSiteWithSkipNeededPublication($id);
}
self::$editMode = $editModeBack;
}
}
/**
* Get hooks for landing.
* @param int $id Landing id.
* @param array $data Data array.
* @return void
*/
public static function saveForLanding(int $id, array $data): void
{
$check = Landing::getList([
'select' => [
'ID'
],
'filter' => [
'ID' => $id
]
])->fetch();
if ($check)
{
$editModeBack = self::$editMode;
self::$editMode = true;
self::saveData($id, self::ENTITY_TYPE_LANDING, $data);
self::indexContent($id, self::ENTITY_TYPE_LANDING);
if (Manager::getOption('public_hook_on_save') === 'Y')
{
self::publicationLandingWithSkipNeededPublication($id);
}
self::$editMode = $editModeBack;
}
}
/**
* Index hook's content for landing.
* @param int $id Landing id.
* @return void
*/
public static function indexLanding($id)
{
self::indexContent($id, self::ENTITY_TYPE_LANDING);
}
/**
* Delete data hooks for entity.
* @param int $id Entity id.
* @param string $type Entity type.
* @return void
*/
protected static function deleteData($id, $type)
{
$id = intval($id);
$res = HookData::getList(array(
'select' => array(
'ID'
),
'filter' => array(
'ENTITY_ID' => $id,
'=ENTITY_TYPE' => $type
)
));
while ($row = $res->fetch())
{
HookData::delete($row['ID']);
}
}
/**
* Delete data hooks for site.
* @param int $id Landing id.
* @return void
*/
public static function deleteForSite($id)
{
self::deleteData($id, self::ENTITY_TYPE_SITE);
}
/**
* Delete data hooks for landing.
* @param int $id Landing id.
* @return void
*/
public static function deleteForLanding($id)
{
self::deleteData($id, self::ENTITY_TYPE_LANDING);
}
}