<?php
/**
 * Copyright (c) 2008-2011 Andreas Heigl<andreas@heigl.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category  Hyphenator
 * @package   Org\Heigl\Hyphenator
 * @author    Andreas Heigl <andreas@heigl.org>
 * @copyright 2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license   http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version   2.0.1
 * @link      http://github.com/heiglandreas/Hyphenator
 * @since     02.11.2011
 */

namespace Org\Heigl\Hyphenator;

/**
 * This class implements word-hyphenation
 *
 * Word-hyphenation is implemented on the basis of the algorithms developed by
 * Franklin Mark Liang for LaTeX as described in his dissertation at the department
 * of computer science at stanford university.
 *
 * The idea to this package came from Mathias Nater <mnater@mac.com> who
 * implemented this word-hyphenation-algorithm for javascript.
 *
 * After Implementing that algorithm for the first Hyphenator-Version I stumbled
 * over the Informations of LÁSZLÓ NÉMETH from OpenOffice.org.
 *
 * That brought me to change three things for the next Version of the
 * Hyphenator.
 *

 *
Use the Dictionary files from OpenOffice.org instead of the ones directly
 * from Tex because the OOo-Files are already stripped of the unnecessary
 * Informations

 *
Add the possibility to use non-standard hyphenations

 *
Add the possibility to add better word-tokenising

 *

 *
 * Beside those changes there are some other changes between the first and the
 * second version of the Hyphenator.
 *
 * So Version 2 of the Hyphenator

 *
requires PHP5.3 as it uses namespaces.

 *
aims to 100% Code-Coverage via Unit-Tests

 *
removes some unnecessary options

 *
is completely rewritten from scratch

 *

 *
 * So here is the smalest example for the usage of the class:
 *
 * <?php
 * use \Org\Heigl\Hyphenator as h;
 * // Create a hyphenator-instance based on a given config-file
 * $hyphenator = h\Hyphenator::factory('/path/to/the/config/file.properties');
 *
 * // And hyphenate a given string
 * $hyphenatedText = $hyphenator->hyphenate($string);
 *
 * Registering the autoloader is essential before the first call to the
 * Hyphenator
 *
 * <?php
 * require_once '/path/to/Org/Heigl/Hyphenator/Hyphenator.php';
 * spl_autoload_register('\Org\Heigl\Hyphenator\Hyphenator::__autoload');
 *
 * Of course the Hyphenator can be adapted to the most requirements via an
 * Options-Object. And the tokenisation in this small example uses the simple
 * WhiteSpace-Tokenizer. Other more complex Tokenizers are available.
 *
 * Examples for those can be found at http://github.com/heiglandreas/Hyphenator
 *
 * @category  Org_Heigl
 * @package   Org_Heigl_Hyphenator
 * @author    Andreas Heigl
 * @copyright 2008-2011 Andreas Heigl
 * @license   http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version   2.0.1
 * @link      http://code.google.com/p/hyphenator
 * @link      http://www.tug.org/docs/liang/liang-thesis.pdf
 * @link      http://hunspell.sourceforge.net/tb87nemeth.pdf
 * @link      http://github.com/heiglandreas/Hyphenator
 * @since     04.11.2011
 */
final class Hyphenator
{

    /**
     * The highest possible hyphernation quality
     *
     * @const int QUALITY_HIGHEST
     */
    const QUALITY_HIGHEST = 9;

    /**
     * A high hyphernation quality
     *
     * @const int QUALITY_HIGH
     */
    const QUALITY_HIGH    = 7;

    /**
     * A medium hyphernation quality
     *
     * @const int QUALITY_NORMAL
     */
    const QUALITY_NORMAL  = 5;

    /**
     * A low hyphernation quality
     *
     * @const int QUALITY_LOW
     */
    const QUALITY_LOW     = 3;

    /**
     * The lowest possible hyphernation quality
     *
     * @const int QUALITY_LOWEST
     */
    const QUALITY_LOWEST  = 1;

    /**
     * Storage for the Home-path.
     *
     * The hyphenation-files iare searched in different places.
     *

Location given via the constant HYPHENATOR_HOME

     *
Location set via \Org\Heigl\Hyphenator\Hyphenator::setDefaultHome()

     *
Location set via \Org\Heigl\Hyphenator\Hyphenator::setHome()

     *
The 'share'-Folder inside the Hyphenator-Package

     *

     *
     * The configuration-object can also be obtained using the
     * \Org\Heigl\Hyphenator::getConfig()-Method and can then be adapted
     * according to ones needs.
     *
     * @var string $homePath
     */
    private $homePath = null;

    /**
     * Storage of the default Home-Path.
     *
     * @var string $defaultHomePath
     */
    private static $defaultHomePath = null;

    /**
     * Storage for the Options-Object.
     *
     * @var Options $options
     */
    private $options = null;

    /**
     * Storage for the Dictionaries.
     *
     * @var Dictionary\DictionaryRegistry $dicts
     */
    private $dicts = null;

    /**
     * Storage for the Filters.
     *
     * @var Filter\FilterRegistry $filters
     */
    private $filters = null;

    /**
     * Storage for the tokenizers.
     *
     * @var \Org\Heigl\Hyphenator\Tokenizer\TokenizerRegistry $tokenizers
     */
    private $tokenizers = null;

    /**
     * Set the Options
     *
     * @param \Org\Heigl\Hyphenator\Options $options The options to set
     *
     * @return \Org\Heigl\Hyphenator\Hyphenator
     */
    public function setOptions(Options $options)
    {
        $this->options = $options;
        $this->tokenizers->cleanup();
        foreach ($this->options->getTokenizers() as $tokenizer) {
            $this->addTokenizer($tokenizer);
        }

        return $this;
    }

    /**
     * Get the Options
     *
     * @return \Org\Heigl\Hyphenator\Options
     */
    public function getOptions()
    {
        if (null === $this->options) {
            $optFile = $this->getHomePath() . DIRECTORY_SEPARATOR . 'Hyphenator.properties';
            $this->setOptions(Options::factory($optFile));
        }

        return $this->options;
    }

    /**
     * Add a Dictionary to the Hyphenator
     *
     * @param \Org\Heigl\Hyphenator\Dictionary\Dictionary $dictionary The
     * Dictionary wit hyphenation-Patterns to add to this Hyphenator
     *
     * @return Org\Heigl\Hyphenator\Hyphenator
     */
    public function addDictionary($dictionary)
    {
        if (! $dictionary instanceof \Org\Heigl\Hyphenator\Dictionary\Dictionary) {
            \Org\Heigl\Hyphenator\Dictionary\Dictionary::setFileLocation($this->getHomePath() . '/files/dictionaries');
            $dictionary = \Org\Heigl\Hyphenator\Dictionary\Dictionary::factory($dictionary);
        }
        $this->dicts->add($dictionary);

        return $this;
    }

    /**
     * Add a Filter to the Hyphenator
     *
     * @param \Org\Heigl\Hyphenator\Filter\Filter|string $filter The Filter with
     * non-standard-hyphenation-patterns
     *
     * @link http://hunspell.sourceforge.net/tb87nemeth.pdf
     * @return Org\Heigl\Hyphenator\Hyphenator
     */
    public function addFilter($filter)
    {
        if (! $filter instanceof \Org\Heigl\Hyphenator\Filter\Filter) {
            $filter = '\\Org\\Heigl\\Hyphenator\\Filter\\' . ucfirst($filter) . 'Filter';
            $filter = new $filter();
        }
        $filter->setOptions($this->getOptions());
        $this->filters->add($filter);

        return $this;
    }

    /**
     * Add a tokenizer to the tokenizer-registry
     *
     * @param Tokenizer\Tokenizer|string $tokenizer The tokenizer to add
     *
     * @return Hyphenator
     */
    public function addTokenizer($tokenizer)
    {
        if (! $tokenizer instanceof \Org\Heigl\Hyphenator\Tokenizer\Tokenizer) {
            $tokenizer = '\\Org\\Heigl\Hyphenator\\Tokenizer\\' . ucfirst($tokenizer) . 'Tokenizer';
            $tokenizer = new $tokenizer();
        }
        $this->tokenizers->add($tokenizer);

        return $this;
    }

    /**
     * Get the tokenizers
     *
     * @return Tokenizer\TokenizerRegistry
     */
    public function getTokenizers()
    {
        if (0 == $this->tokenizers->count()) {
            foreach ($this->getOptions()->getTokenizers() as $tokenizer) {
                $this->addTokenizer($tokenizer);
            }
        }

        return $this->tokenizers;
    }

    /**
     * Get the dictionaries
     *
     * @return Dictionaries\DictionaryRegistry
     */
    public function getDictionaries()
    {
        if (0 == $this->dicts->count()) {
            $this->addDictionary($this->getOptions()->getDefaultLocale());
        }

        return $this->dicts;
    }

    /**
     * Get the filters
     *
     * @return Filter\FilterRegistry
     */
    public function getFilters()
    {
        if (0 == $this->filters->count()) {
            foreach ($this->getOptions()->getFilters() as $filter) {
                $this->addFilter($filter);
            }
        }

        return $this->filters;
    }

    /**
     * The constructor initializing the Object
     *
     * @return void
     */
    public function __construct()
    {
        $this->dicts      = new Dictionary\DictionaryRegistry();
        $this->filters    = new Filter\FilterRegistry();
        $this->tokenizers = new Tokenizer\TokenizerRegistry();
    }

    /**
     * This method does the actual hyphenation.
     *
     * The given <var>$string</var> is splitted into chunks (i.e. Words) at
     * every blank.
     *
     * After that every chunk is hyphenated and the array of chunks is merged
     * into a single string using blanks again.
     *
     * This method does not take into account other word-delimiters than blanks
     * (eg. returns or tabstops) and it will fail with texts containing markup
     * in any way.
     *
     * @param string $string The string to hyphenate
     *
     * @return string The hyphenated string
     */
    public function hyphenate($string)
    {
        $tokens = $this->tokenizers->tokenize($string);
        $tokens = $this->getHyphenationPattern($tokens);
        $tokens = $this->filter($tokens);
        if (1 === count($tokens) && 1 === $this->getFilters()->count()) {
            $tokens->rewind();
            return $tokens->current()->getHyphenatedContent();
        }

        return $this->getFilters()->concatenate($tokens);
    }

