Merge branch 'master' into loops

Conflicts:
	core/lib/Thelia/Core/Template/Loop/Category.php
	core/lib/Thelia/Core/Template/Loop/FeatureValue.php
	core/lib/Thelia/Core/Template/Loop/Folder.php
	core/lib/Thelia/Core/Template/Loop/Product.php
	core/lib/Thelia/Core/Template/Smarty/Plugins/TheliaLoop.php
	install/faker.php
This commit is contained in:
Etienne Roudeix
2013-08-21 09:19:56 +02:00
3275 changed files with 929970 additions and 274940 deletions

32
core/lib/Thelia/Action/BaseAction.php Normal file → Executable file
View File

@@ -22,33 +22,30 @@
/*************************************************************************************/
namespace Thelia\Action;
use Thelia\Form\BaseForm;
use Thelia\Action\Exception\FormValidationException;
use Thelia\Core\Event\ActionEvent;
use Symfony\Component\Form\Form;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BaseAction
{
/**
* @var The container
*/
protected $container;
public function __construct(ContainerInterface $container) {
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Validate a BaseForm
*
* @param BaseForm $aBaseForm the form
* @param string $expectedMethod the expected method, POST or GET, or null for any of them
* @throws FormValidationException is the form contains error, or the method is not the right one
* @param BaseForm $aBaseForm the form
* @param string $expectedMethod the expected method, POST or GET, or null for any of them
* @throws FormValidationException is the form contains error, or the method is not the right one
* @return \Symfony\Component\Form\Form Form the symfony form object
*/
protected function validateForm(BaseForm $aBaseForm, $expectedMethod = null)
@@ -60,14 +57,11 @@ class BaseAction
$form->bind($aBaseForm->getRequest());
if ($form->isValid()) {
return $form;
}
else {
} else {
throw new FormValidationException("Missing or invalid data");
}
}
else {
} else {
throw new FormValidationException(sprintf("Wrong form method, %s expected.", $expectedMethod));
}
}
@@ -75,12 +69,12 @@ class BaseAction
/**
* Propagate a form error in the action event
*
* @param BaseForm $aBaseForm the form
* @param string $error_message an error message that may be displayed to the customer
* @param ActionEvent $event the action event
* @param BaseForm $aBaseForm the form
* @param string $error_message an error message that may be displayed to the customer
* @param ActionEvent $event the action event
*/
protected function propagateFormError(BaseForm $aBaseForm, $error_message, ActionEvent $event) {
protected function propagateFormError(BaseForm $aBaseForm, $error_message, ActionEvent $event)
{
// The form has an error
$aBaseForm->setError(true);
$aBaseForm->setErrorMessage($error_message);
@@ -102,4 +96,4 @@ class BaseAction
return $this->container->get('event_dispatcher');
}
}
}

View File

