diff --git a/Readme.md b/Readme.md index 791b61d1e..ac0862dae 100755 --- a/Readme.md +++ b/Readme.md @@ -12,14 +12,33 @@ Here is the most recent developed code for the next major version (v2). You can Most part of the code can possibly change, a large part will be refactor soon, graphical setup does not exist yet. +Requirements +------------ + +* php 5.4 +* apache 2 +* mysql 5 + +If you use Mac OSX, it still doesn't use php 5.4 as default php version... There are many solutions for you : + +* use linux (the best one) +* use last MAMP version and put the php bin directory in your path : + +```bash +export PATH=/Applications/MAMP/bin/php/php5.4.x/bin/:$PATH +``` + +* configure a complete development environment : http://php-osx.liip.ch/ +* use a virtual machine with vagrant and puppet : https://puphpet.com/ + Installation ------------ ``` bash $ git clone --recursive https://github.com/thelia/thelia.git $ cd thelia -$ wget http://getcomposer.org/composer.phar -$ php composer.phar install +$ curl -sS https://getcomposer.org/installer | php +$ php composer.phar install --optimize-autoloader ``` Finish the installation using cli tools : diff --git a/composer.json b/composer.json index 2d315bd3a..4a3798384 100755 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "simplepie/simplepie": "dev-master", "imagine/imagine": "dev-master", - "symfony/icu": "1.0" + "symfony/icu": "1.0", + "swiftmailer/swiftmailer": "5.0.*" }, "require-dev" : { "phpunit/phpunit": "3.7.*", @@ -53,9 +54,5 @@ "": "local/modules/", "Thelia" : "core/lib/" } - }, - "scripts" : { - "post-update-cmd": "composer dump-autoload -o", - "post-install-cmd": "composer dump-autoload -o" } } diff --git a/composer.lock b/composer.lock index 61160ea49..b0310c075 100755 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "28dfdc7a840f9e70df422581f82a871f", + "hash": "a40be01c82e68ba0c446dc204d2667da", "packages": [ { "name": "imagine/imagine", @@ -445,6 +445,55 @@ ], "time": "2013-07-02 16:38:47" }, + { + "name": "swiftmailer/swiftmailer", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "f3917ecef35a4e4d98b303eb9fee463bc983f379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/f3917ecef35a4e4d98b303eb9fee463bc983f379", + "reference": "f3917ecef35a4e4d98b303eb9fee463bc983f379", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Chris Corbyn" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "http://swiftmailer.org", + "keywords": [ + "mail", + "mailer" + ], + "time": "2013-08-30 12:35:21" + }, { "name": "symfony-cmf/routing", "version": "1.0.0", diff --git a/core/lib/Thelia/Action/BaseCachedFile.php b/core/lib/Thelia/Action/BaseCachedFile.php new file mode 100644 index 000000000..b66496e07 --- /dev/null +++ b/core/lib/Thelia/Action/BaseCachedFile.php @@ -0,0 +1,178 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Action; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +use Thelia\Core\Event\CachedFileEvent; +use Thelia\Model\ConfigQuery; +use Thelia\Tools\URL; + +/** + * + * Cached file management actions. This class handles file caching in the web space + * + * Basically, files are stored outside the web space (by default in local/media/), + * and cached in the web space (by default in web/local/). + * + * In the file cache directory, a subdirectory for files categories (eg. product, category, folder, etc.) is + * automatically created, and the cached file is created here. Plugin may use their own subdirectory as required. + * + * A copy (or symbolic link, by default) of the original file is created in the cache. + * + * @package Thelia\Action + * @author Franck Allimant + * + */ +abstract class BaseCachedFile extends BaseAction +{ + /** + * @return string root of the file cache directory in web space + */ + protected abstract function getCacheDirFromWebRoot(); + + /** + * Clear the file cache. Is a subdirectory is specified, only this directory is cleared. + * If no directory is specified, the whole cache is cleared. + * Only files are deleted, directories will remain. + * + * @param CachedFileEvent $event + */ + public function clearCache(CachedFileEvent $event) + { + $path = $this->getCachePath($event->getCacheSubdirectory(), false); + + $this->clearDirectory($path); + } + + /** + * Recursively clears the specified directory. + * + * @param string $path the directory path + */ + protected function clearDirectory($path) + { + $iterator = new \DirectoryIterator($path); + + foreach ($iterator as $fileinfo) { + + if ($fileinfo->isDot()) continue; + + if ($fileinfo->isFile() || $fileinfo->isLink()) { + @unlink($fileinfo->getPathname()); + } elseif ($fileinfo->isDir()) { + $this->clearDirectory($fileinfo->getPathname()); + } + } + } + + /** + * Return the absolute URL to the cached file + * + * @param string $subdir the subdirectory related to cache base + * @param string $filename the safe filename, as returned by getCacheFilePath() + * @return string the absolute URL to the cached file + */ + protected function getCacheFileURL($subdir, $safe_filename) + { + $path = $this->getCachePathFromWebRoot($subdir); + + return URL::getInstance()->absoluteUrl(sprintf("%s/%s", $path, $safe_filename), null, URL::PATH_TO_FILE); + } + + /** + * Return the full path of the cached file + * + * @param string $subdir the subdirectory related to cache base + * @param string $filename the filename + * @param string $hashed_options a hash of transformation options, or null if no transformations have been applied + * @param boolean $forceOriginalDocument if true, the original file path in the cache dir is returned. + * @return string the cache directory path relative to Web Root + */ + protected function getCacheFilePath($subdir, $filename, $forceOriginalFile = false, $hashed_options = null) + { + $path = $this->getCachePath($subdir); + + $safe_filename = preg_replace("[^:alnum:\-\._]", "-", strtolower(basename($filename))); + + // Keep original safe name if no tranformations are applied + if ($forceOriginalFile || $hashed_options == null) + return sprintf("%s/%s", $path, $safe_filename); + else + return sprintf("%s/%s-%s", $path, $hashed_options, $safe_filename); + } + + /** + * Return the cache directory path relative to Web Root + * + * @param string $subdir the subdirectory related to cache base, or null to get the cache directory only. + * @return string the cache directory path relative to Web Root + */ + protected function getCachePathFromWebRoot($subdir = null) + { + $cache_dir_from_web_root = $this->getCacheDirFromWebRoot(); + + if ($subdir != null) { + $safe_subdir = basename($subdir); + + $path = sprintf("%s/%s", $cache_dir_from_web_root, $safe_subdir); + } else + $path = $cache_dir_from_web_root; + + // Check if path is valid, e.g. in the cache dir + return $path; + } + + /** + * Return the absolute cache directory path + * + * @param string $subdir the subdirectory related to cache base, or null to get the cache base directory. + * @throws \RuntimeException if cache directory cannot be created + * @return string the absolute cache directory path + */ + protected function getCachePath($subdir = null, $create_if_not_exists = true) + { + $cache_base = $this->getCachePathFromWebRoot($subdir); + + $web_root = rtrim(THELIA_WEB_DIR, '/'); + + $path = sprintf("%s/%s", $web_root, $cache_base); + + // Create directory (recursively) if it does not exists. + if ($create_if_not_exists && !is_dir($path)) { + if (!@mkdir($path, 0777, true)) { + throw new \RuntimeException(sprintf("Failed to create %s/%s file in cache directory", $cache_base)); + } + } + + // Check if path is valid, e.g. in the cache dir + $cache_base = realpath(sprintf("%s/%s", $web_root, $this->getCachePathFromWebRoot())); + + if (strpos(realpath($path), $cache_base) !== 0) { + throw new \InvalidArgumentException(sprintf("Invalid cache path %s, with subdirectory %s", $path, $subdir)); + } + + return $path; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Action/Category.php b/core/lib/Thelia/Action/Category.php index 297cd64da..a34f4a85e 100755 --- a/core/lib/Thelia/Action/Category.php +++ b/core/lib/Thelia/Action/Category.php @@ -24,52 +24,88 @@ namespace Thelia\Action; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Thelia\Core\Event\TheliaEvents; -use Thelia\Model\Category as CategoryModel; + use Thelia\Model\CategoryQuery; +use Thelia\Model\Category as CategoryModel; -use Propel\Runtime\ActiveQuery\Criteria; -use Propel\Runtime\Propel; -use Thelia\Model\Map\CategoryTableMap; +use Thelia\Core\Event\TheliaEvents; +use Thelia\Core\Event\CategoryUpdateEvent; use Thelia\Core\Event\CategoryCreateEvent; use Thelia\Core\Event\CategoryDeleteEvent; +use Thelia\Model\ConfigQuery; +use Thelia\Core\Event\UpdatePositionEvent; use Thelia\Core\Event\CategoryToggleVisibilityEvent; -use Thelia\Core\Event\CategoryChangePositionEvent; class Category extends BaseAction implements EventSubscriberInterface { + /** + * Create a new category entry + * + * @param CategoryCreateEvent $event + */ public function create(CategoryCreateEvent $event) { $category = new CategoryModel(); $category ->setDispatcher($this->getDispatcher()) - ->create( - $event->getTitle(), - $event->getParent(), - $event->getLocale() - ); + + ->setLocale($event->getLocale()) + ->setTitle($event->getTitle()) + ->setParent($event->getParent()) + ->setVisible($event->getVisible()) + + ->save() + ; $event->setCategory($category); } - public function update(CategoryChangeEvent $event) + /** + * Change a category + * + * @param CategoryUpdateEvent $event + */ + public function update(CategoryUpdateEvent $event) { + $search = CategoryQuery::create(); + + if (null !== $category = CategoryQuery::create()->findPk($event->getCategoryId())) { + + $category + ->setDispatcher($this->getDispatcher()) + + ->setLocale($event->getLocale()) + ->setTitle($event->getTitle()) + ->setDescription($event->getDescription()) + ->setChapo($event->getChapo()) + ->setPostscriptum($event->getPostscriptum()) + + ->setParent($event->getParent()) + ->setVisible($event->getVisible()) + + ->save(); + + $event->setCategory($category); + } } /** - * Delete a category + * Delete a category entry * - * @param ActionEvent $event + * @param CategoryDeleteEvent $event */ public function delete(CategoryDeleteEvent $event) { - $category = CategoryQuery::create()->findPk($event->getCategoryId()); + if (null !== $category = CategoryQuery::create()->findPk($event->getCategoryId())) { - if ($category !== null) { + $category + ->setDispatcher($this->getDispatcher()) + ->delete() + ; - $category->setDispatcher($this->getDispatcher())->delete(); + $event->setCategory($category); } } @@ -80,178 +116,48 @@ class Category extends BaseAction implements EventSubscriberInterface */ public function toggleVisibility(CategoryToggleVisibilityEvent $event) { - $category = CategoryQuery::create()->findPk($event->getCategoryId()); + $category = $event->getCategory(); - if ($category !== null) { - - $category - ->setDispatcher($this->getDispatcher()) - ->setVisible($category->getVisible() ? false : true) - ->save() + $category + ->setDispatcher($this->getDispatcher()) + ->setVisible($category->getVisible() ? false : true) + ->save() ; + } + + /** + * Changes position, selecting absolute ou relative change. + * + * @param CategoryChangePositionEvent $event + */ + public function updatePosition(UpdatePositionEvent $event) + { + if (null !== $category = CategoryQuery::create()->findPk($event->getObjectId())) { + + $category->setDispatcher($this->getDispatcher()); + + $mode = $event->getMode(); + + if ($mode == UpdatePositionEvent::POSITION_ABSOLUTE) + return $category->changeAbsolutePosition($event->getPosition()); + else if ($mode == UpdatePositionEvent::POSITION_UP) + return $category->movePositionUp(); + else if ($mode == UpdatePositionEvent::POSITION_DOWN) + return $category->movePositionDown(); } } /** - * Changes category position, selecting absolute ou relative change. - * - * @param CategoryChangePositionEvent $event - */ - public function changePosition(CategoryChangePositionEvent $event) - { - if ($event->getMode() == CategoryChangePositionEvent::POSITION_ABSOLUTE) - return $this->changeAbsolutePosition($event); - else - return $this->exchangePosition($event); - } - - /** - * Move up or down a category - * - * @param CategoryChangePositionEvent $event - */ - protected function exchangePosition(CategoryChangePositionEvent $event) - { - $category = CategoryQuery::create()->findPk($event->getCategoryId()); - - if ($category !== null) { - - // The current position of the category - $my_position = $category->getPosition(); - - // Find category to exchange position with - $search = CategoryQuery::create() - ->filterByParent($category->getParent()); - - // Up or down ? - if ($event->getMode() == CategoryChangePositionEvent::POSITION_UP) { - // Find the category immediately before me - $search->filterByPosition(array('max' => $my_position-1))->orderByPosition(Criteria::DESC); - } elseif ($event->getMode() == CategoryChangePositionEvent::POSITION_DOWN) { - // Find the category immediately after me - $search->filterByPosition(array('min' => $my_position+1))->orderByPosition(Criteria::ASC); - } else - - return; - - $result = $search->findOne(); - - // If we found the proper category, exchange their positions - if ($result) { - - $cnx = Propel::getWriteConnection(CategoryTableMap::DATABASE_NAME); - - $cnx->beginTransaction(); - - try { - $category - ->setDispatcher($this->getDispatcher()) - ->setPosition($result->getPosition()) - ->save() - ; - - $result->setPosition($my_position)->save(); - - $cnx->commit(); - } catch (Exception $e) { - $cnx->rollback(); - } - } - } - } - - /** - * Changes category position - * - * @param CategoryChangePositionEvent $event - */ - protected function changeAbsolutePosition(CategoryChangePositionEvent $event) - { - $category = CategoryQuery::create()->findPk($event->getCategoryId()); - - if ($category !== null) { - - // The required position - $new_position = $event->getPosition(); - - // The current position - $current_position = $category->getPosition(); - - if ($new_position != null && $new_position > 0 && $new_position != $current_position) { - - // Find categories to offset - $search = CategoryQuery::create()->filterByParent($category->getParent()); - - if ($new_position > $current_position) { - // The new position is after the current position -> we will offset + 1 all categories located between us and the new position - $search->filterByPosition(array('min' => 1+$current_position, 'max' => $new_position)); - - $delta = -1; - } else { - // The new position is brefore the current position -> we will offset - 1 all categories located between us and the new position - $search->filterByPosition(array('min' => $new_position, 'max' => $current_position - 1)); - - $delta = 1; - } - - $results = $search->find(); - - $cnx = Propel::getWriteConnection(CategoryTableMap::DATABASE_NAME); - - $cnx->beginTransaction(); - - try { - foreach ($results as $result) { - $result->setPosition($result->getPosition() + $delta)->save($cnx); - } - - $category - ->setDispatcher($this->getDispatcher()) - ->setPosition($new_position) - ->save($cnx) - ; - - $cnx->commit(); - } catch (Exception $e) { - $cnx->rollback(); - } - } - } - } - - /** - * Returns an array of event names this subscriber listens to. - * - * The array keys are event names and the value can be: - * - * * The method name to call (priority defaults to 0) - * * An array composed of the method name to call and the priority - * * An array of arrays composed of the method names to call and respective - * priorities, or 0 if unset - * - * For instance: - * - * * array('eventName' => 'methodName') - * * array('eventName' => array('methodName', $priority)) - * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) - * - * @return array The event names to listen to - * - * @api + * {@inheritDoc} */ public static function getSubscribedEvents() { return array( - TheliaEvents::CATEGORY_CREATE => array("create", 128), - TheliaEvents::CATEGORY_UPDATE => array("update", 128), - TheliaEvents::CATEGORY_DELETE => array("delete", 128), - + TheliaEvents::CATEGORY_CREATE => array("create", 128), + TheliaEvents::CATEGORY_UPDATE => array("update", 128), + TheliaEvents::CATEGORY_DELETE => array("delete", 128), TheliaEvents::CATEGORY_TOGGLE_VISIBILITY => array("toggleVisibility", 128), - TheliaEvents::CATEGORY_CHANGE_POSITION => array("changePosition", 128), - - "action.updateCategoryPositionU" => array("changePositionUp", 128), - "action.updateCategoryPositionDown" => array("changePositionDown", 128), - "action.updateCategoryPosition" => array("changePosition", 128), + TheliaEvents::CATEGORY_UPDATE_POSITION => array("updatePosition", 128) ); } } diff --git a/core/lib/Thelia/Action/Config.php b/core/lib/Thelia/Action/Config.php index 83df28524..f8a7c027c 100644 --- a/core/lib/Thelia/Action/Config.php +++ b/core/lib/Thelia/Action/Config.php @@ -22,7 +22,6 @@ /*************************************************************************************/ namespace Thelia\Action; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Thelia\Model\ConfigQuery; @@ -45,18 +44,9 @@ class Config extends BaseAction implements EventSubscriberInterface { $config = new ConfigModel(); - $config - ->setDispatcher($this->getDispatcher()) - - ->setName($event->getEventName()) - ->setValue($event->getValue()) - ->setLocale($event->getLocale()) - ->setTitle($event->getTitle()) - ->setHidden($event->getHidden()) - ->setSecured($event->getSecured()) - - ->save() - ; + $config->setDispatcher($this->getDispatcher())->setName($event->getEventName())->setValue($event->getValue()) + ->setLocale($event->getLocale())->setTitle($event->getTitle())->setHidden($event->getHidden()) + ->setSecured($event->getSecured())->save(); $event->setConfig($config); } @@ -70,18 +60,13 @@ class Config extends BaseAction implements EventSubscriberInterface { $search = ConfigQuery::create(); - if (null !== $config = $search->findOneById($event->getConfigId())) { + if (null !== $config = $search->findPk($event->getConfigId())) { if ($event->getValue() !== $config->getValue()) { - $config - ->setDispatcher($this->getDispatcher()) + $config->setDispatcher($this->getDispatcher())->setValue($event->getValue())->save(); - ->setValue($event->getValue()) - ->save() - ; - - $event->setConfig($config); + $event->setConfig($config); } } } @@ -95,23 +80,12 @@ class Config extends BaseAction implements EventSubscriberInterface { $search = ConfigQuery::create(); - if (null !== $config = ConfigQuery::create()->findOneById($event->getConfigId())) { + if (null !== $config = ConfigQuery::create()->findPk($event->getConfigId())) { - $config - ->setDispatcher($this->getDispatcher()) - - ->setName($event->getEventName()) - ->setValue($event->getValue()) - ->setHidden($event->getHidden()) - ->setSecured($event->getSecured()) - - ->setLocale($event->getLocale()) - ->setTitle($event->getTitle()) - ->setDescription($event->getDescription()) - ->setChapo($event->getChapo()) - ->setPostscriptum($event->getPostscriptum()) - - ->save(); + $config->setDispatcher($this->getDispatcher())->setName($event->getEventName())->setValue($event->getValue()) + ->setHidden($event->getHidden())->setSecured($event->getSecured())->setLocale($event->getLocale()) + ->setTitle($event->getTitle())->setDescription($event->getDescription())->setChapo($event->getChapo()) + ->setPostscriptum($event->getPostscriptum())->save(); $event->setConfig($config); } @@ -125,14 +99,11 @@ class Config extends BaseAction implements EventSubscriberInterface public function delete(ConfigDeleteEvent $event) { - if (null !== ($config = ConfigQuery::create()->findOneById($event->getConfigId()))) { + if (null !== ($config = ConfigQuery::create()->findPk($event->getConfigId()))) { - if (! $config->getSecured()) { + if (!$config->getSecured()) { - $config - ->setDispatcher($this->getDispatcher()) - ->delete() - ; + $config->setDispatcher($this->getDispatcher())->delete(); $event->setConfig($config); } @@ -145,10 +116,15 @@ class Config extends BaseAction implements EventSubscriberInterface public static function getSubscribedEvents() { return array( - TheliaEvents::CONFIG_CREATE => array("create", 128), - TheliaEvents::CONFIG_SETVALUE => array("setValue", 128), - TheliaEvents::CONFIG_UPDATE => array("modify", 128), - TheliaEvents::CONFIG_DELETE => array("delete", 128), + TheliaEvents::CONFIG_CREATE => array( + "create", 128 + ), TheliaEvents::CONFIG_SETVALUE => array( + "setValue", 128 + ), TheliaEvents::CONFIG_UPDATE => array( + "modify", 128 + ), TheliaEvents::CONFIG_DELETE => array( + "delete", 128 + ), ); } } diff --git a/core/lib/Thelia/Action/Currency.php b/core/lib/Thelia/Action/Currency.php index 7908d1f0d..946fee375 100644 --- a/core/lib/Thelia/Action/Currency.php +++ b/core/lib/Thelia/Action/Currency.php @@ -71,7 +71,7 @@ class Currency extends BaseAction implements EventSubscriberInterface { $search = CurrencyQuery::create(); - if (null !== $currency = CurrencyQuery::create()->findOneById($event->getCurrencyId())) { + if (null !== $currency = CurrencyQuery::create()->findPk($event->getCurrencyId())) { $currency ->setDispatcher($this->getDispatcher()) @@ -97,7 +97,7 @@ class Currency extends BaseAction implements EventSubscriberInterface { $search = CurrencyQuery::create(); - if (null !== $currency = CurrencyQuery::create()->findOneById($event->getCurrencyId())) { + if (null !== $currency = CurrencyQuery::create()->findPk($event->getCurrencyId())) { if ($currency->getByDefault() != $event->getIsDefault()) { @@ -123,7 +123,7 @@ class Currency extends BaseAction implements EventSubscriberInterface public function delete(CurrencyDeleteEvent $event) { - if (null !== ($currency = CurrencyQuery::create()->findOneById($event->getCurrencyId()))) { + if (null !== ($currency = CurrencyQuery::create()->findPk($event->getCurrencyId()))) { $currency ->setDispatcher($this->getDispatcher()) diff --git a/core/lib/Thelia/Action/Document.php b/core/lib/Thelia/Action/Document.php new file mode 100644 index 000000000..86fc51ac5 --- /dev/null +++ b/core/lib/Thelia/Action/Document.php @@ -0,0 +1,139 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Action; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +use Thelia\Core\Event\DocumentEvent; +use Thelia\Model\ConfigQuery; +use Thelia\Tools\URL; + +use Imagine\Document\ImagineInterface; +use Imagine\Document\DocumentInterface; +use Imagine\Document\Box; +use Imagine\Document\Color; +use Imagine\Document\Point; +use Thelia\Exception\DocumentException; +use Thelia\Core\Event\TheliaEvents; + +/** + * + * Document management actions. This class handles document processing an caching. + * + * Basically, documents are stored outside the web space (by default in local/media/documents), + * and cached in the web space (by default in web/local/documents). + * + * In the documents caches directory, a subdirectory for documents categories (eg. product, category, folder, etc.) is + * automatically created, and the cached document is created here. Plugin may use their own subdirectory as required. + * + * The cached document name contains a hash of the processing options, and the original (normalized) name of the document. + * + * A copy (or symbolic link, by default) of the original document is always created in the cache, so that the full + * resolution document is always available. + * + * Various document processing options are available : + * + * - resizing, with border, crop, or by keeping document aspect ratio + * - rotation, in degrees, positive or negative + * - background color, applyed to empty background when creating borders or rotating + * - effects. The effects are applied in the specified order. The following effects are available: + * - gamma:value : change the document Gamma to the specified value. Example: gamma:0.7 + * - grayscale or greyscale: switch document to grayscale + * - colorize:color : apply a color mask to the document. Exemple: colorize:#ff2244 + * - negative : transform the document in its negative equivalent + * - vflip or vertical_flip : vertical flip + * - hflip or horizontal_flip : horizontal flip + * + * If a problem occurs, an DocumentException may be thrown. + * + * @package Thelia\Action + * @author Franck Allimant + * + */ +class Document extends BaseCachedFile implements EventSubscriberInterface +{ + /** + * @return string root of the document cache directory in web space + */ + protected function getCacheDirFromWebRoot() { + return ConfigQuery::read('document_cache_dir_from_web_root', 'cache'); + } + + /** + * Process document and write the result in the document cache. + * + * When the original document is required, create either a symbolic link with the + * original document in the cache dir, or copy it in the cache dir if it's not already done. + * + * This method updates the cache_file_path and file_url attributes of the event + * + * @param DocumentEvent $event + * @throws \InvalidArgumentException, DocumentException + */ + public function processDocument(DocumentEvent $event) + { + $subdir = $event->getCacheSubdirectory(); + $source_file = $event->getSourceFilepath(); + + if (null == $subdir || null == $source_file) { + throw new \InvalidArgumentException("Cache sub-directory and source file path cannot be null"); + } + + $originalDocumentPathInCache = $this->getCacheFilePath($subdir, $source_file, true); + + if (! file_exists($originalDocumentPathInCache)) { + + if (! file_exists($source_file)) { + throw new DocumentException(sprintf("Source document file %s does not exists.", $source_file)); + } + + $mode = ConfigQuery::read('original_document_delivery_mode', 'symlink'); + + if ($mode == 'symlink') { + if (false == symlink($source_file, $originalDocumentPathInCache)) { + throw new DocumentException(sprintf("Failed to create symbolic link for %s in %s document cache directory", basename($source_file), $subdir)); + } + } else {// mode = 'copy' + if (false == @copy($source_file, $originalDocumentPathInCache)) { + throw new DocumentException(sprintf("Failed to copy %s in %s document cache directory", basename($source_file), $subdir)); + } + } + } + + // Compute the document URL + $document_url = $this->getCacheFileURL($subdir, basename($originalDocumentPathInCache)); + + // Update the event with file path and file URL + $event->setDocumentPath($originalDocumentPathInCache); + $event->setDocumentUrl(URL::getInstance()->absoluteUrl($document_url, null, URL::PATH_TO_FILE)); + } + + public static function getSubscribedEvents() + { + return array( + TheliaEvents::DOCUMENT_PROCESS => array("processDocument", 128), + TheliaEvents::DOCUMENT_CLEAR_CACHE => array("clearCache", 128), + ); + } +} diff --git a/core/lib/Thelia/Action/Image.php b/core/lib/Thelia/Action/Image.php index 66baffdbb..4660f93b8 100755 --- a/core/lib/Thelia/Action/Image.php +++ b/core/lib/Thelia/Action/Image.php @@ -71,7 +71,7 @@ use Thelia\Core\Event\TheliaEvents; * @author Franck Allimant * */ -class Image extends BaseAction implements EventSubscriberInterface +class Image extends BaseCachedFile implements EventSubscriberInterface { // Resize mode constants const EXACT_RATIO_WITH_BORDERS = 1; @@ -79,38 +79,10 @@ class Image extends BaseAction implements EventSubscriberInterface const KEEP_IMAGE_RATIO = 3; /** - * Clear the image cache. Is a subdirectory is specified, only this directory is cleared. - * If no directory is specified, the whole cache is cleared. - * Only files are deleted, directories will remain. - * - * @param ImageEvent $event + * @return string root of the image cache directory in web space */ - public function clearCache(ImageEvent $event) - { - $path = $this->getCachePath($event->getCacheSubdirectory(), false); - - $this->clearDirectory($path); - } - - /** - * Recursively clears the specified directory. - * - * @param string $path the directory path - */ - protected function clearDirectory($path) - { - $iterator = new \DirectoryIterator($path); - - foreach ($iterator as $fileinfo) { - - if ($fileinfo->isDot()) continue; - - if ($fileinfo->isFile() || $fileinfo->isLink()) { - @unlink($fileinfo->getPathname()); - } elseif ($fileinfo->isDir()) { - $this->clearDirectory($fileinfo->getPathname()); - } - } + protected function getCacheDirFromWebRoot() { + return ConfigQuery::read('image_cache_dir_from_web_root', 'cache'); } /** @@ -138,9 +110,9 @@ class Image extends BaseAction implements EventSubscriberInterface // echo basename($source_file).": "; // Find cached file path - $cacheFilePath = $this->getCacheFilePath($subdir, $source_file, $event); + $cacheFilePath = $this->getCacheFilePath($subdir, $source_file, $event->isOriginalImage(), $event->getOptionsHash()); - $originalImagePathInCache = $this->getCacheFilePath($subdir, $source_file, $event, true); + $originalImagePathInCache = $this->getCacheFilePath($subdir, $source_file, true); if (! file_exists($cacheFilePath)) { @@ -359,94 +331,6 @@ class Image extends BaseAction implements EventSubscriberInterface return $image; } - /** - * Return the absolute URL to the cached image - * - * @param string $subdir the subdirectory related to cache base - * @param string $filename the safe filename, as returned by getCacheFilePath() - * @return string the absolute URL to the cached image - */ - protected function getCacheFileURL($subdir, $safe_filename) - { - $path = $this->getCachePathFromWebRoot($subdir); - - return URL::getInstance()->absoluteUrl(sprintf("%s/%s", $path, $safe_filename), null, URL::PATH_TO_FILE); - } - - /** - * Return the full path of the cached file - * - * @param string $subdir the subdirectory related to cache base - * @param string $filename the filename - * @param boolean $forceOriginalImage if true, the origiunal image path in the cache dir is returned. - * @return string the cache directory path relative to Web Root - */ - protected function getCacheFilePath($subdir, $filename, ImageEvent $event, $forceOriginalImage = false) - { - $path = $this->getCachePath($subdir); - - $safe_filename = preg_replace("[^:alnum:\-\._]", "-", strtolower(basename($filename))); - - // Keep original safe name if no tranformations are applied - if ($forceOriginalImage || $event->isOriginalImage()) - return sprintf("%s/%s", $path, $safe_filename); - else - return sprintf("%s/%s-%s", $path, $event->getOptionsHash(), $safe_filename); - } - - /** - * Return the cache directory path relative to Web Root - * - * @param string $subdir the subdirectory related to cache base, or null to get the cache directory only. - * @return string the cache directory path relative to Web Root - */ - protected function getCachePathFromWebRoot($subdir = null) - { - $cache_dir_from_web_root = ConfigQuery::read('image_cache_dir_from_web_root', 'cache'); - - if ($subdir != null) { - $safe_subdir = basename($subdir); - - $path = sprintf("%s/%s", $cache_dir_from_web_root, $safe_subdir); - } else - $path = $cache_dir_from_web_root; - - // Check if path is valid, e.g. in the cache dir - return $path; - } - - /** - * Return the absolute cache directory path - * - * @param string $subdir the subdirectory related to cache base, or null to get the cache base directory. - * @throws \RuntimeException if cache directory cannot be created - * @return string the absolute cache directory path - */ - protected function getCachePath($subdir = null, $create_if_not_exists = true) - { - $cache_base = $this->getCachePathFromWebRoot($subdir); - - $web_root = rtrim(THELIA_WEB_DIR, '/'); - - $path = sprintf("%s/%s", $web_root, $cache_base); - - // Create directory (recursively) if it does not exists. - if ($create_if_not_exists && !is_dir($path)) { - if (!@mkdir($path, 0777, true)) { - throw new ImageException(sprintf("Failed to create %s/%s image cache directory", $cache_base)); - } - } - - // Check if path is valid, e.g. in the cache dir - $cache_base = realpath(sprintf("%s/%s", $web_root, $this->getCachePathFromWebRoot())); - - if (strpos(realpath($path), $cache_base) !== 0) { - throw new \InvalidArgumentException(sprintf("Invalid cache path %s, with subdirectory %s", $path, $subdir)); - } - - return $path; - } - /** * Create a new Imagine object using current driver configuration * diff --git a/core/lib/Thelia/Action/Message.php b/core/lib/Thelia/Action/Message.php index faac35bf2..8918913fe 100644 --- a/core/lib/Thelia/Action/Message.php +++ b/core/lib/Thelia/Action/Message.php @@ -70,7 +70,7 @@ class Message extends BaseAction implements EventSubscriberInterface { $search = MessageQuery::create(); - if (null !== $message = MessageQuery::create()->findOneById($event->getMessageId())) { + if (null !== $message = MessageQuery::create()->findPk($event->getMessageId())) { $message ->setDispatcher($this->getDispatcher()) @@ -99,7 +99,7 @@ class Message extends BaseAction implements EventSubscriberInterface public function delete(MessageDeleteEvent $event) { - if (null !== ($message = MessageQuery::create()->findOneById($event->getMessageId()))) { + if (null !== ($message = MessageQuery::create()->findPk($event->getMessageId()))) { $message ->setDispatcher($this->getDispatcher()) diff --git a/core/lib/Thelia/Config/Resources/config.xml b/core/lib/Thelia/Config/Resources/config.xml index 0eda0c37b..e8ea3a9d5 100755 --- a/core/lib/Thelia/Config/Resources/config.xml +++ b/core/lib/Thelia/Config/Resources/config.xml @@ -51,7 +51,7 @@
- + @@ -269,6 +269,10 @@ + + + + diff --git a/core/lib/Thelia/Config/Resources/routing/admin.xml b/core/lib/Thelia/Config/Resources/routing/admin.xml index dc650d8d6..53c137988 100755 --- a/core/lib/Thelia/Config/Resources/routing/admin.xml +++ b/core/lib/Thelia/Config/Resources/routing/admin.xml @@ -85,7 +85,7 @@ - Thelia\Controller\Admin\CategoryController::toggleOnlineAction + Thelia\Controller\Admin\CategoryController::setToggleVisibilityAction diff --git a/core/lib/Thelia/Controller/Admin/AbstractCrudController.php b/core/lib/Thelia/Controller/Admin/AbstractCrudController.php index fad774023..5b7f8b60a 100644 --- a/core/lib/Thelia/Controller/Admin/AbstractCrudController.php +++ b/core/lib/Thelia/Controller/Admin/AbstractCrudController.php @@ -240,6 +240,17 @@ abstract class AbstractCrudController extends BaseAdminController return null; } + /** + * Put in this method post object position change processing if required. + * + * @param unknown $deleteEvent the delete event + * @return Response a response, or null to continue normal processing + */ + protected function performAdditionalUpdatePositionAction($positionChangeEvent) + { + return null; + } + /** * Return the current list order identifier, updating it in the same time. */ @@ -309,14 +320,18 @@ abstract class AbstractCrudController extends BaseAdminController $this->adminLogAppend(sprintf("%s %s (ID %s) created", ucfirst($this->objectName), $this->getObjectLabel($createdObject), $this->getObjectId($createdObject))); } - $this->performAdditionalCreateAction($createEvent); + $response = $this->performAdditionalCreateAction($createEvent); - // Substitute _ID_ in the URL with the ID of the created object - $successUrl = str_replace('_ID_', $this->getObjectId($createdObject), $creationForm->getSuccessUrl()); - - // Redirect to the success URL - $this->redirect($successUrl); + if ($response == null) { + // Substitute _ID_ in the URL with the ID of the created object + $successUrl = str_replace('_ID_', $this->getObjectId($createdObject), $creationForm->getSuccessUrl()); + // Redirect to the success URL + $this->redirect($successUrl); + } + else { + return $response; + } } catch (FormValidationException $ex) { // Form cannot be validated @@ -396,16 +411,21 @@ abstract class AbstractCrudController extends BaseAdminController $this->adminLogAppend(sprintf("%s %s (ID %s) modified", ucfirst($this->objectName), $this->getObjectLabel($changedObject), $this->getObjectId($changedObject))); } - $this->performAdditionalUpdateAction($changeEvent); + $response = $this->performAdditionalUpdateAction($changeEvent); - // If we have to stay on the same page, do not redirect to the succesUrl, - // just redirect to the edit page again. - if ($this->getRequest()->get('save_mode') == 'stay') { - $this->redirectToEditionTemplate($this->getRequest()); + if ($response == null) { + // If we have to stay on the same page, do not redirect to the succesUrl, + // just redirect to the edit page again. + if ($this->getRequest()->get('save_mode') == 'stay') { + $this->redirectToEditionTemplate($this->getRequest()); + } + + // Redirect to the success URL + $this->redirect($changeForm->getSuccessUrl()); + } + else { + return $response; } - - // Redirect to the success URL - $this->redirect($changeForm->getSuccessUrl()); } catch (FormValidationException $ex) { // Form cannot be validated @@ -452,7 +472,14 @@ abstract class AbstractCrudController extends BaseAdminController return $this->errorPage($ex); } - $this->redirectToListTemplate(); + $response = $this->performAdditionalUpdatePositionAction($event); + + if ($response == null) { + $this->redirectToListTemplate(); + } + else { + return $response; + } } /** @@ -475,7 +502,7 @@ abstract class AbstractCrudController extends BaseAdminController return $this->errorPage($ex); } - $this->redirectToRoute('admin.categories.default'); + $this->redirectToListTemplate(); } /** diff --git a/core/lib/Thelia/Controller/Admin/AttributeAvController.php b/core/lib/Thelia/Controller/Admin/AttributeAvController.php index b3afa687d..1ab12a081 100644 --- a/core/lib/Thelia/Controller/Admin/AttributeAvController.php +++ b/core/lib/Thelia/Controller/Admin/AttributeAvController.php @@ -33,7 +33,7 @@ use Thelia\Form\AttributeAvCreationForm; use Thelia\Core\Event\UpdatePositionEvent; /** - * Manages attributes-av sent by mail + * Manages attributes-av * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/Admin/AttributeController.php b/core/lib/Thelia/Controller/Admin/AttributeController.php index 247b89632..48a65baa7 100644 --- a/core/lib/Thelia/Controller/Admin/AttributeController.php +++ b/core/lib/Thelia/Controller/Admin/AttributeController.php @@ -37,7 +37,7 @@ use Thelia\Core\Event\AttributeAvUpdateEvent; use Thelia\Core\Event\AttributeEvent; /** - * Manages attributes sent by mail + * Manages attributes * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/Admin/CategoryController.php b/core/lib/Thelia/Controller/Admin/CategoryController.php index fba832ec8..83d90c86e 100755 --- a/core/lib/Thelia/Controller/Admin/CategoryController.php +++ b/core/lib/Thelia/Controller/Admin/CategoryController.php @@ -23,226 +23,160 @@ namespace Thelia\Controller\Admin; -use Thelia\Core\Event\TheliaEvents; -use Thelia\Core\Event\CategoryCreateEvent; -use Thelia\Form\CategoryCreationForm; use Thelia\Core\Event\CategoryDeleteEvent; -use Thelia\Core\Event\CategoryUpdatePositionEvent; +use Thelia\Core\Event\TheliaEvents; +use Thelia\Core\Event\CategoryUpdateEvent; +use Thelia\Core\Event\CategoryCreateEvent; use Thelia\Model\CategoryQuery; use Thelia\Form\CategoryModificationForm; +use Thelia\Form\CategoryCreationForm; +use Thelia\Core\Event\UpdatePositionEvent; +use Thelia\Core\Event\CategoryToggleVisibilityEvent; -class CategoryController extends BaseAdminController +/** + * Manages categories + * + * @author Franck Allimant + */ +class CategoryController extends AbstractCrudController { - /** - * Render the categories list, ensuring the sort order is set. - * - * @return Symfony\Component\HttpFoundation\Response the response - */ - protected function renderList() - { - return $this->render('categories', $this->getTemplateArgs()); + public function __construct() { + parent::__construct( + 'category', + 'manual', + 'category_order', + + 'admin.categories.default', + 'admin.categories.create', + 'admin.categories.update', + 'admin.categories.delete', + + TheliaEvents::CATEGORY_CREATE, + TheliaEvents::CATEGORY_UPDATE, + TheliaEvents::CATEGORY_DELETE, + TheliaEvents::CATEGORY_TOGGLE_VISIBILITY, + TheliaEvents::CATEGORY_UPDATE_POSITION + ); } - protected function getTemplateArgs() - { - // Get the category ID - $category_id = $this->getRequest()->get('category_id', 0); + protected function getCreationForm() { + return new CategoryCreationForm($this->getRequest()); + } - // Find the current category order - $category_order = $this->getRequest()->get( - 'order', - $this->getSession()->get('admin.category_order', 'manual') + protected function getUpdateForm() { + return new CategoryModificationForm($this->getRequest()); + } + + protected function getCreationEvent($formData) { + $createEvent = new CategoryCreateEvent(); + + $createEvent + ->setTitle($formData['title']) + ->setLocale($formData["locale"]) + ->setParent($formData['parent']) + ->setVisible($formData['visible']) + ; + + return $createEvent; + } + + protected function getUpdateEvent($formData) { + $changeEvent = new CategoryUpdateEvent($formData['id']); + + // Create and dispatch the change event + $changeEvent + ->setLocale($formData['locale']) + ->setTitle($formData['title']) + ->setChapo($formData['chapo']) + ->setDescription($formData['description']) + ->setPostscriptum($formData['postscriptum']) + ->setVisible($formData['visible']) + ->setUrl($formData['url']) + ->setParent($formData['parent']) + ; + + return $changeEvent; + } + + protected function createUpdatePositionEvent($positionChangeMode, $positionValue) { + + return new UpdatePositionEvent( + $this->getRequest()->get('category_id', null), + $positionChangeMode, + $positionValue + ); + } + + protected function getDeleteEvent() { + return new CategoryDeleteEvent($this->getRequest()->get('category_id', 0)); + } + + protected function eventContainsObject($event) { + return $event->hasCategory(); + } + + protected function hydrateObjectForm($object) { + + // Prepare the data that will hydrate the form + $data = array( + 'id' => $object->getId(), + 'locale' => $object->getLocale(), + 'title' => $object->getTitle(), + 'chapo' => $object->getChapo(), + 'description' => $object->getDescription(), + 'postscriptum' => $object->getPostscriptum(), + 'visible' => $object->getVisible(), + 'url' => $object->getRewritenUrl($this->getCurrentEditionLocale()), + 'parent' => $object->getParent() ); - $args = array( - 'current_category_id' => $category_id, - 'category_order' => $category_order, + // Setup the object form + return new CategoryModificationForm($this->getRequest(), "form", $data); + } + + protected function getObjectFromEvent($event) { + return $event->hasCategory() ? $event->getCategory() : null; + } + + protected function getExistingObject() { + return CategoryQuery::create() + ->joinWithI18n($this->getCurrentEditionLocale()) + ->findOneById($this->getRequest()->get('category_id', 0)); + } + + protected function getObjectLabel($object) { + return $object->getTitle(); + } + + protected function getObjectId($object) { + return $object->getId(); + } + + protected function renderListTemplate($currentOrder) { + return $this->render('categories', + array( + 'category_order' => $currentOrder, + 'category_id' => $this->getRequest()->get('category_id', 0) + ) + ); + } + + protected function renderEditionTemplate() { + return $this->render('category-edit', array('category_id' => $this->getRequest()->get('category_id', 0))); + } + + protected function redirectToEditionTemplate() { + $this->redirectToRoute( + "admin.categories.update", + array('category_id' => $this->getRequest()->get('category_id', 0)) ); - - // Store the current sort order in session - $this->getSession()->set('admin.category_order', $category_order); - - return $args; } - /** - * The default action is displaying the categories list. - * - * @return Symfony\Component\HttpFoundation\Response the response - */ - public function defaultAction() - { - if (null !== $response = $this->checkAuth("admin.categories.view")) return $response; - return $this->renderList(); - } - - /** - * Create a new category object - * - * @return Symfony\Component\HttpFoundation\Response the response - */ - public function createAction() - { - // Check current user authorization - if (null !== $response = $this->checkAuth("admin.categories.create")) return $response; - - $error_msg = false; - - // Create the Creation Form - $creationForm = new CategoryCreationForm($this->getRequest()); - - try { - - // Validate the form, create the CategoryCreation event and dispatch it. - $form = $this->validateForm($creationForm, "POST"); - - $data = $form->getData(); - - $createEvent = new CategoryCreateEvent( - $data["title"], - $data["parent"], - $data["locale"] - ); - - $this->dispatch(TheliaEvents::CATEGORY_CREATE, $createEvent); - - if (! $createEvent->hasCategory()) throw new \LogicException($this->getTranslator()->trans("No category was created.")); - - $createdObject = $createEvent->getCategory(); - - // Log category creation - $this->adminLogAppend(sprintf("Category %s (ID %s) created", $createdObject->getTitle(), $createdObject->getId())); - - // Substitute _ID_ in the URL with the ID of the created object - $successUrl = str_replace('_ID_', $createdObject->getId(), $creationForm->getSuccessUrl()); - - // Redirect to the success URL - $this->redirect($successUrl); - } catch (FormValidationException $ex) { - // Form cannot be validated - $error_msg = $this->createStandardFormValidationErrorMessage($ex); - } catch (\Exception $ex) { - // Any other error - $error_msg = $ex->getMessage(); - } - - $this->setupFormErrorContext("category creation", $error_msg, $creationForm, $ex); - - // At this point, the form has error, and should be redisplayed. - return $this->renderList(); - } - - /** - * Load a category object for modification, and display the edit template. - * - * @return Symfony\Component\HttpFoundation\Response the response - */ - public function changeAction() - { - // Check current user authorization - if (null !== $response = $this->checkAuth("admin.categories.update")) return $response; - - // Load the category object - $category = CategoryQuery::create() - ->joinWithI18n($this->getCurrentEditionLocale()) - ->findOneById($this->getRequest()->get('category_id')); - - if ($category != null) { - - // Prepare the data that will hydrate the form - $data = array( - 'id' => $category->getId(), - 'locale' => $category->getLocale(), - 'title' => $category->getTitle(), - 'chapo' => $category->getChapo(), - 'description' => $category->getDescription(), - 'postscriptum' => $category->getPostscriptum(), - 'parent' => $category->getParent(), - 'visible' => $category->getVisible() ? true : false, - 'url' => $category->getUrl($this->getCurrentEditionLocale()) - // tbc !!! - ); - - // Setup the object form - $changeForm = new CategoryModificationForm($this->getRequest(), "form", $data); - - // Pass it to the parser - $this->getParserContext()->addForm($changeForm); - } - - // Render the edition template. - return $this->render('category-edit', $this->getTemplateArgs()); - } - - /** - * Save changes on a modified category object, and either go back to the category list, or stay on the edition page. - * - * @return Symfony\Component\HttpFoundation\Response the response - */ - public function saveChangeAction() - { - // Check current user authorization - if (null !== $response = $this->checkAuth("admin.categories.update")) return $response; - - $error_msg = false; - - // Create the form from the request - $changeForm = new CategoryModificationForm($this->getRequest()); - - // Get the category ID - $category_id = $this->getRequest()->get('category_id'); - - try { - - // Check the form against constraints violations - $form = $this->validateForm($changeForm, "POST"); - - // Get the form field values - $data = $form->getData(); - - $changeEvent = new CategoryUpdateEvent($data['id']); - - // Create and dispatch the change event - $changeEvent - ->setCategoryName($data['name']) - ->setLocale($data["locale"]) - ->setSymbol($data['symbol']) - ->setCode($data['code']) - ->setRate($data['rate']) - ; - - $this->dispatch(TheliaEvents::CATEGORY_UPDATE, $changeEvent); - - if (! $createEvent->hasCategory()) throw new \LogicException($this->getTranslator()->trans("No category was updated.")); - - // Log category modification - $changedObject = $changeEvent->getCategory(); - - $this->adminLogAppend(sprintf("Category %s (ID %s) modified", $changedObject->getTitle(), $changedObject->getId())); - - // If we have to stay on the same page, do not redirect to the succesUrl, - // just redirect to the edit page again. - if ($this->getRequest()->get('save_mode') == 'stay') { - $this->redirectToRoute( - "admin.categories.update", - array('category_id' => $category_id) + protected function redirectToListTemplate() { + $this->redirectToRoute( + 'admin.categories.default', + array('category_id' => $this->getRequest()->get('category_id', 0)) ); - } - - // Redirect to the success URL - $this->redirect($changeForm->getSuccessUrl()); - } catch (FormValidationException $ex) { - // Form cannot be validated - $error_msg = $this->createStandardFormValidationErrorMessage($ex); - } catch (\Exception $ex) { - // Any other error - $error_msg = $ex->getMessage(); - } - - $this->setupFormErrorContext("category modification", $error_msg, $changeForm, $ex); - - // At this point, the form has errors, and should be redisplayed. - return $this->render('category-edit', array('category_id' => $category_id)); } /** @@ -253,74 +187,41 @@ class CategoryController extends BaseAdminController // Check current user authorization if (null !== $response = $this->checkAuth("admin.categories.update")) return $response; - $changeEvent = new CategoryUpdateEvent($this->getRequest()->get('category_id', 0)); - - // Create and dispatch the change event - $changeEvent->setIsDefault(true); + $event = new CategoryToggleVisibilityEvent($this->getExistingObject()); try { - $this->dispatch(TheliaEvents::CATEGORY_SET_DEFAULT, $changeEvent); + $this->dispatch(TheliaEvents::CATEGORY_TOGGLE_VISIBILITY, $event); } catch (\Exception $ex) { // Any error return $this->errorPage($ex); } - $this->redirectToRoute('admin.categories.default'); + // Ajax response -> no action + return $this->nullResponse(); } - /** - * Update categoryposition - */ - public function updatePositionAction() + protected function performAdditionalDeleteAction($deleteEvent) { - // Check current user authorization - if (null !== $response = $this->checkAuth("admin.categories.update")) return $response; + // Redirect to parent category list + $this->redirectToRoute( + 'admin.categories.default', + array('category_id' => $deleteEvent->getCategory()->getParent()) + ); + } - try { - $mode = $this->getRequest()->get('mode', null); + protected function performAdditionalUpdatePositionAction($event) + { - if ($mode == 'up') - $mode = CategoryUpdatePositionEvent::POSITION_UP; - else if ($mode == 'down') - $mode = CategoryUpdatePositionEvent::POSITION_DOWN; - else - $mode = CategoryUpdatePositionEvent::POSITION_ABSOLUTE; + $category = CategoryQuery::create()->findPk($event->getObjectId()); - $position = $this->getRequest()->get('position', null); - - $event = new CategoryUpdatePositionEvent( - $this->getRequest()->get('category_id', null), - $mode, - $this->getRequest()->get('position', null) + if ($category != null) { + // Redirect to parent category list + $this->redirectToRoute( + 'admin.categories.default', + array('category_id' => $category->getParent()) ); - - $this->dispatch(TheliaEvents::CATEGORY_UPDATE_POSITION, $event); - } catch (\Exception $ex) { - // Any error - return $this->errorPage($ex); } - $this->redirectToRoute('admin.categories.default'); - } - - /** - * Delete a category object - * - * @return Symfony\Component\HttpFoundation\Response the response - */ - public function deleteAction() - { - // Check current user authorization - if (null !== $response = $this->checkAuth("admin.categories.delete")) return $response; - - // Get the category id, and dispatch the deleted request - $event = new CategoryDeleteEvent($this->getRequest()->get('category_id')); - - $this->dispatch(TheliaEvents::CATEGORY_DELETE, $event); - - if ($event->hasCategory()) - $this->adminLogAppend(sprintf("Category %s (ID %s) deleted", $event->getCategory()->getTitle(), $event->getCategory()->getId())); - - $this->redirectToRoute('admin.categories.default'); + return null; } } diff --git a/core/lib/Thelia/Controller/Admin/ConfigController.php b/core/lib/Thelia/Controller/Admin/ConfigController.php index ff0e4bb39..8701710b0 100644 --- a/core/lib/Thelia/Controller/Admin/ConfigController.php +++ b/core/lib/Thelia/Controller/Admin/ConfigController.php @@ -33,7 +33,7 @@ use Thelia\Form\ConfigCreationForm; use Thelia\Core\Event\UpdatePositionEvent; /** - * Manages variables sent by mail + * Manages variables * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/Admin/CurrencyController.php b/core/lib/Thelia/Controller/Admin/CurrencyController.php index 4f3fdaaea..f0081d698 100644 --- a/core/lib/Thelia/Controller/Admin/CurrencyController.php +++ b/core/lib/Thelia/Controller/Admin/CurrencyController.php @@ -33,7 +33,7 @@ use Thelia\Form\CurrencyCreationForm; use Thelia\Core\Event\UpdatePositionEvent; /** - * Manages currencies sent by mail + * Manages currencies * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/Admin/FeatureAvController.php b/core/lib/Thelia/Controller/Admin/FeatureAvController.php index 25c7a5495..fc6571ccc 100644 --- a/core/lib/Thelia/Controller/Admin/FeatureAvController.php +++ b/core/lib/Thelia/Controller/Admin/FeatureAvController.php @@ -33,7 +33,7 @@ use Thelia\Form\FeatureAvCreationForm; use Thelia\Core\Event\UpdatePositionEvent; /** - * Manages features-av sent by mail + * Manages features-av * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/Admin/FeatureController.php b/core/lib/Thelia/Controller/Admin/FeatureController.php index 92cb89d33..f81f55919 100644 --- a/core/lib/Thelia/Controller/Admin/FeatureController.php +++ b/core/lib/Thelia/Controller/Admin/FeatureController.php @@ -37,7 +37,7 @@ use Thelia\Core\Event\FeatureAvUpdateEvent; use Thelia\Core\Event\FeatureEvent; /** - * Manages features sent by mail + * Manages features * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/Admin/TemplateController.php b/core/lib/Thelia/Controller/Admin/TemplateController.php index 3685a359a..c9e30612c 100644 --- a/core/lib/Thelia/Controller/Admin/TemplateController.php +++ b/core/lib/Thelia/Controller/Admin/TemplateController.php @@ -41,7 +41,7 @@ use Thelia\Core\Event\TemplateAddFeatureEvent; use Thelia\Core\Event\TemplateDeleteFeatureEvent; /** - * Manages templates sent by mail + * Manages product templates * * @author Franck Allimant */ diff --git a/core/lib/Thelia/Controller/BaseController.php b/core/lib/Thelia/Controller/BaseController.php index 19e62a400..3c4c0a5bc 100755 --- a/core/lib/Thelia/Controller/BaseController.php +++ b/core/lib/Thelia/Controller/BaseController.php @@ -281,4 +281,16 @@ class BaseController extends ContainerAware $this->accessDenied(); } } + + /** + * + * return an instance of \Swift_Mailer with good Transporter configured. + * + * @return \Swift_Mailer + */ + public function getMailer() + { + $mailer = $this->container->get('mailer'); + return $mailer->getSwiftMailer(); + } } diff --git a/core/lib/Thelia/Core/Event/CachedFileEvent.php b/core/lib/Thelia/Core/Event/CachedFileEvent.php new file mode 100644 index 000000000..8f3a026d9 --- /dev/null +++ b/core/lib/Thelia/Core/Event/CachedFileEvent.php @@ -0,0 +1,94 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Event; + +class CachedFileEvent extends ActionEvent +{ + /** + * @var string The complete file name (with path) of the source file + */ + protected $source_filepath = null; + /** + * @var string The target subdirectory in the cache + */ + protected $cache_subdirectory = null; + + /** + * @var string The absolute URL of the cached file (in the web space) + */ + protected $file_url = null; + + /** + * @var string The absolute path of the cached file + */ + protected $cache_filepath = null; + + public function getFileUrl() + { + return $this->file_url; + } + + public function setFileUrl($file_url) + { + $this->file_url = $file_url; + + return $this; + } + + public function getCacheFilepath() + { + return $this->cache_filepath; + } + + public function setCacheFilepath($cache_filepath) + { + $this->cache_filepath = $cache_filepath; + + return $this; + } + + public function getSourceFilepath() + { + return $this->source_filepath; + } + + public function setSourceFilepath($source_filepath) + { + $this->source_filepath = $source_filepath; + + return $this; + } + + public function getCacheSubdirectory() + { + return $this->cache_subdirectory; + } + + public function setCacheSubdirectory($cache_subdirectory) + { + $this->cache_subdirectory = $cache_subdirectory; + + return $this; + } +} diff --git a/core/lib/Thelia/Core/Event/CategoryCreateEvent.php b/core/lib/Thelia/Core/Event/CategoryCreateEvent.php index 5a687217c..41529019c 100644 --- a/core/lib/Thelia/Core/Event/CategoryCreateEvent.php +++ b/core/lib/Thelia/Core/Event/CategoryCreateEvent.php @@ -28,13 +28,7 @@ class CategoryCreateEvent extends CategoryEvent protected $title; protected $parent; protected $locale; - - public function __construct($title, $parent, $locale) - { - $this->title = $title; - $this->parent = $parent; - $this->locale = $locale; - } + protected $visible; public function getTitle() { @@ -71,4 +65,16 @@ class CategoryCreateEvent extends CategoryEvent return $this; } + + public function getVisible() + { + return $this->visible; + } + + public function setVisible($visible) + { + $this->visible = $visible; + + return $this; + } } diff --git a/core/lib/Thelia/Core/Event/CategoryToggleVisibilityEvent.php b/core/lib/Thelia/Core/Event/CategoryToggleVisibilityEvent.php new file mode 100644 index 000000000..f90378e38 --- /dev/null +++ b/core/lib/Thelia/Core/Event/CategoryToggleVisibilityEvent.php @@ -0,0 +1,28 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Event; + +class CategoryToggleVisibilityEvent extends CategoryEvent +{ +} diff --git a/core/lib/Thelia/Core/Event/CategoryUpdateEvent.php b/core/lib/Thelia/Core/Event/CategoryUpdateEvent.php index 44e760549..f5f775a22 100644 --- a/core/lib/Thelia/Core/Event/CategoryUpdateEvent.php +++ b/core/lib/Thelia/Core/Event/CategoryUpdateEvent.php @@ -32,7 +32,6 @@ class CategoryUpdateEvent extends CategoryCreateEvent protected $postscriptum; protected $url; - protected $visibility; protected $parent; public function __construct($category_id) @@ -100,18 +99,6 @@ class CategoryUpdateEvent extends CategoryCreateEvent return $this; } - public function getVisibility() - { - return $this->visibility; - } - - public function setVisibility($visibility) - { - $this->visibility = $visibility; - - return $this; - } - public function getParent() { return $this->parent; diff --git a/core/lib/Thelia/Core/Event/DocumentEvent.php b/core/lib/Thelia/Core/Event/DocumentEvent.php new file mode 100644 index 000000000..f58d51083 --- /dev/null +++ b/core/lib/Thelia/Core/Event/DocumentEvent.php @@ -0,0 +1,67 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Event; + +class DocumentEvent extends CachedFileEvent +{ + protected $document_path; + protected $document_url; + + /** + * @return the document file path + */ + public function getDocumentPath() + { + return $this->document_path; + } + + /** + * @param string $document_path the document file path + */ + public function setDocumentPath($document_path) + { + $this->document_path = $document_path; + + return $this; + } + + /** + * @return the document URL + */ + public function getDocumentUrl() + { + return $this->document_url; + } + + /** + * @param string $document_url the document URL + */ + public function setDocumentUrl($document_url) + { + $this->document_url = $document_url; + + return $this; + } + +} diff --git a/core/lib/Thelia/Core/Event/ImageEvent.php b/core/lib/Thelia/Core/Event/ImageEvent.php index 5ea1581cd..462fa012d 100755 --- a/core/lib/Thelia/Core/Event/ImageEvent.php +++ b/core/lib/Thelia/Core/Event/ImageEvent.php @@ -23,22 +23,8 @@ namespace Thelia\Core\Event; -class ImageEvent extends ActionEvent +class ImageEvent extends CachedFileEvent { - /** - * @var string The complete file name (with path) of the source image - */ - protected $source_filepath = null; - /** - * @var string The target subdirectory in the image cache - */ - protected $cache_subdirectory = null; - - /** - * @var string The absolute URL of the cached image (in the web space) - */ - protected $file_url = null; - /** * @var string The absolute path of the cached image file */ @@ -121,6 +107,8 @@ class ImageEvent extends ActionEvent public function setCategory($category) { $this->category = $category; + + return $this; } public function getWidth() @@ -131,6 +119,8 @@ class ImageEvent extends ActionEvent public function setWidth($width) { $this->width = $width; + + return $this; } public function getHeight() @@ -141,6 +131,8 @@ class ImageEvent extends ActionEvent public function setHeight($height) { $this->height = $height; + + return $this; } public function getResizeMode() @@ -151,6 +143,8 @@ class ImageEvent extends ActionEvent public function setResizeMode($resize_mode) { $this->resize_mode = $resize_mode; + + return $this; } public function getBackgroundColor() @@ -161,6 +155,8 @@ class ImageEvent extends ActionEvent public function setBackgroundColor($background_color) { $this->background_color = $background_color; + + return $this; } public function getEffects() @@ -171,6 +167,8 @@ class ImageEvent extends ActionEvent public function setEffects(array $effects) { $this->effects = $effects; + + return $this; } public function getRotation() @@ -181,46 +179,8 @@ class ImageEvent extends ActionEvent public function setRotation($rotation) { $this->rotation = $rotation; - } - public function getFileUrl() - { - return $this->file_url; - } - - public function setFileUrl($file_url) - { - $this->file_url = $file_url; - } - - public function getCacheFilepath() - { - return $this->cache_filepath; - } - - public function setCacheFilepath($cache_filepath) - { - $this->cache_filepath = $cache_filepath; - } - - public function getSourceFilepath() - { - return $this->source_filepath; - } - - public function setSourceFilepath($source_filepath) - { - $this->source_filepath = $source_filepath; - } - - public function getCacheSubdirectory() - { - return $this->cache_subdirectory; - } - - public function setCacheSubdirectory($cache_subdirectory) - { - $this->cache_subdirectory = $cache_subdirectory; + return $this; } public function getQuality() @@ -231,6 +191,8 @@ class ImageEvent extends ActionEvent public function setQuality($quality) { $this->quality = $quality; + + return $this; } public function getOriginalFileUrl() @@ -241,6 +203,8 @@ class ImageEvent extends ActionEvent public function setOriginalFileUrl($original_file_url) { $this->original_file_url = $original_file_url; + + return $this; } public function getCacheOriginalFilepath() @@ -251,5 +215,7 @@ class ImageEvent extends ActionEvent public function setCacheOriginalFilepath($cache_original_filepath) { $this->cache_original_filepath = $cache_original_filepath; + + return $this; } } diff --git a/core/lib/Thelia/Core/Event/MailTransporterEvent.php b/core/lib/Thelia/Core/Event/MailTransporterEvent.php new file mode 100644 index 000000000..d47ac693f --- /dev/null +++ b/core/lib/Thelia/Core/Event/MailTransporterEvent.php @@ -0,0 +1,52 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Event; + + +/** + * Class MailTransporterEvent + * @package Thelia\Core\Event + * @author Manuel Raynaud + */ +class MailTransporterEvent extends ActionEvent { + /** + * @var \Swift_Transport + */ + protected $transporter; + + public function setMailerTransporter(\Swift_Transport $transporter) + { + $this->transporter = $transporter; + } + + public function getTransporter() + { + return $this->transporter; + } + + public function hasTransporter() + { + return null !== $this->transporter; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Event/TheliaEvents.php b/core/lib/Thelia/Core/Event/TheliaEvents.php index 02256a360..451eb89cb 100755 --- a/core/lib/Thelia/Core/Event/TheliaEvents.php +++ b/core/lib/Thelia/Core/Event/TheliaEvents.php @@ -145,50 +145,21 @@ final class TheliaEvents // -- END ADDRESS MANAGEMENT --------------------------------------------------------- - /** - * Sent once the category creation form has been successfully validated, and before category insertion in the database. - */ - const BEFORE_CREATECATEGORY = "action.before_createcategory"; + // -- Categories management ----------------------------------------------- - /** - * Create, change or delete a category - */ - const CATEGORY_CREATE = "action.createCategory"; - const CATEGORY_UPDATE = "action.updateCategory"; - const CATEGORY_DELETE = "action.deleteCategory"; - - /** - * Toggle category visibility - */ + const CATEGORY_CREATE = "action.createCategory"; + const CATEGORY_UPDATE = "action.updateCategory"; + const CATEGORY_DELETE = "action.deleteCategory"; const CATEGORY_TOGGLE_VISIBILITY = "action.toggleCategoryVisibility"; + const CATEGORY_UPDATE_POSITION = "action.updateCategoryPosition"; - /** - * Change category position - */ - const CATEGORY_CHANGE_POSITION = "action.updateCategoryPosition"; - - /** - * Sent just after a successful insert of a new category in the database. - */ + const BEFORE_CREATECATEGORY = "action.before_createcategory"; const AFTER_CREATECATEGORY = "action.after_createcategory"; - /** - * Sent befonre deleting a category - */ - const BEFORE_DELETECATEGORY = "action.before_deletecategory"; - /** - * Sent just after a successful delete of a category from the database. - */ + const BEFORE_DELETECATEGORY = "action.before_deletecategory"; const AFTER_DELETECATEGORY = "action.after_deletecategory"; - /** - * Sent just before a successful change of a category in the database. - */ const BEFORE_UPDATECATEGORY = "action.before_updateCategory"; - - /** - * Sent just after a successful change of a category in the database. - */ const AFTER_UPDATECATEGORY = "action.after_updateCategory"; /** @@ -230,6 +201,11 @@ final class TheliaEvents */ const IMAGE_PROCESS = "action.processImage"; + /** + * Sent on document processing + */ + const DOCUMENT_PROCESS = "action.processDocument"; + /** * Sent on image cache clear request */ @@ -437,4 +413,9 @@ final class TheliaEvents const BEFORE_DELETEFEATURE_AV = "action.before_deleteFeatureAv"; const AFTER_DELETEFEATURE_AV = "action.after_deleteFeatureAv"; + /** + * sent when system find a mailer transporter. + */ + const MAILTRANSPORTER_CONFIG = 'action.mailertransporter.config'; + } diff --git a/core/lib/Thelia/Core/Template/Loop/Category.php b/core/lib/Thelia/Core/Template/Loop/Category.php index 2b1156b98..bd1c32de2 100755 --- a/core/lib/Thelia/Core/Template/Loop/Category.php +++ b/core/lib/Thelia/Core/Template/Loop/Category.php @@ -173,6 +173,22 @@ class Category extends BaseI18nLoop $loopResult = new LoopResult($categories); foreach ($categories as $category) { + + // Find previous and next category + $previous = CategoryQuery::create() + ->filterByParent($category->getParent()) + ->filterByPosition($category->getPosition(), Criteria::LESS_THAN) + ->orderByPosition(Criteria::DESC) + ->findOne() + ; + + $next = CategoryQuery::create() + ->filterByParent($category->getParent()) + ->filterByPosition($category->getPosition(), Criteria::GREATER_THAN) + ->orderByPosition(Criteria::ASC) + ->findOne() + ; + /* * no cause pagination lost : * if ($this->getNotEmpty() && $category->countAllProducts() == 0) continue; @@ -193,7 +209,13 @@ class Category extends BaseI18nLoop ->set("PRODUCT_COUNT", $category->countChild()) ->set("VISIBLE", $category->getVisible() ? "1" : "0") ->set("POSITION", $category->getPosition()) - ; + + ->set("HAS_PREVIOUS", $previous != null ? 1 : 0) + ->set("HAS_NEXT" , $next != null ? 1 : 0) + + ->set("PREVIOUS", $previous != null ? $previous->getId() : -1) + ->set("NEXT" , $next != null ? $next->getId() : -1) + ; $loopResult->addRow($loopResultRow); } diff --git a/core/lib/Thelia/Core/Template/Loop/CategoryTree.php b/core/lib/Thelia/Core/Template/Loop/CategoryTree.php index afcd0410e..712767954 100755 --- a/core/lib/Thelia/Core/Template/Loop/CategoryTree.php +++ b/core/lib/Thelia/Core/Template/Loop/CategoryTree.php @@ -59,7 +59,7 @@ class CategoryTree extends BaseI18nLoop } // changement de rubrique - protected function buildCategoryTree($parent, $visible, $level, $max_level, array $exclude, LoopResult &$loopResult) + protected function buildCategoryTree($parent, $visible, $level, $max_level, $exclude, LoopResult &$loopResult) { if ($level > $max_level) return; @@ -73,7 +73,7 @@ class CategoryTree extends BaseI18nLoop if ($visible != BooleanOrBothType::ANY) $search->filterByVisible($visible); - $search->filterById($exclude, Criteria::NOT_IN); + if ($exclude != null) $search->filterById($exclude, Criteria::NOT_IN); $search->orderByPosition(Criteria::ASC); diff --git a/core/lib/Thelia/Core/Template/Loop/Document.php b/core/lib/Thelia/Core/Template/Loop/Document.php new file mode 100644 index 000000000..0e7e979c9 --- /dev/null +++ b/core/lib/Thelia/Core/Template/Loop/Document.php @@ -0,0 +1,277 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Template\Loop; +use Thelia\Core\Template\Element\BaseI18nLoop; +use Thelia\Core\Template\Loop\Argument\Argument; +use Thelia\Core\Event\DocumentEvent; +use Thelia\Core\Event\TheliaEvents; +use Thelia\Core\Template\Loop\Argument\ArgumentCollection; +use Thelia\Type\TypeCollection; +use Thelia\Type\EnumListType; +use Propel\Runtime\ActiveQuery\Criteria; +use Thelia\Model\ConfigQuery; +use Thelia\Core\Template\Element\LoopResultRow; +use Thelia\Core\Template\Element\LoopResult; +use Thelia\Type\EnumType; +use Thelia\Log\Tlog; + +/** + * The document loop + * + * @author Franck Allimant + */ +class Document extends BaseI18nLoop +{ + public $timestampable = true; + + /** + * @var array Possible document sources + */ + protected $possible_sources = array('category', 'product', 'folder', 'content'); + + /** + * @return \Thelia\Core\Template\Loop\Argument\ArgumentCollection + */ + protected function getArgDefinitions() + { + $collection = new ArgumentCollection( + + Argument::createIntListTypeArgument('id'), + Argument::createIntListTypeArgument('exclude'), + new Argument( + 'order', + new TypeCollection( + new EnumListType(array('alpha', 'alpha-reverse', 'manual', 'manual-reverse', 'random')) + ), + 'manual' + ), + Argument::createIntTypeArgument('lang'), + + Argument::createIntTypeArgument('category'), + Argument::createIntTypeArgument('product'), + Argument::createIntTypeArgument('folder'), + Argument::createIntTypeArgument('content'), + + new Argument( + 'source', + new TypeCollection( + new EnumType($this->possible_sources) + ) + ), + Argument::createIntTypeArgument('source_id') + ); + + // Add possible document sources + foreach ($this->possible_sources as $source) { + $collection->addArgument(Argument::createIntTypeArgument($source)); + } + + return $collection; + } + + /** + * Dynamically create the search query, and set the proper filter and order + * + * @param string $source a valid source identifier (@see $possible_sources) + * @param int $object_id the source object ID + * @return ModelCriteria the propel Query object + */ + protected function createSearchQuery($source, $object_id) + { + $object = ucfirst($source); + + $queryClass = sprintf("\Thelia\Model\%sDocumentQuery", $object); + $filterMethod = sprintf("filterBy%sId", $object); + + // xxxDocumentQuery::create() + $method = new \ReflectionMethod($queryClass, 'create'); + $search = $method->invoke(null); // Static ! + + // $query->filterByXXX(id) + if (! is_null($object_id)) { + $method = new \ReflectionMethod($queryClass, $filterMethod); + $method->invoke($search, $object_id); + } + + $orders = $this->getOrder(); + + // Results ordering + foreach ($orders as $order) { + switch ($order) { + case "alpha": + $search->addAscendingOrderByColumn('i18n_TITLE'); + break; + case "alpha-reverse": + $search->addDescendingOrderByColumn('i18n_TITLE'); + break; + case "manual-reverse": + $search->orderByPosition(Criteria::DESC); + break; + case "manual": + $search->orderByPosition(Criteria::ASC); + break; + case "random": + $search->clearOrderByColumns(); + $search->addAscendingOrderByColumn('RAND()'); + break(2); + break; + } + } + + return $search; + } + + /** + * Dynamically create the search query, and set the proper filter and order + * + * @param string $object_type (returned) the a valid source identifier (@see $possible_sources) + * @param string $object_id (returned) the ID of the source object + * @return ModelCriteria the propel Query object + */ + protected function getSearchQuery(&$object_type, &$object_id) + { + $search = null; + + // Check form source="product" source_id="123" style arguments + $source = $this->getSource(); + + if (! is_null($source)) { + + $source_id = $this->getSourceId(); + $id = $this->getId(); + + // echo "source = ".$this->getSource().", id=".$source_id." - ".$this->getArg('source_id')->getValue()."
"; + + if (is_null($source_id) && is_null($id)) { + throw new \InvalidArgumentException("If 'source' argument is specified, 'id' or 'source_id' argument should be specified"); + } + + $search = $this->createSearchQuery($source, $source_id); + + $object_type = $source; + $object_id = $source_id; + } else { + // Check for product="id" folder="id", etc. style arguments + foreach ($this->possible_sources as $source) { + + $argValue = intval($this->getArgValue($source)); + + if ($argValue > 0) { + + $search = $this->createSearchQuery($source, $argValue); + + $object_type = $source; + $object_id = $argValue; + + break; + } + } + } + + if ($search == null) + throw new \InvalidArgumentException(sprintf("Unable to find document source. Valid sources are %s", implode(',', $this->possible_sources))); + + return $search; + } + + /** + * @param unknown $pagination + */ + public function exec(&$pagination) + { + // Select the proper query to use, and get the object type + $object_type = $object_id = null; + + $search = $this->getSearchQuery($object_type, $object_id); + + /* manage translations */ + $locale = $this->configureI18nProcessing($search); + + $id = $this->getId(); + + if (! is_null($id)) { + $search->filterById($id, Criteria::IN); + } + + $exclude = $this->getExclude(); + if (!is_null($exclude)) + $search->filterById($exclude, Criteria::NOT_IN); + + // Create document processing event + $event = new DocumentEvent($this->request); + + // echo "sql=".$search->toString(); + + $results = $this->search($search, $pagination); + + $loopResult = new LoopResult($results); + + foreach ($results as $result) { + + // Create document processing event + $event = new DocumentEvent($this->request); + + // Put source document file path + $source_filepath = sprintf("%s%s/%s/%s", + THELIA_ROOT, + ConfigQuery::read('documents_library_path', 'local/media/documents'), + $object_type, + $result->getFile() + ); + + $event->setSourceFilepath($source_filepath); + $event->setCacheSubdirectory($object_type); + + try { + // Dispatch document processing event + $this->dispatcher->dispatch(TheliaEvents::DOCUMENT_PROCESS, $event); + + $loopResultRow = new LoopResultRow($loopResult, $result, $this->versionable, $this->timestampable, $this->countable); + + $loopResultRow + ->set("ID" , $result->getId()) + ->set("LOCALE" ,$locale) + ->set("DOCUMENT_URL" , $event->getFileUrl()) + ->set("DOCUMENT_PATH" , $event->getCacheFilepath()) + ->set("ORIGINAL_DOCUMENT_PATH", $source_filepath) + ->set("TITLE" , $result->getVirtualColumn('i18n_TITLE')) + ->set("CHAPO" , $result->getVirtualColumn('i18n_CHAPO')) + ->set("DESCRIPTION" , $result->getVirtualColumn('i18n_DESCRIPTION')) + ->set("POSTSCRIPTUM" , $result->getVirtualColumn('i18n_POSTSCRIPTUM')) + ->set("POSITION" , $result->getPosition()) + ->set("OBJECT_TYPE" , $object_type) + ->set("OBJECT_ID" , $object_id) + ; + + $loopResult->addRow($loopResultRow); + } + catch (\Exception $ex) { + // Ignore the result and log an error + Tlog::getInstance()->addError("Failed to process document in document loop: ", $this->args); + } + } + + return $loopResult; + } +} diff --git a/core/lib/Thelia/Core/Template/Loop/Image.php b/core/lib/Thelia/Core/Template/Loop/Image.php index c7714731b..c85d41fc7 100755 --- a/core/lib/Thelia/Core/Template/Loop/Image.php +++ b/core/lib/Thelia/Core/Template/Loop/Image.php @@ -123,8 +123,10 @@ class Image extends BaseI18nLoop $search = $method->invoke(null); // Static ! // $query->filterByXXX(id) - $method = new \ReflectionMethod($queryClass, $filterMethod); - $method->invoke($search, $object_id); + if (! is_null($object_id)) { + $method = new \ReflectionMethod($queryClass, $filterMethod); + $method->invoke($search, $object_id); + } $orders = $this->getOrder(); @@ -171,11 +173,12 @@ class Image extends BaseI18nLoop if (! is_null($source)) { $source_id = $this->getSourceId(); + $id = $this->getId(); - // echo "source = ".$this->getSource().", id=".$source_id." - ".$this->getArg('source_id')->getValue()."
"; + //echo "source = ".$this->getSource()."source_id=$source_id, id=$id
"; - if (is_null($source_id)) { - throw new \InvalidArgumentException("'source_id' argument cannot be null if 'source' argument is specified."); + if (is_null($source_id) && is_null($id)) { + throw new \InvalidArgumentException("If 'source' argument is specified, 'id' or 'source_id' argument should be specified"); } $search = $this->createSearchQuery($source, $source_id); @@ -259,14 +262,13 @@ class Image extends BaseI18nLoop } - // echo "sql=".$search->toString(); + //echo "sql=".$search->toString(); $results = $this->search($search, $pagination); $loopResult = new LoopResult($results); foreach ($results as $result) { - // Create image processing event $event = new ImageEvent($this->request); @@ -282,7 +284,7 @@ class Image extends BaseI18nLoop // Put source image file path $source_filepath = sprintf("%s%s/%s/%s", THELIA_ROOT, - ConfigQuery::read('documents_library_path', 'local/media/images'), + ConfigQuery::read('images_library_path', 'local/media/images'), $object_type, $result->getFile() ); diff --git a/core/lib/Thelia/Exception/DocumentException.php b/core/lib/Thelia/Exception/DocumentException.php new file mode 100644 index 000000000..9727a4267 --- /dev/null +++ b/core/lib/Thelia/Exception/DocumentException.php @@ -0,0 +1,36 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Exception; + +use Thelia\Log\Tlog; + +class DocumentException extends \RuntimeException +{ + public function __construct($message, $code = null, $previous = null) + { + Tlog::getInstance()->addError($message); + + parent::__construct($message, $code, $previous); + } +} diff --git a/core/lib/Thelia/Form/CategoryCreationForm.php b/core/lib/Thelia/Form/CategoryCreationForm.php index 5dce6a049..172b09ee6 100755 --- a/core/lib/Thelia/Form/CategoryCreationForm.php +++ b/core/lib/Thelia/Form/CategoryCreationForm.php @@ -39,7 +39,8 @@ class CategoryCreationForm extends BaseForm "for" => "title" ) )) - ->add("parent", "integer", array( + ->add("parent", "text", array( + "label" => Translator::getInstance()->trans("Parent category *"), "constraints" => array( new NotBlank() ) @@ -49,6 +50,9 @@ class CategoryCreationForm extends BaseForm new NotBlank() ) )) + ->add("visible", "integer", array( + "label" => Translator::getInstance()->trans("This category is online on the front office.") + )) ; } diff --git a/core/lib/Thelia/Form/CategoryModificationForm.php b/core/lib/Thelia/Form/CategoryModificationForm.php index d9de36d16..42b5893c1 100644 --- a/core/lib/Thelia/Form/CategoryModificationForm.php +++ b/core/lib/Thelia/Form/CategoryModificationForm.php @@ -24,6 +24,7 @@ namespace Thelia\Form; use Symfony\Component\Validator\Constraints\GreaterThan; use Thelia\Core\Translation\Translator; +use Symfony\Component\Validator\Constraints\NotBlank; class CategoryModificationForm extends CategoryCreationForm { @@ -36,12 +37,13 @@ class CategoryModificationForm extends CategoryCreationForm $this->formBuilder ->add("id", "hidden", array("constraints" => array(new GreaterThan(array('value' => 0))))) - ->add("visible", "checkbox", array( - "label" => Translator::getInstance()->trans("This category is online on the front office.") + ->add("url", "text", array( + "label" => Translator::getInstance()->trans("Rewriten URL *"), + "constraints" => array(new NotBlank()) )) ; - // Add standard description fields + // Add standard description fields, excluding title and locale, which a re defined in parent class $this->addStandardDescFields(array('title', 'locale')); } diff --git a/core/lib/Thelia/Mailer/MailerFactory.php b/core/lib/Thelia/Mailer/MailerFactory.php new file mode 100644 index 000000000..a327a155f --- /dev/null +++ b/core/lib/Thelia/Mailer/MailerFactory.php @@ -0,0 +1,91 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Mailer; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Thelia\Core\Event\MailTransporterEvent; +use Thelia\Core\Event\TheliaEvents; +use Thelia\Model\ConfigQuery; + + +/** + * Class MailerFactory + * @package Thelia\Mailer + * @author Manuel Raynaud + */ +class MailerFactory { + /** + * @var \Swift_Mailer + */ + protected $swiftMailer; + + protected $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher) + { + + $this->dispatcher = $dispatcher; + + $transporterEvent = new MailTransporterEvent(); + $this->dispatcher->dispatch(TheliaEvents::MAILTRANSPORTER_CONFIG, $transporterEvent); + + if($transporterEvent->hasTransporter()) { + $transporter = $transporterEvent->getTransporter(); + } else { + if (ConfigQuery::read("smtp.enabled")) { + $transporter = $this->configureSmtp(); + } else { + $transporter = \Swift_MailTransport::newInstance(); + } + } + + $this->swiftMailer = new \Swift_Mailer($transporter); + } + + private function configureSmtp() + { + $smtpTransporter = new \Swift_SmtpTransport(); + $smtpTransporter->setHost(Configquery::read('smtp.host', 'localhost')) + ->setPort(ConfigQuery::read('smtp.host')) + ->setEncryption(ConfigQuery::read('smtp.encryption')) + ->setUsername(ConfigQuery::read('smtp.username')) + ->setPassword(ConfigQuery::read('smtp.password')) + ->setAuthMode(ConfigQuery::read('smtp.authmode')) + ->setTimeout(ConfigQuery::read('smtp.timeout', 30)) + ->setSourceIp(ConfigQuery::read('smtp.sourceip')) + ; + return $smtpTransporter; + } + + public function send(\Swift_Mime_Message $message, &$failedRecipients = null) + { + $this->swiftMailer->send($message, $failedRecipients); + } + + public function getSwiftMailer() + { + return $this->swiftMailer; + } + + +} \ No newline at end of file diff --git a/core/lib/Thelia/Model/Category.php b/core/lib/Thelia/Model/Category.php index ced10c94b..042864de0 100755 --- a/core/lib/Thelia/Model/Category.php +++ b/core/lib/Thelia/Model/Category.php @@ -15,6 +15,8 @@ class Category extends BaseCategory use \Thelia\Model\Tools\PositionManagementTrait; + use \Thelia\Model\Tools\UrlRewritingTrait; + /** * @return int number of child for the current category */ @@ -23,30 +25,12 @@ class Category extends BaseCategory return CategoryQuery::countChild($this->getId()); } - public function getUrl($locale) - { - return URL::getInstance()->retrieve('category', $this->getId(), $locale)->toString(); - } - /** - * Create a new category. - * - * @param string $title the category title - * @param int $parent the ID of the parent category - * @param string $locale the locale of the title + * {@inheritDoc} */ - public function create($title, $parent, $locale) - { - $this - ->setLocale($locale) - ->setTitle($title) - ->setParent($parent) - ->setVisible(1) - ->setPosition($this->getNextPosition($parent)) - ; - - $this->save(); - } + protected function getRewritenUrlViewName() { + return 'category'; + } /** * @@ -71,18 +55,38 @@ class Category extends BaseCategory return $countProduct; } + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByParent($this->getParent()); + } + + /** + * {@inheritDoc} + */ public function preInsert(ConnectionInterface $con = null) { + $this->setPosition($this->getNextPosition()); + + $this->generateRewritenUrl($this->getLocale()); + $this->dispatchEvent(TheliaEvents::BEFORE_CREATECATEGORY, new CategoryEvent($this)); return true; } + /** + * {@inheritDoc} + */ public function postInsert(ConnectionInterface $con = null) { $this->dispatchEvent(TheliaEvents::AFTER_CREATECATEGORY, new CategoryEvent($this)); } + /** + * {@inheritDoc} + */ public function preUpdate(ConnectionInterface $con = null) { $this->dispatchEvent(TheliaEvents::BEFORE_UPDATECATEGORY, new CategoryEvent($this)); @@ -90,16 +94,27 @@ class Category extends BaseCategory return true; } + /** + * {@inheritDoc} + */ public function postUpdate(ConnectionInterface $con = null) { $this->dispatchEvent(TheliaEvents::AFTER_UPDATECATEGORY, new CategoryEvent($this)); } + /** + * {@inheritDoc} + */ public function preDelete(ConnectionInterface $con = null) { $this->dispatchEvent(TheliaEvents::BEFORE_DELETECATEGORY, new CategoryEvent($this)); + + return true; } + /** + * {@inheritDoc} + */ public function postDelete(ConnectionInterface $con = null) { $this->dispatchEvent(TheliaEvents::AFTER_DELETECATEGORY, new CategoryEvent($this)); diff --git a/core/lib/Thelia/Model/CategoryDocument.php b/core/lib/Thelia/Model/CategoryDocument.php index bd86f1d1e..73d99f591 100755 --- a/core/lib/Thelia/Model/CategoryDocument.php +++ b/core/lib/Thelia/Model/CategoryDocument.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\CategoryDocument as BaseCategoryDocument; +use Propel\Runtime\Connection\ConnectionInterface; - class CategoryDocument extends BaseCategoryDocument +class CategoryDocument extends BaseCategoryDocument { + use \Thelia\Model\Tools\PositionManagementTrait; + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByCategory($this->getCategory()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/CategoryImage.php b/core/lib/Thelia/Model/CategoryImage.php index ef555f83e..5bf964e10 100755 --- a/core/lib/Thelia/Model/CategoryImage.php +++ b/core/lib/Thelia/Model/CategoryImage.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\CategoryImage as BaseCategoryImage; +use Propel\Runtime\Connection\ConnectionInterface; - class CategoryImage extends BaseCategoryImage +class CategoryImage extends BaseCategoryImage { + use \Thelia\Model\Tools\PositionManagementTrait; + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByCategory($this->getCategory()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/Content.php b/core/lib/Thelia/Model/Content.php index 5bc0ba6b2..79660f93a 100755 --- a/core/lib/Thelia/Model/Content.php +++ b/core/lib/Thelia/Model/Content.php @@ -4,11 +4,41 @@ namespace Thelia\Model; use Thelia\Model\Base\Content as BaseContent; use Thelia\Tools\URL; +use Propel\Runtime\Connection\ConnectionInterface; class Content extends BaseContent { - public function getUrl($locale) + use \Thelia\Model\Tools\ModelEventDispatcherTrait; + + use \Thelia\Model\Tools\PositionManagementTrait; + + use \Thelia\Model\Tools\UrlRewritingTrait; + + /** + * {@inheritDoc} + */ + protected function getRewritenUrlViewName() { + return 'content'; + } + + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + + // TODO: Find the default folder for this content, + // and generate the position relative to this folder + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) { - return URL::getInstance()->retrieve('content', $this->getId(), $locale)->toString(); + $this->setPosition($this->getNextPosition()); + + $this->generateRewritenUrl($this->getLocale()); + + return true; } } diff --git a/core/lib/Thelia/Model/ContentDocument.php b/core/lib/Thelia/Model/ContentDocument.php index 933a089cb..8ecf3a3a9 100755 --- a/core/lib/Thelia/Model/ContentDocument.php +++ b/core/lib/Thelia/Model/ContentDocument.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\ContentDocument as BaseContentDocument; +use Propel\Runtime\Connection\ConnectionInterface; - class ContentDocument extends BaseContentDocument +class ContentDocument extends BaseContentDocument { + use \Thelia\Model\Tools\PositionManagementTrait; + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByContent($this->getContent()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/ContentImage.php b/core/lib/Thelia/Model/ContentImage.php index 3020e48f3..ac1dcf755 100755 --- a/core/lib/Thelia/Model/ContentImage.php +++ b/core/lib/Thelia/Model/ContentImage.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\ContentImage as BaseContentImage; +use Propel\Runtime\Connection\ConnectionInterface; - class ContentImage extends BaseContentImage +class ContentImage extends BaseContentImage { + use \Thelia\Model\Tools\PositionManagementTrait; -} + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByContent($this->getContent()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Model/Folder.php b/core/lib/Thelia/Model/Folder.php index 8151dae0e..1e73e7e23 100755 --- a/core/lib/Thelia/Model/Folder.php +++ b/core/lib/Thelia/Model/Folder.php @@ -4,9 +4,23 @@ namespace Thelia\Model; use Thelia\Model\Base\Folder as BaseFolder; use Thelia\Tools\URL; +use Propel\Runtime\Connection\ConnectionInterface; class Folder extends BaseFolder { + use \Thelia\Model\Tools\ModelEventDispatcherTrait; + + use \Thelia\Model\Tools\PositionManagementTrait; + + use \Thelia\Model\Tools\UrlRewritingTrait; + + /** + * {@inheritDoc} + */ + protected function getRewritenUrlViewName() { + return 'folder'; + } + /** * @return int number of contents for the folder */ @@ -15,11 +29,6 @@ class Folder extends BaseFolder return FolderQuery::countChild($this->getId()); } - public function getUrl($locale) - { - return URL::getInstance()->retrieve('folder', $this->getId(), $locale)->toString(); - } - /** * * count all products for current category and sub categories @@ -43,4 +52,23 @@ class Folder extends BaseFolder return $contentsCount; } + + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByParent($this->getParent()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + $this->generateRewritenUrl($this->getLocale()); + + return true; + } } diff --git a/core/lib/Thelia/Model/FolderDocument.php b/core/lib/Thelia/Model/FolderDocument.php index c9644835e..0a86995d2 100755 --- a/core/lib/Thelia/Model/FolderDocument.php +++ b/core/lib/Thelia/Model/FolderDocument.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\FolderDocument as BaseFolderDocument; +use Propel\Runtime\Connection\ConnectionInterface; - class FolderDocument extends BaseFolderDocument +class FolderDocument extends BaseFolderDocument { + use \Thelia\Model\Tools\PositionManagementTrait; + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByFolder($this->getFolder()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/FolderImage.php b/core/lib/Thelia/Model/FolderImage.php index 4e6d285f8..58d8f928e 100755 --- a/core/lib/Thelia/Model/FolderImage.php +++ b/core/lib/Thelia/Model/FolderImage.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\FolderImage as BaseFolderImage; +use Propel\Runtime\Connection\ConnectionInterface; - class FolderImage extends BaseFolderImage +class FolderImage extends BaseFolderImage { + use \Thelia\Model\Tools\PositionManagementTrait; + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByFolder($this->getFolder()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/Product.php b/core/lib/Thelia/Model/Product.php index 28951fb44..ca36fd5e7 100755 --- a/core/lib/Thelia/Model/Product.php +++ b/core/lib/Thelia/Model/Product.php @@ -6,19 +6,29 @@ use Propel\Runtime\Exception\PropelException; use Thelia\Model\Base\Product as BaseProduct; use Thelia\Tools\URL; use Thelia\TaxEngine\Calculator; +use Propel\Runtime\Connection\ConnectionInterface; class Product extends BaseProduct { - public function getUrl($locale) - { - return URL::getInstance()->retrieve('product', $this->getId(), $locale)->toString(); + use \Thelia\Model\Tools\ModelEventDispatcherTrait; + + use \Thelia\Model\Tools\PositionManagementTrait; + + use \Thelia\Model\Tools\UrlRewritingTrait; + + /** + * {@inheritDoc} + */ + protected function getRewritenUrlViewName() { + return 'product'; } public function getRealLowestPrice($virtualColumnName = 'real_lowest_price') { try { $amount = $this->getVirtualColumn($virtualColumnName); - } catch(PropelException $e) { + } + catch(PropelException $e) { throw new PropelException("Virtual column `$virtualColumnName` does not exist in Product::getRealLowestPrice"); } @@ -30,4 +40,26 @@ class Product extends BaseProduct $taxCalculator = new Calculator(); return round($taxCalculator->load($this, $country)->getTaxedPrice($this->getRealLowestPrice()), 2); } + + /** + * Calculate next position relative to our default category + */ + protected function addCriteriaToPositionQuery($query) { + + // TODO: Find the default category for this product, + // and generate the position relative to this category + + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + $this->generateRewritenUrl($this->getLocale()); + + return true; + } } diff --git a/core/lib/Thelia/Model/ProductDocument.php b/core/lib/Thelia/Model/ProductDocument.php index a49c4f11e..53515ff3c 100755 --- a/core/lib/Thelia/Model/ProductDocument.php +++ b/core/lib/Thelia/Model/ProductDocument.php @@ -3,8 +3,27 @@ namespace Thelia\Model; use Thelia\Model\Base\ProductDocument as BaseProductDocument; +use Propel\Runtime\Connection\ConnectionInterface; - class ProductDocument extends BaseProductDocument +class ProductDocument extends BaseProductDocument { + use \Thelia\Model\Tools\PositionManagementTrait; + + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByProduct($this->getProduct()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/ProductImage.php b/core/lib/Thelia/Model/ProductImage.php index 9fe5b78e0..4bf0c40a6 100755 --- a/core/lib/Thelia/Model/ProductImage.php +++ b/core/lib/Thelia/Model/ProductImage.php @@ -3,8 +3,26 @@ namespace Thelia\Model; use Thelia\Model\Base\ProductImage as BaseProductImage; +use Propel\Runtime\Connection\ConnectionInterface; - class ProductImage extends BaseProductImage +class ProductImage extends BaseProductImage { + use \Thelia\Model\Tools\PositionManagementTrait; + /** + * Calculate next position relative to our parent + */ + protected function addCriteriaToPositionQuery($query) { + $query->filterByProduct($this->getProduct()); + } + + /** + * {@inheritDoc} + */ + public function preInsert(ConnectionInterface $con = null) + { + $this->setPosition($this->getNextPosition()); + + return true; + } } diff --git a/core/lib/Thelia/Model/Tools/UrlRewritingTrait.php b/core/lib/Thelia/Model/Tools/UrlRewritingTrait.php new file mode 100644 index 000000000..6ab24fbc4 --- /dev/null +++ b/core/lib/Thelia/Model/Tools/UrlRewritingTrait.php @@ -0,0 +1,78 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Model\Tools; + +use Thelia\Tools\URL; +/** + * A trait for managing Rewriten URLs from model classes + */ +trait UrlRewritingTrait { + + /** + * @returns string the view name of the rewriten object (e.g., 'category', 'product') + */ + protected abstract function getRewritenUrlViewName(); + + /** + * Get the object URL for the given locale, rewriten if rewriting is enabled. + * + * @param string $locale a valid locale (e.g. en_US) + */ + public function getUrl($locale) + { + return URL::getInstance()->retrieve($this->getRewritenUrlViewName(), $this->getId(), $locale)->toString(); + } + + /** + * Generate a rewriten URL from the object title, and store it in the rewriting table + * + * @param string $locale a valid locale (e.g. en_US) + */ + public function generateRewritenUrl($locale) + { + URL::getInstance()->generateRewritenUrl($this->getRewritenUrlViewName(), $this->getId(), $locale, $this->getTitle()); + } + + /** + * return the rewriten URL for the given locale + * + * @param string $locale a valid locale (e.g. en_US) + */ + public function getRewritenUrl($locale) + { + return "fake url - TODO"; + } + + /** + * Set the rewriten URL for the given locale + * + * @param string $locale a valid locale (e.g. en_US) + */ + public function setRewritenUrl($locale, $url) + { + // TODO - code me ! + + return $this; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Tests/Action/DocumentTest.php b/core/lib/Thelia/Tests/Action/DocumentTest.php new file mode 100644 index 000000000..39aece1f4 --- /dev/null +++ b/core/lib/Thelia/Tests/Action/DocumentTest.php @@ -0,0 +1,248 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Action\DocumentTest; + +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Thelia\Core\HttpFoundation\Request; +use Thelia\Core\HttpFoundation\Session\Session; + +use Thelia\Action\Document; +use Thelia\Core\Event\DocumentEvent; +use Thelia\Model\ConfigQuery; + +/** + * Class DocumentTest + * + * @package Thelia\Tests\Action\DocumentTest + */ +class DocumentTest extends \Thelia\Tests\TestCaseWithURLToolSetup +{ + protected $request; + + protected $session; + + public function getContainer() + { + $container = new \Symfony\Component\DependencyInjection\ContainerBuilder(); + + $dispatcher = $this->getMock("Symfony\Component\EventDispatcher\EventDispatcherInterface"); + + $container->set("event_dispatcher", $dispatcher); + + $request = new Request(); + $request->setSession($this->session); + + $container->set("request", $request); + + return $container; + } + + public function setUp() + { + $this->session = new Session(new MockArraySessionStorage()); + $this->request = new Request(); + + $this->request->setSession($this->session); + + // mock cache configuration. + $config = ConfigQuery::create()->filterByName('document_cache_dir_from_web_root')->findOne(); + + if ($config != null) { + $this->cache_dir_from_web_root = $config->getValue(); + + $config->setValue(__DIR__."/assets/documents/cache"); + + $config->setValue($this->cache_dir_from_web_root)->save(); + } + } + + public static function setUpBeforeClass() + { + $dir = THELIA_WEB_DIR."/cache/tests"; + if ($dh = @opendir($dir)) { + while ($file = readdir($dh)) { + if ($file == '.' || $file == '..') continue; + + unlink(sprintf("%s/%s", $dir, $file)); + } + + closedir($dh); + } + } + + public function tearDown() + { + // restore cache configuration. + $config = ConfigQuery::create()->filterByName('document_cache_dir_from_web_root')->findOne(); + + if ($config != null) { + $config->setValue($this->cache_dir_from_web_root)->save(); + } + } + + /** + * + * Documentevent is empty, mandatory parameters not specified. + * + * @expectedException \InvalidArgumentException + */ + public function testProcessEmptyDocumentEvent() + { + $event = new DocumentEvent($this->request); + + $document = new Document($this->getContainer()); + + $document->processDocument($event); + } + + /** + * + * Try to process a non-existent file + * + * @expectedException \InvalidArgumentException + */ + public function testProcessNonExistentDocument() + { + $event = new DocumentEvent($this->request); + + $document = new Document($this->getContainer()); + + $event->setCacheFilepath("blablabla.txt"); + $event->setCacheSubdirectory("tests"); + + $document->processDocument($event); + } + + /** + * + * Try to process a file outside of the cache + * + * @expectedException \InvalidArgumentException + */ + public function testProcessDocumentOutsideValidPath() + { + $event = new DocumentEvent($this->request); + + $document = new Document($this->getContainer()); + + $event->setCacheFilepath("blablabla.pdf"); + $event->setCacheSubdirectory("../../../"); + + $document->processDocument($event); + } + + /** + * No operation done on source file -> copie ! + */ + public function testProcessDocumentCopy() + { + $event = new DocumentEvent($this->request); + + $event->setSourceFilepath(__DIR__."/assets/documents/sources/test-document-1.txt"); + $event->setCacheSubdirectory("tests"); + + $document = new Document($this->getContainer()); + + // mock cache configuration. + $config = ConfigQuery::create()->filterByName('original_document_delivery_mode')->findOne(); + + if ($config != null) { + $oldval = $config->getValue(); + $config->setValue('copy')->save(); + } + + $document->processDocument($event); + + if ($config != null) $config->setValue($oldval)->save(); + + $imgdir = ConfigQuery::read('document_cache_dir_from_web_root'); + + $this->assertFileExists(THELIA_WEB_DIR."/$imgdir/tests/test-document-1.txt"); + } + + /** + * No operation done on source file -> link ! + */ + public function testProcessDocumentSymlink() + { + $event = new DocumentEvent($this->request); + + $event->setSourceFilepath(__DIR__."/assets/documents/sources/test-document-2.txt"); + $event->setCacheSubdirectory("tests"); + + $document = new Document($this->getContainer()); + + // mock cache configuration. + $config = ConfigQuery::create()->filterByName('original_document_delivery_mode')->findOne(); + + if ($config != null) { + $oldval = $config->getValue(); + $config->setValue('symlink')->save(); + } + + $document->processDocument($event); + + if ($config != null) $config->setValue($oldval)->save(); + + $imgdir = ConfigQuery::read('document_cache_dir_from_web_root'); + + $this->assertFileExists(THELIA_WEB_DIR."/$imgdir/tests/test-document-2.txt"); + } + + public function testClearTestsCache() + { + $event = new DocumentEvent($this->request); + + $event->setCacheSubdirectory('tests'); + + $document = new Document($this->getContainer()); + + $document->clearCache($event); + } + + public function testClearWholeCache() + { + $event = new DocumentEvent($this->request); + + $document = new Document($this->getContainer()); + + $document->clearCache($event); + } + + /** + * Try to clear directory ouside of the cache + * + * @expectedException \InvalidArgumentException + */ + public function testClearUnallowedPathCache() + { + $event = new DocumentEvent($this->request); + + $event->setCacheSubdirectory('../../../..'); + + $document = new Document($this->getContainer()); + + $document->clearCache($event); + } +} diff --git a/core/lib/Thelia/Tests/Action/assets/documents/sources/test-document-1.txt b/core/lib/Thelia/Tests/Action/assets/documents/sources/test-document-1.txt new file mode 100644 index 000000000..82537f32d --- /dev/null +++ b/core/lib/Thelia/Tests/Action/assets/documents/sources/test-document-1.txt @@ -0,0 +1 @@ +This is a text document. \ No newline at end of file diff --git a/core/lib/Thelia/Tests/Action/assets/documents/sources/test-document-2.txt b/core/lib/Thelia/Tests/Action/assets/documents/sources/test-document-2.txt new file mode 100644 index 000000000..82537f32d --- /dev/null +++ b/core/lib/Thelia/Tests/Action/assets/documents/sources/test-document-2.txt @@ -0,0 +1 @@ +This is a text document. \ No newline at end of file diff --git a/core/lib/Thelia/Tests/Core/Template/Loop/DocumentTest.php b/core/lib/Thelia/Tests/Core/Template/Loop/DocumentTest.php new file mode 100644 index 000000000..04e41b6f8 --- /dev/null +++ b/core/lib/Thelia/Tests/Core/Template/Loop/DocumentTest.php @@ -0,0 +1,89 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Core\Template\Loop; + +use Thelia\Model\DocumentQuery; +use Thelia\Tests\Core\Template\Element\BaseLoopTestor; + +use Thelia\Core\Template\Loop\Document; +use Thelia\Model\ProductDocumentQuery; +use Thelia\Model\CategoryDocumentQuery; +use Thelia\Model\ContentDocumentQuery; +use Thelia\Model\FolderDocumentQuery; + +/** + * + * @author Etienne Roudeix + * + */ +class DocumentTest extends BaseLoopTestor +{ + public function getTestedClassName() + { + return 'Thelia\Core\Template\Loop\Document'; + } + + public function getTestedInstance() + { + return new Document($this->container); + } + + public function getMandatoryArguments() + { + return array('source' => 'product', 'id' => 1); + } + + public function testSearchByProductId() + { + $document = ProductDocumentQuery::create()->findOne(); + + $this->baseTestSearchById($document->getId()); + } + + public function testSearchByFolderId() + { + $document = FolderDocumentQuery::create()->findOne(); + + $this->baseTestSearchById($document->getId()); + } + + public function testSearchByContentId() + { + $document = ContentDocumentQuery::create()->findOne(); + + $this->baseTestSearchById($document->getId()); + } + + public function testSearchByCategoryId() + { + $document = CategoryDocumentQuery::create()->findOne(); + + $this->baseTestSearchById($document->getId()); + } + + public function testSearchLimit() + { + $this->baseTestSearchWithLimit(1); + } +} diff --git a/core/lib/Thelia/Tests/Core/Template/Loop/ImageTest.php b/core/lib/Thelia/Tests/Core/Template/Loop/ImageTest.php new file mode 100644 index 000000000..3d2517c0a --- /dev/null +++ b/core/lib/Thelia/Tests/Core/Template/Loop/ImageTest.php @@ -0,0 +1,89 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Core\Template\Loop; + +use Thelia\Model\ImageQuery; +use Thelia\Tests\Core\Template\Element\BaseLoopTestor; + +use Thelia\Core\Template\Loop\Image; +use Thelia\Model\ProductImageQuery; +use Thelia\Model\CategoryImageQuery; +use Thelia\Model\ContentImageQuery; +use Thelia\Model\FolderImageQuery; + +/** + * + * @author Etienne Roudeix + * + */ +class ImageTest extends BaseLoopTestor +{ + public function getTestedClassName() + { + return 'Thelia\Core\Template\Loop\Image'; + } + + public function getTestedInstance() + { + return new Image($this->container); + } + + public function getMandatoryArguments() + { + return array('source' => 'product', 'id' => 1); + } + + public function testSearchByProductId() + { + $image = ProductImageQuery::create()->findOne(); + + $this->baseTestSearchById($image->getId()); + } + + public function testSearchByFolderId() + { + $image = FolderImageQuery::create()->findOne(); + + $this->baseTestSearchById($image->getId()); + } + + public function testSearchByContentId() + { + $image = ContentImageQuery::create()->findOne(); + + $this->baseTestSearchById($image->getId()); + } + + public function testSearchByCategoryId() + { + $image = CategoryImageQuery::create()->findOne(); + + $this->baseTestSearchById($image->getId()); + } + + public function testSearchLimit() + { + $this->baseTestSearchWithLimit(1); + } +} diff --git a/core/lib/Thelia/Tools/URL.php b/core/lib/Thelia/Tools/URL.php index 32c1aadb5..363860064 100755 --- a/core/lib/Thelia/Tools/URL.php +++ b/core/lib/Thelia/Tools/URL.php @@ -43,14 +43,15 @@ class URL protected static $instance = null; - public function __construct(ContainerInterface $container) + public function __construct(ContainerInterface $container = null) { // Allow singleton style calls once intanciated. // For this to work, the URL service has to be instanciated very early. This is done manually // in TheliaHttpKernel, by calling $this->container->get('thelia.url.manager'); self::$instance = $this; - $this->requestContext = $container->get('router.admin')->getContext(); + if ($container !== null) + $this->requestContext = $container->get('router.admin')->getContext(); $this->retriever = new RewritingRetriever(); $this->resolver = new RewritingResolver(); @@ -183,6 +184,7 @@ class URL return $this->absoluteUrl($path, $parameters); } + /** * Retrieve a rewritten URL from a view, a view id and a locale * @@ -261,4 +263,50 @@ class URL return $this->resolver; } + + protected function sanitize($string, $force_lowercase = true, $alphabetic_only = false) + { + static $strip = array("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]", + "}", "\\", "|", ";", ":", "\"", "'", "‘", "’", "“", "”", "–", "—", + "—", "–", ",", "<", ".", ">", "/", "?"); + + $clean = trim(str_replace($strip, "", strip_tags($string))); + + $clean = preg_replace('/\s+/', "-", $clean); + + $clean = ($alphabetic_only) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean ; + + return ($force_lowercase) ? + (function_exists('mb_strtolower')) ? + mb_strtolower($clean, 'UTF-8') : + strtolower($clean) : + $clean; + } + + /** + * Genenerate the file part of a rewriten URL from a given baseString, using a view, a view id and a locale + * + * @param $view + * @param $viewId + * @param $viewLocale + * @param $baseString the string to be converted in a valid URL + * + * @return A valid file part URL. + */ + public function generateRewritenUrl($view, $viewId, $viewLocale, $baseString) + { + // Borrowed from http://stackoverflow.com/questions/2668854/sanitizing-strings-to-make-them-url-and-filename-safe + + // Replace all weird characters with dashes + $string = preg_replace('/[^\w\-~_\.]+/u', '-', $baseString); + + // Only allow one dash separator at a time (and make string lowercase) + $cleanString = mb_strtolower(preg_replace('/--+/u', '-', $string), 'UTF-8'); + + $urlFilePart = $cleanString . ".html"; + + // TODO : + // check if URL url already exists, and add a numeric suffix, or the like + // insert the URL in the rewriting table + } } diff --git a/install/faker.php b/install/faker.php index 11aad8651..59c5b444f 100755 --- a/install/faker.php +++ b/install/faker.php @@ -4,12 +4,7 @@ use Thelia\Constraint\Rule\AvailableForTotalAmountManager; use Thelia\Constraint\Rule\AvailableForXArticlesManager; use Thelia\Constraint\Rule\Operators; use Thelia\Coupon\CouponRuleCollection; -use Thelia\Model\ProductImage; -use Thelia\Model\CategoryImage; -use Thelia\Model\FolderImage; -use Thelia\Model\ContentImage; -use Imagine\Image\Color; -use Imagine\Image\Point; + require __DIR__ . '/../core/bootstrap.php'; @@ -23,12 +18,17 @@ $con = \Propel\Runtime\Propel::getConnection( ); $con->beginTransaction(); +// Intialize URL management +$url = new Thelia\Tools\URL(); + $currency = \Thelia\Model\CurrencyQuery::create()->filterByCode('EUR')->findOne(); try { $stmt = $con->prepare("SET foreign_key_checks = 0"); $stmt->execute(); + echo "Clearing tables\n"; + $productAssociatedContent = Thelia\Model\ProductAssociatedContentQuery::create() ->find(); $productAssociatedContent->delete(); @@ -125,9 +125,24 @@ try { ->find(); $productPrice->delete(); + \Thelia\Model\ProductImageQuery::create()->find()->delete(); + \Thelia\Model\CategoryImageQuery::create()->find()->delete(); + \Thelia\Model\FolderImageQuery::create()->find()->delete(); + \Thelia\Model\ContentImageQuery::create()->find()->delete(); + + \Thelia\Model\ProductDocumentQuery::create()->find()->delete(); + \Thelia\Model\CategoryDocumentQuery::create()->find()->delete(); + \Thelia\Model\FolderDocumentQuery::create()->find()->delete(); + \Thelia\Model\ContentDocumentQuery::create()->find()->delete(); + + \Thelia\Model\CouponQuery::create()->find()->delete(); + $stmt = $con->prepare("SET foreign_key_checks = 1"); + $stmt->execute(); + echo "Creating customer\n"; + //customer $customer = new Thelia\Model\Customer(); $customer->createOrUpdate( @@ -185,6 +200,8 @@ try { } } + echo "Creating features\n"; + //features and features_av $featureList = array(); for($i=0; $i<4; $i++) { @@ -208,6 +225,8 @@ try { } } + echo "Creating attributes\n"; + //attributes and attributes_av $attributeList = array(); for($i=0; $i<4; $i++) { @@ -230,6 +249,8 @@ try { } } + echo "Creating templates\n"; + $template = new Thelia\Model\Template(); setI18n($faker, $template, array("Name" => 20)); $template->save(); @@ -252,6 +273,8 @@ try { ->save(); } + echo "Creating folders and content\n"; + //folders and contents $contentIdList = array(); for($i=0; $i<4; $i++) { @@ -263,10 +286,14 @@ try { $folder->save(); - $image = new FolderImage(); + $image = new \Thelia\Model\FolderImage(); $image->setFolderId($folder->getId()); generate_image($image, 1, 'folder', $folder->getId()); + $document = new \Thelia\Model\FolderDocument(); + $document->setFolderId($folder->getId()); + generate_document($document, 1, 'folder', $folder->getId()); + for($j=1; $jsetParent($folder->getId()); @@ -276,10 +303,14 @@ try { $subfolder->save(); - $image = new FolderImage(); + $image = new \Thelia\Model\FolderImage(); $image->setFolderId($subfolder->getId()); generate_image($image, 1, 'folder', $subfolder->getId()); + $document = new \Thelia\Model\FolderDocument(); + $document->setFolderId($folder->getId()); + generate_document($document, 1, 'folder', $subfolder->getId()); + for($k=0; $kaddFolder($subfolder); @@ -291,13 +322,20 @@ try { $contentId = $content->getId(); $contentIdList[] = $contentId; - $image = new ContentImage(); - $image->setContentId($content->getId()); + $image = new \Thelia\Model\ContentImage(); + $image->setContentId($contentId); generate_image($image, 1, 'content', $contentId); + + $document = new \Thelia\Model\ContentDocument(); + $document->setContentId($contentId); + generate_document($document, 1, 'content', $contentId); + } } } + echo "Creating categories and products\n"; + //categories and products $productIdList = array(); $categoryIdList = array(); @@ -405,6 +443,8 @@ try { } } + echo "Generating coupns fixtures\n"; + generateCouponFixtures($thelia); $con->commit(); @@ -429,10 +469,14 @@ function createProduct($faker, $category, $position, $template, &$productIdList) $productId = $product->getId(); $productIdList[] = $productId; - $image = new ProductImage(); + $image = new \Thelia\Model\ProductImage(); $image->setProductId($productId); generate_image($image, 1, 'product', $productId); + $document = new \Thelia\Model\ProductDocument(); + $document->setProductId($productId); + generate_document($document, 1, 'product', $productId); + return $product; } @@ -464,10 +508,14 @@ function createCategory($faker, $parent, $position, &$categoryIdList, $contentId ->save(); } - $image = new CategoryImage(); + $image = new \Thelia\Model\CategoryImage(); $image->setCategoryId($categoryId); generate_image($image, 1, 'category', $categoryId); + $document = new \Thelia\Model\CategoryDocument(); + $document->setCategoryId($categoryId); + generate_document($document, 1, 'category', $categoryId); + return $category; } @@ -480,37 +528,36 @@ function generate_image($image, $position, $typeobj, $id) { ->setDescription($faker->text(250)) ->setChapo($faker->text(40)) ->setPostscriptum($faker->text(40)) - ->setPosition($position) ->setFile(sprintf("sample-image-%s.png", $id)) ->save() ; // Generate images $imagine = new Imagine\Gd\Imagine(); - $image = $imagine->create(new Imagine\Image\Box(320,240), new Color('#E9730F')); + $image = $imagine->create(new Imagine\Image\Box(320,240), new Imagine\Image\Color('#E9730F')); - $white = new Color('#FFF'); + $white = new Imagine\Image\Color('#FFF'); $font = $imagine->font(__DIR__.'/faker-assets/FreeSans.ttf', 14, $white); $tbox = $font->box("THELIA"); - $image->draw()->text("THELIA", $font, new Point((320 - $tbox->getWidth()) / 2, 30)); + $image->draw()->text("THELIA", $font, new Imagine\Image\Point((320 - $tbox->getWidth()) / 2, 30)); $str = sprintf("%s sample image", ucfirst($typeobj)); $tbox = $font->box($str); - $image->draw()->text($str, $font, new Point((320 - $tbox->getWidth()) / 2, 80)); + $image->draw()->text($str, $font, new Imagine\Image\Point((320 - $tbox->getWidth()) / 2, 80)); $font = $imagine->font(__DIR__.'/faker-assets/FreeSans.ttf', 18, $white); $str = sprintf("%s ID %d", strtoupper($typeobj), $id); $tbox = $font->box($str); - $image->draw()->text($str, $font, new Point((320 - $tbox->getWidth()) / 2, 180)); + $image->draw()->text($str, $font, new Imagine\Image\Point((320 - $tbox->getWidth()) / 2, 180)); $image->draw() - ->line(new Point(0, 0), new Point(319, 0), $white) - ->line(new Point(319, 0), new Point(319, 239), $white) - ->line(new Point(319, 239), new Point(0,239), $white) - ->line(new Point(0, 239), new Point(0, 0), $white) + ->line(new Imagine\Image\Point(0, 0), new Imagine\Image\Point(319, 0), $white) + ->line(new Imagine\Image\Point(319, 0), new Imagine\Image\Point(319, 239), $white) + ->line(new Imagine\Image\Point(319, 239), new Imagine\Image\Point(0,239), $white) + ->line(new Imagine\Image\Point(0, 239), new Imagine\Image\Point(0, 0), $white) ; $image_file = sprintf("%s/../local/media/images/%s/sample-image-%s.png", __DIR__, $typeobj, $id); @@ -520,6 +567,26 @@ function generate_image($image, $position, $typeobj, $id) { $image->save($image_file); } +function generate_document($document, $position, $typeobj, $id) { + + global $faker; + + $document + ->setTitle($faker->text(20)) + ->setDescription($faker->text(250)) + ->setChapo($faker->text(40)) + ->setPostscriptum($faker->text(40)) + ->setFile(sprintf("sample-document-%s.txt", $id)) + ->save() + ; + + $document_file = sprintf("%s/../local/media/documents/%s/sample-document-%s.txt", __DIR__, $typeobj, $id); + + if (! is_dir(dirname($document_file))) mkdir(dirname($document_file), 0777, true); + + file_put_contents($document_file, $faker->text(256)); +} + function setI18n($faker, &$object, $fields = array('Title' => 20, 'Description' => 50) ) { $localeList = $localeList = array('fr_FR', 'en_US', 'es_ES', 'it_IT'); diff --git a/install/insert.sql b/install/insert.sql index 348e447c2..e50c0bd81 100755 --- a/install/insert.sql +++ b/install/insert.sql @@ -13,8 +13,11 @@ INSERT INTO `config` (`name`, `value`, `secured`, `hidden`, `created_at`, `updat ('imagine_graphic_driver', 'gd', 0, 0, NOW(), NOW()), ('default_images_quality_percent', '75', 0, 0, NOW(), NOW()), ('original_image_delivery_mode', 'symlink', 0, 0, NOW(), NOW()), +('original_document_delivery_mode', 'symlink', 0, 0, NOW(), NOW()), ('images_library_path', 'local/media/images', 0, 0, NOW(), NOW()), +('documents_library_path', 'local/media/documents', 0, 0, NOW(), NOW()), ('image_cache_dir_from_web_root', 'cache/images', 0, 0, NOW(), NOW()), +('document_cache_dir_from_web_root', 'cache/documents', 0, 0, NOW(), NOW()), ('currency_rate_update_url', 'http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml', 0, 0, NOW(), NOW()), ('page_not_found_view', '404.html', 0, 0, NOW(), NOW()), ('use_tax_free_amounts', 0, 0, 0, NOW(), NOW()), diff --git a/reset_install.sh b/reset_install.sh index 399156b67..380a80a5f 100755 --- a/reset_install.sh +++ b/reset_install.sh @@ -8,7 +8,7 @@ echo -e "\n\e[01;34m[INFO] Clearing caches\e[00m\n" php Thelia cache:clear echo -e "\n\e[01;34m[INFO] Downloading vendors\e[00m\n" -composer install --prefer-dist +composer install --prefer-dist --optimize-autoloader cd local/config/ diff --git a/templates/admin/default/assets/less/thelia/thelia.less b/templates/admin/default/assets/less/thelia/thelia.less index 50cd9bde6..6dc130918 100644 --- a/templates/admin/default/assets/less/thelia/thelia.less +++ b/templates/admin/default/assets/less/thelia/thelia.less @@ -34,7 +34,7 @@ .topbar { - background: url("@{imgDir}/top.jpg") repeat-x; + background: url("@{imgDir}/top.jpg") repeat-x; font-weight: bold; .version-info { @@ -202,7 +202,7 @@ border-bottom: 2px solid #A5CED8; margin-bottom: 0.5em; } - + // The action bar on the right .actions { text-align: right; @@ -217,6 +217,10 @@ text-transform: none; } } + + .inner-actions { + margin-top: 0.5em; + } } // The overall form container diff --git a/templates/admin/default/categories.html b/templates/admin/default/categories.html index 6fe06f8bc..32b9e6cba 100755 --- a/templates/admin/default/categories.html +++ b/templates/admin/default/categories.html @@ -1,396 +1,260 @@ {extends file="admin-layout.tpl"} -{block name="page-title"}{intl l='Catalog'}{/block} +{block name="page-title"}{intl l='Categories'}{/block} -{block name="check-permissions"}admin.catalog.view{/block} +{block name="check-permissions"}admin.categories.view{/block} {block name="main-content"} -
-
+
- {include file="includes/catalog-breadcrumb.html"} +
- {module_include location='catalog_top'} + {include file="includes/catalog-breadcrumb.html"} -
-
-
- - - {module_include location='catalog_bottom'} - - + {module_include location='category_list_row'} - {* Adding a new Category *} + + + + + + + {/loop} + + {/ifloop} + + {elseloop rel="category_list"} + + + + + + {/elseloop} +
- {* display parent category name, and get current cat ID *} - {loop name="category_title" type="category" visible="*" id=$current_category_id} - {intl l="Categories in %cat" cat=$TITLE} - {$cat_id = $ID} - {/loop} - {elseloop rel="category_title"} - {intl l="Top level categories"} - {/elseloop} + {module_include location='categories_top'} - {module_include location='category_list_caption'} +
+
+
+ + + {module_include location='category_list_caption'} - {ifloop rel="category_list"} - - - + {loop type="auth" name="can_create" roles="ADMIN" permissions="admin.categories.create"} + + + + {/loop} + - + {ifloop rel="category_list"} + + + - + - {module_include location='category_list_header'} + - + {module_include location='category_list_header'} - + - - - + - - {loop name="category_list" type="category" visible="*" parent=$current_category_id order=$category_order backend_context="1" lang=$lang_id} - - + + + - + + {loop name="category_list" type="category" visible="*" parent=$category_id order=$category_order backend_context="1" lang=$lang_id} + + - - - {module_include location='category_list_row'} - - - - - - - - {/loop} - - {/ifloop} - - {elseloop rel="category_list"} - - - - - - {/elseloop} -
+ {* display parent category name, and get current cat ID *} + {loop name="category_title" type="category" visible="*" id=$category_id} + {intl l="Categories in %cat" cat=$TITLE} + {$cat_id = $ID} + {/loop} + {elseloop rel="category_title"} + {intl l="Top level categories"} + {/elseloop} - {loop type="auth" name="can_create" roles="ADMIN" permissions="admin.categories.create"} - - - - {/loop} -
- {admin_sortable_header - current_order=$category_order - order='id' - reverse_order='id_reverse' - path={url path='/admin/catalog' id_category=$current_category_id} - label="{intl l='ID'}" - } -  
+ {admin_sortable_header + current_order=$category_order + order='id' + reverse_order='id_reverse' + path={url path='/admin/categories' id_category=$category_id} + request_parameter_name='category_order' + label="{intl l='ID'}" + } + - {admin_sortable_header - current_order=$category_order - order='alpha' - reverse_order='alpha_reverse' - path={url path='/admin/catalog' id_category=$current_category_id} - label="{intl l='Category title'}" - } -   + {admin_sortable_header + current_order=$category_order + order='alpha' + reverse_order='alpha_reverse' + path={url path='/admin/categories' id_category=$category_id} + request_parameter_name='category_order' + label="{intl l='Category title'}" + } + - {admin_sortable_header - current_order=$category_order - order='visible' - reverse_order='visible_reverse' - path={url path='/admin/catalog' id_category=$current_category_id} - label="{intl l='Online'}" - } - - {admin_sortable_header - current_order=$category_order - order='manual' - reverse_order='manual_reverse' - path={url path='/admin/catalog' id_category=$current_category_id} - label="{intl l='Position'}" - } - + {admin_sortable_header + current_order=$category_order + order='visible' + reverse_order='visible_reverse' + path={url path='/admin/categories' id_category=$category_id} + request_parameter_name='category_order' + label="{intl l='Online'}" + } + {intl l='Actions'}
+ {admin_sortable_header + current_order=$category_order + order='manual' + reverse_order='manual_reverse' + path={url path='/admin/categories' id_category=$category_id} + request_parameter_name='category_order' + label="{intl l='Position'}" + } +
{$ID}{intl l='Actions'}
- {loop type="image" name="cat_image" source="category" source_id="$ID" limit="1" width="50" height="50" resize_mode="crop" backend_context="1"} - {$TITLE} - {/loop} -
{$ID} - - {$TITLE} - - - {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.categories.edit"} -
- -
- {/loop} - - {elseloop rel="can_change"} -
- -
- {/elseloop} -
- {admin_position_block - permission="admin.categories.edit" - path={url path='admin/category/update-position' category_id=$ID} - url_parameter="category_id" - in_place_edit_class="categoryPositionChange" - position=$POSITION - id=$ID - } - -
- - - {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.categories.edit"} - - {/loop} - - {loop type="auth" name="can_delete" roles="ADMIN" permissions="admin.categories.delete"} - - {/loop} -
-
-
- {loop type="auth" name="can_create" roles="ADMIN" permissions="admin.categories.create"} - {intl l="This category has no sub-categories. To create a new one, click the + button above."} - {/loop} - - {elseloop rel="can_create"} - {intl l="This category has no sub-categories."} - {/elseloop} -
-
-
-
-
- -
-
-
- - - - {ifloop rel="product_list"} - - - - - - - - - - - - - - - - {loop name="product_list" type="product" category=$current_category_id order="manual"} - - - - - - - - {module_include location='product_list_row'} - - - - - - - - {/loop} - - {/ifloop} + - {elseloop rel="product_list"} - - - - - - {/elseloop} -
- {* display parent category name *} - {loop name="category_title" type="category" visible="*" id=$current_category_id} - {intl l="Products in %cat" cat=$TITLE} - {/loop} - - {elseloop rel="category_title"} - {intl l="Top level Products"} - {/elseloop} - - {module_include location='product_list_caption'} - - - - -
- {admin_sortable_header - current_order=$product_order - order='id' - reverse_order='id_reverse' - path={url path='/admin/product' category_id=$current_category_id} - label="{intl l='ID'}" - } - -   - {admin_sortable_header - current_order=$product_order - order='ref' - reverse_order='ref_reverse' - path={url path='/admin/product' category_id=$current_category_id} - label="{intl l='Reference'}" - } - - {admin_sortable_header - current_order=$product_order - order='alpha' - reverse_order='alpha_reverse' - path={url path='/admin/product' category_id=$current_category_id} - label="{intl l='Product title'}" - } - - {module_include location='product_list_header'} - - - {admin_sortable_header - current_order=$product_order - order='visible' - reverse_order='visible_reverse' - path={url path='/admin/product' category_id=$current_category_id} - label="{intl l='Online'}" - } - - {admin_sortable_header - current_order=$product_order - order='manual' - reverse_order='manual_reverse' - path={url path='/admin/product' category_id=$current_category_id} - label="{intl l='Position'}" - } -  
{$ID} - {loop type="image" name="cat_image" source="product" source_id="$ID" limit="1" width="50" height="50" resize_mode="crop" backend_context="1"} - - {$TITLE} - - {/loop} - - {$REF}{$TITLE} - {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.products.edit"} -
- -
- {/loop} - - {elseloop rel="can_change"} -
- -
- {/elseloop} -
- {admin_position_block - permission="admin.product.edit" - path={url path='admin/product' category_id=$ID} - url_parameter="product_id" - in_place_edit_class="productPositionChange" - position=$POSITION - id=$ID - } - -
- {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.product.edit"} - - {/loop} - - {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.product.delete"} - +
+ {loop type="image" name="cat_image" source="category" source_id="$ID" limit="1" width="50" height="50" resize_mode="crop" backend_context="1"} + {$TITLE} {/loop} - -
{intl l="This category doesn't have any products. To add a new product, click the + button above."}
-
-
-
+
+ + {$TITLE} + + + {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.categories.edit"} +
+ +
+ {/loop} - {form name="thelia.admin.category.creation"} + {elseloop rel="can_change"} +
+ +
+ {/elseloop} +
+ {admin_position_block + permission="admin.categories.edit" + path={url path='admin/categories/update-position' category_id=$ID} + url_parameter="category_id" + in_place_edit_class="categoryPositionChange" + position=$POSITION + id=$ID + } + +
+ + + {loop type="auth" name="can_change" roles="ADMIN" permissions="admin.categories.edit"} + + {/loop} + + {loop type="auth" name="can_delete" roles="ADMIN" permissions="admin.categories.delete"} + + {/loop} +
+
+
+ {loop type="auth" name="can_create" roles="ADMIN" permissions="admin.categories.create"} + {intl l="This category has no sub-categories. To create a new one, click the + button above."} + {/loop} + + {elseloop rel="can_create"} + {intl l="This category has no sub-categories."} + {/elseloop} +
+
+
+
+
+ + {module_include location='categories_bottom'} + +
+
+ + + {* Adding a new category *} + + {form name="thelia.admin.category.creation"} {* Capture the dialog body, to pass it to the generic dialog *} {capture "category_creation_dialog"} {form_hidden_fields form=$form} - {form_field form=$form field='success_url'} - {* on success, redirect to the edition page, _ID_ is replaced with the created object ID, see controller *} - - {/form_field} + {form_field form=$form field='success_url'} + {* on success, redirect to the edition page, _ID_ is replaced with the created object ID, see controller *} + + {/form_field} - {form_field form=$form field='parent'} - - {/form_field} + {form_field form=$form field='parent'} + + {/form_field} - {form_field form=$form field='title'} -
- + {form_field form=$form field='title'} +
+ + {loop type="lang" name="default-lang" default_only="1"} +
+ + $TITLE +
- {loop type="lang" name="default-lang" default_only="1"} +
{intl l='Enter here the category name in the default language (%title)' title="$TITLE"}
-
- - $TITLE -
+ {* Switch edition to the current locale *} + -
{intl l='Enter here the category name in the default language (%title)' title="$TITLE"}
+ {form_field form=$form field='locale'} + + {/form_field} + {/loop} +
+ {/form_field} - {* Switch edition to the current locale *} - - - {form_field form=$form field='locale'} - - {/form_field} - {/loop} -
- {/form_field} + {form_field form=$form field='visible'} +
+
+ +
+
+ {/form_field} {module_include location='category_create_form'} - {/capture} + {/capture} - {include - file = "includes/generic-create-dialog.html" + {include + file = "includes/generic-create-dialog.html" - dialog_id = "add_category_dialog" - dialog_title = {intl l="Create a new category"} - dialog_body = {$smarty.capture.category_creation_dialog nofilter} + dialog_id = "category_creation_dialog" + dialog_title = {intl l="Create a new category"} + dialog_body = {$smarty.capture.category_creation_dialog nofilter} - dialog_ok_label = {intl l="Create this category"} - dialog_cancel_label = {intl l="Cancel"} + dialog_ok_label = {intl l="Create this category"} - form_action = {url path='/admin/categories/create'} - form_enctype = {form_enctype form=$form} - form_error_message = $form_error_message - } + form_action = {url path='/admin/categories/create'} + form_enctype = {form_enctype form=$form} + form_error_message = $form_error_message + } {/form} - {* Delete category confirmation dialog *} + + {* Delete confirmation dialog *} {capture "category_delete_dialog"} - - + {module_include location='category_delete_form'} @@ -399,9 +263,9 @@ {include file = "includes/generic-confirm-dialog.html" - dialog_id = "delete_category_dialog" - dialog_title = {intl l="Delete a category"} - dialog_message = {intl l="Do you really want to delete this category, and all its contents ?"} + dialog_id = "category_delete_dialog" + dialog_title = {intl l="Delete category"} + dialog_message = {intl l="Do you really want to delete this category and all its content ?"} form_action = {url path='/admin/categories/delete'} form_content = {$smarty.capture.category_delete_dialog nofilter} @@ -410,108 +274,76 @@ {block name="javascript-initialization"} - {javascripts file='assets/js/bootstrap-switch/bootstrap-switch.js'} - - {/javascripts} + {javascripts file='assets/js/bootstrap-switch/bootstrap-switch.js'} + + {/javascripts} - {javascripts file='assets/js/bootstrap-editable/bootstrap-editable.js'} - - {/javascripts} + {javascripts file='assets/js/bootstrap-editable/bootstrap-editable.js'} + + {/javascripts} - + {/block} \ No newline at end of file diff --git a/templates/admin/default/category-edit.html b/templates/admin/default/category-edit.html index e01849475..626114d2a 100755 --- a/templates/admin/default/category-edit.html +++ b/templates/admin/default/category-edit.html @@ -8,206 +8,154 @@
- {include file="includes/catalog-breadcrumb.html"} + {include file="includes/catalog-breadcrumb.html" editing_category="true"}
- {loop name="category_edit" type="category" visible="*" id="{$current_category_id}" backend_context="1" lang="$edit_language_id"} + {loop name="category_edit" type="category" visible="*" id="{$category_id}" backend_context="1" lang="$edit_language_id"}
- {intl l='Edit category'} + {intl l='Edit category %title' title=$TITLE}
- - - + + {if $HAS_PREVIOUS != 0} + + {else} + + {/if} + + + + {if $HAS_NEXT != 0} + + {else} + + {/if}
- +
+
-
- -
+
-
-
-
+
- {include file="includes/inner-form-toolbar.html" close_url="{url path='admin/catalog/category/edit' category_id=$current_category_id}"} +
-
-
-
- + {form name="thelia.admin.category.modification"} + -
- -
-
+ {include file="includes/inner-form-toolbar.html" close_url="{url path='/admin/categories' category_id=$category_id}"} -
- + {* Be sure to get the category ID, even if the form could not be validated *} + -
- -
-
+ {form_hidden_fields form=$form} -
- + {form_field form=$form field='success_url'} + + {/form_field} -
- + {form_field form=$form field='locale'} + + {/form_field} -
-
+ {if $form_error}
{$form_error_message}
{/if} -
- + {include file="includes/standard-description-form-fields.html"} -
- -
-
+ {form_field form=$form field='url'} +
+ -
- + +
+ {/form_field} -
- -
{intl l="The rewritten URL to the category page. Click \"Use Default\" button to use the default URL. Use only digits, letters, - and _ characters."}
-
-
+
+
+ {form_field form=$form field='parent'} +
-
-
+ -
-
-
-   -
-

{intl l='Category created on %date_create. Last modification: %date_change' date_create="{format_date date=$CREATE_DATE}" date_change="{format_date date=$UPDATE_DATE}"}}

-
-
-
-
+ +
+ {/form_field} +
+ +
+ {form_field form=$form field='visible'} +
+ +
+ +
+
+ {/form_field} +
+
-
-
- - -
- -
-
-
- -
-
- - -
- -
-
-
-
- -
- - -
- +   +
+

{intl l='Category created on %date_create. Last modification: %date_change' date_create="{format_date date=$CREATE_DATE}" date_change="{format_date date=$UPDATE_DATE}"}

-
+
-
-
-
+ + {/form} +
+
-
-

Images

-
+
+ klljkmk +
-
-

Documents

-
+
+
-
-

Modules

-
+
+
-
-
- - {/loop} +
+
+
+
+
+ {/loop}
@@ -217,10 +165,6 @@