    /**
     * Get the hyphenation pattern for the contained tokens
     *
     * Use the dictionaties and options of the given Hyphenator-Object
     *
     * @param Tokenizer\TokenRegistry $registry The Hyphenator object containing the
     * dictionaries and options
     *
     * @return Tokenizer\TokenRegistry
     */
    public function getHyphenationPattern(Tokenizer\TokenRegistry $registry)
    {
        $minWordLength = $this->getOptions()->getMinWordLength();
        foreach ($registry as $token) {
            if (! $token instanceof \Org\Heigl\Hyphenator\Tokenizer\WordToken) {
                continue;
            }
            if ($minWordLength > $token->length()) {
                continue;
            }
            $this->getPatternForToken($token);
        }

        return $registry;
    }

    /**
     * Filter the content of the given TokenRegistry
     *
     * @param \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $registry The tokens
     * to filter
     *
     * @return \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry
     */
    public function filter(Tokenizer\TokenRegistry $registry)
    {
        return $this->getFilters()->filter($registry);
    }

    /**
     * Hyphenate a Token-Object
     *
     * @param Tokenizer\Token $token The token to hyphenate
     *
     * @return Tokenizer\Token
     */
    public function getPatternForToken(Tokenizer\WordToken $token)
    {
        foreach ($this->getDictionaries() as $dictionary) {
            $token->addPattern($dictionary->getPatternsForWord($token->get()));
        }

        return $token;
    }

    /**
     * Set the default home-Path
     *
     * @param string $homePath The default Hyphenator Home-path.
     *
     * @throws Exception\PathNotFoundException
     * @throws Exception\PathNotDirException
     * @return void
     */
    public static function setDefaultHomePath($homePath)
    {
        if (! file_exists($homePath)) {
            throw new Exception\PathNotFoundException($homePath . ' does not exist');
        }
        if (! is_Dir($homePath)) {
            throw new Exception\PathNotDirException($homePath . ' is not a directory');
        }

        self::$defaultHomePath = realpath($homePath);
    }

    /**
     * Get the default Home-Path
     *
     * @return string
     */
    public static function getDefaultHomePath()
    {
        if (is_Dir(self::$defaultHomePath)) {
            return self::$defaultHomePath;
        }
        if (defined('HYPHENATOR_HOME') && is_Dir(HYPHENATOR_HOME)) {
            return realpath(HYPHENATOR_HOME);
        }
        if ($home = getenv('HYPHENATOR_HOME')) {
            if (is_Dir($home)) {
                return $home;
            }
        }

        return __DIR__ . '/share';
    }

    /**
     * Set the instance-home-Path
     *
     * @param string $homePath This instances home-path.
     *
     * @throws Exception\PathNotFoundException
     * @throws Exception\PathNotDirException
     * @return \Org\Heigl\Hyphenator\Hyphenator
     */
    public function setHomePath($homePath)
    {
        if (! file_exists($homePath)) {
            throw new Exception\PathNotFoundException($homePath . ' does not exist');
        }
        if (! is_Dir($homePath)) {
            throw new Exception\PathNotDirException($homePath . ' is not a directory');
        }

        $this->homePath = realpath($homePath);

        return $this;
    }

    /**
     * Get this instances Home-Path.
     *
     * If no homePath is set for this instance this method will return the
     * result of the \Org\HEigl\Hyphenator\Hyphenator::getdefaultzHomePath()
     * Method
     *
     * @return string
     */
    public function getHomePath()
    {
        if (! is_dir($this->homePath)) {
            return self::getDefaultHomePath();
        }

        return $this->homePath;
    }

    /**
     * Create a new Hyphenator-Object for a certain locale
     *
     * To determine the storage of the dictionaries we either use the set
     * default configuration-file or we take the provided file and set the
     * home-path from the information within that file.
     *
     * @param string $path   The path to the configuration-file to use
     * @param string $locale The locale to be used
     *
     * @return Hyphenator
     */
    public static function factory($path = null, $locale = null)
    {
        $hyphenator = new Hyphenator();
        if (null !== $path && file_Exists($path)) {
            $hyphenator->setHomePath($path);
        }
        if (null !== $locale) {
            $hyphenator->getOptions()->setDefaultLocale($locale);
        }

        return $hyphenator;
    }

    /**
     * autoload classes.
     *
     * @param string $className the name of the class to load
     *
     * @return void
     */
    public static function __autoload($className)
    {
        if (0 !== strpos($className, 'Org\\Heigl\\Hyphenator')) {
            return false;
        }
        $className = substr($className, strlen('Org\\Heigl\\Hyphenator\\'));
        $file = str_replace('\\', '/', $className) . '.php';
        $fileName = __DIR__ . DIRECTORY_SEPARATOR . $file;
        if (! file_exists(realpath($fileName))) {
            return false;
        }
        if (! @include_once $fileName) {
            return false;
        }

        return true;
    }

    /**
     * Register this packages autoloader with the autoload-stack
     *
     * @return void
     */
    public static function registerAutoload()
    {
        return spl_autoload_register(array('\Org\Heigl\Hyphenator\Hyphenator', '__autoload'));
    }
}

/*
 * Check for requirements and if these are not met throw an exception
 */
if (! extension_loaded('mbstring')) {
    throw new \Exception('\Org\Heigl\Hyphenator requires the mbstring-extension to be loaded');
}
mb_internal_encoding('UTF-8');


<?php
/**
 * Copyright (c) 2008-2011 Andreas Heigl<andreas@heigl.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category   Hyphenation
 * @package    Org\Heigl\Hyphenator
 * @subpackage Tokenizer
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      04.11.2011
 */

namespace Org\Heigl\Hyphenator\Tokenizer;

/**
 * This class provides a registry for toring Tokens
 *
 * @category   Hyphenation
 * @package    Org\Heigl\Hyphenator
 * @subpackage Tokenizer
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      04.11.2011
 */
class TokenRegistry implements \Iterator, \Countable
{
    /**
     * Storage for the Tokens.
     *
     * @var \Org\Heigl\Hyphenator\Tokenizer\Token[] $_registry
     */
    private $registry = array();

    /**
     * Add an item to the registry
     *
     * @param \Org\Heigl\Hyphenator\Tokenizer\Token $token The token to add
     *
     * @return \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry
     */
    public function add(Token $token)
    {
        $this->registry[] = $token;

        return $this;
    }

    /**
     * Replace a given token with an array of new tokens
     *
     * This will be used when a tokenizer has to replace a token that has been
     * divided into multiple tokens.
     *
     * @param Token   $oldToken  The token to be replaced
     * @param Token[] $newTokens The array of tokens replacing the old one
     *
     * @return TokenRegistry
     */
    public function replace(Token $oldToken, array $newTokens)
    {
        // Get the current key of the element.
        $key = array_search($oldToken, $this->registry, true);
        if (false === $key) {
            return $this;
        }
        $replacement = array();

        // Check for any non-token-elements and remove them.
        foreach ($newTokens as $token) {
            if (! $token instanceof Token) {
                continue;
            }
            $replacement[] = $token;
        }

        // Replace the old element with the newly created array
        array_splice($this->registry, $key, 1, $replacement);

        return $this;
    }

    /**
     * Get a Token entry by it's key
     *
     * @param mixed $key The key to get the token for
     *
     * @return \Org\Heigl\Hyphenator\Tokenizer\Token
     */
    public function getTokenWithKey($key)
    {
        if (array_key_exists($key, $this->registry)) {
            return $this->registry[$key];
        }

        return null;
    }

    /**
     * Implementation of \Iterator
     *
     * @see \Iterator::rewind()
     *
     * @return void
     */
    public function rewind()
    {
        reset($this->registry);
    }

    /**
     * Get the current object
     *
     * @see \Iterator::current()
     *
     * @return \Org\Heigl\Hyphenator\Tokenizer\Token
     */
    public function current()
    {
        return current($this->registry);
    }

    /**
     * Get the current key
     *
     * @see \Iterator::key()
     *
     * @return mixed
     */
    public function key()
    {
        return key($this->registry);
    }

    /**
     * Get the number of items in the registry
     *
     * @see \Countable::count()
     *
     * @return int
     */
    public function count()
    {
        return count($this->registry);
    }

    /**
     * Push the internal pointer forward one step
     *
     * @see \Iterator::next()
     *
     * @return void
     */
    public function next()
    {
        next($this->registry);
    }

    /**
     * Check whether the current pointer is in a valid place
     *
     * @see \Iterator::valid()
     *
     * @return boolean
     */
    public function valid()
    {
        if (false === current($this->registry)) {
            return false;
        }

        return true;
    }
}


<?php
/**
 * Copyright (c) 2008-2011 Andreas Heigl<andreas@heigl.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category   Hyphenation
 * @package    Org_Heigl_Hyphenator
 * @subpackage Filter
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      02.11.2011
 * @todo       Implement!
 */

namespace Org\Heigl\Hyphenator\Filter;

use \Org\Heigl\Hyphenator\Tokenizer as t;

/**
 * This class provides a filter for non-standard hyphenation-patterns
 *
 * @category   Hyphenation
 * @package    Org_Heigl_Hyphenator
 * @subpackage Filter
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      02.11.2011s
 */
class NonStandardFilter extends Filter
{

    /**
     * Implements interface Filter
     *
     * @param \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $tokens The registry
     * to act upon
     *
     * @see Org\Heigl\Hyphenator\Filter\Filter::run()
     * @return \Org\Heigl\Hyphenator\Tokenizer\Token
     */
    public function run(t\TokenRegistry $tokens)
    {
        foreach ($tokens as $token) {
            if (! $token instanceof t\WordToken) {
                continue;
            }
            $string = $token->getFilteredContent();
            $pattern = $token->getMergedPattern($this->options->getQuality());
            $length  = $token->length();
            $result = array();
            for ($i = 1; $i <= $length; $i++) {
                $currPattern = mb_substr($pattern, $i, 1);
                if ($i < $this->options->getLeftMin()) {
                    continue;
                }
                if ($i > $length - $this->options->getRightMin()) {
                    continue;
                }
                if (0 == $currPattern) {
                    continue;
                }
                if (0 === (int) $currPattern % 2) {
                    continue;
                }
                $start = mb_substr($string, 0, $i);
                $end   = mb_substr($string, $i);
                $result[] = $start . $this->options->getHyphen() . $end;
            }
            $token->setHyphenatedContent($result);
        }

        return $tokens;
    }