@@ -25,10 +25,8 @@ namespace Thelia\Action;
use Propel\Runtime\Exception\PropelException;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Core\Event\ActionEvent;
use Thelia\Core\Event\CartEvent;
use Thelia\Form\CartAdd;
use Thelia\Model\ProductPrice;
@@ -36,7 +34,6 @@ use Thelia\Model\ProductPriceQuery;
use Thelia\Model\CartItem;
use Thelia\Model\CartItemQuery;
use Thelia\Model\ConfigQuery;
use Thelia\Action\Exception\FormValidationException;
/**
*
@@ -52,74 +49,46 @@ class Cart extends BaseAction implements EventSubscriberInterface
* add an article in the current cart
* @param \Thelia\Core\Event\CartEvent $event
*/
public function addArticle(CartEvent $event)
public function addItem(CartEvent $event)
{
$request = $event->getRequest();
$message = null;
try {
$cartAdd = $this->getAddCartForm($request);
$cart = $event->getCart();
$newness = $event->getNewness();
$append = $event->getAppend();
$quantity = $event->getQuantity();
$form = $this->validateForm($cartAdd);
$productSaleElementsId = $event->getProductSaleElementsId();
$productId = $event->getProduct();
$cart = $event->getCart();
$newness = $form->get("newness")->getData();
$append = $form->get("append")->getData();
$quantity = $form->get("quantity")->getData();
$cartItem = $this->findItem($cart->getId(), $productId, $productSaleElementsId);
$productSaleElementsId = $form->get("product_sale_elements_id")->getData();
$productId = $form->get("product")->getData();
if ($cartItem === null || $newness) {
$productPrice = ProductPriceQuery::create()
->filterByProductSaleElementsId($productSaleElementsId)
->findOne();
$cartItem = $this->findItem($cart->getId(), $productId, $productSaleElementsId);
if ($cartItem === null || $newness) {
$productPrice = ProductPriceQuery::create()
->filterByProductSaleElementsId($productSaleElementsId)
->findOne()
;
$this->addItem($cart, $productId, $productSaleElementsId, $quantity, $productPrice);
}
if ($append && $cartItem !== null) {
$this->updateQuantity($cartItem, $quantity);
}
} catch (PropelException $e) {
\Thelia\Log\Tlog::getInstance()->error(sprintf("Failed to add item to cart with message : %s", $e->getMessage()));
$message = "Failed to add this article to your cart, please try again";
} catch (FormValidationException $e) {
$message = $e->getMessage();
}
if($message) {
// The form has errors, propagate it.
$this->propagateFormError($cartAdd, $message, $event);
$this->doAddItem($cart, $productId, $productSaleElementsId, $quantity, $productPrice);
}
if ($append && $cartItem !== null) {
$this->updateQuantity($cartItem, $quantity);
}
}
/**
*
* Delete specify article present into cart
*
* @param \Thelia\Core\Event\CartEvent $event
*/
public function deleteArticle(CartEvent $event)
public function deleteItem(CartEvent $event)
{
$request = $event->getRequest();
if (null !== $cartItemId = $request->get('cartItem')) {
if (null !== $cartItemId = $event->getCartItem()) {
$cart = $event->getCart();
try {
$cartItem = CartItemQuery::create()
->filterByCartId($cart->getId())
->filterById($cartItemId)
->delete();
} catch (PropelException $e) {
\Thelia\Log\Tlog::getInstance()->error(sprintf("error during deleting cartItem with message : %s", $e->getMessage()));
}
$cartItem = CartItemQuery::create()
->filterByCartId($cart->getId())
->filterById($cartItemId)
->delete();
}
}
@@ -132,25 +101,18 @@ class Cart extends BaseAction implements EventSubscriberInterface
*
* @param \Thelia\Core\Event\CartEvent $event
*/
public function modifyArticle(CartEvent $event)
public function changeItem(CartEvent $event)
{
$request = $event->getRequest();
if ((null !== $cartItemId = $event->getCartItem()) && (null !== $quantity = $event->getQuantity())) {
$cart = $event->getCart();
if (null !== $cartItemId = $request->get("cartItem") && null !== $quantity = $request->get("quantity")) {
$cartItem = CartItemQuery::create()
->filterByCartId($cart->getId())
->filterById($cartItemId)
->findOne();
try {
$cart = $event->getCart($request);
$cartItem = CartItemQuery::create()
->filterByCartId($cart->getId())
->filterById($cartItemId)
->findOne();
if ($cartItem) {
$this->updateQuantity($cartItem, $quantity);
}
} catch (PropelException $e) {
\Thelia\Log\Tlog::getInstance()->error(sprintf("error during updating cartItem with message : %s", $e->getMessage()));
if ($cartItem) {
$this->updateQuantity($cartItem, $quantity);
}
}
}
@@ -178,24 +140,22 @@ class Cart extends BaseAction implements EventSubscriberInterface
public static function getSubscribedEvents()
{
return array(
"action.addArticle" => array("addArticle", 128),
"action.deleteArticle" => array("deleteArticle", 128),
"action.changeArticle" => array("modifyArticle", 128),
"action.addArticle" => array("addItem", 128),
"action.deleteArticle" => array("deleteItem", 128),
"action.changeArticle" => array("changeItem", 128),
);
}
/**
* increase the quantity for an existing cartItem
*
* @param CartItem $cartItem
* @param float $quantity
* @param float $quantity
*/
protected function updateQuantity(CartItem $cartItem, $quantity)
{
$cartItem->setDisptacher($this->getDispatcher());
$cartItem->addQuantity($quantity)
$cartItem->updateQuantity($quantity)
->save();
}
@@ -203,12 +163,12 @@ class Cart extends BaseAction implements EventSubscriberInterface
* try to attach a new item to an existing cart
*
* @param \Thelia\Model\Cart $cart
* @param int $productId
* @param int $productSaleElementsId
* @param float $quantity
* @param ProductPrice $productPrice
* @param int $productId
* @param int $productSaleElementsId
* @param float $quantity
* @param ProductPrice $productPrice
*/
protected function addItem(\Thelia\Model\Cart $cart, $productId, $productSaleElementsId, $quantity, ProductPrice $productPrice)
protected function doAddItem(\Thelia\Model\Cart $cart, $productId, $productSaleElementsId, $quantity, ProductPrice $productPrice)
{
$cartItem = new CartItem();
$cartItem->setDisptacher($this->getDispatcher());
@@ -227,9 +187,9 @@ class Cart extends BaseAction implements EventSubscriberInterface
* find a specific record in CartItem table using the Cart id, the product id
* and the product_sale_elements id
*
* @param int $cartId
* @param int $productId
* @param int $productSaleElementsId
* @param int $cartId
* @param int $productId
* @param int $productSaleElementsId
* @return ChildCartItem
*/
protected function findItem($cartId, $productId, $productSaleElementsId)
@@ -241,28 +201,4 @@ class Cart extends BaseAction implements EventSubscriberInterface
->findOne();
}
/**
* Find the good way to construct the cart form
*
* @param Request $request
* @return CartAdd
*/
private function getAddCartForm(Request $request)
{
if ($request->isMethod("post")) {
$cartAdd = new CartAdd($request);
} else {
$cartAdd = new CartAdd(
$request,
"form",
array(),
array(
'csrf_protection' => false,
)
);
}
return $cartAdd;
}
}

176
core/lib/Thelia/Action/Category.php Normal file → Executable file
View File

@@ -40,15 +40,14 @@ use Propel\Runtime\Propel;
use Thelia\Model\Map\CategoryTableMap;
use Propel\Runtime\Exception\PropelException;
class Category extends BaseAction implements EventSubscriberInterface
{
public function create(ActionEvent $event)
{
$this->checkAuth("ADMIN", "admin.category.create");
$this->checkAuth("ADMIN", "admin.category.create");
$request = $event->getRequest();
$request = $event->getRequest();
try {
$categoryCreationForm = new CategoryCreationForm($request);
@@ -92,7 +91,7 @@ class Category extends BaseAction implements EventSubscriberInterface
public function modify(ActionEvent $event)
{
$this->checkAuth("ADMIN", "admin.category.delete");
$this->checkAuth("ADMIN", "admin.category.delete");
$request = $event->getRequest();
@@ -163,9 +162,9 @@ class Category extends BaseAction implements EventSubscriberInterface
public function delete(ActionEvent $event)
{
$this->checkAuth("ADMIN", "admin.category.delete");
$this->checkAuth("ADMIN", "admin.category.delete");
$request = $event->getRequest();
$request = $event->getRequest();
try {
$categoryDeletionForm = new CategoryDeletionForm($request);
@@ -214,11 +213,11 @@ class Category extends BaseAction implements EventSubscriberInterface
public function toggleVisibility(ActionEvent $event)
{
$this->checkAuth("ADMIN", "admin.category.edit");
$this->checkAuth("ADMIN", "admin.category.edit");
$request = $event->getRequest();
$request = $event->getRequest();
$category = CategoryQuery::create()->findPk($request->get('category_id', 0));
$category = CategoryQuery::create()->findPk($request->get('category_id', 0));
if ($category !== null) {
@@ -237,8 +236,9 @@ class Category extends BaseAction implements EventSubscriberInterface
*
* @param ActionEvent $event
*/
public function changePositionUp(ActionEvent $event) {
return $this->exchangePosition($event, 'up');
public function changePositionUp(ActionEvent $event)
{
return $this->exchangePosition($event, 'up');
}
/**
@@ -246,67 +246,65 @@ class Category extends BaseAction implements EventSubscriberInterface
*
* @param ActionEvent $event
*/
public function changePositionDown(ActionEvent $event) {
return $this->exchangePosition($event, 'down');
public function changePositionDown(ActionEvent $event)
{
return $this->exchangePosition($event, 'down');
}
/**
* Move up or down a category
*
* @param ActionEvent $event
* @param string $direction up to move up, down to move down
* @param string $direction up to move up, down to move down
*/
protected function exchangePosition(ActionEvent $event, $direction) {
protected function exchangePosition(ActionEvent $event, $direction)
{
$this->checkAuth("ADMIN", "admin.category.edit");
$this->checkAuth("ADMIN", "admin.category.edit");
$request = $event->getRequest();
$request = $event->getRequest();
$category = CategoryQuery::create()->findPk($request->get('category_id', 0));
$category = CategoryQuery::create()->findPk($request->get('category_id', 0));
if ($category !== null) {
if ($category !== null) {
// The current position of the category
$my_position = $category->getPosition();
// The current position of the category
$my_position = $category->getPosition();
// Find category to exchange position with
$search = CategoryQuery::create()
->filterByParent($category->getParent());
// Find category to exchange position with
$search = CategoryQuery::create()
->filterByParent($category->getParent());
// Up or down ?
if ($direction == 'up') {
// Find the category immediately before me
$search->filterByPosition(array('max' => $my_position-1))->orderByPosition(Criteria::DESC);
} elseif ($direction == 'down') {
// Find the category immediately after me
$search->filterByPosition(array('min' => $my_position+1))->orderByPosition(Criteria::ASC);
} else
// Up or down ?
if ($direction == 'up') {
// Find the category immediately before me
$search->filterByPosition(array('max' => $my_position-1))->orderByPosition(Criteria::DESC);
}
else if ($direction == 'down') {
// Find the category immediately after me
$search->filterByPosition(array('min' => $my_position+1))->orderByPosition(Criteria::ASC);
}
else
return;
return;
$result = $search->findOne();
$result = $search->findOne();
// If we found the proper category, exchange their positions
if ($result) {
// If we found the proper category, exchange their positions
if ($result) {
$cnx = Propel::getWriteConnection(CategoryTableMap::DATABASE_NAME);
$cnx = Propel::getWriteConnection(CategoryTableMap::DATABASE_NAME);
$cnx->beginTransaction();
$cnx->beginTransaction();
try {
$category->setPosition($result->getPosition())->save();
try {
$category->setPosition($result->getPosition())->save();
$result->setPosition($my_position)->save();
$result->setPosition($my_position)->save();
$cnx->commit();
}
catch(Exception $e) {
$cnx->rollback();
}
}
}
$cnx->commit();
} catch (Exception $e) {
$cnx->rollback();
}
}
}
}
/**
@@ -314,61 +312,59 @@ class Category extends BaseAction implements EventSubscriberInterface
*
* @param ActionEvent $event
*/
public function changePosition(ActionEvent $event) {
public function changePosition(ActionEvent $event)
{
$this->checkAuth("ADMIN", "admin.category.edit");
$this->checkAuth("ADMIN", "admin.category.edit");
$request = $event->getRequest();
$request = $event->getRequest();
$category = CategoryQuery::create()->findPk($request->get('category_id', 0));
$category = CategoryQuery::create()->findPk($request->get('category_id', 0));
if ($category !== null) {
if ($category !== null) {
// The required position
$new_position = $request->get('position', null);
// The required position
$new_position = $request->get('position', null);
// The current position
$current_position = $category->getPosition();
// The current position
$current_position = $category->getPosition();
if ($new_position != null && $new_position > 0 && $new_position != $current_position) {
if ($new_position != null && $new_position > 0 && $new_position != $current_position) {
// Find categories to offset
$search = CategoryQuery::create()->filterByParent($category->getParent());
// 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));
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;
}
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;
}
$delta = 1;
}
$results = $search->find();
$results = $search->find();
$cnx = Propel::getWriteConnection(CategoryTableMap::DATABASE_NAME);
$cnx = Propel::getWriteConnection(CategoryTableMap::DATABASE_NAME);
$cnx->beginTransaction();
$cnx->beginTransaction();
try {
foreach ($results as $result) {
$result->setPosition($result->getPosition() + $delta)->save($cnx);
}
try {
foreach($results as $result) {
$result->setPosition($result->getPosition() + $delta)->save($cnx);
}
$category->setPosition($new_position)->save($cnx);
$category->setPosition($new_position)->save($cnx);
$cnx->commit();
}
catch(Exception $e) {
$cnx->rollback();
}
}
}
}
$cnx->commit();
} catch (Exception $e) {
$cnx->rollback();
}
}
}
}
/**
* Returns an array of event names this subscriber listens to.

View File

@@ -25,8 +25,8 @@ namespace Thelia\Action;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Core\Event\ActionEvent;
use Thelia\Core\Event\CustomerCreateOrUpdateEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Form\BaseForm;
use Thelia\Form\CustomerCreation;
use Thelia\Form\CustomerModification;
use Thelia\Model\Customer as CustomerModel;
@@ -34,105 +34,57 @@ use Thelia\Log\Tlog;
use Thelia\Model\CustomerQuery;
use Thelia\Form\CustomerLogin;
use Thelia\Core\Security\Authentication\CustomerUsernamePasswordFormAuthenticator;
use Thelia\Core\Security\SecurityContext;
use Symfony\Component\Validator\Exception\ValidatorException;
use Thelia\Core\Security\Exception\AuthenticationException;
use Thelia\Core\Security\Exception\UsernameNotFoundException;
use Propel\Runtime\Exception\PropelException;
use Thelia\Action\Exception\FormValidationException;
class Customer extends BaseAction implements EventSubscriberInterface
{
public function create(ActionEvent $event)
public function create(CustomerCreateOrUpdateEvent $event)
{
$request = $event->getRequest();
try {
$customerCreationForm = new CustomerCreation($request);
$customer = new CustomerModel();
$customer->setDispatcher($this->getDispatcher());
$form = $this->validateForm($customerCreationForm, "POST");
$this->createOrUpdateCustomer($customer, $event);
$data = $form->getData();
$customer = new CustomerModel();
$customer->setDispatcher($event->getDispatcher());
$customer->createOrUpdate(
$data["title"],
$data["firstname"],
$data["lastname"],
$data["address1"],
$data["address2"],
$data["address3"],
$data["phone"],
$data["cellphone"],
$data["zipcode"],
$data["country"],
$data["email"],
$data["password"],
$request->getSession()->getLang()
);
$event->customer = $customer;
} catch (PropelException $e) {
Tlog::getInstance()->error(sprintf('error during creating customer on action/createCustomer with message "%s"', $e->getMessage()));
$message = "Failed to create your account, please try again.";
} catch (FormValidationException $e) {
$message = $e->getMessage();
}
// The form has errors, propagate it.
$this->propagateFormError($customerCreationForm, $message, $event);
}
public function modify(ActionEvent $event)
public function modify(CustomerCreateOrUpdateEvent $event)
{
$request = $event->getRequest();
try {
$customerModification = new CustomerModification($request);
$customer = $event->getCustomer();
$customer->setDispatcher($this->getDispatcher());
$form = $this->validateForm($customerModification, "POST");
$this->createOrUpdateCustomer($customer, $event);
$data = $form->getData();
}
$customer = CustomerQuery::create()->findPk(1);
private function createOrUpdateCustomer(CustomerModel $customer, CustomerCreateOrUpdateEvent $event)
{
$customer->createOrUpdate(
$event->getTitle(),
$event->getFirstname(),
$event->getLastname(),
$event->getAddress1(),
$event->getAddress2(),
$event->getAddress3(),
$event->getPhone(),
$event->getCellphone(),
$event->getZipcode(),
$event->getCity(),
$event->getCountry(),
$event->getEmail(),
$event->getPassword(),
$event->getLang(),
$event->getReseller(),
$event->getSponsor(),
$event->getDiscount()
);
$data = $form->getData();
$customer->createOrUpdate(
$data["title"],
$data["firstname"],
$data["lastname"],
$data["address1"],
$data["address2"],
$data["address3"],
$data["phone"],
$data["cellphone"],
$data["zipcode"],
$data["country"]
);
// Update the logged-in user, and redirect to the success URL (exits)
// We don-t send the login event, as the customer si already logged.
$this->processSuccessfullLogin($event, $customer, $customerModification);
} catch (PropelException $e) {
Tlog::getInstance()->error(sprintf('error during modifying customer on action/modifyCustomer with message "%s"', $e->getMessage()));
$message = "Failed to change your account, please try again.";
} catch (FormValidationException $e) {
$message = $e->getMessage();
}
// The form has errors, propagate it.
$this->propagateFormError($customerModification, $message, $event);
$event->setCustomer($customer);
}
/**
@@ -144,49 +96,7 @@ class Customer extends BaseAction implements EventSubscriberInterface
{
$event->getDispatcher()->dispatch(TheliaEvents::CUSTOMER_LOGOUT, $event);
$this->getFrontSecurityContext()->clear();
}
/**
* Perform user login. On a successful login, the user is redirected to the URL
* found in the success_url form parameter, or / if none was found.
*
* If login is not successfull, the same view is dispolyed again.
*
* @param ActionEvent $event
*/
public function login(ActionEvent $event)
{
$request = $event->getRequest();
$customerLoginForm = new CustomerLogin($request);
$authenticator = new CustomerUsernamePasswordFormAuthenticator($request, $customerLoginForm);
try {
$user = $authenticator->getAuthentifiedUser();
$event->customer = $customer;
} catch (ValidatorException $ex) {
$message = "Missing or invalid information. Please check your input.";
} catch (UsernameNotFoundException $ex) {
$message = "This email address was not found.";
} catch (AuthenticationException $ex) {
$message = "Login failed. Please check your username and password.";
} catch (\Exception $ex) {
$message = sprintf("Unable to process your request. Please try again (%s in %s).", $ex->getMessage(), $ex->getFile());
}
// The for has an error
$customerLoginForm->setError(true);
$customerLoginForm->setErrorMessage($message);
// Dispatch the errored form
$event->setErrorForm($customerLoginForm);
// A this point, the same view is displayed again.
$this->getFrontSecurityContext()->clear();
}
public function changePassword(ActionEvent $event)
@@ -219,8 +129,6 @@ class Customer extends BaseAction implements EventSubscriberInterface
return array(
"action.createCustomer" => array("create", 128),
"action.modifyCustomer" => array("modify", 128),
"action.loginCustomer" => array("login", 128),
"action.logoutCustomer" => array("logout", 128),
);
}
}

View File

@@ -1,28 +0,0 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Action\Exception;
class ActionException extends \RuntimeException
{
}

View File

@@ -1,28 +0,0 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Action\Exception;
class FormValidationException extends ActionException
{
}

View File

@@ -1,28 +0,0 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Action\Exception;
class ProductNotFoundException extends ActionException
{
}

View File

@@ -1,29 +0,0 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Action\Exception;
class StockNotFoundException extends ActionException
{
}

497
core/lib/Thelia/Action/Image.php Executable file
View File

@@ -0,0 +1,497 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Thelia\Action;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Core\Event\ActionEvent;
use Thelia\Core\Event\ImageEvent;
use Thelia\Model\ConfigQuery;
use Thelia\Tools\URL;
use Imagine\Image\ImagineInterface;
use Imagine\Image\ImageInterface;
use Imagine\Image\Box;
use Imagine\Image\Color;
use Imagine\Image\Point;
use Thelia\Exception\ImageException;
use Thelia\Core\Event\TheliaEvents;
/**
*
* Image management actions. This class handles image processing an caching.
*
* Basically, images are stored outside the web space (by default in local/media/images),
* and cached in the web space (by default in web/local/images).
*
* In the images caches directory, a subdirectory for images categories (eg. product, category, folder, etc.) is
* automatically created, and the cached image is created here. Plugin may use their own subdirectory as required.
*
* The cached image name contains a hash of the processing options, and the original (normalized) name of the image.
*
* A copy (or symbolic link, by default) of the original image is always created in the cache, so that the full
* resolution image is always available.
*
* Various image processing options are available :
*
* - resizing, with border, crop, or by keeping image 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 image Gamma to the specified value. Example: gamma:0.7
* - grayscale or greyscale: switch image to grayscale
* - colorize:color : apply a color mask to the image. Exemple: colorize:#ff2244
* - negative : transform the image in its negative equivalent
* - vflip or vertical_flip : vertical flip
* - hflip or horizontal_flip : horizontal flip
*
* If a problem occurs, an ImageException may be thrown.
*
* @package Thelia\Action
* @author Franck Allimant <franck@cqfdev.fr>
*
*/
class Image extends BaseAction implements EventSubscriberInterface
{
// Resize mode constants
const EXACT_RATIO_WITH_BORDERS = 1;
const EXACT_RATIO_WITH_CROP = 2;
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
*/
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());
}
else if ($fileinfo->isDir()) {
$this->clearDirectory($fileinfo->getPathname());
}
}
}
/**
* Process image and write the result in the image cache.
*
* If the image already exists in cache, the cache file is immediately returned, without any processing
* If the original (full resolution) image is required, create either a symbolic link with the
* original image in the cache dir, or copy it in the cache dir.
*
* This method updates the cache_file_path and file_url attributes of the event
*
* @param ImageEvent $event
* @throws \InvalidArgumentException, ImageException
*/
public function processImage(ImageEvent $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");
}
// echo basename($source_file).": ";
// Find cached file path
$cacheFilePath = $this->getCacheFilePath($subdir, $source_file, $event);
$originalImagePathInCache = $this->getCacheFilePath($subdir, $source_file, $event, true);
if (! file_exists($cacheFilePath)) {
if (! file_exists($source_file)) {
throw new ImageException(sprintf("Source image file %s does not exists.", $source_file));
}
// Create a chached version of the original image in the web space, if not exists
if (! file_exists($originalImagePathInCache)) {
$mode = ConfigQuery::read('original_image_delivery_mode', 'symlink');
if ($mode == 'symlink') {
if (false == symlink($source_file, $originalImagePathInCache)) {
throw new ImageException(sprintf("Failed to create symbolic link for %s in %s image cache directory", basename($source_file), $subdir));
}
}
else {// mode = 'copy'
if (false == @copy($source_file, $originalImagePathInCache)) {
throw new ImageException(sprintf("Failed to copy %s in %s image cache directory", basename($source_file), $subdir));
}
}
}
// Process image only if we have some transformations to do.
if (! $event->isOriginalImage()) {
// We have to process the image.
$imagine = $this->createImagineInstance();
$image = $imagine->open($source_file);
if ($image) {
$background_color = $event->getBackgroundColor();
if ($background_color != null) {
$bg_color = new Color($background_color);
}
else
$bg_color = null;
// Apply resize
$image = $this->applyResize($imagine, $image, $event->getWidth(), $event->getHeight(), $event->getResizeMode(), $bg_color);
// Rotate if required
$rotation = intval($event->getRotation());
if ($rotation != 0)
$image->rotate($rotation, $bg_color);
// Flip
// Process each effects
foreach ($event->getEffects() as $effect) {
$effect = trim(strtolower($effect));
$params = explode(':', $effect);
switch ($params[0]) {
case 'greyscale':
case 'grayscale':
$image->effects()->grayscale();
break;
case 'negative':
$image->effects()->negative();
break;
case 'horizontal_flip':
case 'hflip':
$image->flipHorizontally();
break;
case 'vertical_flip':
case 'vflip':
$image-> flipVertically();
break;
case 'gamma':
// Syntax: gamma:value. Exemple: gamma:0.7
if (isset($params[1])) {
$gamma = floatval($params[1]);
$image->effects()->gamma($gamma);
}
break;
case 'colorize':
// Syntax: colorize:couleur. Exemple: colorize:#ff00cc
if (isset($params[1])) {
$the_color = new Color($params[1]);
$image->effects()->colorize($the_color);
}
break;
}
}
$quality = $event->getQuality();
if (is_null($quality)) $quality = ConfigQuery::read('default_image_quality_percent', 75);
$image->save(
$cacheFilePath,
array('quality' => $quality)
);
}
else {
throw new ImageException(sprintf("Source file %s cannot be opened.", basename($source_file)));
}
}
}
// Compute the image URL
$processed_image_url = $this->getCacheFileURL($subdir, basename($cacheFilePath));
// compute the full resulution image path in cache
$original_image_url = $this->getCacheFileURL($subdir, basename($originalImagePathInCache));
// Update the event with file path and file URL
$event->setCacheFilepath($cacheFilePath);
$event->setCacheOriginalFilepath($originalImagePathInCache);
$event->setFileUrl(URL::absoluteUrl($processed_image_url, null, URL::PATH_TO_FILE));
$event->setOriginalFileUrl(URL::absoluteUrl($original_image_url, null, URL::PATH_TO_FILE));
}
/**
* Process image resizing, with borders or cropping. If $dest_width and $dest_height
* are both null, no resize is performed.
*
* @param ImagineInterface $imagine the Imagine instance
* @param ImageInterface $image the image to process
* @param int $dest_width the required width
* @param int $dest_height the required height
* @param int $resize_mode the resize mode (crop / bands / keep image ratio)p
* @param string $bg_color the bg_color used for bands
* @return ImageInterface the resized image.
*/
protected function applyResize(ImagineInterface $imagine, ImageInterface $image, $dest_width, $dest_height, $resize_mode, $bg_color)
{
if (! (is_null($dest_width) && is_null($dest_height))) {
$width_orig = $image->getSize()->getWidth();
$height_orig = $image->getSize()->getHeight();
if (is_null($dest_width))
$dest_width = $width_orig;
if (is_null($dest_height))
$dest_height = $height_orig;
if (is_null($resize_mode))
$resize_mode = self::KEEP_IMAGE_RATIO;
$width_diff = $dest_width / $width_orig;
$height_diff = $dest_height / $height_orig;
$delta_x = $delta_y = $border_width = $border_height = 0;
if ($width_diff > 1 AND $height_diff > 1) {
$next_width = $width_orig;
$next_height = $height_orig;
$dest_width = ($resize_mode == self::EXACT_RATIO_WITH_BORDERS ? $dest_width : $next_width);
$dest_height = ($resize_mode == self::EXACT_RATIO_WITH_BORDERS ? $dest_height : $next_height);
}
else if ($width_diff > $height_diff) {
// Image height > image width
$next_height = $dest_height;
$next_width = intval(($width_orig * $next_height) / $height_orig);
if ($resize_mode == self::EXACT_RATIO_WITH_CROP) {
$next_width = $dest_width;
$next_height = intval($height_orig * $dest_width / $width_orig);
$delta_y = ($next_height - $dest_height) / 2;
}
else if ($resize_mode != self::EXACT_RATIO_WITH_BORDERS) {
$dest_width = $next_width;
}
}
else {
// Image width > image height
$next_width = $dest_width;
$next_height = intval($height_orig * $dest_width / $width_orig);
if ($resize_mode == self::EXACT_RATIO_WITH_CROP) {
$next_height = $dest_height;
$next_width = intval(($width_orig * $next_height) / $height_orig);
$delta_x = ($next_width - $dest_width) / 2;
}
else if ($resize_mode != self::EXACT_RATIO_WITH_BORDERS) {
$dest_height = $next_height;
}
}
$image->resize(new Box($next_width, $next_height));
// echo "w=$dest_width, h=$dest_height, nw=$next_width, nh=$next_height, dx=$delta_x, dy=$delta_y, bw=$border_width, bh=$border_height\n";
if ($resize_mode == self::EXACT_RATIO_WITH_BORDERS) {
$border_width = intval(($dest_width - $next_width) / 2);
$border_height = intval(($dest_height - $next_height) / 2);
$canvas = new Box($dest_width, $dest_height);
return $imagine->create($canvas, $bg_color)
->paste($image, new Point($border_width, $border_height));
}
else if ($resize_mode == self::EXACT_RATIO_WITH_CROP) {
$image->crop(
new Point($delta_x, $delta_y),
new Box($dest_width, $dest_height)
);
}
}
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::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
*
* @return \Imagine\ImagineInterface
*/
protected function createImagineInstance()
{
$driver = ConfigQuery::read("imagine_graphic_driver", "gd");
switch ($driver) {
case 'imagik':
$image = new \Imagine\Imagick\Imagine();
break;
case 'gmagick':
$image = new \Imagine\Gmagick\Imagine();
break;
case 'gd':
default:
$image = new \Imagine\Gd\Imagine();
}
return $image;
}
public static function getSubscribedEvents()
{
return array(
TheliaEvents::IMAGE_PROCESS => array("processImage", 128),
TheliaEvents::IMAGE_CLEAR_CACHE => array("clearCache", 128),
);
}
}