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`);