    /**
     * Implements interface Filter
     *
     * @param \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $tokens The registry
     * to act upon
     *
     * @see Org\Heigl\Hyphenator\Filter\Filter::run()
     * @return \Org\Heigl\Hyphenator\Tokenizer\Token
     */
    protected function doConcatenate(t\TokenRegistry $tokens)
    {
        $string = '';
        foreach ($tokens as $token) {
            $string .= $token->getFilteredContent();
        }

        return $string;
    }
}


<?php
/**
 * Copyright (c) 2008-2011 Andreas Heigl<andreas@heigl.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category   Hyphenation
 * @package    Org_Heigl_Hyphenator
 * @subpackage Tokenizer
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      04.11.2011
 */

namespace Org\Heigl\Hyphenator\Tokenizer;

/**
 * This Class describes a Token representing a word
 *
 * @category   Hyphenation
 * @package    Org_Heigl_Hyphenator
 * @subpackage Tokenizer
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      04.11.2011
 */
class WordToken extends Token
{
    /**
     * The hyphenation patterns for this token.
     *
     * @var \array $_pattern
     */
    protected $pattern = array();

    /**
     * Add a substring=>pattern array to the already existing ones
     *
     * @param \array $pattern The new patterns to add
     *
     * @return Token
     */
    public function addPattern(array $pattern)
    {
        $this->pattern = array_merge($this->pattern, $pattern);

        return $this;
    }

    /**
     * Get the content for the hyphenator.
     *
     * THis will prepend and append a dot to the content for better hyphenation
     *
     * @return \string
     */
    public function getHyphenateContent()
    {
        return '.' . $this->content . '.';
    }

    /**
     * Merge the given Hyphenation patterns to one pattern for the given token
     *
     * This is done using the given quality value.
     *
     * @param \int $quality The hyphenation quality to use
     *
     * @return Token
     */
    public function getMergedPattern($quality = \Org\Heigl\Hyphenator\Hyphenator::QUALITY_HIGHEST)
    {
        $content = $this->getHyphenateContent();
        $endPattern = str_repeat('0', mb_strlen($content)+1);
        foreach ($this->pattern as $string => $pattern) {
            $strStart = -1;
            while (false !== $strStart = @mb_strpos($content, $string, $strStart + 1)) {
                $strLen   = mb_strlen($string);
                for ($i=0; $i <= $strLen; $i++) {
                    $start = $i+$strStart;
                    $currentQuality = substr($endPattern, $start, 1);
                    $patternQuality = substr($pattern, $i, 1);
                    if ($currentQuality >= $patternQuality) {
                        continue;
                    }
                    if ($quality < $patternQuality) {
                        continue;
                    }
                    $endPattern = substr($endPattern, 0, $start) . $patternQuality . substr($endPattern, $start+1);
                }
            }
        }

        return substr($endPattern, 1, strlen($endPattern)-2);
    }
}


<?php
/**
 * Copyright (c) 2008-2011 Andreas Heigl<andreas@heigl.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category   Hyphenation
 * @package    Org\Heigl\Hyphenator
 * @subpackage Filter
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      02.11.2011
 * @todo       Implement!
 */

namespace Org\Heigl\Hyphenator\Filter;

/**
 * This class provides a registry for storing multiple filters
 *
 * @category   Hyphenation
 * @package    Org\Heigl\Hyphenator
 * @subpackage Filter
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      02.11.2011s
 */
class FilterRegistry implements \Iterator, \Countable
{
    /**
     * Storage for the Tokenizers.
     *
     * @var \Org\Heigl\Hyphenator\Filter\Filter[] $registry
     */
    protected $registry = array();

    /**
     * Add an item to the registry
     *
     * @param \Org\Heigl\Hyphenator\Filter\Filter $filter The Filter
     * to be added
     *
     * @return \Org\Heigl\Hyphenator\Filter\FilterRegistry
     */
    public function add(Filter $filter)
    {
        if (! in_array($filter, $this->registry)) {
            $this->registry[] = $filter;
        }

        return $this;
    }

    /**
     * Get a Filters entry by it's key
     *
     * @param mixed $key The key to get the Filter for.
     *
     * @return \Org\Heigl\Hyphenator\Filter\Filter
     */
    public function getFilterWithKey($key)
    {
        if (array_key_exists($key, $this->registry)) {
            return $this->registry[$key];
        }

        return null;
    }

    /**
     * Cleanup the registry
     *
     * @return Filter\FilterRegistry
     */
    public function cleanup()
    {
        $this->registry = array();

        return $this;
    }

    /**
     * Pass the given string through the given Filter
     *
     * @param \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $tokens The
     * Registry to filter
     *
     * @return \Org\Heigl\Hyphenator\Tokenizer\TokenizerRegistry
     */
    public function filter(\Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $tokens)
    {
        foreach ($this as $filter) {
            $tokens = $filter->run($tokens);
        }

        return $tokens;
    }

    /**
     * Concatenate the content of the given TokenRegistry
     *
     * @param \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $tokens The
     * Registry to filter
     *
     * @return mixed
     */
    public function concatenate(\Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $tokens)
    {
        foreach ($this as $filter) {
            $tokens = $filter->concatenate($tokens);
        }

        return $tokens;
    }
    /**
     * Implementation of \Iterator
     *
     * @see \Iterator::rewind()
     *
     * @return void
     */
    public function rewind()
    {
        reset($this->registry);
    }

    /**
     * Get the current object
     *
     * @see \Iterator::current()
     *
     * @return \Org\Heigl\Hyphenator\Filter\Filter
     */
    public function current()
    {
        return current($this->registry);
    }

    /**
     * Get the current key
     *
     * @see \Iterator::key()
     *
     * @return mixed
     */
    public function key()
    {
        return key($this->registry);
    }

    /**
     * Get the number of items in the registry
     *
     * @see \Countable::count()
     *
     * @return int
     */
    public function count()
    {
        return count($this->registry);
    }

    /**
     * Push the internal pointer forward one step
     *
     * @see \Iterator::next()
     *
     * @return void
     */
    public function next()
    {
        next($this->registry);
    }

    /**
     * Check whether the current pointer is in a valid place
     *
     * @see \Iterator::valid()
     *
     * @return boolean
     */
    public function valid()
    {
        if (false === current($this->registry)) {
            return false;
        }

        return true;
    }
}


<?php
/**
 * Copyright (c) 2008-2011 Andreas Heigl<andreas@heigl.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category   Hyphenation
 * @package    Org_Heigl_Hyphenator
 * @subpackage Tokenizer
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      11.11.2011
 */

namespace Org\Heigl\Hyphenator\Tokenizer;

/**
 * Use Punktuation to split any input into tokens
 *
 * @category   Hyphenation
 * @package    Org_Heigl_Hyphenator
 * @subpackage Tokenizer
 * @author     Andreas Heigl <andreas@heigl.org>
 * @copyright  2008-2011 Andreas Heigl<andreas@heigl.org>
 * @license    http://www.opensource.org/licenses/mit-license.php MIT-License
 * @version    2.0.1
 * @link       http://github.com/heiglandreas/Hyphenator
 * @since      04.11.2011
 */
class PunctuationTokenizer implements Tokenizer
{

    /**
     * The tokens to be handled by this tokenizer as an array.
     *
     * @var string $tokens
     */
    protected $tokens = array(
        '.',
        '?',
        '!',
        ':',
        ';',
        ',',
        '#',
        '"',
        '$',
        '§',
        '%',
        '&',
        '/',
        '(',
        ')',
        '=',
        '[',
        ']',
        '|',
        '{',
        '}',
        '\\',
        '<',
        '>',
        '«',
        '»',
        '“',
        '”',
        '^',
        '°',
        '≤',
        '≥',
        '¥',
        '©',
        '€',
        "'",
        '-',
        '_',
    );

    /**
     * Split the given input into tokens using punktuation marks as splitter
     *
     * The input can be a string or a tokenRegistry. If the input is a
     * TokenRegistry, each item will be tokenized.
     *
     * @param string|\Org\Heigl\Hyphenator\Tokenizer\TokenRegistry $input The
     * input to be tokenized
     *
     * @return \Org\Heigl\Hyphenator\Tokenizer\TokenRegistry
     */
    public function run($input)
    {
        if ($input instanceof TokenRegistry) {
            // Tokenize a TokenRegistry
            $f = clone($input);
            foreach ($input as $token) {
                if (! $token instanceof WordToken) {
                    continue;
                }
                $newTokens = $this->tokenize($token->get());
                if ($newTokens == array($token)) {
                    continue;
                }
                $f->replace($token, $newTokens);
            }

            return $f ;
        }

        // Tokenize a simple string.
        $array =  $this->tokenize($input);
        $registry = new TokenRegistry();
        foreach ($array as $item) {
            $registry->add($item);
        }

        return $registry;
    }

    /**
     * Split the given string into tokens using whitespace.
     *
     * Each whitespace is placed in a WhitespaceToken and everything else is
     * placed in a WordToken-Object
     *
     * @param \string $input The String to tokenize
     *
     * @return Token
     */
    private function tokenize($input)
    {
        $tokens = array();
        $signs = '\\' . implode('\\', $this->tokens);
        $splits = preg_split('/([' . $signs . ']+)/u', $input, -1, PREG_SPLIT_DELIM_CAPTURE);
        foreach ($splits as $split) {
            if ('' == $split) {
                continue;
            }
            if (in_array(mb_substr($split, 0, 1), $this->tokens)) {
                $tokens[] = new NonWordToken($split);
                continue;
            }
            $tokens[] = new WordToken($split);
        }

        return $tokens;
    }
}


