<?php
/**
 * YAWIK
 *
 * @filesource
 * @license MIT
 * @copyright  2013 - 2016 Cross Solution <http://cross-solution.de>
 */

/** */
namespace Core\EventManager;

use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateTrait as ZfListenerAggregateTrait;

/**
 * Extension of the ZF ListenerAggregateTrait.
 *
 * Lets you specify the events to attach as an array in a property named "events".
 * which must be an array of enumerated arrays where the entries represents the
 * arguments to an event managers' attach method.
 *
 * <pre>
 * protected $events = [
 *      [ event, callback, priority ]
 * ];
 *
 * </pre>
 *
 * an Event can be:
 * * An event name as string
 * * Multiple event names as enumerated array
 * * A string prefixed with '->': Call the method on this instance to get the event.
 *   e.g. '->getEventForSpecialCase' will call $this->getEventForSpecialCase() and use
 *        its return value as the event argument.
 * * A string prefixes with '::': Call the method on SELF statically.
 * * A string in the format 'FQCN::method' will call the static method specified.
 *
 * Alternatively - if you need to instantiate an event prior to attaching to it,
 * you may reimplement {@link eventsProvider()}, which should return the event specification array of arrays
 * as described above.
 *
 * Further, If you need to provide some additional custom logic in the attach method, you can still benefit
 * from this trait by calling {@link attachEvents()}.
 *
 * @property array $events
 * @author Mathias Gelhausen <gelhausen@cross-solution.de>
 */
trait ListenerAggregateTrait
{
    use ZfListenerAggregateTrait;

    public function attach(EventManagerInterface $events, $priority=1)
    {
        return $this->attachEvents($events);
    }

    /**
     * Provides the event specification array.
     *
     * @return array
     */
    protected function eventsProvider()
    {
        return property_exists($this, 'events') ? $this->events : [];
    }

    /**
     * Attachs the events to the provided event manager.
     *
     * @param EventManagerInterface $events
     * @param array                 $eventsSpec
     *
     * @return $this
     * @throws \UnexpectedValueException
     */
    public function attachEvents(EventManagerInterface $events, array $eventsSpec = null)
    {
        if (null === $eventsSpec) {
            $eventsSpec = $this->eventsProvider();
        }

        foreach ($eventsSpec as $spec) {
            if (!is_array($spec) || 2 > count($spec)) {
                throw new \UnexpectedValueException('Event specification must be an array with at least two entries: event name and method name.');
            }

            $event  = $spec[0];
            $method = $spec[1];
            $priority = isset($spec[2]) ? $spec[2] : 0;

            $this->listeners[] = $events->attach($event, [ $this, $method ], $priority);
        }

        return $this;
    }
}


<?php
/**
 * YAWIK
 *
 * @filesource
 * @license    MIT
 * @copyright  2013 - 2016 Cross Solution <http://cross-solution.de>
 */

namespace Core\Factory\Controller;

use Core\EventManager\EventManager;
use Core\Repository\RepositoryService;
use Interop\Container\ContainerInterface;
use Zend\Form\FormElementManager\FormElementManagerV3Polyfill as FormElementManager;
use Zend\ModuleManager\ModuleManagerInterface;
use Zend\Mvc\I18n\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
use Zend\Validator\ValidatorPluginManager;

/**
 * Install module main controller.
 *
 * @author Anthonius Munthi <me@itstoni.com>
 * @since  0.29
 */
class LazyControllerFactory implements AbstractFactoryInterface
{
    protected $aliases = [
        FormElementManager::class => 'FormElementManager',
        ValidatorPluginManager::class => 'ValidatorManager',
        Translator::class => 'translator',
        ModuleManagerInterface::class => 'ModuleManager',
        EventManager::class => 'Core/EventManager',
        RepositoryService::class => 'repositories',
    ];

    public function canCreate(ContainerInterface $container, $requestedName)
    {
        return strstr($requestedName, '\Controller') !== false;
    }

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $className = $this->getClassName($requestedName);
        $class = new \ReflectionClass($className);

        if ($constructor = $class->getConstructor()) {
            if ($params = $constructor->getParameters()) {
                $constructorArgs = [];
                foreach ($params as $p) {
                    $serviceName = '';
                    if ($p->getClass()) {
                        $serviceName = $p->getClass()->getName();
                        if (array_key_exists($serviceName, $this->aliases)) {
                            $serviceName = $this->aliases[$serviceName];
                        }
                    } else {
                        if ($p->getName() == 'config') {
                            $serviceName = 'config';
                        }
                    }

                    if (!$container->has($serviceName)) {
                        throw new ServiceNotCreatedException(sprintf(
                            'Can\'t create constructor argument "%s" for service "%s"',
                            $p->getName(),
                            $requestedName
                        ));
                    }
                    try {
                        $constructorArgs[] = $container->get($serviceName);
                    } catch (\Exception $x) {
                        echo __CLASS__ . " couldn't create an instance of {$p->getName()} to satisfy the constructor for $requestedName.";
                        exit;
                    }
                }
                return $class->newInstanceArgs($constructorArgs);
            }
        }

        return new $className;
    }

    /**
     * Generate class name
     *
     * @param string $requestedName
     * @return string
     */
    private function getClassName($requestedName)
    {
        $exp = explode('/', $requestedName);

        $className = array_shift($exp).'\\Controller\\'.implode('\\', $exp).'Controller';
        if (!class_exists($className)) {
            throw new ServiceNotCreatedException(
                sprintf(
                    'Can\'t find correct controller class for "%s"',
                    $requestedName
                )
            );
        }

        return $className;
    }
}


<?php

namespace PUGX\Poser;

use PUGX\Poser\Render\RenderInterface;

class Poser
{
    private $renders;

    /**
     * Constructor.
     *
     * @param $renders
     */
    public function __construct($renders)
    {
        $this->renders = array();
        if (!is_array($renders)) {
            $renders = array($renders);
        }

        foreach ($renders as $render) {
            $this->addFormatRender($render);
        }
    }

    /**
     * Generate and Render a badge according to the format.
     *
     * @param $subject
     * @param $status
     * @param $color
     * @param $format
     *
     * @return Image
     */
    public function generate($subject, $status, $color, $format)
    {
        $badge = new Badge($subject, $status, $color, $format);

        return $this->getRenderFor($badge->getFormat())->render($badge);
    }

    /**
     * Generate and Render a badge according to the format from an URI,
     * eg license-MIT-blue.svg or I_m-liuggio-yellow.svg.
     *
     * @param $string
     * @return Image
     */
    public function generateFromURI($string)
    {
        $badge = Badge::fromURI($string);

        return $this->getRenderFor($badge->getFormat())->render($badge);
    }

    /**
     * All the formats available.
     *
     * @return array
     */
    public function validFormats()
    {
        return array_keys($this->renders);
    }

    private function addFormatRender(RenderInterface $render)
    {
        foreach ($render->supportedFormats() as $format) {
            $this->renders[$format] = $render;
        }
    }

    /**
     * @param $format
     *
     * @return RenderInterface
     */
    private function getRenderFor($format)
    {
        if (!isset($this->renders[$format])) {
            throw new \InvalidArgumentException(sprintf('No render founds for this format [%s]', $format));
        }

        return $this->renders[$format];
    }
}


<?php

namespace PUGX\Poser;

class Badge
{
    const DEFAULT_FORMAT = 'svg';
    private static $colorScheme = array(
        "brightgreen" => "44cc11",
        "green"       => "97CA00",
        "yellow"      => "dfb317",
        "yellowgreen" => "a4a61d",
        "orange"      => "fe7d37",
        "red"         => "e05d44",
        "blue"        => "007ec6",
        "grey"        => "555555",
        "lightgray"   => "9f9f9f"
    );

    private $subject;
    private $status;
    private $color;
    private $format;

    public function __construct($subject, $status, $color, $format = self::DEFAULT_FORMAT)
    {
        $this->subject = $this->escapeValue($subject);
        $this->status  = $this->escapeValue($status);
        $this->format  = $this->escapeValue($format);
        $this->color   = $this->getColorHex($color);

        if (!$this->isValidColorHex($this->color)) {
            throw new \InvalidArgumentException(sprintf('Color not valid %s', $this->color));
        }
    }

    /**
     * An array of the color names available.
     *
     * @return array
     */
    public static function getColorNamesAvailable()
    {
        return array_keys(self::$colorScheme);
    }

    /**
     * Factory method the creates a Badge from an URI
     * eg. I_m-liuggio-yellow.svg
     *
     * @param string $URI
     *
     * @return Badge
     * @throws \InvalidArgumentException
     */
    public static function fromURI($URI)
    {
        $regex = '/^(([^-]|--)+)-(([^-]|--)+)-(([^-]|--)+)\.(svg|png|gif|jpg)$/';
        $match = array();

        if (1 != preg_match($regex, $URI, $match) && (7 != count($match))) {
            throw new \InvalidArgumentException('The URI given is not a valid URI'.$URI);
        }

        $subject = $match[1];
        $status  = $match[3];
        $color   = $match[5];
        $format  = $match[7];

        return new self($subject, $status, $color, $format);
    }

    /**
     * @return string the Hexadecimal #FFFFFF.
     */
    public function getHexColor()
    {
        return '#'.$this->color;
    }

    /**
     * @return string the format of the image eg. `svg`.
     */
    public function getFormat()
    {
        return $this->format;
    }

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

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

    public function __toString()
    {
        return sprintf(
            "%s-%s-%s.%s",
            $this->subject,
            $this->status,
            $this->color,
            $this->format
        );
    }

    private function escapeValue($value)
    {
        $pattern = array(
            // '/([^_])_([^_])/g', // damn it global doesn't work in PHP
            '/([^_])_$/',
            '/^_([^_])/',
            '/__/',
            '/--+/',
        );
        $replacement = array(
            //'$1 $2',
            '$1 ',
            ' $1',
            '°§*¼',
            '-',
        );
        $ret = preg_replace($pattern, $replacement, $value);
        $ret = str_replace('_', ' ', $ret);    // this fix the php pgrep_replace is not global :(
        $ret = str_replace('°§*¼', '_', $ret); // this fix the php pgrep_replace is not global :(

        return $ret;
    }

    private function getColorHex($color)
    {
        return array_key_exists($color, self::$colorScheme) ? self::$colorScheme[$color] : $color;
    }

    private function isValidColorHex($color)
    {
        $color = ltrim($color, "#");
        $regex = '/^[0-9a-fA-F]{6}$/';

        return preg_match($regex, $color);
    }
}


<?php

