<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug\Output;

use bdk\Debug;
use bdk\Debug\MethodTable;
use bdk\PubSub\Event;

/**
 * Output log as HTML
 *
 * @property HtmlObject $object lazy-loaded HtmlObject... only loaded if dumping an object
 */
class Html extends Base
{

    protected $errorSummary;
    protected $wrapAttribs = array();
    protected $channels = array();
    protected $tableInfo;

    /**
     * Constructor
     *
     * @param \bdk\Debug $debug debug instance
     */
    public function __construct(Debug $debug)
    {
        $this->errorSummary = new HtmlErrorSummary($this, $debug->errorHandler);
        parent::__construct($debug);
    }

    /**
     * Formats an array as a table
     *
     * @param array $rows    array of \Traversable
     * @param array $options options
     *                           'attribs' : key/val array (or string - interpreted as class value)
     *                           'caption' : optional caption
     *                           'columns' : array of columns to display (defaults to all)
     *                           'totalCols' : array of column keys that will get totaled
     *
     * @return string
     */
    public function buildTable($rows, $options = array())
    {
        $options = \array_merge(array(
            'attribs' => array(),
            'caption' => null,
            'columns' => array(),
            'totalCols' => array(),
        ), $options);
        if (\is_string($options['attribs'])) {
            $options['attribs'] = array(
                'class' => $options['attribs'],
            );
        }
        if ($this->debug->abstracter->isAbstraction($rows) && $rows['traverseValues']) {
            $options['caption'] .= ' ('.$this->markupClassname(
                $rows['className'],
                'span',
                array(
                    'title' => $rows['phpDoc']['summary'] ?: null,
                )
            ).')';
            $options['caption'] = \trim($options['caption']);
            $rows = $rows['traverseValues'];
        }
        $keys = $options['columns'] ?: $this->debug->methodTable->colKeys($rows);
        $this->tableInfo = array(
            'colClasses' => \array_fill_keys($keys, null),
            'haveObjRow' => false,
            'totals' => \array_fill_keys($options['totalCols'], null),
        );
        $tBody = '';
        foreach ($rows as $k => $row) {
            $tBody .= $this->buildTableRow($row, $keys, $k);
        }
        if (!$this->tableInfo['haveObjRow']) {
            $tBody = \str_replace('	', '', $tBody);
        }
        return $this->debug->utilities->buildTag(
            'table',
            $options['attribs'],
            "\n"
                .($options['caption'] ? '
'.$options['caption'].'
'."\n" : '')
                .$this->buildTableHeader($keys)
                .'
'."\n".$tBody.'
'."\n"
                .$this->buildTableFooter($keys)
        );
    }

    /**
     * Dump value as html
     *
     * @param mixed        $val      value to dump
     * @param boolean      $sanitize (true) apply htmlspecialchars?
     * @param string|false $tagName  (span) tag to wrap value in (or false)
     *
     * @return string
     */
    public function dump($val, $sanitize = true, $tagName = 'span')
    {
        $this->wrapAttribs = array(
            'class' => array(),
            'title' => null,
        );
        $this->sanitize = $sanitize;
        $val = parent::dump($val);
        if ($tagName && !\in_array($this->dumpType, array('recursion'))) {
            $wrapAttribs = $this->debug->utilities->arrayMergeDeep(
                array(
                    'class' => array(
                        't_'.$this->dumpType,
                        $this->dumpTypeMore,
                    ),
                ),
                $this->wrapAttribs
            );
            $val = $this->debug->utilities->buildTag($tagName, $wrapAttribs, $val);
        }
        $this->wrapAttribs = array();
        return $val;
    }

    /**
     * Wrap classname in span.t_classname
     * if namespaced'd additionally wrap namespace in span.namespace
     * If callable, also wrap .t_operator and .t_method-name
     *
     * @param string $str     classname or classname(::|->)methodname
     * @param string $tagName ("span") html tag to use
     * @param array  $attribs additional html attributes
     *
     * @return string
     */
    public function markupClassname($str, $tagName = 'span', $attribs = array())
    {
        if (\preg_match('/^(.+)(::|->)(.+)$/', $str, $matches)) {
            $classname = $matches[1];
            $opMethod = ''.\htmlspecialchars($matches[2]).''
                    . ''.$matches[3].'';
        } else {
            $classname = $str;
            $opMethod = '';
        }
        $idx = \strrpos($classname, '\\');
        if ($idx) {
            $classname = ''.\substr($classname, 0, $idx + 1).''
                . \substr($classname, $idx + 1);
        }
        $attribs = \array_merge(array(
            'class' => 't_classname',
        ), $attribs);
        return $this->debug->utilities->buildTag($tagName, $attribs, $classname)
            .$opMethod;
    }

    /**
     * Return the log as HTML
     *
     * @param Event $event event object
     *
     * @return string|void
     */
    public function onOutput(Event $event)
    {
        $this->data = $this->debug->getData();
        $this->channels = array();
        $str = '
debug->utilities->buildAttribString(array(
            'class' => 'debug',
            // channel list gets built as log processed...  we'll str_replace this...
            'data-channels' => '{{channels}}',
            'data-channel-root' => $this->channelNameRoot,
        )).">\n";
        if ($this->debug->getCfg('output.outputCss')) {
            $str .= ''."\n";
        }
        if ($this->debug->getCfg('output.outputScript')) {
            $str .= ''."\n";
        }
        $str .= '

Debug Log

'."\n";
        $str .= $this->processAlerts();
        /*
            If outputing script, initially hide the output..
            this will help page load performance (fewer redraws)... by magnitudes
        */
        if ($this->debug->getCfg('output.outputScript')) {
            $str .= '
Loading
'."\n";
        }
        $str .= '
debug->getCfg('outputScript') ? ' style="display:none;"' : '').'>'."\n";
        $str .= $this->processSummary();
        $str .= '
'."\n";
        $str .= '
debug->getCfg('outputScript') ? ' style="display:none;"' : '').'>'."\n";
        $str .= $this->processLog();
        $str .= '
'."\n";  // close .debug-content
        $str .= '
'."\n";  // close .debug
        $str = \strtr($str, array(
            '{{channels}}' => \htmlspecialchars(\json_encode($this->buildChannelTree(), JSON_FORCE_OBJECT)),
        ));
        $this->data = array();
        $event['return'] .= $str;
    }

    /**
     * Return a log entry as HTML
     *
     * @param string $method method
     * @param array  $args   args
     * @param array  $meta   meta values
     *
     * @return string|void
     */
    public function processLogEntry($method, $args = array(), $meta = array())
    {
        $str = '';
        if (!\in_array($meta['channel'], $this->channels) && $meta['channel'] !== 'phpError') {
            $this->channels[] = $meta['channel'];
        }
        if ($meta['channel'] === $this->channelNameRoot) {
            $meta['channel'] = null;
        }
        if ($method == 'alert') {
            $str = $this->methodAlert($args, $meta);
        } elseif (\in_array($method, array('group', 'groupCollapsed', 'groupEnd'))) {
            $str = $this->buildGroupMethod($method, $args, $meta);
        } elseif (\in_array($method, array('profileEnd','table','trace'))) {
            $meta = \array_merge(array(
                'caption' => null,
                'columns' => array(),
                'sortable' => false,
                'totalCols' => array(),
            ), $meta);
            $asTable = \is_array($args[0]) && $args[0];
            if (!$asTable && $meta['caption']) {
                \array_unshift($args, $meta['caption']);
            }
            $str = $this->debug->utilities->buildTag(
                'div',
                array(
                    'class' => 'm_'.$method,
                    'data-channel' => $meta['channel'],
                ),
                $asTable
                    ? "\n"
                        .$this->buildTable(
                            $args[0],
                            array(
                                'attribs' => array(
                                    'class' => array(
                                        'table-bordered',
                                        $meta['sortable'] ? 'sortable' : null,
                                    ),
                                ),
                                'caption' => $meta['caption'],
                                'columns' => $meta['columns'],
                                'totalCols' => $meta['totalCols'],
                            )
                        )."\n"
                    : $this->buildArgString($args)
            );
        } else {
            $sanitize = isset($meta['sanitize'])
                ? $meta['sanitize']
                : true;
            $attribs = array(
                'class' => 'm_'.$method,
                'data-channel' => $meta['channel'],
                'title' => isset($meta['file'])
                    ? $meta['file'].': line '.$meta['line']
                    : null,
            );
            if (\in_array($method, array('assert','clear','error','info','log','warn'))) {
                if (\in_array($method, array('error','warn'))) {
                    if (isset($meta['errorCat'])) {
                        $attribs['class'] .= ' error-'.$meta['errorCat'];
                    }
                }
                if (\count($args) > 1 && \is_string($args[0])) {
                    $hasSubs = false;
                    $args = $this->processSubstitutions($args, $hasSubs);
                    if ($hasSubs) {
                        $args = array( \implode('', $args) );
                    }
                }
            }
            $str = $this->debug->utilities->buildTag(
                'div',
                $attribs,
                $this->buildArgString($args, $sanitize)
            );
        }
        $str = \str_replace(' data-channel="null"', '', $str);
        $str .= "\n";
        return $str;
    }

    /**
     * Convert all arguments to html and join them together.
     *
     * @param array   $args     arguments
     * @param boolean $sanitize apply htmlspecialchars (to non-first arg)?
     *
     * @return string html
     */
    protected function buildArgString($args, $sanitize = true)
    {
        $glue = ', ';
        $glueAfterFirst = true;
        if (\is_string($args[0])) {
            if (\preg_match('/[=:] ?$/', $args[0])) {
                // first arg ends with "=" or ":"
                $glueAfterFirst = false;
            } elseif (\count($args) == 2) {
                $glue = ' = ';
            }
        }
        foreach ($args as $i => $v) {
            $args[$i] = $i > 0
                ? $this->dump($v, $sanitize)
                : $this->dump($v, false);
        }
        if (!$glueAfterFirst) {
            return $args[0].\implode($glue, \array_slice($args, 1));
        } else {
            return \implode($glue, $args);
        }
    }

    /**
     * Build a tree of all channels that have been output
     *
     * @return array
     */
    protected function buildChannelTree()
    {
        if ($this->channels == array($this->channelNameRoot)) {
            return array();
        }
        \sort($this->channels);
        // move root to the top
        $rootKey = \array_search($this->channelNameRoot, $this->channels);
        if ($rootKey !== false) {
            unset($this->channels[$rootKey]);
            \array_unshift($this->channels, $this->channelName);
        }
        $tree = array();
        foreach ($this->channels as $channel) {
            $ref = &$tree;
            $path = \explode('.', $channel);
            foreach ($path as $k) {
                if (!isset($ref[$k])) {
                    $ref[$k] = array();
                }
                $ref = &$ref[$k];
            }
        }
        return $tree;
    }

    /**
     * handle html output of group, groupCollapsed, & groupEnd
     *
     * @param string $method group|groupCollapsed|groupEnd
     * @param array  $args   args passed to method
     * @param array  $meta   meta values
     *
     * @return string
     */
    protected function buildGroupMethod($method, $args = array(), $meta = array())
    {
        $str = '';
        if (\in_array($method, array('group','groupCollapsed'))) {
            $label = \array_shift($args);
            $levelClass = isset($meta['level'])
                ? 'level-'.$meta['level']
                : null;
            if (!empty($meta['isMethodName'])) {
                $label = $this->markupClassname($label);
            }
            foreach ($args as $k => $v) {
                $args[$k] = $this->dump($v);
            }
            $argStr = \implode(', ', $args);
            /*
                Header
            */
            $str .= $this->debug->utilities->buildTag(
                'div',
                array(
                    'class' => array(
                        'group-header',
                        $method == 'groupCollapsed'
                            ? 'collapsed'
                            : 'expanded',
                        $levelClass,
                    ),
                    'data-channel' => $meta['channel'],
                ),
                ''
                    .$label
                    .(!empty($argStr)
                        ? '('.$argStr.')'
                        : '')
                .''
            )."\n";
            /*
                Group open
            */
            $str .= '
debug->utilities->buildAttribString(array(
                'class' => array(
                    'm_group',
                    $levelClass,
                ),
            )).'>';
        } elseif ($method == 'groupEnd') {
            $str = '
';
        }
        return $str;
    }

    /**
     * Returns table's tfoot
     *
     * @param array $keys column header values (keys of array or property names)
     *
     * @return string
     */
    protected function buildTableFooter($keys)
    {
        $haveTotal = false;
        $cells = array();
        foreach ($keys as $key) {
            $colHasTotal = isset($this->tableInfo['totals'][$key]);
            $cells[] = $colHasTotal
                ? $this->dump(\round($this->tableInfo['totals'][$key], 6), true, 'td')
                : '	';
            $haveTotal = $haveTotal || $colHasTotal;
        }
        if (!$haveTotal) {
            return '';
        }
        return '
'."\n"
            .'
 	'
                .($this->tableInfo['haveObjRow'] ? ' 	' : '')
                .\implode('', $cells)
            .'
'."\n"
            .'
'."\n";
    }

    /**
     * Returns table's thead
     *
     * @param array $keys column header values (keys of array or property names)
     *
     * @return string
     */
    protected function buildTableHeader($keys)
    {
        $headers = array();
        foreach ($keys as $key) {
            $headers[$key] = $key === MethodTable::SCALAR
                ? 'value'
                : \htmlspecialchars($key);
            if ($this->tableInfo['colClasses'][$key]) {
                $headers[$key] .= ' '.$this->markupClassname($this->tableInfo['colClasses'][$key]);
            }
        }
        return '
'."\n"
            .'
 	'
                .($this->tableInfo['haveObjRow'] ? ' 	' : '')
                .''.\implode('	', $headers).'	'
            .'
'."\n"
            .'
'."\n";
    }

    /**
     * Returns table row
     *
     * @param mixed $row    should be array or abstraction
     * @param array $keys   column keys
     * @param array $rowKey row key
     *
     * @return string
     */
    protected function buildTableRow($row, $keys, $rowKey)
    {
        $str = '';
        $values = $this->debug->methodTable->keyValues($row, $keys, $objInfo);
        $parsed = $this->debug->utilities->parseTag($this->dump($rowKey));
        $str .= '
';
        $str .= $this->debug->utilities->buildTag(
            'th',
            array(
                'class' => 't_key text-right '.$parsed['attribs']['class'],
                'scope' => 'row',
            ),
            $parsed['innerhtml']
        );
        if ($objInfo['row']) {
            $str .= $this->markupClassname($objInfo['row']['className'], 'td', array(
                'title' => $objInfo['row']['phpDoc']['summary'] ?: null,
            ));
            $this->tableInfo['haveObjRow'] = true;
        } else {
            $str .= '	';
        }
        foreach ($values as $v) {
            $str .= $this->dump($v, true, 'td');
        }
        $str .= '
'."\n";
        $str = \str_replace(' title=""', '', $str);
        foreach (\array_keys($this->tableInfo['totals']) as $k) {
            $this->tableInfo['totals'][$k] += $values[$k];
        }
        foreach ($objInfo['cols'] as $k2 => $classname) {
            if ($this->tableInfo['colClasses'][$k2] === false) {
                // column values not of the same type
                continue;
            }
            if ($this->tableInfo['colClasses'][$k2] === null) {
                $this->tableInfo['colClasses'][$k2] = $classname;
            }
            if ($this->tableInfo['colClasses'][$k2] !== $classname) {
                $this->tableInfo['colClasses'][$k2] = false;
            }
        }
        return $str;
    }

    /**
     * Dump array as html
     *
     * @param array $array array
     *
     * @return string html
     */
    protected function dumpArray($array)
    {
        if (empty($array)) {
            $html = 'array'
                .'()';
        } else {
            $displayKeys = $this->debug->getCfg('output.displayListKeys') || !$this->debug->utilities->isList($array);
            $html = 'array'
                .'('."\n";
            if ($displayKeys) {
                $html .= ''."\n";
                foreach ($array as $key => $val) {
                    $html .= "\t".''
                            .''
                                .$this->dump($key, true, false) // don't wrap it
                            .' '
                            .'=> '
                            .$this->dump($val)
                        .''."\n";
                }
                $html .= '';
            } else {
                // display as list
                $html .= '
'."\n";
                foreach ($array as $val) {
                    $html .= $this->dump($val, true, 'li');
                }
                $html .= '
';
            }
            $html .= ')';
        }
        return $html;
    }

    /**
     * Dump boolean
     *
     * @param boolean $val boolean value
     *
     * @return string
     */
    protected function dumpBool($val)
    {
        return $val ? 'true' : 'false';
    }

    /**
     * Dump "Callable" as html
     *
     * @param array $abs array/callable abstraction
     *
     * @return string
     */
    protected function dumpCallable($abs)
    {
        return 'callable '
            .$this->markupClassname($abs['values'][0].'::'.$abs['values'][1]);
    }

    /**
     * Dump float value
     *
     * @param integer $val float value
     *
     * @return float
     */
    protected function dumpFloat($val)
    {
        $date = $this->checkTimestamp($val);
        if ($date) {
            $this->wrapAttribs['class'][] = 'timestamp';
            $this->wrapAttribs['title'] = $date;
        }
        return $val;
    }

    /**
     * Dump integer value
     *
     * @param integer $val integer value
     *
     * @return integer
     */
    protected function dumpInt($val)
    {
        return $this->dumpFloat($val);
    }

    /**
     * Dump null value
     *
     * @return string
     */
    protected function dumpNull()
    {
        return 'null';
    }

    /**
     * Dump object as html
     *
     * @param array $abs object abstraction
     *
     * @return string
     */
    protected function dumpObject($abs)
    {
        /*
            Were we debugged from inside or outside of the object?
        */
        $dump = $this->object->dump($abs);
        $this->wrapAttribs['data-accessible'] = $abs['scopeClass'] == $abs['className']
            ? 'private'
            : 'public';
        return $dump;
    }

    /**
     * Dump recursion (array recursion)
     *
     * @return string
     */
    protected function dumpRecursion()
    {
        return 'array *RECURSION*';
    }

    /**
     * Dump string
     *
     * @param string $val string value
     *
     * @return string
     */
    protected function dumpString($val)
    {
        if (\is_numeric($val)) {
            $date = $this->checkTimestamp($val);
            if ($date) {
                $this->wrapAttribs['class'][] = 'timestamp';
                $this->wrapAttribs['title'] = $date;
            }
        } else {
            if ($this->sanitize) {
                $val = $this->debug->utf8->dump($val, true, true);
                $val = $this->visualWhiteSpace($val);
            } else {
                $this->wrapAttribs['class'][] = 'no-pseudo';
                $val = $this->debug->utf8->dump($val, true, false);
            }
        }
        return $val;
    }

    /**
     * Dump undefined
     *
     * @return string
     */
    protected function dumpUndefined()
    {
        return '';
    }

    /**
     * Getter for this->object
     *
     * @return HtmlObject
     */
    protected function getObject()
    {
        $this->object = new HtmlObject($this->debug);
        return $this->object;
    }

    /**
     * Handle alert method
     *
     * @param array $args arguments
     * @param array $meta meta info
     *
     * @return array array($method, $args)
     */
    protected function methodAlert($args, $meta)
    {
        $attribs = array(
            'class' => 'alert alert-'.$meta['class'],
            'data-channel' => $meta['channel'],
            'role' => 'alert',
        );
        if ($meta['dismissible']) {
            $attribs['class'] .= ' alert-dismissible';
            $args[0] = '<button type="button" class="close" data-dismiss="alert" aria-label="Close">'
                .'×'
                .'</button>'
                .$args[0];
        }
        return $this->debug->utilities->buildTag('div', $attribs, $args[0]);
    }

    /**
     * process alerts
     *
     * @return string
     */
    protected function processAlerts()
    {
        $errorSummary = $this->errorSummary->build($this->debug->internal->errorStats());
        if ($errorSummary) {
            \array_unshift($this->data['alerts'], array(
                'alert',
                array($errorSummary),
                array(
                    'class' => 'danger error-summary',
                    'dismissible' => false,
                )
            ));
        }
        return parent::processAlerts();
    }

    /**
     * Cooerce value to string
     *
     * @param mixed $val value
     *
     * @return string
     */
    protected function substitutionAsString($val)
    {
        $type = $this->debug->abstracter->getType($val);
        if ($type == 'string') {
            $val = $this->dump($val, true, false);
        } elseif ($type == 'array') {
            $count = \count($val);
            $val = 'array'
                .'('.$count.')';
        } elseif ($type == 'object') {
            $val = $this->markupClassname($val['className']);
        } else {
            $val = $this->dump($val);
        }
        return $val;
    }

    /**
     * Add whitespace markup
     *
     * @param string $str string which to add whitespace html markup
     *
     * @return string
     */
    protected function visualWhiteSpace($str)
    {
        // display \r, \n, & \t
        $str = \preg_replace_callback('/(\r\n|\r|\n)/', array($this, 'visualWhiteSpaceCallback'), $str);
        $str = \preg_replace('#(
)?\n$#', '', $str);
        $str = \str_replace("\t", ''."\t".'', $str);
        return $str;
    }

    /**
     * Adds whitespace markup
     *
     * @param array $matches passed from preg_replace_callback
     *
     * @return string
     */
    protected function visualWhiteSpaceCallback($matches)
    {
        $strBr = $this->debug->getCfg('addBR') ? '
' : '';
        $search = array("\r","\n");
        $replace = array('',''.$strBr."\n");
        return \str_replace($search, $replace, $matches[1]);
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug\Output;

use bdk\PubSub\Event;

/**
 * Output log as plain-text
 */
class Text extends Base
{

    protected $depth = 0;   // for keeping track of indentation
    protected $valDepth = 0;

    /**
     * Output the log as text
     *
     * @param Event $event event object
     *
     * @return string|void
     */
    public function onOutput(Event $event)
    {
        $this->channelName = $this->debug->getCfg('channel');
        $this->data = $this->debug->getData();
        $str = '';
        $str .= $this->processAlerts();
        $str .= $this->processSummary();
        $str .= $this->processLog();
        $this->data = array();
        $event['return'] .= $str;
    }

    /**
     * Return log entry as text
     *
     * @param string $method method
     * @param array  $args   arguments
     * @param array  $meta   meta values
     *
     * @return string
     */
    public function processLogEntry($method, $args = array(), $meta = array())
    {
        $prefixes = array(
            'error' => '⦻ ',
            'info' => 'ℹ ',
            'log' => '',
            'warn' => '⚠ ',
            'assert' => '≠ ',
            'clear' => '⌦ ',
            'count' => '✚ ',
            'countReset' => '✚ ',
            'time' => '⏱ ',
            'timeLog' => '⏱ ',
            'group' => '▸ ',
            'groupCollapsed' => '▸ ',
        );
        $prefix = isset($prefixes[$method])
            ? $prefixes[$method]
            : '';
        $strIndent = \str_repeat('    ', $this->depth);
        if (\in_array($method, array('assert','clear','error','info','log','warn'))) {
            if (\count($args) > 1 && \is_string($args[0])) {
                $hasSubs = false;
                $args = $this->processSubstitutions($args, $hasSubs);
                if ($hasSubs) {
                    $args = array( \implode('', $args) );
                }
            }
        } elseif ($method == 'alert') {
            $classToPrefix = array(
                'danger' => 'error',
                'info' => 'info',
                'success' => 'info',
                'warning' => 'warn',
            );
            $class = $meta['class'];
            $prefix = $prefixes[$classToPrefix[$class]];
            $prefix = '[Alert '.$prefix.$class.'] ';
            $args = array($args[0]);
        } elseif (\in_array($method, array('profileEnd','table'))) {
            if (\is_array($args[0])) {
                $args = array($this->methodTable($args[0], $meta['columns']));
            }
            if ($meta['caption']) {
                \array_unshift($args, $meta['caption']);
            }
        } elseif ($method == 'trace') {
            \array_unshift($args, 'trace');
        } elseif (\in_array($method, array('group','groupCollapsed'))) {
            $this->depth ++;
        } elseif ($method == 'groupEnd' && $this->depth > 0) {
            $this->depth --;
        }
        $str = $prefix.$this->buildArgString($args);
        $str = \rtrim($str);
        if ($str) {
            $str = $strIndent.\str_replace("\n", "\n".$strIndent, $str);
            return $str."\n";
        }
        return '';
    }

    /**
     * Convert all arguments to text and join them together.
     *
     * @param array $args arguments
     *
     * @return string
     */
    protected function buildArgString($args)
    {
        $numArgs = \count($args);
        if ($numArgs == 1 && \is_string($args[0])) {
            $args[0] = \strip_tags($args[0]);
        }
        foreach ($args as $k => $v) {
            if ($k > 0 || !\is_string($v)) {
                $args[$k] = $this->dump($v);
            }
            $this->valDepth = 0;
        }
        $glue = ', ';
        $glueAfterFirst = true;
        if ($numArgs && \is_string($args[0])) {
            if (\preg_match('/[=:] ?$/', $args[0])) {
                // first arg ends with "=" or ":"
                $glueAfterFirst = false;
            } elseif (\count($args) == 2) {
                $glue = ' = ';
            }
        }
        if (!$glueAfterFirst) {
            return $args[0].\implode($glue, \array_slice($args, 1));
        } else {
            return \implode($glue, $args);
        }
    }

    /**
     * Dump array as text
     *
     * @param array $array Array to display
     *
     * @return string
     */
    protected function dumpArray($array)
    {
        $isNested = $this->valDepth > 0;
        $this->valDepth++;
        $array = parent::dumpArray($array);
        $str = \trim(\print_r($array, true));
        $str = \preg_replace('#^Array\n\(#', 'array(', $str);
        $str = \preg_replace('#^array\s*\(\s+\)#', 'array()', $str); // single-lineify empty array
        if ($isNested) {
            $str = \str_replace("\n", "\n    ", $str);
        }
        return $str;
    }

    /**
     * Dump boolean
     *
     * @param boolean $val boolean value
     *
     * @return string
     */
    protected function dumpBool($val)
    {
        return $val ? 'true' : 'false';
    }

    /**
     * Dump float value
     *
     * @param float $val float value
     *
     * @return float|string
     */
    protected function dumpFloat($val)
    {
        $date = $this->checkTimestamp($val);
        return $date
            ? '📅 '.$val.' ('.$date.')'
            : $val;
    }

    /**
     * Dump null value
     *
     * @return string
     */
    protected function dumpNull()
    {
        return 'null';
    }

    /**
     * Dump object as text
     *
     * @param array $abs object "abstraction"
     *
     * @return string
     */
    protected function dumpObject($abs)
    {
        $isNested = $this->valueDepth > 0;
        $this->valueDepth++;
        if ($abs['isRecursion']) {
            $str = '(object) '.$abs['className'].' *RECURSION*';
        } elseif ($abs['isExcluded']) {
            $str = '(object) '.$abs['className'].' (not inspected)';
        } else {
            $str = '(object) '.$abs['className']."\n";
            $str .= $this->dumpProperties($abs);
            if ($abs['collectMethods'] && $this->debug->output->getCfg('outputMethods')) {
                $str .= $this->dumpMethods($abs['methods']);
            }
        }
        $str = \trim($str);
        if ($isNested) {
            $str = \str_replace("\n", "\n    ", $str);
        }
        return $str;
    }

    /**
     * Dump object properties as text
     *
     * @param array $abs object abstraction
     *
     * @return string
     */
    protected function dumpProperties($abs)
    {
        $str = '';
        $propHeader = '';
        if (isset($abs['methods']['__get'])) {
            $str .= '    ✨ This object has a __get() method'."\n";
        }
        foreach ($abs['properties'] as $name => $info) {
            $vis = (array) $info['visibility'];
            foreach ($vis as $i => $v) {
                if (\in_array($v, array('magic','magic-read','magic-write'))) {
                    $vis[$i] = '✨ '.$v;    // "sparkles" there is no magic-wand unicode char
                } elseif ($v == 'private' && $info['inheritedFrom']) {
                    $vis[$i] = '🔒 '.$v;
                }
            }
            $vis = \implode(' ', $vis);
            $str .= $info['isExcluded']
                ? '    ('.$vis.' excluded) '.$name."\n"
                : '    ('.$vis.') '.$name.' = '.$this->dump($info['value'])."\n";
        }
        $propHeader = $str
            ? 'Properties:'
            : 'Properties: none!';
        $str = '  '.$propHeader."\n".$str;
        return $str;
    }

    /**
     * Dump object methods as text
     *
     * @param array $methods methods as returned from getMethods
     *
     * @return string html
     */
    protected function dumpMethods($methods)
    {
        $str = '';
        if (!empty($methods)) {
            $counts = array(
                'public' => 0,
                'protected' => 0,
                'private' => 0,
                'magic' => 0,
            );
            foreach ($methods as $info) {
                $counts[ $info['visibility'] ] ++;
            }
            $str .= '  Methods:'."\n";
            foreach ($counts as $vis => $count) {
                if ($count) {
                    $str .= '    '.$vis.': '.$count."\n";
                }
            }
        } else {
            $str .= '  Methods: none!'."\n";
        }
        return $str;
    }

    /**
     * Dump string
     *
     * @param string $val string value
     *
     * @return string
     */
    protected function dumpString($val)
    {
        if (\is_numeric($val)) {
            $date = $this->checkTimestamp($val);
            return $date
                ? '📅 "'.$val.'" ('.$date.')'
                : '"'.$val.'"';
        } else {
            return '"'.$this->debug->utf8->dump($val).'"';
        }
    }

    /**
     * Dump undefined
     *
     * @return null
     */
    protected function dumpUndefined()
    {
        return 'undefined';
    }
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2016-2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

/**
 * Novalnet SEPA payment provider
 *
 * @package MShop
 * @subpackage Service
 */
class NovalnetSepa
	extends \Aimeos\MShop\Service\Provider\Payment\OmniPay
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{
	private $feConfig = array(
		'novalnetsepa.holder' => array(
			'code' => 'novalnetsepa.holder',
			'internalcode'=> 'holder',
			'label'=> 'Bank account holder',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
		'novalnetsepa.iban' => array(
			'code' => 'novalnetsepa.iban',
			'internalcode'=> 'iban',
			'label'=> 'IBAN number',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
		'novalnetsepa.bic' => array(
			'code' => 'novalnetsepa.bic',
			'internalcode'=> 'bic',
			'label'=> 'BIC code',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
	);

	/**
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
	 * rules for the value of each field in the frontend.
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
	 */
	public function getConfigFE( \Aimeos\MShop\Order\Item\Base\Iface $basket )
	{
		$list = [];
		$feconfig = $this->feConfig;

		try
		{
			$code = $this->getServiceItem()->getCode();
			$service = $basket->getService( \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT, $code );

			foreach( $service->getAttributeItems() as $item )
			{
				if( isset( $feconfig[$item->getCode()] ) ) {
					$feconfig[$item->getCode()]['default'] = $item->getValue();
				}
			}
		}
		catch( \Aimeos\MShop\Order\Exception $e ) {; } // If payment isn't available yet

		$addresses = $basket->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

		if( ( $address = current( $addresses ) ) !== false )
		{
			if( $feconfig['novalnetsepa.holder']['default'] == ''
				&& ( $fn = $address->getFirstname() ) !== '' && ( $ln = $address->getLastname() ) !== ''
			) {
				$feconfig['novalnetsepa.holder']['default'] = $fn . ' ' . $ln;
			}
		}

		foreach( $feconfig as $key => $config ) {
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $config );
		}

		return $list;
	}

	/**
	 * Checks the frontend configuration attributes for validity.
	 *
	 * @param array $attributes Attributes entered by the customer during the checkout process
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
	 * 	known by the provider but aren't valid resp. null for attributes whose values are OK
	 */
	public function checkConfigFE( array $attributes )
	{
		return $this->checkConfig( $this->feConfig, $attributes );
	}

	/**
	 * Sets the payment attributes in the given service.
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $orderServiceItem Order service item that will be added to the basket
	 * @param array $attributes Attribute key/value pairs entered by the customer during the checkout process
	 */
	public function setConfigFE( \Aimeos\MShop\Order\Item\Base\Service\Iface $orderServiceItem, array $attributes )
	{
		$this->setAttributes( $orderServiceItem, $attributes, 'session' );
	}

	/**
	 * Tries to get an authorization or captures the money immediately for the given order if capturing the money
	 * separately isn't supported or not configured by the shop owner.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Standard Form object with URL, action and parameters to redirect to
	 * 	(e.g. to an external server of the payment provider or to a local success page)
	 */
	public function process( \Aimeos\MShop\Order\Item\Iface $order, array $params = [] )
	{
		return $this->processOrder( $order, $params );
	}

	/**
	 * Returns the data passed to the Omnipay library
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Basket object
	 * @param $orderid Unique order ID
	 * @param array $params Request parameter if available
	 */
	protected function getData( \Aimeos\MShop\Order\Item\Base\Iface $base, $orderid, array $params )
	{
		$urls = $this->getPaymentUrls();
		$card = $this->getCardDetails( $base, $params );
		$desc = $this->getContext()->getI18n()->dt( 'mshop', 'Order %1$s' );
		$addresses = $base->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

		if( ( $address = current( $addresses ) ) !== false ) {
			$langid = $address->getLanguageId();
		} else {
			$langid = $context->getLocale()->getLanguageId();
		}

		$data = array(
			'token' => '',
			'card' => $card,
			'language' => $langid,
			'transactionId' => $orderid,
			'description' => sprintf( $desc, $orderid ),
			'amount' => $this->getAmount( $base->getPrice() ),
			'currency' => $base->getLocale()->getCurrencyId(),
			'clientIp' => $this->getValue( 'client.ipaddress' ),
			'bic' => ( isset( $params['novalnetsepa.bic'] ) ? $params['novalnetsepa.bic'] : '' ),
			'iban' => ( isset( $params['novalnetsepa.iban'] ) ? $params['novalnetsepa.iban'] : '' ),
			'bankaccount_holder' => ( isset( $params['novalnetsepa.holder'] ) ? $params['novalnetsepa.holder'] : '' ),
		) + $urls;

		return $data;
	}
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

use Omnipay\Omnipay as OPay;

/**
 * Payment provider for Datatrans
 *
 * @package MShop
 * @subpackage Service
 */
class Datatrans
	extends \Aimeos\MShop\Service\Provider\Payment\OmniPay
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{
	/**
	 * Executes the payment again for the given order if supported.
	 * This requires support of the payment gateway and token based payment
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @return void
	 */
	public function repay( \Aimeos\MShop\Order\Item\Iface $order )
	{
		$base = $this->getOrderBase( $order->getBaseId() );

		if( ( $cfg = $this->getCustomerData( $base->getCustomerId(), 'repay' ) ) === null )
		{
			$msg = sprintf( 'No reoccurring payment data available for customer ID "%1$s"', $base->getCustomerId() );
			throw new \Aimeos\MShop\Service\Exception( $msg );
		}

		if( !isset( $cfg['token'] ) )
		{
			$msg = sprintf( 'No payment token available for customer ID "%1$s"', $base->getCustomerId() );
			throw new \Aimeos\MShop\Service\Exception( $msg );
		}

		$data = array(
			'transactionId' => $order->getId(),
			'currency' => $base->getPrice()->getCurrencyId(),
			'amount' => $this->getAmount( $base->getPrice() ),
			'cardReference' => $cfg['token'],
			'paymentPage' => false,
		);

		if( isset( $cfg['month'] ) && isset( $cfg['year'] ) )
		{
			$data['card'] = new \Omnipay\Common\CreditCard( [
				'expiryMonth' => $cfg['month'],
				'expiryYear' => $cfg['year'],
			] );
		}

		$response = $this->getXmlProvider()->purchase( $data )->send();

		if( $response->isSuccessful() )
		{
			$this->saveTransationRef( $base, $response->getTransactionReference() );
			$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED );
			$this->saveOrder( $order );
		}
		else
		{
			$msg = ( method_exists( $response, 'getMessage' ) ? $response->getMessage() : '' );
			throw new \Aimeos\MShop\Service\Exception( sprintf( 'Token based payment failed: %1$s', $msg ) );
		}
	}

	/**
	 * Returns the value for the given configuration key
	 *
	 * @param string $key Configuration key name
	 * @param mixed $default Default value if no configuration is found
	 * @return mixed Configuration value
	 */
	protected function getValue( $key, $default = null )
	{
		switch( $key ) {
			case 'type': return 'Datatrans';
		}

		return parent::getValue( $key, $default );
	}

	/**
	 * Returns the Datatrans XML payment provider
	 *
	 * @return \Omnipay\Common\GatewayInterface Gateway provider object
	 */
	protected function getXmlProvider()
	{
		$provider = OPay::create('Datatrans\Xml');
		$provider->initialize( $this->getServiceItem()->getConfig() );

		return $provider;
	}
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2015-2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

use Omnipay\Omnipay as OPay;

/**
 * Payment provider for payment gateways supported by the Omnipay library.
 *
 * @package MShop
 * @subpackage Service
 */
class OmniPay
	extends \Aimeos\MShop\Service\Provider\Payment\Base
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{
	private $beConfig = array(
		'type' => array(
			'code' => 'type',
			'internalcode'=> 'type',
			'label'=> 'Payment provider type',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true,
		),
		'onsite' => array(
			'code' => 'onsite',
			'internalcode'=> 'onsite',
			'label'=> 'Collect data locally',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
		'address' => array(
			'code' => 'address',
			'internalcode'=> 'address',
			'label'=> 'Send address to payment gateway too',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
		'authorize' => array(
			'code' => 'authorize',
			'internalcode'=> 'authorize',
			'label'=> 'Authorize payments and capture later',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
		'createtoken' => array(
			'code' => 'createtoken',
			'internalcode'=> 'createtoken',
			'label'=> 'Request token for recurring payments',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '1',
			'required'=> false,
		),
		'testmode' => array(
			'code' => 'testmode',
			'internalcode'=> 'testmode',
			'label'=> 'Test mode without payments',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
	);

	private $feConfig = array(
		'payment.firstname' => array(
			'code' => 'payment.firstname',
			'internalcode'=> 'firstName',
			'label'=> 'First name',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false
		),
		'payment.lastname' => array(
			'code' => 'payment.lastname',
			'internalcode'=> 'lastName',
			'label'=> 'Last name',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
		'payment.cardno' => array(
			'code' => 'payment.cardno',
			'internalcode'=> 'number',
			'label'=> 'Credit card number',
			'type'=> 'number',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> true
		),
		'payment.cvv' => array(
			'code' => 'payment.cvv',
			'internalcode'=> 'cvv',
			'label'=> 'Verification number',
			'type'=> 'number',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> true
		),
		'payment.expirymonth' => array(
			'code' => 'payment.expirymonth',
			'internalcode'=> 'expiryMonth',
			'label'=> 'Expiry month',
			'type'=> 'select',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> true
		),
		'payment.expiryyear' => array(
			'code' => 'payment.expiryyear',
			'internalcode'=> 'expiryYear',
			'label'=> 'Expiry year',
			'type'=> 'select',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> true
		),
		'payment.company' => array(
			'code' => 'payment.company',
			'internalcode'=> 'company',
			'label'=> 'Company',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.address1' => array(
			'code' => 'payment.address1',
			'internalcode'=> 'billingAddress1',
			'label'=> 'Street',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.address2' => array(
			'code' => 'payment.address2',
			'internalcode'=> 'billingAddress2',
			'label'=> 'Additional',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.city' => array(
			'code' => 'payment.city',
			'internalcode'=> 'billingCity',
			'label'=> 'City',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.postal' => array(
			'code' => 'payment.postal',
			'internalcode'=> 'billingPostcode',
			'label'=> 'Zip code',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.state' => array(
			'code' => 'payment.state',
			'internalcode'=> 'billingState',
			'label'=> 'State',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.countryid' => array(
			'code' => 'payment.countryid',
			'internalcode'=> 'billingCountry',
			'label'=> 'Country',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.telephone' => array(
			'code' => 'payment.telephone',
			'internalcode'=> 'billingPhone',
			'label'=> 'Telephone',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
		'payment.email' => array(
			'code' => 'payment.email',
			'internalcode'=> 'email',
			'label'=> 'E-Mail',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false,
			'public' => false,
		),
	);

	private $provider;

	/**
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
	 * rules for the value of each field in the administration interface.
	 *
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
	 */
	public function getConfigBE()
	{
		$list = [];

		foreach( $this->beConfig as $key => $config ) {
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $config );
		}

		return $list;
	}

	/**
	 * Checks the backend configuration attributes for validity.
	 *
	 * @param array $attributes Attributes added by the shop owner in the administraton interface
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
	 * 	known by the provider but aren't valid
	 */
	public function checkConfigBE( array $attributes )
	{
		return array_merge( parent::checkConfigBE( $attributes ), $this->checkConfig( $this->beConfig, $attributes ) );
	}

	/**
	 * Cancels the authorization for the given order if supported.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 */
	public function cancel( \Aimeos\MShop\Order\Item\Iface $order )
	{
		$provider = $this->getProvider();

		if( !$provider->supportsVoid() ) {
			return;
		}

		$base = $this->getOrderBase( $order->getBaseId() );

		$data = array(
			'transactionReference' => $this->getTransactionReference( $base ),
			'currency' => $base->getPrice()->getCurrencyId(),
			'amount' => $this->getAmount( $base->getPrice() ),
			'transactionId' => $order->getId(),
		);

		$response = $provider->void( $data )->send();

		if( $response->isSuccessful() )
		{
			$status = \Aimeos\MShop\Order\Item\Base::PAY_CANCELED;
			$order->setPaymentStatus( $status );
			$this->saveOrder( $order );
		}
	}

	/**
	 * Captures the money later on request for the given order if supported.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 */
	public function capture( \Aimeos\MShop\Order\Item\Iface $order )
	{
		$provider = $this->getProvider();

		if( !$provider->supportsCapture() ) {
			return;
		}

		$base = $this->getOrderBase( $order->getBaseId() );

		$data = array(
			'transactionReference' => $this->getTransactionReference( $base ),
			'currency' => $base->getPrice()->getCurrencyId(),
			'amount' => $this->getAmount( $base->getPrice() ),
			'transactionId' => $order->getId(),
		);

		$response = $provider->capture( $data )->send();

		if( $response->isSuccessful() )
		{
			$status = \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED;
			$order->setPaymentStatus( $status );
		}
	}

	/**
	 * Checks what features the payment provider implements.
	 *
	 * @param integer $what Constant from abstract class
	 * @return boolean True if feature is available in the payment provider, false if not
	 */
	public function isImplemented( $what )
	{
		$provider = $this->getProvider();

		switch( $what )
		{
			case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_CAPTURE:
				return $provider->supportsCapture();
			case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_CANCEL:
				return $provider->supportsVoid();
			case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_REFUND:
				return $provider->supportsRefund();
			case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_REPAY:
				return method_exists( $provider, 'createCard' );
		}

		return false;
	}

	/**
	 * Tries to get an authorization or captures the money immediately for the given order if capturing the money
	 * separately isn't supported or not configured by the shop owner.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Standard Form object with URL, action and parameters to redirect to
	 * 	(e.g. to an external server of the payment provider or to a local success page)
	 */
	public function process( \Aimeos\MShop\Order\Item\Iface $order, array $params = [] )
	{
		if( $this->getValue( 'onsite' ) == true && ( !isset( $params['number'] ) || !isset( $params['cvv'] ) ) ) {
			return $this->getPaymentForm( $order, $params );
		}

		return $this->processOrder( $order, $params );
	}

	/**
	 * Refunds the money for the given order if supported.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 */
	public function refund( \Aimeos\MShop\Order\Item\Iface $order )
	{
		$provider = $this->getProvider();

		if( !$provider->supportsRefund() ) {
			return;
		}

		$base = $this->getOrderBase( $order->getBaseId() );
		$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT;
		$service = $this->getBasketService( $base, $type, $this->getServiceItem()->getCode() );

		$data = array(
			'transactionReference' => $this->getTransactionReference( $base ),
			'currency' => $base->getPrice()->getCurrencyId(),
			'amount' => $this->getAmount( $base->getPrice() ),
			'transactionId' => $order->getId(),
		);

		$response = $provider->refund( $data )->send();

		if( $response->isSuccessful() )
		{
			$attr = array( 'REFUNDID' => $response->getTransactionReference() );
			$this->setAttributes( $service, $attr, 'payment/omnipay' );
			$this->saveOrderBase( $base );

			$status = \Aimeos\MShop\Order\Item\Base::PAY_REFUND;
			$order->setPaymentStatus( $status );
			$this->saveOrder( $order );
		}
	}

	/**
	 * Executes the payment again for the given order if supported.
	 * This requires support of the payment gateway and token based payment
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @return void
	 */
	public function repay( \Aimeos\MShop\Order\Item\Iface $order )
	{
		$base = $this->getOrderBase( $order->getBaseId() );

		if( !$this->isImplemented( \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_REPAY ) ) {
			throw new \Aimeos\MShop\Service\Exception( sprintf( 'Method "%1$s" for provider not available', 'repay' ) );
		}

		if( ( $cfg = $this->getCustomerData( $base->getCustomerId(), 'repay' ) ) === null )
		{
			$msg = sprintf( 'No reoccurring payment data available for customer ID "%1$s"', $base->getCustomerId() );
			throw new \Aimeos\MShop\Service\Exception( $msg );
		}

		if( !isset( $cfg['token'] ) )
		{
			$msg = sprintf( 'No payment token available for customer ID "%1$s"', $base->getCustomerId() );
			throw new \Aimeos\MShop\Service\Exception( $msg );
		}

		$data = array(
			'transactionId' => $order->getId(),
			'currency' => $base->getPrice()->getCurrencyId(),
			'amount' => $this->getAmount( $base->getPrice() ),
			'cardReference' => $cfg['token'],
			'paymentPage' => false,
		);

		if( isset( $cfg['month'] ) && isset( $cfg['year'] ) )
		{
			$data['card'] = new \Omnipay\Common\CreditCard( [
				'expiryMonth' => $cfg['month'],
				'expiryYear' => $cfg['year'],
			] );
		}

		$response = $this->getProvider()->purchase( $data )->send();

		if( $response->isSuccessful() )
		{
			$this->saveTransationRef( $base, $response->getTransactionReference() );
			$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED );
			$this->saveOrder( $order );
		}
		else
		{
			$msg = ( method_exists( $response, 'getMessage' ) ? $response->getMessage() : '' );
			throw new \Aimeos\MShop\Service\Exception( sprintf( 'Token based payment failed: %1$s', $msg ) );
		}
	}

	/**
	 * Updates the order status sent by payment gateway notifications
	 *
	 * @param \Psr\Http\Message\ServerRequestInterface $request Request object
	 * @param \Psr\Http\Message\ResponseInterface $response Response object
	 * @return \Psr\Http\Message\ResponseInterface Response object
	 */
	public function updatePush( \Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response )
	{
		try
		{
			$provider = $this->getProvider();
			$params = $request->getQueryParams();

			if( !isset( $params['orderid'] ) ) {
				throw new \Aimeos\MShop\Service\Exception( 'No order ID available' );
			}

			if( !method_exists( $provider, 'supportsAcceptNotification' ) || !$provider->supportsAcceptNotification() ) {
				return; // call updateOrderSync()
			}

			$order = $this->getOrder( $params['orderid'] );
			$omniRequest = $provider->acceptNotification();
			$omniResponse = $omniRequest->send();

			if( method_exists( $omniResponse, 'isSuccessful' ) && $omniResponse->isSuccessful() )
			{
				$order->setPaymentStatus( $this->translateStatus( $omniRequest->getTransactionStatus() ) );
			}
			elseif( method_exists( $omniResponse, 'isPending' ) && $omniResponse->isPending() )
			{
				$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_PENDING );
			}
			elseif( method_exists( $omniResponse, 'isCancelled' ) && $omniResponse->isCancelled() )
			{
				$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_CANCELED );
			}
			else
			{
				$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_REFUSED );
				$this->saveOrder( $order );

				throw new \Aimeos\MShop\Service\Exception( $omniResponse->getMessage() );
			}

			$base = $this->getOrderBase( $order->getBaseId() );
			$this->saveTransationRef( $base, $omniResponse->getTransactionReference() );
			$this->saveRepayData( $omniResponse, $base->getCustomerId() );
			$this->saveOrder( $order );

			$response->withStatus( 200 );
		}
		catch( \Exception $e )
		{
			$response->withStatus( 500, $e->getMessage() );
		}

		return $response;
	}

	/**
	 * Updates the orders for whose status updates have been received by the confirmation page
	 *
	 * @param \Psr\Http\Message\ServerRequestInterface $request Request object with parameters and request body
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order item that should be updated
	 * @return \Aimeos\MShop\Order\Item\Iface Updated order item
	 * @throws \Aimeos\MShop\Service\Exception If updating the orders failed
	 */
	public function updateSync( \Psr\Http\Message\ServerRequestInterface $request, \Aimeos\MShop\Order\Item\Iface $order )
	{
		try
		{
			$provider = $this->getProvider();
			$base = $this->getOrderBase( $order->getBaseId() );

			$params = (array) $request->getAttributes() + (array) $request->getParsedBody() + (array) $request->getQueryParams();
			$params['transactionId'] = $order->getId();
			$params['transactionReference'] = $this->getTransactionReference( $base );
			$params['amount'] = $this->getAmount( $base->getPrice() );
			$params['currency'] = $base->getLocale()->getCurrencyId();
			$params['createCard'] = true;

			if( $this->getValue( 'authorize', false ) && $provider->supportsCompleteAuthorize() )
			{
				$response = $provider->completeAuthorize( $params )->send();
				$status = \Aimeos\MShop\Order\Item\Base::PAY_AUTHORIZED;
			}
			elseif( $provider->supportsCompletePurchase() )
			{
				$response = $provider->completePurchase( $params )->send();
				$status = \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED;
			}
			else
			{
				return $order;
			}

			if( $response->getTransactionId() != $order->getId() ) {
				return $order;
			}

			if( method_exists( $response, 'isSuccessful' ) && $response->isSuccessful() )
			{
				$order->setPaymentStatus( $status );
			}
			elseif( method_exists( $response, 'isPending' ) && $response->isPending() )
			{
				$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_PENDING );
			}
			elseif( method_exists( $response, 'isCancelled' ) && $response->isCancelled() )
			{
				$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_CANCELED );
			}
			elseif( method_exists( $response, 'isRedirect' ) && $response->isRedirect() )
			{
				$url = $response->getRedirectUrl();
				throw new \Aimeos\MShop\Service\Exception( sprintf( 'Unexpected redirect: %1$s', $url ) );
			}
			else
			{
				$this->saveOrder( $order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_REFUSED ) );
				throw new \Aimeos\MShop\Service\Exception( $response->getMessage() );
			}

			$this->saveTransationRef( $base, $response->getTransactionReference() );
			$this->saveRepayData( $response, $base->getCustomerId() );
			$this->saveOrder( $order );
		}
		catch( \Exception $e )
		{
			throw new \Aimeos\MShop\Service\Exception( $e->getMessage() );
		}

		return $order;
	}

	/**
	 * Returns an Omnipay credit card object
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Order base object with addresses and services
	 * @param array $params POST parameters passed to the provider
	 * @return \Omnipay\Common\CreditCard Credit card object
	 */
	protected function getCardDetails( \Aimeos\MShop\Order\Item\Base\Iface $base, array $params )
	{
		if( $this->getValue( 'address' ) )
		{
			$addresses = $base->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

			if( ( $addr = current( $addresses ) ) !== false )
			{
				$params['billingName'] = $addr->getFirstname() . ' ' . $addr->getLastname();
				$params['billingFirstName'] = $addr->getFirstname();
				$params['billingLastName'] = $addr->getLastname();
				$params['billingCompany'] = $addr->getCompany();
				$params['billingAddress1'] = $addr->getAddress1();
				$params['billingAddress2'] = $addr->getAddress2();
				$params['billingCity'] = $addr->getCity();
				$params['billingPostcode'] = $addr->getPostal();
				$params['billingState'] = $addr->getState();
				$params['billingCountry'] = $addr->getCountryId();
				$params['billingPhone'] = $addr->getTelephone();
				$params['billingFax'] = $addr->getTelefax();
				$params['email'] = $addr->getEmail();

				$type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_DELIVERY;
				$addr = current( $base->getAddress( $type ) ) ?: $addr;

				$params['shippingName'] = $addr->getFirstname() . ' ' . $addr->getLastname();
				$params['shippingFirstName'] = $addr->getFirstname();
				$params['shippingLastName'] = $addr->getLastname();
				$params['shippingCompany'] = $addr->getCompany();
				$params['shippingAddress1'] = $addr->getAddress1();
				$params['shippingAddress2'] = $addr->getAddress2();
				$params['shippingCity'] = $addr->getCity();
				$params['shippingPostcode'] = $addr->getPostal();
				$params['shippingState'] = $addr->getState();
				$params['shippingCountry'] = $addr->getCountryId();
				$params['shippingPhone'] = $addr->getTelephone();
				$params['shippingFax'] = $addr->getTelefax();
			}
		}

		return new \Omnipay\Common\CreditCard( $params );
	}

	/**
	 * Returns the data passed to the Omnipay library
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Basket object
	 * @param $orderid string Unique order ID
	 * @param array $params Request parameter if available
	 */
	protected function getData( \Aimeos\MShop\Order\Item\Base\Iface $base, $orderid, array $params )
	{
		$addresses = $base->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

		if( ( $address = current( $addresses ) ) === false ) {
			$langid = $this->getContext()->getLocale()->getLanguageId();
		} else {
			$langid = $address->getLanguageId();
		}

		$data = array(
			'language' => $langid,
			'transactionId' => $orderid,
			'amount' => $this->getAmount( $base->getPrice() ),
			'currency' => $base->getLocale()->getCurrencyId(),
			'description' => sprintf( $this->getContext()->getI18n()->dt( 'mshop', 'Order %1$s' ), $orderid ),
			'clientIp' => $this->getValue( 'client.ipaddress' ),
		);

		if( $this->getValue( 'createtoken', false ) ) {
			$data['createCard'] = true;
		}

		if( $this->getValue( 'onsite', false ) || $this->getValue( 'address', false ) ) {
			$data['card'] = $this->getCardDetails( $base, $params );
		}

		return $data + $this->getPaymentUrls();
	}

	/**
	 * Returns the Omnipay gateway provider object.
	 *
	 * @return \Omnipay\Common\GatewayInterface Gateway provider object
	 */
	protected function getProvider()
	{
		if( !isset( $this->provider ) )
		{
			$this->provider = OPay::create( $this->getValue( 'type' ) );
			$this->provider->setTestMode( (bool) $this->getValue( 'testmode', false ) );
			$this->provider->initialize( $this->getServiceItem()->getConfig() );
		}

		return $this->provider;
	}

	/**
	 * Returns the required URLs
	 *
	 * @return array List of the Omnipay URL name as key and the URL string as value
	 */
	protected function getPaymentUrls()
	{
		return array(
			'returnUrl' => $this->getConfigValue( array( 'payment.url-success' ) ),
			'cancelUrl' => $this->getConfigValue( array( 'payment.url-cancel', 'payment.url-success' ) ),
			'notifyUrl' => $this->getConfigValue( array( 'payment.url-update' ) ),
		);
	}

	/**
	 * Returns the value for the given configuration key
	 *
	 * @param string $key Configuration key name
	 * @param mixed $default Default value if no configuration is found
	 * @return mixed Configuration value
	 */
	protected function getValue( $key, $default = null )
	{
		return $this->getConfigValue( $key, $default );
	}

	/**
	 * Returns the payment form for entering payment details at the shop site.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Iface Form helper object
	 */
	protected function getPaymentForm( \Aimeos\MShop\Order\Item\Iface $order, array $params )
	{
		$list = [];
		$feConfig = $this->feConfig;
		$baseItem = $this->getOrderBase( $order->getBaseId(), \Aimeos\MShop\Order\Item\Base\Base::PARTS_ADDRESS );
		$addresses = $baseItem->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

		if( ( $address = current( $addresses ) ) !== false )
		{
			if( !isset( $params[$feConfig['payment.firstname']['internalcode']] )
				|| $params[$feConfig['payment.firstname']['internalcode']] == ''
			) {
				$feConfig['payment.firstname']['default'] = $address->getFirstname();
			}

			if( !isset( $params[$feConfig['payment.lastname']['internalcode']] )
				|| $params[$feConfig['payment.lastname']['internalcode']] == ''
			) {
				$feConfig['payment.lastname']['default'] = $address->getLastname();
			}

			if( $this->getValue( 'address' ) )
			{
				$feConfig['payment.address1']['default'] = $address->getAddress1();
				$feConfig['payment.address2']['default'] = $address->getAddress2();
				$feConfig['payment.city']['default'] = $address->getCity();
				$feConfig['payment.postal']['default'] = $address->getPostal();
				$feConfig['payment.state']['default'] = $address->getState();
				$feConfig['payment.countryid']['default'] = $address->getCountryId();
				$feConfig['payment.telephone']['default'] = $address->getTelephone();
				$feConfig['payment.company']['default'] = $address->getCompany();
				$feConfig['payment.email']['default'] = $address->getEmail();
			}
		}

		$year = date( 'Y' );
		$feConfig['payment.expirymonth']['default'] = array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 );
		$feConfig['payment.expiryyear']['default'] = array( $year, $year + 1, $year + 2, $year + 3, $year + 4, $year + 5, $year + 6, $year + 7 );

		foreach( $feConfig as $key => $config ) {
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $config );
		}

		$url = $this->getConfigValue( array( 'payment.url-self' ) );
		return new \Aimeos\MShop\Common\Helper\Form\Standard( $url, 'POST', $list, false );
	}

	/**
	 * Returns the form for redirecting customers to the payment gateway.
	 *
	 * @param \Omnipay\Common\Message\RedirectResponseInterface $response Omnipay response object
	 * @return \Aimeos\MShop\Common\Helper\Form\Iface Form helper object
	 */
	protected function getRedirectForm( \Omnipay\Common\Message\RedirectResponseInterface $response )
	{
		$list = [];

		foreach( (array) $response->getRedirectData() as $key => $value )
		{
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( array(
				'label' => $key,
				'code' => $key,
				'type' => 'string',
				'internalcode' => $key,
				'internaltype' => 'string',
				'default' => $value,
				'public' => false,
			) );
		}

		$url = $response->getRedirectUrl();
		$method = $response->getRedirectMethod();

		return new \Aimeos\MShop\Common\Helper\Form\Standard( $url, $method, $list );
	}

	/**
	 * Returns the payment transaction ID stored in the basket
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Basket including (payment) service items
	 * @return string|null Payment transaction ID or null if not available
	 */
	protected function getTransactionReference( \Aimeos\MShop\Order\Item\Base\Iface $base )
	{
		$code = $this->getServiceItem()->getCode();
		$service = $base->getService( \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT, $code );

		return $service->getAttribute( 'TRANSACTIONID', 'payment/omnipay' );
	}

	/**
	 * Tries to get an authorization or captures the money immediately for the given order if capturing the money
	 * separately isn't supported or not configured by the shop owner.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Standard Form object with URL, action and parameters to redirect to
	 * 	(e.g. to an external server of the payment provider or to a local success page)
	 */
	protected function processOrder( \Aimeos\MShop\Order\Item\Iface $order, array $params = [] )
	{
		$parts = \Aimeos\MShop\Order\Item\Base\Base::PARTS_SERVICE
			| \Aimeos\MShop\Order\Item\Base\Base::PARTS_PRODUCT
			| \Aimeos\MShop\Order\Item\Base\Base::PARTS_ADDRESS;

		$base = $this->getOrderBase( $order->getBaseId(), $parts );
		$data = $this->getData( $base, $order->getId(), $params );
		$urls = $this->getPaymentUrls();

		try
		{
			$provider = $this->getProvider();

			if( $this->getValue( 'authorize', false ) && $provider->supportsAuthorize() )
			{
				$response = $provider->authorize( $data )->send();
				$status = \Aimeos\MShop\Order\Item\Base::PAY_AUTHORIZED;
			}
			else
			{
				$response = $provider->purchase( $data )->send();
				$status = \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED;
			}

			if( $response->isSuccessful() )
			{
				$this->saveTransationRef( $base, $response->getTransactionReference() );

				$order->setPaymentStatus( $status );
				$this->saveOrder( $order );
			}
			elseif( $response->isRedirect() )
			{
				if( ( $ref = $response->getTransactionReference() ) != null ) {
					$this->saveTransationRef( $base, $ref );
				}

				return $this->getRedirectForm( $response );
			}
			else
			{
				$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_REFUSED );
				$this->saveOrder( $order );

				throw new \Aimeos\MShop\Service\Exception( $response->getMessage() );
			}
		}
		catch( \Exception $e )
		{
			throw new \Aimeos\MShop\Service\Exception( $e->getMessage() );
		}

		return new \Aimeos\MShop\Common\Helper\Form\Standard( $urls['returnUrl'], 'POST', [] );
	}

	/**
	 * Saves the required data for recurring payments in the customer profile
	 *
	 * @param \Omnipay\Common\Message\ResponseInterface $response Omnipay response object
	 * @param string $customerId Unique customer ID
	 */
	protected function saveRepayData( \Omnipay\Common\Message\ResponseInterface $response, $customerId )
	{
		$data = [];

		if( method_exists( $response, 'getCardReference' ) ) {
			$data['token'] = $response->getCardReference();
		}

		if( method_exists( $response, 'getExpiryMonth' ) ) {
			$data['month'] = $response->getExpiryMonth();
		}

		if( method_exists( $response, 'getExpiryYear' ) ) {
			$data['year'] = $response->getExpiryYear();
		}

		if( !empty( $data ) ) {
			$this->setCustomerData( $customerId, 'repay', $data );
		}
	}

	/**
	 * Adds the transation reference to the order service attributes.
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $baseItem Order base object with service items attached
	 * @param string $ref Transaction reference from the payment gateway
	 */
	protected function saveTransationRef( \Aimeos\MShop\Order\Item\Base\Iface $baseItem, $ref )
	{
		$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT;
		$serviceItem = $this->getBasketService( $baseItem, $type, $this->getServiceItem()->getCode() );

		$attr = array( 'TRANSACTIONID' => $ref );
		$this->setAttributes( $serviceItem, $attr, 'payment/omnipay' );
		$this->saveOrderBase( $baseItem );
	}

	/**
	 * Translates the Omnipay status into the Aimeos payment status value
	 *
	 * @param string $status Omnipay payment status
	 * @return integer|null Aimeos payment status value or null for no new status
	 */
	protected function translateStatus( $status )
	{
		if( !interface_exists( '\Omnipay\Common\Message\NotificationInterface' ) ) {
			return \Aimeos\MShop\Order\Item\Base::PAY_REFUSED;
		}

		switch( $status )
		{
			case \Omnipay\Common\Message\NotificationInterface::STATUS_COMPLETED:
				return \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED;
			case \Omnipay\Common\Message\NotificationInterface::STATUS_PENDING:
				return \Aimeos\MShop\Order\Item\Base::PAY_PENDING;
			case \Omnipay\Common\Message\NotificationInterface::STATUS_FAILED:
				return \Aimeos\MShop\Order\Item\Base::PAY_REFUSED;
		}
	}
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2016-2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

/**
 * Novalnet credit card payment provider
 *
 * @package MShop
 * @subpackage Service
 */
class NovalnetCredit
	extends \Aimeos\MShop\Service\Provider\Payment\OmniPay
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{
	private $feConfig = array(
		'novalnetcredit.holder' => array(
			'code' => 'novalnetcredit.holder',
			'internalcode'=> 'ccholder',
			'label'=> 'Credit card owner',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
		'novalnetcredit.number' => array(
			'code' => 'novalnetcredit.number',
			'internalcode'=> 'ccnumber',
			'label'=> 'Credit card number',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
		'novalnetcredit.year' => array(
			'code' => 'novalnetcredit.year',
			'internalcode'=> 'ccyear',
			'label'=> 'Expiry year',
			'type'=> 'select',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> true
		),
		'novalnetcredit.month' => array(
			'code' => 'novalnetcredit.month',
			'internalcode'=> 'ccmonth',
			'label'=> 'Expiry month',
			'type'=> 'select',
			'internaltype'=> 'integer',
			'default'=> array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ),
			'required'=> true
		),
		'novalnetcredit.cvv' => array(
			'code' => 'novalnetcredit.cvv',
			'internalcode'=> 'cccvv',
			'label'=> 'CVC code',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true
		),
	);

	/**
	 * Initializes the service provider object.
	 *
	 * @param \Aimeos\MShop\Context\Item\Iface $context Context object with required objects
	 * @param \Aimeos\MShop\Service\Item\Iface $serviceItem Service item with configuration for the provider
	 */
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context, \Aimeos\MShop\Service\Item\Iface $serviceItem )
	{
		parent::__construct( $context, $serviceItem );

		$year = date( 'Y' );
		$this->feConfig['novalnetcredit.year']['default'] = array( $year, $year + 1, $year + 2, $year + 3, $year + 4, $year + 5, $year + 6, $year + 7 );
	}

	/**
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
	 * rules for the value of each field in the frontend.
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
	 */
	public function getConfigFE( \Aimeos\MShop\Order\Item\Base\Iface $basket )
	{
		$list = [];
		$feconfig = $this->feConfig;

		try
		{
			$code = $this->getServiceItem()->getCode();
			$service = $basket->getService( \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT, $code );

			foreach( $service->getAttributeItems() as $item )
			{
				if( isset( $feconfig[$item->getCode()] ) )
				{
					if( is_array( $feconfig[$item->getCode()]['default'] ) ) {
						$feconfig[$item->getCode()]['default'] = array_merge( array( $item->getValue() ), $feconfig[$item->getCode()]['default'] );
					} else {
						$feconfig[$item->getCode()]['default'] = $item->getValue();
					}
				}
			}
		}
		catch( \Aimeos\MShop\Order\Exception $e ) {; } // If payment isn't available yet

		$addresses = $basket->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

		if( ( $address = current( $addresses ) ) !== false )
		{
			if( $feconfig['novalnetcredit.holder']['default'] == ''
				&& ( $fn = $address->getFirstname() ) !== '' && ( $ln = $address->getLastname() ) !== ''
			) {
				$feconfig['novalnetcredit.holder']['default'] = $fn . ' ' . $ln;
			}
		}

		foreach( $feconfig as $key => $config ) {
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $config );
		}

		return $list;
	}

	/**
	 * Checks the frontend configuration attributes for validity.
	 *
	 * @param array $attributes Attributes entered by the customer during the checkout process
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
	 * 	known by the provider but aren't valid resp. null for attributes whose values are OK
	 */
	public function checkConfigFE( array $attributes )
	{
		return $this->checkConfig( $this->feConfig, $attributes );
	}

	/**
	 * Sets the payment attributes in the given service.
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $orderServiceItem Order service item that will be added to the basket
	 * @param array $attributes Attribute key/value pairs entered by the customer during the checkout process
	 */
	public function setConfigFE( \Aimeos\MShop\Order\Item\Base\Service\Iface $orderServiceItem, array $attributes )
	{
		$this->setAttributes( $orderServiceItem, $attributes, 'session' );
	}

	/**
	 * Tries to get an authorization or captures the money immediately for the given order if capturing the money
	 * separately isn't supported or not configured by the shop owner.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Standard Form object with URL, action and parameters to redirect to
	 * 	(e.g. to an external server of the payment provider or to a local success page)
	 */
	public function process( \Aimeos\MShop\Order\Item\Iface $order, array $params = [] )
	{
		return $this->processOrder( $order, $params );
	}

	/**
	 * Returns an Omnipay credit card object
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Order base object with addresses and services
	 * @param array $params POST parameters passed to the provider
	 * @return \Omnipay\Common\CreditCard Credit card object
	 */
	protected function getCardDetails( \Aimeos\MShop\Order\Item\Base\Iface $base, array $params )
	{
		$addresses = $base->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT );

		if( $this->getValue( 'address' ) && ( $addr = current( $addresses ) ) !== false )
		{
			$params['billingName'] = $addr->getFirstname() . ' ' . $addr->getLastname();
			$params['billingFirstName'] = $addr->getFirstname();
			$params['billingLastName'] = $addr->getLastname();
			$params['billingCompany'] = $addr->getCompany();
			$params['billingAddress1'] = $addr->getAddress1();
			$params['billingAddress2'] = $addr->getAddress2();
			$params['billingCity'] = $addr->getCity();
			$params['billingPostcode'] = $addr->getPostal();
			$params['billingState'] = $addr->getState();
			$params['billingCountry'] = $addr->getCountryId();
			$params['billingPhone'] = $addr->getTelephone();
			$params['billingFax'] = $addr->getTelefax();
			$params['email'] = $addr->getEmail();
		}

		$params['holder'] = ( isset( $params['novalnetcredit.holder'] ) ? $params['novalnetcredit.holder'] : '' );
		$params['number'] = ( isset( $params['novalnetcredit.number'] ) ? $params['novalnetcredit.number'] : '' );
		$params['expiryYear'] = ( isset( $params['novalnetcredit.year'] ) ? $params['novalnetcredit.year'] : '' );
		$params['expiryMonth'] = ( isset( $params['novalnetcredit.month'] ) ? $params['novalnetcredit.month'] : '' );
		$params['cvv'] = ( isset( $params['novalnetcredit.cvv'] ) ? $params['novalnetcredit.cvv'] : '' );

		return new \Omnipay\Common\CreditCard( $params );
	}
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2017-2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

