Zend_Form_Element_Hash in Magento

I just managed to skip all the rather horrible and complex looking Magento form config by getting Zend_Form working in the controllers instead. (I reserve the right to retract what I said just there about Magento forms, seeing as I haven’t actually tried it that way yet!)

Anyway, as you know, CSRF (cross site request forgery) tokens are pretty important to secure up your forms, and Zend_Form_Element_Hash does a fine job of taking care of this, however the problem I got was the old “session already started” message. What I did was create my own Form_Element_Hash. It’s a copy paste job of the original Zend class, only it uses the Magento session. Here it is:

<?php /**  * Created by PhpStorm.  * User: delboy1978uk  * Date: 03/06/15  * Time: 15:30  */ class Del_Form_Element_Hash extends Zend_Form_Element_Xhtml {     /**      * Use formHidden view helper by default      * @var string      */     public $helper = 'formHidden';     /**      * Actual hash used.      *      * @var mixed      */     protected $_hash;     /**      * Salt for CSRF token      * @var string      */     protected $_salt = '54lt4ndP3pp3r';     /**      * @var Zend_Session_Namespace      */     protected $_session;     /**      * TTL for CSRF token      * @var int      */     protected $_timeout = 300;     /**      * Constructor      *      * Creates session namespace for CSRF token, and adds validator for CSRF      * token.      *      * @param array|string|Zend_Config $spec      * @param null $options      * @throws Zend_Form_Exception      */     public function __construct($spec, $options = null)     {         parent::__construct($spec, $options);         $this->setAllowEmpty(false)
            ->setRequired(true)
            ->initCsrfValidator();
    }

    /**
     * Set session object
     *
     * @param  Mage_Core_Model_Session $session
     * @return Del_Form_Element_Hash
     */
    public function setSession($session)
    {
        $this->_session = $session;
        return $this;
    }

    /**
     * Get session object
     *
     * Instantiate session object if none currently exists
     *
     * @return Mage_Core_Model_Session
     */
    public function getSession()
    {
        if (null === $this->_session)
        {
            $this->_session = Mage::getSingleton('core/session');
        }
        return $this->_session;
    }

    /**
     * Initialize CSRF validator
     *
     * Creates Session namespace, and initializes CSRF token in session.
     * Additionally, adds validator for validating CSRF token.
     *
     * @return Del_Form_Element_Hash
     */
    public function initCsrfValidator()
    {
        $session = $this->getSession();
        $key = $session->getData('csrf');
        if (isset($key))
        {
            $rightHash = $session->getData('csrf');
        }
        else
        {
            $rightHash = null;
        }

        $this->addValidator('Identical', true, array($rightHash));
        return $this;
    }

    /**
     * Salt for CSRF token
     *
     * @param  string $salt
     * @return Del_Form_Element_Hash
     */
    public function setSalt($salt)
    {
        $this->_salt = (string) $salt;
        return $this;
    }

    /**
     * Retrieve salt for CSRF token
     *
     * @return string
     */
    public function getSalt()
    {
        return $this->_salt;
    }

    /**
     * Retrieve CSRF token
     *
     * If no CSRF token currently exists, generates one.
     *
     * @return string
     */
    public function getHash()
    {
        if (null === $this->_hash) {
            $this->_generateHash();
        }
        return $this->_hash;
    }

    /**
     * Get session namespace for CSRF token
     *
     * Generates a session namespace based on salt, element name, and class.
     *
     * @return string
     */
    public function getSessionName()
    {
        return __CLASS__ . '_' . $this->getSalt() . '_' . $this->getName();
    }

    /**
     * Set timeout for CSRF session token
     *
     * @param  int $ttl
     * @return Del_Form_Element_Hash
     */
    public function setTimeout($ttl)
    {
        $this->_timeout = (int) $ttl;
        return $this;
    }

    /**
     * Get CSRF session token timeout
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->_timeout;
    }

    /**
     * Override getLabel() to always be empty
     *
     * @return null
     */
    public function getLabel()
    {
        return null;
    }

    /**
     * Initialize CSRF token in session
     *
     * @return void
     */
    public function initCsrfToken()
    {
        $session = $this->getSession();
        $session->setData('csrf',$this->getHash());
    }

    /**
     * Render CSRF token in form
     *
     * @param  Zend_View_Interface $view
     * @return string
     */
    public function render(Zend_View_Interface $view = null)
    {
        $this->initCsrfToken();
        return parent::render($view);
    }

    /**
     * Generate CSRF token
     *
     * Generates CSRF token and stores both in {@link $_hash} and element
     * value.
     *
     * @return void
     */
    protected function _generateHash()
    {
        $this->_hash = md5(
            mt_rand(1,1000000)
            .  $this->getSalt()
            .  $this->getName()
            .  mt_rand(1,1000000)
        );
        $this->setValue($this->_hash);
    }

}
Advertisements

