"\r\n", "unix" => "\n", "mac" => "\r"]; /** * @var \Magento\Framework\Filesystem\Directory\Write */ protected $_directory; /** * @var \Magento\Framework\Filesystem\File\Write */ protected $_stream; /** * Sitemap data * * @var \Magento\Sitemap\Helper\Data */ protected $_sitemapData; /** * @var \Magento\Framework\Escaper */ protected $_escaper; /** * @var \Magento\Sitemap\Model\ResourceModel\Catalog\CategoryFactory */ protected $_categoryFactory; /** * @var \Magento\Sitemap\Model\ResourceModel\Catalog\ProductFactory */ protected $_productFactory; /** * @var \Magento\Sitemap\Model\ResourceModel\Cms\PageFactory */ protected $_cmsFactory; /** * @var \Magento\Framework\Stdlib\DateTime\DateTime */ protected $_dateModel; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** * @var \Magento\Framework\App\RequestInterface */ protected $_request; /** * @var \Magento\Framework\Stdlib\DateTime */ protected $dateTime; /** * Model cache tag for clear cache in after save and after delete * * @var string * @since 100.1.5 */ protected $_cacheTag = true; /** * Item resolver * * @var ItemProviderInterface */ private $itemProvider; /** * Sitemap config reader * * @var SitemapConfigReaderInterface */ private $configReader; /** * Sitemap Item Factory * * @var \Magento\Sitemap\Model\SitemapItemInterfaceFactory */ private $sitemapItemFactory; /** * Last mode min timestamp value * * @var int */ private $lastModMinTsVal; /** * Initialize dependencies. * * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Escaper $escaper * @param \Magento\Sitemap\Helper\Data $sitemapData * @param \Magento\Framework\Filesystem $filesystem * @param ResourceModel\Catalog\CategoryFactory $categoryFactory * @param ResourceModel\Catalog\ProductFactory $productFactory * @param ResourceModel\Cms\PageFactory $cmsFactory * @param \Magento\Framework\Stdlib\DateTime\DateTime $modelDate * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\RequestInterface $request * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param DocumentRoot|null $documentRoot * @param ItemProviderInterface|null $itemProvider * @param SitemapConfigReaderInterface|null $configReader * @param \Magento\Sitemap\Model\SitemapItemInterfaceFactory|null $sitemapItemFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, \Magento\Framework\Registry $registry, \Magento\Framework\Escaper $escaper, \Magento\Sitemap\Helper\Data $sitemapData, \Magento\Framework\Filesystem $filesystem, \Magento\Sitemap\Model\ResourceModel\Catalog\CategoryFactory $categoryFactory, \Magento\Sitemap\Model\ResourceModel\Catalog\ProductFactory $productFactory, \Magento\Sitemap\Model\ResourceModel\Cms\PageFactory $cmsFactory, \Magento\Framework\Stdlib\DateTime\DateTime $modelDate, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\RequestInterface $request, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], \Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot $documentRoot = null, ItemProviderInterface $itemProvider = null, SitemapConfigReaderInterface $configReader = null, \Magento\Sitemap\Model\SitemapItemInterfaceFactory $sitemapItemFactory = null ) { $this->_escaper = $escaper; $this->_sitemapData = $sitemapData; $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); $this->_categoryFactory = $categoryFactory; $this->_productFactory = $productFactory; $this->_cmsFactory = $cmsFactory; $this->_dateModel = $modelDate; $this->_storeManager = $storeManager; $this->_request = $request; $this->dateTime = $dateTime; $this->itemProvider = $itemProvider ?: ObjectManager::getInstance()->get(ItemProviderInterface::class); $this->configReader = $configReader ?: ObjectManager::getInstance()->get(SitemapConfigReaderInterface::class); $this->sitemapItemFactory = $sitemapItemFactory ?: ObjectManager::getInstance()->get( \Magento\Sitemap\Model\SitemapItemInterfaceFactory::class ); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } /** * Init model * * @return void */ protected function _construct() { $this->_init(SitemapResource::class); } /** * Get file handler * * @return \Magento\Framework\Filesystem\File\WriteInterface * @throws LocalizedException */ protected function _getStream() { if ($this->_stream) { return $this->_stream; } else { throw new LocalizedException(__('File handler unreachable')); } } /** * Add a sitemap item to the array of sitemap items * * @param DataObject $sitemapItem * @return $this * @deprecated 100.3.0 * @see ItemProviderInterface * @since 100.2.0 */ public function addSitemapItem(DataObject $sitemapItem) { $this->_sitemapItems[] = $sitemapItem; return $this; } /** * Collect all sitemap items * * @return void * @deprecated 100.3.0 * @see ItemProviderInterface * @since 100.2.0 */ public function collectSitemapItems() { /** @var $helper \Magento\Sitemap\Helper\Data */ $helper = $this->_sitemapData; $storeId = $this->getStoreId(); $this->addSitemapItem(new DataObject( [ 'changefreq' => $helper->getCategoryChangefreq($storeId), 'priority' => $helper->getCategoryPriority($storeId), 'collection' => $this->_categoryFactory->create()->getCollection($storeId), ] )); $this->addSitemapItem(new DataObject( [ 'changefreq' => $helper->getProductChangefreq($storeId), 'priority' => $helper->getProductPriority($storeId), 'collection' => $this->_productFactory->create()->getCollection($storeId), ] )); $this->addSitemapItem(new DataObject( [ 'changefreq' => $helper->getPageChangefreq($storeId), 'priority' => $helper->getPagePriority($storeId), 'collection' => $this->_cmsFactory->create()->getCollection($storeId), ] )); } /** * Initialize sitemap * * @return void */ protected function _initSitemapItems() { $sitemapItems = $this->itemProvider->getItems($this->getStoreId()); $mappedItems = $this->mapToSitemapItem(); $this->_sitemapItems = array_merge($sitemapItems, $mappedItems); $this->_tags = [ self::TYPE_INDEX => [ self::OPEN_TAG_KEY => '' . PHP_EOL . '' . PHP_EOL, self::CLOSE_TAG_KEY => '', ], self::TYPE_URL => [ self::OPEN_TAG_KEY => '' . PHP_EOL . '' . PHP_EOL, self::CLOSE_TAG_KEY => '', ], ]; } /** * Check sitemap file location and permissions * * @return \Magento\Framework\Model\AbstractModel * @throws LocalizedException */ public function beforeSave() { $path = $this->getSitemapPath(); /** * Check path is allow */ if ($path && preg_match('#\.\.[\\\/]#', $path)) { throw new LocalizedException(__('Please define a correct path.')); } /** * Check exists and writable path */ if (!$this->_directory->isExist($path)) { throw new LocalizedException( __( 'Please create the specified folder "%1" before saving the sitemap.', $this->_escaper->escapeHtml($this->getSitemapPath()) ) ); } if (!$this->_directory->isWritable($path)) { throw new LocalizedException( __('Please make sure that "%1" is writable by the web-server.', $this->getSitemapPath()) ); } /** * Check allow filename */ if (!preg_match('#^[a-zA-Z0-9_\.]+$#', $this->getSitemapFilename())) { throw new LocalizedException( __( 'Please use only letters (a-z or A-Z), numbers (0-9) or underscores (_) in the filename.' . ' No spaces or other characters are allowed.' ) ); } if (!preg_match('#\.xml$#', $this->getSitemapFilename())) { $this->setSitemapFilename($this->getSitemapFilename() . '.xml'); } $this->setSitemapPath(rtrim(str_replace(str_replace('\\', '/', $this->_getBaseDir()), '', $path), '/') . '/'); return parent::beforeSave(); } /** * Generate XML file * * @see http://www.sitemaps.org/protocol.html * * @return $this */ public function generateXml() { $this->_initSitemapItems(); /** @var $item SitemapItemInterface */ foreach ($this->_sitemapItems as $item) { $xml = $this->_getSitemapRow( $item->getUrl(), $item->getUpdatedAt(), $item->getChangeFrequency(), $item->getPriority(), $item->getImages() ); if ($this->_isSplitRequired($xml) && $this->_sitemapIncrement > 0) { $this->_finalizeSitemap(); } if (!$this->_fileSize) { $this->_createSitemap(); } $this->_writeSitemapRow($xml); // Increase counters $this->_lineCount++; $this->_fileSize += strlen($xml); } $this->_finalizeSitemap(); if ($this->_sitemapIncrement == 1) { // In case when only one increment file was created use it as default sitemap $path = rtrim( $this->getSitemapPath(), '/' ) . '/' . $this->_getCurrentSitemapFilename( $this->_sitemapIncrement ); $destination = rtrim($this->getSitemapPath(), '/') . '/' . $this->getSitemapFilename(); $this->_directory->renameFile($path, $destination); } else { // Otherwise create index file with list of generated sitemaps $this->_createSitemapIndex(); } $this->setSitemapTime($this->_dateModel->gmtDate('Y-m-d H:i:s')); $this->save(); return $this; } /** * Generate sitemap index XML file * * @return void */ protected function _createSitemapIndex() { $this->_createSitemap($this->getSitemapFilename(), self::TYPE_INDEX); for ($i = 1; $i <= $this->_sitemapIncrement; $i++) { $xml = $this->_getSitemapIndexRow($this->_getCurrentSitemapFilename($i), $this->_getCurrentDateTime()); $this->_writeSitemapRow($xml); } $this->_finalizeSitemap(self::TYPE_INDEX); } /** * Get current date time * * @return string */ protected function _getCurrentDateTime() { return (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); } /** * Check is split required * * @param string $row * @return bool */ protected function _isSplitRequired($row) { $storeId = $this->getStoreId(); if ($this->_lineCount + 1 > $this->configReader->getMaximumLinesNumber($storeId)) { return true; } if ($this->_fileSize + strlen($row) > $this->configReader->getMaximumFileSize($storeId)) { return true; } return false; } /** * Get sitemap row * * @param string $url * @param null|string $lastmod * @param null|string $changefreq * @param null|string $priority * @param null|array|\Magento\Framework\DataObject $images * @return string * Sitemap images * @see http://support.google.com/webmasters/bin/answer.py?hl=en&answer=178636 * * Sitemap PageMap * @see http://support.google.com/customsearch/bin/answer.py?hl=en&answer=1628213 */ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $priority = null, $images = null) { $url = $this->_getUrl($url); $row = '' . htmlspecialchars($url) . ''; if ($lastmod) { $row .= '' . $this->_getFormattedLastmodDate($lastmod) . ''; } if ($changefreq) { $row .= '' . $changefreq . ''; } if ($priority) { $row .= sprintf('%.1f', $priority); } if ($images) { // Add Images to sitemap foreach ($images->getCollection() as $image) { $row .= ''; $row .= '' . htmlspecialchars($image->getUrl()) . ''; $row .= '' . htmlspecialchars($images->getTitle()) . ''; if ($image->getCaption()) { $row .= '' . htmlspecialchars($image->getCaption()) . ''; } $row .= ''; } // Add PageMap image for Google web search $row .= ''; $row .= ''; $row .= ''; $row .= ''; } return '' . $row . ''; } /** * Get sitemap index row * * @param string $sitemapFilename * @param null|string $lastmod * @return string */ protected function _getSitemapIndexRow($sitemapFilename, $lastmod = null) { $url = $this->getSitemapUrl($this->getSitemapPath(), $sitemapFilename); $row = '' . htmlspecialchars($url) . ''; if ($lastmod) { $row .= '' . $this->_getFormattedLastmodDate($lastmod) . ''; } return '' . $row . ''; } /** * Create new sitemap file * * @param null|string $fileName * @param string $type * @return void * @throws LocalizedException */ protected function _createSitemap($fileName = null, $type = self::TYPE_URL) { if (!$fileName) { $this->_sitemapIncrement++; $fileName = $this->_getCurrentSitemapFilename($this->_sitemapIncrement); } $path = rtrim($this->getSitemapPath(), '/') . '/' . $fileName; $this->_stream = $this->_directory->openFile($path); $fileHeader = sprintf($this->_tags[$type][self::OPEN_TAG_KEY], $type); $this->_stream->write($fileHeader); $this->_fileSize = strlen($fileHeader . sprintf($this->_tags[$type][self::CLOSE_TAG_KEY], $type)); } /** * Write sitemap row * * @param string $row * @return void */ protected function _writeSitemapRow($row) { $this->_getStream()->write($row . PHP_EOL); } /** * Write closing tag and close stream * * @param string $type * @return void */ protected function _finalizeSitemap($type = self::TYPE_URL) { if ($this->_stream) { $this->_stream->write(sprintf($this->_tags[$type][self::CLOSE_TAG_KEY], $type)); $this->_stream->close(); } // Reset all counters $this->_lineCount = 0; $this->_fileSize = 0; } /** * Get current sitemap filename * * @param int $index * @return string */ protected function _getCurrentSitemapFilename($index) { return str_replace('.xml', '', $this->getSitemapFilename()) . '-' . $this->getStoreId() . '-' . $index . '.xml'; } /** * Get base dir * * @return string */ protected function _getBaseDir() { return $this->_directory->getAbsolutePath(); } /** * Get store base url * * @param string $type * @return string */ protected function _getStoreBaseUrl($type = UrlInterface::URL_TYPE_LINK) { /** @var \Magento\Store\Model\Store $store */ $store = $this->_storeManager->getStore($this->getStoreId()); $isSecure = $store->isUrlSecure(); return rtrim($store->getBaseUrl($type, $isSecure), '/') . '/'; } /** * Get url * * @param string $url * @param string $type * @return string */ protected function _getUrl($url, $type = UrlInterface::URL_TYPE_LINK) { return $this->_getStoreBaseUrl($type) . ltrim($url, '/'); } /** * Get media url * * @param string $url * @return string * @deprecated 100.2.0 No longer used, as we're generating product image URLs inside collection instead * @see \Magento\Sitemap\Model\ResourceModel\Catalog\Product::_loadProductImages() */ protected function _getMediaUrl($url) { return $this->_getUrl($url, UrlInterface::URL_TYPE_MEDIA); } /** * Get date in correct format applicable for lastmod attribute * * @param string $date * @return string */ protected function _getFormattedLastmodDate($date) { if ($this->lastModMinTsVal === null) { $this->lastModMinTsVal = strtotime(self::LAST_MOD_MIN_VAL); } $timestamp = max(strtotime($date), $this->lastModMinTsVal); return date('c', $timestamp); } /** * Get Document root of Magento instance * * @return string */ protected function _getDocumentRoot() { return realpath($this->_request->getServer('DOCUMENT_ROOT')); } /** * Get domain from store base url * * @return string */ protected function _getStoreBaseDomain() { $storeParsedUrl = parse_url($this->_getStoreBaseUrl()); $url = $storeParsedUrl['scheme'] . '://' . $storeParsedUrl['host']; $documentRoot = trim(str_replace('\\', '/', $this->_getDocumentRoot()), '/'); $baseDir = trim(str_replace('\\', '/', $this->_getBaseDir()), '/'); if (strpos($baseDir, $documentRoot) === 0) { //case when basedir is in document root $installationFolder = trim(str_replace($documentRoot, '', $baseDir), '/'); $storeDomain = rtrim($url . '/' . $installationFolder, '/'); } else { //case when documentRoot contains symlink to basedir $url = $this->_getStoreBaseUrl(UrlInterface::URL_TYPE_WEB); $storeDomain = rtrim($url, '/'); } return $storeDomain; } /** * Get sitemap.xml URL according to all config options * * @param string $sitemapPath * @param string $sitemapFileName * @return string */ public function getSitemapUrl($sitemapPath, $sitemapFileName) { return $this->_getStoreBaseDomain() . str_replace('//', '/', $sitemapPath . '/' . $sitemapFileName); } /** * Check is enabled submission to robots.txt * * @return bool * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. */ protected function _isEnabledSubmissionRobots() { $storeId = $this->getStoreId(); return (bool)$this->configReader->getEnableSubmissionRobots($storeId); } /** * Add sitemap file to robots.txt * * @param string $sitemapFileName * @return void * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. */ protected function _addSitemapToRobotsTxt($sitemapFileName) { $robotsSitemapLine = 'Sitemap: ' . $this->getSitemapUrl($this->getSitemapPath(), $sitemapFileName); $filename = 'robots.txt'; $content = ''; if ($this->_directory->isExist($filename)) { $content = $this->_directory->readFile($filename); } if (strpos($content, $robotsSitemapLine) === false) { if (!empty($content)) { $content .= $this->_findNewLinesDelimiter($content); } $content .= $robotsSitemapLine; } $this->_directory->writeFile($filename, $content); } /** * Find new lines delimiter * * @param string $text * @return string */ private function _findNewLinesDelimiter($text) { foreach ($this->_crlf as $delimiter) { if (strpos($text, $delimiter) !== false) { return $delimiter; } } return PHP_EOL; } /** * Sitemap item mapper for backwards compatibility * * @return array */ private function mapToSitemapItem() { $items = []; foreach ($this->_sitemapItems as $data) { foreach ($data->getCollection() as $item) { $items[] = $this->sitemapItemFactory->create([ 'url' => $item->getUrl(), 'updatedAt' => $item->getUpdatedAt(), 'images' => $item->getImages(), 'priority' => $data->getPriority(), 'changeFrequency' => $data->getChangeFrequency(), ]); } } return $items; } /** * Get unique page cache identities * * @return array * @since 100.1.5 */ public function getIdentities() { return [ Value::CACHE_TAG . '_' . $this->getStoreId(), ]; } }