/*
 * This file is part of the badge-poser package.
 *
 * (c) PUGX <http://pugx.github.io/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace PUGX\Poser\Calculator;

class GDTextSizeCalculator implements TextSizeCalculatorInterface
{
    const TEXT_FONT             = '/Font/DejaVuSans.ttf';

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

    public function __construct()
    {
        $this->fontPath = __DIR__ . self::TEXT_FONT;
    }

    /**
     * Calculate the width of the text box.
     *
     * @param string $text
     * @param int    $size
     *
     * @return float
     */
    public function calculateWidth($text, $size = self::TEXT_SIZE)
    {
        $size = $this->convertToPt($size);
        $box  = imagettfbbox($size, 0, $this->fontPath, $text);

        return round(abs($box[2] - $box[0]) + self::SHIELD_PADDING_EXTERNAL + self::SHIELD_PADDING_INTERNAL, 1);
    }

    private function convertToPt($pixels)
    {
        return round($pixels * 0.75, 1);
    }
}


<?php

/*
 * This file is part of the badge-poser package.
 *
 * (c) PUGX <http://pugx.github.io/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace PUGX\Poser\Render;

use PUGX\Poser\Badge;
use PUGX\Poser\Calculator\GDTextSizeCalculator;
use PUGX\Poser\Calculator\TextSizeCalculatorInterface;
use PUGX\Poser\Image;
use SimpleXMLElement;

/**
 * Local SVG renderer.
 * Generate SVG badges from local templates.
 *
 * @author JM Leroux <jmleroux.pro@gmail.com>
 */
abstract class LocalSvgRenderer implements RenderInterface
{
    const VENDOR_COLOR = '#555';

    /**
     * @var TextSizeCalculatorInterface
     */
    private $textSizeCalculator;

    /**
     * @var string
     */
    private $templatesDirectory;

    /**
     * @param TextSizeCalculatorInterface $textSizeCalculator
     * @param null|string $templatesDirectory
     */
    public function __construct(TextSizeCalculatorInterface $textSizeCalculator = null, $templatesDirectory = null)
    {
        $this->textSizeCalculator = $textSizeCalculator;
        if (null === $this->textSizeCalculator) {
            $this->textSizeCalculator = new GDTextSizeCalculator();
        }

        $this->templatesDirectory = $templatesDirectory;
        if (null === $this->templatesDirectory) {
            $this->templatesDirectory = __DIR__ . '/../Resources/templates';
        }
    }

    /**
     * @param Badge $badge
     *
     * @return mixed
     */
    public function render(Badge $badge)
    {
        $template = $this->getTemplate($this->getTemplateName());
        $parameters = $this->buildParameters($badge);

        return $this->renderSvg($template, $parameters, $badge->getFormat());
    }

    /**
     * @return string
     */
    abstract protected function getTemplateName();

    /**
     * @param $format
     *
     * @return string SVG content of the template
     */
    private function getTemplate($format)
    {
        $filepath = sprintf('%s/%s.svg', $this->templatesDirectory, $format);

        if (!file_exists($filepath)) {
            throw new \InvalidArgumentException(sprintf('No template for format %s', $format));
        }

        return file_get_contents($filepath);
    }

    /**
     * @param $text
     *
     * @return float
     */
    private function stringWidth($text)
    {
        return $this->textSizeCalculator->calculateWidth($text);
    }

    /**
     * @param string $render
     * @param array $parameters
     * @param string $format
     *
     * @return Image
     */
    private function renderSvg($render, $parameters, $format)
    {
        foreach ($parameters as $key => $variable) {
            $render = str_replace(sprintf('{{ %s }}', $key), $variable, $render);
        }

        try {
            $xml = new SimpleXMLElement($render);
        } catch (\Exception $e) {
            throw new \RuntimeException('Generated string is not a valid XML');
        }
        if ('svg' !== $xml->getName()) {
            throw new \RuntimeException('Generated xml is not a SVG');
        }

        return Image::createFromString($render, $format);
    }

    /**
     * @param Badge $badge
     *
     * @return array
     */
    private function buildParameters(Badge $badge)
    {
        $parameters = array();

        $parameters['vendorWidth'] = $this->stringWidth($badge->getSubject());
        $parameters['valueWidth'] = $this->stringWidth($badge->getStatus());
        $parameters['totalWidth'] = $parameters['valueWidth'] + $parameters['vendorWidth'];
        $parameters['vendorColor'] = static::VENDOR_COLOR;
        $parameters['valueColor'] = $badge->getHexColor();
        $parameters['vendor'] = $badge->getSubject();
        $parameters['value'] = $badge->getStatus();
        $parameters['vendorStartPosition'] = round($parameters['vendorWidth'] / 2, 1) + 1;
        $parameters['valueStartPosition'] = $parameters['vendorWidth'] + round($parameters['valueWidth'] / 2, 1) - 1;

        return $parameters;
    }
}


<?php
namespace Gt\Curl;

class Curl implements CurlInterface {
	/** @var resource */
	protected $ch;
	/** @var string */
	protected $buffer;

	public function __construct(string $url = null) {
		$this->init($url);
	}

	/**
	 * Close a cURL session
	 * @see http://php.net/manual/en/function.curl-close.php
	 */
	public function __destruct() {
		curl_close($this->ch);
	}

	/**
	 * Copy a cURL handle along with all of its preferences
	 * @see http://php.net/manual/en/function.curl-copy-handle.php
	 */
	public function __clone() {
		$this->ch = curl_copy_handle($this->ch);
	}

	/**
	 * @see http://php.net/manual/en/function.curl-version.php
	 */
	public static function version(
		int $age = CURLVERSION_NOW
	):CurlVersionInterface {
		return new CurlVersion(
			curl_version($age)
		);
	}

	/**
	 * Return string describing the given error code
	 * @see http://php.net/manual/en/function.curl-strerror.php
	 */
	public static function strError(int $errorNum):string {
		return curl_strerror($errorNum);
	}

	/**
	 * Return string containing last exec call's output
	 * @throws NoOutputException
	 */
	public function output():string {
// Buffer will be null before exec is called...
		if(is_null($this->buffer)) {
			$this->exec();
		}

		curl_close($this->ch);

// ...But will always be a string after exec is called, even if empty.
		if(strlen($this->buffer) === 0) {
			throw new NoOutputException();
		}

		return $this->buffer;
	}

	/**
	 * Return json-decoded output from last exec call
	 * @throws JsonDecodeException
	 */
	public function outputJson(
		int $depth = 512,
		int $options = 0
	) {
		$json = json_decode(
			$this->output(),
			false,
			$depth,
			$options
		);
		if(is_null($json)) {
			$errorMessage = json_last_error_msg();
			throw new JsonDecodeException($errorMessage);
		}

		return $json;
	}

	/**
	 * Return the last error number
	 * @see http://php.net/manual/en/function.curl-errno.php
	 */
	public function errno():int {
		return curl_errno($this->ch);
	}

	/**
	 * Return a string containing the last error for the current session
	 * @see http://php.net/manual/en/function.curl-error.php
	 */
	public function error():string {
		return curl_error($this->ch);
	}

	/**
	 * URL encodes the given string
	 * @see http://php.net/manual/en/function.curl-escape.php
	 */
	public function escape(string $str):string {
		return curl_escape($this->ch, $str);
	}

	/**
	 * Perform a cURL session
	 * @see http://php.net/manual/en/function.curl-exec.php
	 */
	public function exec():string {
		ob_start();
		$response = curl_exec($this->ch);
		$this->buffer = ob_get_contents();
		ob_end_clean();

		if(false === $response) {
			throw new CurlException($this->error());
		}
		if(true === $response) {
			$response = $this->buffer;
		}

		return $response;
	}

	/**
	 * Get information regarding the transfer
	 * @see http://php.net/manual/en/function.curl-getinfo.php
	 */
	public function getInfo(int $opt):string {
		if($opt <= 0) {
			throw new CurlException(
				"Option must be greater than zero, "
				. $opt
				. " given."
			);
		}
		return curl_getinfo($this->ch, $opt);
	}

	/**
	 * Initialize a cURL session
	 * @see http://php.net/manual/en/function.curl-init.php
	 */
	public function init(string $url = null):void {
		$this->ch = curl_init($url);
		CurlObjectLookup::add($this);
	}

	/**
	 * Pause and unpause a connection
	 * @see http://php.net/manual/en/function.curl-pause.php
	 */
	public function pause(int $bitmask):int {
		return curl_pause($this->ch, $bitmask);
	}

	/**
	 * Reset all options of the libcurl session handle
	 * @see http://php.net/manual/en/function.curl-reset.php
	 */
	public function reset():void {
		curl_reset($this->ch);
	}

	/**
	 * Set an option for the cURL transfer
	 * @see http://php.net/manual/en/function.curl-setopt.php
	 */
	public function setOpt(int $option, $value):bool {
		return curl_setopt($this->ch, $option, $value);
	}

	/**
	 * Set multiple options for the cURL transfer
	 * @see http://php.net/manual/en/function.curl-setopt-array.php
	 */
	public function setOptArray(array $options):bool {
		return curl_setopt_array($this->ch, $options);
	}

	/**
	 * Decodes the given URL encoded string
	 * @see http://php.net/manual/en/function.curl-unescape.php
	 */
	public function unescape(string $str):string {
		return curl_unescape($this->ch, $str);
	}

	/**
	 * Obtain the underlying curl resource, as created with curl_init.
	 */
	public function getHandle(){
		return $this->ch;
	}

	/**
	 * Gets all CURLINFO_ data, identical to calling curl_getinfo with no arguments.
	 */
	public function getAllInfo():array {
		return curl_getinfo($this->ch, 0);
	}
}


<?php
namespace Gt\Curl;

class CurlShare implements CurlShareInterface {
	/** @var resource */
	protected $sh;

	public function __construct() {
		$this->init();
	}

	public function __destruct() {
		$this->close();
	}

	/**
	 * Return string describing the given error code
	 * @see http://php.net/manual/en/function.curl-share-strerror.php
	 */
	public static function strError(int $errorNum):string {
		return curl_share_strerror($errorNum);
	}

	/**
	 * Close the cURL share handle
	 * @see http://php.net/manual/en/function.curl-share-close.php
	 */
	public function close():void {
		curl_share_close($this->sh);
	}

	/**
	 * Initialize the cURL share handle
	 * @see http://php.net/manual/en/function.curl-share-init.php
	 */
	public function init():void {
		$this->sh = curl_share_init();
	}

	/**
	 * Set an option for the cURL share handle
	 * @see http://php.net/manual/en/function.curl-share-setopt.php
	 */
	public function setOpt(int $option, string $value):bool {
		return curl_share_setopt($this->sh, $option, $value);
	}
}


<?php
namespace Gt\Curl;

class CurlMulti implements CurlMultiInterface {
	/** @var resource */
	protected $mh;

	public function __construct() {
		$this->init();
	}

	public function __destruct() {
		$this->close();
	}

	/**
	 * Returns a text error message describing the given CURLM error code.
	 * @see http://php.net/manual/en/function.curl-multi-strerror.php
	 */
	public static function strerror(int $errorNum):string {
		return curl_multi_strerror($errorNum);
	}

	/**
	 * @see curl_multi_add_handle()
	 * @throws CurlException if a CURLM_XXX error is made
	 */
	public function add(CurlInterface $curl):void {
		curl_multi_add_handle($this->mh, $curl->getHandle());
	}