<?php

namespace Autodesk\Auth\OAuth2;

use Autodesk\Auth\ScopeValidator;
use Autodesk\Auth\TokenFetcher;
use Autodesk\Auth\Configuration;
use Autodesk\Core\Exception\RuntimeException;

class ThreeLeggedAuth extends AbstractOAuth2
{
    /**
     * @var string
     */
    private $refreshToken;

    /**
     * @var Configuration
     */
    private $configuration;

    /**
     * OAuth2ThreeLegged constructor.
     * @param Configuration|null $configuration
     * @param TokenFetcher|null $tokenFetcher
     * @param ScopeValidator|null $scopeValidator
     */
    public function __construct(
        Configuration $configuration = null,
        TokenFetcher $tokenFetcher = null,
        ScopeValidator $scopeValidator = null
    ) {
        // @codeCoverageIgnoreStart
        if ($configuration === null) {
            $configuration = Configuration::getDefaultConfiguration();
        }
        // @codeCoverageIgnoreEnd

        $this->configuration = $configuration;

        parent::__construct($tokenFetcher, $scopeValidator);
    }

    /**
     * @return string
     */
    public function createAuthUrl()
    {
        $host = $this->configuration->getHost();
        $parameters = http_build_query([
            'response_type' => 'code',
            'client_id'     => $this->configuration->getClientId(),
            'redirect_uri'  => $this->configuration->getRedirectUrl(),
            'scope'         => implode(' ', $this->getScopes()),
        ]);

        return "{$host}/authentication/v1/authorize?{$parameters}";
    }

    /**
     * Returns application token
     * @param $authorizationCode
     * @throws RuntimeException
     */
    public function fetchToken($authorizationCode)
    {
        $additionalParams = [
            'code' => $authorizationCode,
            'redirect_uri' => $this->configuration->getRedirectUrl(),
        ];

        $response = parent::fetchAccessToken('authentication/v1/gettoken', 'authorization_code', $additionalParams);

        $this->saveRefreshToken($response);
    }

    /**
     * @param $refreshToken
     * @throws RuntimeException
     */
    public function refreshToken($refreshToken)
    {
        $additionalParams = [
            'refresh_token' => $refreshToken,
        ];

        $response = parent::fetchAccessToken('authentication/v1/refreshtoken', 'refresh_token', $additionalParams);

        $this->saveRefreshToken($response);
    }

    /**
     * @return string
     */
    public function getRefreshToken()
    {
        return $this->refreshToken;
    }

    /**
     * @param $response
     * @throws RuntimeException
     */
    private function saveRefreshToken($response)
    {
        if ( ! array_key_exists('refresh_token', $response)) {
            throw new RuntimeException('Refresh token was not found in the response');
        }

        $this->refreshToken = $response['refresh_token'];
    }
}


<?php

namespace SilverShop\Tasks;

use SilverShop\Model\Modifiers\Shipping\Base;
use SilverShop\Model\Order;
use SilverStripe\Dev\MigrationTask;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;

/**
 * TODO: Implement for 2.x to 3.x
 * Updates database to work with latest version of the code.
 */
class ShopMigrationTask extends MigrationTask
{
    /**
     * Choose how many orders get processed at a time.
     */
    public static $batch_size = 250;

    protected $title = 'Migrate Shop';

    protected $description = 'Where dev/build is not enough, this task updates database to work with latest version of shop module.
You may want to run the CartCleanupTask before migrating if you want to discard past carts.';

    /**
     * Migrate upwards
     */
    public function up()
    {
        $this->migrateOrders();
        $this->migrateProductPrice();
        $this->migrateProductVariationsAttribues();
        $this->migrateProductImages();
        //TODO: migrate CheckoutPage->TermsPageID to ShopConfig
    }

    /**
     * batch process orders
     */
    public function migrateOrders()
    {
        $start = $count = 0;
        $batch = Order::get()->sort('Created', 'ASC')->limit($start, self::$batch_size);
        while ($batch->exists()) {
            foreach ($batch as $order) {
                $this->migrate($order);
                echo '. ';
                $count++;
            }
            $start += self::$batch_size;
            $batch = $batch->limit($start, self::$batch_size);
        };
        echo "$count orders updated.\n<br/>";
    }

    /**
     * Perform migration scripts on a single order.
     */
    public function migrate(Order $order)
    {
        //TODO: set a from / to version to preform a migration with
        $this->migrateStatuses($order);
        $this->migrateMemberFields($order);
        $this->migrateShippingValues($order);
        $this->migrateOrderCalculation($order);
        $order->write();
    }

    public function migrateProductPrice()
    {
        $db = DB::get_conn();
        //if BasePrice has no values, but Price does, then copy from Price
        if ($db->hasTable('Product') && !DataObject::get_one('Product', '"BasePrice" > 0')) {
            //TODO: warn against lost data
            DB::query('UPDATE "Product" SET "BasePrice" = "Price";');
            DB::query('UPDATE "Product_Live" SET "BasePrice" = "Price";');
            DB::query('UPDATE "Product_versions" SET "BasePrice" = "Price";');
            //TODO: rename, if possible without breaking migraton task next time it runs
            //$db->renameField("Product","Price","Price_obselete");
            //$db->renameField("Product_Live","Price","Price_obselete");
        }
    }

    /**
     * Rename all Product_Image ClassNames to Image
     * Added in v1.0
     */
    public function migrateProductImages()
    {
        DB::query('UPDATE "File" SET "ClassName"=\'Image\' WHERE "ClassName" = \'Product_Image\'');
    }

    /**
     * Customer and shipping details have been added to Order,
     * so that memberless (guest) orders can be placed.
     */
    public function migrateMemberFields($order)
    {
        if ($member = $order->Member()) {
            $fieldstocopy = array(
                'FirstName',
                'Surname',
                'Email',
                'Address',
                'AddressLine2',
                'City',
                'Country',
                'HomePhone',
                'MobilePhone',
                'Notes',
            );
            foreach ($fieldstocopy as $field) {
                if (!$order->$field) {
                    $order->$field = $member->$field;
                }
            }
        }
    }

    /**
     * Migrate old statuses
     */
    public function migrateStatuses($order)
    {
        switch ($order->Status) {
            case "Cancelled": //Pre version 0.5
                $order->Status = 'AdminCancelled';
                break;
            case "":
                $order->Status = 'Cart';
                break;
        }
    }

    /**
     * Convert shipping and tax columns into modifiers
     *
     * Applies to pre 0.6 sites
     */
    public function migrateShippingValues($order)
    {
        //TODO: see if this actually works..it probably needs to be writeen to a SQL query
        if ($order->hasShippingCost && abs($order->Shipping)) {
            $modifier1 = Base::create();
            $modifier1->Amount = $order->Shipping < 0 ? abs($order->Shipping) : $order->Shipping;
            $modifier1->Type = 'Chargable';
            $modifier1->OrderID = $order->ID;
            $modifier1->ShippingChargeType = 'Default';
            $modifier1->write();
            $order->hasShippingCost = null;
            $order->Shipping = null;
        }
        if ($order->AddedTax) {
            $modifier2 = \SilverShop\Model\Modifiers\Tax\Base::create();
            $modifier2->Amount = $order->AddedTax < 0 ? abs($order->AddedTax) : $order->AddedTax;
            $modifier2->Type = 'Chargable';
            $modifier2->OrderID = $order->ID;
            //$modifier2->Name = 'Undefined After Ecommerce Upgrade';
            $modifier2->TaxType = 'Exclusive';
            $modifier2->write();
            $order->AddedTax = null;
        }
    }

    /**
     * Performs calculation function on un-calculated orders.
     */
    public function migrateOrderCalculation($order)
    {
        if (!is_numeric($order->Total) || $order->Total <= 0) {
            $order->calculate();
            $order->write();
        }
    }

    public function migrateProductVariationsAttribues()
    {
        $db = DB::get_conn();
        //TODO: delete Product_VariationAttribute, if it's empty
        if ($db->hasTable(
            'Product_VariationAttributes'
        )
        ) { //TODO: check if Product_VariationAttributeTypes table is empty
            DB::query('DROP TABLE "Product_VariationAttributeTypes"');
            $db->renameTable('Product_VariationAttributes', 'Product_VariationAttributeTypes');
        }
    }

    public function migrateShippingTaxValues()
    {
        //rename obselete columns
        //DB::query("ALTER TABLE \"Order\" CHANGE COLUMN \"hasShippingCost\" \"_obsolete_hasShippingCost\" tinyint(1)");
        //DB::query("ALTER TABLE \"Order\" CHANGE COLUMN \"Shipping\" \"_obsolete_Shipping\" decimal(9,2)");
        //DB::query("ALTER TABLE \"Order\" CHANGE COLUMN \"AddedTax\" \"_obsolete_AddedTax\" decimal(9,2)");
    }
}


<?php

namespace SilverShop\Tasks;

use SilverShop\Page\Product;
use SilverStripe\Dev\BuildTask;
use SilverStripe\ORM\DB;

class CalculateProductPopularity extends BuildTask
{
    protected $title = 'Calculate Product Sales Popularity';

    protected $description = 'Count up total sales quantites for each product';

    private static $number_sold_calculation_type = 'SUM'; //SUM or COUNT

    public function run($request)
    {
        if ($request->getVar('via') == 'php') {
            $this->viaphp();
        } else {
            $this->viasql();
        }
        echo 'product sales counts updated';
    }

