Current Path : /var/www/www-root/data/www.catalog.monolith-realty.ru/bitrix/modules/landing/lib/ |
Current File : /var/www/www-root/data/www.catalog.monolith-realty.ru/bitrix/modules/landing/lib/block.php |
<?php namespace Bitrix\Landing; use \Bitrix\Main\Page\Asset; use \Bitrix\Main\Web\Json; use \Bitrix\Main\Web\DOM; use \Bitrix\Main\Localization\Loc; use \Bitrix\Landing\Connector; use \Bitrix\Landing\Controller; use \Bitrix\Landing\Internals; use \Bitrix\Landing\Assets; use \Bitrix\Landing\Block\Cache; use \Bitrix\Landing\Restriction; use \Bitrix\Landing\Node\Type as NodeType; use \Bitrix\Landing\Node\Img; use \Bitrix\Landing\PublicAction\Utils as UtilsAction; Loc::loadMessages(__FILE__); class Block extends \Bitrix\Landing\Internals\BaseTable { /** * Dir of repository of blocks. */ const BLOCKS_DIR = 'blocks'; /** * Tag for managed cache. */ const BLOCKS_TAG = 'landing_blocks'; /** * Block preview filename. */ const PREVIEW_FILE_NAME = 'preview.jpg'; /** * Local css filename. */ const CSS_FILE_NAME = 'style.css'; /** * Local js filename. */ const JS_FILE_NAME = 'script.js'; /** * Pattern for repo code. */ const REPO_MASK = '/^repo_([\d]+)$/'; /** * Life time for mark new block. */ const NEW_BLOCK_LT = 1209600;//86400 * 14 /** * Access level: any access denied to all blocks. */ const ACCESS_A = 'A'; /** * Access level: access denied. */ const ACCESS_D = 'D'; /** * Access level: edit only design. */ const ACCESS_V = 'V'; /** * Access level: edit content and design (not delete). */ const ACCESS_W = 'W'; /** * Access level: full access. */ const ACCESS_X = 'X'; /** * Symbolic code of card. */ const CARD_SYM_CODE = 'card'; /** * Symbolic code of preset. */ const PRESET_SYM_CODE = 'preset'; /** * Default setting for block wrapper style, if not set manifest[styles][block] section */ public const DEFAULT_WRAPPER_STYLE = ['block-default']; /** * Maximum allowed number of favorite blocks */ public const FAVOURITE_BLOCKS_LIMIT = 5000; /** * Maximum allowed number of favorite blocks with preview image */ public const FAVOURITE_BLOCKS_LIMIT_WITH_PREVIEW = 1000; /** * Internal class. * @var string */ public static $internalClass = 'BlockTable'; /** * Id of current block. * @var int */ protected $id = 0; /** * Id of landing. * @var int */ protected $lid = 0; /** * Parent id of block (public version id). * @var int */ protected $parentId = 0; /** * Id of site of landing. * @var int */ protected $siteId = 0; /** * Sort of current block. * @var int */ protected $sort = 0; /** * Is the rest block if > 0. * @var int */ protected $repoId = 0; /** * REST repository some info. * @var array */ protected $repoInfo = []; /** * Code of current block. * @var string */ protected $code = ''; /** * Custom anchor of the block. * @var string */ protected $anchor = ''; /** * Actually content of current block. * @var string */ protected $content = ''; /** * Required user action just added. * @var array */ protected $runtimeRequiredUserAction = []; /** * Access for this block. * @see ACCESS_* constants. * @var string */ protected $access = 'X'; /** * Additional data of current block. * @var array */ protected $metaData = array(); /** * Additional block assets. * @var array */ protected $assets = array(); /** * Active or not current block. * @var boolean */ protected $active = false; /** * Active or not page of current block. * @var boolean */ protected $landingActive = false; /** * Deleted or not current block. * @var boolean */ protected $deleted = false; /** * Current block was designed. * @var boolean */ protected $designed = false; /** * Public or not current block. * @var boolean */ protected $public = false; /** * This block allowed or not by tariff. * @var bool */ protected $allowedByTariff = true; /** * Document root. * @var string */ protected $docRoot = ''; /** * Instance of Error. * @var Error */ protected $error = null; /** * Dynamic params. * @var array */ protected $dynamicParams = []; /** * Allowed extensions for developers. * @var array */ protected $allowedExtensions = [ 'landing_form', 'landing_carousel', 'landing_google_maps_new', 'landing_map', 'landing_countdown', 'landing_gallery_cards', 'landing_chat' ]; /** * Constructor. * @param int $id Block id. * @param array $data Data row from BlockTable (by default get from DB). * @param array $params Some additional params. */ public function __construct($id, $data = [], array $params = []) { if (empty($data) || !is_array($data)) { $data = parent::getList(array( 'select' => array( '*', 'LANDING_TITLE' => 'LANDING.TITLE', 'LANDING_ACTIVE' => 'LANDING.ACTIVE', 'LANDING_TPL_CODE' => 'LANDING.TPL_CODE', 'SITE_TPL_CODE' => 'LANDING.SITE.TPL_CODE', 'SITE_TYPE' => 'LANDING.SITE.TYPE', 'SITE_ID' => 'LANDING.SITE_ID' ), 'filter' => array( 'ID' => (int)$id ) ))->fetch(); if (!$data) { $id = 0; } } // if content is empty, fill from repository if (!isset($data['CONTENT']) || trim($data['CONTENT']) == '') { $data['CONTENT'] = ''; } $this->id = intval($id); $this->lid = isset($data['LID']) ? intval($data['LID']) : 0; $this->parentId = isset($data['PARENT_ID']) ? intval($data['PARENT_ID']) : 0; $this->siteId = isset($data['SITE_ID']) ? intval($data['SITE_ID']) : 0; $this->sort = isset($data['SORT']) ? intval($data['SORT']) : ''; $this->code = isset($data['CODE']) ? trim($data['CODE']) : ''; $this->anchor = isset($data['ANCHOR']) ? trim($data['ANCHOR']) : ''; $this->active = isset($data['ACTIVE']) && $data['ACTIVE'] == 'Y'; $this->landingActive = isset($data['LANDING_ACTIVE']) && $data['LANDING_ACTIVE'] == 'Y'; $this->deleted = isset($data['DELETED']) && $data['DELETED'] == 'Y'; $this->designed = isset($data['DESIGNED']) && $data['DESIGNED'] == 'Y'; $this->public = isset($data['PUBLIC']) && $data['PUBLIC'] == 'Y'; $this->content = (!$this->deleted && isset($data['CONTENT'])) ? trim($data['CONTENT']) : ''; // access if (isset($data['ACCESS'])) { $this->access = $data['ACCESS']; } // assets if (isset($data['ASSETS'])) { $this->assets = $data['ASSETS']; } // fill meta data $keys = [ 'LID', 'FAVORITE_META', 'CREATED_BY_ID', 'DATE_CREATE', 'MODIFIED_BY_ID', 'DATE_MODIFY', 'SITE_TYPE' ]; foreach ($keys as $key) { if (isset($data[$key])) { $this->metaData[$key] = $data[$key]; } } $this->metaData['LANDING_TITLE'] = isset($data['LANDING_TITLE']) ? $data['LANDING_TITLE'] : ''; $this->metaData['LANDING_TPL_CODE'] = isset($data['LANDING_TPL_CODE']) ? $data['LANDING_TPL_CODE'] : ''; $this->metaData['SITE_TPL_CODE'] = isset($data['SITE_TPL_CODE']) ? $data['SITE_TPL_CODE'] : ''; $this->metaData['XML_ID'] = isset($data['XML_ID']) ? $data['XML_ID'] : ''; $this->metaData['DESIGNER_MODE'] = isset($params['designer_mode']) && $params['designer_mode'] === true; // other data if (preg_match(self::REPO_MASK, $this->code, $matches)) { $this->repoId = $matches[1]; } if (!$this->content && !$this->deleted) { $this->content = self::getContentFromRepository($this->code); } $this->error = new Error; $this->docRoot = Manager::getDocRoot(); // dynamic params if (isset($data['SOURCE_PARAMS'])) { $this->dynamicParams = (array)$data['SOURCE_PARAMS']; } } /** * Fill landing with blocks. * @param Landing $landing Landing instance. * @param int $limit Limit count for blocks. * @param array $params Additional params. * @return boolean */ public static function fillLanding(Landing $landing, $limit = 0, array $params = array()) { if ($landing->exist()) { $editMode = $landing->getEditMode() || $landing->getPreviewMode(); $repo = array(); $blocks = array(); // get all blocks by filter $filter = array( 'LID' => $landing->getId(), '=PUBLIC' => $editMode ? 'N' : 'Y', '=DELETED' => (isset($params['deleted']) && $params['deleted'] === true) ? 'Y' : 'N' ); if (isset($params['id']) && $params['id']) { $filter['ID'] = $params['id']; } $res = parent::getList([ 'select' => [ '*', 'LANDING_ACTIVE' => 'LANDING.ACTIVE', 'LANDING_TPL_CODE' => 'LANDING.TPL_CODE', 'SITE_TPL_CODE' => 'LANDING.SITE.TPL_CODE', 'SITE_TYPE' => 'LANDING.SITE.TYPE', 'SITE_ID' => 'LANDING.SITE_ID' ], 'filter' => $filter, 'order' => [ 'SORT' => 'ASC', 'ID' => 'ASC' ], 'limit' => $limit ?: null ]); while ($row = $res->fetch()) { $blockParams = []; if (!$landing->canEdit()) { $row['ACCESS'] = self::ACCESS_A; } $row['SITE_ID'] = $landing->getSiteId(); $block = new self( $row['ID'], $row, $blockParams ); if ($block->getRepoId()) { $repo[] = $block->getRepoId(); } $blocks[$row['ID']] = $block; } unset($row, $res); if (!empty($repo)) { $repo = Repo::getAppInfo($repo); } // add blocks to landing foreach ($blocks as $block) { if ( isset($repo[$block->getRepoId()]['PAYMENT_ALLOW']) && $repo[$block->getRepoId()]['PAYMENT_ALLOW'] != 'Y' ) { $allowedByTariff = false; } else { $allowedByTariff = true; } if ($editMode) { $block->setAllowedByTariff($allowedByTariff); $landing->addBlockToCollection($block); } elseif ($allowedByTariff) { $landing->addBlockToCollection($block); } } unset($blocks, $block, $repo); return true; } return false; } /** * Create copy of blocks for draft version. * @param \Bitrix\Landing\Landing $landing Landing instance. * @return void */ public static function cloneForEdit(\Bitrix\Landing\Landing $landing) { if ($landing->exist()) { $clone = true; $forClone = array(); $res = parent::getList(array( 'select' => array( 'ID', 'LID', 'CODE', 'SORT', 'ACTIVE', 'CONTENT', 'PUBLIC', 'ACCESS', 'ANCHOR', 'DESIGNED' ), 'filter' => array( 'LID' => $landing->getId() ) )); while ($row = $res->fetch()) { if ($row['PUBLIC'] != 'Y') { $clone = false; break; } else { if (!$row['ANCHOR']) { $row['ANCHOR'] = 'b' . $row['ID']; } $row['PUBLIC'] = 'N'; $row['PARENT_ID'] = $row['ID']; unset($row['ID']); $forClone[] = $row; } } if ($clone) { foreach ($forClone as $row) { parent::add($row); } } } } /** * Publication blocks for landing. * @param \Bitrix\Landing\Landing $landing Landing instance. * @return void */ public static function publicationBlocks(\Bitrix\Landing\Landing $landing) { Mutator::blocksPublication($landing); } /** * Recognize landing id by block id. * @param int|array $id Block id (id array). * @return int|array|false */ public static function getLandingIdByBlockId($id) { $data = array(); $res = parent::getList(array( 'select' => array( 'ID', 'LID' ), 'filter' => array( 'ID' => $id ) )); while ($row = $res->fetch()) { $data[$row['ID']] = $row['LID']; } if (is_array($id)) { return $data; } elseif (!empty($data)) { return array_pop($data); } return false; } /** * Gets row by block id. * @param int|array $id Block id (id array). * @param array $select Select row. * @deprecated since 18.5.0 * @return int|array|false */ public static function getLandingRowByBlockId($id, array $select = array('ID')) { return self::getRowByBlockId($id, $select); } /** * Gets landing row by block id. * @param int|array $id Block id (id array). * @param array $select Select row. * @return int|array|false */ public static function getRowByBlockId($id, array $select = array('ID')) { $data = array(); $res = parent::getList(array( 'select' => $select, 'filter' => array( 'ID' => $id ) )); while ($row = $res->fetch()) { $data[$row['ID']] = $row; } if (is_array($id)) { return $data; } elseif (!empty($data)) { return array_pop($data); } return false; } /** * Returns normalized block data. * @param string $code Block code. * @return array|null */ protected static function getNormalizedBlock(string $code): ?array { static $cached = []; if (isset($cached[$code])) { return $cached[$code]; } $codeOriginal = $code; [$code, $blockId] = explode('@', $code); $filter = [ 'LID' => 0, '=DELETED' => 'N', '=CODE' => $code ]; if ($blockId) { $filter['ID'] = $blockId; } $res = Internals\BlockTable::getList([ 'select' => [ 'ID', 'CODE', 'CONTENT', 'SOURCE_PARAMS', 'DESIGNED' ], 'filter' => $filter ]); if ($row = $res->fetch()) { $cached[$codeOriginal] = $row; $cached[$codeOriginal]['FILES'] = File::getFilesFromBlockContent($row['ID'], $row['CONTENT']); } return $cached[$codeOriginal] ?? null; } /** * Get content from repository by code. * @param string $code Block code. * @param string|null $namespace Namespace (optional). * @return string|null */ public static function getContentFromRepository(string $code, string $namespace = null): ?string { if (!is_string($code)) { return null; } if (strpos($code, '@')) { $normalizedBlock = self::getNormalizedBlock($code); return $normalizedBlock['CONTENT'] ?? null; } $content = null; // local repo if (preg_match(self::REPO_MASK, $code, $matches)) { $repo = Repo::getById($matches[1])->fetch(); $content = $repo['CONTENT']; } // files storage elseif ($path = self::getBlockPath($code, $namespace)) { $path = Manager::getDocRoot() . $path . '/block.php'; if (file_exists($path)) { $content = file_get_contents($path); if (preg_match('/MESS\[[^\]]+\]/', $content)) { $mess = Loc::loadLanguageFile($path); if ($mess) { $replace = []; foreach ($mess as $key => $title) { $replace['MESS[' . $key . ']'] = $title; } $content = str_replace( array_keys($replace), array_values($replace), $content ); } } } } return $content; } /** * Create instance by string code. * @param Landing $landing Landing - owner for new block. * @param string $code Code of block from repository. * @param array $data Additional data array. * @return Block|false */ public static function createFromRepository(Landing $landing, string $code, array $data = array()) { // get content and manifest $filesFromContent = []; $sourceParams = []; $codeOriginal = null; $designed = 'N'; $content = $data['CONTENT'] ?? self::getContentFromRepository($code); if (isset($data['PREPARE_BLOCK_DATA']['ACTION'])) { if ( $data['PREPARE_BLOCK_DATA']['ACTION'] === 'changeComponentParams' && isset($data['PREPARE_BLOCK_DATA']['PARAMS']) && is_array($data['PREPARE_BLOCK_DATA']['PARAMS']) ) { foreach ($data['PREPARE_BLOCK_DATA']['PARAMS'] as $paramName => $paramValue) { $search = "'" . $paramName . "' => '',"; $replace = "'" . $paramName . "' => '". $paramValue . "',"; $content = str_replace($search, $replace, $content); } } } if (strpos($code, '@')) { $codeOriginal = $code; $normalizedBlock = self::getNormalizedBlock($code); $designed = $normalizedBlock['DESIGNED'] ?? 'N'; $filesFromContent = $normalizedBlock['FILES'] ?? []; $sourceParams = $normalizedBlock['SOURCE_PARAMS'] ?? []; [$code, ] = explode('@', $code); } $manifest = self::getManifestFile($code); // version control if ( isset($manifest['block']['version']) && version_compare(Manager::getVersion(), $manifest['block']['version']) < 0 ) { $landing->getError()->addError( 'BLOCK_WRONG_VERSION', Loc::getMessage('LANDING_BLOCK_WRONG_VERSION') ); return false; } // check errors if (!$landing->exist()) { $landing->getError()->addError( 'LANDING_NOT_EXIST', Loc::getMessage('LANDING_BLOCK_LANDING_NOT_EXIST') ); return false; } if ($content == '') { $landing->getError()->addError( 'BLOCK_NOT_FOUND', Loc::getMessage('LANDING_BLOCK_NOT_FOUND') ); return false; } // add $fields = array( 'LID' => $landing->getId(), 'CODE' => $code, 'SOURCE_PARAMS' => $sourceParams, 'CONTENT' => $content, 'ACTIVE' => 'Y', 'DESIGNED' => $designed ); $availableReplace = array( 'ACTIVE', 'PUBLIC', 'ACCESS', 'SORT', 'CONTENT', 'ANCHOR', 'SOURCE_PARAMS', 'INITIATOR_APP_CODE', 'XML_ID', 'DESIGNED', 'FAVORITE_META' ); foreach ($availableReplace as $replace) { if (isset($data[$replace])) { $fields[$replace] = $data[$replace]; } } $res = parent::add($fields); if ($res->isSuccess()) { $block = new self($res->getId()); $manifest = $block->getManifest(); if (!$block->getLocalAnchor()) { $historyActivity = History::isActive(); History::deactivate(); $block->setAnchor('b' . $block->getId()); $historyActivity ? History::activate() : History::deactivate(); } Assets\PreProcessing::blockAddProcessing($block); if ( isset($manifest['callbacks']['afteradd']) && is_callable($manifest['callbacks']['afteradd']) ) { $manifest['callbacks']['afteradd']($block); } // calling class(es) of block foreach ($block->getClass() as $class) { $classBlock = $block->includeBlockClass($class); $classBlock->beforeAdd($block); } // for set filter if ($fields['SOURCE_PARAMS']) { $block->saveDynamicParams( $fields['SOURCE_PARAMS'] ); } if (isset($manifest['block']['app_code'])) { $block->save([ 'INITIATOR_APP_CODE' => $manifest['block']['app_code'] ]); } else// index search only { $block->save(); } // copy references to files from content to new block foreach ($filesFromContent as $fileId) { File::addToBlock($block->getId(), $fileId); } return $block; } else { $landing->getError()->addFromResult($res); return false; } } /** * New or not the block. * @param string $block Block code. * @return boolean */ protected static function isNewBlock($block) { static $newBlocks = null; if (!is_string($block)) { return false; } if ($newBlocks === null) { $newBlocks = unserialize(Manager::getOption('new_blocks'), ['allowed_classes' => false]); if (!is_array($newBlocks)) { $newBlocks = array(); } if ( !isset($newBlocks['date']) || ( isset($newBlocks['date']) && ((time() - $newBlocks['date']) > self::NEW_BLOCK_LT) ) ) { $newBlocks = array(); } if (isset($newBlocks['items'])) { $newBlocks = $newBlocks['items']; } } return in_array($block, $newBlocks); } /** * Gets general paths, where blocks can be found. * @return array */ protected static function getGeneralPaths() { static $paths = null; if (!$paths) { $paths = [ BX_ROOT . '/' . self::BLOCKS_DIR, \getLocalPath(self::BLOCKS_DIR) ]; if ($paths[0] == $paths[1]) { unset($paths[1]); } } return $paths; } /** * Clear cache repository. * @return void */ public static function clearRepositoryCache() { if (Cache::isCaching()) { Manager::getCacheManager()->clearByTag(self::BLOCKS_TAG); } } /** * Gets all available namespaces. * @return array */ protected static function getNamespaces() { static $namespaces = []; if ($namespaces) { return $namespaces; } $paths = self::getGeneralPaths(); $disableNamespace = (array)Config::get('disable_namespace'); $enableNamespace = Config::get('enable_namespace'); $enableNamespace = $enableNamespace ? (array) $enableNamespace : array(); $namespaces = []; foreach ($paths as $path) { if ($path !== false) { $path = Manager::getDocRoot() . $path; // read all subdirs ($namespaces) in block dir if (($handle = opendir($path))) { while ((($entry = readdir($handle)) !== false)) { if (!empty($enableNamespace)) { if (in_array($entry, $enableNamespace)) { $namespaces[] = $entry; } } else if ( $entry != '.' && $entry != '..' && is_dir($path . '/' . $entry) && !in_array($entry, $disableNamespace) ) { $namespaces[] = $entry; } } } } } $namespaces = array_unique($namespaces); return $namespaces; } /** * Get blocks from repository. * @param bool $withManifest Get repo with manifest files of blocks. * @return array */ public static function getRepository($withManifest = false) { static $blocksCats = array(); // function for prepare return $returnFunc = function($blocksCats) use($withManifest) { $event = new \Bitrix\Main\Event('landing', 'onBlockGetRepository', array( 'blocks' => $blocksCats, 'withManifest' => $withManifest )); $event->send(); foreach ($event->getResults() as $result) { if ($result->getResultType() != \Bitrix\Main\EventResult::ERROR) { if (($modified = $result->getModified())) { if (isset($modified['blocks'])) { $blocksCats = $modified['blocks']; } } } } return $blocksCats; }; // static cache if (!$withManifest && !empty($blocksCats)) { return $returnFunc($blocksCats); } // local function for fill last used blocks $fillLastUsed = function($blocksCats) { $blocksCats['last']['items'] = array(); $lastUsed = self::getLastUsed(); if ($lastUsed) { foreach ($lastUsed as $code) { $blocksCats['last']['items'][$code] = array(); } foreach ($blocksCats as $catCode => &$cat) { foreach ($cat['items'] as $code => &$block) { if ( in_array($code, $lastUsed) && $catCode != 'last' && !empty($block) ) { $block['section'][] = 'last'; $blocksCats['last']['items'][$code] = $block; } } unset($block); } unset($cat); // clear last-section foreach ($blocksCats['last']['items'] as $code => $block) { if (!$block) { unset($blocksCats['last']['items'][$code]); } } } return $blocksCats; }; // config $disableNamespace = (array)Config::get('disable_namespace'); $enableNamespace = Config::get('enable_namespace'); $enableNamespace = $enableNamespace ? (array) $enableNamespace : array(); // system cache begin $cache = new \CPHPCache(); $cacheTime = 86400; $cacheStarted = false; $cacheId = $withManifest ? 'blocks_manifest' : 'blocks'; $cacheId .= LANGUAGE_ID; $cacheId .= 'user:' . Manager::getUserId(); $cacheId .= 'version:2'; $cacheId .= 'disable:' . implode(',', $disableNamespace); $cacheId .= 'enable:' . implode(',', $enableNamespace); $cachePath = 'landing/blocks'; if ($cache->initCache($cacheTime, $cacheId, $cachePath)) { $blocksCats = $cache->getVars(); if (is_array($blocksCats) && !empty($blocksCats)) { $blocksCats = $fillLastUsed($blocksCats); return $returnFunc($blocksCats); } } if ($cache->startDataCache($cacheTime, $cacheId, $cachePath)) { $cacheStarted = true; if (Cache::isCaching()) { Manager::getCacheManager()->startTagCache($cachePath); Manager::getCacheManager()->registerTag(self::BLOCKS_TAG); } } // not in cache - init $blocks = array(); $sections = array(); // general paths and namespaces $paths = self::getGeneralPaths(); $namespaces = self::getNamespaces(); //get all blocks with description-file sort($namespaces); foreach ($namespaces as $subdir) { foreach ($paths as $path) { $path = Manager::getDocRoot() . $path; if ( is_dir($path . '/' . $subdir) && ($handle = opendir($path . '/' . $subdir)) ) { // sections $sectionsPath = $path . '/' . $subdir . '/.sections.php'; if (file_exists($sectionsPath)) { $sections = array_merge( $sections, (array) include $sectionsPath ); } if (!isset($sections['last'])) { $sections['last'] = [ 'name' => Loc::getMessage('LD_BLOCK_SECTION_LAST') ]; } // blocks while ((($entry = readdir($handle)) !== false)) { $descriptionPath = $path . '/' . $subdir . '/' . $entry . '/.description.php'; $previewPathJpg = $path . '/' . $subdir . '/' . $entry . '/' . self::PREVIEW_FILE_NAME; if ($entry != '.' && $entry != '..' && file_exists($descriptionPath)) { Loc::loadLanguageFile($descriptionPath); $description = include $descriptionPath; if (isset($description['block']['name'])) { $previewFileName = Manager::getUrlFromFile( \getLocalPath( self::BLOCKS_DIR . '/' . $subdir . '/' . $entry . '/' . self::PREVIEW_FILE_NAME ) ); $blocks[$entry] = array( 'id' => isset($description['block']['id']) ? (string)$description['block']['id'] : null, 'name' => $description['block']['name'], 'namespace' => $subdir, 'new' => self::isNewBlock($entry), 'version' => isset($description['block']['version']) ? $description['block']['version'] : null, 'type' => isset($description['block']['type']) ? $description['block']['type'] : array(), 'section' => isset($description['block']['section']) ? $description['block']['section'] : 'other', 'description' => isset($description['block']['description']) ? $description['block']['description'] : '', 'preview' => file_exists($previewPathJpg) ? $previewFileName : '', 'restricted' => false, 'repo_id' => false, 'app_code' => false, 'only_for_license' => $description['block']['only_for_license'] ?? '', ); if ($withManifest) { $blocks[$entry]['manifest'] = self::getManifestFile( $subdir . ':' . $entry ); $blocks[$entry]['content'] = self::getContentFromRepository( $entry, $subdir ); if (isset($blocks[$entry]['manifest']['block'])) { $blocks[$entry]['manifest']['block']['preview'] = $blocks[$entry]['preview']; } // local assets to manifest's assets if (!isset($blocks[$entry]['manifest']['assets'])) { $blocks[$entry]['manifest']['assets'] = array(); } // if css exists if (file_exists($path . '/' . $subdir . '/' . $entry . '/style.min.css')) { if (!isset($blocks[$entry]['manifest']['assets']['css'])) { $blocks[$entry]['manifest']['assets']['css'] = array(); } $blocks[$entry]['manifest']['assets']['css'][] = Manager::getUrlFromFile( \getLocalPath( self::BLOCKS_DIR . '/' . $subdir . '/' . $entry . '/style.min.css' ) ); } // if js exists if (file_exists($path . '/' . $subdir . '/' . $entry . '/script.min.js' )) { if (!isset($blocks[$entry]['manifest']['assets']['js'])) { $blocks[$entry]['manifest']['assets']['js'] = array(); } $blocks[$entry]['manifest']['assets']['js'][] = Manager::getUrlFromFile( \getLocalPath( self::BLOCKS_DIR . '/' . $subdir . '/' . $entry . '/script.min.js' ) ); } if (empty($blocks[$entry]['manifest']['assets'])) { unset($blocks[$entry]['manifest']['assets']); } } } } } } } } // rest repo $blocksRepo = Repo::getRepository(); // get apps by blocks $apps = array(); foreach ($blocksRepo as $block) { if ($block['app_code']) { $apps[] = $block['app_code']; } } if ($apps) { $apps = array_unique($apps); $apps = Repo::getAppByCode($apps); // mark repo blocks expired foreach ($blocksRepo as &$block) { if ( $block['app_code'] && isset($apps[$block['app_code']]) && $apps[$block['app_code']]['PAYMENT_ALLOW'] == 'N' ) { $block['app_expired'] = true; } } unset($block); } $blocks += $blocksRepo; // favorites block $currentUser = Manager::getUserId(); $favoriteBlocks = []; $favoriteMyBlocks = []; $res = Internals\BlockTable::getList([ 'select' => [ 'ID', 'CODE', 'FAVORITE_META', 'CREATED_BY_ID' ], 'filter' => [ 'LID' => 0, '=DELETED' => 'N' ], 'order' => [ 'ID' => 'desc' ], 'limit' => self::FAVOURITE_BLOCKS_LIMIT, ]); $countFavoriteBlocks = 0; while ($row = $res->fetch()) { $countFavoriteBlocks++; if (isset($blocks[$row['CODE']])) { if (!is_array($row['FAVORITE_META'])) { continue; } $meta = $row['FAVORITE_META']; $meta['preview'] = $meta['preview'] ?? 0; $meta['favorite'] = true; $meta['favoriteMy'] = ((int)$row['CREATED_BY_ID'] === $currentUser); if ($meta['preview'] > 0 && $countFavoriteBlocks < self::FAVOURITE_BLOCKS_LIMIT_WITH_PREVIEW) { $meta['preview'] = File::getFilePath($meta['preview']); } else { unset($meta['preview']); } if (isset($meta['section'])) { $meta['section'] = (array)$meta['section']; } $item = array_merge( $blocks[$row['CODE']], $meta ); $code = $row['CODE'] . '@' . $row['ID']; if ($item['type'] === 'null') { $item['type'] = []; } $meta['favoriteMy'] ? ($favoriteMyBlocks[$code] = $item) : ($favoriteBlocks[$code] = $item) ; } } $blocks = $favoriteMyBlocks + $blocks + $favoriteBlocks; // create new section in repo $createNewSection = function($item) { return array( 'name' => isset($item['name']) ? (string) $item['name'] : (string) $item, 'meta' => $item['meta'] ?? [], 'new' => false, 'type' => $item['type'] ?? null, 'separator' => false, 'app_code' => false, 'items' => array() ); }; // set by sections $createdSects = []; foreach ($sections as $code => $item) { $title = $item['name'] ?? $item; $title = (string) $title; $title = trim($title); $blocksCats[$code] = $createNewSection($item); $createdSects[$title] = $code; } foreach ($blocks as $key => $block) { if (!is_array($block['section'])) { $block['section'] = array($block['section']); } foreach ($block['section'] as $section) { $section = trim($section); if (!$section) { $section = 'other'; } // adding new sections (actual for repo blocks) if (!isset($blocksCats[$section])) { if (isset($createdSects[$section])) { $section = $createdSects[$section]; } else { $blocksCats[$section] = $createNewSection($section); } } $blocksCats[$section]['items'][$key] = $block; if ($block['new']) { $blocksCats[$section]['new'] = true; } } } // add apps sections if (!empty($blocksRepo) && !empty($apps)) { $blocksCats['separator_apps'] = array( 'name' => Loc::getMessage('LANDING_BLOCK_SEPARATOR_PARTNER_2'), 'separator' => true, 'items' => array() ); foreach ($apps as $app) { $blocksCats[$app['CODE']] = array( 'name' => $app['APP_NAME'], 'new' => false, 'separator' => false, 'app_code' => $app['CODE'], 'items' => array() ); } // add blocks to the app sections foreach ($blocksRepo as $key => $block) { if ($block['app_code']) { $blocksCats[$block['app_code']]['items'][$key] = $block; } } } // sort by id foreach ($blocksCats as $codeCat => &$blocksCat) { $codeCat = mb_strtoupper($codeCat); uasort($blocksCat['items'], function($item1, $item2) use($codeCat) { if ($item1['repo_id']) { return 1; } if ($item2['repo_id']) { return 0; } if ( ($item1['id'] && $item2['id']) && mb_strpos($item1['id'], 'BX_'.$codeCat.'_') === 0 && mb_strpos($item2['id'], 'BX_'.$codeCat.'_') === 0 ) { return ($item1['id'] > $item2['id']) ? 1 : -1; } return 0; }); } unset($blocksCat); // system cache end if ($cacheStarted) { $cache->endDataCache($blocksCats); if (Cache::isCaching()) { Manager::getCacheManager()->endTagCache(); } } $blocksCats = $fillLastUsed($blocksCats); return $returnFunc($blocksCats); } /** * Returns last used blocks by current user. * @param int $count Count of blocks. * @return array */ public static function getLastUsed(int $count = 15): array { $blocks = array(); $res = Internals\BlockLastUsedTable::getList([ 'select' => [ 'CODE' ], 'filter' => [ 'USER_ID' => Manager::getUserId() ], 'order' => [ 'DATE_CREATE' => 'DESC' ], 'limit' => $count ?: null ]); while ($row = $res->fetch()) { $blocks[] = $row['CODE']; } return $blocks; } /** * Stores block by code as last used. * @param string $blockCode Block code. * @return void */ public static function markAsUsed(string $blockCode): void { $res = Internals\BlockLastUsedTable::getList([ 'select' => [ 'ID' ], 'filter' => [ 'USER_ID' => Manager::getUserId(), '=CODE' => $blockCode ], 'limit' => 1 ]); if ($row = $res->fetch()) { Internals\BlockLastUsedTable::update($row['ID'], [ 'DATE_CREATE' => new \Bitrix\Main\Type\DateTime ]); } else { Internals\BlockLastUsedTable::add([ 'CODE' => $blockCode, 'USER_ID' => Manager::getUserId(), 'DATE_CREATE' => new \Bitrix\Main\Type\DateTime ]); } } /** * Removes block by code from last used. * @param string $blockCode Block code. * @return void */ public static function removeAsUsed(string $blockCode): void { $res = Internals\BlockLastUsedTable::getList([ 'select' => [ 'ID' ], 'filter' => [ '=CODE' => $blockCode ] ]); while ($row = $res->fetch()) { Internals\BlockLastUsedTable::delete($row['ID']); } } /** * Returns blocks style manifests from repository. * @return array */ public static function getStyle(): array { return self::getSpecialManifest('style'); } /** * Returns blocks semantic manifests from repository. * @return array */ public static function getSemantic(): array { return self::getSpecialManifest('semantic'); } /** * Returns blocks attrs manifests from repository. * @return array */ public static function getAttrs(): array { return self::getSpecialManifest('attrs'); } /** * Returns blocks style manifest from repository. * @return array */ protected static function getSpecialManifest(string $type): array { static $style = []; if (array_key_exists($type, $style)) { return $style[$type]; } $style[$type] = []; $paths = self::getGeneralPaths(); // read all subdirs ($namespaces) in block dir foreach ($paths as $path) { $path = Manager::getDocRoot() . $path; if (($handle = opendir($path))) { while ((($entry = readdir($handle)) !== false)) { if ( $entry != '.' && $entry != '..' && is_dir($path . '/' . $entry) && file_exists($path . '/' . $entry . '/.' . $type . '.php') ) { $style[$type][$entry] = include $path . '/' . $entry . '/.' . $type . '.php'; if (!is_array($style[$type][$entry])) { unset($style[$type][$entry]); } } } } } return $style[$type]; } /** * Get block content array. * @param int $id Block id. * @param boolean $editMode Edit mode if true. * @param array $params Some params. * @return array */ public static function getBlockContent($id, $editMode = false, array $params = array()) { if (!isset($params['wrapper_show'])) { $params['wrapper_show'] = true; } if ($editMode) { $params['force_unactive'] = true; } $params['skip_system_script'] = true; ob_start(); $id = intval($id); $block = new self($id); $extContent = ''; if (($ext = $block->getExt())) { $extContent = \CUtil::initJSCore($ext, true); $extContent = preg_replace( '#<script(\sdata\-skip\-moving\="true")?>.*?</script>#is', '', $extContent ); } $landing = Landing::createInstance( $block->getLandingId(), [ 'skip_blocks' => true ] ); if ($editMode) { Cache::disableCache(); } $block->view( $editMode, $landing->exist() ? $landing : null, $params ); if ($editMode) { Cache::enableCache(); } $content = ob_get_contents(); $content = self::replaceMetaMarkers($content); if ($landing->exist() && mb_strpos($content, '#crm') !== false) { $replace = Connector\Crm::getReplacesForContent($landing->getSiteId(), false); $content = str_replace( array_keys($replace), array_values($replace), $content ); } ob_end_clean(); if ($block->exist()) { Manager::getApplication()->restartBuffer(); $availableJS = !$editMode || !$block->getRepoId(); $manifest = $block->getManifest(); if ( !isset($manifest['requiredUserAction']) && $block->getRuntimeRequiredUserAction() ) { $manifest['requiredUserAction'] = $block->getRuntimeRequiredUserAction(); } $sections = (array)(($manifest['block']['section']) ?? null); $return = array( 'id' => $id, 'sections' => implode(',', $sections), 'active' => $block->isActive(), 'access' => $block->getAccess(), 'anchor' => $block->getLocalAnchor(), 'php' => mb_strpos($block->getContent(), '<?') !== false, 'designed' => $block->isDesigned(), 'repoId' => $block->repoId ? (int)$block->repoId : null, 'content' => $content, 'content_ext' => $extContent, 'css' => $block->getCSS(), 'js' => $availableJS ? $block->getJS() : array(), 'manifest' => $manifest, 'dynamicParams' => $block->dynamicParams ); if ( $editMode && isset($return['manifest']['requiredUserAction']) ) { $return['requiredUserAction'] = $return['manifest']['requiredUserAction']; } // add ajax initiated assets to output $ajaxAssets = self::getAjaxInitiatedAssets(); $return['js'] = array_merge($return['js'], $ajaxAssets['js']); $return['css'] = array_merge($return['css'], $ajaxAssets['css']); // todo: what about strings, langs? // todo: what about core.js in strings. And etc relative extensions, which already init return $return; } else { return array(); } } /** * Get block anchor. * @param int $id Block id. * @return string */ public static function getAnchor($id) { return 'block' . (int)$id; } /** * Get namespace for block. * @param string $code Code of block. * @return string */ protected static function getBlockNamespace($code) { static $paths = array(); static $namespace = array(); if (!is_string($code)) { return ''; } $code = trim($code); if (isset($paths[$code])) { return $paths[$code]; } $paths[$code] = ''; $namespaces = self::getNamespaces(); $generalPaths = self::getGeneralPaths(); // get first needed block from end foreach (array_reverse($namespaces) as $subdir) { foreach ($generalPaths as $path) { $path = Manager::getDocRoot() . $path; if (file_exists($path . '/' . $subdir . '/' . $code . '/.description.php')) { $paths[$code] = $subdir; break 2; } } } return $paths[$code]; } /** * Get local path for block. * @param string $code Code of block. * @param string $namespace Namespace (optional). * @return string */ protected static function getBlockPath($code, $namespace = null) { if (!is_string($code)) { return ''; } if (strpos($code, '@')) { [$code, ] = explode('@', $code); } if (!$namespace) { $namespace = self::getBlockNamespace($code); } if ($namespace) { return \getLocalPath( self::BLOCKS_DIR . '/' . $namespace . '/' . $code ); } return ''; } /** * Exist or not block in current instance. * @return boolean */ public function exist() { return $this->id > 0; } /** * Get id of the block. * @return int */ public function getId() { return $this->id; } /** * Gets landing id. * @return int */ public function getLandingId() { return $this->lid; } /** * Gets site id (of landing). * @return int */ public function getSiteId() { return $this->siteId; } /** * Get code of the block. * @return string */ public function getCode() { return $this->code; } /** * Get anchor of the block. * @return string */ public function getLocalAnchor() { return $this->anchor; } /** * Get content of the block. * @return string */ public function getContent() { return $this->content; } /** * Get class of block. * @return LandingBlock */ public function getBlockClass() { $class = $this->getClass(); $class = !empty($class) ? $class[0] : null; if ($class !== null) { return $this->includeBlockClass($class); } return null; } /** * Marks block as allowed or not by tariff. * @param bool $mark Mark. * @return void */ public function setAllowedByTariff(bool $mark): void { $this->allowedByTariff = $mark; } /** * Reset content of current block. * @return void */ public function resetContent() { $data = parent::getList([ 'select' => [ 'CONTENT' ], 'filter' => [ 'ID' => $this->id ] ])->fetch(); if ($data) { $this->content = $data['CONTENT']; } } /** * Active or not the block. * @return boolean */ public function isActive() { return $this->active; } /** * Public or not the block. * @return boolean */ public function isPublic() { return $this->public; } /** * Returns true if block was designed by user. * @return bool */ public function isDesigned(): bool { return $this->designed; } /** * Get current access. * @return string */ public function getAccess() { return $this->access; } /** * Set new access to the block. * @param string $letter Access letter. * @return void */ public function setAccess($letter) { if (is_string($letter)) { $this->access = $letter; } } /** * Set active to the block. * @param boolean $active Bool: true or false. * @return boolean */ public function setActive($active) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $this->active = (boolean) $active; return true; } /** * Get repo id, if block from repo. * @return int */ public function getRepoId() { return $this->repoId; } /** * Gets site row. * @return array */ public function getSite() { static $site = null; if ( $site === null && $this->siteId ) { $site = Site::getList(array( 'filter' => array( 'ID' => $this->siteId ) ))->fetch(); } return $site; } /** * Get preview picture of the block. * @return string */ public function getPreview() { $path = self::getBlockPath($this->code); if ($path && file_exists($this->docRoot . '/' . $path . '/' . self::PREVIEW_FILE_NAME)) { return $path . '/' . self::PREVIEW_FILE_NAME; } return ''; } /** * Get error collection * @return \Bitrix\Landing\Error */ public function getError() { return $this->error; } /** * Get class handler for type of node. * * @deprecated * @see Node\Type::getClassName * * @param string $type Type. * @return string */ protected function getTypeClass($type) { return Node\Type::getClassName($type); } /** * Returns additional manifest nodes from content. * @return array */ protected function parseManifest(): array { static $manifests = []; if (!$this->id || !$this->designed) { return []; } if (array_key_exists($this->id, $manifests)) { return $manifests[$this->id]; } $manifests[$this->id] = Block\Designer::parseManifest($this->content); return $manifests[$this->id]; } /** * Checks that current block are designed and adds new manifest parts. * @param array $manifest Current manifest. * @return array */ protected function checkDesignedManifest(array $manifest): array { if (isset($manifest['block']['name'])) { $designerBlockManifest = $this->parseManifest(); if (!empty($designerBlockManifest['nodes'])) { foreach ($designerBlockManifest['nodes'] as $keyNode => $node) { if (isset($manifest['nodes'][$keyNode])) { continue; } $node['code'] = $keyNode; $class = Node\Type::getClassName($node['type']); if (isset($node['type']) && class_exists($class)) { $node['handler'] = call_user_func( [ $class, 'getHandlerJS' ] ); $manifest['nodes'][$keyNode] = $node; } } } if (!empty($designerBlockManifest['style'])) { $manifest['style']['nodes'] = array_merge( $designerBlockManifest['style'], $manifest['style']['nodes'] ); } } return $manifest; } /** * Get manifest array from block. * @param bool $extended Get extended manifest. * @param bool $missCache Don't save in static cache. * @param array $params Additional params. * @return array */ public function getManifest($extended = false, $missCache = false, array $params = array()) { static $manifestStore = array(); if ( !$missCache && isset($manifestStore[$this->code]) ) { if ( !isset($manifestStore[$this->code]['disableCache']) || $manifestStore[$this->code]['disableCache'] !== true ) { return $this->checkDesignedManifest($manifestStore[$this->code]); } } // manifest from market, files, or rest if ($this->repoId) { $manifest = Repo::getBlock($this->repoId); } else if ($path = self::getBlockPath($this->code)) { //isolate variables from .description.php $includeDesc = function($path) { Loc::loadLanguageFile($path . '/.description.php'); $manifest = include $path . '/.description.php'; $manifest['timestamp'] = file_exists($path . '/block.php') ? filectime($path . '/block.php') : time(); return $manifest; }; $manifest = $includeDesc($this->docRoot . $path); } // prepare manifest if (isset($manifest['block']['name'])) { // prepare by subtype if ( isset($manifest['block']['subtype']) && ( !isset($params['miss_subtype']) || $params['miss_subtype'] !== true ) ) { $subtypes = $manifest['block']['subtype']; if (!is_array($subtypes)) { $subtypes = [$subtypes]; } foreach ($subtypes as $subtype) { $subtypeClass = '\\Bitrix\\Landing\\Subtype\\'; $subtypeClass .= $subtype; if (class_exists($subtypeClass)) { $manifest = $subtypeClass::prepareManifest( $manifest, $this, isset($manifest['block']['subtype_params']) ? (array)$manifest['block']['subtype_params'] : array() ); } } } // set empty array if no exists foreach (['cards', 'nodes', 'attrs', 'menu'] as $code) { if (!isset($manifest[$code]) || !is_array($manifest[$code])) { $manifest[$code] = array(); } } // prepare every node foreach ($manifest['nodes'] as $keyNode => &$node) { if (is_callable($node) && !$this->repoId) { $node = $node(); } $node['code'] = $keyNode; $class = Node\Type::getClassName($node['type']); if (isset($node['type']) && class_exists($class)) { $node['handler'] = call_user_func(array( $class, 'getHandlerJS' )); if (method_exists($class, 'prepareManifest')) { $node = call_user_func_array(array( $class, 'prepareManifest' ), array( $this, $node, &$manifest )); if (!is_array($node)) { unset($manifest['nodes'][$keyNode]); } } } else { unset($manifest['nodes'][$keyNode]); } } unset($node); // and attrs foreach ($manifest['attrs'] as $keyNode => &$node) { if (is_callable($node) && !$this->repoId) { $node = $node(); } } unset($node); // callbacks if (isset($manifest['callbacks']) && is_array($manifest['callbacks'])) { $callbacks = array(); foreach ($manifest['callbacks'] as $code => $callback) { $callbacks[mb_strtolower($code)] = $callback; } $manifest['callbacks'] = $callbacks; } // prepare styles if (!isset($manifest['namespace'])) { $manifest['namespace'] = $this->getBlockNamespace($this->code); } if ( isset($manifest['style']) && !( isset($manifest['style']['block']) && isset($manifest['style']['nodes']) && count($manifest['style']) == 2 ) ) { $manifest['style'] = [ 'block' => [], 'nodes' => is_array($manifest['style']) ? $manifest['style'] : [] ]; } elseif ( !isset($manifest['style']) || !is_array($manifest['style']) ) { $manifest['style'] = [ 'block' => [], 'nodes' => [] ]; } // default block types if ( !is_array($manifest['style']['block']) || empty($manifest['style']['block']) ) { $manifest['style']['block'] = ['type' => self::DEFAULT_WRAPPER_STYLE]; } // fake nodes for images from style $styleNodes = []; foreach ($manifest['style']['nodes'] as $selector => $styleNode) { if (!isset($manifest['nodes'][$selector]) && isset($styleNode['type']) && is_array($styleNode)) { $styleNodes[$selector] = is_array($styleNode['type']) ? $styleNode['type'] : [$styleNode['type']]; } } $styleNodes['#wrapper'] = is_array($manifest['style']['block']['type']) ? $manifest['style']['block']['type'] : [$manifest['style']['block']['type']]; foreach ($styleNodes as $selector => $type) { if (!empty(array_intersect($type, Node\StyleImg::STYLES_WITH_IMAGE))) { $manifest['nodes'][$selector] = [ 'type' => Node\Type::STYLE_IMAGE, 'code' => $selector, ]; } } // other $manifest['code'] = $this->code; } else { $manifest = array(); } $manifest['preview'] = $this->getPreview(); if (!$missCache) { $manifestStore[$this->code] = $manifest; } // localization if ( isset($manifest['lang']) && isset($manifest['lang_original']) && is_array($manifest['lang']) ) { // detect translated messages $lang = null; $langPortal = LANGUAGE_ID; if (in_array($langPortal, ['ru', 'kz', 'by'])) { $langPortal = 'ru'; } $langArray = $manifest['lang']; $langOrig = $manifest['lang_original']; if (isset($langArray[$langPortal])) { $lang = $langArray[$langPortal]; } else if ( $langOrig != $langPortal && isset($langArray['en']) ) { $lang = $langArray['en']; } // replace all 'name' keys in manifest if ($lang) { $this->localizationManifest( $manifest, $lang ); } unset($manifest['lang']); } return $this->checkDesignedManifest($manifest); } /** * Localize manifest. * @param array $manifest Manifest array. * @param array $lang Lang array. * @return void */ protected function localizationManifest(array &$manifest, array $lang) { foreach ($manifest as $key => &$value) { if (is_array($value)) { $this->localizationManifest($value, $lang); } if ( $key == 'name' && isset($lang[$value]) ) { $value = $lang[$value]; } } } /** * Get manifest array as is from block. * @param string $code Code name, format "namespace:code" or just "code". * @return array */ public static function getManifestFile($code) { static $manifests = array(); if (!is_string($code)) { return []; } if (preg_match('/[^a-z0-9_.:-]+/i', $code)) { return []; } if (isset($manifests[$code])) { return $manifests[$code]; } $manifests[$code] = array(); $namespace = null; if (mb_strpos($code, ':') !== false) { [$namespace, $code] = explode(':', $code); } if ($path = self::getBlockPath($code ,$namespace)) { $docRoot = Manager::getDocRoot(); Loc::loadLanguageFile($docRoot . $path . '/.description.php'); $manifests[$code] = include $docRoot . $path . '/.description.php'; } return $manifests[$code]; } /** * Get some assets of block. * @param string $type What return: css, js, ext, class. * @return array */ public function getAsset($type = null) { static $asset = array(); if (!is_string($type)) { return []; } if (!isset($asset[$this->code])) { $asset[$this->code] = array( 'css' => array(), 'js' => array(), 'ext' => array(), 'class' => array() ); // additional asset first if ($this->repoId) { $manifest = Repo::getBlock($this->repoId); } else if ($path = self::getBlockPath($this->code)) { $manifest = include $this->docRoot . $path . '/.description.php'; $manifest['timestamp'] = file_exists($this->docRoot . $path . '/.description.php') ? filectime($this->docRoot . $path . '/.description.php') : time(); } if (isset($manifest['block']['namespace'])) { $classFile = self::BLOCKS_DIR; $classFile .= '/' . $manifest['block']['namespace'] . '/'; $classFile .= $this->code . '/class.php'; $classFile = \getLocalPath($classFile); if ($classFile) { $asset[$this->code]['class'][] = $this->docRoot . $classFile; } } // prepare by subtype if ( isset($manifest['block']['subtype']) && ( !isset($params['miss_subtype']) || $params['miss_subtype'] !== true ) ) { $subtypes = $manifest['block']['subtype']; if (!is_array($subtypes)) { $subtypes = [$subtypes]; } foreach ($subtypes as $subtype) { $subtypeClass = '\\Bitrix\\Landing\\Subtype\\'; $subtypeClass .= $subtype; if (class_exists($subtypeClass)) { $manifest = $subtypeClass::prepareManifest( $manifest, $this, isset($manifest['block']['subtype_params']) ? (array)$manifest['block']['subtype_params'] : array() ); } } } foreach (array_keys($asset[$this->code]) as $ass) { if ( isset($manifest['assets'][$ass]) && !empty($manifest['assets'][$ass]) ) { foreach ($manifest['assets'][$ass] as $file) { if (!is_string($file)) { continue; } if ($ass != 'ext') { $asset[$this->code][$ass][] = trim($file); } // for rest block allowed only this else if ( !$this->repoId || in_array($file, $this->allowedExtensions) ) { $asset[$this->code][$ass][] = trim($file); } } $asset[$this->code][$ass] = array_unique($asset[$this->code][$ass]); } } // next is phis files if (isset($path) && $path) { // base files next $file = $path . '/' . ($this->metaData['DESIGNER_MODE'] ? 'design_' : '') . self::CSS_FILE_NAME; if (file_exists($this->docRoot . $file)) { $asset[$this->code]['css'][] = $file; } $file = $path . '/' . self::JS_FILE_NAME; if (file_exists($this->docRoot . $file)) { $asset[$this->code]['js'][] = $file; } } } $designerBlockManifest = $this->parseManifest(); if (!empty($designerBlockManifest['assets'])) { foreach ($designerBlockManifest['assets'] as $key => $assets) { $asset[$this->code][$key] = array_merge($asset[$this->code][$key], $assets); $asset[$this->code][$key] = array_unique($asset[$this->code][$key]); } } return $asset[$this->code][$type] ?? $asset[$this->code]; } /** * Get css file path, if exists. * @return array */ public function getCSS() { return $this->getAsset('css'); } /** * Get js file path, if exists. * @return array */ public function getJS() { return $this->getAsset('js'); } /** * Get extensions. * @return array */ public function getExt() { return $this->getAsset('ext'); } /** * Get executable classes. * @return array */ public function getClass() { return $this->getAsset('class'); } /** * Include class of block. * @param string $path Path of block class. * @return \Bitrix\Landing\LandingBlock */ protected function includeBlockClass($path) { static $classes = []; static $calledClasses = []; if (!isset($classes[$path])) { // include class $beforeClasses = get_declared_classes(); $beforeClassesCount = count($beforeClasses); include_once($path); $afterClasses = get_declared_classes(); $afterClassesCount = count($afterClasses); // ... and detect class name for ($i = $beforeClassesCount; $i < $afterClassesCount; $i++) { if (is_subclass_of($afterClasses[$i], '\\Bitrix\\Landing\\LandingBlock')) { $classes[$path] = $afterClasses[$i]; } } } $landingId = $this->getLandingId(); $landingPath = $path . '@' . $landingId; // call init method if (!isset($calledClasses[$landingPath])) { $calledClasses[$landingPath] = new $classes[$path]; $calledClasses[$landingPath]->init([ 'site_id' => $this->getSiteId(), 'landing_id' => $this->getLandingId() ]); } return $calledClasses[$landingPath]; } /** * Gets message string. * @param array $params Component's params. * @param string $template Template name. * @return string */ protected static function getMessageBlock($params, $template = '') { ob_start(); Manager::getApplication()->includeComponent( 'bitrix:landing.blocks.message', $template, $params, false ); $blockMesage = ob_get_contents(); ob_end_clean(); return $blockMesage; } /** * Out the block. * @param boolean $edit Out block in edit mode. * @param Landing|null $landing Landing of this block. * @param array $params Some params. * @return void */ public function view($edit = false, \Bitrix\Landing\Landing $landing = null, array $params = array()) { global $APPLICATION; if ($this->dynamicParams) { $this->setDynamic($edit); } if (!isset($params['wrapper_show'])) { $params['wrapper_show'] = true; } if (!isset($params['force_unactive'])) { $params['force_unactive'] = false; } if (!isset($params['skip_system_script'])) { $params['skip_system_script'] = false; } if ( !$edit && $params['wrapper_show'] && !Config::get('public_wrapper_block') ) { $params['wrapper_show'] = false; } if ($this->deleted) { return; } if ($edit || $this->active || $params['force_unactive']) { $assets = Assets\Manager::getInstance(); if ($css = $this->getCSS()) { $assets->addAsset($css, Assets\Location::LOCATION_TEMPLATE); } if ($ext = $this->getExt()) { $assets->addAsset($ext, Assets\Location::LOCATION_TEMPLATE); } if (!$edit || !$this->repoId) { if ($js = $this->getJS()) { $assets->addAsset($js, Assets\Location::LOCATION_AFTER_TEMPLATE); } } // calling class(es) of block foreach ($this->getClass() as $class) { $classBlock = $this->includeBlockClass($class); if ($classBlock->beforeView($this) === false) { return; } } } // get manifest if ($edit && !$params['skip_system_script']) { $manifest = $this->getManifest(); } // develop mode - rebuild and reset content if ( $this->id > 0 && !$params['skip_system_script'] && defined('LANDING_DEVELOPER_MODE') && LANDING_DEVELOPER_MODE === true ) { if (!isset($manifest)) { $manifest = $this->getManifest(); } if (isset($this->metaData['DATE_MODIFY'])) { $modifyTime = $this->metaData['DATE_MODIFY']->getTimeStamp(); } else { $modifyTime = 0; } if ($modifyTime < $manifest['timestamp']) { $count = 0; $limit = 1; Update\Block::executeStep([ 'ID' => $this->id ], $count, $limit, $paramsUpdater = []); $this->resetContent(); $this->content = $this->getContent(); } } if (!\Bitrix\Main\ModuleManager::isModuleInstalled('bitrix24')) { if (mb_strpos($this->content, '/upload/') !== false) { $this->content = preg_replace( '#"//[^\'^"]+/upload/#', '"/upload/', $this->content ); } if (Manager::getOption('badpicture2x') == 'Y') { if (mb_strpos($this->content, 'srcset="') !== false) { $this->content = str_replace( 'srcset="', 'data-srcset-bad="', $this->content ); } if (mb_strpos($this->content, '2x)') !== false) { $this->content = preg_replace( "#(, url\('[^'^\"]+'\) 2x)#", '', $this->content ); } } } // show or not a wrapper of block if ($params['wrapper_show']) { if ($this->id > 0) { if ($edit) { $anchor = $this->getAnchor($this->id); } else { $anchor = $this->anchor ? \htmlspecialcharsbx($this->anchor) : $this->getAnchor($this->id); } } else { $anchor = 'block' . rand(10000, 100000); } $classFromCode = 'block-' . $this->code; $classFromCode = preg_replace('/([^a-z0-9-])/i', '-', $classFromCode); $classFromCode = ' ' . $classFromCode; $content = ''; if ($landing && $landing->getPreviewMode()) { $content .= "<a id=\"editor{$this->getId()}\"></a>"; } $content .= '<div id="' . $anchor . '" ' . ($edit ? 'data-id="' . $this->id . '" ' : '') . (($edit && isset($manifest['block']['subtype'])) ? 'data-subtype="' . $manifest['block']['subtype'] . '" ' : '') . 'class="block-wrapper' . (!$this->active ? ' landing-block-deactive' : '') . ($this->metaData['DESIGNER_MODE'] ? ' landing-designer-block-mode' : '') . $classFromCode . '">' . $this->content . '</div>'; } else { $content = $this->content; } // replace in requisite page, marker to company title if (mb_strpos($this->content, '#requisiteCompanyTitle') !== false) { $replace = Connector\Crm::getReplaceRequisiteCompanyNameForContent($landing->getXmlId()); $content = str_replace( array_keys($replace), array_values($replace), $content ); } // @tmp bug with setInnerHTML save result $content = preg_replace('/&([^\s]{1})/is', '&$1', $content); if ($edit) { if ($manifest ?? null) { if ( !isset($manifest['requiredUserAction']) && $this->runtimeRequiredUserAction ) { $manifest['requiredUserAction'] = $this->runtimeRequiredUserAction; } $sections = (array)($manifest['block']['section'] ?? null); $designerRepository = $this->metaData['DESIGNER_MODE'] ? \Bitrix\Landing\Block\Designer::getRepository() : []; $anchor = $this->anchor; if (!$anchor) { $anchor = $this->parentId ? 'block' . $this->parentId : 'b' . $this->id; } $autoPublicationEnabled = Site\Type::isPublicScope() && \CUserOptions::getOption('landing', 'auto_publication', 'Y') === 'Y'; echo '<script>' . 'BX.ready(function(){' . 'if (typeof BX.Landing.Block !== "undefined")' . '{' . 'new BX.Landing.' . ($this->metaData['DESIGNER_MODE'] ? 'DesignerBlock' : 'Block') . '(' . 'BX("block' . $this->id . '"), ' . '{' . 'id: ' . $this->id . ', ' . 'lid: ' . $this->lid . ', ' . 'code: "' . $this->code . '", ' . 'sections: "' . implode(',', $sections) . '", ' . 'repoId: ' . ($this->repoId ? (int)$this->repoId : "null") . ', ' . 'php: ' . (mb_strpos($content, '<?') !== false ? 'true' : 'false') . ', ' . 'designed: ' . ($this->designed ? 'true' : 'false') . ', ' . 'active: ' . ($this->active ? 'true' : 'false') . ', ' . 'allowedByTariff: ' . ($this->allowedByTariff ? 'true' : 'false') . ', ' . 'autoPublicationEnabled: ' . ($autoPublicationEnabled ? 'true' : 'false') . ', ' . 'anchor: ' . '"' . \CUtil::jsEscape($anchor) . '"' . ', ' . 'access: ' . '"' . $this->access . '"' . ', ' . 'dynamicParams: ' . Json::encode($this->dynamicParams) . ',' . ($this->metaData['DESIGNER_MODE'] ? 'repository: ' . Json::encode($designerRepository) . ',' : '') . 'manifest: ' . Json::encode($manifest) . ( isset($manifest['requiredUserAction']) ? ', requiredUserAction: ' . Json::encode($manifest['requiredUserAction']) : '' ) . '}' . ');' . '}' . '});' . '</script>'; } $content = $this::replaceMetaMarkers($content); $event = new \Bitrix\Main\Event('landing', 'onBlockEditView', [ 'block' => $this, 'outputContent' => $content ]); $event->send(); foreach ($event->getResults() as $result) { $content = $result->getParameters(); } if ($this->repoId) { echo $content; } else { try { eval('?>' . $content . '<?'); } catch (\ParseError $e) { $errMessage = $this::getMessageBlock([ 'MESSAGE' => Loc::getMessage('LANDING_BLOCK_MESSAGE_ERROR_EVAL') ]); if ($params['wrapper_show']) { echo '<div id="' . $anchor . '" class="block-wrapper' . (!$this->active ? ' landing-block-deactive' : '') . '">' . $errMessage . '</div>'; } else { echo $errMessage; } } } } elseif ($this->active || $params['force_unactive']) { // @todo make better static $sysPagesSites = []; if (!array_key_exists($this->siteId, $sysPagesSites)) { $sysPages = array(); foreach (Syspage::get($this->siteId) as $syspage) { $sysPages['@#system_' . $syspage['TYPE'] . '@'] = $syspage['LANDING_ID']; } // for main page we should get current site main page if (!isset($sysPages['@#system_mainpage@'])) { $currentSite = $this->getSite(); if ($currentSite['LANDING_ID_INDEX']) { $sysPages['@#system_mainpage@'] = $currentSite['LANDING_ID_INDEX']; } } if (!empty($sysPages)) { $urls = $landing->getPublicUrl($sysPages); foreach ($sysPages as $code => $lid) { if (isset($urls[$lid])) { $sysPages[$code] = \htmlspecialcharsbx($urls[$lid]); } else { unset($sysPages[$code]); } } } $sysPagesSites[$this->siteId] = $sysPages; } $sysPages = $sysPagesSites[$this->siteId]; if (!Connector\Mobile::isMobileHit()) { $sysPages['@' . Connector\Disk::FILE_MASK_HREF . '@i'] = str_replace( '#fileId#', '$2', Controller\DiskFile::getDownloadLink($this->metaData['SITE_TYPE'], $this->id) ); } if (!empty($sysPages)) { $content = preg_replace( array_keys($sysPages), array_values($sysPages), $content ); } $event = new \Bitrix\Main\Event('landing', 'onBlockPublicView', [ 'block' => $this, 'outputContent' => $content ]); $event->send(); foreach ($event->getResults() as $result) { $content = $result->getParameters(); } if ($this->repoId) { echo $content; } else { try { eval('?>' . $content . '<?'); } catch (\ParseError $e) { } } } Assets\PreProcessing::blockViewProcessing($this, $edit); } /** * Save assets to the block. * @param array $assets New assets array. * @return void */ public function saveAssets(array $assets): void { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return; } foreach (['font', 'icon', 'ext'] as $assetCode) { if (isset($this->assets[$assetCode]) && !isset($assets[$assetCode])) { $assets[$assetCode] = $this->assets[$assetCode]; } if (isset($assets[$assetCode]) && !$assets[$assetCode]) { unset($assets[$assetCode]); } } $this->assets = $assets; } /** * Returns the block assets. * @return array */ public function getAssets(): array { return $this->assets; } /** * Set new content. * @param string $content New content. * @param bool $designed Content was designed. * @return void */ public function saveContent(string $content, $designed = false): void { if (!is_string($content)) { return; } if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return; } if ($designed) { $this->designed = true; } $this->content = trim($content); $this->getDom(true); } /** * Save current block in DB. * @param array $additionalFields Additional fields for saving. * @return boolean */ public function save(array $additionalFields = []) { if ($this->access == $this::ACCESS_A) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $data = array( 'SORT' => $this->sort, 'ACTIVE' => $this->active ? 'Y' : 'N', 'ANCHOR' => $this->anchor, 'DELETED' => $this->deleted ? 'Y' : 'N', 'DESIGNED' => $this->designed ? 'Y' : 'N', 'ASSETS' => $this->assets ? $this->assets : null ); if ($additionalFields) { $data = array_merge($data, $additionalFields); } if ($this->content) { $data['CONTENT'] = $this->content; $data['SEARCH_CONTENT'] = $this->getSearchContent(); } Cache::clear($this->id); $res = parent::update($this->id, $data); $this->error->addFromResult($res); return $res->isSuccess(); } /** * Change landing of current block. * @param int $lid New landing id. * @return boolean * */ public function changeLanding($lid) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $res = parent::update($this->id, array( 'LID' => (int)$lid, 'PARENT_ID' => null, 'PUBLIC' => 'N' )); $this->error->addFromResult($res); return $res->isSuccess(); } /** * Set meta information for favorite block. * @param array $meta Meta information. * @return bool */ public function changeFavoriteMeta(array $meta): bool { $res = parent::update($this->id, [ 'TPL_CODE' => $meta['tpl_code'] ?? null, 'FAVORITE_META' => $meta ]); $this->error->addFromResult($res); return $res->isSuccess(); } /** * Delete current block. * @return boolean */ public function unlink() { if ($this->access < $this::ACCESS_X) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $manifest = $this->getManifest(); $res = self::parentDelete($this->id); if (!$res->isSuccess()) { $this->error->addFromResult($res); } return $res->isSuccess(); } /** * Mark delete or not current block. * @param boolean $mark Mark. * @return void */ public function markDeleted($mark) { if ($this->access < $this::ACCESS_X) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return; } $this->deleted = (boolean) $mark; } /** * Set new sort to current block. * @param int $sort New sort. * @return void */ public function setSort($sort) { $this->sort = $sort; } /** * Set new anchor to current block. * @param string $anchor New anchor. * @return boolean */ public function setAnchor($anchor) { if (!is_string($anchor)) { return false; } $anchor = trim($anchor); $check = !$anchor || preg_match_all('/^[a-z]{1}[a-z0-9\-\_\.\:]+$/i', $anchor); if (!$check) { $this->error->addError( 'BAD_ANCHOR', Loc::getMessage('LANDING_BLOCK_BAD_ANCHOR') ); return false; } if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('CHANGE_ANCHOR', [ 'block' => $this, 'valueBefore' => $this->anchor, 'valueAfter' => $anchor, ]); } $this->anchor = $anchor; return true; } /** * Save new sort to current block to DB. * @param int $sort New sort. * @return void */ public function saveSort($sort) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return; } $sort = intval($sort); $this->sort = $sort; Internals\BlockTable::update($this->id, array( 'SORT' => $sort )); } /** * Get sort of current block. * @return int */ public function getSort() { return $this->sort; } /** * Gets dynamic source params. * @param int $id Not current block id. * @return array */ public function getDynamicParams($id = null) { $params = []; if ($id !== null) { $id = intval($id); $res = parent::getList([ 'select' => [ 'SOURCE_PARAMS' ], 'filter' => [ 'ID' => $id ] ]); if ($row = $res->fetch()) { $params = $row['SOURCE_PARAMS']; } unset($row, $res); } else { $params = $this->dynamicParams; } return $params; } /** * @param array $data * @param array $replace * @return array */ private function dynamicLinkReplacer(array $data, array $replace) { foreach ($data as $key => $value) { if (is_array($value)) { $data[$key] = $this->dynamicLinkReplacer($value, $replace); } else { $data[$key] = str_replace( array_keys($replace), array_values($replace), $data[$key] ); } } unset($key, $value); return $data; } /** * Save dynamic params for the block. * @param array $sourceParams Source params. * @param array $params Additional params. * @return void */ public function saveDynamicParams(array $sourceParams = [], array $params = []) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return; } // replace old link to new in dynamic manifest if ( isset($params['linkReplace']) && is_array($params['linkReplace']) ) { $sourceParams = $this->dynamicLinkReplacer( $sourceParams, $params['linkReplace'] ); } // save $paramsBefore = $this->dynamicParams; $this->dynamicParams = $sourceParams; $resUpdate = Internals\BlockTable::update($this->id, [ 'SOURCE_PARAMS' => $sourceParams ]); if ($resUpdate->isSuccess()) { if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('UPDATE_DYNAMIC', [ 'block' => $this, 'valueBefore' => $paramsBefore, 'valueAfter' => $sourceParams, ]); } } unset($sourceParams, $params); } /** * Build dynamic content for the block. * @param bool $edit Edit mode. * @return void */ protected function setDynamic($edit) { static $sourceList = null; static $isDetailDynamic = null; static $dynamicElementId = null; static $dynamicFilter = null; $data = $this->dynamicParams; $caching = false; $cache = null; // check if is true dynamic if (!$this->active || !$this->content) { return; } if (!is_array($data) || empty($data)) { return; } // check feature $availableFeature = Restriction\Manager::isAllowed( 'limit_sites_dynamic_blocks', ['targetBlockId' => $this->id] ); if (!$availableFeature) { $hackContent = preg_replace( '/^<([a-z]+)\s/', '<$1 style="display: none;" ', $this->content ); $this->saveContent( $hackContent . $this::getMessageBlock([ 'HEADER' => Loc::getMessage('LANDING_BLOCK_MESSAGE_ERROR_DYNAMIC_LIMIT_TITLE'), 'MESSAGE' => Restriction\Manager::getSystemErrorMessage('limit_sites_dynamic_blocks'), 'BUTTON' => Loc::getMessage('LANDING_BLOCK_MESSAGE_ERROR_LIMIT_BUTTON'), 'LINK' => Manager::BUY_LICENSE_PATH ], 'locked') ); return; } // if is detail page if ($isDetailDynamic === null) { $isDetailDynamic = Landing::isDynamicDetailPage(); } if ($dynamicElementId === null) { $dynamicElementId = Landing::getDynamicElementId(); } if ($dynamicFilter === null) { $dynamicFilter = Landing::getDynamicFilter(); } if (!$edit && Cache::isCaching()) { $cache = new \CPHPCache(); $cacheTime = 3600; $cacheId = 'block_' . $this->id . '_' . $dynamicElementId . '_'; $cacheId .= md5(serialize($dynamicFilter)); $cachePath = '/landing/dynamic/' . $this->id; if ($cache->initCache($cacheTime, $cacheId, $cachePath)) { $result = $cache->getVars(); if ($result['title']) { Manager::setPageTitle($result['title'], true); Landing\Seo::changeValue('title', $result['title']); } $rememberAccess = $this->access; $this->access = $this::ACCESS_W; $this->saveContent($result['content']); $this->access = $rememberAccess; header('X-Bitrix24-Page: dynamic'); return; } else { $caching = true; $cache->startDataCache($cacheTime, $cacheId, $cachePath); Manager::getCacheManager()->startTagCache($cachePath); Cache::register($this->id); } } $updated = false; // @todo: remove after refactoring $manifest = $this->getManifest(); // build sources list if ($sourceList === null) { $sourceList = new Source\Selector(); } // @todo: remove after refactoring $getDetailPage = function(array $detailPage, $filterId = 0, $elemId = 0) { $filterId = intval($filterId); $elemId = intval($elemId); $query = []; if (isset($detailPage['query'])) { $query = (array) $detailPage['query']; unset($detailPage['query']); } // normalize the array $detailPage = array_merge( array_fill_keys(['text', 'href', 'target'], ''), $detailPage ); foreach ($detailPage as $key => &$detailPageItem) { if (!is_array($detailPageItem)) { $detailPageItem = trim($detailPageItem); } if (empty($detailPageItem)) { unset($detailPage[$key]); } } unset($detailPageItem); if ($filterId && $elemId && $detailPage['href']) { $detailPage['href'] = str_replace( '#landing', '#dynamic', $detailPage['href'] ); $detailPage['href'] .= '_' . $filterId; $detailPage['href'] .= '_' . $elemId; } else if ($filterId && $elemId) { $detailPage['href'] = '#'; } if ($detailPage['href'] && $query) { $detailPage['query'] = http_build_query($query); } return $detailPage; }; // apply for each selector dynamic data from source $disableUpdate = false; $pageTitle = ''; foreach ($data as $cardSelector => $item) { $update = []; $itemDetail = $cardSelector == 'wrapper'; if ( !isset($item['source']) || !isset($item['settings']) || !isset($item['references']) ) { continue; } // build start params $sourceId = $item['source']; $settings = $item['settings']; $references = (array)$item['references']; $filterId = isset($item['filterId']) ? intval($item['filterId']) : 0; $detailPage = isset($settings['detailPage']) ? (array)$settings['detailPage'] : []; $pagesCount = ( isset($settings['pagesCount']) && $settings['pagesCount'] > 0 ) ? (int)$settings['pagesCount'] : 10; $filter = isset($settings['source']['filter']) ? (array)$settings['source']['filter'] : []; $order = isset($settings['source']['sort']) ? (array)$settings['source']['sort'] : []; $additional = isset($settings['source']['additional']) ? (array)$settings['source']['additional'] : []; $stubs = isset($item['stubs']) ? (array)$item['stubs'] : []; // load external filter, if we on detail if ( $isDetailDynamic && $itemDetail && $dynamicFilter['SOURCE_ID'] == $sourceId ) { $filter = $dynamicFilter['FILTER']; } $sourceParameters = [ 'select' => array_values($references), 'filter' => $filter, 'order' => $order, 'limit' => $pagesCount, 'additional' => $additional ]; // gets list or singleton data $sourceData = []; $source = $sourceList->getDataLoader( $sourceId, $sourceParameters, [ 'context_filter' => [ 'SITE_ID' => $this->siteId, 'LANDING_ID' => $this->lid, 'LANDING_ACTIVE' => $this->landingActive ? 'Y' : ['Y', 'N'] ], 'cache' => $cache, 'block' => $this ] ); if (is_object($source)) { // detail page if ($isDetailDynamic && $itemDetail) { $sourceData = $source->getElementData($dynamicElementId); if (!$sourceData) { $disableUpdate = true; continue; } $pageTitle = $source->getSeoTitle(); Manager::setPageTitle($pageTitle, true); Landing\Seo::changeValue('title', $pageTitle); } // element list else { $sourceData = $source->getElementListData(); $pagesCount = max(1, count($sourceData)); } } // apply getting data in block if (!empty($sourceData) && is_array($sourceData)) { // collect array for update html foreach ($references as $selector => $field) { if (empty($field) || !is_array($field)) { continue; } if (empty($field['id'])) { continue; } if (mb_strpos($selector, '@') !== false) { [$selector,] = explode('@', $selector); } if (!isset($update[$selector])) { $update[$selector] = []; } $fieldCode = $field['id']; $fieldType = isset($manifest['nodes'][$selector]['type']) ? $manifest['nodes'][$selector]['type'] : NodeType::TEXT; // fill ever selector with data, if data exist $detailPageData = []; foreach ($sourceData as $dataItem) { // set link to the card // @todo: need refactoring if ( $fieldType == NodeType::LINK && isset($field['action']) ) { switch ($field['action']) { case 'detail': { $detailPage['text'] = isset($field['text']) ? $field['text'] : ''; $update[$selector][] = $detailPageData[$selector][] = $getDetailPage( $detailPage, $filterId, $dataItem['ID'] ); break; } case 'link': { if (isset($field['link'])) { $field['link'] = (array) $field['link']; if (isset($field['text'])) { $field['link']['text'] = $field['text']; } $update[$selector][] = $getDetailPage( $field['link'] ); } break; } case 'landing': { if (isset($dataItem['LINK'])) { $update[$selector][] = $detailPageData[$selector][] = $getDetailPage([ 'text' => isset($field['text']) ? $field['text'] : '', 'href' => $dataItem['LINK'], 'target' => '_self', 'query' => isset($dataItem['_GET']) ? $dataItem['_GET'] : [] ]); } } } } else// if ($fieldType != NodeType::LINK) { $value = isset($dataItem[$fieldCode]) ? $dataItem[$fieldCode] : ''; $update[$selector][] = $value; if ($detailPage) { $detailPageData[$selector][] = $getDetailPage( $detailPage, $filterId, $dataItem['ID'] );; } else if (isset($dataItem['LINK'])) { $detailPageData[$selector][] = $getDetailPage([ 'text' => isset($field['text']) ? $field['text'] : '', 'href' => $dataItem['LINK'], 'target' => '_self', 'query' => isset($dataItem['_GET']) ? $dataItem['_GET'] : [] ]); } } } // not touch the selector, if there is no data if (!$update[$selector]) { unset($update[$selector]); } // set detail url for nodes // @todo: refactor else if ( isset($field['link']) && ( $fieldType == NodeType::IMAGE || $fieldType == NodeType::TEXT ) ) { if (!isset($detailPageData[$selector])) { continue; } foreach ($update[$selector] as $i => &$value) { if ($fieldType == NodeType::IMAGE) { $value = (array) $value; } else { $value = [ 'text' => (string) $value ]; } if ( $detailPageData[$selector][$i] && UtilsAction::isTrue($field['link']) ) { $detailPageData[$selector][$i]['enabled'] = true; } else { $detailPageData[$selector][$i]['enabled'] = false; } if ($detailPageData[$selector][$i]['enabled']) { $value['url'] = $detailPageData[$selector][$i]; } } unset($value); } } if (!$itemDetail) { $rememberAccess = $this->access; $this->access = $this::ACCESS_W; $this->adjustCards( $cardSelector, $pagesCount ); $this->access = $rememberAccess; } } // stubs (common content) if ($stubs) { foreach ($stubs as $selector => $stub) { if (mb_strpos($selector, '@') !== false) { [$selector,] = explode('@', $selector); } $update[$selector] = array_fill(0, $pagesCount, $stub); } } // update dynamic if ($update) { $updated = true; $rememberAccess = $this->access; $this->access = $this::ACCESS_W; $this->updateNodes( $update, [ 'sanitize' => false, 'skipCheckAffected' => true ] ); if(!$edit) { Assets\PreProcessing::blockSetDynamicProcessing($this); } $this->access = $rememberAccess; header('X-Bitrix24-Page: dynamic'); if ($caching) { $cache->endDataCache([ 'title' => $pageTitle, 'content' => $this->content ]); Manager::getCacheManager()->endTagCache(); } } else if (false) { $this->runtimeRequiredUserAction = [ 'header' => Loc::getMessage('LANDING_BLOCK_MESSAGE_ERROR_NO_DATA_TITLE'), 'description' => Loc::getMessage('LANDING_BLOCK_MESSAGE_ERROR_NO_DATA_TEXT') ]; } } if ( $disableUpdate || (!$updated && !Landing::getEditMode()) ) { if ($cache) { $cache->abortDataCache(); } $this->deleted = true; } } /** * Make block not dynamic. * @return void */ public function clearDynamic() { $this->saveDynamicParams(); } /** * Gets only runtime required actions. * @return array */ public function getRuntimeRequiredUserAction(): array { return $this->runtimeRequiredUserAction; } /** * Set only runtime required actions. * @param array $action */ public function setRuntimeRequiredUserAction(array $action): void { $this->runtimeRequiredUserAction = $action; } /** * Load current content in DOM html structure. * @param bool $clear CLear static cache. * @return DOM\Document */ public function getDom($clear = false) { static $doc = array(); if ( $clear && isset($doc[$this->id]) ) { unset($doc[$this->id]); } if (!isset($doc[$this->id])) { $doc[$this->id] = new DOM\Document; try { $doc[$this->id]->loadHTML($this->content); } catch (\Exception $e) {} } return $doc[$this->id]; } /** * Get metadata of current block. * @return array */ public function getMeta() { return $this->metaData; } /** * Adjust cards count by selector. * @param string $selector Selector. * @param int $count Needed cards count. * @param bool &$changed Changed. * @return boolean Success or failure. */ public function adjustCards($selector, $count, &$changed = false) { if (!is_string($selector)) { return false; } if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $manifest = $this->getManifest(); if (isset($manifest['cards'][$selector])) { $count = (int)$count; $doc = $this->getDom(); $resultList = $doc->querySelectorAll($selector); $resultCount = count($resultList); if ($count > $resultCount) { for ($i = $resultCount; $i < $count; $i++) { $changed = true; $this->cloneCard($selector, $i - 1); } } elseif ($count < $resultCount) { for ($i = $resultCount; $i > $count; $i--) { $changed = true; $this->removeCard($selector, $i - 1); } } return true; } $this->error->addError( 'CARD_NOT_FOUND', Loc::getMessage('LANDING_BLOCK_CARD_NOT_FOUND') ); return false; } /** * Clone one card in block by selector. * @param string $selector Selector. * @param int $position Card position. * @param string $content New content for cloned card. * @return boolean Success or failure. */ public function cloneCard($selector, $position, $content = '') { if (!is_string($selector)) { return false; } if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $manifest = $this->getManifest(); if (isset($manifest['cards'][$selector])) { $position = intval($position); $position = max($position, -1); $realPosition = max($position, 0); $doc = $this->getDom(); $resultList = $doc->querySelectorAll($selector); if (isset($resultList[$realPosition])) { $parentNode = $resultList[$realPosition]->getParentNode(); $refChild = isset($resultList[$position + 1]) ? $resultList[$position + 1] : null; $haveChild = false; if ($refChild) { foreach ($parentNode->getChildNodes() as $child) { if ($child === $refChild) { $haveChild = true; break; } } } if ($parentNode && (!$refChild || $haveChild)) { // some dance for set new content ;) if ($content) { $tmpCardName = mb_strtolower('tmpcard'.randString(10)); $newChild = new DOM\Element($tmpCardName); $newChild->setOwnerDocument($doc); $newChild->setInnerHTML($content); } else { $newChild = $resultList[$realPosition]; } $parentNode->insertBefore( $newChild, $refChild, false ); // history before save content if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('ADD_CARD', [ 'block' => $this, 'selector' => $selector, 'position' => $position, 'content' => $content, ]); } // cleaning and save if (isset($tmpCardName)) { $this->saveContent( str_replace( array('<' . $tmpCardName . '>', '</' . $tmpCardName . '>'), '', $doc->saveHTML() ) ); } else { $this->saveContent($doc->saveHTML()); } } return true; } } $this->error->addError( 'CARD_NOT_FOUND', Loc::getMessage('LANDING_BLOCK_CARD_NOT_FOUND') ); return false; } /** * Set card content from block by selector. * @param string $selector Selector. * @param int $position Card position. * @param string $content New content. * @return boolean Success or failure. */ public function setCardContent($selector, $position, $content) { if (!is_string($selector)) { return false; } if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $doc = $this->getDom(); $position = intval($position); $resultList = $doc->querySelectorAll($selector); if (isset($resultList[$position])) { $resultList[$position]->setInnerHTML( $content ); $this->saveContent($doc->saveHTML()); return true; } $this->error->addError( 'CARD_NOT_FOUND', Loc::getMessage('LANDING_BLOCK_CARD_NOT_FOUND') ); return false; } /** * Gets card content from block by selector. * @param string $selector Selector. * @param int $position Card position. * @return string */ public function getCardContent($selector, $position) { if (!is_string($selector)) { return ''; } $doc = $this->getDom(); $position = intval($position); $resultList = $doc->querySelectorAll($selector); if (isset($resultList[$position])) { return $resultList[$position]->getOuterHtml(); } return null; } /** * Gets count of cards from block by selector. * @param string $selector Selector. * @return int */ public function getCardCount($selector) { if (!is_string($selector)) { return 0; } $doc = $this->getDom(); $resultList = $doc->querySelectorAll($selector); return count($resultList); } /** * Remove one card from block by selector. * @param string $selector Selector. * @param int $position Card position. * @return boolean Success or failure. */ public function removeCard($selector, $position) { if (!is_string($selector)) { return false; } if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $manifest = $this->getManifest(); $position = intval($position); if (isset($manifest['cards'][$selector])) { $doc = $this->getDom(); $resultList = $doc->querySelectorAll($selector); if (isset($resultList[$position])) { $resultList[$position]->getParentNode()->removeChild( $resultList[$position] ); // history before save! if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('REMOVE_CARD', [ 'block' => $this, 'selector' => $selector, 'position' => $position, ]); } $this->saveContent($doc->saveHTML()); Assets\PreProcessing::blockUpdateNodeProcessing($this); return true; } } $this->error->addError( 'CARD_NOT_FOUND', Loc::getMessage('LANDING_BLOCK_CARD_NOT_FOUND') ); return false; } /** * Set new names for nodes of block. * @param array $data Nodes data array. * @return boolean */ public function changeNodeName($data) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $doc = $this->getDom(); $manifest = $this->getManifest(); $valueBefore = []; // find available nodes by manifest from data foreach ($manifest['nodes'] as $selector => $node) { if (isset($data[$selector])) { $resultList = $doc->querySelectorAll($selector); foreach ($data[$selector] as $pos => $value) { $value = trim($value['tagName'] ?? $value); if ( preg_match('/^[a-z0-9]+$/i', $value) && isset($resultList[$pos])) { $valueBefore[$selector][$pos] = $resultList[$pos]->getNodeName(); $resultList[$pos]->setNodeName($value); } } } } if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('CHANGE_NODE_NAME_ACTION', [ 'block' => $this, 'valueBefore' => $valueBefore, 'valueAfter' => $data, ]); } // save rebuild html as text $this->saveContent($doc->saveHTML()); return true; } /** * Set new content to nodes of block. * @param array $data Nodes data array. * @param array $additional Additional prams for save. * @return boolean */ public function updateNodes($data, $additional = array()) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $affected = []; $doc = $this->getDom(); $manifest = $this->getManifest(); // find available nodes by manifest from data $manifest['nodes'] = $manifest['nodes'] ?? []; foreach ($manifest['nodes'] as $selector => $node) { $isFind = false; $dataSelector = []; if (isset($data[$selector])) { if (!is_array($data[$selector])) { $data[$selector] = array( $data[$selector] ); } $dataSelector = $data[$selector]; $isFind = true; } if (!$isFind && ($node['isWrapper'] ?? false) === true) { if (isset($data['#wrapper']) && $node['type'] === 'styleimg') { if (!is_array($data['#wrapper'])) { $data['#wrapper'] = array( $data['#wrapper'] ); } $dataSelector = $data['#wrapper']; } else { $selector = '#wrapper'; if (!is_array($data[$selector])) { $data[$selector] = array( $data[$selector] ); } $dataSelector = $data[$selector]; } $isFind = true; } if ($node['type'] === 'img') { $node = Img::changeNodeType($node, $this); } if ($isFind) { // and save content from frontend in DOM by handler-class $affected[$selector] = call_user_func_array(array( Node\Type::getClassName($node['type']), 'saveNode' ), array( $this, $selector, $dataSelector, $additional )); } } // additional work with menu if (isset($additional['appendMenu']) && $additional['appendMenu']) { $export = $this->export(); } else { $additional['appendMenu'] = false; } $manifest['menu'] = $manifest['menu'] ?? []; foreach ($manifest['menu'] as $selector => $node) { if (isset($data[$selector]) && is_array($data[$selector])) { if (isset($data[$selector][0][0])) { $data[$selector] = array_shift($data[$selector]); } if ($additional['appendMenu'] && isset($export['menu'][$selector])) { $data[$selector] = array_merge( $export['menu'][$selector], $data[$selector] ); } $resultList = $doc->querySelectorAll($selector); foreach ($resultList as $pos => $resultNode) { $parentNode = $resultNode->getParentNode(); if ($parentNode) { $parentNode->setInnerHtml( $this->getMenuHtml( $data[$selector], $node ) ); } break;// we need only first position } } } // save rebuild html as text $this->saveContent($doc->saveHTML()); // check affected content in block's content if (!($additional['skipCheckAffected'] ?? false) && Manager::getOption('strict_verification_update') === 'Y') { $pos = 0; $domCorrect = true; $content = $this->content; foreach ($affected as $selector => $resultItem) { $selector = trim($selector, '.'); // prepare content for search $content = str_replace('class="', 'class=" ', $content); $content = preg_replace_callback( '/class="[^"]*[\s]+(' . $selector . ')[\s"]+[^"]*"[^>]*>/s', function($match) use(&$pos) { return str_replace($match[1], $match[1] . '@' . ($pos++), $match[0]); }, $content ); if (is_array($resultItem)) { foreach ($resultItem as $pos => $affectedItem) { if ($affectedItem['content'] ?? null) { $affectedItem['content'] = str_replace('/', '\/', $affectedItem['content']); $mask = '/class="[^"]*[\s]+' . $selector . '@' . $pos . '[\s"]+[^"]*"[^>]*>' . $affectedItem['content'] . '<\//s'; $domCorrect = preg_match_all($mask, $content); if (!$domCorrect) { break 2; } } } } } if (!$domCorrect) { $this->error->addError( 'INCORRECT_AFFECTED', Loc::getMessage('LANDING_BLOCK_INCORRECT_AFFECTED') ); return false; } } Assets\PreProcessing::blockUpdateNodeProcessing($this); return true; } /** * Returns menu html with child submenu. * @param array $data Data array. * @param array $manifestNode Manifest node for current selector. * @param string $level Level (root or children). * @return string */ protected function getMenuHtml($data, $manifestNode, $level = 'root') { if (!is_array($data) || !isset($manifestNode[$level])) { return ''; } $htmlContent = ''; $rootSelector = $manifestNode[$level]; if ( isset($rootSelector['ulClassName']) && isset($rootSelector['liClassName']) && isset($rootSelector['aClassName']) && is_string($rootSelector['ulClassName']) && is_string($rootSelector['liClassName']) && is_string($rootSelector['aClassName']) ) { foreach ($data as $menuItem) { if ( isset($menuItem['text']) && is_string($menuItem['text']) && isset($menuItem['href']) && is_string($menuItem['href']) ) { if ($menuItem['href'] === 'page:#landing0') { $res = Landing::addByTemplate( $this->getSiteId(), Assets\PreProcessing\Theme::getNewPageTemplate($this->getSiteId()), [ 'TITLE' => $menuItem['text'] ] ); if ($res->isSuccess()) { $menuItem['href'] = '#landing' . $res->getId(); } } if (isset($menuItem['target']) && is_string($menuItem['target'])) { $target = $menuItem['target']; } else { $target = '_self'; } $htmlContent .= '<li class="' . \htmlspecialcharsbx($rootSelector['liClassName']) . '">'; $htmlContent .= '<a href="' . \htmlspecialcharsbx($menuItem['href']) . '" target="' . $target . '" class="' . \htmlspecialcharsbx($rootSelector['aClassName']) . '">'; $htmlContent .= \htmlspecialcharsbx($menuItem['text']); $htmlContent .= '</a>'; if (isset($menuItem['children'])) { $htmlContent .= $this->getMenuHtml( $menuItem['children'], $manifestNode, 'children' ); } $htmlContent .= '</li>'; } } if ($htmlContent) { $htmlContent = '<ul class="' . \htmlspecialcharsbx($rootSelector['ulClassName']) . '">' . $htmlContent . '</ul>'; } else if ($level == 'root') { $htmlContent = '<ul class="' . \htmlspecialcharsbx($rootSelector['ulClassName']) . '"></ul>'; } } return $htmlContent; } /** * Change cards multiple. * @param array $data Array with cards. * @return boolean */ public function updateCards(array $data = array()) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $manifest = $this->getManifest(); foreach ($data as $selector => $item) { $cardManifest = $manifest['cards'][$selector]; // first gets content of current cards $cardContent = array(); $cardCount = $this->getCardCount($selector); for ($i = 0; $i < $cardCount; $i++) { $cardContent[$i] = $this->getCardContent( $selector, $i ); } // then fill all cards by content from existing cards and presets if ( isset($item['source']) && is_array($item['source']) ) { $newContent = array(); foreach ($item['source'] as $i => $source) { $type = isset($source['type']) ? $source['type'] : self::CARD_SYM_CODE; $value = isset($source['value']) ? $source['value'] : 0; // clone card if ( $type == self::CARD_SYM_CODE && isset($cardContent[$value]) ) { $newContent[$i] = $cardContent[$value]; } // clone preset else if ( $type == 'preset' && isset($cardManifest['presets'][$value]['html']) ) { $newContent[$i] = $cardManifest['presets'][$value]['html']; } else { $newContent[$i] = ''; } } $newContent = trim(implode('', $newContent)); if ($newContent) { $dom = $this->getDom(); $resultList = $dom->querySelectorAll($selector); if (isset($resultList[0])) { $resultList[0]->getParentNode()->setInnerHtml( $newContent ); } $this->saveContent( $dom->saveHTML() ); } } // and finally update content cards if ( isset($item['values']) && is_array($item['values']) ) { $updNodes = array(); foreach ($item['values'] as $upd) { if (is_array($upd)) { foreach ($upd as $sel => $content) { if(mb_strpos($sel, '@')) { [$sel, $pos] = explode('@', $sel); } if (!isset($updNodes[$sel])) { $updNodes[$sel] = array(); } $updNodes[$sel][$pos] = $content; } } } if (!empty($updNodes)) { $this->updateNodes($updNodes); } } } return true; } /** * Recursive styles remove in Node. * @param \Bitrix\Main\Web\DOM\Node $node Node for clear. * @param array $styleToRemove Array of styles to remove. * @return \Bitrix\Main\Web\DOM\Node */ protected function removeStyle(DOM\Node $node, array $styleToRemove) { foreach ($node->getChildNodesArray() as $nodeChild) { if ($nodeChild instanceof DOM\Element) { $styles = DOM\StyleInliner::getStyle($nodeChild, false); if (!empty($styles)) { foreach ($styleToRemove as $remove) { if (!is_array($remove)) { $remove = [$remove => $remove]; } $styles = array_diff_key($styles, $remove); } DOM\StyleInliner::setStyle($nodeChild, $styles); } } $node = $this->removeStyle($nodeChild, $styleToRemove); } return $node; } /** * Set new classes to nodes of block. * @param array $data Classes data array. * @return boolean */ public function setClasses($data) { if ($this->access < $this::ACCESS_V) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return false; } $doc = $this->getDom(); $manifest = $this->getManifest(); // detects position $positions = []; $position = -1; foreach ((array)$data as $selector => $item) { if (mb_strpos($selector, '@') !== false) { [$selector, $position] = explode('@', $selector); } else { $position = -1; } if ($selector === '#wrapper') { $selector = '#block' . $this->id; } if ($position >= 0) { if (!isset($positions[$selector])) { $positions[$selector] = []; } $positions[$selector][] = (int)$position; } $data[$selector] = $item; } // wrapper (not realy exist) $wrapper = '#' . $this->getAnchor($this->id); // find available nodes by manifest from data $styles = $manifest['style']['nodes']; $styles[$wrapper] = $manifest['style']['block']; foreach ($styles as $selector => $node) { if (isset($data[$selector])) { // prepare data if (!is_array($data[$selector])) { $data[$selector] = [ $data[$selector] ]; } if (!isset($data[$selector]['classList'])) { $data[$selector] = [ 'classList' => $data[$selector] ]; } if (!isset($data[$selector]['affect'])) { $data[$selector]['affect'] = []; } // apply classes to the block if ($selector === $wrapper) { $nodesArray = $doc->getChildNodesArray(); $resultList = [array_pop($nodesArray)]; } // or by selector else { $resultList = $doc->querySelectorAll($selector); } foreach ($resultList as $pos => $resultNode) { $relativeSelector = $selector; if (isset($positions[$selector])) { if (!in_array($pos, $positions[$selector], true)) { continue; } $relativeSelector .= '@' . $pos; } $contentBefore = $resultNode->getOuterHTML(); if ($resultNode) { if ((int)$resultNode->getNodeType() === $resultNode::ELEMENT_NODE) { $resultNode->setClassName( implode(' ', $data[$relativeSelector]['classList']) ); } // affected styles if (!empty($data[$relativeSelector]['affect'])) { $this->removeStyle( $resultNode, $data[$relativeSelector]['affect'] ); } // inline styles if (!empty($data[$relativeSelector]['style'])) { $stylesInline = DOM\StyleInliner::getStyle($resultNode, false); DOM\StyleInliner::setStyle( $resultNode, array_merge($stylesInline, $data[$relativeSelector]['style']) ); } else if (preg_match_all('/background-image:.*;/i', $resultNode->getAttribute('style'), $matches)) { $resultNode->removeAttribute('style'); $resultNode->setAttribute('style', implode('', $matches[0])); } if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('EDIT_STYLE', [ 'block' => $this, 'selector' => $selector, 'isWrapper' => ($selector === $wrapper), 'position' => $position >= 0 ? (int)$pos : -1, 'affect' => $data[$relativeSelector]['affect'], 'contentBefore' => $contentBefore, 'contentAfter' => $resultNode->getOuterHTML(), ]); } } } } } // save rebuild html as text $this->saveContent($doc->saveHTML()); Assets\PreProcessing::blockUpdateClassesProcessing($this); return true; } /** * Collects and returns allowed attributes ([selector] => [data-test, data-test2]). * @param string $selector Selector, if attr have't own selector. * @param array &$allowed Array for collecting. * @return void */ protected static function collectAllowedAttrs(array $mixed, array &$allowed, $selector = null) { foreach ($mixed as $itemSelector => $item) { if (!is_string($itemSelector)) { $itemSelector = $selector; } if ( isset($item['attrs']) && is_array($item['attrs']) ) { self::collectAllowedAttrs($item['attrs'], $allowed, $itemSelector); } else if ( isset($item['additional']['attrsType']) || $itemSelector === 'additional' ) { $manifestAttrs = self::getAttrs(); $attrs = $manifestAttrs['bitrix']['attrs']; if (is_array($item['additional']['attrsType'])) { foreach ($attrs as $attr) { $allowed[$itemSelector][] = $attr['attribute']; } } if (is_array($item['attrsType'])) { foreach ($attrs as $attr) { $allowed['#wrapper'][] = $attr['attribute']; } } } else if ( isset($item['additional']['attrs']) && is_array($item['additional']['attrs']) ) { self::collectAllowedAttrs($item['additional']['attrs'], $allowed, $itemSelector); } else if ( isset($item['additional']) && is_array($item['additional']) ) { self::collectAllowedAttrs($item['additional'], $allowed, $itemSelector); } else if ( isset($item['attribute']) && is_string($item['attribute']) ) { if ( isset($item['selector']) && is_string($item['selector']) ) { $itemSelector = trim($item['selector']); } if ($itemSelector) { if (!isset($allowed[$itemSelector])) { $allowed[$itemSelector] = []; } $allowed[$itemSelector][] = $item['attribute']; } } else if (is_array($item)) { self::collectAllowedAttrs($item, $allowed, $itemSelector); } } } /** * Set attributes to nodes of block. * @param array $data Attrs data array. * @return void */ public function setAttributes($data) { if ($this->access < $this::ACCESS_W) { $this->error->addError( 'ACCESS_DENIED', Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED') ); return; } $doc = $this->getDom(); $manifest = $this->getManifest(); $wrapper = '#' . $this->getAnchor($this->id); // collect allowed attrs $allowedAttrs = []; self::collectAllowedAttrs($manifest['style']['nodes'], $allowedAttrs); self::collectAllowedAttrs($manifest['attrs'], $allowedAttrs); self::collectAllowedAttrs($manifest['cards'], $allowedAttrs); self::collectAllowedAttrs($manifest['style']['block'], $allowedAttrs); // update attrs if ($allowedAttrs) { // all allowed attrs from manifest with main selector ([selector] => [data-test, data-test2]) foreach ($allowedAttrs as $selector => $allowed) { // it's not interesting for us, if there is no new data for this selector if ((isset($data[$selector]) && is_array($data[$selector])) || isset($data[$wrapper]) ) { // set attrs to the block if ($selector === '#wrapper') { $selector = $wrapper; } if ($selector == $wrapper) { $nodesArray = $doc->getChildNodesArray(); $resultList = [array_pop($nodesArray)]; } // or by selector else { $resultList = $doc->querySelectorAll($selector); } // external data for changing in allowed attrs foreach ($data[$selector] as $attrKey => $attrData) { // if key without position (compatibility) if (!($attrKey == (string)(int)$attrKey)) { $attrData = [$attrKey => $attrData]; $attrKey = -1; } if (!is_array($attrData)) { continue; } // attrs new data in each selector ([data-test] => value) foreach ($attrData as $key => $value) { if (!in_array($key, $allowed)) { continue; } $key = \htmlspecialcharsbx($key); $value = is_array($value) ? json_encode($value) : $value; // result nodes by main selector foreach ($resultList as $pos => $resultNode) { // if position of node that we try to find if ($attrKey == -1 || $attrKey == $pos) { $valueBefore = $resultNode->getAttribute($key); // update node $resultNode->setAttribute($key, $value); if (History::isActive()) { $history = new History($this->getLandingId(), History::ENTITY_TYPE_LANDING); $history->push('EDIT_ATTRIBUTES', [ 'block' => $this, 'selector' => $selector, 'isWrapper' => ($selector === $wrapper), 'attribute' => $key, 'position' => (int)$attrKey, 'valueBefore' => $valueBefore, 'valueAfter' => $value, ]); } } } } } } } } // save result $this->saveContent($doc->saveHTML()); } /** * Replace title and breadcrumb marker in the block. * @param string $content Some content. * @return string */ protected static function replaceMetaMarkers($content) { if (mb_strpos($content, '#breadcrumb#') !== false) { ob_start(); $arResult = array( array( 'LINK' => '#', 'TITLE' => '' ), array( 'LINK' => '#', 'TITLE' => Loc::getMessage('LANDING_BLOCK_BR1') ), array( 'LINK' => '#', 'TITLE' => Loc::getMessage('LANDING_BLOCK_BR2') ), array( 'LINK' => '#', 'TITLE' => '' ) ); $tplId = Manager::getTemplateId( Manager::getMainSiteId() ); $strChainTemplate = getLocalPath('templates/' . $tplId . '/chain_template.php'); $strChainTemplate = Manager::getDocRoot() . $strChainTemplate; if (file_exists($strChainTemplate)) { echo include $strChainTemplate; } $breadcrumb = ob_get_contents(); ob_end_clean(); $content = str_replace( '#breadcrumb#', $breadcrumb, $content ); } if (mb_strpos($content, '#title#') !== false) { $content = str_replace( '#title#', Loc::getMessage('LANDING_BLOCK_TITLE'), $content ); } return $content; } /** * Delete all blocks from db by codes. * @param array $code Array of codes to delete. * @return void */ public static function deleteByCode($code) { if (!is_array($code)) { $code = array($code); } $res = parent::getList(array( 'select' => array( 'ID' ), 'filter' => array( '=CODE' => $code ) )); while ($row = $res->fetch()) { self::parentDelete($row['ID']); } } /** * Delete block row. * @param int $id Block id. * @return \Bitrix\Main\Result */ private static function parentDelete($id) { return parent::delete($id); } /** * Delete all blocks for the landing. * @param int $lid Landing id. * @return void */ public static function deleteAll($lid) { $res = parent::getList([ 'select' => [ 'ID' ], 'filter' => [ 'LID' => (int)$lid ] ]); while ($row = $res->fetch()) { parent::delete($row['ID']); } } /** * Returns search content for this block. * @return string */ public function getSearchContent() { $manifest = $this->getManifest(); $search = []; // get content nodes if (isset($manifest['nodes'])) { foreach ($manifest['nodes'] as $selector => $node) { /** @var Node $class */ $class = NodeType::getClassName($node['type']); if (is_callable([$class, 'getSearchableNode'])) { $search = array_merge($search, $class::getSearchableNode($this, $selector)); } } } return $search ? implode(' ', $search) : ''; } /** * Export nodes, style, attrs, etc. from block. * @param array $params Some params. * @return array */ public function export(array $params = []) { $manifest = $this->getManifest(); $doc = $this->getDom(); $cards = []; $nodes = []; $menu = []; $styles = []; $allAttrs = []; // prepare params if (!isset($params['clear_form'])) { $params['clear_form'] = true; } // get actual cards content if (isset($manifest['cards'])) { foreach ($manifest['cards'] as $selector => $node) { $cards[$selector] = [ 'source' => [] ]; $resultList = $doc->querySelectorAll($selector); $resultListCnt = count($resultList); foreach ($resultList as $pos => $result) { $cards[$selector]['source'][$pos] = array( 'value' => $result->getAttribute('data-card-preset'), 'type' => Block::PRESET_SYM_CODE ); if (!$cards[$selector]['source'][$pos]['value']) { //@tmp for menu first item if (mb_strpos($this->getCode(), 'menu') !== false) { $cards[$selector]['source'][$pos]['value'] = $resultListCnt > 0 ? 1 : 0; } else { $cards[$selector]['source'][$pos]['value'] = 0; } $cards[$selector]['source'][$pos]['type'] = Block::CARD_SYM_CODE; } } // attrs if ( isset($node['additional']['attrs']) && is_array($node['additional']['attrs']) ) { foreach ($node['additional']['attrs'] as $attr) { if (isset($attr['attribute'])) { if (!isset($allAttrs[$selector])) { $allAttrs[$selector] = []; } $allAttrs[$selector][] = $attr['attribute']; } } } } } // get content nodes if (isset($manifest['nodes'])) { foreach ($manifest['nodes'] as $selector => $node) { /** @var Node $class */ $class = NodeType::getClassName($node['type']); $nodes[$selector] = $class::getNode($this, $selector); } } if (isset($manifest['menu'])) { // recursive getting menu $exportMenu = function($resultList) use(&$exportMenu) { if(!$resultList) { return []; } $menu = []; foreach ($resultList->getChildNodesArray() as $pos => $node) { $menu[$pos] = []; if ($node->getNodeName() == 'LI') { foreach ($node->getChildNodesArray() as $nodeInner) { if ($nodeInner->getNodeName() == 'A') { $menu[$pos]['text'] = trim($nodeInner->getTextContent()); $menu[$pos]['href'] = trim($nodeInner->getAttribute('href')); $menu[$pos]['target'] = trim($nodeInner->getAttribute('target')); } else if ($nodeInner->getNodeName() == 'UL') { $menu[$pos]['children'] = $exportMenu($nodeInner); } } } if (!$menu[$pos]) { unset($menu[$pos]); } } return array_values($menu); }; foreach ($manifest['menu'] as $selector => $menuNode) { $menu[$selector] = $exportMenu($doc->querySelector($selector)); } } // get actual css from nodes if (isset($manifest['style']['nodes'])) { foreach ($manifest['style']['nodes'] as $selector => $node) { $nodeStyle = Node\Style::getStyle($this, $selector); if ($nodeStyle) { $styles[$selector] = $nodeStyle; } // attrs if ( isset($node['additional']['attrs']) && is_array($node['additional']['attrs']) ) { foreach ($node['additional']['attrs'] as $attr) { if (isset($attr['attribute'])) { if (!isset($allAttrs[$selector])) { $allAttrs[$selector] = []; } $allAttrs[$selector][] = $attr['attribute']; } } } } } // get actual css from block wrapper if (!empty($manifest['style']['block'])) { $selector = '#wrapper'; $wrapperStyle = Node\Style::getStyle($this, $selector); if ($wrapperStyle) { $styles[$selector] = $wrapperStyle; } } // attrs if ( isset($manifest['style']['block']['additional']['attrs']) && is_array($manifest['style']['block']['additional']['attrs']) ) { $selector = '#wrapper'; foreach ($manifest['style']['block']['additional']['attrs'] as $attr) { if (isset($attr['attribute'])) { if (!isset($allAttrs[$selector])) { $allAttrs[$selector] = []; } $allAttrs[$selector][] = $attr['attribute']; } } } // get actual attrs from nodes if (isset($manifest['attrs'])) { foreach ($manifest['attrs'] as $selector => $item) { if (isset($item['attribute'])) { if (!isset($allAttrs[$selector])) { $allAttrs[$selector] = []; } $allAttrs[$selector][] = $item['attribute']; } else if (is_array($item)) { foreach ($item as $itemAttr) { if (isset($itemAttr['attribute'])) { if (!isset($allAttrs[$selector])) { $allAttrs[$selector] = []; } $allAttrs[$selector][] = $itemAttr['attribute']; } } } } } // remove some system attrs if ( $params['clear_form'] && isset($allAttrs['.bitrix24forms']) ) { unset($allAttrs['.bitrix24forms']); } // collect attrs $allAttrsNew = []; if (isset($allAttrs['#wrapper'])) { $allAttrsNew['#wrapper'] = []; $nodesArray = $doc->getChildNodesArray(); $resultList = [array_pop($nodesArray)]; foreach ($resultList as $pos => $result) { foreach ($allAttrs['#wrapper'] as $attrKey) { if (!isset($allAttrsNew['#wrapper'][$pos])) { $allAttrsNew['#wrapper'][$pos] = []; } $allAttrsNew['#wrapper'][$pos][$attrKey] = $result->getAttribute($attrKey); } } unset($allAttrs['#wrapper']); } foreach ($allAttrs as $selector => $attr) { $resultList = $doc->querySelectorAll($selector); foreach ($resultList as $pos => $result) { if (!isset($allAttrsNew[$selector])) { $allAttrsNew[$selector] = []; } if (!isset($allAttrsNew[$selector][$pos])) { $allAttrsNew[$selector][$pos] = []; } foreach ($attr as $attrKey) { $allAttrsNew[$selector][$pos][$attrKey] = $result->getAttribute($attrKey); } unset($attrVal); } } $allAttrs = $allAttrsNew; unset($allAttrsNew); return [ 'cards' => $cards, 'nodes' => $nodes, 'menu' => $menu, 'style' => $styles, 'attrs' => $allAttrs, 'dynamic' => $this->dynamicParams ]; } /** * Search in blocks. * @param string $query Query string. * @param array $filter Filter array. * @param array $select Select fields. * @param array $group Group fields. * @return array */ public static function search($query, array $filter = [], array $select = ['LID'], array $group = ['LID']) { $result = []; $filter['*%SEARCH_CONTENT'] = $query; $filter['=DELETED'] = 'N'; $res = Internals\BlockTable::getList([ 'select' => $select, 'filter' => $filter, 'group' => $group, 'order' => ['SORT' => 'desc'] ]); while ($row = $res->fetch()) { $result[] = $row; } return $result; } /** * Add block row. * @param array $fields Block data. * @return \Bitrix\Main\Result */ public static function add($fields) { if ( !defined('LANDING_MUTATOR_MODE') || LANDING_MUTATOR_MODE !== true ) { throw new \Bitrix\Main\SystemException( 'Disabled for direct access.' ); } else { return parent::add($fields); } } /** * Update block row. * @param int $id Primary key. * @param array $fields Block data. * @return \Bitrix\Main\Result */ public static function update($id, $fields = array()) { if ( !defined('LANDING_MUTATOR_MODE') || LANDING_MUTATOR_MODE !== true ) { throw new \Bitrix\Main\SystemException( 'Disabled for direct access.' ); } else { return parent::update($id, $fields); } } /** * Delete block row. * @param int $id Primary key. * @return \Bitrix\Main\Result */ public static function delete($id) { if ( !defined('LANDING_MUTATOR_MODE') || LANDING_MUTATOR_MODE !== true ) { throw new \Bitrix\Main\SystemException( 'Disabled for direct access.' ); } else { return parent::delete($id); } } /** * Returns all favorites blocks. * @param string|null $tplCode Page template code. * @return array */ public static function getFavorites(?string $tplCode): array { return parent::getList([ 'filter' => [ 'LID' => 0, '=DELETED' => 'N', '=TPL_CODE' => $tplCode ], 'order' => [ 'ID' => 'asc' ] ])->fetchAll(); } /** * Gets block's rows. * @param array $fields Block orm data. * @return \Bitrix\Main\DB\Result */ public static function getList($fields = array()) { if ( !defined('LANDING_MUTATOR_MODE') || LANDING_MUTATOR_MODE !== true ) { throw new \Bitrix\Main\SystemException( 'Disabled for direct access.' ); } else { return parent::getList($fields); } } /** * In ajax hit may be initiated some assets (JS extensions), but will not be added on page. * We need get them all and add to output. * @return array * @throws \Bitrix\Main\ArgumentNullException * @throws \Bitrix\Main\ArgumentOutOfRangeException */ protected static function getAjaxInitiatedAssets() { Asset::getInstance()->getJs(); Asset::getInstance()->getCss(); Asset::getInstance()->getStrings(); $targetTypeList = array('JS', 'CSS'); $CSSList = $JSList = $stringsList = []; foreach ($targetTypeList as $targetType) { $targetAssetList = Asset::getInstance()->getTargetList($targetType); foreach ($targetAssetList as $targetAsset) { $assetInfo = Asset::getInstance()->getAssetInfo($targetAsset['NAME'], \Bitrix\Main\Page\AssetMode::ALL); if (!empty($assetInfo['JS'])) { $JSList = array_merge($JSList, $assetInfo['JS']); } if (!empty($assetInfo['CSS'])) { $CSSList = array_merge($CSSList, $assetInfo['CSS']); } if (!empty($assetInfo['STRINGS'])) { $stringsList = array_merge($stringsList, $assetInfo['STRINGS']); } } } return [ 'js' => array_unique($JSList), 'css' => array_unique($CSSList), 'strings' => array_unique($stringsList), ]; } /** * Returns true if block's content contains needed string. * * @param int $entityId Block or landing id. * @param string $needed String for search. * @param bool $isLanding Set to true, if entity id is landing id. * @return bool */ public static function isContains(int $entityId, string $needed, bool $isLanding = false): bool { $filter = [ '=ACTIVE' => 'Y', '=DELETED' => 'N', 'CONTENT' => '%' . $needed . '%', ]; if ($isLanding) { $filter['LID'] = $entityId; } else { $filter['ID'] = $entityId; } $res = parent::getList([ 'select' => [ 'LID', 'SITE_ID' => 'LANDING.SITE_ID', ], 'filter' => $filter, ]); if ($row = $res->fetch()) { $res = Landing::getList([ 'select' => [ 'ID' ], 'filter' => [ 'ID' => $row['LID'] ] ]); if ($res->fetch()) { return true; } if (\Bitrix\Landing\Site\Scope\Group::getGroupIdBySiteId($row['SITE_ID'], true)) { return true; } } return false; } /** * Temporary function for check components, when includeModule check is not enough * @return bool */ public static function checkComponentExists(string $componentName): bool { $path2Component = \CComponentEngine::MakeComponentPath($componentName); if ($path2Component !== '') { $componentPath = getLocalPath("components" . $path2Component); $componentFile = $_SERVER["DOCUMENT_ROOT"] . $componentPath . "/component.php"; return file_exists($componentFile) && is_file($componentFile); } return false; } }