/**
 * Payone payment provider
 *
 * @package MShop
 * @subpackage Service
 */
class Payone
	extends \Aimeos\MShop\Service\Provider\Payment\OmniPay
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{
	/**
	 * Returns the data passed to the Omnipay library
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Basket object
	 * @param $orderid Unique order ID
	 * @param array $params Request parameter if available
	 */
	protected function getData( \Aimeos\MShop\Order\Item\Base\Iface $base, $orderid, array $params )
	{
		$lines = [];

		foreach( $base->getProducts() as $product )
		{
			$list = $product->toArray();

			$lines[] = new \Omnipay\Payone\Extend\Item([
				'id' => $list['order.base.product.prodcode'],
				'name' => $product->getName(),
				'itemType' => 'goods', // Available types: goods, shipping etc.
				'quantity' => $product->getQuantity(),
				'price' => $product->getPrice()->getValue(),
				'vat' => (int) $product->getPrice()->getTaxRate(),
			]);
		}

		foreach( $base->getService( 'delivery' ) as $delivery )
		{
			if( $delivery->getPrice()->getCosts() != '0.00' )
			{
				$lines[] = new \Omnipay\Payone\Extend\Item([
					'id' => $delivery->getId(),
					'name' => $delivery->getName(),
					'itemType' => 'shipment',
					'quantity' => 1,
					'price' => $delivery->getPrice()->getCosts(),
					'vat' => (int) $delivery->getPrice()->getTaxRate(),
				]);
			}
		}

		$data = parent::getData( $base, $orderid, $params );
		$data['items'] = new \Omnipay\Common\ItemBag( $lines );
		$data['accessMethod'] = 'classic';

		return $data;
	}

	/**
	 * Updates the order status sent by payment gateway notifications
	 *
	 * @param \Psr\Http\Message\ServerRequestInterface Request object
	 * @return \Psr\Http\Message\ResponseInterface Response object
	 */
	public function updatePush( \Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response )
	{
		$params = (array) $request->getAttributes() + (array) $request->getParsedBody() + (array) $request->getQueryParams();

		if( isset( $params['reference'] ) )
		{
			$response = parent::updatePush( $request, $response );
			$response = $response->withBody( $response->createStreamFromString( 'TSOK' ) ); // payment update successful
		}

		return $response;
	}
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2015-2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

/**
 * Payment provider for Authorize.NET SIM.
 *
 * @package MShop
 * @subpackage Service
 */
class AuthorizeSIM
	extends \Aimeos\MShop\Service\Provider\Payment\OmniPay
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{
	private $beConfig = array(
		'payment.url-success' => array(
			'code' => 'payment.url-success',
			'internalcode'=> 'payment.url-success',
			'label'=> 'URL to confirm page',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true,
		),
	);

	/**
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
	 * rules for the value of each field in the administration interface.
	 *
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
	 */
	public function getConfigBE()
	{
		$list = parent::getConfigBE();

		foreach( $this->beConfig as $key => $config )
		{
			$config['code'] = $config['code'];
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $config );
		}

		return $list;
	}

	/**
	 * Checks the backend configuration attributes for validity.
	 *
	 * @param array $attributes Attributes added by the shop owner in the administraton interface
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
	 * 	known by the provider but aren't valid
	 */
	public function checkConfigBE( array $attributes )
	{
		return array_merge( parent::checkConfigBE( $attributes ), $this->checkConfig( $this->beConfig, $attributes ) );
	}

	/**
	 * Updates the order status sent by payment gateway notifications
	 *
	 * @param \Psr\Http\Message\ServerRequestInterface Request object
	 * @return \Psr\Http\Message\ResponseInterface Response object
	 */
	public function updatePush( \Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response )
	{
		$params = (array) $request->getAttributes() + (array) $request->getParsedBody() + (array) $request->getQueryParams();

		if( isset( $params['x_MD5_Hash'] ) )
		{
			$url = $this->getConfigValue( array( 'payment.url-success' ) );
			$output = sprintf( $this->getValue( 'body', 'success' ), $url );

			$response = parent::updatePush( $request, $response );
			$response = $response->withBody( $response->createStreamFromString( $output ) );
			$response = $response->withHeader( 'Location', $url );
		}

		return $response;
	}

	/**
	 * Returns the value for the given configuration key
	 *
	 * @param string $key Configuration key name
	 * @param mixed $default Default value if no configuration is found
	 * @return mixed Configuration value
	 */
	protected function getValue( $key, $default = null )
	{
		switch( $key ) {
			case 'type': return 'AuthorizeNet_SIM';
		}

		return parent::getValue( $key, $default );
	}
}