    /**
     * Update both live and stage tables, based on the algorithm:
     *    product popularity = sum(1/order_age) * sum(item_quantity)
     */
    public function viasql()
    {
        foreach (array('_Live', '') as $stage) {
            $sql = <<<SQL
UPDATE "SilverShop_Product$stage" SET "Popularity" = (
  SELECT
    SUM(1 / (DATEDIFF(NOW(),"SilverShop_Order"."Paid")+1)) * SUM("SilverShop_OrderItem"."Quantity")
    #  / DATEDIFF("SiteTree$stage"."Created",NOW())
    AS Popularity
  FROM "SiteTree$stage"
    INNER JOIN "SilverShop_Product_OrderItem" ON "SiteTree$stage"."ID" = "SilverShop_Product_OrderItem"."ProductID"
    INNER JOIN "SilverShop_OrderItem" ON "SilverShop_OrderItem"."ID" = "SilverShop_Product_OrderItem"."ID"
    INNER JOIN "SilverShop_OrderAttribute" ON "SilverShop_OrderItem"."ID" = "SilverShop_OrderAttribute"."ID"
    INNER JOIN "SilverShop_Order" ON "SilverShop_OrderAttribute"."OrderID" = "SilverShop_Order"."ID"
  WHERE "SiteTree$stage"."ID" = "SilverShop_Product$stage"."ID"
    AND "SilverShop_Order"."Paid" IS NOT NULL
  GROUP BY "SilverShop_Product$stage"."ID"
);
SQL;
            DB::query($sql);
        }
    }

    //legacy function  for working out popularity
    public function viaphp()
    {
        $ps = singleton(Product::class);
        $q = $ps->buildSQL('"SilverShop_Product"."AllowPurchase" = 1');
        $select = $q->select;
        $select['NewPopularity'] =
            self::config()->number_sold_calculation_type . '("SilverShop_OrderItem"."Quantity") AS "NewPopularity"';
        $q->select($select);
        $q->groupby('"Product"."ID"');
        $q->orderby('"NewPopularity" DESC');
        $q->leftJoin('SilverShop_Product_OrderItem', '"SilverShop_Product"."ID" = "SilverShop_Product_OrderItem"."ProductID"');
        $q->leftJoin('SilverShop_OrderItem', '"SilverShop_Product_OrderItem"."ID" = "SilverShop_OrderItem"."ID"');
        $records = $q->execute();
        $productssold = $ps->buildDataObjectSet($records, "DataObjectSet", $q, Product::class);
        //TODO: this could be done faster with an UPDATE query (SQLQuery doesn't support this yet @11/06/2010)
        foreach ($productssold as $product) {
            if ($product->NewPopularity != $product->Popularity) {
                $product->Popularity = $product->NewPopularity;
                $product->writeToStage('Stage');
                $product->publishSingle();
            }
        }
    }
}


<?php

namespace SilverShop\Page;

use Page;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\ORM\DB;

//use PageController;

/**
 * CheckoutPage is a CMS page-type that shows the order
 * details to the customer for their current shopping
 * cart on the site.
 *
 * @see CheckoutPage_Controller->Order()
 *
 * @package shop
 */
class CheckoutPage extends Page
{
    private static $db   = array(
        'PurchaseComplete' => 'HTMLText',
    );

    private static $icon = 'silvershop/core: client/dist/images/icons/money.gif';

    private static $table_name = 'SilverShop_CheckoutPage';

    /**
     * @config
     * @var array
     */
    private static $steps;

    /**
     * Returns the link to the checkout page on this site
     *
     * @param boolean $urlSegment If set to TRUE, only returns the URLSegment field
     *
     * @return string Link to checkout page
     */
    public static function find_link($urlSegment = false, $action = null, $id = null)
    {
        $base = CheckoutPageController::config()->url_segment;
        if ($page = self::get()->first()) {
            $base = $page->Link();
        }
        return Controller::join_links($base, $action, $id);
    }

    public function getCMSFields()
    {
        $this->beforeUpdateCMSFields(
            function (FieldList $fields) {
                $fields->addFieldsToTab(
                    'Root.Main',
                    array(
                    HtmlEditorField::create(
                        'PurchaseComplete',
                        $this->fieldLabel('PurchaseComplete'),
                        4
                    )
                        ->setDescription(
                            _t(
                                __CLASS__ . '.PurchaseCompleteDescription',
                                "This message is included in reciept email, after the customer submits the checkout"
                            )
                        ),
                    ),
                    'Metadata'
                );
            }
        );

        return parent::getCMSFields();
    }

    /**
     * This module always requires a page model.
     */
    public function requireDefaultRecords()
    {
        parent::requireDefaultRecords();
        if (!self::get()->exists() && $this->config()->create_default_pages) {
            $page = self::create()->update(
                [
                    'Title'       => 'Checkout',
                    'URLSegment'  => CheckoutPageController::config()->url_segment,
                    'ShowInMenus' => 0,
                ]
            );
            $page->write();
            $page->publishSingle();
            $page->flushCache();
            DB::alteration_message('Checkout page created', 'created');
        }
    }
}


<?php

namespace SilverShop\Checkout;

use ErrorException;
use Exception;
use SilverShop\Cart\ShoppingCart;
use SilverShop\Extension\OrderManipulationExtension;
use SilverShop\Extension\ShopConfigExtension;
use SilverShop\Model\Order;
use SilverShop\ShopTools;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config_ForClass;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Omnipay\GatewayInfo;
use SilverStripe\Omnipay\Model\Payment;
use SilverStripe\Omnipay\Service\ServiceFactory;
use SilverStripe\Omnipay\Service\ServiceResponse;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;

/**
 * Handles tasks to be performed on orders, particularly placing and processing/fulfilment.
 * Placing, Emailing Reciepts, Status Updates, Printing, Payments - things you do with a completed order.
 *
 * @package shop
 */
class OrderProcessor
{
    use Injectable;
    use Configurable;

    /**
     * @var Order
     */
    protected $order;

    /**
     * @var OrderEmailNotifier
     */
    protected $notifier;

    /**
     * @var string
     */
    protected $error;

    /**
     * Assign the order to a local variable
     *
     * @param Order $order
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
        $this->notifier = OrderEmailNotifier::create($order);
    }

    /**
     * URL to display success message to the user.
     * Happens after any potential offsite gateway redirects.
     *
     * @return String Relative URL
     */
    public function getReturnUrl()
    {
        return $this->order->Link();
    }

    /**
     * Create a payment model, and provide link to redirect to external gateway,
     * or redirect to order link.
     *
     * @param string $gateway the gateway to use
     * @param array $gatewaydata the data that should be passed to the gateway
     * @param string $successUrl (optional) return URL for successful payments.
     *                            If left blank, the default return URL will be
     *                            used @see getReturnUrl
     * @param string $cancelUrl (optional) return URL for cancelled/failed payments
     *
     * @return ServiceResponse|null
     * @throws \SilverStripe\Omnipay\Exception\InvalidConfigurationException
     */
    public function makePayment($gateway, $gatewaydata = array(), $successUrl = null, $cancelUrl = null)
    {
        //create payment
        $payment = $this->createPayment($gateway);
        if (!$payment) {
            //errors have been stored.
            return null;
        }

        $payment->setSuccessUrl($successUrl ? $successUrl : $this->getReturnUrl());

        // Explicitly set the cancel URL
        if ($cancelUrl) {
            $payment->setFailureUrl($cancelUrl);
        }

        // Create a payment service, by using the Service Factory. This will automatically choose an
        // AuthorizeService or PurchaseService, depending on Gateway configuration.
        // Set the user-facing success URL for redirects
        /**
         * @var ServiceFactory $factory
         */
        $factory = ServiceFactory::create();
        $service = $factory->getService($payment, ServiceFactory::INTENT_PAYMENT);

        // Initiate payment, get the result back
        try {
            $serviceResponse = $service->initiate($this->getGatewayData($gatewaydata));
        } catch (\SilverStripe\Omnipay\Exception\Exception $ex) {
            // error out when an exception occurs
            $this->error($ex->getMessage());
            return null;
        }

        // Check if the service response itself contains an error
        if ($serviceResponse->isError()) {
            if ($opResponse = $serviceResponse->getOmnipayResponse()) {
                $this->error($opResponse->getMessage());
            } else {
                $this->error('An unspecified payment error occurred. Please check the payment messages.');
            }
        }

        // For an OFFSITE payment, serviceResponse will now contain a redirect
        // For an ONSITE payment, ShopPayment::onCaptured will have been called, which will have called completePayment

        return $serviceResponse;
    }

    /**
     * Map shop data to omnipay fields
     *
     * @param array $customData Usually user submitted data.
     *
     * @return array
     */
    protected function getGatewayData($customData)
    {
        $shipping = $this->order->getShippingAddress();
        $billing = $this->order->getBillingAddress();

        $numPayments = Payment::get()
            ->filter(array('OrderID' => $this->order->ID))
            ->count() - 1;

        $transactionId = $this->order->Reference . ($numPayments > 0 ? "-$numPayments" : '');

        return array_merge(
            $customData,
            array(
                'transactionId'    => $transactionId,
                'firstName'        => $this->order->FirstName,
                'lastName'         => $this->order->Surname,
                'email'            => $this->order->Email,
                'company'          => $this->order->Company,
                'billingAddress1'  => $billing->Address,
                'billingAddress2'  => $billing->AddressLine2,
                'billingCity'      => $billing->City,
                'billingPostcode'  => $billing->PostalCode,
                'billingState'     => $billing->State,
                'billingCountry'   => $billing->Country,
                'billingPhone'     => $billing->Phone,
                'shippingAddress1' => $shipping->Address,
                'shippingAddress2' => $shipping->AddressLine2,
                'shippingCity'     => $shipping->City,
                'shippingPostcode' => $shipping->PostalCode,
                'shippingState'    => $shipping->State,
                'shippingCountry'  => $shipping->Country,
                'shippingPhone'    => $shipping->Phone,
            )
        );
    }

    /**
     * Create a new payment for an order
     */
    public function createPayment($gateway)
    {
        if (!GatewayInfo::isSupported($gateway)) {
            $this->error(
                _t(
                    __CLASS__ . ".InvalidGateway",
                    "`{gateway}` isn't a valid payment gateway.",
                    'gateway is the name of the payment gateway',
                    array('gateway' => $gateway)
                )
            );
            return false;
        }
        if (!$this->order->canPay(Security::getCurrentUser())) {
            $this->error(_t(__CLASS__ . ".CantPay", "Order can't be paid for."));
            return false;
        }
        $payment = Payment::create()->init(
            $gateway,
            $this->order->TotalOutstanding(true),
            ShopConfigExtension::config()->base_currency
        );
        $this->order->Payments()->add($payment);
        return $payment;
    }