Using Zend_Form in Magento

As we all know, Magento is built using some of the Zend Framework library. However, that doesn’t mean you can go straight into a controller and start programming like you would ZF. However, preparing your Zend_Form in order to work in Magento is pretty straigtforward.

Magento’s controllers do not have a view! So your form will fail to render! To fix this, we override the __toString() method:

/**
 *  Overridden because Magento doesnt have the zend view
 */
public function __toString()
{
    return  $this->render(new Zend_View());
}

Now your form will work in your magento controller.  If you are new to Magento like I am, you’ll also need to know how to instantiate the form, send the form to the view, and how to display it once we are in the view! In the controller:

public function indexAction()
{
    $this->loadLayout();
 
 // new My_Form_Class(); also works here, but this is how Magento does it, from the xml config.
 $this->form = Mage::getModel('madskull_feedback/form'); 

 if($this->getRequest()->isPost())
 {
   $data = $this->getRequest()->getPost();
   if($this->form->isValid($data))
   {
     // get sanitised data
     $data = $this->form->getValues();
     $this->sendFeedbackEmail($data);
     $this->_redirect('*/*/thanks');
   }
   else
   {
     $this->form->populate($data);
   }
  }

  $this->getLayout()->getBlock('madskull_feedback.form')->setData('form', $this->form);
  $this->renderLayout();
}

On the .phtml file, it’s a piece of cake:

echo  $this->getData('form');

There you go! Full Zend_Form goodness! Have fun!

Printing Magento currencies and prices on a PDF

This is a short little post, primarily just to remind me. We need to convert to html entities or symbols like $ and £ won’t print. There are lots of ways of getting the price from the product object, but getFinalPrice seems to be the simplest.

$raw_price = $this->getProduct()->getFinalPrice(); // string "£3,295.00"
$price =  htmlspecialchars(Mage::helper('core')->currency($raw_price,true,false));

Setting Magento to use SMTP

In my new work we run a Magento eCommerce store. As such, I’ve set up a fresh Vagrant Box through http://www.puphpet.com (try it!) and installed Magento into one of the vHosts. Stupidly, I forgot my password almost immediately, which highlighted a problem! Mail wasn’t sending out.

Of course, Zend_Mail defaults to using Zend_Mail_Transport_Sendmail and not Zend_Mail_Transport_Smtp, so I looked around for how to set this in Magento. Everyone on IRC told me to use 3rd party software! I checked it out, and the thing is totally insecure! https://github.com/aschroder/Magento-SMTP-Pro-Email-Extension/issues
So I decided against that.

The real answer lies in the way that Magento overrides existing classes. I told the guys in the #magento IRC channel that I refused to use the above insecure library, and would rather just hack the core ZF class.

It turns out that you can create a local directory in /app/code. So for a completely customised Zend_Mail without touching third party vendor files (ie. the ZF lib), you create /app/code/local/Zend/Mail.php, and tweak away to your liking!

line 1175 is the culprit . Change

$transport = new Zend_Mail_Transport_Sendmail();   // the ZF default
to
$transport = new Zend_Mail_Transport_Smtp();

Lastly, if you need to set a custom port (on my dev box, I wanted all my mail going through port 1025 so that Mailcatcher would stop development mails going out), then run the following SQL query on your DB:

INSERT INTO `core_config_data` (`config_id`, `scope`, `scope_id`, `path`, `value`) VALUES (NULL, 'default', 0, 'system/smtp/port', '1025');

If you set a  db prefix then tweak the above table name to suit. That should be you ready to rock! Have fun!

Getting Started with Pimcore

I’ve only just started using Pimcore, but of all the CMS’es out there, this looks the best. Anyway, in the admin panel, you can create new pages etc. As the developer, we can create the templates and views, but restrict to just the editable areas for standard cms users. These editable areas are called areablocks.

For instance, in my homepage.phtml, I have the following:

<?= $this->areablock('some-id-or-other'); ?>

Once there is an areablock on a view, when you are in the admin panel, you can select an area block from the collapsible menu and literally just drag it into place, and start editing.

Customised Area files go in the website/views/areas folder. I’m making a Carousel, so in website/views/areas, I created a gallery-carousel folder, and inside that I created a view.phtml file (or .php, depending on your pimcore prefs), and a area.xml:

