diff --git a/core/lib/Thelia/Model/Tools/SitemapURL.php b/core/lib/Thelia/Model/Tools/SitemapURL.php new file mode 100644 index 000000000..bf9c76bed --- /dev/null +++ b/core/lib/Thelia/Model/Tools/SitemapURL.php @@ -0,0 +1,120 @@ + + */ +class SitemapURL +{ + /** + * URL of the page. + * + * @var string + */ + protected $loc = null; + + /** + * The date of last modification of the file. + * + * @var string + */ + protected $lastmod = null; + + /** + * How frequently the page is likely to change. + * + * @var string + */ + protected $changfreq = null; + + /** + * The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0. + * + * @var float + */ + protected $priotity = null; + + public function __construct($loc, $lastmod=null) + { + $this->loc = $loc; + $this->lastmod = $lastmod; + } + + /** + * @param string $changfreq + */ + public function setChangfreq($changfreq) + { + $this->changfreq = $changfreq; + } + + /** + * @return string + */ + public function getChangfreq() + { + return $this->changfreq; + } + + /** + * @param string $lastmod + */ + public function setLastmod($lastmod) + { + $this->lastmod = $lastmod; + } + + /** + * @return string + */ + public function getLastmod() + { + return $this->lastmod; + } + + /** + * @param string $loc + */ + public function setLoc($loc) + { + $this->loc = $loc; + } + + /** + * @return string + */ + public function getLoc() + { + return $this->loc; + } + + /** + * @param float $priotity + */ + public function setPriotity($priotity) + { + $this->priotity = $priotity; + } + + /** + * @return float + */ + public function getPriotity() + { + return $this->priotity; + } + +} diff --git a/core/lib/Thelia/Tools/SitemapURLNormalizer.php b/core/lib/Thelia/Tools/SitemapURLNormalizer.php new file mode 100644 index 000000000..e75a456f9 --- /dev/null +++ b/core/lib/Thelia/Tools/SitemapURLNormalizer.php @@ -0,0 +1,58 @@ + + */ +class SitemapURLNormalizer extends SerializerAwareNormalizer implements NormalizerInterface +{ + public function normalize($object, $format = null, array $context = array()) + { + $normalizeSitemapURL = array( + 'loc' => $this->urlEncode($object->getLoc()) + ); + if (null !== $object->getLastmod()) { + $normalizeSitemapURL['lastmod'] = $object->getLastmod(); + } + if (null !== $object->getChangfreq()) { + $normalizeSitemapURL['changfreq'] = $object->getChangfreq(); + } + if (null !== $object->getPriotity()) { + $normalizeSitemapURL['priority'] = $object->getPriotity(); + } + + return $normalizeSitemapURL; + } + + protected function urlEncode($url) + { + return str_replace(array('&', '"', '\'', '<', '>'), array('&', ''', '"', '>', '<'), $url); + } + // public function denormalize($data, $class, $format = null) {} + + public function supportsNormalization($data, $format = null) + { + return $data instanceof SitemapURL; + } + + public function supportsDenormalization($data, $type, $format = null) + { + return false; + } +} diff --git a/local/modules/Front/Config/front.xml b/local/modules/Front/Config/front.xml index 319d9b4c2..4f32372a3 100644 --- a/local/modules/Front/Config/front.xml +++ b/local/modules/Front/Config/front.xml @@ -194,6 +194,12 @@ + + + Front\Controller\SitemapController::generateAction + + + Thelia\Controller\Front\DefaultController::noAction diff --git a/local/modules/Front/Controller/SitemapController.php b/local/modules/Front/Controller/SitemapController.php new file mode 100644 index 000000000..75f9bdaa2 --- /dev/null +++ b/local/modules/Front/Controller/SitemapController.php @@ -0,0 +1,357 @@ + + */ +class SitemapController extends BaseFrontController { + + + /** + * Folder name for sitemap cache + */ + const SITEMAP_DIR = "sitemap"; + + /** + * String array of active locals : fr_FR, en_US, ... + * + * @var array + */ + protected $locales = array(); + + /** + * Array of ` Thelia\Model\Tools\SitemapURL` object for categories + * + * @var array + */ + protected $categoryUrls = array(); + + /** + * Array of ` Thelia\Model\Tools\SitemapURL` object for products + * + * @var array + */ + protected $productUrls = array(); + + /** + * Array of ` Thelia\Model\Tools\SitemapURL` object for folders + * + * @var array + */ + protected $folderUrls = array(); + + /** + * Array of ` Thelia\Model\Tools\SitemapURL` object for contents + * + * @var array + */ + protected $contentUrls = array(); + + /** + * Array of ` Thelia\Model\Tools\SitemapURL` object for static contents + * + * @var array + */ + protected $staticUrls = array(); + + /** + * @return Response + */ + public function generateAction() + { + + // check if already cached + /** @var Request $request */ + $request = $this->getRequest(); + $locale = $request->query->get("locale"); + // todo: implement contextual sitemap : product, category, cms + $context = $request->query->get("context", ""); + $flush = $request->query->get("flush", ""); + $expire = ConfigQuery::read("sitemap_ttl", '7200'); + + // load locals + $langs = LangQuery::create()->find(); + /** @var Lang $lang */ + foreach ($langs AS $lang){ + if (null !== $locale) { + if ($locale === $lang->getLocale()){ + $this->locales[] = $lang->getLocale(); + break; + } + } + else { + $this->locales[] = $lang->getLocale(); + } + } + + // check if sitemap already in cache + $cacheDir = $this->getCacheDir(); + $sitemapHash = md5("sitemap." . implode($this->locales) . "." . $context); + $expire = intval($expire) ?: 7200; + $cacheFileURL = $cacheDir . $sitemapHash . '.xml'; + $cacheContent = null; + if (!($this->checkAdmin() && "" !== $flush)){ + try { + $cacheContent = $this->getCache($cacheFileURL, $expire); + } catch (\RuntimeException $ex) { + // Problem loading cache, permission errors ? + Tlog::getInstance()->addAlert($ex->getMessage()); + } + } + + if (null === $cacheContent){ + $encoders = array(new XmlEncoder("urlset"), new JsonEncoder()); + $normalizers = array(new SitemapURLNormalizer()); + $serializer = new Serializer($normalizers, $encoders); + + $this->findStaticUrls(); + $this->findCategoryUrls(); + $this->findFolderUrls(); + + $map = array(); + $map['@xmlns'] = "http://www.sitemaps.org/schemas/sitemap/0.9"; + $map['url'] = array_merge( + $this->staticUrls, + $this->categoryUrls, + $this->productUrls, + $this->folderUrls, + $this->contentUrls + ); + + $cacheContent = $serializer->serialize($map, 'xml'); + + // save cache + try { + $this->setCache($cacheFileURL, $cacheContent); + } catch (\RuntimeException $ex) { + // Problem loading cache, permission errors ? + Tlog::getInstance()->addAlert($ex->getMessage()); + } + + } + + $response = new Response(); + $response->setContent($cacheContent); + $response->headers->set('Content-Type', 'application/xml'); + + return $response; + } + + + /** + * Get all static URLs + * + * @param int $parent Parent category id + */ + protected function findStaticUrls() + { + $url = URL::getInstance()->getIndexPage(); + $home = new SitemapURL($url); + $home->setPriotity(1.0); + $this->staticUrls[] = $home; + } + + /** + * Get all child visible categories of category id `$parent` + * This function is recursive and is called for all child categories + * + * @param int $parent Parent category id + */ + protected function findCategoryUrls($parent = 0) + { + $categoryQuery = CategoryQuery::create(); + $categoryQuery->filterByParent($parent); + $categoryQuery->filterByVisible(true, Criteria::EQUAL); + $categories = $categoryQuery->find(); + + /** @var Category $category */ + foreach($categories AS $category){ + foreach ($this->locales AS $local){ + $loc = $category->getUrl($local); + $this->categoryUrls[] = new SitemapURL($loc, $category->getUpdatedAt("c")); + } + // call sub categories + $this->findCategoryUrls($category->getId()); + // call products + $this->findProductUrls($category); + } + } + + /** + * Get all visible product which have `category` as default category + * + * @param Category $category + */ + protected function findProductUrls(Category $category = null) + { + $products = ProductQuery::create() + //->filterByCategory($category) + ->filterByVisible(true, Criteria::EQUAL) + ->joinProductCategory() + ->where('ProductCategory.default_category' . Criteria::EQUAL . '1') + ->where('ProductCategory.category_id = ?', $category->getId()) + ->find(); + + /** @var Product $product */ + foreach($products AS $product){ + foreach ($this->locales AS $local){ + $loc = $product->getUrl($local); + $this->productUrls[] = new SitemapURL($loc, $product->getUpdatedAt("c")); + } + } + } + + /** + * Get all child visible folders of folder id `$parent` + * This function is recursive and is called for all child folders + * + * @param int $parent Parent folder id + */ + protected function findFolderUrls($parent = 0) + { + $folderQuery = FolderQuery::create(); + $folderQuery->filterByParent($parent); + $folderQuery->filterByVisible(true, Criteria::EQUAL); + $folders = $folderQuery->find(); + + /** @var Folder $folders */ + foreach($folders AS $folder){ + foreach ($this->locales AS $local){ + $loc = $folder->getUrl($local); + $this->folderUrls[] = new SitemapURL($loc, $folder->getUpdatedAt("c")); + } + // call sub folders + $this->findFolderUrls($folder->getId()); + // call contents + $this->findContentUrls($folder); + } + } + + /** + * Get all visible content which have in `$folder` folder + * + * @param Folder $folder + */ + protected function findContentUrls(Folder $folder=null) + { + $contents = ContentQuery::create() + ->filterByVisible(true, Criteria::EQUAL) + ->filterByFolder($folder) + ->find(); + + /** @var Content $content */ + foreach($contents AS $content){ + foreach ($this->locales AS $local){ + $loc = $content->getUrl($local); + $this->contentUrls[] = new SitemapURL($loc, $content->getUpdatedAt("c")); + } + } + } + + + /** + * Check if current user has ADMIN role + * + * @return bool + */ + protected function checkAdmin(){ + return $this->getSecurityContext()->isGranted(array("ADMIN"), array(), array(), array()); + } + + /** + * Get the content of the file if it exists and not expired? + * + * @param $fileURL path to the file + * @param $expire TTL for the file + * @return null|string The content of the file if it exists and not expired + * @throws \RuntimeException + */ + protected function getCache($fileURL, $expire) + { + $content = null; + if (is_file($fileURL)){ + $mtime = filemtime($fileURL); + if ($mtime + $expire < time()){ + if (! @unlink($fileURL)){ + throw new \RuntimeException(sprintf("Failed to remove %s file in cache directory", $fileURL)); + } + } else { + $content = file_get_contents($fileURL); + } + } + return $content; + } + + /** + * Save content in the file specified by `$fileURL` + * + * @param $fileURL the path to the file + * @param $content the content of the file + * @throws \RuntimeException + */ + protected function setCache($fileURL, $content) + { + if (! @file_put_contents($fileURL, $content)){ + throw new \RuntimeException(sprintf("Failed to save %s file in cache directory", $fileURL)); + } + } + + + /** + * Retrieve the cache dir used for sitemaps + * + * @return string the path to the cache dir + * @throws \RuntimeException + */ + protected function getCacheDir() + { + $cacheDir = $this->container->getParameter("kernel.cache_dir") . + $cacheDir = rtrim($cacheDir, '/'); + $cacheDir .= '/' . self::SITEMAP_DIR . '/'; + if (! is_dir($cacheDir)){ + if (! @mkdir($cacheDir, 0777, true)) { + throw new \RuntimeException(sprintf("Failed to create %s dir in cache directory", self::SITEMAP_DIR)); + } + } + return $cacheDir; + } + +} \ No newline at end of file diff --git a/setup/insert.sql b/setup/insert.sql index a8a1bb212..e751f8805 100644 --- a/setup/insert.sql +++ b/setup/insert.sql @@ -45,7 +45,8 @@ INSERT INTO `config` (`name`, `value`, `secured`, `hidden`, `created_at`, `updat ('thelia_release_version','1', 1, 1, NOW(), NOW()), ('thelia_extra_version','', 1, 1, NOW(), NOW()), ('front_cart_country_cookie_name','fcccn', 1, 1, NOW(), NOW()), -('front_cart_country_cookie_expires','2592000', 1, 1, NOW(), NOW()); +('front_cart_country_cookie_expires','2592000', 1, 1, NOW(), NOW()), +('sitemap_ttl','7200', 1, 1, NOW(), NOW()); INSERT INTO `config_i18n` (`id`, `locale`, `title`, `description`, `chapo`, `postscriptum`) VALUES diff --git a/setup/update/2.0.1.sql b/setup/update/2.0.1.sql index cc368353a..7b91f3232 100644 --- a/setup/update/2.0.1.sql +++ b/setup/update/2.0.1.sql @@ -10,6 +10,8 @@ INSERT INTO `config` (`name`, `value`, `secured`, `hidden`, `created_at`, `updat ('front_cart_country_cookie_name','fcccn', 1, 1, NOW(), NOW()); INSERT INTO `config` (`name`, `value`, `secured`, `hidden`, `created_at`, `updated_at`) VALUES ('front_cart_country_cookie_expires','2592000', 1, 1, NOW(), NOW()); +INSERT INTO `config` (`name`, `value`, `secured`, `hidden`, `created_at`, `updated_at`) VALUES +('sitemap_ttl','7200', 1, 1, NOW(), NOW()); ALTER TABLE `module` ADD INDEX `idx_module_activate` (`activate`);