diff --git a/.gitignore b/.gitignore index b2dc20145..ae4975b08 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ coverage local/cache/* composer.lock web/assets/* +web/.htaccess diff --git a/composer.json b/composer.json index 1f74cb713..03c4e7e11 100755 --- a/composer.json +++ b/composer.json @@ -27,8 +27,6 @@ "symfony/form": "2.2.*", "symfony/validator": "2.2.*", - "symfony/security": "2.2.*", - "symfony/templating": "2.2.*", "smarty/smarty": "v3.1.13", "kriswallsmith/assetic": "1.2.*@dev", diff --git a/core/lib/Thelia/Admin/Controller/AdminController.php b/core/lib/Thelia/Admin/Controller/AdminController.php index 91b6123b8..38084ba89 100755 --- a/core/lib/Thelia/Admin/Controller/AdminController.php +++ b/core/lib/Thelia/Admin/Controller/AdminController.php @@ -28,17 +28,23 @@ use Thelia\Form\AdminLogin; class AdminController extends BaseAdminController { - public function indexAction() + public function loginAction() { - $form = $this->getLoginForm(); $request = $this->getRequest(); if($request->isMethod("POST")) { + $form->bind($request); - if($form->isValid()) { + if ($form->isValid()) { + + $this->container->get('request')->authenticate( + $form->get('username')->getData(), + $form->get('password')->getData() + ); + echo "valid"; exit; } } @@ -48,6 +54,15 @@ class AdminController extends BaseAdminController { )); } + public function indexAction() + { + $form = $this->getLoginForm(); + + return $this->render("login.html", array( + "form" => $form->createView() + )); + } + protected function getLoginForm() { $adminLogin = new AdminLogin($this->getRequest()); diff --git a/core/lib/Thelia/Config/Resources/config.xml b/core/lib/Thelia/Config/Resources/config.xml index 6f9ec52f6..f4140d5a6 100755 --- a/core/lib/Thelia/Config/Resources/config.xml +++ b/core/lib/Thelia/Config/Resources/config.xml @@ -6,6 +6,7 @@ + @@ -68,6 +69,10 @@ + + + + diff --git a/core/lib/Thelia/Core/Security/Authentication/AuthenticationProviderInterface.php b/core/lib/Thelia/Core/Security/Authentication/AuthenticationProviderInterface.php new file mode 100644 index 000000000..eb60f0482 --- /dev/null +++ b/core/lib/Thelia/Core/Security/Authentication/AuthenticationProviderInterface.php @@ -0,0 +1,60 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security\Authentication; + +use Thelia\Core\Security\UserNotFoundException; +use Thelia\Core\Security\IncorrectPasswordException; + +/** + * Aunthentication providers are in charge or retrieving users, and check their + * credentials. + * + * @author Franck + * + */ +interface AuthenticationProviderInterface { + + /** + * Set the authentication token + * + * @param TokenInterface $token the authentication token + */ + public function setToken(TokenInterface $token); + + + /** + * Set the authentication token + * + * @param unknown $key + */ + public function supportsToken(TokenInterface $token); + + /** + * Authenticate the token + * + *@throws Exception if authentication was not successful + */ + public function authenticate(); +} +?> \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/Authentication/UsernamePasswordAuthenticator.php b/core/lib/Thelia/Core/Security/Authentication/UsernamePasswordAuthenticator.php new file mode 100644 index 000000000..f6161f038 --- /dev/null +++ b/core/lib/Thelia/Core/Security/Authentication/UsernamePasswordAuthenticator.php @@ -0,0 +1,67 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security\Authentication; + +use Thelia\Core\Security\Authentication\AuthenticationProviderInterface; +use Thelia\Core\Security\Encoder\PasswordEncoderInterface; +use Thelia\Core\Security\User\UserProviderInterface; +use Thelia\Security\Token\TokenInterface; +use Thelia\Core\Security\Exception\IncorrectPasswordException; +use Thelia\Core\Security\Token\UsernamePasswordToken; + +class UsernamePasswordAuthenticator implements AuthenticationProviderInterface { + + protected $userProvider; + protected $encoder; + + private $token; + + public function __construct(UserProviderInterface $userProvider, PasswordEncoderInterface $encoder) { + $this->userProvider = $userProvider; + $this->encoder = $encoder; + } + + public function supportsToken(TokenInterface $token) { + + return $token instanceof UsernamePasswordToken; + } + + public function authenticate($token) { + + if (!$this->supports($token)) { + return null; + } + + // Retreive user + $user = $this->userProvider->getUser($this->token->getUsername()); + + // Check password + $authOk = $this->encoder->isEqual($password, $user->getPassword(), $user->getAlgo(), $user->getSalt()) === true; + + $authenticatedToken = new UsernamePasswordToken($user, $token->getCredentials(), $authOk); + + return $authenticatedToken; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/Encoder/PasswordEncoderInterface.php b/core/lib/Thelia/Core/Security/Encoder/PasswordEncoderInterface.php new file mode 100644 index 000000000..3353969b8 --- /dev/null +++ b/core/lib/Thelia/Core/Security/Encoder/PasswordEncoderInterface.php @@ -0,0 +1,52 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security\Encoder; + +/** + * This interface defines a password encoder. + * + * @author Franck Allimant + * + */ +interface PasswordEncoderInterface { + + /** + * Encode a string. + * + * @param string $password the password to encode + * @param string $algorithm the hash() algorithm + * @return string $salt the salt + */ + public function encode($password, $algorithm, $salt); + + /** + * Check a string against an encoded password. + * + * @param string $string the string to compare against password + * @param string $password the encoded password + * @param string $algorithm the hash() algorithm + * @return string $salt the salt + */ + public function isEqual($string, $password, $algorithm, $salt); +} diff --git a/core/lib/Thelia/Core/Security/Encoder/PasswordHashEncoder.php b/core/lib/Thelia/Core/Security/Encoder/PasswordHashEncoder.php new file mode 100644 index 000000000..7a66812f5 --- /dev/null +++ b/core/lib/Thelia/Core/Security/Encoder/PasswordHashEncoder.php @@ -0,0 +1,66 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security\Encoder; + +/** + * This interface defines a hash based password encoder. + * + * @author Franck Allimant + */ + +class PasswordHashEncoder implements PasswordEncoderInterface { + + /** + * {@inheritdoc} + */ + public function encode($password, $algorithm, $salt) + { + if (!in_array($algorithm, hash_algos(), true)) { + throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $algorithm)); + } + + // Salt the string + $salted = $password.$salt; + + // Create the hash + $digest = hash($algorithm, $salted, true); + + // "stretch" hash + for ($i = 1; $i < 5000; $i++) { + $digest = hash($algorithm, $digest.$salted, true); + } + + return base64_encode($digest); + } + + /** + * {@inheritdoc} + */ + public function isEqual($string, $password, $algorithm, $salt) + { + $encoded = $this->encode($password, $algorithm, $salt); + + return $encoded == $string; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/Exception/AuthenticationTokenNotFoundException.php b/core/lib/Thelia/Core/Security/Exception/AuthenticationTokenNotFoundException.php new file mode 100644 index 000000000..061b43257 --- /dev/null +++ b/core/lib/Thelia/Core/Security/Exception/AuthenticationTokenNotFoundException.php @@ -0,0 +1,28 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security\Exception; + +class AuthenticationTokenNotFoundException extends \Exception +{ +} diff --git a/core/lib/Thelia/Core/Security/SecurityManager.php b/core/lib/Thelia/Core/Security/SecurityManager.php new file mode 100644 index 000000000..82c45a304 --- /dev/null +++ b/core/lib/Thelia/Core/Security/SecurityManager.php @@ -0,0 +1,81 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security; + +use Thelia\Core\Security\Authentication\AuthenticationProviderInterface; +use Thelia\Core\Security\Exception\AuthenticationTokenNotFoundException; + +/** + * A simple security manager, in charge of authenticating users using various authentication systems. + * + * @author Franck Allimant + */ +class SecurityManager { + + protected $authProvider; + + public function __construct(AuthenticationProviderInterface $authProvider) { + $this->authProvider = $authProvider; + } + + /** + * Checks if the current token is authenticated + * + * @throws AuthenticationCredentialsNotFoundException when the security context has no authentication token. + * + * @return Boolean + */ + final public function isGranted() + { + if (null === $this->token) { + throw new AuthenticationTokenNotFoundException('The security context contains no authentication token.'); + } + + if (!$this->token->isAuthenticated()) { + $this->token = $this->authProvider->authenticate($this->token); + } + + return $this->token->isAuthenticated(); + } + + /** + * Gets the currently authenticated token. + * + * @return TokenInterface|null A TokenInterface instance or null if no authentication information is available + */ + public function getToken() + { + return $this->token; + } + + /** + * Sets the token. + * + * @param TokenInterface $token A TokenInterface token, or null if no further authentication information should be stored + */ + public function setToken(TokenInterface $token = null) + { + $this->token = $token; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/Token/AbstractToken.php b/core/lib/Thelia/Core/Security/Token/AbstractToken.php new file mode 100644 index 000000000..be1f8cb8b --- /dev/null +++ b/core/lib/Thelia/Core/Security/Token/AbstractToken.php @@ -0,0 +1,148 @@ + +* @author Johannes M. Schmitt +*/ +abstract class AbstractToken implements TokenInterface +{ + private $user; + private $authenticated; + + /** + * Constructor. + * + * @param RoleInterface[] $roles An array of roles + * + * @throws \InvalidArgumentException + */ + public function __construct() + { + $this->authenticated = false; + } + + /** + * {@inheritdoc} + */ + public function getUsername() + { + if ($this->user instanceof UserInterface) { + return $this->user->getUsername(); + } + + return (string) $this->user; + } + + public function getUser() + { + return $this->user; + } + + /** + * Sets the user in the token. + * + * The user can be a UserInterface instance, or an object implementing + * a __toString method or the username as a regular string. + * + * @param mixed $user The user + * @throws \InvalidArgumentException + */ + public function setUser($user) + { + if (!($user instanceof UserInterface || is_string($user))) { + throw new \InvalidArgumentException('$user must be an instanceof UserInterface, or a primitive string.'); + } + + if (null === $this->user) { + $changed = false; + } elseif ($this->user instanceof UserInterface) { + if (!$user instanceof UserInterface) { + $changed = true; + } else { + $changed = $this->hasUserChanged($user); + } + } elseif ($user instanceof UserInterface) { + $changed = true; + } else { + $changed = (string) $this->user !== (string) $user; + } + + if ($changed) { + $this->setAuthenticated(false); + } + + $this->user = $user; + } + + /** + * {@inheritdoc} + */ + public function isAuthenticated() + { + return $this->authenticated; + } + + /** + * {@inheritdoc} + */ + public function setAuthenticated($authenticated) + { + $this->authenticated = (Boolean) $authenticated; + } + + /** + * {@inheritdoc} + */ + public function eraseCredentials() + { + if ($this->getUser() instanceof UserInterface) { + $this->getUser()->eraseCredentials(); + } + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array($this->user, $this->authenticated)); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list($this->user, $this->authenticated) = unserialize($serialized); + } + + private function hasUserChanged(UserInterface $user) + { + if (!($this->user instanceof UserInterface)) { + throw new \BadMethodCallException('Method "hasUserChanged" should be called when current user class is instance of "UserInterface".'); + } + + if ($this->user instanceof EquatableInterface) { + return ! (Boolean) $this->user->isEqualTo($user); + } + + if ($this->user->getPassword() !== $user->getPassword()) { + return true; + } + + if ($this->user->getSalt() !== $user->getSalt()) { + return true; + } + + if ($this->user->getUsername() !== $user->getUsername()) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/Token/TokenInterface.php b/core/lib/Thelia/Core/Security/Token/TokenInterface.php new file mode 100644 index 000000000..d70c8eeea --- /dev/null +++ b/core/lib/Thelia/Core/Security/Token/TokenInterface.php @@ -0,0 +1,81 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Security\Token; + +/** + * TokenInterface is the interface for the user authentication information. + * + * Parts borrowed from Symfony Security Framework (Fabien Potencier / Johannes M. Schmitt ) + */ + +interface TokenInterface extends \Serializable +{ + /** + * Returns the user credentials. + * + * @return mixed The user credentials + */ + public function getCredentials(); + + /** + * Returns a user representation. + * + * @return mixed either returns an object which implements __toString(), or + * a primitive string is returned. + */ + public function getUser(); + + /** + * Sets a user instance + * + * @param mixed $user + */ + public function setUser($user); + + /** + * Returns the username. + * + * @return string + */ + public function getUsername(); + + /** + * Returns whether the user is authenticated or not. + * + * @return Boolean true if the token has been authenticated, false otherwise + */ + public function isAuthenticated(); + + /** + * Sets the authenticated flag. + * + * @param Boolean $isAuthenticated The authenticated flag + */ + public function setAuthenticated($isAuthenticated); + + /** + * Removes sensitive information from the token. + */ + public function eraseCredentials(); +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/Token/UsernamePasswordToken.php b/core/lib/Thelia/Core/Security/Token/UsernamePasswordToken.php new file mode 100644 index 000000000..f605d1767 --- /dev/null +++ b/core/lib/Thelia/Core/Security/Token/UsernamePasswordToken.php @@ -0,0 +1,75 @@ + +*/ +class UsernamePasswordToken extends AbstractToken +{ + private $credentials; + + /** + * Constructor. + * + * @param string $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method. + * @param string $password The password of the user + * + * @throws \InvalidArgumentException + */ + public function __construct($username, $password, $authenticated = false) + { + $this->setUser($username); + $this->credentials = $password; + + parent::setAuthenticated($authenticated); + } + + /** + * {@inheritdoc} + */ + public function setAuthenticated($isAuthenticated) + { + if ($isAuthenticated) { + throw new \LogicException('Cannot set this token to trusted after instantiation.'); + } + + parent::setAuthenticated(false); + } + + public function getCredentials() + { + return $this->credentials; + } + + /** + * {@inheritdoc} + */ + public function eraseCredentials() + { + parent::eraseCredentials(); + + $this->credentials = null; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array($this->credentials, $this->providerKey, parent::serialize())); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list($this->credentials, $this->providerKey, $parentStr) = unserialize($serialized); + parent::unserialize($parentStr); + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/User/AdminUserProvider.php b/core/lib/Thelia/Core/Security/User/AdminUserProvider.php new file mode 100644 index 000000000..9df5ee484 --- /dev/null +++ b/core/lib/Thelia/Core/Security/User/AdminUserProvider.php @@ -0,0 +1,18 @@ +filterByLogin($key) + ->findOne(); + + return $admin; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/User/CustomerUserProvider.php b/core/lib/Thelia/Core/Security/User/CustomerUserProvider.php new file mode 100644 index 000000000..824d9e774 --- /dev/null +++ b/core/lib/Thelia/Core/Security/User/CustomerUserProvider.php @@ -0,0 +1,20 @@ +filterByEmail($key) + ->findOne(); + + return $customer; + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/User/UserInterface.php b/core/lib/Thelia/Core/Security/User/UserInterface.php new file mode 100644 index 000000000..9057504ca --- /dev/null +++ b/core/lib/Thelia/Core/Security/User/UserInterface.php @@ -0,0 +1,42 @@ + + * + */ +interface UserInterface { + + /** + * Return the user unique name + */ + public function getUsername(); + + /** + * Return the user encoded password + */ + public function getPassword(); + + /** + * return the salt used to calculate the user password + */ + public function getSalt(); + + /** + * return the algorithm used to calculate the user password + */ + public function getAlgo(); + + /** + * Removes sensitive data from the user. + * + * This is important if, at any given point, sensitive information like + * the plain-text password is stored on this object. + * + * @return void + */ + public function eraseCredentials(); +} \ No newline at end of file diff --git a/core/lib/Thelia/Core/Security/User/UserProviderInterface.php b/core/lib/Thelia/Core/Security/User/UserProviderInterface.php new file mode 100644 index 000000000..217e693c1 --- /dev/null +++ b/core/lib/Thelia/Core/Security/User/UserProviderInterface.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/core/lib/Thelia/Core/Template/Element/BaseLoop.php b/core/lib/Thelia/Core/Template/Element/BaseLoop.php index c2399a3d3..8345ae06a 100755 --- a/core/lib/Thelia/Core/Template/Element/BaseLoop.php +++ b/core/lib/Thelia/Core/Template/Element/BaseLoop.php @@ -68,6 +68,9 @@ abstract class BaseLoop $this->dispatcher = $dispatcher; } + /** + * @return \Thelia\Core\Template\Loop\Argument\ArgumentCollection + */ public function getArgs() { return $this->defineArgs()->addArguments($this->getDefaultArgs()); diff --git a/core/lib/Thelia/Core/Template/Element/LoopResultRow.php b/core/lib/Thelia/Core/Template/Element/LoopResultRow.php index 4a14521c7..3ac05fe5c 100755 --- a/core/lib/Thelia/Core/Template/Element/LoopResultRow.php +++ b/core/lib/Thelia/Core/Template/Element/LoopResultRow.php @@ -29,12 +29,12 @@ class LoopResultRow public function set($key, $value) { - $this->substitution["#".$key] = $value; + $this->substitution[$key] = $value === null ? '' : $value; } public function get($key) { - return $this->substitution["#".$key]; + return $this->substitution[$key]; } public function getVarVal() @@ -42,4 +42,9 @@ class LoopResultRow return $this->substitution; } + public function getVars() + { + return array_keys($this->substitution); + } + } diff --git a/core/lib/Thelia/Core/Template/Loop/Argument/Argument.php b/core/lib/Thelia/Core/Template/Loop/Argument/Argument.php index 9fb72bff0..86c11edfb 100755 --- a/core/lib/Thelia/Core/Template/Loop/Argument/Argument.php +++ b/core/lib/Thelia/Core/Template/Loop/Argument/Argument.php @@ -72,6 +72,32 @@ class Argument ); } + public static function createFloatTypeArgument($name, $default=null, $mandatory=false, $empty=true) + { + return new Argument( + $name, + new TypeCollection( + new Type\FloatType() + ), + $default, + $mandatory, + $empty + ); + } + + public static function createBooleanTypeArgument($name, $default=null, $mandatory=false, $empty=true) + { + return new Argument( + $name, + new TypeCollection( + new Type\BooleanType() + ), + $default, + $mandatory, + $empty + ); + } + public static function createIntListTypeArgument($name, $default=null, $mandatory=false, $empty=true) { return new Argument( diff --git a/core/lib/Thelia/Core/Template/Loop/Category.php b/core/lib/Thelia/Core/Template/Loop/Category.php index 7240b688c..44874ed59 100755 --- a/core/lib/Thelia/Core/Template/Loop/Category.php +++ b/core/lib/Thelia/Core/Template/Loop/Category.php @@ -64,6 +64,7 @@ use Thelia\Type; * Class Category * @package Thelia\Core\Template\Loop * @author Manuel Raynaud + * @author Etienne Roudeix */ class Category extends BaseLoop { @@ -85,9 +86,9 @@ class Category extends BaseLoop return new ArgumentCollection( Argument::createIntListTypeArgument('id'), Argument::createIntTypeArgument('parent'), - Argument::createIntTypeArgument('current'), - Argument::createIntTypeArgument('not_empty', 0), - Argument::createIntTypeArgument('visible', 1), + Argument::createBooleanTypeArgument('current'), + Argument::createBooleanTypeArgument('not_empty', 0), + Argument::createBooleanTypeArgument('visible', 1), Argument::createAnyTypeArgument('link'), new Argument( 'order', @@ -95,7 +96,7 @@ class Category extends BaseLoop new Type\EnumType('alpha', 'alpha_reverse', 'reverse') ) ), - Argument::createIntTypeArgument('random', 0), + Argument::createBooleanTypeArgument('random', 0), Argument::createIntListTypeArgument('exclude') ); } @@ -110,28 +111,28 @@ class Category extends BaseLoop $search = CategoryQuery::create(); if (!is_null($this->id)) { - $search->filterById(explode(',', $this->id), Criteria::IN); + $search->filterById($this->id, Criteria::IN); } if (!is_null($this->parent)) { $search->filterByParent($this->parent); } - if ($this->current == 1) { + if ($this->current === true) { $search->filterById($this->request->get("category_id")); - } elseif (null !== $this->current && $this->current == 0) { + } elseif ($this->current === false) { $search->filterById($this->request->get("category_id"), Criteria::NOT_IN); } if (!is_null($this->exclude)) { - $search->filterById(explode(",", $this->exclude), Criteria::NOT_IN); + $search->filterById($this->exclude, Criteria::NOT_IN); } if (!is_null($this->link)) { $search->filterByLink($this->link); } - $search->filterByVisible($this->visible); + $search->filterByVisible($this->visible ? 1 : 0); switch ($this->order) { case "alpha": @@ -148,7 +149,7 @@ class Category extends BaseLoop break; } - if ($this->random == 1) { + if ($this->random === true) { $search->clearOrderByColumns(); $search->addAscendingOrderByColumn('RAND()'); } diff --git a/core/lib/Thelia/Core/Template/Loop/Product.php b/core/lib/Thelia/Core/Template/Loop/Product.php new file mode 100755 index 000000000..c80a47ade --- /dev/null +++ b/core/lib/Thelia/Core/Template/Loop/Product.php @@ -0,0 +1,303 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Core\Template\Loop; + +use Propel\Runtime\ActiveQuery\Criteria; + +use Thelia\Core\Template\Element\BaseLoop; +use Thelia\Core\Template\Element\LoopResult; +use Thelia\Core\Template\Element\LoopResultRow; + +use Thelia\Core\Template\Loop\Argument\ArgumentCollection; +use Thelia\Core\Template\Loop\Argument\Argument; +use Thelia\Log\Tlog; + +use Thelia\Model\CategoryQuery; +use Thelia\Model\ProductCategoryQuery; +use Thelia\Model\ProductPeer; +use Thelia\Model\ProductQuery; +use Thelia\Model\ConfigQuery; +use Thelia\Type\TypeCollection; +use Thelia\Type; + +/** + * + * Product loop + * + * + * Class Product + * @package Thelia\Core\Template\Loop + * @author Etienne Roudeix + */ +class Product extends BaseLoop +{ + public $id; + public $ref; + public $category; + public $new; + public $promo; + public $min_price; + public $max_price; + public $min_stock; + public $min_weight; + public $max_weight; + public $current; + public $current_category; + public $depth; + public $visible; + public $order; + public $random; + public $exclude; + + /** + * @return ArgumentCollection + */ + protected function defineArgs() + { + return new ArgumentCollection( + Argument::createIntListTypeArgument('id'), + new Argument( + 'ref', + new TypeCollection( + new Type\AlphaNumStringListType() + ) + ), + Argument::createIntListTypeArgument('category'), + Argument::createBooleanTypeArgument('new'), + Argument::createBooleanTypeArgument('promo'), + Argument::createFloatTypeArgument('min_price'), + Argument::createFloatTypeArgument('max_price'), + Argument::createIntTypeArgument('min_stock'), + Argument::createFloatTypeArgument('min_weight'), + Argument::createFloatTypeArgument('max_weight'), + Argument::createBooleanTypeArgument('current'), + Argument::createBooleanTypeArgument('current_category'), + Argument::createIntTypeArgument('depth'), + Argument::createBooleanTypeArgument('visible', 1), + new Argument( + 'order', + new TypeCollection( + new Type\EnumType(array('alpha', 'alpha_reverse', 'reverse', 'min_price', 'max_price', 'category', 'manual', 'manual_reverse', 'ref', 'promo', 'new')) + ) + ), + Argument::createBooleanTypeArgument('random', 0), + Argument::createIntListTypeArgument('exclude') + ); + } + + /** + * @param $pagination + * + * @return \Thelia\Core\Template\Element\LoopResult + */ + public function exec(&$pagination) + { + $search = ProductQuery::create(); + + if (!is_null($this->id)) { + $search->filterById($this->id, Criteria::IN); + } + + if (!is_null($this->ref)) { + $search->filterByRef($this->ref, Criteria::IN); + } + + if (!is_null($this->category)) { + $categories = CategoryQuery::create()->filterById($this->category, Criteria::IN)->find(); + + if(null !== $this->depth) { + foreach(CategoryQuery::findAllChild($this->category, $this->depth) as $subCategory) { + $categories->prepend($subCategory); + } + } + + $search->filterByCategory( + $categories, + Criteria::IN + ); + } + + if ($this->new === true) { + $search->filterByNewness(1, Criteria::EQUAL); + } else if($this->new === false) { + $search->filterByNewness(0, Criteria::EQUAL); + } + + if ($this->promo === true) { + $search->filterByPromo(1, Criteria::EQUAL); + } else if($this->promo === false) { + $search->filterByNewness(0, Criteria::EQUAL); + } + + if (null != $this->min_stock) { + $search->filterByQuantity($this->min_stock, Criteria::GREATER_EQUAL); + } + + if(null !== $this->min_price) { + $search->condition('in_promo', ProductPeer::PROMO . Criteria::EQUAL . '1') + ->condition('not_in_promo', ProductPeer::PROMO . Criteria::NOT_EQUAL . '1') + ->condition('min_price2', ProductPeer::PRICE2 . Criteria::GREATER_EQUAL . '?', $this->min_price) + ->condition('min_price', ProductPeer::PRICE . Criteria::GREATER_EQUAL . '?', $this->min_price) + ->combine(array('in_promo', 'min_price2'), Criteria::LOGICAL_AND, 'in_promo_min_price') + ->combine(array('not_in_promo', 'min_price'), Criteria::LOGICAL_AND, 'not_in_promo_min_price') + ->where(array('not_in_promo_min_price', 'in_promo_min_price'), Criteria::LOGICAL_OR); + } + + if(null !== $this->max_price) { + $search->condition('in_promo', ProductPeer::PROMO . Criteria::EQUAL . '1') + ->condition('not_in_promo', ProductPeer::PROMO . Criteria::NOT_EQUAL . '1') + ->condition('max_price2', ProductPeer::PRICE2 . Criteria::LESS_EQUAL . '?', $this->max_price) + ->condition('max_price', ProductPeer::PRICE . Criteria::LESS_EQUAL . '?', $this->max_price) + ->combine(array('in_promo', 'max_price2'), Criteria::LOGICAL_AND, 'in_promo_max_price') + ->combine(array('not_in_promo', 'max_price'), Criteria::LOGICAL_AND, 'not_in_promo_max_price') + ->where(array('not_in_promo_max_price', 'in_promo_max_price'), Criteria::LOGICAL_OR); + } + + if(null !== $this->min_weight) { + $search->filterByWeight($this->min_weight, Criteria::GREATER_EQUAL); + } + + if(null !== $this->max_weight) { + $search->filterByWeight($this->max_weight, Criteria::LESS_EQUAL); + } + + if ($this->current === true) { + $search->filterById($this->request->get("product_id")); + } elseif($this->current === false) { + $search->filterById($this->request->get("product_id"), Criteria::NOT_IN); + } + + if ($this->current_category === true) { + $search->filterByCategory( + CategoryQuery::create()->filterByProduct( + ProductCategoryQuery::create()->filterByProductId( + $this->request->get("product_id"), + Criteria::EQUAL + )->find(), + Criteria::IN + )->find(), + Criteria::IN + ); + } elseif($this->current_category === false) { + $search->filterByCategory( + CategoryQuery::create()->filterByProduct( + ProductCategoryQuery::create()->filterByProductId( + $this->request->get("product_id"), + Criteria::EQUAL + )->find(), + Criteria::IN + )->find(), + Criteria::NOT_IN + ); + } + + $search->filterByVisible($this->visible); + + switch ($this->order) { + case "alpha": + $search->addAscendingOrderByColumn(\Thelia\Model\CategoryI18nPeer::TITLE); + break; + case "alpha_reverse": + $search->addDescendingOrderByColumn(\Thelia\Model\CategoryI18nPeer::TITLE); + break; + case "reverse": + $search->orderByPosition(Criteria::DESC); + break; + case "min_price": + //$search->order + //$search->orderByPosition(Criteria::DESC); + break; + /*case "max_price": + $search->orderByPosition(Criteria::DESC); + break; + case "category": + $search->orderByPosition(Criteria::DESC); + break;*/ + case "manual": + $search->addAscendingOrderByColumn(\Thelia\Model\ProductPeer::POSITION); + break; + case "manual_reverse": + $search->addDescendingOrderByColumn(\Thelia\Model\ProductPeer::POSITION); + break; + case "ref": + $search->addAscendingOrderByColumn(\Thelia\Model\ProductPeer::REF); + break; + case "promo": + $search->addDescendingOrderByColumn(\Thelia\Model\ProductPeer::PROMO); + break; + case "new": + $search->addDescendingOrderByColumn(\Thelia\Model\ProductPeer::NEWNESS); + break; + default: + $search->orderByPosition(); + break; + } + + if ($this->random === true) { + $search->clearOrderByColumns(); + $search->addAscendingOrderByColumn('RAND()'); + } + + if (!is_null($this->exclude)) { + $search->filterById($this->exclude, Criteria::NOT_IN); + } + + /** + * Criteria::INNER_JOIN in second parameter for joinWithI18n exclude query without translation. + * + * @todo : verify here if we want results for row without translations. + */ + + $search->joinWithI18n( + $this->request->getSession()->get('locale', 'en_US'), + (ConfigQuery::read("default_lang_without_translation", 1)) ? Criteria::LEFT_JOIN : Criteria::INNER_JOIN + ); + + $products = $this->search($search, $pagination); + + $loopResult = new LoopResult(); + + foreach ($products as $product) { + $loopResultRow = new LoopResultRow(); + $loopResultRow->set("ID", $product->getId()); + $loopResultRow->set("REF",$product->getRef()); + $loopResultRow->set("TITLE",$product->getTitle()); + $loopResultRow->set("CHAPO", $product->getChapo()); + $loopResultRow->set("DESCRIPTION", $product->getDescription()); + $loopResultRow->set("POSTSCRIPTUM", $product->getPostscriptum()); + $loopResultRow->set("PRICE", $product->getPrice()); + $loopResultRow->set("PROMO_PRICE", $product->getPrice2()); + $loopResultRow->set("WEIGHT", $product->getWeight()); + $loopResultRow->set("PROMO", $product->getPromo()); + $loopResultRow->set("NEW", $product->getNewness()); + + //$loopResultRow->set("URL", $product->getUrl()); + + $loopResult->addRow($loopResultRow); + } + + return $loopResult; + } + +} diff --git a/core/lib/Thelia/Core/Template/Smarty/Plugins/Assetic.php b/core/lib/Thelia/Core/Template/Smarty/Plugins/Assetic.php index f79f98a1a..4418bd50c 100755 --- a/core/lib/Thelia/Core/Template/Smarty/Plugins/Assetic.php +++ b/core/lib/Thelia/Core/Template/Smarty/Plugins/Assetic.php @@ -35,7 +35,7 @@ class Assetic implements SmartyPluginInterface { $web_root = THELIA_WEB_DIR; - $asset_dir_from_web_root = '/assets/admin/default'; // FIXME + $asset_dir_from_web_root = 'assets/admin/default'; // FIXME $this->asset_manager = new SmartyAssetsManager($web_root, $asset_dir_from_web_root); } diff --git a/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaLoop.php b/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaLoop.php index 4d2ea7d57..86a29da6d 100755 --- a/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaLoop.php +++ b/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaLoop.php @@ -43,6 +43,9 @@ class TheliaLoop implements SmartyPluginInterface protected $dispatcher; + protected $loopstack = array(); + protected $varstack = array(); + public function __construct(Request $request, EventDispatcherInterface $dispatcher) { $this->request = $request; @@ -78,13 +81,16 @@ class TheliaLoop implements SmartyPluginInterface if (empty($params['name'])) throw new \InvalidArgumentException("Missing 'name' parameter in loop arguments"); - if (empty($params['type'])) throw new \InvalidArgumentException("Missing 'type' parameter in loop arguments"); $name = $params['name']; if ($content === null) { + // Check if a loop with the same name exists in the current scope, and abort if it's the case. + if (array_key_exists($name, $this->varstack)) { + throw new \InvalidArgumentException("A loop named '$name' already exists in the current scope."); + } $loop = $this->createLoopInstance(strtolower($params['type'])); @@ -93,11 +99,10 @@ class TheliaLoop implements SmartyPluginInterface self::$pagination[$name] = null; $loopResults = $loop->exec(self::$pagination[$name]); + $this->loopstack[$name] = $loopResults; - $template->assignByRef($name, $loopResults); - - } else { - $loopResults = $template->getTemplateVars($name); + } else { + $loopResults = $this->loopstack[$name]; $loopResults->next(); } @@ -105,21 +110,50 @@ class TheliaLoop implements SmartyPluginInterface if ($loopResults->valid()) { $loopResultRow = $loopResults->current(); + // On first iteration, save variables that may be overwritten by this loop + if (! isset($this->varstack[$name])) { + + $saved_vars = array(); + + $varlist = $loopResultRow->getVars(); + $varlist[] = 'LOOP_COUNT'; + $varlist[] = 'LOOP_TOTAL'; + + foreach($varlist as $var) { + $saved_vars[$var] = $template->getTemplateVars($var); + } + + $this->varstack[$name] = $saved_vars; + } + foreach($loopResultRow->getVarVal() as $var => $val) { - $template->assign(substr($var, 1), $val); + $template->assign($var, $val); } - $template->assign('__COUNT__', 1 + $loopResults->key()); - $template->assign('__TOTAL__', $loopResults->getCount()); - - //$repeat = $loopResults->valid(); - $repeat = true; + $repeat = true; } + // Assign meta information + $template->assign('LOOP_COUNT', 1 + $loopResults->key()); + $template->assign('LOOP_TOTAL', $loopResults->getCount()); + + // Loop is terminated. Cleanup. + if (! $repeat) { + // Restore previous variables values before terminating + if (isset($this->varstack[$name])) { + foreach($this->varstack[$name] as $var => $value) { + $template->assign($var, $value); + } + + unset($this->varstack[$name]); + } + } if ($content !== null) { + if ($loopResults->isEmpty()) { + $content = ""; + } - if ($loopResults->isEmpty()) $content = ""; return $content; } } @@ -178,10 +212,10 @@ class TheliaLoop implements SmartyPluginInterface $loopName = $params['rel']; // Find loop results in the current template vars - $loopResults = $template->getTemplateVars($loopName); + /* $loopResults = $template->getTemplateVars($loopName); if (empty($loopResults)) { throw new \InvalidArgumentException("Loop $loopName is not defined."); - } + }*/ // Find pagination $pagination = self::getPagination($loopName); @@ -225,14 +259,11 @@ class TheliaLoop implements SmartyPluginInterface $loopName = $params['rel']; - // Find loop results in the current template vars - $loopResults = $template->getTemplateVars($loopName); - - if (empty($loopResults)) { + if (! isset($this->loopstack[$loopName])) { throw new \InvalidArgumentException("Loop $loopName is not defined."); } - return $loopResults->isEmpty(); + return $this->loopstack[$loopName]->isEmpty(); } /** @@ -279,7 +310,7 @@ class TheliaLoop implements SmartyPluginInterface $argumentsCollection = $loop->getArgs(); foreach( $argumentsCollection as $argument ) { - $value = isset($smartyParam[$argument->name]) ? $smartyParam[$argument->name] : null; + $value = isset($smartyParam[$argument->name]) ? (string)$smartyParam[$argument->name] : null; /* check if mandatory */ if($value === null && $argument->mandatory) { @@ -304,11 +335,11 @@ class TheliaLoop implements SmartyPluginInterface /* set default */ /* did it as last checking for we consider default value is acceptable no matter type or empty restriction */ - if($value === null) { - $value = $argument->default; + if($value === null && $argument->default !== null) { + $value = (string)$argument->default; } - $loop->{$argument->name} = $value; + $loop->{$argument->name} = $value === null ? null : $argument->type->getFormatedValue($value); } if (!empty($faultActor)) { diff --git a/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaSyntax.php b/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaSyntax.php new file mode 100644 index 000000000..9b508cf3e --- /dev/null +++ b/core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaSyntax.php @@ -0,0 +1,54 @@ +. */ +/* */ +/*************************************************************************************/ +namespace Thelia\Core\Template\Smarty\Plugins; + +use Thelia\Core\Template\Smarty\SmartyPluginDescriptor; +use Thelia\Core\Template\Smarty\SmartyPluginInterface; + +/** + * Class TheliaSyntax + * @package Thelia\Core\Template\Smarty\Plugins + * + * @author Etienne Roudeix + */ +class TheliaSyntax implements SmartyPluginInterface +{ + public function dieseCancel($value, $diese) + { + if($value === null) { + return $diese; + } + + return $value; + } + + /** + * @return SmartyPluginDescriptor[] + */ + public function getPluginDescriptors() + { + return array( + new SmartyPluginDescriptor("modifier", "dieseCanceller", $this, "dieseCancel") + ); + } +} diff --git a/core/lib/Thelia/Core/Template/Smarty/Plugins/Translation.php b/core/lib/Thelia/Core/Template/Smarty/Plugins/Translation.php index 403cfac80..ec984613e 100755 --- a/core/lib/Thelia/Core/Template/Smarty/Plugins/Translation.php +++ b/core/lib/Thelia/Core/Template/Smarty/Plugins/Translation.php @@ -44,7 +44,7 @@ class Translation implements SmartyPluginInterface } // TODO - return "[$string]"; + return "$string"; } /** diff --git a/core/lib/Thelia/Core/Template/Smarty/SmartyParser.php b/core/lib/Thelia/Core/Template/Smarty/SmartyParser.php index 42a79a6d4..8e34a89ed 100755 --- a/core/lib/Thelia/Core/Template/Smarty/SmartyParser.php +++ b/core/lib/Thelia/Core/Template/Smarty/SmartyParser.php @@ -29,10 +29,11 @@ class SmartyParser extends Smarty implements ParserInterface protected $status = 200; /** - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher - * @param bool $template - * @param string $env Environment define for the kernel application. Used for the cache directory + * @param Request $request + * @param EventDispatcherInterface $dispatcher + * @param bool $template + * @param string $env + * @param bool $debug */ public function __construct(Request $request, EventDispatcherInterface $dispatcher, $template = false, $env = "prod", $debug = false) { @@ -72,13 +73,13 @@ class SmartyParser extends Smarty implements ParserInterface // The default HTTP status $this->status = 200; - $this->registerFilter('pre', array($this, "pretest")); + $this->registerFilter('pre', array($this, "preThelia")); } - public function pretest($tpl_source, \Smarty_Internal_Template $template) + public function preThelia($tpl_source, \Smarty_Internal_Template $template) { $new_source = preg_replace('`{#([a-zA-Z][a-zA-Z0-9\-_]*)(.*)}`', '{\$$1$2}', $tpl_source); - $new_source = preg_replace('`#([a-zA-Z][a-zA-Z0-9\-_]*)`', '{\$$1|default:\'#$1\'}', $new_source); + $new_source = preg_replace('`#([a-zA-Z][a-zA-Z0-9\-_]*)`', '{\$$1|dieseCanceller:\'#$1\'}', $new_source); return $new_source; } diff --git a/core/lib/Thelia/Form/AdminLogin.php b/core/lib/Thelia/Form/AdminLogin.php index 33810f771..71e18d3c3 100644 --- a/core/lib/Thelia/Form/AdminLogin.php +++ b/core/lib/Thelia/Form/AdminLogin.php @@ -26,6 +26,7 @@ namespace Thelia\Form; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Choice; class AdminLogin extends BaseForm { @@ -38,7 +39,12 @@ class AdminLogin extends BaseForm { new Length(array("min" => 3)) ) )) - ->add("password", "password"); + ->add("password", "password", array( + "constraints" => array( + new NotBlank() + ) + )) + ->add("remember_me", "checkbox"); } public function getName() diff --git a/core/lib/Thelia/Model/CategoryQuery.php b/core/lib/Thelia/Model/CategoryQuery.php index f2b56e85a..2abb04fa4 100644 --- a/core/lib/Thelia/Model/CategoryQuery.php +++ b/core/lib/Thelia/Model/CategoryQuery.php @@ -32,7 +32,7 @@ class CategoryQuery extends BaseCategoryQuery { * * find all category children for a given category. an array of \Thelia\Model\Category is return * - * @param $categoryId the category id + * @param $categoryId the category id or an array of id * @param int $depth max depth you want to search * @param int $currentPos don't change this param, it is used for recursion * @return \Thelia\Model\Category[] @@ -40,17 +40,25 @@ class CategoryQuery extends BaseCategoryQuery { public static function findAllChild($categoryId, $depth = 0, $currentPos = 0) { $result = array(); - $currentPos++; - if($depth == $currentPos && $depth != 0) return; + if(is_array($categoryId)) { + foreach($categoryId as $categorySingleId) { + $result = array_merge($result, (array) self::findAllChild($categorySingleId, $depth, $currentPos)); + } + } else { + $currentPos++; - $categories = self::create() - ->filterByParent($categoryId) - ->find(); + if($depth == $currentPos && $depth != 0) return; - foreach ($categories as $category) { - array_push($result, $category); - $result = array_merge($result, (array) self::findAllChild($category->getId(), $depth, $currentPos)); + $categories = self::create() + ->filterByParent($categoryId) + ->find(); + + + foreach ($categories as $category) { + array_push($result, $category); + $result = array_merge($result, (array) self::findAllChild($category->getId(), $depth, $currentPos)); + } } return $result; diff --git a/core/lib/Thelia/Tests/Core/Security/Encoder/PasswordHashEncoderTest.php b/core/lib/Thelia/Tests/Core/Security/Encoder/PasswordHashEncoderTest.php new file mode 100644 index 000000000..7590b3788 --- /dev/null +++ b/core/lib/Thelia/Tests/Core/Security/Encoder/PasswordHashEncoderTest.php @@ -0,0 +1,75 @@ +encode('password', 'sha512', 'a simple salt'); + + // echo "PASS=\{$pass\}"; + + $this->assertEquals("L3f/gGy4nBVhi8WSsC1a7E9JM8U+rtk6ZT+NiqX8M1UDJv6mahQEZ1z2cN/y9pixH+hgWbkBitONMiXWscomoQ==", $pass, "Expected password not found."); + } + + public function testIsEqual() + { + $encoder = new PasswordHashEncoder(); + + $exp = "L3f/gGy4nBVhi8WSsC1a7E9JM8U+rtk6ZT+NiqX8M1UDJv6mahQEZ1z2cN/y9pixH+hgWbkBitONMiXWscomoQ=="; + + $this->assertTrue($encoder->isEqual($exp, 'password', 'sha512', 'a simple salt')); + } + + public function testWrongPass() + { + $encoder = new PasswordHashEncoder(); + + $exp = "L3f/gGy4nBVhi8WSsC1a7E9JM8U+rtk6ZT+NiqX8M1UDJv6mahQEZ1z2cN/y9pixH+hgWbkBitONMiXWscomoQ=="; + + $this->assertFalse($encoder->isEqual($exp, 'grongron', 'sha512', 'a simple salt')); + } + + public function testWrongSalt() + { + $encoder = new PasswordHashEncoder(); + + $exp = "L3f/gGy4nBVhi8WSsC1a7E9JM8U+rtk6ZT+NiqX8M1UDJv6mahQEZ1z2cN/y9pixH+hgWbkBitONMiXWscomoQ=="; + + $this->assertFalse($encoder->isEqual($exp, 'password', 'sha512', 'another salt')); + } + + public function testWrongAlgo() + { + $encoder = new PasswordHashEncoder(); + + $exp = "L3f/gGy4nBVhi8WSsC1a7E9JM8U+rtk6ZT+NiqX8M1UDJv6mahQEZ1z2cN/y9pixH+hgWbkBitONMiXWscomoQ=="; + + $this->assertFalse($encoder->isEqual($exp, 'password', 'md5', 'another salt')); + } + + /** + * @expectedException LogicException + */ + public function testUnsupportedAlgo() + { + $encoder = new PasswordHashEncoder(); + + $exp = "L3f/gGy4nBVhi8WSsC1a7E9JM8U+rtk6ZT+NiqX8M1UDJv6mahQEZ1z2cN/y9pixH+hgWbkBitONMiXWscomoQ=="; + + $encoder->isEqual($exp, 'password', 'sbonk', 'another salt'); + } + + /** + * @expectedException LogicException + */ + public function testEncodeWrongAlgorithm() + { + $encoder = new PasswordHashEncoder(); + + $encoder->encode('password', 'pouët', 'a simple salt'); + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Tests/Core/Security/SecurityManagerTest.php b/core/lib/Thelia/Tests/Core/Security/SecurityManagerTest.php new file mode 100644 index 000000000..84cb0d1ef --- /dev/null +++ b/core/lib/Thelia/Tests/Core/Security/SecurityManagerTest.php @@ -0,0 +1,49 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Security; + +use Thelia\Core\Security\SecurityManager; +/** + * + * @author Franck Allimant + * + */ +class SecurityManagerTest extends \PHPUnit_Framework_TestCase +{ + public function testGetSetToken() + { + /* + $context = new SecurityManager($authProvider)( + $this->getMock('AuthenticationProviderInterface'), + $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface') + ); + $this->assertNull($context->getToken()); + + $context->setToken($token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); + $this->assertSame($token, $context->getToken()); + */ + // $this->assertFalse(1==1, "faux !"); + } +} diff --git a/core/lib/Thelia/Tests/Core/Security/Token/UsernamePasswordTokenTest.php b/core/lib/Thelia/Tests/Core/Security/Token/UsernamePasswordTokenTest.php new file mode 100644 index 000000000..147c5b816 --- /dev/null +++ b/core/lib/Thelia/Tests/Core/Security/Token/UsernamePasswordTokenTest.php @@ -0,0 +1,38 @@ +assertFalse($token->isAuthenticated()); + + $token = new UsernamePasswordToken('username', 'password', true); + $this->assertTrue($token->isAuthenticated()); + } + + /** + * @expectedException LogicException + */ + public function testSetAuthenticatedToTrue() + { + $token = new UsernamePasswordToken('foo', 'bar', true); + $token->setAuthenticated(true); + } + + public function testSetAuthenticatedToFalse() + { + $token = new UsernamePasswordToken('foo', 'bar', true); + $token->setAuthenticated(false); + $this->assertFalse($token->isAuthenticated()); + } + + public function testEraseCredentials() + { + $token = new UsernamePasswordToken('foo', 'bar', true); + $token->eraseCredentials(); + $this->assertEquals('', $token->getCredentials()); + } +} \ No newline at end of file diff --git a/core/lib/Thelia/Tests/Type/AlphaNumStringListTypeTest.php b/core/lib/Thelia/Tests/Type/AlphaNumStringListTypeTest.php new file mode 100755 index 000000000..22d1c52ce --- /dev/null +++ b/core/lib/Thelia/Tests/Type/AlphaNumStringListTypeTest.php @@ -0,0 +1,48 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Type; + +use Thelia\Type\AlphaNumStringListType; + +/** + * + * @author Etienne Roudeix + * + */ +class AlphaNumStringListTypeTest extends \PHPUnit_Framework_TestCase +{ + public function testAlphaNumStringListType() + { + $type = new AlphaNumStringListType(); + $this->assertTrue($type->isValid('FOO1,FOO_2,FOO-3')); + $this->assertFalse($type->isValid('FOO.1,FOO$_2,FOO-3')); + } + + public function testFormatAlphaNumStringListType() + { + $type = new AlphaNumStringListType(); + $this->assertTrue(is_array($type->getFormatedValue('FOO1,FOO_2,FOO-3'))); + $this->assertNull($type->getFormatedValue('5€')); + } +} diff --git a/core/lib/Thelia/Tests/Type/AlphaNumStringTypeTest.php b/core/lib/Thelia/Tests/Type/AlphaNumStringTypeTest.php new file mode 100755 index 000000000..46526715b --- /dev/null +++ b/core/lib/Thelia/Tests/Type/AlphaNumStringTypeTest.php @@ -0,0 +1,43 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Type; + +use Thelia\Type\AlphaNumStringType; + +/** + * + * @author Etienne Roudeix + * + */ +class AlphaNumStringTypeTest extends \PHPUnit_Framework_TestCase +{ + public function testAlphaNumStringType() + { + $type = new AlphaNumStringType(); + $this->assertTrue($type->isValid('azs_qs-0-9ds')); + $this->assertFalse($type->isValid('3.3')); + $this->assertFalse($type->isValid('3 3')); + $this->assertFalse($type->isValid('3€3')); + } +} diff --git a/core/lib/Thelia/Tests/Type/BooleanTypeTest.php b/core/lib/Thelia/Tests/Type/BooleanTypeTest.php new file mode 100755 index 000000000..84889ff40 --- /dev/null +++ b/core/lib/Thelia/Tests/Type/BooleanTypeTest.php @@ -0,0 +1,55 @@ +. */ +/* */ +/*************************************************************************************/ + +namespace Thelia\Tests\Type; + +use Thelia\Type\BooleanType; + +/** + * + * @author Etienne Roudeix + * + */ +class BooleanTypeTest extends \PHPUnit_Framework_TestCase +{ + public function testBooleanType() + { + $booleanType = new BooleanType(); + $this->assertTrue($booleanType->isValid('y')); + $this->assertTrue($booleanType->isValid('yes')); + $this->assertTrue($booleanType->isValid('true')); + $this->assertTrue($booleanType->isValid('no')); + $this->assertTrue($booleanType->isValid('n')); + $this->assertTrue($booleanType->isValid('false')); + $this->assertFalse($booleanType->isValid('foo')); + $this->assertFalse($booleanType->isValid(5)); + } + + public function testFormatBooleanType() + { + $booleanType = new BooleanType(); + $this->assertTrue($booleanType->getFormatedValue('yes')); + $this->assertFalse($booleanType->getFormatedValue('no')); + $this->assertNull($booleanType->getFormatedValue('foo')); + } +} diff --git a/core/lib/Thelia/Tests/Type/IntListTypeTest.php b/core/lib/Thelia/Tests/Type/IntListTypeTest.php index 68b628a5c..6f9cc1e99 100755 --- a/core/lib/Thelia/Tests/Type/IntListTypeTest.php +++ b/core/lib/Thelia/Tests/Type/IntListTypeTest.php @@ -39,4 +39,11 @@ class IntListTypeTest extends \PHPUnit_Framework_TestCase $this->assertTrue($intListType->isValid('1,2,3')); $this->assertFalse($intListType->isValid('1,2,3.3')); } + + public function testFormatIntListType() + { + $intListType = new IntListType(); + $this->assertTrue(is_array($intListType->getFormatedValue('1,2,3'))); + $this->assertNull($intListType->getFormatedValue('foo')); + } } diff --git a/core/lib/Thelia/Tests/Type/JsonTypeTest.php b/core/lib/Thelia/Tests/Type/JsonTypeTest.php index 276a867d2..2dc15a2f5 100755 --- a/core/lib/Thelia/Tests/Type/JsonTypeTest.php +++ b/core/lib/Thelia/Tests/Type/JsonTypeTest.php @@ -38,4 +38,11 @@ class JsonTypeTest extends \PHPUnit_Framework_TestCase $this->assertTrue($jsonType->isValid('{"k0":"v0","k1":"v1","k2":"v2"}')); $this->assertFalse($jsonType->isValid('1,2,3')); } + + public function testFormatJsonType() + { + $jsonType = new JsonType(); + $this->assertTrue(is_array($jsonType->getFormatedValue('{"k0":"v0","k1":"v1","k2":"v2"}'))); + $this->assertNull($jsonType->getFormatedValue('foo')); + } } diff --git a/core/lib/Thelia/Type/AlphaNumStringListType.php b/core/lib/Thelia/Type/AlphaNumStringListType.php new file mode 100755 index 000000000..1fa210cba --- /dev/null +++ b/core/lib/Thelia/Type/AlphaNumStringListType.php @@ -0,0 +1,52 @@ +. */ +/* */ +/*************************************************************************************/ +namespace Thelia\Type; + +/** + * + * @author Etienne Roudeix + * + */ + +class AlphaNumStringListType implements TypeInterface +{ + public function getType() + { + return 'Alphanumeric string list type'; + } + + public function isValid($values) + { + foreach(explode(',', $values) as $value) { + if(!preg_match('#^[a-zA-Z0-9\-_]+$#', $value)) + return false; + } + + return true; + } + + public function getFormatedValue($values) + { + return $this->isValid($values) ? explode(',', $values) : null; + } +} diff --git a/core/lib/Thelia/Type/AlphaNumStringType.php b/core/lib/Thelia/Type/AlphaNumStringType.php new file mode 100755 index 000000000..b893f8a46 --- /dev/null +++ b/core/lib/Thelia/Type/AlphaNumStringType.php @@ -0,0 +1,47 @@ +. */ +/* */ +/*************************************************************************************/ +namespace Thelia\Type; + +/** + * + * @author Etienne Roudeix + * + */ + +class AlphaNumStringType implements TypeInterface +{ + public function getType() + { + return 'Alphanumeric string type'; + } + + public function isValid($value) + { + return preg_match('#^[a-zA-Z0-9\-_]+$#', $value) ? true : false; + } + + public function getFormatedValue($value) + { + return $this->isValid($value) ? $value : null; + } +} diff --git a/core/lib/Thelia/Type/AnyType.php b/core/lib/Thelia/Type/AnyType.php index 5b00a5aeb..8e22d44eb 100755 --- a/core/lib/Thelia/Type/AnyType.php +++ b/core/lib/Thelia/Type/AnyType.php @@ -39,4 +39,9 @@ class AnyType implements TypeInterface { return true; } + + public function getFormatedValue($value) + { + return $value; + } } diff --git a/core/lib/Thelia/Type/BooleanType.php b/core/lib/Thelia/Type/BooleanType.php new file mode 100755 index 000000000..939ff0d79 --- /dev/null +++ b/core/lib/Thelia/Type/BooleanType.php @@ -0,0 +1,60 @@ +. */ +/* */ +/*************************************************************************************/ +namespace Thelia\Type; + +/** + * + * @author Etienne Roudeix + * + */ + +class BooleanType implements TypeInterface +{ + protected $trueValuesArray = array( + '1', + 'true', + 'yes', + 'y', + ); + protected $falseValuesArray = array( + '0', + 'false', + 'no', + 'n', + ); + + public function getType() + { + return 'Boolean type'; + } + + public function isValid($value) + { + return in_array($value, $this->trueValuesArray) || in_array($value, $this->falseValuesArray); + } + + public function getFormatedValue($value) + { + return $this->isValid($value) ? ( in_array($value, $this->trueValuesArray) ) : null; + } +} diff --git a/core/lib/Thelia/Type/EnumType.php b/core/lib/Thelia/Type/EnumType.php index d0a1c9b89..0aff0ec88 100755 --- a/core/lib/Thelia/Type/EnumType.php +++ b/core/lib/Thelia/Type/EnumType.php @@ -47,4 +47,9 @@ class EnumType implements TypeInterface { return in_array($value, $this->values); } + + public function getFormatedValue($value) + { + return $this->isValid($value) ? $value : null; + } } diff --git a/core/lib/Thelia/Type/FloatType.php b/core/lib/Thelia/Type/FloatType.php index e341b540f..83d3561a4 100755 --- a/core/lib/Thelia/Type/FloatType.php +++ b/core/lib/Thelia/Type/FloatType.php @@ -39,4 +39,9 @@ class FloatType implements TypeInterface { return filter_var($value, FILTER_VALIDATE_FLOAT) === false ? false : true; } + + public function getFormatedValue($value) + { + return $this->isValid($value) ? $value : null; + } } diff --git a/core/lib/Thelia/Type/IntListType.php b/core/lib/Thelia/Type/IntListType.php index 74b0a6790..b023173f7 100755 --- a/core/lib/Thelia/Type/IntListType.php +++ b/core/lib/Thelia/Type/IntListType.php @@ -44,4 +44,9 @@ class IntListType implements TypeInterface return true; } + + public function getFormatedValue($values) + { + return $this->isValid($values) ? explode(',', $values) : null; + } } diff --git a/core/lib/Thelia/Type/IntType.php b/core/lib/Thelia/Type/IntType.php index d077a6c89..4adc2e10e 100755 --- a/core/lib/Thelia/Type/IntType.php +++ b/core/lib/Thelia/Type/IntType.php @@ -39,4 +39,9 @@ class IntType implements TypeInterface { return filter_var($value, FILTER_VALIDATE_INT) === false ? false : true; } + + public function getFormatedValue($value) + { + return $this->isValid($value) ? $value : null; + } } diff --git a/core/lib/Thelia/Type/JsonType.php b/core/lib/Thelia/Type/JsonType.php index 805977a32..397967df9 100755 --- a/core/lib/Thelia/Type/JsonType.php +++ b/core/lib/Thelia/Type/JsonType.php @@ -37,7 +37,12 @@ class JsonType implements TypeInterface public function isValid($value) { - json_decode($value); + json_decode($value, true); return (json_last_error() == JSON_ERROR_NONE); } + + public function getFormatedValue($value) + { + return $this->isValid($value) ? json_decode($value, true) : null; + } } diff --git a/core/lib/Thelia/Type/TypeCollection.php b/core/lib/Thelia/Type/TypeCollection.php index 4430be5df..bff384f92 100755 --- a/core/lib/Thelia/Type/TypeCollection.php +++ b/core/lib/Thelia/Type/TypeCollection.php @@ -132,4 +132,14 @@ class TypeCollection implements \Iterator return false; } + + public function getFormatedValue($value) { + foreach($this as $type) { + if($type->isValid($value)) { + return $type->getFormatedValue($value); + } + } + + return null; + } } diff --git a/core/lib/Thelia/Type/TypeInterface.php b/core/lib/Thelia/Type/TypeInterface.php index ae626f0b9..9268df7d5 100755 --- a/core/lib/Thelia/Type/TypeInterface.php +++ b/core/lib/Thelia/Type/TypeInterface.php @@ -33,4 +33,6 @@ interface TypeInterface public function getType(); public function isValid($value); + + public function getFormatedValue($value); } diff --git a/templates/admin/default/login.html b/templates/admin/default/login.html index 5c33d742e..20fadb19b 100755 --- a/templates/admin/default/login.html +++ b/templates/admin/default/login.html @@ -2,7 +2,7 @@ {include file='includes/header.inc.html'}
- + {{intl l='abcd'}|capitalize} @@ -15,7 +15,7 @@

{intl l='Thelia Back Office'}

-
+ {form_field_hidden form=$form} {form_field form=$form.username} {form_error form=$form.username} @@ -27,7 +27,9 @@ {/form_field} + {form_field form=$form.remember_me} + {/form_field}
diff --git a/templates/smarty-sample/category.html b/templates/smarty-sample/category.html new file mode 100755 index 000000000..594be9cc6 --- /dev/null +++ b/templates/smarty-sample/category.html @@ -0,0 +1,29 @@ +{*loop name="category0" type="category" parent="0"} +

CATEGORY : #TITLE

+ {loop name="category1" type="category" parent="#ID"} +
+

SUBCATEGORY : #TITLE (#LOOP_COUNT / #LOOP_TOTAL)

+ {loop name="product" type="product" category="#ID"} +

PRODUCT : #REF / #TITLE

+ #PRICE € + {/loop} +
+
+ {/loop} + + {loop name="product" type="product" category="#ID"} +

PRODUCT : #REF / #TITLE

+ #PRICE € + {/loop} +
+
+{/loop*} +

PRODUCTS

+{loop name="product" type="product" order="min_price"} +

PRODUCT : #REF / #TITLE

+ price : #PRICE €
+ promo price : #PROMO_PRICE €
+ is promo : #PROMO
+ is new : #NEW
+ weight : #WEIGHT
+{/loop} diff --git a/templates/smarty-sample/included.html b/templates/smarty-sample/included.html new file mode 100644 index 000000000..93581ff7f --- /dev/null +++ b/templates/smarty-sample/included.html @@ -0,0 +1,12 @@ +{loop name="included0" type="category" parent="0"} +

Out before - CATEGORY : #TITLE (#LOOP_COUNT / #LOOP_TOTAL)

+ {loop name="category1" type="category" parent="#ID"} +

Inner - SUBCATEGORY : #TITLE (#LOOP_COUNT / #LOOP_TOTAL)

+ {/loop} + + {loop name="category2" type="category" parent="#ID"} +

Inner 2 - SUBCATEGORY : #TITLE (#LOOP_COUNT / #LOOP_TOTAL)

+ {/loop} +

Out after - CATEGORY : #TITLE (#LOOP_COUNT / #LOOP_TOTAL)

+
+{/loop} \ No newline at end of file diff --git a/templates/smarty-sample/index.html b/templates/smarty-sample/index.html index 5a73519b0..c185ae5f8 100755 --- a/templates/smarty-sample/index.html +++ b/templates/smarty-sample/index.html @@ -17,7 +17,7 @@ An image from asset directory :

Category loop example

    {loop type="category" name="catloop1"} -
  • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}, children: {$NB_CHILD} +
  • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}, children: {$NB_CHILD} {ifloop rel="inner1"}
      {loop type="category" name="inner1" parent="{$ID}"} @@ -40,7 +40,7 @@ An image from asset directory : Hey ! Loop catloop2 is not empty:
        {loop type="category" name="catloop2" parent="12"} -
      • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}
      • +
      • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}
      • {/loop}
      {/ifloop} @@ -57,7 +57,7 @@ An image from asset directory : Loop catloop3 is not empty:
        {loop type="category" name="catloop3" parent="0"} -
      • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}
      • +
      • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}
      • {/loop}
      {/ifloop} @@ -86,20 +86,20 @@ An image from asset directory :

      PAGE 1

        {loop type="category" name="catloopwithpagination1" limit="2" page="1"} -
      • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}
      • +
      • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}
      • {/loop}

      PAGE 2

        {loop type="category" name="catloopwithpagination2" limit="2" page="2"} -
      • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}
      • +
      • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}
      • {/loop}

      PAGE 1000

        {loop type="category" name="catloopwithpagination1000" limit="2" page="1000"} -
      • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}
      • +
      • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}
      • {/loop} {elseloop rel="catloopwithpagination1000"} @@ -114,7 +114,7 @@ An image from asset directory :

        PAGE {$current_page} :

          {loop type="category" name="catloopwithpaginationchoice" limit="2" page="{$current_page}"} -
        • {$__COUNT__}/{$__TOTAL__} : {$ID} {$TITLE}
        • +
        • {$LOOP_COUNT}/{$LOOP_TOTAL} : {$ID} {$TITLE}
        • {/loop}

        page choice