    /**
     * Complete payment processing
     *    - send receipt
     *    - update order status accordingling
     *    - fire event hooks
     */
    public function completePayment()
    {
        if (!$this->order->IsPaid()) {
            // recalculate order to be sure we have the correct total
            $this->order->calculate();

            $this->order->extend('onPayment'); //a payment has been made
            //place the order, if not already placed
            if ($this->canPlace($this->order)) {
                $this->placeOrder();
            } else {
                if ($this->order->Locale) {
                    ShopTools::install_locale($this->order->Locale);
                }
            }

            if (($this->order->GrandTotal() > 0 && $this->order->TotalOutstanding(false) <= 0)
                // Zero-dollar order (e.g. paid with loyalty points)
                || ($this->order->GrandTotal() == 0 && Order::config()->allow_zero_order_total)
            ) {
                //set order as paid
                $this->order->Status = 'Paid';
                $this->order->write();
            }
        }
    }

    /**
     * Determine if an order can be placed.
     *
     * @param boolean $order
     */
    public function canPlace(Order $order)
    {
        if (!$order) {
            $this->error(_t(__CLASS__ . ".NoOrder", "Order does not exist."));
            return false;
        }
        //order status is applicable
        if (!$order->IsCart()) {
            $this->error(_t(__CLASS__ . ".NotCart", "Order is not a cart."));
            return false;
        }
        //order has products
        if ($order->Items()->Count() <= 0) {
            $this->error(_t(__CLASS__ . ".NoItems", "Order has no items."));
            return false;
        }

        return true;
    }

    /**
     * Takes an order from being a cart to awaiting payment.
     *
     * @return boolean - success/failure
     */
    public function placeOrder()
    {
        if (!$this->order) {
            $this->error(_t(__CLASS__ . ".NoOrderStarted", "A new order has not yet been started."));
            return false;
        }
        if (!$this->canPlace($this->order)) { //final cart validation
            return false;
        }

        if ($this->order->Locale) {
            ShopTools::install_locale($this->order->Locale);
        }

        // recalculate order to be sure we have the correct total
        $this->order->calculate();

        if (DB::get_conn()->supportsTransactions()) {
            DB::get_conn()->transactionStart();
        }

        //update status
        if ($this->order->TotalOutstanding(false)) {
            $this->order->Status = 'Unpaid';
        } else {
            $this->order->Status = 'Paid';
        }

        if (!$this->order->Placed) {
            $this->order->Placed = DBDatetime::now()->Rfc2822(); //record placed order datetime
            if ($request = Controller::curr()->getRequest()) {
                $this->order->IPAddress = $request->getIP(); //record client IP
            }
        }

        // Add an error handler that throws an exception upon error, so that we can catch errors as exceptions
        // in the following block.
        set_error_handler(
            function ($severity, $message, $file, $line) {
                throw new ErrorException($message, 0, $severity, $file, $line);
            },
            E_ALL & ~(E_STRICT | E_NOTICE)
        );

        try {
            //re-write all attributes and modifiers to make sure they are up-to-date before they can't be changed again
            $items = $this->order->Items();
            if ($items->exists()) {
                foreach ($items as $item) {
                    $item->onPlacement();
                    $item->write();
                }
            }
            $modifiers = $this->order->Modifiers();
            if ($modifiers->exists()) {
                foreach ($modifiers as $modifier) {
                    $modifier->write();
                }
            }
            //add member to order & customers group
            if ($member = Security::getCurrentUser()) {
                if (!$this->order->MemberID) {
                    $this->order->MemberID = $member->ID;
                }
                $cgroup = ShopConfigExtension::current()->CustomerGroup();
                if ($cgroup->exists()) {
                    $member->Groups()->add($cgroup);
                }
            }
            //allow decorators to do stuff when order is saved.
            $this->order->extend('onPlaceOrder');
            $this->order->write();
        } catch (Exception $ex) {
            // Rollback the transaction if an error occurred
            if (DB::get_conn()->supportsTransactions()) {
                DB::get_conn()->transactionRollback();
            }
            $this->error($ex->getMessage());
            return false;
        } finally {
            // restore the error handler, no matter what
            restore_error_handler();
        }

        // Everything went through fine, complete the transaction
        if (DB::get_conn()->supportsTransactions()) {
            DB::get_conn()->transactionEnd();
        }

        //remove from session
        ShoppingCart::singleton()->clear(false);
        /*
        $cart = ShoppingCart::curr();
        if ($cart && $cart->ID == $this->order->ID) {
            // clear the cart, but don't write the order in the process (order is finalized and should NOT be overwritten)
        }
        */

        //send confirmation if configured and receipt hasn't been sent
        if (self::config()->send_confirmation
            && !$this->order->ReceiptSent
        ) {
            $this->notifier->sendConfirmation();
        }

        //notify admin, if configured
        if (self::config()->send_admin_notification) {
            $this->notifier->sendAdminNotification();
        }

        // Save order reference to session
        OrderManipulationExtension::add_session_order($this->order);

        return true; //report success
    }

    /**
     * @return Order
     */
    public function getOrder()
    {
        return $this->order;
    }

    public function getError()
    {
        return $this->error;
    }

    protected function error($message)
    {
        $this->error = $message;
    }
}


<?php

namespace SilverShop\ORM\FieldType;

use SilverShop\Extension\ShopConfigExtension;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\FieldType\DBVarchar;

class ShopCountry extends DBVarchar
{
    public function __construct($name = null, $size = 3, $options = [])
    {
        parent::__construct($name, $size = 3, $options);
    }

    public function forTemplate()
    {
        return $this->Nice();
    }

    /**
     * Convert ISO abbreviation to full, translated country name
     */
    public function Nice()
    {
        $val = ShopConfigExtension::countryCode2name($this->value);
        if (!$val) {
            $val = $this->value;
        }
        return _t(__CLASS__ . '.' . $this->value, $val);
    }

    public function XML()
    {
        return Convert::raw2xml($this->Nice());
    }
}


<?php

namespace SilverShop\Checkout;

use SilverShop\Cart\ShoppingCart;
use SilverShop\Model\Address;
use SilverShop\Model\Order;
use SilverShop\ShopTools;
use SilverShop\ShopUserInfo;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Omnipay\GatewayInfo;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;

/**
 * Helper class for getting an order throught the checkout process
 */
class Checkout
{
    use Injectable;

    /**
     * 4 different membership schemes:
     *    1: creation disabled & membership not required
     *        no body can, or is required to, become a member at checkout.
     *    2: creation disabled & membership required
     *        only existing members can use checkout.
     *        (ideally the entire shop should be disabled in this case)
     *    3: creation enabled & membership required
     *        everyone must be, or become a member at checkout.
     *    4: creation enabled & membership not required (default)
     *        it is optional to be, or become a member at checkout.
     */

    public static function member_creation_enabled()
    {
        return CheckoutConfig::config()->member_creation_enabled;
    }

    public static function membership_required()
    {
        return CheckoutConfig::config()->membership_required;
    }

    public static function get($order = null)
    {
        if ($order === null) {
            $order = ShoppingCart::curr(); //roll back to current cart
        }
        if ($order->exists() && $order->isInDB()) {//check if order can go through checkout
            return Checkout::create($order);
        }
        return false;
    }

    /**
     * @var Order
     */
    protected $order;

    /**
     * @var string
     */
    protected $message;

    /**
     * @var string
     */
    protected $type;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * Get stored message
     *
     * @return string
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * Get type of stored message
     *
     * @return string
     */
    public function getMessageType()
    {
        return $this->type;
    }

    /**
     * contact information
     */
    public function setContactDetails($email, $firstname, $surname)
    {
        $this->order->Email = $email;
        $this->order->FirstName = $firstname;
        $this->order->Surname = $surname;
        $this->order->write();
    }

    //save / set up addresses
    public function setShippingAddress(Address $address)
    {
        $this->order->ShippingAddressID = $address->ID;
        if ($member = Security::getCurrentUser()) {
            $this->order->MemberID = $member->ID;
        }
        $this->order->write();
        $this->order->extend('onSetShippingAddress', $address);

        ShopUserInfo::singleton()->setAddress($address);
    }

    public function setBillingAddress(Address $address)
    {
        $this->order->BillingAddressID = $address->ID;
        if ($member = Security::getCurrentUser()) {
            $this->order->MemberID = $member->ID;
        }
        $this->order->write();
        $this->order->extend('onSetBillingAddress', $address);
    }

    /**
     * Set payment method
     *
     * @throws \SilverStripe\Omnipay\Exception\InvalidConfigurationException
     */
    public function setPaymentMethod($paymentmethod)
    {
        $methods = GatewayInfo::getSupportedGateways();
        if (!isset($methods[$paymentmethod])) {
            ShopTools::getSession()
                ->set('Checkout.PaymentMethod', null)
                ->clear('Checkout.PaymentMethod');
            return $this->error(_t(__CLASS__ . '.NoPaymentMethod', 'Payment method does not exist'));
        }
        ShopTools::getSession()->set('Checkout.PaymentMethod', $paymentmethod);
        return true;
    }

    /**
     * Gets the selected payment method from the session,
     * or the only available method, if there is only one.
     *
     * @throws \SilverStripe\Omnipay\Exception\InvalidConfigurationException
     */
    public function getSelectedPaymentMethod($nice = false)
    {
        $methods = GatewayInfo::getSupportedGateways();
        reset($methods);
        $method = count($methods) === 1 ? key($methods) : ShopTools::getSession()->get('Checkout.PaymentMethod');
        if ($nice && isset($methods[$method])) {
            $method = $methods[$method];
        }
        return $method;
    }

    /**
     * Checks if member (or not) is allowed, in accordance with configuration
     */
    public function validateMember($member)
    {
        if (!CheckoutConfig::config()->membership_required) {
            return true;
        }
        if (empty($member) || !($member instanceof Member)) {
            return false;
        }

        return true;
    }

    /**
     * Store a new error & return false;
     */
    protected function error($message)
    {
        $this->message($message, 'bad');
        return false;
    }