	/**
	 * Close the set of cURL handles
	 * @see http://php.net/manual/en/function.curl-multi-close.php
	 */
	public function close():void {
		curl_multi_close($this->mh);
	}

	/**
	 * Return the last multi curl error number
	 * @return int
	 */
	public function errno():int {
		return curl_multi_errno($this->mh);
	}

	/**
	 * Run the sub-connections of the current cURL handle
	 * @see http://php.net/manual/en/function.curl-multi-exec.php()
	 * @throws CurlException if a CURLM_XXX error is made
	 */
	public function exec(int &$stillRunning):int {
		return curl_multi_exec($this->mh, $stillRunning);
	}

	/**
	 * Obtain the underlying Curl Multi resource, as created with curl_multi_init.
	 * @return resource
	 */
	public function getHandle() {
		return $this->mh;
	}

	/**
	 * Return the content of a cURL handle if CURLOPT_RETURNTRANSFER is set
	 * @see http://php.net/manual/en/function.curl-multi-getcontent.php
	 * @return string the content of a cURL handle if CURLOPT_RETURNTRANSFER is set
	 */
	public function getContent(CurlInterface $curl):string {
		return curl_multi_getcontent($curl->getHandle());
	}

	/**
	 * Get information about the current transfers
	 * @see http://php.net/manual/en/function.curl-multi-info-read.php
	 * @throws CurlException on failure
	 */
	public function infoRead(int &$msgsInQueue = null):?CurlMultiInfoInterface {
		$info = curl_multi_info_read(
			$this->mh,
			$msgsInQueue
		);

		if(!$info) {
			return null;
		}

		return new CurlMultiInfo($info);
	}

	/**
	 * Initialises a new cURL multi handle
	 * @see http://php.net/manual/en/function.curl-multi-init.php
	 */
	public function init():void {
		$this->mh = curl_multi_init();
	}

	/**
	 * Remove a multi handle from the set of cURL handles
	 * @see http://php.net/manual/en/function.curl-multi-remove-handle.php
	 * @throws CurlException on failure
	 */
	public function remove(CurlInterface $curl):void {
		curl_multi_remove_handle($this->mh, $curl->getHandle());
	}

	/**
	 * Wait for activity on any curl_multi connection
	 *
	 * @see http://php.net/manual/en/function.curl-multi-select.php
	 * @throws CurlException on select failure or timeout
	 */
	public function select(float $timeout = 1.0):int {
		return curl_multi_select($this->mh, $timeout);
	}

	/**
	 * Set an option for the cURL multi handle, one of the CURLMOPT_* constants.
	 *
	 * @see http://php.net/manual/en/function.curl-multi-setopt.php
	 * @throws CurlException on failure
	 */
	public function setOpt(int $option, $value):void {
		curl_multi_setopt($this->mh, $option, $value);
	}
}


<?php

/**
 *
 * PHP TableOfContents Library
 *
 * @license http://opensource.org/licenses/MIT
 * @link https://github.com/caseyamcl/toc
 * @version 1.0
 * @package caseyamcl/toc
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * ------------------------------------------------------------------
 */

namespace TOC;

use Cocur\Slugify\Slugify;

/**
 * UniqueSluggifier creates slugs from text without repeating the same slug twice per instance
 *
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 */
class UniqueSluggifier
{
    /**
     * @var \Cocur\Slugify\Slugify
     */
    private $slugify;

    /**
     * @var array
     */
    private $used;

    /**
     * Constructor
     *
     * @param \Cocur\Slugify\Slugify $slugify
     */
    public function __construct(Slugify $slugify = null)
    {
        $this->used = array();
        $this->slugify = $slugify ?: new Slugify();
    }

    /**
     * Slugify
     *
     * @param string $text
     * @return string
     */
    public function slugify($text)
    {
        $slugged = $this->slugify->slugify($text);

        $count = 1;
        $orig = $slugged;
        while (in_array($slugged, $this->used)) {
            $slugged = $orig . '-' . $count;
            $count++;
        }

        $this->used[] = $slugged;
        return $slugged;
    }
}

/* EOF: UniqueSluggifier.php */


<?php

/**
 * PHP TableOfContents Library
 *
 * @license http://opensource.org/licenses/MIT
 * @link https://github.com/caseyamcl/toc
 * @version 1.0
 * @package caseyamcl/toc
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * ------------------------------------------------------------------
 */

namespace TOC;

use Masterminds\HTML5;
use RuntimeException;

/**
 * TOC Markup Fixer adds `id` attributes to all H1...H6 tags where they do not
 * already exist
 *
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 */
class MarkupFixer
{
    use HtmlHelper;

    /**
     * @var HTML5
     */
    private $htmlParser;

    /**
     * Constructor
     *
     * @param HTML5 $htmlParser
     */
    public function __construct(HTML5 $htmlParser = null)
    {
        $this->htmlParser = $htmlParser ?: new HTML5();
    }

    /**
     * Fix markup
     *
     * @param string $markup
     * @param int    $topLevel
     * @param int    $depth
     * @return string Markup with added IDs
     * @throws RuntimeException
     */
    public function fix($markup, $topLevel = 1, $depth = 6)
    {
        if ( ! $this->isFullHtmlDocument($markup)) {
            $partialID = uniqid('toc_generator_');
            $markup = sprintf("<body id='%s'>%s</body>", $partialID, $markup);
        }

        $domDocument = $this->htmlParser->loadHTML($markup);
        $domDocument->preserveWhiteSpace = true; // do not clobber whitespace

        $sluggifier = new UniqueSluggifier();

        /** @var \DOMElement $node */
        foreach ($this->traverseHeaderTags($domDocument, $topLevel, $depth) as $node) {
            if ($node->getAttribute('id')) {
                continue;
            }

            $node->setAttribute('id', $sluggifier->slugify($node->getAttribute('title') ?: $node->textContent));
        }

        return $this->htmlParser->saveHTML((isset($partialID)) ? $domDocument->getElementById($partialID)->childNodes : $domDocument);
    }
}


<?php

/**
 * PHP TableOfContents Library
 *
 * @license http://opensource.org/licenses/MIT
 * @link https://github.com/caseyamcl/toc
 * @version 1.0
 * @package caseyamcl/toc
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * ------------------------------------------------------------------
 */

namespace TOC;

/**
 * Trait that helps with HTML-related operations
 *
 * @package TOC
 */
trait HtmlHelper
{
    /**
     * Convert a topLevel and depth to H1..H6 tags array
     *
     * @param int $topLevel
     * @param int $depth
     * @return array|string[]  Array of header tags; ex: ['h1', 'h2', 'h3']
     */
    protected function determineHeaderTags($topLevel, $depth)
    {
        $desired = range((int) $topLevel, (int) $topLevel + ((int) $depth - 1));
        $allowed = [1, 2, 3, 4, 5, 6];

        return array_map(function($val) { return 'h'.$val; }, array_intersect($desired, $allowed));
    }

    /**
     * Traverse Header Tags in DOM Document
     *
     * @param \DOMDocument $domDocument
     * @param int          $topLevel
     * @param int          $depth
     * @return \ArrayIterator|\DomElement[]
     */
    protected function traverseHeaderTags(\DOMDocument $domDocument, $topLevel, $depth)
    {
        $xpath = new \DOMXPath($domDocument);

        $xpathQuery = sprintf(
            "//*[%s]",
            implode(' or ', array_map(function($v) {
                return sprintf('local-name() = "%s"', $v);
            }, $this->determineHeaderTags($topLevel, $depth)))
        );

        $nodes = [];
        foreach ($xpath->query($xpathQuery) as $node) {
            $nodes[] = $node;
        }

        return new \ArrayIterator($nodes);
    }

    /**
     * Is this a full HTML document
     *
     * Guesses, based on presence of ... tags
     *
     * @param string $markup
     * @return bool
     */
    protected function isFullHtmlDocument($markup)
    {
        return (strpos($markup, "<body") !== false && strpos($markup, "") !== false);
    }
}


<?php

/**
 * PHP TableOfContents Library
 *
 * @license http://opensource.org/licenses/MIT
 * @link https://github.com/caseyamcl/toc
 * @version 1.0
 * @package caseyamcl/toc
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * ------------------------------------------------------------------
 */

namespace TOC;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Matcher;
use Knp\Menu\MenuFactory;
use Knp\Menu\Renderer\ListRenderer;
use Knp\Menu\Renderer\RendererInterface;
use Masterminds\HTML5;

/**
 * Table Of Contents Generator generates TOCs from HTML Markup
 *
 * @author Casey McLaughlin <caseyamcl@gmail.com>
 */
class TocGenerator
{
    use HtmlHelper;

    /**
     * @var HTML5
     */
    private $domParser;

    /**
     * @var MenuFactory
     */
    private $menuFactory;

    /**
     * Constructor
     *
     * @param MenuFactory $menuFactory
     * @param HTML5       $htmlParser
     */
    public function __construct(MenuFactory $menuFactory = null, HTML5 $htmlParser = null)
    {
        $this->domParser   = $htmlParser  ?: new HTML5();
        $this->menuFactory = $menuFactory ?: new MenuFactory();
    }

    /**
     * Get Menu
     *
     * Returns a KNP Menu object, which can be traversed or rendered
     *
     * @param string  $markup    Content to get items fro $this->getItems($markup, $topLevel, $depth)m
     * @param int     $topLevel  Top Header (1 through 6)
     * @param int     $depth     Depth (1 through 6)
     * @return ItemInterface     KNP Menu
     */
    public function getMenu($markup, $topLevel = 1, $depth = 6)
    {
        // Setup an empty menu object
        $menu = $this->menuFactory->createItem('TOC');

        // Empty?  Return empty menu item
        if (trim($markup) == '') {
            return $menu;
        }

        // Parse HTML
        $tagsToMatch = $this->determineHeaderTags($topLevel, $depth);

        // Initial settings
        $lastElem = $menu;

        // Do it...
        $domDocument = $this->domParser->loadHTML($markup);
        foreach ($this->traverseHeaderTags($domDocument, $topLevel, $depth) as $node) {

            // Skip items without IDs
            if ( ! $node->hasAttribute('id')) {
                continue;
            }

            // Get the TagName and the level
            $tagName = $node->tagName;
            $level   = array_search(strtolower($tagName), $tagsToMatch) + 1;

            // Determine parent item which to add child
            if ($level == 1) {
                $parent = $menu;
            }
            elseif ($level == $lastElem->getLevel()) {
                $parent = $lastElem->getParent();
            }
            elseif ($level > $lastElem->getLevel()) {
                $parent = $lastElem;
                for ($i = $lastElem->getLevel(); $i < ($level - 1); $i++) {
                    $parent = $parent->addChild('');
                }
            }
            else { //if ($level < $lastElem->getLevel())
                $parent = $lastElem->getParent();
                while ($parent->getLevel() > $level - 1) {
                    $parent = $parent->getParent();
                }
            }

            $lastElem = $parent->addChild(
                $node->getAttribute('title') ?: $node->textContent,
                ['uri' => '#' . $node->getAttribute('id')]
            );
        }

        return $menu;
    }