<?xml version="1.0"?>
<zend-config xmlns:zf="http://framework.zend.com/xml/zend-config-xml/1.0/">
    <id>tabbed-slider-text</id>
    <name>Slider (Tabs/Text)</name>
    <description></description>
    <author>pimcore.org</author>
    <version>1.0</version>
</zend-config>
<section class="area-tabbed-slider-text">

    <?php if($this->editmode) { ?>
        <div class="alert alert-info">
            How many tabs you want to show?

            <?php
                // prepare the store
                $selectStore = [];
                for($i=2; $i<6; $i++) {
                    $selectStore[] = [$i, $i];
                }
            ?>
            <?= $this->select("slides",[
                "store" => $selectStore,
                "reload" => true,
                "width" => 60
            ]); ?>
        </div>
    <?php } ?>

    <?php
        $id = "tabbed-slider-" . uniqid();
        $slides = 2; // default value
        if(!$this->select("slides")->isEmpty())
        {
            $slides = (int) $this->select("slides")->getData();
        }
    ?>
    <div id="<?= $id ?>" class="tabbed-slider carousel slide">
        <div class="carousel-inner">
            <?php for($i=0; $i<$slides; $i++) { ?>
                <div class="item <?= ($i==0 ? "active" : "") ?> item-<?= $i ?> <?= $id . "-" . $i ?>">
                    <?php if(!$this->image("image_" . $i)->isEmpty() || $this->editmode) { ?>
                        <?= $this->image("image_" . $i, [
                            "dropClass" => $id . "-" . $i,
                            "thumbnail" => ""
                        ]); ?>
                    <?php } ?>
                    <div class="carousel-caption">
                        <h1><?= $this->input("headline_" . $i) ?></h1>
                        <p><?= $this->textarea("description_" . $i, ["nl2br" => true]) ?></p>
                    </div>
                </div>
            <?php } ?>
        </div>
        <!-- End Carousel Inner -->
        <ul class="nav nav-pills nav-justified">
            <?php for($i=0; $i<$slides; $i++) { ?>
                <li data-target="#<?= $id ?>" data-slide-to="<?= $i ?>" class="<?= ($i==0 ? "active" : "") ?> item-<?= $i ?>">
                    <a href="#">
                        <?= $this->input("pill-title_" . $i) ?>
                        <small><?= $this->input("pill-small_" . $i) ?></small>
                    </a></li>
            <?php } ?>
        </ul>
    </div>

</section>

The customised area blocks must be enabled before they will appear in Pimcore. Click on Extras -> Extensions in the Admin section, and you will see each folder you have added. Click the icon to enable/disable the area. Now if you refresh your page with the $this->areablock(…) code, you should see your customised area available to use!

Last thing! Pimcore has all manner of caching going on! Go into Settings > Cache in order to clear it and see your changes!

Custom Navigation in Pimcore

Pimcore looks to be a great CMS! I haven’t played around with it much but if you are a fan of Zend Framework 1 then you’ll be right at home using it!

Right now, I’m designing the front end for a small promotional site that requires a CMS admin panel etc, and I’m rendering a customised Navigation view, in order to get the Bootstrap classes that I want.

Pimcore allows you to run multiple sites from the one system. So first we put a check in our layout.phtml :

$navStartNode = $this->document->getProperty("navigationRoot");
    if(!$navStartNode instanceof Document_Page) 
    {
        if(Site::isSiteRequest()) 
        {
            $site = Site::getCurrentSite();
            $navStartNode = $site->getRootDocument();
        } 
        else 
        {
            $navStartNode = Document::getById(1);
        }
    }

The root site always has Document ID 1. In the Pimcore admin panel, you right click and add a new page, then right click and you can tell it to use that page as an actual site, so you can add as many as you like!

In the layout.phtml (or view partial like header.phtml), I have the following:

<div class="navbar-collapse collapse">
<?php
        $navigation = $this->pimcoreNavigation()->getNavigation($this->document, $navStartNode);;
        echo $this->navigation()->menu()->renderPartial($navigation, 'elements/theme-nav.phtml', array(
                "maxDepth" => 1,
                "ulClass" => "nav navbar-nav"
          ));
 ?>
</div>

Pimcore checks in the views folder for the partial (elements/theme-nav.phtml in my example). In my project the views folder is located in /website/views/scripts, so create an elements folder, and create theme-nav.phtml:

<ul class="nav navbar-nav"  data-0="margin-top:20px;" data-300="margin-top:5px;">
    <?php
    foreach ($this->container as $page) 
    {
        echo '<li>'.$this->navigation()->menu()->htmlify($page).'</li>';
    }
    ?>
</ul>

And that’s it! Your Navigation is now rendered in the way we want!