    /**
     * Store a message to be fed back to user.
     *
     * @param string $message
     * @param string $type    - good, bad, warning
     */
    protected function message($message, $type = 'good')
    {
        $this->message = $message;
        $this->type = $type;
    }
}


<?php

namespace SilverShop\Checkout;

use SilverShop\Extension\ShopConfigExtension;
use SilverShop\Model\Order;
use SilverShop\Model\OrderStatusLog;
use SilverShop\Page\CheckoutPage;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;

/**
 * Handles email notifications to customers and / or admins.
 *
 * @package shop
 */
class OrderEmailNotifier
{
    use Injectable;
    use Configurable;

    /**
     * @var Order $order
     */
    protected $order;

    /**
     * @var bool
     */
    protected $debugMode = false;

    /**
     * Assign the order to a local variable
     *
     * @param Order $order
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * @param bool $bool
     * @return $this
     */
    public function setDebugMode($bool)
    {
        $this->debugMode = $bool;
        return $this;
    }

    /**
     * @param string $template
     * @param string $subject
     *
     * @return Email
     */
    protected function buildEmail($template, $subject)
    {
        $from = ShopConfigExtension::config()->email_from ? ShopConfigExtension::config()->email_from : Email::config()->admin_email;
        $to = $this->order->getLatestEmail();
        $checkoutpage = CheckoutPage::get()->first();
        $completemessage = $checkoutpage ? $checkoutpage->PurchaseComplete : '';

        /**
         * @var Email $email
         */
        $email = Email::create()
            ->setHTMLTemplate($template)
            ->setFrom($from)
            ->setTo($to)
            ->setSubject($subject);

        $email->setData(
            [
                'PurchaseCompleteMessage' => $completemessage,
                'Order' => $this->order,
                'BaseURL' => Director::absoluteBaseURL(),
            ]
        );

        return $email;
    }

    /**
     * Send a mail of the order to the client (and another to the admin).
     *
     * @param string $template    - the class name of the email you wish to send
     * @param string $subject     - subject of the email
     * @param bool   $copyToAdmin - true by default, whether it should send a copy to the admin
     *
     * @return bool|string
     */
    public function sendEmail($template, $subject, $copyToAdmin = true)
    {
        $email = $this->buildEmail($template, $subject);

        if ($copyToAdmin) {
            $email->setBcc(Email::config()->admin_email);
        }
        if ($this->debugMode) {
            return $this->debug($email);
        } else {
            return $email->send();
        }
    }

    /**
     * Send customer a confirmation that the order has been received
     *
     * @return bool
     */
    public function sendConfirmation()
    {
        $subject = _t(
            'SilverShop\ShopEmail.ConfirmationSubject',
            'Order #{OrderNo} confirmation',
            '',
            array('OrderNo' => $this->order->Reference)
        );
        return $this->sendEmail(
            'SilverShop/Model/Order_ConfirmationEmail',
            $subject,
            self::config()->bcc_confirmation_to_admin
        );
    }

    /**
     * Notify store owner about new order.
     *
     * @return bool|string
     */
    public function sendAdminNotification()
    {
        $subject = _t(
            'SilverShop\ShopEmail.AdminNotificationSubject',
            'Order #{OrderNo} notification',
            '',
            array('OrderNo' => $this->order->Reference)
        );

        $email = $this->buildEmail('SilverShop/Model/Order_AdminNotificationEmail', $subject)
            ->setTo(Email::config()->admin_email);

        if ($this->debugMode) {
            return $this->debug($email);
        } else {
            return $email->send();
        }
    }

    /**
     * Send customer an order receipt email.
     * Precondition: The order payment has been successful
     */
    public function sendReceipt()
    {
        $subject = _t(
            'SilverShop\ShopEmail.ReceiptSubject',
            'Order #{OrderNo} receipt',
            '',
            array('OrderNo' => $this->order->Reference)
        );

        return $this->sendEmail(
            'SilverShop/Model/Order_ReceiptEmail',
            $subject,
            self::config()->bcc_receipt_to_admin
        );
    }

    /**
     * Sends an email to the admin that an order has been cancelled
     */
    public function sendCancelNotification()
    {
        $email = Email::create()
            ->setSubject(_t(
                'SilverShop\ShopEmail.CancelSubject',
                'Order #{OrderNo} cancelled by member',
                '',
                ['OrderNo' => $this->order->Reference]
            ))
            ->setFrom(Email::config()->admin_email)
            ->setTo(Email::config()->admin_email)
            ->setBody($this->order->renderWith(Order::class));

        if ($this->debugMode) {
            return $this->debug($email);
        } else {
            return $email->send();
        }
    }

    /**
     * Send an email to the customer containing the latest note of {@link OrderStatusLog} and the current status.
     *
     * @param string $title Subject for email
     * @param string $note  Optional note-content (instead of using the OrderStatusLog)
     *
     * @return bool|string
     */
    public function sendStatusChange($title, $note = null)
    {
        if (!$note) {
            $latestLog = OrderStatusLog::get()
                ->filter("OrderID", $this->order->ID)
                ->filter("SentToCustomer", 1)
                ->first();

            if ($latestLog) {
                $note = $latestLog->Note;
                $title = $latestLog->Title;
            }
        }

        if (Config::inst()->get(OrderProcessor::class, 'receipt_email')) {
            $adminEmail = Config::inst()->get(OrderProcessor::class, 'receipt_email');
        } else {
            $adminEmail = Email::config()->admin_email;
        }

        /**
         * @var Email $e
         */
        $email = Email::create()
            ->setFrom($adminEmail)
            ->setSubject(_t('SilverShop\ShopEmail.StatusChangeSubject', 'SilverShop – {Title}', ['Title' => $title]))
            ->setTo($this->order->getLatestEmail())
            ->setHTMLTemplate('SilverShop/Model/Order_StatusEmail')
            ->setData(
                [
                    'Order' => $this->order,
                    'Note' => $note,
                    'FromEmail' => $adminEmail
                ]
            );

        if ($this->debugMode) {
            return $this->debug($email);
        } else {
            return $email->send();
        }
    }

    /**
     * The new Email::debug method in SilverStripe dumps the entire message with all message parts,
     * which makes it unusable to preview an Email.
     * This method simulates the old way of the message output and renders only the HTML body.
     *
     * @param Email $email
     * @return string
     */
    protected function debug(Email $email)
    {
        $email->render();
        $template = $email->getHTMLTemplate();
        $headers = $email->getSwiftMessage()->getHeaders()->toString();

        return "
Email HTML template: $template
\n" .
            "
$headers
" .
            $email->getBody();
    }
}


<?php

namespace SilverShop\Cart;

use SilverShop\Extension\ViewableCartExtension;
use SilverShop\Model\Buyable;
use SilverShop\Model\Variation\Variation;
use SilverShop\Page\CartPage;
use SilverShop\Page\Product;
use SilverShop\ShopTools;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Dev\Debug;
use SilverStripe\ErrorPage\ErrorPage;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Permission;
use SilverStripe\Security\SecurityToken;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Requirements;

/**
 * Manipulate the cart via urls.
 *
 * @mixin  ViewableCartExtension
 * @method ShoppingCart Cart()
 */
class ShoppingCartController extends Controller
{
    private static $url_segment = 'shoppingcart';

    /**
     * Whether or not this controller redirects to the cart-page whenever an item was added
     *
     * @config
     * @var    bool
     */
    private static $direct_to_cart_page = false;

    /**
     * @var ShoppingCart
     */
    protected $cart;

    /**
     * @config
     * @var array
     */
    private static $url_handlers = [
        '$Action/$Buyable/$ID' => 'handleAction',
    ];

    /**
     * @config
     * @var array
     */
    private static $allowed_actions = [
        'add',
        'additem',
        'remove',
        'removeitem',
        'removeall',
        'removeallitem',
        'setquantity',
        'setquantityitem',
        'clear',
        'debug',
    ];

    public static function add_item_link(Buyable $buyable, $parameters = array())
    {
        return self::build_url('add', $buyable, $parameters);
    }

    public static function remove_item_link(Buyable $buyable, $parameters = array())
    {
        return self::build_url('remove', $buyable, $parameters);
    }

    public static function remove_all_item_link(Buyable $buyable, $parameters = array())
    {
        return self::build_url('removeall', $buyable, $parameters);
    }

    public static function set_quantity_item_link(Buyable $buyable, $parameters = array())
    {
        return self::build_url('setquantity', $buyable, $parameters);
    }

    /**
     * Helper for creating a url
     */
    protected static function build_url($action, $buyable, $params = [])
    {
        if (!$action || !$buyable) {
            return false;
        }

        if (SecurityToken::is_enabled() && !self::config()->disable_security_token) {
            $params[SecurityToken::inst()->getName()] = SecurityToken::inst()->getValue();
        }

        $className = get_class($buyable);

        $link = Controller::join_links(
            [
            self::config()->url_segment,
            $action,
            ShopTools::sanitiseClassName($className),
            $buyable->ID
            ]
        );

        return empty($params) ? $link : $link . '?' . http_build_query($params);
    }

    /**
     * This is used here and in VariationForm and AddProductForm
     *
     * @param bool|string $status
     *
     * @return string|HTTPResponse
     */
    public static function direct($status = true)
    {
        if (Director::is_ajax()) {
            return (string)$status;
        }
        if (self::config()->direct_to_cart_page && ($cartlink = CartPage::find_link())) {
            return Controller::curr()->redirect($cartlink);
        } else {
            return Controller::curr()->redirectBack();
        }
    }

    public function init()
    {
        parent::init();
        $this->cart = ShoppingCart::singleton();
    }