    /**
     * Get HTML Links in list form
     *
     * @param string            $markup   Content to get items from
     * @param int               $topLevel Top Header (1 through 6)
     * @param int               $depth    Depth (1 through 6)
     * @param RendererInterface $renderer
     * @return string HTML <LI> items
     */
    public function getHtmlMenu($markup, $topLevel = 1, $depth = 6, RendererInterface $renderer = null)
    {
        if ( ! $renderer) {
            $renderer = new ListRenderer(new Matcher(), [
                'currentClass'  => 'active',
                'ancestorClass' => 'active_ancestor'
            ]);
        }

        return $renderer->render($this->getMenu($markup, $topLevel, $depth));
    }
}


<?php

namespace Nails\Api\Console\Command\Controller;

use Nails\Api\Exception\Console\ControllerExistsException;
use Nails\Console\Command\BaseMaker;
use Nails\Factory;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class Create extends BaseMaker
{
    const RESOURCE_PATH   = NAILS_PATH . 'module-api/resources/console/';
    const CONTROLLER_PATH = NAILS_APP_PATH . 'src/Api/Controller/';

    // --------------------------------------------------------------------------

    /**
     * Configure the command
     */
    protected function configure(): void
    {
        $this
            ->setName('make:controller:api')
            ->setDescription('Creates a new Api controller')
            ->addArgument(
                'modelName',
                InputArgument::OPTIONAL,
                'Define the name of the model on which to base the controller'
            )
            ->addArgument(
                'modelProvider',
                InputArgument::OPTIONAL,
                'Define the provider of the model',
                'app'
            );
    }

    // --------------------------------------------------------------------------

    /**
     * Executes the app
     *
     * @param  InputInterface  $oInput  The Input Interface provided by Symfony
     * @param  OutputInterface $oOutput The Output Interface provided by Symfony
     *
     * @return int
     */
    protected function execute(InputInterface $oInput, OutputInterface $oOutput): int
    {
        parent::execute($oInput, $oOutput);

        // --------------------------------------------------------------------------

        try {
            //  Ensure the paths exist
            $this->createPath(self::CONTROLLER_PATH);
            //  Create the controller
            $this->createController();
        } catch (\Exception $e) {
            return $this->abort(
                self::EXIT_CODE_FAILURE,
                [$e->getMessage()]
            );
        }

        // --------------------------------------------------------------------------

        //  Cleaning up
        $oOutput->writeln('');
        $oOutput->writeln('<comment>Cleaning up...</comment>');

        // --------------------------------------------------------------------------

        //  And we're done
        $oOutput->writeln('');
        $oOutput->writeln('Complete!');

        return self::EXIT_CODE_SUCCESS;
    }

    // --------------------------------------------------------------------------

    /**
     * Create the Model
     *
     * @throws \Exception
     * @return void
     */
    private function createController(): void
    {
        $aFields  = $this->getArguments();
        $aCreated = [];

        try {

            $aModels = array_filter(explode(',', $aFields['MODEL_NAME']));

            foreach ($aModels as $sModel) {

                $aFields['MODEL_NAME'] = $sModel;
                $this->oOutput->write('Creating controller <comment>' . $sModel . '</comment>... ');

                //  Validate model exists by attempting to load it
                Factory::model($sModel, $aFields['MODEL_PROVIDER']);

                //  Check for existing controller
                $sPath = static::CONTROLLER_PATH . $sModel . '.php';
                if (file_exists($sPath)) {
                    throw new ControllerExistsException(
                        'Controller "' . $sModel . '" exists already at path "' . $sPath . '"'
                    );
                }

                $this->createFile($sPath, $this->getResource('template/controller.php', $aFields));
                $aCreated[] = $sPath;
                $this->oOutput->writeln('<info>done!</info>');
            }

        } catch (\Exception $e) {
            $this->oOutput->writeln('<error>failed!</error>');
            //  Clean up created models
            if (!empty($aCreated)) {
                $this->oOutput->writeln('<error>Cleaning up - removing newly created controllers</error>');
                foreach ($aCreated as $sPath) {
                    @unlink($sPath);
                }
            }
            throw $e;
        }
    }
}


<?php

/**
 * Routes requests to the API to the appropriate controllers
 *
 * @package     Nails
 * @subpackage  module-api
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

use Nails\Api\Exception\ApiException;
use Nails\Api\Factory\ApiResponse;
use Nails\Components;
use Nails\Common\Exception\NailsException;
use Nails\Common\Exception\ValidationException;
use Nails\Environment;
use Nails\Factory;

// --------------------------------------------------------------------------

/**
 * Allow the app to add functionality, if needed
 */
if (class_exists('\App\Api\Controller\BaseRouter')) {
    abstract class BaseMiddle extends \App\Api\Controller\BaseRouter
    {
    }
} else {
    abstract class BaseMiddle extends \Nails\Common\Controller\Base
    {
    }
}

// --------------------------------------------------------------------------

class ApiRouter extends BaseMiddle
{
    const FORMAT_JSON                  = 'JSON';
    const FORMAT_TXT                   = 'TXT';
    const DEFAULT_FORMAT               = 'JSON';
    const VALID_FORMATS                = [
        'TXT',
        'JSON',
    ];
    const ACCESS_CONTROL_ALLOW_ORIGIN  = '*';
    const ACCESS_CONTROL_ALLOW_HEADERS = [
        'X-Access-Token',
        'content',
        'origin',
        'content-type',
    ];
    const ACCESS_CONTROL_ALLOW_METHODS = [
        'GET',
        'PUT',
        'POST',
        'DELETE',
        'OPTIONS',
    ];
    const OUTPUT_FORMAT_PATTERN        = '/\.([a-z]*)$/';

    // --------------------------------------------------------------------------

    private $sRequestMethod;
    private $sModuleName;
    private $sClassName;
    private $sMethod;
    private $aOutputValidFormats;
    private $sOutputFormat;
    private $bOutputSendHeader;
    private $oLogger;

    // --------------------------------------------------------------------------

    /**
     * Constructs the router, defining the request variables
     */
    public function __construct()
    {
        parent::__construct();

        // --------------------------------------------------------------------------

        //  Set defaults
        $this->bOutputSendHeader = true;

        // --------------------------------------------------------------------------

        //  Work out the request method
        $oInput               = Factory::service('Input');
        $this->sRequestMethod = $oInput->server('REQUEST_METHOD');
        $this->sRequestMethod = $this->sRequestMethod ? $this->sRequestMethod : 'GET';

        /**
         * In order to work out the next few parts we'll analyse the URI string manually.
         * We're doing this because of the optional return type at the end of the string;
         * it's easier to regex that quickly, remove it, then split up the segments.
         */

        $this->sOutputFormat = static::getOutputFormat();
        $sUri                = preg_replace(static::OUTPUT_FORMAT_PATTERN, '', uri_string());

        //  Remove the module prefix (i.e "api/") then explode into segments
        //  Using regex as some systems will report a leading slash (e.g CLI)
        $sUri = preg_replace('#/?api/#', '', $sUri);
        $aUri = explode('/', $sUri);

        //  Work out the sModuleName, sClassName and method
        $this->sModuleName = getFromArray(0, $aUri, null);
        $this->sClassName  = ucfirst(getFromArray(1, $aUri, $this->sModuleName));
        $this->sMethod     = getFromArray(2, $aUri, 'index');

        //  Configure logging
        $oNow          = Factory::factory('DateTime');
        $this->oLogger = Factory::service('Logger');
        $this->oLogger->setFile('api-' . $oNow->format('y-m-d') . '.php');
    }

    // --------------------------------------------------------------------------

    public static function getOutputFormat()
    {
        preg_match(static::OUTPUT_FORMAT_PATTERN, uri_string(), $aMatches);
        $sFormat = !empty($aMatches[1]) ? strtoupper($aMatches[1]) : null;
        return static::isValidFormat($sFormat) ? $sFormat : static::DEFAULT_FORMAT;
    }

    // --------------------------------------------------------------------------