<?php

/**
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2015-2018
 * @package MShop
 * @subpackage Service
 */

namespace Aimeos\MShop\Service\Provider\Payment;

use Omnipay\Omnipay as OPay;

/**
 * Payment provider for Stripe.
 *
 * @package MShop
 * @subpackage Service
 */
class Stripe
	extends \Aimeos\MShop\Service\Provider\Payment\OmniPay
	implements \Aimeos\MShop\Service\Provider\Payment\Iface
{

	private $beConfig = array(
		'address' => array(
			'code' => 'address',
			'internalcode'=> 'address',
			'label'=> 'Send address to payment gateway too',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
		'authorize' => array(
			'code' => 'authorize',
			'internalcode'=> 'authorize',
			'label'=> 'Authorize payments and capture later',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
		'testmode' => array(
			'code' => 'testmode',
			'internalcode'=> 'testmode',
			'label'=> 'Test mode without payments',
			'type'=> 'boolean',
			'internaltype'=> 'boolean',
			'default'=> '0',
			'required'=> false,
		),
		'apiKey' => array(
			'code' => 'apiKey',
			'internalcode'=> 'apiKey',
			'label'=> 'API key',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true,
		),
		'publishableKey' => array(
			'code' => 'publishableKey',
			'internalcode'=> 'publishableKey',
			'label'=> 'Publishable key',
			'type'=> 'string',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> true,
		),
	);

	protected $feConfig = array(

		'paymenttoken' => array(
			'code' => 'paymenttoken',
			'internalcode' => 'paymenttoken',
			'label' => 'Authentication token',
			'type' => 'string',
			'internaltype' => 'integer',
			'default' => '',
			'required' => true,
			'public' => false,
		),

		'payment.cardno' => array(
			'code' => 'payment.cardno',
			'internalcode'=> 'number',
			'label'=> 'Credit card number',
			'type'=> 'container',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> false
		),
		'payment.expiry' => array(
			'code' => 'payment.expiry',
			'internalcode'=> 'expiry',
			'label'=> 'Expiry',
			'type'=> 'container',
			'internaltype'=> 'string',
			'default'=> '',
			'required'=> false
		),
		'payment.cvv' => array(
			'code' => 'payment.cvv',
			'internalcode'=> 'cvv',
			'label'=> 'Verification number',
			'type'=> 'container',
			'internaltype'=> 'integer',
			'default'=> '',
			'required'=> false
		),
	);

	private $provider;

	/**
	 * Checks the frontend configuration attributes for validity.
	 *
	 * @param array $attributes Attributes entered by the customer during the checkout process
	 * @return array An array with the attribute keys as key and an error message as values for all attributes that are
	 *    known by the provider but aren't valid resp. null for attributes whose values are OK
	 */
	public function checkConfigFE( array $attributes )
	{
		return [];
	}

	/**
	 * Returns the configuration attribute definitions of the provider to generate a list of available fields and
	 * rules for the value of each field in the frontend.
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
	 * @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface
	 */
	public function getConfigFE( \Aimeos\MShop\Order\Item\Base\Iface $basket )
	{
		return [];
	}

	/**
	 * Tries to get an authorization or captures the money immediately for the given order if capturing the money
	 * separately isn't supported or not configured by the shop owner.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Standard Form object with URL, action and parameters to redirect to
	 *    (e.g. to an external server of the payment provider or to a local success page)
	 */
	public function process( \Aimeos\MShop\Order\Item\Iface $order, array $params = [] )
	{
		if( !isset( $params['paymenttoken'] ) ) {
			return $this->getPaymentForm( $order, $params );
		}

		return $this->processOrder( $order, $params );
	}

	/**
	 * Returns the data passed to the Omnipay library
	 *
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $base Basket object
	 * @param $orderid Unique order ID
	 * @param array $params Request parameter if available
	 */
	protected function getData( \Aimeos\MShop\Order\Item\Base\Iface $base, $orderid, array $params )
	{
		$data = parent::getData( $base, $orderid, $params );

		if( isset( $params['paymenttoken'] ) ) {
			$data['token'] = $params['paymenttoken'];
		}

		return $data;
	}

	/**
	 * Returns the payment form for entering payment details at the shop site.
	 *
	 * @param \Aimeos\MShop\Order\Item\Iface $order Order object
	 * @param array $params Request parameter if available
	 * @return \Aimeos\MShop\Common\Helper\Form\Iface Form helper object
	 */
	protected function getPaymentForm( \Aimeos\MShop\Order\Item\Iface $order, array $params )
	{
		$list = [];
		$feConfig = $this->feConfig;

		foreach( $feConfig as $key => $config ) {
			$list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $config );
		}

		$url = $this->getConfigValue( 'payment.url-self' );
		return new \Aimeos\MShop\Common\Helper\Form\Standard( $url, 'POST', $list, false, $this->getStripeJs() );
	}

	/**
	 * Returns the Omnipay gateway provider object.
	 *
	 * @return \Omnipay\Common\GatewayInterface Gateway provider object
	 */
	protected function getProvider()
	{
		$config = $this->getServiceItem()->getConfig();
		$config['apiKey'] = $this->getServiceItem()->getConfigValue( 'stripe.apiKey' );

		if( !isset( $this->provider ) )
		{
			$this->provider = OPay::create( 'Stripe' );
			$this->provider->setTestMode( (bool) $this->getValue( 'testmode', false ) );
			$this->provider->initialize( $config );
		}

		return $this->provider;
	}

	/**
	 * Returns the required Javascript code for Stripe payment form
	 *
	 * @return string Stripe JS code
	 */
	protected function getStripeJs()
	{
		return '

';
	}

	/**
	 * Returns the value for the given configuration key
	 *
	 * @param string $key Configuration key name
	 * @param mixed $default Default value if no configuration is found
	 * @return mixed Configuration value
	 */
	protected function getValue( $key, $default = null )
	{
		switch( $key )
		{
			case 'type': return 'Stripe';
			case 'onsite': return true;
		}

		return parent::getValue( $key, $default );
	}
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Layouts;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Type;
use eZ\Publish\API\Repository\Values\Content\Location;
use Netgen\BlockManager\API\Service\LayoutService;
use Netgen\BlockManager\API\Values\Layout\Layout;
use Netgen\BlockManager\API\Values\Value;
use Netgen\BlockManager\Version;
use PDO;

class RelatedLayoutsLoader
{
    /**
     * @var \Netgen\BlockManager\API\Service\LayoutService
     */
    protected $layoutService;

    /**
     * @var \Doctrine\DBAL\Connection
     */
    protected $databaseConnection;

    public function __construct(
        LayoutService $layoutService,
        Connection $databaseConnection
    ) {
        $this->layoutService = $layoutService;
        $this->databaseConnection = $databaseConnection;
    }

    /**
     * Returns all layouts related to provided location and its content, sorted by name.
     *
     * Related layout is a layout where the location or its content are referenced by
     * a manual item in one of the block collections.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
     *
     * @return \Netgen\BlockManager\API\Values\Layout\Layout[]
     */
    public function loadRelatedLayouts(Location $location)
    {
        $query = $this->databaseConnection->createQueryBuilder();

        $valueColumnName = class_exists('Netgen\BlockManager\Version') && Version::VERSION_ID < 1100
            ? 'value_id' :
            'value';

        $query->select('DISTINCT b.layout_id')
            ->from('ngbm_collection_item', 'ci')
            ->innerJoin(
                'ci',
                'ngbm_block_collection',
                'bc',
                $query->expr()->andX(
                    $query->expr()->eq('bc.collection_id', 'ci.collection_id'),
                    $query->expr()->eq('bc.collection_status', 'ci.status')
                )
            )
            ->innerJoin(
                'bc',
                'ngbm_block',
                'b',
                $query->expr()->andX(
                    $query->expr()->eq('b.id', 'bc.block_id'),
                    $query->expr()->eq('b.status', 'bc.block_status')
                )
            )
            ->where(
                $query->expr()->andX(
                    $query->expr()->orX(
                        $query->expr()->andX(
                            $query->expr()->eq('ci.value_type', ':content_value_type'),
                            $query->expr()->eq('ci.' . $valueColumnName, ':content_id')
                        ),
                        $query->expr()->andX(
                            $query->expr()->eq('ci.value_type', ':location_value_type'),
                            $query->expr()->eq('ci.' . $valueColumnName, ':location_id')
                        )
                    ),
                    $query->expr()->eq('ci.status', ':status')
                )
            )
            ->setParameter('status', Value::STATUS_PUBLISHED, Type::INTEGER)
            ->setParameter('content_value_type', 'ezcontent', Type::STRING)
            ->setParameter('location_value_type', 'ezlocation', Type::STRING)
            ->setParameter('content_id', $location->contentInfo->id, Type::INTEGER)
            ->setParameter('location_id', $location->id, Type::INTEGER);

        $relatedLayouts = array_map(
            function (array $dataRow) {
                return $this->layoutService->loadLayout($dataRow['layout_id']);
            },
            $query->execute()->fetchAll(PDO::FETCH_ASSOC)
        );

        usort(
            $relatedLayouts,
            function (Layout $layout1, Layout $layout2) {
                if ($layout1->getName() === $layout2->getName()) {
                    return 0;
                }

                return $layout1->getName() > $layout2->getName() ? 1 : -1;
            }
        );

        return $relatedLayouts;
    }
}


<?php

namespace Netgen\Bundle\AdminUIBundle\Helper;

use eZ\Publish\API\Repository\Exceptions\UnauthorizedException;
use eZ\Publish\API\Repository\LocationService;
use eZ\Publish\Core\Helper\TranslationHelper;
use Symfony\Component\Routing\RouterInterface;

class PathHelper
{
    /**
     * @var \eZ\Publish\API\Repository\LocationService
     */
    protected $locationService;

    /**
     * @var \eZ\Publish\Core\Helper\TranslationHelper
     */
    protected $translationHelper;

    /**
     * @var \Symfony\Component\Routing\RouterInterface
     */
    protected $router;

    /**
     * @var int|string
     */
    protected $rootLocationId;

    /**
     * Constructor.
     *
     * @param \eZ\Publish\API\Repository\LocationService $locationService
     * @param \eZ\Publish\Core\Helper\TranslationHelper $translationHelper
     * @param \Symfony\Component\Routing\RouterInterface $router
     */
    public function __construct(
        LocationService $locationService,
        TranslationHelper $translationHelper,
        RouterInterface $router
    ) {
        $this->locationService = $locationService;
        $this->translationHelper = $translationHelper;
        $this->router = $router;
    }

    /**
     * Sets the root location ID.
     *
     * @param int|string $rootLocationId
     */
    public function setRootLocationId($rootLocationId)
    {
        $this->rootLocationId = (int) $rootLocationId;
    }

    /**
     * Returns the path array for location ID.
     *
     * @param mixed $locationId
     *
     * @return array
     */
    public function getPath($locationId)
    {
        $pathArray = array();

        $startingLocation = $this->locationService->loadLocation($locationId);
        $path = $startingLocation->path;

        // Shift of location "1" from path as it is not
        // a fully valid location and not readable by most users
        array_shift($path);

        $rootLocationFound = false;
        foreach ($path as $index => $pathItem) {
            if ((int) $pathItem === $this->rootLocationId) {
                $rootLocationFound = true;
            }

            if (!$rootLocationFound) {
                continue;
            }

            try {
                $location = $this->locationService->loadLocation($pathItem);
            } catch (UnauthorizedException $e) {
                return array();
            }

            $pathArray[] = array(
                'text' => $this->translationHelper->getTranslatedContentNameByContentInfo(
                    $location->contentInfo
                ),
                'url' => $location->id !== $startingLocation->id ?
                    $this->router->generate($location) :
                    false,
                'locationId' => $location->id,
                'contentId' => $location->contentId,
            );
        }

        return $pathArray;
    }
}