    /**
     * @return Product|Variation|Buyable
     * @throws \SilverStripe\Control\HTTPResponse_Exception
     */
    protected function buyableFromRequest()
    {
        $request = $this->getRequest();
        if (SecurityToken::is_enabled()
            && !self::config()->disable_security_token
            && !SecurityToken::inst()->checkRequest($request)
        ) {
            return $this->httpError(
                400,
                _t(
                    'SilverShop\Cart\ShoppingCart.InvalidSecurityToken',
                    'Invalid security token, possible CSRF attack.'
                )
            );
        }
        $id = (int)$request->param('ID');
        if (empty($id)) {
            //TODO: store error message
            return null;
        }
        $buyableclass = Product::class;
        if ($class = $request->param('Buyable')) {
            $buyableclass = ShopTools::unsanitiseClassName($class);
        }
        if (!ClassInfo::exists($buyableclass)) {
            //TODO: store error message
            return null;
        }
        //ensure only live products are returned, if they are versioned
        $buyable = $buyableclass::has_extension(Versioned::class)
            ? Versioned::get_by_stage($buyableclass, 'Live')->byID($id)
            : DataObject::get($buyableclass)->byID($id);

        if (!$buyable || !($buyable instanceof Buyable)) {
            //TODO: store error message
            return null;
        }

        return $this->cart->getCorrectBuyable($buyable);
    }

    /**
     * Action: add item to cart
     *
     * @param HTTPRequest $request
     *
     * @return HTTPResponse
     * @throws \SilverStripe\Control\HTTPResponse_Exception
     */
    public function add($request)
    {
        if ($product = $this->buyableFromRequest()) {
            $quantity = (int)$request->getVar('quantity');
            if (!$quantity) {
                $quantity = 1;
            }
            $this->cart->add($product, $quantity, $request->getVars());
        }

        $this->updateLocale($request);
        $this->extend('updateAddResponse', $request, $response, $product, $quantity);
        return $response ? $response : self::direct();
    }

    /**
     * Action: remove a certain number of items from the cart
     *
     * @param HTTPRequest $request
     *
     * @return HTTPResponse
     * @throws \SilverStripe\Control\HTTPResponse_Exception
     */
    public function remove($request)
    {
        if ($product = $this->buyableFromRequest()) {
            $this->cart->remove($product, $quantity = 1, $request->getVars());
        }

        $this->updateLocale($request);
        $this->extend('updateRemoveResponse', $request, $response, $product, $quantity);
        return $response ? $response : self::direct();
    }

    /**
     * Action: remove all of an item from the cart
     *
     * @param HTTPRequest $request
     *
     * @return HTTPResponse
     * @throws \SilverStripe\Control\HTTPResponse_Exception
     */
    public function removeall($request)
    {
        if ($product = $this->buyableFromRequest()) {
            $this->cart->remove($product, null, $request->getVars());
        }

        $this->updateLocale($request);
        $this->extend('updateRemoveAllResponse', $request, $response, $product);
        return $response ? $response : self::direct();
    }

    /**
     * Action: update the quantity of an item in the cart
     *
     * @param HTTPRequest $request
     *
     * @return HTTPResponse
     * @throws \SilverStripe\Control\HTTPResponse_Exception
     */
    public function setquantity($request)
    {
        $product = $this->buyableFromRequest();
        $quantity = (int)$request->getVar('quantity');
        if ($product) {
            $this->cart->setQuantity($product, $quantity, $request->getVars());
        }

        $this->updateLocale($request);
        $this->extend('updateSetQuantityResponse', $request, $response, $product, $quantity);
        return $response ? $response : self::direct();
    }

    /**
     * Action: clear the cart
     *
     * @param HTTPRequest $request
     *
     * @return HTTPResponse
     */
    public function clear($request)
    {
        $this->updateLocale($request);
        $this->cart->clear();
        $this->extend('updateClearResponse', $request, $response);
        return $response ? $response : self::direct();
    }

    /**
     * Handle index requests
     *
     * @throws \SilverStripe\Control\HTTPResponse_Exception
     */
    public function index()
    {
        if ($cart = $this->Cart()) {
            return $this->redirect($cart->CartLink);
        } elseif ($response = ErrorPage::response_for(404)) {
            return $response;
        }
        return $this->httpError(404, _t('SilverShop\Cart\ShoppingCart.NoCartInitialised', 'no cart initialised'));
    }

    /**
     * Displays order info and cart contents.
     */
    public function debug()
    {
        if (Director::isDev() || Permission::check('ADMIN')) {
            //TODO: allow specifying a particular id to debug
            Requirements::css('silvershop/core: client/dist/css/cartdebug.css');
            $order = ShoppingCart::curr();
            $content = ($order)
                ? Debug::text($order)
                : 'Cart has not been created yet. Add a product.';
            return ['Content' => $content];
        }
    }

    /**
     * @param HTTPRequest $request
     */
    protected function updateLocale($request)
    {
        $order = $this->cart->current();
        if ($request && $request->isAjax() && $order) {
            ShopTools::install_locale($order->Locale);
        }
    }
}


<?php

namespace SilverShop\ORM\Filters;

use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataObject;

//TODO: Rewrite this with ORM in mind… reduce the amount of raw queries.

/**
 * Helper class to create a filter for matching a dataobject,
 * using field values or relationship ids and only those ids.
 *
 * Combining fields defines a way to uniquely identify an object.
 *
 * Useful for finding if a dataobject with given field values exists.
 * Protects against SQL injection, and searching on unauthroised fields.
 * Ignores fields that don't exist on the object.
 * Adds IS NULL, or = 0 for values that are not passed.
 *
 * Similar to SearchContext
 *
 * Conjunctive query
 *
 * Example input:
 * $data = array(
 *        'FieldName' => 'data'
 *        'AnotherField' => 32,
 *        'NotIncludedField' => 'blah'
 * );
 *
 * $required = array(
 *        'FieldName',
 *        'AnotherField',
 *        'ARequiredField'
 * );
 *
 * Example output:
 * "FieldName" = 'data' AND  "AnotherField" = 32 AND "ARequiredField" IS NULL
 */
class MatchObjectFilter
{
    protected $className;

    protected $data;

    protected $required;

    /**
     * @param string $className
     * @param array  $data           field values to use
     * @param array  $requiredfields fields required to be included in the query
     */
    public function __construct($className, array $data, array $requiredfields)
    {
        $this->className = $className;
        $this->required = $requiredfields;
        $this->data = $data;
    }

    /**
     * Create SQL where filter
     *
     * @return array of filter statements
     */
    public function getFilter()
    {
        if (!is_array($this->data)) {
            return null;
        }
        $allowed = array_keys(DataObject::getSchema()->databaseFields($this->className));
        $fields = array_flip(array_intersect($allowed, $this->required));
        $singleton = singleton($this->className);

        $new = [];
        foreach ($fields as $field => $value) {
            $field = Convert::raw2sql($field);
            if (in_array($field, $allowed)) {
                if (isset($this->data[$field])) {
                    // allow wildcard in filter
                    if ($this->data[$field] === '*') {
                        continue;
                    }

                    $dbfield = $singleton->dbObject($field);
                    $value = $dbfield->prepValueForDB($this->data[$field]);

                    $new[] = "\"$field\" = '$value'";
                } else {
                    $new[] = "\"$field\" IS NULL";
                }
            } else {
                if (isset($this->data[$field])) {
                    $value = Convert::raw2sql($this->data[$field]);
                    $new[] = "\"$field\" = '$value'";
                } else {
                    $new[] = "\"$field\" IS NULL";
                }
            }
        }
        return $new;
    }
}


<?php

namespace SilverShop\Forms;

use SilverShop\Extension\ShopConfigExtension;
use SilverShop\Page\AccountPageController;
use SilverShop\Page\CheckoutPage;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Security\Security;

/**
 * Allows shop members to update their details with the shop.
 */
class ShopAccountForm extends Form
{
    public function __construct($controller, $name)
    {
        $member = Security::getCurrentUser();
        $requiredFields = null;
        if ($member && $member->exists()) {
            $fields = $member->getMemberFormFields();
            $fields->removeByName('Password');
            $requiredFields = $member->getValidator();
            $requiredFields->addRequiredField('Surname');
        } else {
            $fields = FieldList::create();
        }
        if ($controller instanceof AccountPageController) {
            $actions = FieldList::create(FormAction::create('submit', _t(__CLASS__ . '.Save', 'Save Changes')));
        } else {
            $actions = FieldList::create(
                FormAction::create('submit', _t(__CLASS__ . '.Save', 'Save Changes'))
                    ->setUseButtonTag(Config::inst()->get(ShopConfigExtension::class, 'forms_use_button_tag')),
                FormAction::create('proceed', _t(__CLASS__ . '.SaveAndProceed', 'Save and proceed to checkout'))
                    ->setUseButtonTag(Config::inst()->get(ShopConfigExtension::class, 'forms_use_button_tag'))
            );
        }
        parent::__construct($controller, $name, $fields, $actions, $requiredFields);

        $this->extend('updateShopAccountForm');

        if ($member) {
            $member->Password = ''; //prevents password field from being populated with encrypted password data
            $this->loadDataFrom($member);
        }
    }

    /**
     * Save the changes to the form
     *
     * @param array       $data
     * @param Form        $form
     * @param HTTPRequest $request
     *
     * @return bool|HTTPResponse
     * @throws \SilverStripe\ORM\ValidationException
     */
    public function submit($data, $form, $request)
    {
        $member = Security::getCurrentUser();
        if (!$member) {
            return false;
        }

        $form->saveInto($member);
        $member->write();
        $form->sessionMessage(_t(__CLASS__ . '.DetailsSaved', 'Your details have been saved'), 'good');

        $this->extend('updateShopAccountFormResponse', $request, $form, $data, $response);

        return $response ?: $this->getController()->redirectBack();
    }

    /**
     * Save the changes to the form, and redirect to the checkout page
     *
     * @param array       $data
     * @param Form        $form
     * @param HTTPRequest $request
     *
     * @return bool|HTTPResponse
     * @throws \SilverStripe\ORM\ValidationException
     */
    public function proceed($data, $form, $request)
    {
        $member = Security::getCurrentUser();
        if (!$member) {
            return false;
        }

        $form->saveInto($member);
        $member->write();
        $form->sessionMessage(_t(__CLASS__ . '.DetailsSaved', 'Your details have been saved'), 'good');

        $this->extend('updateShopAccountFormResponse', $request, $form, $data, $response);

        return $response ?: $this->getController()->redirect(CheckoutPage::find_link());
    }
}