    /**
     * Route the call to the correct place
     * @return void
     */
    public function index()
    {
        //  Handle OPTIONS CORS pre-flight requests
        if ($this->sRequestMethod === 'OPTIONS') {

            $oOutput = Factory::service('Output');
            $oOutput->set_header('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN);
            $oOutput->set_header('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS));
            $oOutput->set_header('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS));
            return;

        } else {

            try {
                /**
                 * If an access token has been passed then verify it
                 *
                 * Passing the token via the header is preferred, but fallback to the GET
                 * and POST arrays.
                 */

                $oInput                = Factory::service('Input');
                $oHttpCodes            = Factory::service('HttpCodes');
                $oUserAccessTokenModel = Factory::model('UserAccessToken', 'nails/module-auth');
                $sAccessToken          = $oInput->header('X-Access-Token');

                if (!$sAccessToken) {
                    $sAccessToken = $oInput->post('accessToken');
                }

                if (!$sAccessToken) {
                    $sAccessToken = $oInput->get('accessToken');
                }

                if ($sAccessToken) {
                    $oAccessToken = $oUserAccessTokenModel->getByValidToken($sAccessToken);
                    if ($oAccessToken) {
                        $oUserModel = Factory::model('User', 'nails/module-auth');
                        $oUserModel->setLoginData($oAccessToken->user_id, false);
                    } else {
                        throw new ApiException(
                            'Invalid access token',
                            $oHttpCodes::STATUS_UNAUTHORIZED
                        );
                    }
                }

                // --------------------------------------------------------------------------

                if (!$this->outputSetFormat($this->sOutputFormat)) {
                    throw new ApiException(
                        '"' . $this->sOutputFormat . '" is not a valid format.',
                        $oHttpCodes::STATUS_BAD_REQUEST
                    );
                }

                // --------------------------------------------------------------------------

                //  Register API modules
                $aNamespaces = [
                    'app' => (object) [
                        'namespace' => 'App\\',
                    ],
                ];
                foreach (Components::modules() as $oModule) {
                    if (!empty($oModule->data->{'nails/module-api'}->namespace)) {
                        $sNamespace = $oModule->data->{'nails/module-api'}->namespace;
                        if (array_key_exists($sNamespace, $aNamespaces)) {
                            throw new NailsException(
                                'Conflicting API namespace "' . $sNamespace . '" in use by ' .
                                '"' . $oModule->slug . '" and "' . $aNamespaces[$sNamespace]->slug . '"'
                            );
                        }
                        $aNamespaces[$sNamespace] = $oModule;
                    }
                }

                $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
                $s404Error  = '"' . strtolower($this->sModuleName . '/' . $this->sClassName . '/' . $this->sMethod) . '" is not a valid API route.';

                if (!array_key_exists($this->sModuleName, $aNamespaces)) {
                    throw new ApiException($s404Error, $i404Status);
                }

                $oNamespace          = $aNamespaces[$this->sModuleName];
                $sOriginalController = $this->sClassName;

                //  Do we need to remap the controller?
                if (!empty($oNamespace->data->{'nails/module-api'}->{'controller-map'})) {

                    $aMap             = (array) $oNamespace->data->{'nails/module-api'}->{'controller-map'};
                    $this->sClassName = getFromArray($this->sClassName, $aMap, $this->sClassName);

                    //  This prevents users from accessing the "correct" controller, so we only have one valid route
                    $sRemapped = array_search($sOriginalController, $aMap);
                    if ($sRemapped !== false) {
                        $this->sClassName = $sRemapped;
                    }
                }

                $sController = $oNamespace->namespace . 'Api\\Controller\\' . $this->sClassName;

                if (!class_exists($sController)) {
                    throw new ApiException($s404Error, $i404Status);
                }

                $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
                if ($mAuth !== true) {

                    if (is_array($mAuth)) {
                        $sError  = getFromArray('error', $mAuth, $oHttpCodes::getByCode($oHttpCodes::STATUS_UNAUTHORIZED));
                        $iStatus = (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED);
                    } else {
                        $sError  = 'You must be logged in to access this resource';
                        $iStatus = $oHttpCodes::STATUS_UNAUTHORIZED;
                    }

                    throw new ApiException($sError, $iStatus);
                }

                if (!empty($sController::REQUIRE_SCOPE)) {
                    if (!$oUserAccessTokenModel->hasScope($oAccessToken, $sController::REQUIRE_SCOPE)) {
                        throw new ApiException(
                            'Access token with "' . $sController::REQUIRE_SCOPE . '" scope is required.',
                            $oHttpCodes::STATUS_UNAUTHORIZED
                        );
                    }
                }

                //  New instance of the controller
                $oInstance = new $sController($this);

                /**
                 * We need to look for the appropriate method; we'll look in the following order:
                 *
                 * - {sRequestMethod}Remap()
                 * - {sRequestMethod}{method}()
                 * - anyRemap()
                 * - any{method}()
                 *
                 * The second parameter is whether the method is a remap method or not.
                 */
                $bDidFindRoute = false;
                $aMethods      = [
                    [
                        strtolower($this->sRequestMethod) . 'Remap',
                        true,
                    ],
                    [
                        strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
                        false,
                    ],
                    [
                        'anyRemap',
                        true,
                    ],
                    [
                        'any' . ucfirst($this->sMethod),
                        false,
                    ],
                ];

                foreach ($aMethods as $aMethodName) {

                    $sMethod  = getFromArray(0, $aMethodName);
                    $bIsRemap = (bool) getFromArray(1, $aMethodName);
                    if (is_callable([$oInstance, $sMethod])) {

                        $bDidFindRoute = true;

                        /**
                         * If the method we're trying to call is a remap method, then the first
                         * param should be the name of the method being called
                         */
                        if ($bIsRemap) {
                            $oResponse = call_user_func_array([$oInstance, $sMethod], [$this->sMethod]);
                        } else {
                            $oResponse = call_user_func([$oInstance, $sMethod]);
                        }
                        break;
                    }
                }

                if (!$bDidFindRoute) {
                    throw new ApiException(
                        '"' . $this->sRequestMethod . ': ' . $this->sModuleName . '/' . $this->sClassName . '/' . $this->sMethod . '" is not a valid API route.',
                        $oHttpCodes::STATUS_NOT_FOUND
                    );
                }

                if (!($oResponse instanceof ApiResponse)) {
                    //  This is a misconfiguration error, which we want to bubble up to the error handler
                    throw new NailsException(
                        'Return object must be an instance of \Nails\Api\Factory\ApiResponse',
                        $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
                    );
                }

                $aOut = [
                    'status' => $oHttpCodes::STATUS_OK,
                    'data'   => $oResponse->getData(),
                    'meta'   => $oResponse->getMeta(),
                ];

            } catch (ValidationException $e) {

                $aOut = [
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_BAD_REQUEST,
                    'error'   => $e->getMessage() ?: 'An unkown validation error occurred',
                    'details' => $e->getData() ?: [],
                ];
                if (isSuperuser()) {
                    $aOut['exception'] = (object) array_filter([
                        'type' => get_class($e),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                    ]);
                }

                $this->writeLog($aOut);

            } catch (ApiException $e) {

                $aOut = [
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
                    'error'   => $e->getMessage() ?: 'An unkown error occurred',
                    'details' => $e->getData() ?: [],
                ];
                if (isSuperuser()) {
                    $aOut['exception'] = (object) array_filter([
                        'type' => get_class($e),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                    ]);
                }

                $this->writeLog($aOut);

            } catch (\Exception $e) {
                /**
                 * When running in PRODUCTION we want the global error handler to catch exceptions so that they
                 * can be handled proeprly and reported if necessary. In other environments we want to show the
                 * developer the error quickly and with as much info as possible.
                 */
                if (Environment::is(Environment::ENV_PROD)) {
                    throw $e;
                } else {
                    $aOut = [
                        'status'    => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
                        'error'     => $e->getMessage() ?: 'An unkown error occurred',
                        'exception' => (object) array_filter([
                            'type' => get_class($e),
                            'file' => $e->getFile(),
                            'line' => $e->getLine(),
                        ]),
                    ];

                    $this->writeLog($aOut);
                }
            }

            $this->output($aOut);
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Sends $aOut to the browser in the desired format
     *
     * @param  array $aOut The data to output to the browser
     *
     * @return void
     */
    protected function output($aOut = [])
    {
        $oInput     = Factory::service('Input');
        $oOutput    = Factory::service('Output');
        $oHttpCodes = Factory::service('HttpCodes');

        //  Set cache headers
        $oOutput->set_header('Cache-Control: no-store, no-cache, must-revalidate');
        $oOutput->set_header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
        $oOutput->set_header('Pragma: no-cache');

        //  Set access control headers
        $oOutput->set_header('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN);
        $oOutput->set_header('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS));
        $oOutput->set_header('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS));

        // --------------------------------------------------------------------------

        //  Send the correct status header, default to 200 OK
        if ($this->bOutputSendHeader) {
            $sProtocol   = $oInput->server('SERVER_PROTOCOL');
            $iHttpCode   = getFromArray('status', $aOut, $oHttpCodes::STATUS_OK);
            $sHttpString = $oHttpCodes::getByCode($iHttpCode);
            $oOutput->set_header($sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
        }

        // --------------------------------------------------------------------------

        //  Output content
        switch ($this->sOutputFormat) {
            case static::FORMAT_TXT:
                $sOut = $this->outputTxt($aOut);
                break;

            case static::FORMAT_JSON:
                $sOut = $this->outputJson($aOut);
                break;
        }

        $oOutput->set_output($sOut);
    }

    // --------------------------------------------------------------------------

    /**
     * Formats $aOut as a plain text string formatted as JSON (for easy reading)
     * but a plaintext contentType
     *
     * @param  array $aOut The result of the API call
     *
     * @return string
     */
    private function outputTxt($aOut)
    {
        $oOutput = Factory::service('Output');
        $oOutput->set_content_type('text/html');
        if (Environment::not(Environment::ENV_PROD) && defined('JSON_PRETTY_PRINT')) {
            return json_encode($aOut, JSON_PRETTY_PRINT);
        } else {
            return json_encode($aOut);
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Formats $aOut as a JSON string
     *
     * @param  array $aOut The result of the API call
     *
     * @return string
     */
    private function outputJson($aOut)
    {
        $oOutput = Factory::service('Output');
        $oOutput->set_content_type('application/json');
        if (Environment::not(Environment::ENV_PROD) && defined('JSON_PRETTY_PRINT')) {
            return json_encode($aOut, JSON_PRETTY_PRINT);
        } else {
            return json_encode($aOut);
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Sets the output format
     *
     * @param  string $sFormat The format to use
     *
     * @return boolean
     */
    public function outputSetFormat($sFormat)
    {
        if (static::isValidFormat($sFormat)) {
            $this->sOutputFormat = strtoupper($sFormat);
            return true;
        }

        return false;
    }

    // --------------------------------------------------------------------------

    /**
     * Sets whether the status header should be sent or not
     *
     * @param  boolean $sendHeader Whether the header should be sent or not
     *
     * @return void
     */
    public function outputSendHeader($sendHeader)
    {
        $this->bOutputSendHeader = !empty($sendHeader);
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether the format is valid
     *
     * @param string $sFormat The format to check
     *
     * @return boolean
     */
    private static function isValidFormat($sFormat)
    {
        return in_array(strtoupper($sFormat), static::VALID_FORMATS);
    }

    // --------------------------------------------------------------------------

    /**
     * Write a line to the API log
     *
     * @param string $sLine The line to write
     */
    public function writeLog($sLine)
    {
        if (!is_string($sLine)) {
            $sLine = print_r($sLine, true);
        }
        $sLine = ' [' . $this->sModuleName . '->' . $this->sMethod . '] ' . $sLine;
        $this->oLogger->line($sLine);
    }
}


<?php

/**
 * This class builds an entire API interface for content that is managed using a model. It makes some assumptions:
 *
 * - The model does not deviate from the standards defined by the base model
 * - There is only one model to work with
 *
 * @package     Nails
 * @subpackage  module-api
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Api\Controller;

use Nails\Api\Exception\ApiException;
use Nails\Factory;

class DefaultController extends Base
{
    /**
     * The model to use
     */
    const CONFIG_MODEL_NAME     = '';
    const CONFIG_MODEL_PROVIDER = '';

    /**
     * The minimum length of string required for a search to be accepted
     */
    const CONFIG_MIN_SEARCH_LENGTH = 0;

    /**
     * The maximum number of items a user can request at once.
     */
    const CONFIG_MAX_ITEMS_PER_REQUEST = 100;

    /**
     * The maximum number of items to return per page
     */
    const CONFIG_MAX_ITEMS_PER_PAGE = 10;

    /**
     * Which fields to ignore when POSTing
     */
    const CONFIG_POST_IGNORE_FIELDS = ['id', 'slug', 'created', 'is_deleted', 'created_by', 'modified', 'modified_by'];

    // --------------------------------------------------------------------------

    /**
     * DefaultController constructor.
     *
     * @throws ApiException
     *
     * @param $oApiRouter
     */
    public function __construct($oApiRouter)
    {
        parent::__construct($oApiRouter);

        if (empty(static::CONFIG_MODEL_NAME)) {
            throw new ApiException('"static::CONFIG_MODEL_NAME" is required.');
        }
        if (empty(static::CONFIG_MODEL_PROVIDER)) {
            throw new ApiException('"static::CONFIG_MODEL_PROVIDER" is required.');
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Get multiple items
     *
     * @param  array   $aData    Any data to pass to the model
     * @param  integer $iPage    The page to display
     * @param  integer $iPerPage The number of items to display at the moment
     *
     * @return array
     */
    public function getIndex($aData = [], $iPage = null, $iPerPage = null)
    {
        $oInput     = Factory::service('Input');
        $oItemModel = Factory::model(
            static::CONFIG_MODEL_NAME,
            static::CONFIG_MODEL_PROVIDER
        );

        if (is_null($iPage)) {
            $iPage = (int) $oInput->get('page') ?: 1;
        }

        if (is_null($iPerPage)) {
            $iPerPage = static::CONFIG_MAX_ITEMS_PER_PAGE;
        }

        $aResults = $oItemModel->getAll(
            $iPage,
            $iPerPage,
            $aData
        );

        //  @todo (Pablo - 2018-06-24) - Paging
        $oResponse = Factory::factory('ApiResponse', 'nails/module-api');
        $oResponse->setData(array_map([$this, 'formatObject'], $aResults));
        return $oResponse;
    }

    // --------------------------------------------------------------------------

    /**
     * Return an item by it's ID, or an array of items by their ID.
     *
     * @return array
     */
    public function getId($aData = [])
    {
        $sIds       = '';
        $oInput     = Factory::service('Input');
        $oHttpCodes = Factory::service('HttpCodes');

        if (!empty($oInput->get('id'))) {
            $sIds = $oInput->get('id');
        }

        if (!empty($oInput->get('ids'))) {
            $sIds = $oInput->get('ids');
        }

        $aIds = explode(',', $sIds);
        $aIds = array_filter($aIds);
        $aIds = array_unique($aIds);

        if (count($aIds) > self::CONFIG_MAX_ITEMS_PER_REQUEST) {
            throw new ApiException(
                'You can request a maximum of ' . self::CONFIG_MAX_ITEMS_PER_REQUEST . ' items per request',
                $oHttpCodes::STATUS_BAD_REQUEST
            );
        }

        // --------------------------------------------------------------------------

        $oItemModel = Factory::model(
            static::CONFIG_MODEL_NAME,
            static::CONFIG_MODEL_PROVIDER
        );
        $aResults   = $oItemModel->getByIds($aIds, $aData);

        if ($oInput->get('id')) {
            $oItem = reset($aResults);
            $mData = $oItem ? $this->formatObject($oItem) : null;
        } else {
            $mData = array_map([$this, 'formatObject'], $aResults);
        }

        $oResponse = Factory::factory('ApiResponse', 'nails/module-api');
        $oResponse->setData($mData);
        return $oResponse;
    }

    // --------------------------------------------------------------------------

    /**
     * Search for an item
     *
     * @param array $aData The configuration array
     *
     * @return array
     */
    public function getSearch($aData = [])
    {
        $oInput     = Factory::service('Input');
        $oHttpCodes = Factory::service('HttpCodes');
        $oItemModel = Factory::model(
            static::CONFIG_MODEL_NAME,
            static::CONFIG_MODEL_PROVIDER
        );
        $sKeywords  = $oInput->get('search') ?: $oInput->get('keywords');
        $iPage      = (int) $oInput->get('page');

        if (strlen($sKeywords) < static::CONFIG_MIN_SEARCH_LENGTH) {
            throw new ApiException(
                'Search term must be ' . static::CONFIG_MIN_SEARCH_LENGTH . ' characters or longer.',
                $oHttpCodes::STATUS_BAD_REQUEST
            );
        }

        $oResult = $oItemModel->search(
            $sKeywords,
            $iPage,
            static::CONFIG_MAX_ITEMS_PER_PAGE,
            $aData
        );

        $oResponse = Factory::factory('ApiResponse', 'nails/module-api');
        $oResponse->setData(array_map([$this, 'formatObject'], $oResult->data));
        return $oResponse;
    }

    // --------------------------------------------------------------------------

    /**
     * Creates a new item
     *
     * @return array
     */
    public function postRemap()
    {
        $oUri = Factory::service('Uri');
        //  Test that there's not an explicit method defined for this
        $sMethod = 'post' . ucfirst($oUri->segment(4));
        if (method_exists($this, $sMethod)) {
            return $this->$sMethod();
        }

        $oInput     = Factory::service('Input');
        $oHttpCodes = Factory::service('HttpCodes');
        $oItemModel = Factory::model(
            static::CONFIG_MODEL_NAME,
            static::CONFIG_MODEL_PROVIDER
        );
        //  Validate fields
        $aFields  = $oItemModel->describeFields();
        $aValid   = [];
        $aInvalid = [];
        foreach ($aFields as $oField) {
            if (in_array($oField->key, static::CONFIG_POST_IGNORE_FIELDS)) {
                continue;
            }
            $aValid[] = $oField->key;
        }

        $aPost = $oInput->post();
        foreach ($aPost as $sKey => $sValue) {
            if (!in_array($sKey, $aValid)) {
                $aInvalid[] = $sKey;
            }
        }

        if (!empty($aInvalid)) {
            throw new ApiException(
                'The following arguments are invalid: ' . implode(', ', $aInvalid),
                $oHttpCodes::STATUS_BAD_REQUEST
            );
        }

        $iItemId = (int) $oUri->segment(4);
        if ($iItemId) {
            $oItem = $oItemModel->getById($iItemId);
            if (empty($oItem)) {
                throw new ApiException(
                    'Item does not exist',
                    $oHttpCodes::STATUS_NOT_FOUND
                );
            } elseif (!$oItemModel->update($iItemId, $aPost)) {
                throw new ApiException(
                    'Failed to update item. ' . $oItemModel->lastError(),
                    $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
                );
            } elseif (classUses($oItemModel, 'Nails\Common\Traits\Caching')) {
                $oItemModel->disableCache();
            }
            $oItem = $oItemModel->getById($iItemId);
            if (classUses($oItemModel, 'Nails\Common\Traits\Caching')) {
                $oItemModel->enableCache();
            }
        } else {
            $oItem = $oItemModel->create($aPost, true);
        }

        $oResponse = Factory::factory('ApiResponse', 'nails/module-api');
        $oResponse->setData($this->formatObject($oItem));
        return $oResponse;
    }

    // --------------------------------------------------------------------------

    /**
     * Format the output
     *
     * @param \stdClass $oObj The object to format
     *
     * @return array
     */
    protected function formatObject($oObj)
    {
        return [
            'id'    => $oObj->id,
            'label' => $oObj->label,
        ];
    }
}


<?php

namespace Nails\Api\Controller;

use Nails\Api\Exception\ApiException;
use Nails\Api\Factory\ApiResponse;
use Nails\Common\Exception\FactoryException;
use Nails\Common\Exception\NailsException;
use Nails\Factory;

class CrudController extends Base
{
    /**
     * The model to use
     *
     * @var string
     */
    const CONFIG_MODEL_NAME     = '';
    const CONFIG_MODEL_PROVIDER = '';

    /**
     * What to use for looking up resources; ID, SLUG, or TOKEN
     *
     * @var string
     */
    const CONFIG_LOOKUP_METHOD = 'ID';

    /**
     * The $_GET parameter with the search query in it
     *
     * @var string
     */
    const CONFIG_SEARCH_PARAM = 'search';

    /**
     * The $_GET parameter with the ID restrictions in it
     *
     * @var strong
     */
    const CONFIG_IDS_PARAM = 'ids';

    /**
     * The $_GET parameter with the page query in it
     *
     * @var string
     */
    const CONFIG_PAGE_PARAM = 'page';

    /**
     * The number of items to return per page
     *
     * @var integer
     */
    const CONFIG_PER_PAGE = 25;

    /**
     * The default data array to use when looking up an item
     *
     * @var array
     */
    const CONFIG_LOOKUP_DATA = [];

    /**
     * Actions which can be performed
     *
     * @var string
     */
    const ACTION_CREATE = 'CREATE';
    const ACTION_READ   = 'READ';
    const ACTION_UPDATE = 'UPDATE';
    const ACTION_DELETE = 'DELETE';

    /**
     * An array of fields which should be ignored when reading
     *
     * @var array
     */
    const IGNORE_FIELDS_READ = [];

    /**
     * An array of fields which should be ignored when writing
     *
     * @var array
     */
    const IGNORE_FIELDS_WRITE = [
        'id',
        'token',
    ];

    // --------------------------------------------------------------------------

    /**
     * The model instance
     *
     * @var \Nails\Common\Model\Base
     */
    protected $oModel;

    // --------------------------------------------------------------------------

    /**
     * CrudController constructor.
     *
     * @param \ApiRouter $oApiRouter the ApiRouter object
     *
     * @throws NailsException
     */
    public function __construct($oApiRouter)
    {
        parent::__construct($oApiRouter);

        if (empty(static::CONFIG_MODEL_NAME)) {
            throw new NailsException('"static::CONFIG_MODEL_NAME" is required.');
        }
        if (empty(static::CONFIG_MODEL_PROVIDER)) {
            throw new NailsException('"static::CONFIG_MODEL_PROVIDER" is required.');
        }

        $this->oModel = Factory::model(
            static::CONFIG_MODEL_NAME,
            static::CONFIG_MODEL_PROVIDER
        );

    }

    // --------------------------------------------------------------------------

    /**
     * Handles GET requests
     *
     * @param string $sMethod The method being called
     * @param array  $aData   Any data to apply to the requests
     *
     * @return ApiResponse
     * @throws ApiException
     * @throws FactoryException
     */
    public function getRemap($sMethod, array $aData = [])
    {
        $oUri       = Factory::service('Uri');
        $oHttpCodes = Factory::service('HttpCodes');
        if ($oUri->segment(4)) {

            //  Test that there's not an explicit method defined for this
            $sMethod = 'get' . ucfirst($oUri->segment(4));
            if (method_exists($this, $sMethod)) {
                return $this->$sMethod();
            }

            $oItem = $this->lookUpResource($aData);
            if (!$oItem) {
                throw new ApiException(
                    'Resource not found',
                    $oHttpCodes::STATUS_NOT_FOUND
                );
            }

            $this->userCan(static::ACTION_READ, $oItem);
            $oResponse = Factory::factory('ApiResponse', 'nails/module-api');

            //  If there's a submethod defined, call that
            $sSubMethod = $oUri->segment(5);
            if ($sSubMethod && method_exists($this, $sSubMethod)) {
                $oResponse->setData($this->$sSubMethod($oItem));
            } elseif ($sSubMethod && !method_exists($this, $sSubMethod)) {
                throw new ApiException(
                    '"' . $sSubMethod . '" is not a valid subresource',
                    $oHttpCodes::STATUS_NOT_FOUND
                );
            } else {
                $oResponse->setData($this->formatObject($oItem));
            }

        } else {

            $this->userCan(static::ACTION_READ);

            $oInput = Factory::service('Input');
            $aData  = array_merge(static::CONFIG_LOOKUP_DATA, $aData);

            //  Paging
            $iPage = (int) $oInput->get(static::CONFIG_PAGE_PARAM) ?: 1;
            $iPage = $iPage < 0 ? $iPage * -1 : $iPage;

            //  Searching
            if ($oInput->get(static::CONFIG_SEARCH_PARAM)) {
                $aData['keywords'] = $oInput->get(static::CONFIG_SEARCH_PARAM);
            }

            // Requesting specific IDs
            if ($oInput->get(static::CONFIG_IDS_PARAM)) {
                $aData['where_in'] = [
                    [$this->oModel->getcolumn('id'), explode(',', $oInput->get(static::CONFIG_IDS_PARAM))],
                ];
            }

            $iTotal   = $this->oModel->countAll($aData);
            $aResults = array_map(
                [$this, 'formatObject'],
                $this->oModel->getAll(
                    $iPage,
                    static::CONFIG_PER_PAGE,
                    $aData
                )
            );

            $oResponse = Factory::factory('ApiResponse', 'nails/module-api')
                ->setData($aResults)
                ->setMeta([
                    'pagination' => [
                        'page'     => $iPage,
                        'per_page' => static::CONFIG_PER_PAGE,
                        'total'    => $iTotal,
                        'previous' => $this->buildUrl($iTotal, $iPage, -1),
                        'next'     => $this->buildUrl($iTotal, $iPage, 1),
                    ],
                ]);
        }

        return $oResponse;
    }

    // --------------------------------------------------------------------------

    /**
     * Creates a new resource
     *
     * @return array
     */
    public function postIndex()
    {
        $oInput     = Factory::service('Input');
        $oHttpCodes = Factory::service('HttpCodes');

        $this->userCan(static::ACTION_CREATE);

        /**
         * First check the $_POST superglobal, if that's empty then fall back to
         * the body of the request assuming it is JSON.
         */
        $aData = $oInput->post();
        if (empty($aData)) {
            $sData = stream_get_contents(fopen('php://input', 'r'));
            $aData = json_decode($sData, JSON_OBJECT_AS_ARRAY) ?: [];
        }

        $aData   = $this->validateUserInput($aData);
        $iItemId = $this->oModel->create($aData);

        if (empty($iItemId)) {
            throw new ApiException(
                'Failed to create resource. ' . $this->oModel->lastError(),
                $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
            );
        }

        $oItem = $this->oModel->getById($iItemId, static::CONFIG_LOOKUP_DATA);

        $oResponse = Factory::factory('ApiResponse', 'nails/module-api');
        $oResponse->setData($this->formatObject($oItem));

        return $oResponse;
    }

    // --------------------------------------------------------------------------

    /**
     * Updates an existing resource
     *
     * @param string $sMethod The method being called
     *
     * @return array
     */
    public function putRemap($sMethod)
    {
        //  Test that there's not an explicit method defined for this action
        $oUri    = Factory::service('Uri');
        $sMethod = 'put' . ucfirst($oUri->segment(4));
        if (method_exists($this, $sMethod)) {
            return $this->$sMethod();
        }

        // --------------------------------------------------------------------------

        $oItem = $this->lookUpResource();
        if (!$oItem) {
            throw new ApiException('Resource not found', 404);
        }

        $this->userCan(static::ACTION_UPDATE, $oItem);

        $oHttpCodes = Factory::service('HttpCodes');

        //  Read from php:://input as using PUT; expecting a JSONobject as the payload
        $sData = stream_get_contents(fopen('php://input', 'r'));
        $aData = json_decode($sData, JSON_OBJECT_AS_ARRAY) ?: [];
        $aData = $this->validateUserInput($aData, $oItem);

        if (!$this->oModel->update($oItem->id, $aData)) {
            throw new ApiException(
                'Failed to update resource. ' . $this->oModel->lastError(),
                $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
            );
        }

        return Factory::factory('ApiResponse', 'nails/module-api');
    }

    // --------------------------------------------------------------------------

    /**
     * Deletes an existing resource
     *
     * @param string $sMethod The method being called
     *
     * @return array
     */
    public function deleteRemap($sMethod)
    {
        //  Test that there's not an explicit method defined for this action
        $oUri    = Factory::service('Uri');
        $sMethod = 'put' . ucfirst($oUri->segment(4));
        if (method_exists($this, $sMethod)) {
            return $this->$sMethod();
        }

        // --------------------------------------------------------------------------

        $oHttpCodes = Factory::service('HttpCodes');
        $oItem      = $this->lookUpResource();
        if (!$oItem) {
            throw new ApiException(
                'Resource not found',
                $oHttpCodes::STATUS_NOT_FOUND
            );
        }

        $this->userCan(static::ACTION_DELETE, $oItem);

        if (!$this->oModel->delete($oItem->id)) {
            throw new ApiException(
                'Failed to delete resource. ' . $this->oModel->lastError(),
                $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
            );
        }

        return Factory::factory('ApiResponse', 'nails/module-api');
    }

    // --------------------------------------------------------------------------

    /**
     * Fetches an object by it's ID, SLUG, or TOKEN
     *
     * @param array   $aData    Any data to pass to the lookup
     * @param integer $iSegment The segment containing the item's ID/Token/Slug
     *
     * @return \stdClass|false
     * @throws FactoryException
     */
    protected function lookUpResource($aData = [], $iSegment = 4)
    {
        $oUri        = Factory::service('Uri');
        $sIdentifier = $oUri->segment($iSegment);

        //  Handle requests for expansions
        $oInput      = Factory::service('Input');
        $aData       = array_merge(static::CONFIG_LOOKUP_DATA, $aData);
        $aExpansions = array_filter((array) $oInput->get('expand'));
        if ($aExpansions) {
            if (!array_key_exists('expand', $aData)) {
                $aData['expand'] = [];
            }

            $aData['expand'] = array_merge($aData['expand'], $aExpansions);
        }

        switch (static::CONFIG_LOOKUP_METHOD) {
            case 'ID':
                return $this->oModel->getById($sIdentifier, $aData);
                break;
            case 'SLUG':
                return $this->oModel->getBySlug($sIdentifier, $aData);
                break;
            case 'TOKEN':
                return $this->oModel->getByToken($sIdentifier, $aData);
                break;
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Validates a user can perform this action
     *
     * @param string    $sAction The action being performed
     * @param \stdClass $oItem   The item the action is being performed against
     *
     * @throws ApiException
     */
    protected function userCan($sAction, $oItem = null)
    {
        //  By default users can perform any action, apply restrictions by overloading this method
    }

    // --------------------------------------------------------------------------

    /**
     * Validates user input
     *
     * @param array     $aData The user data to validate
     * @param \stdClass $oItem The current object (when editing)
     *
     * @return array
     * @throws ApiException;
     */
    protected function validateUserInput($aData, $oItem = null)
    {
        $aOut       = [];
        $aFields    = $this->oModel->describeFields();
        $aKeys      = array_unique(
            array_merge(
                array_keys($aFields),
                arrayExtractProperty($this->oModel->getExpandableFields(), 'trigger')
            )
        );
        $aValidKeys = array_diff($aKeys, static::IGNORE_FIELDS_WRITE);

        foreach ($aValidKeys as $sValidKey) {

            $oField = getFromArray($sValidKey, $aFields);
            if (array_key_exists($sValidKey, $aData)) {
                $aOut[$sValidKey] = getFromArray($sValidKey, $aData);
            }

            //  @todo (Pablo - 2018-08-20) - Further validation using the $oField->validation rules
        }

        return $aOut;
    }

    // --------------------------------------------------------------------------

    /**
     * Builds pagination URL
     *
     * @param integer $iTotal      The total number of items
     * @param integer $iPage       The current page number
     * @param integer $iPageOffset The offset to the page number
     *
     * @return null|string
     */
    protected function buildUrl($iTotal, $iPage, $iPageOffset)
    {
        $aParams = [
            'page' => $iPage + $iPageOffset,
        ];

        if ($aParams['page'] <= 0) {
            return null;
        } elseif ($aParams['page'] === 1) {
            unset($aParams['page']);
        }

        $iTotalPages = ceil($iTotal / static::CONFIG_PER_PAGE);
        if (!empty($aParams['page']) && $aParams['page'] > $iTotalPages) {
            return null;
        }

        $sUrl = site_url() . uri_string();

        if (!empty($aParams)) {
            $sUrl .= '?' . http_build_query($aParams);
        }

        return $sUrl;
    }

    // --------------------------------------------------------------------------

    /**
     * Formats an object
     *
     * @param \stdClass $oObj The object to format
     *
     * @return \stdClass
     */
    protected function formatObject($oObj)
    {
        foreach (static::IGNORE_FIELDS_READ as $sIgnoredField) {
            unset($oObj->{$sIgnoredField});
        }
        return $oObj;
    }
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\Exceptions\SquidException;
use Squid\Exceptions\NotInTransactionException;
use Squid\Exceptions\AlreadyInTransactionException;

use Squid\MySql\Command\ICmdTransaction;
use Squid\MySql\Command\IMySqlCommandConstructor;
use Squid\MySql\Impl\Traits\TMysqlCommand;

class CmdTransaction implements ICmdTransaction
{
	use TMysqlCommand;

	private $isInTransaction = false;

	/**
	 * @param string $command
	 * @param array $bind
	 * @throws
	 * @return bool
	 */
	private function executeDirect(string $command, array $bind = []): bool
	{
		$cmd = new CmdDirect();
		$cmd->setConnection($this->connection());
		$cmd->command($command, $bind);
		return $cmd->executeDml(false);
	}

	private function executeCommandConstructorSafe(IMySqlCommandConstructor $cmd)
	{
		return $this->executeInTransaction(function ()
			use ($cmd)
			{
				return $cmd->execute();
			});
	}

	/**
	 * @param IMySqlCommandConstructor[] $cmd
	 * @return mixed
	 */
	private function executeCommandConstructorArraySafe(array $cmd)
	{
		return $this->executeInTransaction(function ()
			use ($cmd)
			{
				$result = false;

				foreach ($cmd as $operation)
				{
					$result = $operation->execute();

					if ($result === false) break;
				}

				return $result;
			});
	}

	/**
	 * @param callable $callback
	 * @return mixed
	 */
	private function executeCallbackSafe(callable $callback)
	{
		try
		{
			$result = $callback();

			if ($result === false)
			{
				$this->rollback();
			}
			else
			{
				$this->commit();
			}

			return $result;
		}
		catch (\Throwable $e)
		{
			$this->rollback();
			throw $e;
		}
	}

	/**
	 * Start a new transaction.
	 * @see https://dev.mysql.com/doc/refman/5.7/en/commit.html
	 * @return bool
	 */
	public function startTransaction(): bool
	{
		if ($this->isInTransaction)
			throw new AlreadyInTransactionException();

		$this->isInTransaction = $this->executeDirect('START TRANSACTION');
		return $this->isInTransaction;
	}

	/**
	 * Commit current transaction.
	 * @see https://dev.mysql.com/doc/refman/5.7/en/commit.html
	 * @return bool
	 */
	public function commit(): bool
	{
		if (!$this->isInTransaction)
			throw new NotInTransactionException();

		$this->isInTransaction = false;
		return $this->executeDirect('COMMIT');
	}

	/**
	 * Rollback current transaction.
	 * @see https://dev.mysql.com/doc/refman/5.7/en/commit.html
	 * @return bool
	 */
	public function rollback(): bool
	{
		$this->isInTransaction = false;
		$this->executeDirect('ROLLBACK');
		return true;
	}

	/**
	 * Are we currently inside a transaction.
	 * @return bool
	 */
	public function isInTransaction(): bool
	{
		return $this->isInTransaction;
	}

	/**
	 * @param IMySqlCommandConstructor|IMySqlCommandConstructor[]|callable $operation
	 * @return mixed
	 */
	public function executeInTransaction($operation)
	{
		if ($operation instanceof IMySqlCommandConstructor)
		{
			return $this->executeCommandConstructorSafe($operation);
		}
		else if (is_array($operation))
		{
			return $this->executeCommandConstructorArraySafe($operation);
		}
		else if (is_callable($operation))
		{
			return $this->executeCallbackSafe($operation);
		}
		else
		{
			throw new SquidException('Operation must be an IMySqlCommandConstructor, ' .
				'IMySqlCommandConstructor array or callable!');
		}
	}

	/**
	 * For debug only
	 * @return string Return string in format: "Query string : {json bind params}"
	 */
	public function __toString()
	{
		return 'Transaction Command [' . ($this->isInTransaction ? 'IN TRANSACTION' : 'NO TRANSACTION') . ']';
	}
}


<?php
namespace Squid\MySql\Impl\Traits\CmdTraits;

use Objection\LiteObject;

use Structura\Map;

use Squid\MySql\Exceptions\MySqlException;
use Squid\Exceptions\SquidException;

/**
 * @method \PDOStatement|mixed execute()
 * @method string __toString()
 */
trait TQuery
{
	/**
	 * @inheritdoc
	 */
	private function resolveFetchMode($fetchMode)
	{
		if ($fetchMode === true) return \PDO::FETCH_ASSOC;
		else if ($fetchMode === false) return \PDO::FETCH_NUM;

		return $fetchMode;
	}

	private function parseObject(array $row, string $className): LiteObject
	{
		$object = new $className;

		foreach ($row as $key => $value)
		{
			$object->$key = $value;
		}

		return $object;
	}

	/**
	 * Identical to queryAll(true);
	 * @return array
	 */
	public function query()
	{
		return $this->queryAll(true);
	}

	/**
	 * @param bool|int $isAssoc Will accept \PDO::FETCH_*
	 * @return array|bool
	 */
	public function queryAll($isAssoc = false)
	{
		$result = $this->execute();

		if (!$result) return $result;

		return $result->fetchAll($this->resolveFetchMode($isAssoc));
	}

	/**
	 * @inheritdoc
	 * @throws SquidException
	 */
	public function queryRow($isAssoc = false, $oneOrNone = true)
	{
		$result = $this->execute();

		if (!$result)
		{
			return $result;
		}
		else if ($oneOrNone && $result->rowCount() > 1)
		{
			throw new SquidException('More than one row was selected! ' . $this->__toString());
		}

		return $result->fetch($this->resolveFetchMode($isAssoc));
	}

	/**
	 * @inheritdoc
	 * @throws SquidException
	 */
	public function queryColumn($oneOrNone = true)
	{
		$result = $this->execute();
		$data = [];

		if (!$result)
		{
			return $result;
		}
		else if ($oneOrNone && $result->columnCount() > 1)
		{
			throw new SquidException('More than one column was selected!');
		}

		while ($row = $result->fetch(\PDO::FETCH_NUM))
		{
			$data[] = $row[0];
		}

		return $data;
	}

	/**
	 * @inheritdoc
	 * @throws SquidException
	 */
	public function queryScalar($default = false, $expectOne = true)
	{
		$result = $this->execute();

		if (!$result)
		{
			return $default;
		}
		else if ($expectOne && $result->rowCount() != 1 && $result->columnCount() != 1)
		{
			throw new SquidException('More than one column or row was selected!');
		}
		else if ($result->rowCount() == 0)
		{
			return $default;
		}

		return $result->fetch(\PDO::FETCH_NUM)[0];
	}

	/**
	 * @inheritdoc
	 */
	public function queryInt($expectOne = true)
	{
		$result = $this->queryScalar(false, $expectOne);
		return ($result === false ? false : (int)$result);
	}

	/**
	 * @inheritdoc
	 */
	public function queryBool($expectOne = true)
	{
		$result = $this->queryScalar(false, $expectOne);
		return ($result === false ? false : (bool)$result);
	}

	/**
	 * @inheritdoc
	 */
	public function queryWithCallback($callback, $isAssoc = true)
	{
		$fetchMode = $this->resolveFetchMode($isAssoc);
		$result = $this->execute();

		if (!$result)
			return $result;

		while ($row = $result->fetch($fetchMode))
		{
			$value = call_user_func($callback, $row);

			if ($value === false)	return false;
			else if ($value === 0)	break;
		}

		return true;
	}

	/**
	 * @inheritdoc
	 */
	public function queryIterator($isAssoc = true)
	{
		$fetchMode = $this->resolveFetchMode($isAssoc);
		$result = $this->execute();

		try
		{
			while ($row = $result->fetch($fetchMode))
			{
				yield $row;
			}
		}
		// Free resources when generator released before reaching the end of the iteration.
		finally
		{
			$result->closeCursor();
		}
	}

	/**
	 * Return an array where the result of one column is the index and the second is value.
	 * @param int|string $key Name of the key column.
	 * @param int|string $value Name of the value column
	 * @return array|false
	 */
	public function queryMap($key = 0, $value = 1)
	{
		$fetchMode = $this->resolveFetchMode(is_string($key) || is_string($value));
		$result = $this->execute();
		$map = [];

		try
		{
			while ($row = $result->fetch($fetchMode))
			{
				if (!isset($row[$key]) || !key_exists($value, $row))
					throw new MySqlException(
						"Key '$key' or Value '$value' columns not found in the query result: " .
						implode(array_keys($row)));

				$map[$row[$key]] = $row[$value];
			}
		}
		// Free resources when generator released before reaching the end of the iteration.
		finally
		{
			$result->closeCursor();
		}

		return $map;
	}

	/**
	 * @param string $className LiteObject class name.
	 * @return LiteObject|null
	 */
	public function queryObject(string $className): ?LiteObject
	{
		$result = $this->queryRow(true);

		if (!$result)
			return null;

		return $this->parseObject($result, $className);
	}

	/**
	 * @param string $className LiteObject class name.
	 * @return LiteObject[]
	 */
	public function queryObjects(string $className): array
	{
		$data = $this->query();
		$result = [];

		foreach ($data as $row)
		{
			$result[] = $this->parseObject($row, $className);
		}

		return $result;
	}

	/**
	 * @param string|int $byColumn
	 * @param bool $removeColumn
	 * @return Map
	 */
	public function queryGroupBy($byColumn, bool $removeColumn = false): Map
	{
		$fetchMode = $this->resolveFetchMode(is_string($byColumn));
		$result = $this->execute();
		$map = new Map();

		try
		{
			while ($row = $result->fetch($fetchMode))
			{
				if (!isset($row[$byColumn]))
					throw new MySqlException(
						"Column '$byColumn' not found in the query result: " .
						implode(array_keys($row)));

				$key = $row[$byColumn];

				if ($removeColumn)
				{
					unset($row[$byColumn]);
				}

				if (!$map->has($key))
				{
					$map->add($key, [$row]);
				}
				else
				{
					$map->add($key, array_merge($map->get($key), [$row]));
				}
			}
		}
		// Free resources when generator released before reaching the end of the iteration.
		finally
		{
			$result->closeCursor();
		}

		return $map;
	}

	/**
	 * Return an array where the result of one column is the index and the remaining data is value.
	 * @param int|string $key Name of the key column.
	 * @param bool $removeColumnFromRow Should remove the key column from values.
	 * @return array|false
	 */
	public function queryMapRow($key = 0, $removeColumnFromRow = false)
	{
		$fetchMode = $this->resolveFetchMode(is_string($key));
		$result = $this->execute();
		$map = [];

		try
		{
			while ($row = $result->fetch($fetchMode))
			{
				if (!isset($row[$key]))
					throw new MySqlException(
						"Key '$key' column not found in the query result: " .
						implode(array_keys($row)));

				if ($removeColumnFromRow)
				{
					$map[$row[$key]] = $row;
					unset($map[$row[$key]][$key]);
				}
				else
				{
					$map[$row[$key]] = $row;
				}
			}
		}
		// Free resources when generator released before reaching the end of the iteration.
		finally
		{
			$result->closeCursor();
		}

		return $map;
	}
}


<?php
namespace Squid\MySql\Impl\Connectors;

use Squid\MySql\IMySqlConnector;
use Squid\Exceptions\SquidException;

class FileConnector
{
	/** @var IMySqlConnector */
	private $connector;

	public function __construct(IMySqlConnector $connector = null)
	{
		if ($connector)
			$this->connector = $connector;
	}

	/**
	 * Set the connector to use.
	 * @param IMySqlConnector $connector
	 */
	public function setConnector(IMySqlConnector $connector)
	{
		$this->connector = $connector;
	}

	/**
	 * Execute the entire file. On error the execution is aborted.
	 * @param string $path Full path to the file.
	 * @return bool
	 */
	public function execute($path)
	{
		if (!file_exists($path) || !is_readable($path))
			throw new SquidException("The file at [$path] is unreadable or doesn't exists");

		$data = file_get_contents($path);

		$result = $this->connector
			->bulk()
			->add($data)
			->executeAll();

		return (bool)$result;
	}
}


<?php
namespace Squid\MySql\Impl\Command;

use Squid\MySql\Command\ICmdDirect;

class CmdDirect extends AbstractCommand implements ICmdDirect
{
	use \Squid\MySql\Impl\Traits\CmdTraits\TDml;
	use \Squid\MySql\Impl\Traits\CmdTraits\TQuery;

	private $sql;

	/** @var array */
	private $params;

	/**
	 * Execute this query as a sub query of another query that must return a scalar value.
	 * @param callable $callback Callback to use to create the new query. Callback must be
	 * of format string func(string), where the param is the current query and return value is
	 * the new query to execute.
	 * @param mixed $default Default value to return on failure.
	 * @return bool|mixed
	 */
	private function asScalarSubQuery($callback, $default = false)
	{
		$sql = $this->sql;

		$this->sql = $callback($sql);
		$result = $this->queryScalar(null);
		$this->sql = $sql;

		return (is_null($result) ? $default : $result);
	}

	/**
	 * @return array
	 */
	public function bind()
	{
		return $this->params;
	}

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

	/**
	 * @inheritdoc
	 */
	public function command($sql, array $bind = array())
	{
		$this->sql = $sql;
		$this->params = $bind;

		return $this;
	}

	/**
	 * @inheritdoc
	 */
	public function queryExists()
	{
		$result = $this->asScalarSubQuery(
			function($sql) {
				return "SELECT EXISTS ($sql)";
			},
			null);

		return (is_null($result) ? null : (bool)$result);
	}

	/**
	 * @inheritdoc
	 */
	public function queryCount()
	{
		return $this->asScalarSubQuery(
			function($sql)
			{
				return "SELECT COUNT(*) FROM ($sql) as _sub_";
			});
	}
}