![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/old/vendor/mageworx/module-seocrosslinks/Model/ |
<?php /** * Copyright © 2016 MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\SeoCrossLinks\Model; use MageWorx\SeoCrossLinks\Model\ResourceModel\Catalog\ProductFactory; use MageWorx\SeoCrossLinks\Model\ResourceModel\Catalog\CategoryFactory; use MageWorx\SeoCrossLinks\Helper\Data as HelperData; use MageWorx\SeoCrossLinks\Helper\StoreUrl as HelperStoreUrl; use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\ManagerInterface as EventManagerInterface; class Replacer { /** * @var \MageWorx\SeoCrossLinks\Model\ResourceModel\Catalog\ProductFactory */ protected $productFactory; /** * @var \MageWorx\SeoCrossLinks\Model\ResourceModel\Catalog\CategoryFactory */ protected $categoryFactory; /** * @var \MageWorx\SeoCrossLinks\Helper\Data */ protected $helperData; /** * @var \MageWorx\SeoCrossLinks\Helper\StoreUrl */ protected $helperStoreUrl; /** * @var \Magento\Framework\UrlInterface */ protected $url; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $storeManager; /** * * @var array */ protected $productDataList; /** * * @var array */ protected $categoryDataList; /** * * @var array */ protected $landingpageDataList; /** @var EventManagerInterface */ protected $eventManager; /** @var \Psr\Log\LoggerInterface */ protected $logger; /** * The replaced pairs before converting. * The order is important. * * @var array */ protected $exceptFromConvert = array( '&' => '!%24#amp#%24!', '& ' => '!$#a#$!' ); /** * Replacer constructor. * * @param EventManagerInterface $eventManager * @param ProductFactory $productFactory * @param CategoryFactory $categoryFactory * @param HelperData $helperData * @param HelperStoreUrl $helperStoreUrl * @param UrlInterface $url * @param StoreManagerInterface $storeManager */ public function __construct( \Psr\Log\LoggerInterface $logger, EventManagerInterface $eventManager, ProductFactory $productFactory, CategoryFactory $categoryFactory, HelperData $helperData, HelperStoreUrl $helperStoreUrl, UrlInterface $url, StoreManagerInterface $storeManager ) { $this->logger = $logger; $this->eventManager = $eventManager; $this->productFactory = $productFactory; $this->categoryFactory = $categoryFactory; $this->helperData = $helperData; $this->helperStoreUrl = $helperStoreUrl; $this->url = $url; $this->storeManager = $storeManager; } /** * Replace keywords to links in html * * @param $collection * @param $html * @param $maxReplaceCount * @param null $ignoreProductSku * @param null $ignoreCategoryId * @param null $ignoreLandingPageId * @return bool|string */ public function replace( $collection, $html, $maxReplaceCount, $ignoreProductSku = null, $ignoreCategoryId = null, $ignoreLandingPageId = null ) { if (!trim($html)) { return false; } $preparedHtml = $this->_prepareBeforeConvert($html); $dom = new \DOMDocument(); $dom->preserveWhiteSpace = false; libxml_use_internal_errors(true); $dom->loadHTML($preparedHtml); libxml_clear_errors(); $textParts = array(); $xpath = new \DOMXPath($dom); $domNodeList = $xpath->query('//text()[not(ancestor::script)][not(ancestor::a)]'); foreach ($domNodeList as $node) { if ($node->nodeType === 3) { $textParts[] = $node->wholeText; } } if (!$collection->getSize()) { return false; } $this->_specifyCollection($collection, $textParts, $maxReplaceCount); $pairs = array(); $textPartsMod = $this->_dispatchByDestination( $collection, $textParts, $maxReplaceCount, $ignoreProductSku, $ignoreCategoryId, $ignoreLandingPageId, $pairs ); foreach ($domNodeList as $node) { if ($node->nodeType !== 3) { continue; } $replace = array_shift($textPartsMod); $newNode = $dom->createDocumentFragment(); $newNode->textContent = $replace; if (is_object($node->parentNode)) { $node->parentNode->replaceChild($newNode, $node); } } $convertedHtml = $dom->saveHTML(); if (!$convertedHtml) { return false; } $modifyHtml = str_replace(array_keys($pairs), array_values($pairs), $convertedHtml); return $this->_recoveryAfterConvert($modifyHtml); } /** * * @param string $html * @return string */ protected function _cropWrapper(string $html): string { return $this->getTagContentFromHtml($html, 'style', true) . $this->getTagContentFromHtml($html, 'body'); } /** * @param string $html * @param string $tag * @param bool $includeTag * @return string */ protected function getTagContentFromHtml(string $html, string $tag, bool $includeTag = false): string { $openingTag = "<$tag>"; $closingTag = "</$tag>"; $posStart = mb_strpos($html, $openingTag); $posEnd = mb_strpos($html, $closingTag); if ($posStart === false || $posEnd === false) { return ''; } $start = $includeTag ? $posStart : $posStart + strlen($openingTag); $length = $includeTag ? $posEnd + strlen($closingTag) - $start : $posEnd - $start; return mb_substr($html, $start, $length); } /** * Replaces certain characters * * @param type $html * @return type */ protected function _prepareBeforeConvert($html) { $html = mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); return str_replace(array_keys($this->exceptFromConvert), array_values($this->exceptFromConvert), $html); } /** * Recovers the characters replaced earlier * * @param string $html * @return string */ protected function _recoveryAfterConvert($html) { $croppedHtml = $this->_cropWrapper($html); return str_replace(array_values($this->exceptFromConvert), array_keys($this->exceptFromConvert), $croppedHtml); } /** * Delegate replacements if URL exists * * @param \MageWorx\SeoCrossLinks\Model\ResourceModel\Crosslink\Collection $collection * @param array $textParts * @param int $maxGlobalCount * @param string $ignoreProductSku * @param int $ignoreCategoryId * @param array $pairs * @return array */ protected function _dispatchByDestination( $collection, $textParts, $maxGlobalCount, $ignoreProductSku, $ignoreCategoryId, $ignoreLandingPageId, &$pairs ) { foreach ($collection as $crosslink) { if (!$maxGlobalCount) { continue; } if ($crosslink->getRefProductSku()) { $productUrlData = $this->_getProductData($collection, $crosslink, $ignoreProductSku); if ($productUrlData) { $textParts = $this->_preliminaryReplaceAndCreateReplacementPairs( $textParts, $crosslink, $productUrlData['url'], $productUrlData['name'], $maxGlobalCount, $pairs ); } } elseif ($crosslink->getRefCategoryId()) { $categoryUrlData = $this->_getCategoryData($collection, $crosslink, $ignoreCategoryId); if ($categoryUrlData) { $textParts = $this->_preliminaryReplaceAndCreateReplacementPairs( $textParts, $crosslink, $categoryUrlData['url'], $categoryUrlData['name'], $maxGlobalCount, $pairs ); } } elseif ($crosslink->getRefStaticUrl()) { $staticUrl = $this->_getStaticUrl($crosslink); if ($staticUrl) { $textParts = $this->_preliminaryReplaceAndCreateReplacementPairs( $textParts, $crosslink, $staticUrl, false, $maxGlobalCount, $pairs ); } } elseif ($crosslink->getRefLandingpageId()) { $landingPageData = $this->_getLandingpageData($collection, $crosslink, $ignoreLandingPageId); if ($landingPageData) { $textParts = $this->_preliminaryReplaceAndCreateReplacementPairs( $textParts, $crosslink, $landingPageData['url'], $landingPageData['header'], $maxGlobalCount, $pairs ); } } } return $textParts; } /** * Retrive list of modified text parts ( ...keyword... => ...hash... ) * Fill $pairs array (hash => URL) * * * @param array $textParts * @param \MageWorx\SeoCrossLinks\Model\Crosslink $crosslink * @param string $url * @param int $maxGlobalCount * @param array $pairs * @return array */ protected function _preliminaryReplaceAndCreateReplacementPairs($textParts, $crosslink, $url, $name, &$maxGlobalCount, &$pairs) { $replaceCount = 0; if ($crosslink->getKeywords()) { foreach ($crosslink->getKeywords() as $keyword) { $availableCount = 1; if ($maxGlobalCount == 0) { continue ; } $pattern = $this->_getReplacePattern($keyword); $href = $this->_getHtmlHref($crosslink, $keyword, $url, $name); for ($i= 0; $i < count($textParts); $i++) { if ($maxGlobalCount == 0) { break 2; } $key = substr(hash('md5', rand()), 0, 7); $res = preg_replace($pattern, $key, $textParts[$i], $availableCount, $replaceCount); if ($res && $replaceCount) { $pairs[$key] = $href; $availableCount -= $replaceCount; $maxGlobalCount -= $replaceCount; $textParts[$i] = $res; break; } } } } else { $keyword = $crosslink->getKeyword(); $availableCount = min(array($maxGlobalCount, $crosslink->getReplacementCount())); $pattern = $this->_getReplacePattern($keyword); $href = $this->_getHtmlHref($crosslink, $keyword, $url, $name); for ($i= 0; $i < count($textParts); $i++) { $key = substr(hash('md5', rand()), 0, 7); $res = preg_replace($pattern, $key, $textParts[$i], $availableCount, $replaceCount); if ($res && $replaceCount) { $pairs[$key] = $href; $availableCount -= $replaceCount; $maxGlobalCount -= $replaceCount; $textParts[$i] = $res; } } } return $textParts; } /** * Retrive product data (URL, name) if it is not current URL/product * * @param \MageWorx\SeoCrossLinks\Model\ResourceModel\Crosslink\Collection $collection * @param \MageWorx\SeoCrossLinks\Model\Crosslink $crosslink * @param string $ignoreProductSku * @return string */ protected function _getProductData($collection, $crosslink, $ignoreProductSku) { if (is_null($this->productDataList)) { $prodSkuList = array(); foreach ($collection as $item) { if ($item->getRefProductSku() && $item->getRefProductSku() != $ignoreProductSku) { $prodSkuList[] = $item->getRefProductSku(); } } $store = $this->storeManager->getStore(); $isUseName = ($this->helperData->isUseNameForTitle() != \MageWorx\SeoCrossLinks\Model\CrossLink::USE_CROSSLINK_TITLE_ONLY ); $this->productDataList = $this->productFactory->create()->getCollection($prodSkuList, $store, $isUseName); } if (!empty($this->productDataList[$crosslink->getRefProductSku()]['url']) && !$this->_isCurrentUrl($this->productDataList[$crosslink->getRefProductSku()]['url']) ) { return $this->productDataList[$crosslink->getRefProductSku()]; } return false; } /** * Retrive category data (URL, name) if it is not current URL/category * * @param \MageWorx\SeoCrossLinks\Model\ResourceModel\Crosslink\Collection $collection * @param \MageWorx\SeoCrossLinks\Model\Crosslink $crosslink * @param int $ignoreCategoryId * @return string */ protected function _getCategoryData($collection, $crosslink, $ignoreCategoryId) { if (is_null($this->categoryDataList)) { $catIds = array(); foreach ($collection as $item) { if ($item->getRefCategoryId() && $item->getRefCategoryId() != $ignoreCategoryId) { $catIds[] = $item->getRefCategoryId(); } } $store = $this->storeManager->getStore(); $isUseName = ($this->helperData->isUseNameForTitle() != \MageWorx\SeoCrossLinks\Model\CrossLink::USE_CROSSLINK_TITLE_ONLY ); $this->categoryDataList = $this->categoryFactory->create()->getCollection($catIds, $store, $isUseName); } if (!empty($this->categoryDataList[$crosslink->getRefCategoryId()]['url']) && !$this->_isCurrentUrl($this->categoryDataList[$crosslink->getRefCategoryId()]['url']) ) { return $this->categoryDataList[$crosslink->getRefCategoryId()]; } return false; } /** * Retrive landing page data (URL, name) if it is not current landing page * * @param $collection * @param $crosslink * @param $ignoreLandingPageId * @return bool|mixed */ protected function _getLandingpageData($collection, $crosslink, $ignoreLandingPageId) { if (is_null($this->landingpageDataList)) { $lpIds = array(); foreach ($collection as $item) { if ($item->getRefLandingpageId() && $item->getRefLandingpageId() != $ignoreLandingPageId) { $lpIds[] = $item->getRefLandingpageId(); } } $data = new DataObject(); $data->setIds($lpIds); $data->setLandingpagesData([]); $this->eventManager->dispatch( 'mageworx_landingpages_get_landingpages_data', ['object' => $data] ); $this->landingpageDataList = $data->getLandingpagesData(); } if (!empty($this->landingpageDataList[$crosslink->getRefLandingpageId()]['url']) && !$this->_isCurrentUrl($this->landingpageDataList[$crosslink->getRefLandingpageId()]['url']) ) { return $this->landingpageDataList[$crosslink->getRefLandingpageId()]; } return false; } /** * Retrive URL * * @param \MageWorx\SeoCrossLinks\Model\Crosslink $crosslink * @return string */ protected function _getStaticUrl($crosslink) { if (strpos($crosslink->getRefStaticUrl(), '://') === false) { $staticUrl = $this->helperStoreUrl->getUrl($crosslink->getRefStaticUrl()); } else { $staticUrl = trim($crosslink->getRefStaticUrl()); } if (!$this->_isCurrentUrl($staticUrl)) { return $staticUrl; } return false; } /** * Minimize collection using search keywords in text and keyword count * * @param /MageWorx/SeoCrossLinks/Model/ResourceModel/Crosslink/Collection $collection * @param array $textParts * @param int $maxReplaceAllCount */ protected function _specifyCollection($collection, $textParts, $maxReplaceAllCount) { $keywords = $collection->loadKeywordsOnly(); if ($collection->isLoaded()) { $this->logger->critical('Crosslink collection was loaded too early. This can cause performance problems.'); } $text = implode(' ***mageworx*** ', $textParts); $replaceStaticUrlCount = 0; foreach ($keywords as $id => $keyword) { $replace = $this->_isCrosslinkFound($keyword, $text); if (!$replace) { unset($keywords[$id]); } } $collection->addFieldToFilter('crosslink_id', ['in' => array_keys($keywords)]); foreach ($collection->getItems() as $item) { if ($replaceStaticUrlCount > $maxReplaceAllCount) { $collection->removeItemByKey($item->getCrosslinkId()); continue; } $replaceCount = $this->_renderCrossLink($item, $text); if ($item->getRefStaticUrl()) { $replaceStaticUrlCount += $replaceCount; } } } /** * Return count of matches or false. * Rewrite keyword value for crosslink: * if identical match found modify crosslink keyword "cak+" => cake * else create keywords property in crosslink model * * @param \MageWorx\SeoCrossLinks\Model\Crosslink $crosslink * @param string $text * @return int|false */ protected function _renderCrossLink($crosslink, $text) { if (stripos($text, trim($crosslink->getKeyword(), '+')) !== false) { $pattern = $this->_getMatchPattern($crosslink->getKeyword()); $matches = array(); $res = preg_match_all($pattern, $text, $matches); if ($res) { $cropMatches = array_slice($matches[0], 0, $crosslink->getReplacementCount()); if (count($cropMatches) == 1) { $crosslink->setKeyword($cropMatches[0]); } else { $crosslink->setKeywords($cropMatches); } return (count($cropMatches)); } } return false; } /** * @param string $keyword * @param string $text * @return bool */ protected function _isCrosslinkFound($keyword, $text) { if (stripos($text, trim($keyword, '+')) !== false) { $pattern = $this->_getMatchPattern($keyword); $matches = []; $res = preg_match_all($pattern, $text, $matches); if ($res) { return true; } } return false; } /** * Convert string to regexp * * @param string $keyword * @return string */ protected function _getMatchPattern($keyword) { $keyword = trim($keyword); if (substr($keyword, 0, 1) == '+') { $leftPlus = true; $keyword = ltrim($keyword, '+'); } if (substr($keyword, -1, 1) == '+') { $rightPlus = true; $keyword = rtrim($keyword, '+'); } $keyword = preg_quote($keyword, '/'); if (!empty($leftPlus)) { $keyword = '[^\s.<,]*' . $keyword; } else { $keyword = '(\b)' . $keyword; } if (!empty($rightPlus)) { $keyword = rtrim($keyword, '+') . '[^\s.<,]*'; } else { $keyword = $keyword . '(\b)'; } return '/' . $keyword . '/iu'; } /** * Convert string to regexp * * @param string $keyword * @return string */ protected function _getReplacePattern($keyword) { return '/(\b)' . preg_quote($keyword, '/') . '(\b)/iu'; } /** * Retrive HTML link * * @param \MageWorx\SeoCrossLinks\Model\Crosslink $crosslink * @param string $keyword * @param string $urlRaw * @return string */ protected function _getHtmlHref($crosslink, $keywordRaw, $urlRaw, $name) { $url = htmlspecialchars($urlRaw, ENT_COMPAT, 'UTF-8', false); $target = $crosslink->getTargetLinkValue($crosslink->getLinkTarget()); switch ($this->helperData->isUseNameForTitle()) { case Crosslink::USE_CROSSLINK_TITLE_ONLY: $title = $crosslink->getLinkTitle(); break; case Crosslink::USE_NAME_IF_EMPTY_TITLE: $title = trim($crosslink->getLinkTitle()) ? $crosslink->getLinkTitle() : $name; break; case Crosslink::USE_NAME_ALWAYS: $title = $name; break; } $title = htmlspecialchars(strip_tags($title)); $keyword = htmlspecialchars($keywordRaw); $class = $this->helperData->getLinkClass(); $crosslinkHtml = "<a " . $class . " href=\"" . $url . "\" target=\"" . $target . "\" title=\"" . $title . "\""; if ($crosslink->getNofollowRel()) { $crosslinkHtml .= ' rel="nofollow"'; } return $crosslinkHtml . ">" . $keyword . "</a>"; } /** * Check if input string is current URL * * @param string $url * @return bool */ protected function _isCurrentUrl($url) { list($currentUrl) = explode('?', $this->url->getCurrentUrl()); return (mb_substr($currentUrl, mb_strpos($currentUrl, '://')) == mb_substr($url, mb_strpos($url, '://'))); } }