<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\services;

use DaveChild\TextStatistics\TextStatistics;
use ICanBoogie\Inflector;
use Stringy\Stringy;
use SubStringy\SubStringy;
use topshelfcraft\wordsmith\libs\Hacksaw;
use yii\helpers\Markdown;

use craft\base\Component;
use topshelfcraft\wordsmith\Wordsmith;
use topshelfcraft\wordsmith\libs\APTitleCapitalizer;
use topshelfcraft\wordsmith\libs\FullNameParser;
use topshelfcraft\wordsmith\libs\RomanNumerals;
use topshelfcraft\wordsmith\Word;

/**
 * @author Michael Rog <michael@michaelrog.com>
 * @package Wordsmith
 * @since 3.0.0
 */
class WordsmithService extends Component
{

	/*
     * Private properties
     * ===========================================================================
     */

	private $_methods = [

		// Documented methods

		'amp' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'apTitleize' => [
			'source' => 'internal', 'category' => 'casing'
		],
		'automatedReadabilityIndex' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'averageWordsPerSentence' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'between' => [
			'source' => 'Stringy', 'category' => 'substrings'
		],
		'camelize' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'caps' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'chop' => [
			'source' => 'internal', 'category' => 'truncation', 'is_safe' => ['html']
		],
		'colemanLiauIndex' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'daleChallReadabilityScore' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'dasherize' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'emojify' => [
			'source' => 'internal', 'category' => 'emoji'
		],
		'entitle' => [
			'source' => 'internal', 'category' => 'casing'
		],
		'firstName' => [
			'source' => 'FullNameParser', 'category' => 'names'
		],
		'firstWord' => [
			'source' => 'internal', 'category' => 'substrings'
		],
		'fleschKincaidReadingEase' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'fleschKincaidGradeLevel' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'givenName' => [
			'source' => 'FullNameParser', 'category' => 'names'
		],
		'gunningFogScore' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'hacksaw' => [
			'source' => 'internal', 'category' => 'truncation', 'is_safe' => ['html']
		],
		'humanize' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'hyphenate' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'isStringy' => [
			'source' => 'internal', 'category' => 'utilities'
		],
		'lastName' => [
			'source' => 'FullNameParser', 'category' => 'names'
		],
		'lastWord' => [
			'source' => 'internal', 'category' => 'substrings'
		],
		'lowerCaseRoman' => [
			'source' => 'internal', 'category' => 'roman-numerals'
		],
		'markdown' => [
			'source' => 'internal', 'category' => 'markdown', 'is_safe' => ['html']
		],
		'md' => [
			'source' => 'internal', 'category' => 'markdown', 'is_safe' => ['html']
		],
		'ordinal' => [
			'source' => 'Inflector', 'category' => 'inflection'
		],
		'ordinalize' => [
			'source' => 'Inflector', 'category' => 'inflection'
		],
		'parsedown' => [
			'source' => 'internal', 'category' => 'markdown', 'is_safe' => ['html']
		],
		'parsedownExtra' => [
			'source' => 'internal', 'category' => 'markdown', 'is_safe' => ['html']
		],
		'parseName' => [
			'source' => 'FullNameParser', 'category' => 'names'
		],
		'parseUrl' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'pde' => [
			'source' => 'internal', 'category' => 'markdown', 'is_safe' => ['html']
		],
		'pascalize' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'pluralize' => [
			'source' => 'Inflector', 'category' => 'inflection'
		],
		'readTime' => [
			'source' => 'internal', 'category' => 'statistics'
		],
		'sentenceCount' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'singularize' => [
			'source' => 'Inflector', 'category' => 'inflection'
		],
		'slugify' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'smartypants' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'smogIndex' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'spacheReadabilityScore' => [
			'source' => 'TextStatistics', 'category' => 'statistics'
		],
		'substringAfterFirst' => [
			'source' => 'SubStringy', 'category' => 'substrings'
		],
		'substringAfterLast' => [
			'source' => 'SubStringy', 'category' => 'substrings'
		],
		'substringBeforeFirst' => [
			'source' => 'SubStringy', 'category' => 'substrings'
		],
		'substringBeforeLast' => [
			'source' => 'SubStringy', 'category' => 'substrings'
		],
		'substringBetween' => [
			'source' => 'SubStringy', 'category' => 'substrings'
		],
		'substringCount' => [
			'source' => 'SubStringy', 'category' => 'substrings'
		],
		'surname' => [
			'source' => 'FullNameParser', 'category' => 'names'
		],
		'titleize' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'typogrify' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'typogrifyFeed' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'underscore' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'underscored' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'upperCamelize' => [
			'source' => 'Stringy', 'category' => 'casing'
		],
		'upperCaseRoman' => [
			'source' => 'internal', 'category' => 'roman-numerals'
		],
		'urlFragment' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlHost' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlPass' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlPath' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlPort' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlQuery' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlScheme' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'urlUser' => [
			'source' => 'internal', 'category' => 'urls'
		],
		'trim' => [
			'source' => 'internal', 'category' => 'truncation'
		],
		'trimLeft' => [
			'source' => 'internal', 'category' => 'truncation'
		],
		'trimRight' => [
			'source' => 'internal', 'category' => 'truncation'
		],
		'wc' => [
			'source' => 'internal', 'category' => 'statistics'
		],
		'widont' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'wrapAmps' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'wrapCaps' => [
			'source' => 'PHP-Typography', 'category' => 'typography', 'is_safe' => ['html']
		],
		'wordcount' => [
			'source' => 'internal', 'category' => 'statistics'
		],
		'youtubeId' => [
			'source' => 'internal', 'category' => 'urls'
		],

		// Internal junk methods

		'test' => [
			'source' => 'internal', 'category' => 'internal'
		],

	];

    /*
     * Public internal methods
     * ===========================================================================
     */

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

    /*
     * Public API methods
     * a.k.a The ever-lovin Beef
     * ===========================================================================
     */

	/**
	 * Alias for `wrapAmps()`
	 *
	 * @param string $s
	 * @param string $class
	 * @return string
	 */
	public function amp($s, $class = 'amp') : string
	{
		return $this->wrapAmps($s, $class);
	}

	/**
	 * Intelligently up-cases a string as a headline/title using AP capitalization rules.
	 *
	 * @param string $s
	 * @param array $protectedWords
	 * @return string
	 */
	public function apTitleize($s, $protectedWords = []) : string
	{
		return (new APTitleCapitalizer($protectedWords))->capitalize($s);
	}

	/**
	 * Gives the Automated Readability Index of the text, from 0 to 12.
	 *
	 * @param $s
	 * @return int
	 */
	public function automatedReadabilityIndex($s) : int
	{
		return (new TextStatistics())->automatedReadabilityIndex($s);
	}

	/**
	 * Returns the average number of words per sentence in the text.
	 *
	 * @param $s
	 * @return int
	 */
	public function averageWordsPerSentence($s) : int
	{
		return (new TextStatistics())->averageWordsPerSentence($s);
	}

	/**
	 * Returns the substring between $start and $end, if found, or an empty
	 * string. An optional offset may be supplied from which to begin the
	 * search for the start string.
	 *
	 * c.f. Stringy::between()
	 *
	 * @param  string $start  Delimiter marking the start of the substring
	 * @param  string $end    Delimiter marking the end of the substring
	 * @param  int    $offset Index from which to begin the search
	 *
	 * @return string
	 */
	public function between($s, $start, $end, int $offset = 0) : string
	{
		return (string) Stringy::create($s)->between($start, $end, $offset);
	}

	/**
	 * Returns a camelCase version of the string.
	 *
	 * c.f. Stringy::camelize()
	 *
	 * @param string $s
	 * @return string
	 */
	public function camelize($s) : string
	{
		return (string) Stringy::create($s)->camelize();
	}

	/**
	 * Alias for `wrapCaps()`
	 *
	 * @param string $s
	 * @param string $class
	 * @return string
	 */
	public function caps($s, $class = 'caps') : string
	{
		return $this->wrapCaps($s, $class);
	}

	/**
	 * Helps chop your content down to a manageable size.
	 *
	 * @param $s
	 * @param int $limit
	 * @param string $unit
	 * @param null $append
	 * @param null $allowedTags
	 * @return string
	 */
	public function chop($s, $limit = 1, $unit = 'p', $append = null, $allowedTags = null) : string
	{
		return (new Hacksaw())->chop($s, $limit, $unit, $append, $allowedTags);
	}

	/**
	 * Gives the Coleman-Liau Index of the text, from 0 to 12.
	 *
	 * @param $s
	 * @return int
	 */
	public function colemanLiauIndex($s) : int
	{
		return (new TextStatistics())->colemanLiauIndex($s);
	}

	/**
	 * Gives the Dale-Chall readability score of the text, from 0 to 10.
	 *
	 * @param $s
	 * @return int
	 */
	public function daleChallReadabilityScore($s) : int
	{
		return (new TextStatistics())->daleChallReadabilityScore($s);
	}

	/**
	 * Returns a lowercase and trimmed string separated by dashes.
	 *
	 * c.f. Stringy::dasherize()
	 *
	 * @param string $s
	 * @return string
	 */
	public function dasherize($s) : string
	{
		return (string) Stringy::create($s)->dasherize();
	}

	/**
	 * Replaces emoticons and Emoji name codes in text with their actual Unicode Emoji characters.
	 *
	 * @param $s
	 * @param string $name_delimiter
	 * @param string $emoticon_delimiter
	 *
	 * @return string
	 */
	public function emojify($s, $name_delimiter=':', $emoticon_delimiter='') : string
	{
		return Wordsmith::$plugin->emoji->emojify($s, $name_delimiter, $name_delimiter, $emoticon_delimiter, $emoticon_delimiter);
	}

	/**
	 * Alias for `apTitleize()`
	 *
	 * @param $s
	 * @return string
	 */
	public function entitle($s) : string
	{
		return $this->apTitleize($s);
	}

	/**
	 * Alias for `givenName()`
	 *
	 * Attempts to parse a string as a name and returns the first/given name component.
	 *
	 * @param $s
	 * @return string
	 */
	public function firstName($s) : string
	{
		return $this->givenName($s);
	}

	/**
	 * Returns the first word in the string, or an empty string if the source string is empty.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function firstWord($s)
	{
		if (empty($s))
		{
			return null;
		}
		return (Stringy::create($s)->collapseWhitespace()->split(' ', 2))[0];
	}

	/**
	 * Gives the Flesch-Kincaid Reading Ease of the text, from 0 to 100.
	 *
	 * @param $s
	 * @return int
	 */
	public function fleschKincaidReadingEase($s) : int
	{
		return (new TextStatistics())->fleschKincaidReadingEase($s);
	}

	/**
	 * Gives the Flesch-Kincaid Grade level of the text, from 0 to 12.
	 *
	 * @param $s
	 * @return int
	 */
	public function fleschKincaidGradeLevel($s) : int
	{
		return (new TextStatistics())->fleschKincaidGradeLevel($s);
	}

	/**
	 * Gives the Gunning-Fog score of the text, from 0 to 19.
	 *
	 * @param $s
	 * @return int
	 */
	public function gunningFogScore($s) : int
	{
		return (new TextStatistics())->gunningFogScore($s);
	}

	/**
	 * Attempts to parse a string as a name and returns the first/given name component.
	 *
	 * @param $s
	 * @return string
	 */
	public function givenName($s) : string
	{
		$parts = $this->parseName($s);
		return $parts['givenName'];
	}

	/**
	 * Alias for `chop()`
	 *
	 * Provides additional parameters beyond the `chop` method in order to provide
	 * backwards compatibility with the old Hacksaw plugins.
	 *
	 * @param $s
	 * @param string $unit
	 * @param int $limit
	 * @param null $allowedTags
	 * @param null $append
	 * @param null $hack
	 * @param null $allow
	 * @param null $chars
	 * @param null $chars_start
	 * @param null $words
	 * @return string
	 */
	public function hacksaw($s, $unit = 'p', $limit = 1, $allowedTags = null, $append = null, $hack = null, $allow = null, $chars = null, $chars_start = null, $words = null) : string
	{
		if ($hack !== null)
		{
			$unit = $hack;
		}

		if ($allow !== null)
		{
			$allowedTags = $allow;
		}

		if ($chars !== null)
		{

			$unit = 'c';
			$limit = $chars;

			if ($chars_start !== null)
			{
				$s = mb_substr($s, $chars_start);
			}

		}

		if ($words !== null)
		{
			$unit = 'w';
			$limit = $words;
		}

		return $this->chop($s, $limit, $unit, $append, $allowedTags);
	}

	/**
	 * Capitalizes the first word of the string, replaces underscores with spaces, and strips away an '_id' suffix if one is present.
	 *
	 * c.f. Stringy::humanize()
	 *
	 * @param string $s
	 * @return string
	 */
	public function humanize($s) : string
	{
		return Stringy::create($s)->humanize();
	}

	/**
	 * Alias for `dasherize()`
	 *
	 * @param string $s
	 * @return string
	 */
	public function hyphenate($s) : string
	{
		return $this->dasherize($s);
	}

    /**
     * Returns whether a thingy is stringy.
     *
     * Thingies that are stringy include: a string, a printable number, or a string-castable object
     *
     * @param $thingy
     * @return bool
     */
    public function isStringy($thingy) : bool
    {
        return (
            is_string($thingy)
            || is_numeric($thingy)
            || (is_object($thingy) && method_exists($thingy, '__toString' ))
        );
    }

	/**
	 * Alias for `surname()`
	 *
	 * Attempts to parse a string as a name and returns the surname (e.g. 'last name') component.
	 *
	 * @param $s
	 * @return string
	 */
	public function lastName($s) : string
	{
		return $this->surname($s);
	}

	/**
	 * Returns the last word in the string, or an empty string if the source string is empty.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function lastWord($s)
	{
		if (empty($s))
		{
			return null;
		}
		$words = Stringy::create($s)->collapseWhitespace()->split(' ');
		return (string) current(end($words));
	}

	/**
	 * Lowercases any Roman numerals found in the string.
	 *
	 * @param $s
	 * @param string $matchMode
	 * @return string
	 */
	public function lowerCaseRoman($s, $matchMode = 'strict') : string
	{
		$result = RomanNumerals::romanNumeralMatchCallback(
			$s,
			$matchMode,
			function (array $matches) {
				if (empty($matches[1])) {
					return $matches[1];
				}
				return strtolower($matches[1]);
			}
		);
		return $result;
	}

	/**
	 * Parses text through Markdown.
	 *
	 * @param $s
	 * @param string $flavor
	 * @param bool $inlineOnly
	 * @return string
	 */
	public function markdown($s, $flavor = 'gfm', $inlineOnly = false) : string
	{

		// If flavor is 'extra' we're looking for Parsedown

		if ($flavor == 'extra')
		{

			if ($inlineOnly) {
				return (new \ParsedownExtra())->line($s);
			} else {
				return (new \ParsedownExtra())->text($s);
			}

		}

		// Otherwise, use Craft's built-in Yii-powered parser

		$flavor = ($flavor == 'yii-extra' ? 'extra' : $flavor);

		if ($inlineOnly) {
			return Markdown::processParagraph($s, $flavor);
		} else {
			return Markdown::process($s, $flavor);
		}

	}

	/**
	 * Alias for `markdown()`
	 *
	 * @param $s
	 * @param string $flavor
	 * @param bool $inlineOnly
	 * @return string
	 */
	public function md($s, $flavor = 'gfm', $inlineOnly = false) : string
	{
		return $this->markdown($s, $flavor, $inlineOnly);
	}

	/**
	 * Returns the ordinal suffix for the given number.
	 *
	 * @param $num
	 * @return string
	 */
	public function ordinal($num) : string
	{
		return Inflector::get()->ordinal($num);
	}

	/**
	 * Returns the ordinalized variation of the given number.
	 *
	 * @param $num
	 * @return string
	 */
	public function ordinalize($num) : string
	{
		return Inflector::get()->ordinalize($num);
	}

	/**
	 * Alias for `markdown()` using the 'gfm' flavor.
	 *
	 * @param $s
	 * @param bool $inlineOnly
	 * @return string
	 */
	public function parsedown($s, $inlineOnly = false) : string
	{
		return $this->markdown($s, 'gfm', $inlineOnly);
	}

	/**
	 * Alias for `markdown()` using the 'extra' flavor.
	 *
	 * @param $s
	 * @param bool $inlineOnly
	 * @return string
	 */
	public function parsedownExtra($s, $inlineOnly = false) : string
	{
		return $this->markdown($s, 'extra', $inlineOnly);
	}

	/**
	 * Attempts to parse a string as a name and returns its component parts (i.e. firstName, lastName, etc.)
	 *
	 * c.f. FullNameParser::parse()
	 *
	 * (We hijack the return to provide nicer key names than FullNameParser uses out of the box.)
	 *
	 * @param $s
	 * @return array
	 */
	public function parseName($s) : array
	{

		$parts = FullNameParser::parse($s);

		return [
			'nickname' => $parts['nickname'] ?? '',
			'salutation' => $parts['salutation'],
			'givenName' => $parts['fname'],
			'firstName' => $parts['fname'],
			'initials' => $parts['initials'],
			'lastName' => $parts['lname'],
			'surname' => $parts['lname'],
			'lastNameBase' => $parts['lname_base'],
			'surnameBase' => $parts['lname_base'],
			'lastNameCompound' => $parts['lname_compound'],
			'surnameCompound' => $parts['lname_compound'],
			'suffix' => $parts['suffix'],
		];

	}

	/**
	 * Attempts to parse a string as a URL and returns its component parts (i.e. scheme, host, path, etc.)
	 *
	 * @param $s
	 * @return array
	 */
	public function parseUrl($s) : array
	{

		$parts = parse_url($s);

		return [
			'scheme' => $parts['scheme'] ?? null,
			'host' => $parts['host'] ?? null,
			'port' => $parts['port'] ?? null,
			'user' => $parts['user'] ?? null,
			'pass' => $parts['pass'] ?? null,
			'path' => $parts['path'] ?? null,
			'query' => $parts['query'] ?? null,
			'fragment' => $parts['fragment'] ?? null,
		];

	}

	/**
	 * Alias for `parsedownExtra()`
	 *
	 * @param $s
	 * @param bool $inlineOnly
	 * @return string
	 */
	public function pde($s, $inlineOnly = false) : string
	{
		return $this->parsedownExtra($s, $inlineOnly);
	}

	/**
	 * Alias for `upperCamelize()`
	 *
	 * @param string $s
	 * @return string
	 */
	public function pascalize($s) : string
	{
		return $this->upperCamelize($s);
	}

	/**
	 * Returns the correct singular/plural form of the given word, for the provided quantity and language.
	 *
	 * @param $s
	 * @param int $qty
	 * @param string $locale
	 * @return string
	 */
	public function pluralize($s, $qty = 2, $locale = 'en') : string
	{
		if (intval($qty) != 1)
		{
			return Inflector::get($locale)->pluralize($s);
		}
		return Inflector::get($locale)->singularize($s);
	}

	/**
	 * Calculates the units of time required for an average person to read the given passage of text.
	 *
	 * // TODO: Add label and inflection
	 *
	 * @param string $s
	 * @param int $rate
	 * @param int $minimum
	 * @return mixed
	 */
	public function readTime($s, $rate = 200, $minimum = 1)
	{
		$words = $this->wordcount($s);
		return max( intval($minimum), floor($words / intval($rate)) );
	}

	/**
	 * Returns an approximate count of sentences in the text.
	 *
	 * @param $s
	 * @return int
	 */
	public function sentenceCount($s) : int
	{
		return (new TextStatistics())->sentenceCount($s);
	}

	/**
	 * Returns the singular form of the given word.
	 *
	 * @param $s
	 * @param string $locale
	 * @return string
	 */
	public function singularize($s, $locale = 'en') : string
	{
		return Inflector::get($locale)->singularize($s);
	}

	/**
	 * Converts the string into an URL slug.
	 *
	 * c.f. Stringy::slugify()
	 *
	 * @param string $s
	 * @return string
	 */
	public function slugify($s) : string
	{
		return (string) Stringy::create($s)->slugify();
	}

	/**
	 * c.f. PHP-Typography
	 *
	 * @param string $s
	 * @return string
	 */
	public function smartypants($s, $settings = []) : string
	{
		return Wordsmith::$plugin->typography->smartypants($s, $settings);
	}

	/**
	 * Gives the SMOG Index of the text, from 0 to 12.
	 *
	 * @param $s
	 * @return int
	 */
	public function smogIndex($s) : int
	{
		return (new TextStatistics())->smogIndex($s);
	}

	/**
	 * Gives the Spache readability score of the text, from 0 to 5.
	 * (This scale is not really suitable for measuring readability above grade 4.)
	 *
	 * @param $s
	 * @return int
	 */
	public function spacheReadabilityScore($s) : int
	{
		return (new TextStatistics())->spacheReadabilityScore($s);
	}

	/**
	 * Gets the substring after the first occurrence of a separator.
	 * If no match is found returns an empty string.
	 *
	 * (We're hijacking the return from SubStringy to return a `null` instead of boolean `false` if the substring isn't matched.)
	 *
	 * c.f. SubStringy::substringAfterFirst()
	 *
	 * @param string $s
	 * @param string $separator
	 *
	 * @return string|null
	 */
	public function substringAfterFirst($s, $separator)
	{
		$s = SubStringy::create($s)->substringAfterFirst($separator);
		return ($s === false ? null : $s);
	}

	/**
	 * Gets the substring after the last occurrence of a separator.
	 * If no match is found returns an empty string.
	 *
	 * (We're hijacking the return from SubStringy to return a `null` instead of boolean `false` if the substring isn't matched.)
	 *
	 * c.f. SubStringy::substringAfterLast()
	 *
	 * @param string $s
	 * @param string $separator
	 *
	 * @return Word
	 */
	public function substringAfterLast($s, $separator)
	{
		$s = SubStringy::create($s)->substringAfterLast($separator);
		return ($s === false ? null : $s);
	}

	/**
	 * Gets the substring before the first occurrence of a separator.
	 * If no match is found returns an empty string.
	 *
	 * (We're hijacking the return from SubStringy to return a `null` instead of boolean `false` if the substring isn't matched.)
	 *
	 * c.f. SubStringy::substringBeforeFirst()
	 *
	 * @param string $s
	 * @param string $separator
	 *
	 * @return Word
	 */
	public function substringBeforeFirst($s, $separator)
	{
		$s = SubStringy::create($s)->substringBeforeFirst($separator);
		return ($s === false ? null : $s);
	}

	/**
	 * Gets the substring before the last occurrence of a separator.
	 * If no match is found returns an empty string.
	 *
	 * (We're hijacking the return from SubStringy to return a `null` instead of boolean `false` if the substring isn't matched.)
	 *
	 * c.f. SubStringy::substringBeforeLast()
	 *
	 * @param string $s
	 * @param string $separator
	 *
	 * @return Word
	 */
	public function substringBeforeLast($s, $separator)
	{
		$s = SubStringy::create($s)->substringBeforeLast($separator);
		return ($s === false ? null : $s);
	}

	/**
	 * Count occurrences of the substring in the source string.
	 *
	 * @param string $s
	 * @param string $substring
	 *
	 * c.f. SubStringy::substringCount()
	 *
	 * @return int
	 */
	public function substringCount($s, $substring) : int
	{
		return SubStringy::create($s)->substringCount($substring);
	}

	/**
	 * Attempts to parse a string as a name and returns the surname (i.e. 'last name') component.
	 *
	 * @param $s
	 * @return string
	 */
	public function surname($s) : string
	{
		$parts = $this->parseName($s);
		return $parts['surname'];
	}

	/**
	 * Returns a trimmed string with the first letter of each word capitalized.
	 *
	 * c.f. Stringy::titleize()
	 *
	 * @param string $s
	 * @param array $ignore
	 * @return string
	 */
	public function titleize($s, $ignore=[]) : string
	{
		return (string) Stringy::create($s)->titleize($ignore);
	}

	/**
	 * Returns a string with whitespace removed from the start and end of the string.
	 *
	 * c.f. Stringy::trim()
	 *
	 * @param string $s
	 * @param string $chars
	 * @return string
	 */
	public function trim($s, $chars = null) : string
	{
		return (string) Stringy::create($s)->trim($chars);
	}

	/**
	 * Returns a string with whitespace removed from the start of the string.
	 *
	 * c.f. Stringy::trimLeft()
	 *
	 * @param string $s
	 * @param string $chars
	 * @return string
	 */
	public function trimLeft($s, $chars = null) : string
	{
		return (string) Stringy::create($s)->trimLeft($chars);
	}

	/**
	 * Returns a string with whitespace removed from the end of the string.
	 *
	 * c.f. Stringy::trimRight()
	 *
	 * @param string $s
	 * @param string $chars
	 * @return string
	 */
	public function trimRight($s, $chars = null) : string
	{
		return (string) Stringy::create($s)->trimRight($chars);
	}

	/**
	 * Typogrify applies a full suite of typographic treatments to beautify your web typography.
	 *
	 * c.f. PHP-Typography
	 *
	 * @param string $s
	 * @return string
	 */
	public function typogrify($s, $settings = []) : string
	{
		return Wordsmith::$plugin->typography->typogrify($s, $settings);
	}

	/**
	 * Applies a full suite of typographic treatments to beautify your web typography,
	 * in a way that is appropriate for RSS feeds (i.e. excluding processes that may not display well
	 * with limited character set intelligence).
	 *
	 * c.f. PHP-Typography
	 *
	 * @param string $s
	 * @return string
	 */
	public function typogrifyFeed($s, $settings = []) : string
	{
		return Wordsmith::$plugin->typography->typogrifyFeed($s, $settings);
	}

	/**
	 * Returns a lowercase and trimmed string separated by underscores.
	 *
	 * c.f. Stringy::underscored()
	 *
	 * @param string $s
	 * @return string
	 */
	public function underscore($s) : string
	{
		return (string) Stringy::create($s)->underscored();
	}

	/**
	 * Alias for `underscore()`
	 *
	 * @param string $s
	 * @return Word
	 */
	public function underscored($s)
	{
		return $this->underscore($s);
	}

	/**
	 * Uppercases any Roman numerals found in the string.
	 *
	 * @param $s
	 * @param string $match_mode
	 * @return string
	 */
	public function upperCaseRoman($s, $match_mode = 'strict') : string
	{
		$result = RomanNumerals::romanNumeralMatchCallback(
			$s,
			$match_mode,
			function (array $matches) {
				if (empty($matches[1])) {
					return $matches[1];
				}
				return strtoupper($matches[1]);
			}
		);
		return $result;
	}

	/**
	 * Returns an UpperCamelCase version of the supplied string.
	 *
	 * c.f. Stringy::upperCamelize()
	 *
	 * @param string $s
	 * @return string
	 */
	public function upperCamelize($s) : string
	{
		return (string) Stringy::create($s)->upperCamelize();
	}

	/**
	 * Returns the fragment component of a URL. (i.e. the part after a hashmark '#', which usually represents an anchor on the target page.)
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlFragment($s)
	{
		return ($this->parseUrl($s))['fragment'];
	}

	/**
	 * Returns the host name component of a URL.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlHost($s)
	{
		return ($this->parseUrl($s))['host'];
	}

	/**
	 * Returns the password component of a URL.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlPass($s)
	{
		return ($this->parseUrl($s))['pass'];
	}

	/**
	 * Returns the resource path component of a URL.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlPath($s)
	{
		return ($this->parseUrl($s))['path'];
	}

	/**
	 * Returns the port number component of a URL.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlPort($s)
	{
		return ($this->parseUrl($s))['port'];
	}

	/**
	 * Returns the query string component of a URL.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlQuery($s)
	{
		return ($this->parseUrl($s))['query'];
	}

	/**
	 * Returns the protocol component of a URL (e.g. 'http', 'https').
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlScheme($s)
	{
		return ($this->parseUrl($s))['scheme'];
	}

	/**
	 * Returns the user component of a URL.
	 *
	 * @param $s
	 * @return string|null
	 */
	public function urlUser($s)
	{
		return ($this->parseUrl($s))['user'];
	}

	/**
	 * Alias for `wordcount()`
	 *
	 * @param $s
	 * @return int
	 */
	public function wc($s) : int
	{
		return $this->wordcount($s);
	}

	/**
	 * Replaces the space between the last two words in a string with a ` ` to prevent widowing.
	 *
	 * c.f. PHP-Typography
	 *
	 * @param string $s
	 * @param array $settings
	 * @return string
	 */
	public function widont($s, $settings = []) : string
	{
		return Wordsmith::$plugin->typography->widont($s, $settings);
	}

	/**
	 * Wraps ampersands in `<span class="amp">` so they can be styled with CSS.
	 *
	 * c.f. PHP-Typography
	 *
	 * @param string $s
	 * @param string $class
	 * @param array $settings
	 * @return string
	 */
	public function wrapAmps($s, $class = 'amp', $settings = []) : string
	{
		return Wordsmith::$plugin->typography->wrapAmps($s, $class, $settings);
	}

	/**
	 * Wraps all-uppercase words in `<span class="caps">` so they can be styled with CSS.
	 *
	 * c.f. PHP-Typography
	 *
	 * @param string $s
	 * @param string $class
	 * @param array $settings
	 * @return string
	 */
	public function wrapCaps($s, $class = 'caps', $settings = []) : string
	{
		return Wordsmith::$plugin->typography->wrapCaps($s, $class, $settings);
	}

	/**
	 * // TODO: Document `wordcount`
	 *
	 * @param $s
	 * @return int
	 */
	public function wordcount($s) : int
	{
		return Stringy::create(strip_tags($s))->collapseWhitespace()->countSubstr(' ') + 1;
	}

	/**
	 * Returns the YouTube video ID from a URL, if one is present in the URL.
	 *
	 * c.f. https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url
	 *
	 * @param $s
	 * @return string|null
	 */
    public function youtubeId($s)
	{
		if (preg_match("/^.*(youtu.be\/|v\/|e\/|u\/\w+\/|embed\/|v=)([^#\&\?]*).*/", $s, $matches))
		{
			return $matches[2];
		}
		return null;
	}

	/*
	 * TEST METHODS
	 *
	 */

	/**
	 * @param null $arg1
	 * @param null $arg2
	 * @param null $arg3
	 * @param null $arg4
	 * @return mixed
	 */
	public function test($arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null)
	{
		return print_r(
			[$arg1, $arg2, $arg3, $arg4],
			true
		);
	}

	public function testEmptyStringy()
	{
		$stringy = Stringy::create('') ? 'not empty' : 'empty';
		$string = '' ? 'not empty' : 'empty';
		var_dump([$stringy, $string]);
	}

	/*
	 * MAGIC METHODS
	 *
	 *
	 *
	 */

	/**
	 * @param string $name
	 * @param array $params
	 * @return mixed|void
	 */
	public function __call($name, $params)
	{

//		exit(print_r([$name, $params], true));
//		return parent::__call($name, $params); // TODO: Change the autogenerated stub
//		exit("go away");
//		if (!method_exists($callable[0], $callable[1])) {
//			// __call()
//			return array(null, array());
//		}
//		$r = new ReflectionMethod($callable[0], $callable[1]);
//		throw new \BadMethodCallException();
	}

}


<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\libs;

/*
 * This file is based on the `RomanNumeralTrait` class from GeckoPackages.
 * c.f. https://github.com/GeckoPackages/GeckoTwig
 *
 * The original source code is (c) GeckoPackages https://github.com/GeckoPackages
 * c.f. https://github.com/GeckoPackages/GeckoTwig/blob/2.x/LICENSE
 */

/**
 * Supports the Roman numerals in modern notation (strict) or loose notation.
 *
 * Roman | Value
 * ------+-------
 *  I    |    1
 *  IV   |    4
 *  V    |    5
 *  IX   |    9
 *  X    |   10
 *  XL   |   40
 *  L    |   50
 *  XC   |   90
 *  C    |  100
 *  CD   |  400
 *  D    |  500
 *  CM   |  900
 *  M    | 1000
 *
 * Note: large numbers, for example in 'apostrophus' and 'vinculum' are not supported.
 *
 * In 'strict' mode:
 * - Symbols are combined from left to right, high to low values.
 * - Symbols are not repeated more than 3 times.
 * - C may b placed after D or M.
 * - X may be placed before L or C.
 * - I may be placed before V or X.
 * - This makes the maximum number supported 'MMMCMXCIX'.
 *
 * In 'loose' mode, follows 'strict' mode with the following exceptions:
 * - Symbols may be repeated more than 3 times.
 * - There is no more maximum number.
 *
 * In 'loose-order', follows 'loose' mode with the following exception:
 * - Symbols may appear in any order.
 *
 * More details:
 * https://en.wikipedia.org/wiki/Roman_numerals
 *
 *
 * @author SpacePossum
 */
class RomanNumerals
{

	/**
	 * @param mixed $string       String to modify
	 * @param string $matchMode   'strict', 'loose' or 'loose-order'
	 * @param callable $callback  Callback that does the manipulation
	 *
	 * @return string
	 */
	public static function romanNumeralMatchCallback($string, $matchMode, callable $callback)
	{

		switch ($matchMode) {
			case 'loose':
			{
				// Note: empty strings are also captured.
				$matchMode = '#\b(M*(?:CM|CD|D?C*)(?:XC|XL|L?X*)(?:IX|IV|V?I*)\b)#i';
				break;
			}
			case 'loose-order':
			{
				$matchMode = '#\b([MDCLXVI]+)\b#i';
				break;
			}
			case 'strict':
			default:
			{
				// Note: empty strings are also captured.
				$matchMode = '#\b(M{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})\b)#i';
				break;
			}
		}

		return preg_replace_callback($matchMode, $callback, $string);

	}

}


<?php

namespace Omnipay\PaymentExpress\Message;

use SimpleXMLElement;
use Omnipay\Common\Message\AbstractRequest;

/**
 * PaymentExpress PxPay Authorize Request
 *
 * @link https://www.paymentexpress.com/Technical_Resources/Ecommerce_Hosted/PxPay_2_0
 */
class PxPayAuthorizeRequest extends AbstractRequest
{
    /**
     * PxPay Live Endpoint URL
     *
     * @var string URL
     */
    protected $liveEndpoint = 'https://sec.paymentexpress.com/pxaccess/pxpay.aspx';

    /**
     * PxPay test Endpoint URL
     *
     * @var string URL
     */
    protected $testEndpoint = 'https://uat.paymentexpress.com/pxaccess/pxpay.aspx';

    /**
     * PxPay TxnType
     *
     * @var string TxnType
     */
    protected $action = 'Auth';

    /**
     * Get the PxPay PxPayUserId
     *
     * Unique username to identify customer account.
     *
     * @return mixed
     */
    public function getUsername()
    {
        return $this->getParameter('username');
    }

    /**
     * Set the PxPay PxPayUserId
     *
     * @param string $value
     * @return $this
     */
    public function setUsername($value)
    {
        return $this->setParameter('username', $value);
    }

    /**
     * Get the PxPay PxPayKey
     *
     * @return mixed
     */
    public function getPassword()
    {
        return $this->getParameter('password');
    }

    /**
     * Set the PxPay PxPayKey
     *
     * @param string $value
     * @return $this
     */
    public function setPassword($value)
    {
        return $this->setParameter('password', $value);
    }

    public function getPxPostUsername()
    {
        return $this->getParameter('pxPostUsername');
    }

    public function setPxPostUsername($value)
    {
        return $this->setParameter('pxPostUsername', $value);
    }

    public function getPxPostPassword()
    {
        return $this->getParameter('pxPostPassword');
    }

    public function setPxPostPassword($value)
    {
        return $this->setParameter('pxPostPassword', $value);
    }

    public function getEndpoint()
    {
        return $this->getTestMode() === true ? $this->testEndpoint : $this->liveEndpoint;
    }

    /**
     * Get the PxPay TxnData1
     *
     * Optional free text field that can be used to store information against a
     * transaction. Returned in the response and can be retrieved from DPS
     * reports.
     *
     * @return mixed
     */
    public function getTransactionData1()
    {
        return $this->getParameter('transactionData1');
    }

    /**
     * Set the PxPay TxnData1
     *
     * @param string $value Max 255 bytes
     * @return $this
     */
    public function setTransactionData1($value)
    {
        return $this->setParameter('transactionData1', $value);
    }

    /**
     * Get the PxPay TxnData2
     *
     * Optional free text field that can be used to store information against a
     * transaction. Returned in the response and can be retrieved from DPS
     * reports.
     *
     * @return mixed
     */
    public function getTransactionData2()
    {
        return $this->getParameter('transactionData2');
    }

    /**
     * Set the PxPay TxnData2
     *
     * @param string $value Max 255 bytes
     * @return $this
     */
    public function setTransactionData2($value)
    {
        return $this->setParameter('transactionData2', $value);
    }

    /**
     * Get the PxPay TxnData3
     *
     * Optional free text field that can be used to store information against a
     * transaction. Returned in the response and can be retrieved from DPS
     * reports.
     *
     * @return mixed
     */
    public function getTransactionData3()
    {
        return $this->getParameter('transactionData3');
    }

    /**
     * Set the TxnData3 field on the request
     *
     * @param string $value Max 255 bytes
     * @return $this
     */
    public function setTransactionData3($value)
    {
        return $this->setParameter('transactionData3', $value);
    }

    /**
     * Get the transaction data
     *
     * @return SimpleXMLElement
     */
    public function getData()
    {
        $this->validate('amount', 'returnUrl');

        $data = new SimpleXMLElement('<GenerateRequest/>');
        $data->PxPayUserId = $this->getUsername();
        $data->PxPayKey = $this->getPassword();
        $data->TxnType = $this->action;
        $data->AmountInput = $this->getAmount();
        $data->CurrencyInput = $this->getCurrency();
        $data->UrlSuccess = $this->getReturnUrl();
        $data->UrlFail = $this->getCancelUrl() ?: $this->getReturnUrl();

        if ($this->getDescription()) {
            $data->MerchantReference = $this->getDescription();
        }

        if ($this->getTransactionId()) {
            $data->TxnId = $this->getTransactionId();
        }

        if ($this->getTransactionData1()) {
            $data->TxnData1 = $this->getTransactionData1();
        }

        if ($this->getTransactionData2()) {
            $data->TxnData2 = $this->getTransactionData2();
        }

        if ($this->getTransactionData3()) {
            $data->TxnData3 = $this->getTransactionData3();
        }

        if ($this->getCardReference()) {
            $data->DpsBillingId = $this->getCardReference();
        }

        return $data;
    }

    /**
     * Send request
     *
     * @param  SimpleXMLElement $data
     * @return Omnipay\PaymentExpress\Message\PxPayAuthorizeResponse
     */
    public function sendData($data)
    {
        $httpResponse = $this->httpClient->request('POST', $this->getEndpoint(), [], $data->asXML());

        return $this->createResponse($httpResponse->getBody()->getContents());
    }

    /**
     * Create an authorize response
     *
     * @param  SimpleXMLElement $data
     * @return Omnipay\PaymentExpress\Message\PxPayAuthorizeResponse
     */
    protected function createResponse($data)
    {
        return $this->response = new PxPayAuthorizeResponse($this, simplexml_load_string($data));
    }

    /**
     * Get the request return URL.
     *
     * @return string
     */
    public function getReturnUrl()
    {
        return htmlentities($this->getParameter('returnUrl'));
    }
}


<?php

namespace Xenolope\Quahog;

use InvalidArgumentException;
use Xenolope\Quahog\Exception\ConnectionException;
use Socket\Raw\Socket;

/**
 * Class Client
 * @package Quahog
 */
class Client
{
    const RESULT_OK = 'OK';
    const RESULT_FOUND = 'FOUND';
    const RESULT_ERROR = 'ERROR';

    /** @var Socket $_socket */
    private $_socket;

    /** @var bool $_inSession Has the current connection a Session? */
    private $_inSession = false;
    /** @var int $_timeout Read timeout */
    private $_timeout = 30;
    private $_mode;

    /**
     * Instantiate a Quahog\Client instance.
     *
     * @param Socket $socket An instance of \Socket\Raw\Socket which points to clamd
     * @param int $timeout
     * @param int $mode
     */
    public function __construct(Socket $socket, $timeout = 30, $mode = PHP_BINARY_READ)
    {
        $this->_mode = $mode;
        $this->_socket = $socket;
        $this->_timeout = $timeout;
        if ($mode === PHP_BINARY_READ) {
            trigger_error("Using binary read is deprecated and will be removed in a future release. Explicitly set $mode to PHP_NORMAL_READ to avoid this message.", E_USER_DEPRECATED);
        }
    }

    /**
     * Ping clamd to see if we get a response.
     *
     * @throws ConnectionException
     * @return bool
     */
    public function ping()
    {
        $this->_sendCommand('PING');

        if ($this->_receiveResponse(true) === 'PONG') {
            return true;
        }

        throw new ConnectionException('Could not ping clamd');
    }

    /**
     * Retrieve the running ClamAV version information.
     *
     * @return string
     */
    public function version()
    {
        $this->_sendCommand('VERSION');

        return $this->_receiveResponse(true);
    }

    /**
     * Fetch stats for the ClamAV scan queue.
     *
     * @return string
     */
    public function stats()
    {
        $this->_sendCommand('STATS');

        return $this->_receiveResponse(true, "END\n");
    }

    /**
     * Reload the ClamAV virus definition database.
     *
     * @return string
     */
    public function reload()
    {
        $this->_sendCommand('RELOAD');

        return $this->_receiveResponse();
    }

    /**
     * Shutdown clamd cleanly.
     *
     * @return string
     */
    public function shutdown()
    {
        $this->_sendCommand('SHUTDOWN');

        return $this->_receiveResponse();
    }

    /**
     * Disconnect the client.
     *
     * @return bool
     */
    public function disconnect()
    {
        return $this->_closeConnection();
    }

    /**
     * Scan a single file.
     *
     * @param string $file The location of the file to scan.
     *
     * @return array
     */
    public function scanFile($file)
    {
        $this->_sendCommand('SCAN ' . $file);

        $response = $this->_receiveResponse();

        return $this->_parseResponse($response);
    }

    /**
     * Scan a file or directory recursively using multiple threads.
     *
     * @param string $file The location of the file or directory to scan.
     *
     * @return array
     */
    public function multiscanFile($file)
    {
        $this->_sendCommand('MULTISCAN ' . $file);

        $response = $this->_receiveResponse();

        return $this->_parseResponse($response);
    }

    /**
     * Scan a file or directory recursively.
     *
     * @param string $file The location of the file or directory to scan.
     *
     * @return array
     */
    public function contScan($file)
    {
        $this->_sendCommand('CONTSCAN ' . $file);

        $response = $this->_receiveResponse();

        return $this->_parseResponse($response);
    }

    /**
     * Scan a local file via a stream.
     *
     * @param string $file The location of the file to scan.
     * @param int    $maxChunkSize The maximum chunk size in bytes to send to clamd at a time.
     *
     * @return array
     */
    public function scanLocalFile($file, $maxChunkSize = 1024)
    {
        return $this->scanStream(file_get_contents($file), $maxChunkSize);
    }

    /**
     * Scan a stream.
     *
     * @param resource $stream A file stream
     * @param int      $maxChunkSize The maximum chunk size in bytes to send to clamd at a time.
     *
     * @return array
     * @throws InvalidArgumentException
     */
    public function scanResourceStream($stream, $maxChunkSize = 1024)
    {
        if (!is_resource($stream)) {
            throw new InvalidArgumentException('Passed stream is not a resource!');
        }

        $this->_sendCommand("INSTREAM");

        while ($chunk = fread($stream, $maxChunkSize)) {
            $size = pack('N', strlen($chunk));
            $this->_socket->send($size, MSG_DONTROUTE);
            $this->_socket->send($chunk, MSG_DONTROUTE);
        }

        $this->_socket->send(pack('N', 0), MSG_DONTROUTE);

        $response = $this->_receiveResponse();

        return $this->_parseResponse($response);
    }

    /**
     * Scan a stream.
     *
     * @param string $stream A file stream in string form.
     * @param int    $maxChunkSize The maximum chunk size in bytes to send to clamd at a time.
     *
     * @return array
     */
    public function scanStream($stream, $maxChunkSize = 1024)
    {
        $this->_sendCommand("INSTREAM");

        $chunksLeft = $stream;

        while (strlen($chunksLeft) > 0) {
            $chunk = substr($chunksLeft, 0, $maxChunkSize);
            $chunksLeft = substr($chunksLeft, $maxChunkSize);

            $size = pack('N', strlen($chunk));

            $this->_socket->send($size, MSG_DONTROUTE);
            $this->_socket->send($chunk, MSG_DONTROUTE);
        }

        $this->_socket->send(pack('N', 0), MSG_DONTROUTE);

        $response = $this->_receiveResponse();

        return $this->_parseResponse($response);
    }

    public function startSession()
    {
        $this->_inSession = true;

        $this->_sendCommand('IDSESSION');
    }

    public function endSession()
    {
        $this->_sendCommand('END');

        $this->_inSession = false;
    }

    /**
     * A wrapper to send a command to clamd.
     *
     * @param string $command
     */
    private function _sendCommand($command)
    {
        $this->_socket->send("n$command\n", MSG_DONTROUTE);
    }

    /**
     * A wrapper to cleanly read a response from clamd.
     *
     * @param bool $removeId
     * @param string $readUntil
     * @return string
     * @throws ConnectionException
     */
    private function _receiveResponse($removeId = false, $readUntil = "\n")
    {
        $result = null;
        $readUntilLen = strlen($readUntil);
        do {
            if ($this->_socket->selectRead($this->_timeout)) {
                $rt = $this->_socket->read(4096, $this->_mode);
                if ($rt === "") {
                    break;
                }
                $result .= $rt;
                if (strcmp(substr($result, 0 - $readUntilLen), $readUntil) == 0) {
                    break;
                }
            } else if ($this->_mode === PHP_NORMAL_READ) {
                throw new ConnectionException("Timeout waiting to read response");
            } else {
                break;
            }
        } while (true);

        if (!$this->_inSession) {
            $this->_closeConnection();
        } else if ($removeId) {
            $result = preg_replace('/^\d+: /', "", $result, 1);
        }

        return trim($result);
    }

    /**
     * Explicitly close the current socket's connection.
     *
     * @return bool
     *
     * @throws ConnectionException  If the socket fails to close.
     */
    private function _closeConnection()
    {
        try {
            $this->_socket->close();

            return true;
        } catch (ConnectionException $e) {
            throw $e;
        }
    }

    /**
     * Parse the received response into a structured array ($filename, $reason, $status).
     *
     * @param string $response
     *
     * @return array
     */
    private function _parseResponse($response)
    {
        $splitResponse = explode(': ', $response);

        $idReturn = [];
        if (!$this->_inSession) {
            $filename = $splitResponse[0];
            $message = $splitResponse[1];
        }
        else {
            $idReturn = ['id' => $splitResponse[0]];
            $filename = $splitResponse[1];
            $message = $splitResponse[2];
        }

        if ($message === self::RESULT_OK) {
            return $idReturn + ['filename' => $filename, 'reason' => null, 'status' => self::RESULT_OK];
        } else {
            $parts = explode(' ', $message);
            $status = array_pop($parts);
            $reason = implode(' ', $parts);

            return $idReturn + ['filename' => $filename, 'reason' => $reason, 'status' => $status];
        }
    }
}


<?php
namespace Sirius\Upload;

use Sirius\Upload\Result\Collection;
use Sirius\Upload\Util\Arr;

class HandlerAggregate implements \IteratorAggregate
{

    protected $handlers = array();

    /**
     * Adds a handler on the aggregate
     *
     * @param  string  $selector
     * @param  Handler $handler
     * @return $this
     */
    public function addHandler($selector, Handler $handler)
    {
        $this->handlers[$selector] = $handler;

        return $this;
    }

    /**
     * Processes
     * @param $files
     * @return Collection
     */
    public function process($files)
    {
        $result = new Collection();
        foreach ($this->handlers as $selector => $handler) {
            /* @var $handler Handler */
            $selectedFiles = Arr::getBySelector($files, $selector);

            if (!$selectedFiles || !is_array($selectedFiles) || empty($selectedFiles)) {
                continue;
            }

            foreach ($selectedFiles as $path => $file) {
                if (is_array($file)) {
                    $result[$path] = $handler->process($file);
                }
            }
        }

        return $result;
    }

    /**
     * (PHP 5 >= 5.0.0)

     * Retrieve an external iterator
     *
     * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     * @return \Traversable An instance of an object implementing Iterator or
     *                      Traversable
     */
    public function getIterator()
    {
        return $this->handlers;
    }
}


<?php
namespace Sirius\Upload;

use Sirius\Upload\Container\ContainerInterface;
use Sirius\Upload\Container\Local as LocalContainer;
use Sirius\Upload\Exception\InvalidContainerException;
use Sirius\Upload\Util\Arr;
use Sirius\Validation\ErrorMessage;
use Sirius\Validation\ValueValidator;

class Handler implements UploadHandlerInterface
{
    // constants for constructor options
    const OPTION_PREFIX = 'prefix';
    const OPTION_OVERWRITE = 'overwrite';
    const OPTION_AUTOCONFIRM = 'autoconfirm';

    // constants for validation rules
    const RULE_EXTENSION = 'extension';
    const RULE_SIZE = 'size';
    const RULE_IMAGE = 'image';
    const RULE_IMAGE_HEIGHT = 'imageheight';
    const RULE_IMAGE_WIDTH = 'imagewidth';
    const RULE_IMAGE_RATIO = 'imageratio';

    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * Prefix to be added to the file.
     * It can be a subfolder (if it ends with '/', a string to be used as prefix)
     * or a callback that returns a string
     *
     * @var string|callback
     */
    protected $prefix = '';

    /**
     * When uploading a file that has the same name as a file that is
     * already in the container should it overwrite it or use another name
     *
     * @var boolean
     */
    protected $overwrite = false;

    /**
     * Whether or not the uploaded files are auto confirmed
     *
     * @var boolean
     */
    protected $autoconfirm = false;

    /**
     * @var \Sirius\Validation\ValueValidator
     */
    protected $validator;

    /**
     * @var function|callback
     */
    protected $sanitizerCallback;

    /**
     * @param $directoryOrContainer
     * @param  array                               $options
     * @param  ValueValidator                      $validator
     * @throws InvalidContainerException
     */
    public function __construct($directoryOrContainer, $options = array(), ValueValidator $validator = null)
    {
        $container = $directoryOrContainer;
        if (is_string($directoryOrContainer)) {
            $container = new LocalContainer($directoryOrContainer);
        }
        if (!$container instanceof ContainerInterface) {
            throw new InvalidContainerException('Destination container for uploaded files is not valid');
        }
        $this->container = $container;

        // create the validator
        if (!$validator) {
            $validator = new ValueValidator();
        }
        $this->validator = $validator;

        // set options
        $availableOptions = array(
            static::OPTION_PREFIX => 'setPrefix',
            static::OPTION_OVERWRITE => 'setOverwrite',
            static::OPTION_AUTOCONFIRM => 'setAutoconfirm'
        );
        foreach ($availableOptions as $key => $method) {
            if (isset($options[$key])) {
                $this->{$method}($options[$key]);
            }
        }
    }

    /**
     * Enable/disable upload overwrite
     *
     * @param  bool                   $overwrite
     * @return \Sirius\Upload\Handler
     */
    public function setOverwrite($overwrite)
    {
        $this->overwrite = (bool) $overwrite;

        return $this;
    }

    /**
     * File prefix for the upload. Can be
     * - a folder (if it ends with /)
     * - a string to be used as prefix
     * - a function that returns a string
     *
     * @param  string|callable        $prefix
     * @return \Sirius\Upload\Handler
     */
    public function setPrefix($prefix)
    {
        $this->prefix = $prefix;

        return $this;
    }

    /**
     * Enable/disable upload autoconfirmation
     * Autoconfirmation does not require calling `confirm()`
     *
     * @param  boolean                $autoconfirm
     * @return \Sirius\Upload\Handler
     */
    public function setAutoconfirm($autoconfirm)
    {
        $this->autoconfirm = (bool) $autoconfirm;

        return $this;
    }

    /**
     * Set the sanitizer function for cleaning up the file names
     *
     * @param callable $callback
     * @throws \InvalidArgumentException
     * @return \Sirius\Upload\Handler
     */
    function setSanitizerCallback($callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('The $callback parameter is not a valid callable entity');
        }
        $this->sanitizerCallback = $callback;
        return $this;
    }

    /**
     * Add validation rule (extension|size|width|height|ratio)
     *
     * @param  string                 $name
     * @param  mixed                  $options
     * @param  string                 $errorMessageTemplate
     * @param  string                 $label
     * @return \Sirius\Upload\Handler
     */
    public function addRule($name, $options = null, $errorMessageTemplate = null, $label = null)
    {
        $predefinedRules = array(
            static::RULE_EXTENSION,
            static::RULE_IMAGE,
            static::RULE_SIZE,
            static::RULE_IMAGE_WIDTH,
            static::RULE_IMAGE_HEIGHT,
            static::RULE_IMAGE_RATIO
        );
        // convert to a name that is known by the default RuleFactory
        if (in_array($name, $predefinedRules)) {
            $name = 'upload' . $name;
        }
        $this->validator->add($name, $options, $errorMessageTemplate, $label);

        return $this;
    }

    /**
     * Processes a file upload and returns an upload result file/collection
     *
     * @param  array                         $files
     * @return Result\Collection|Result\File
     */
    public function process($files = array())
    {
        $files = Arr::normalizeFiles($files);

        foreach ($files as $k => $file) {
            $files[$k] = $this->processSingleFile($file);
        }

        if (count($files) == 1) {
            return new Result\File(array_pop($files), $this->container);
        }

        return new Result\Collection($files, $this->container);
    }

    /**
     * Processes a single uploaded file
     * - sanitize the name
     * - validates the file
     * - if valid, moves the file to the container
     *
     * @param  array $file
     * @return array
     */
    protected function processSingleFile(array $file)
    {
        // store it for future reference
        $file['original_name'] = $file['name'];

        // sanitize the file name
        $file['name'] = $this->sanitizeFileName($file['name']);

        $file = $this->validateFile($file);
        // if there are messages the file is not valid
        if (isset($file['messages']) && $file['messages']) {
            return $file;
        }

        // add the prefix
        $prefix = '';
        if (is_callable($this->prefix)) {
            $prefix = (string) call_user_func($this->prefix, $file['name']);
        } elseif (is_string($this->prefix)) {
            $prefix = (string) $this->prefix;
        }

        // if overwrite is not allowed, check if the file is already in the container
        if (!$this->overwrite) {
            if ($this->container->has($prefix . $file['name'])) {
                // add the timestamp to ensure the file is unique
                // method is not bulletproof but it's pretty safe
                $file['name'] = time() . '_' . $file['name'];
            }
        }

        // attempt to move the uploaded file into the container
        if (!$this->container->moveUploadedFile($file['tmp_name'], $prefix . $file['name'])) {
            $file['name'] = false;

            return $file;
        }

        $file['name'] = $prefix . $file['name'];
        // create the lock file if autoconfirm is disabled
        if (!$this->autoconfirm) {
            $this->container->save($file['name'] . '.lock', (string) time());
        }

        return $file;
    }

    /**
     * Validates a file according to the rules configured on the handler
     *
     * @param $file
     * @return mixed
     */
    protected function validateFile($file)
    {
        if (!$this->validator->validate($file)) {
            $file['messages'] = $this->validator->getMessages();
        }

        return $file;
    }

    /**
     * Sanitize the name of the uploaded file by stripping away bad characters
     * and replacing "invalid" characters with underscore _
     *
     * @param  string $name
     * @return string
     */
    protected function sanitizeFileName($name)
    {
        if ($this->sanitizerCallback) {
            return call_user_func($this->sanitizerCallback, $name);
        }
        return preg_replace('/[^A-Za-z0-9\.]+/', '_', $name);
    }
}


<?php

namespace Sirius\Upload\Container;

class Local implements ContainerInterface
{

    protected $baseDirectory;

    public function __construct($baseDirectory)
    {
        $this->baseDirectory = $this->normalizePath($baseDirectory) . DIRECTORY_SEPARATOR;
        $this->ensureDirectory($this->baseDirectory);
    }

    protected function normalizePath($path)
    {
        $path = dirname(rtrim($path, '\\/') . DIRECTORY_SEPARATOR . 'xxx');

        return rtrim($path, DIRECTORY_SEPARATOR);
    }

    protected function ensureDirectory($directory)
    {
        if (!file_exists($directory)) {
            mkdir($directory, 0755, true);
        }

        return is_dir($directory) && $this->isWritable();
    }

    /**
     * Check if the container is writable
     */
    public function isWritable()
    {
        return is_writable($this->baseDirectory);
    }

    /**
     * This will check if a file is in the container
     *
     * @param  string $file
     * @return bool
     */
    public function has($file)
    {
        return $file && file_exists($this->baseDirectory . $file);
    }

    /**
     * Saves the $content string as a file
     *
     * @param  string $file
     * @param  string $content
     * @return bool
     */
    public function save($file, $content)
    {
        $file = $this->normalizePath($file);
        $dir = dirname($this->baseDirectory . $file);
        if ($this->ensureDirectory($dir)) {
            return (bool) file_put_contents($this->baseDirectory . $file, $content);
        }

        return false;
    }

    /**
     * Delete the file from the container
     *
     * @param  string $file
     * @return bool
     */
    public function delete($file)
    {
        $file = $this->normalizePath($file);
        if (file_exists($this->baseDirectory . $file)) {
            return unlink($this->baseDirectory . $file);
        }

        return true;
    }

    /**
     * Moves a temporary uploaded file to a destination in the container
     *
     * @param  string $localFile   local path
     * @param  string $destination
     * @return bool
     */
    public function moveUploadedFile($localFile, $destination)
    {
        $dir = dirname($this->baseDirectory . $destination);
        if (file_exists($localFile) && $this->ensureDirectory($dir)) {
            /**
             * we could use is_uploaded_file() and move_uploaded_file()
             * but in case of ajax uploads that would fail
             */
            if (is_readable($localFile)) {
                // rename() would be good but this is better because $localFile may become 'unwritable'
                $result = copy($localFile, $this->baseDirectory . $destination);
                @unlink($localFile);
                return $result;
            }
        }
        return false;
    }

}


<?php

namespace Sirius\Upload\Util;

class Arr extends \Sirius\Validation\Util\Arr
{
    public static function remapFilesArray(array $files)
    {
        $result = array();
        foreach ($files['name'] as $k => $v) {
            $result[$k] = array(
                'name' => $files['name'][$k],
                'type' => @$files['type'][$k],
                'size' => @$files['size'][$k],
                'error' => @$files['error'][$k],
                'tmp_name' => $files['tmp_name'][$k]
            );
        }

        return $result;
    }

    /**
     * Fixes the $_FILES array problem and ensures the result is an array of files
     *
     * PHP's $_FILES variable is not properly formated for iteration when
     * multiple files are uploaded under the same name
     * @see http://www.php.net/manual/en/features.file-upload.php
     *
     * @param  array $files
     * @return array
     */
    public static function normalizeFiles(array $files)
    {
        // The caller passed $_FILES['some_field_name']
        if (isset($files['name'])) {
            // we have a single file
            if(!is_array($files['name'])) {
                return array($files);
            }
            // we have list of files, which PHP messes up
            else {
                return Arr::remapFilesArray($files);
            }
        }
        // The caller passed $_FILES
        else {
            $keys = array_keys($files);
            if (isset($keys[0]) && isset($files[$keys[0]]['name'])) {
                if (!is_array($files[$keys[0]]['name'])) {
                    // $files is in the correct format already, even in the
                    // case it contains a single element.
                    return $files;
                }
                // we have list of files, which PHP messes up
                else {
                    return Arr::remapFilesArray($files[$keys[0]]);
                }
            }
        }

        // if we got here, the $file argument is wrong
        return array();
    }

}


<?php

namespace Sirius\Upload\Result;

use Sirius\Upload\Container\ContainerInterface;

class File
{

    /**
     * Array containing the details of the uploaded file:
     * - name (uploaded name)
     * - original name
     * - tmp_name
     * etc
     *
     * @var array
     */
    protected $file;

    /**
     * The container to which this file belongs to
     * @var \Sirius\Upload\Container\ContainerInterface
     */
    protected $container;

    /**
     * @param $file
     * @param ContainerInterface $container
     */
    public function __construct($file, ContainerInterface $container)
    {
        $this->file = $file;
        $this->container = $container;
    }

    /**
     * Returns if the uploaded file is valid
     *
     * @return bool
     */
    public function isValid()
    {
        return $this->file['name'] && count($this->getMessages()) === 0;
    }

    /**
     * Returns the validation error messages
     *
     * @return array
     */
    public function getMessages()
    {
        if (isset($this->file['messages'])) {
            return $this->file['messages'];
        } else {
            return array();
        }
    }

    /**
     * The file that was saved during process() and has a .lock file attached
     * will be cleared, in case the form processing fails
     */
    public function clear()
    {
        $this->container->delete($this->name);
        $this->container->delete($this->name . '.lock');
        $this->file['name'] = null;
    }

    /**
     * Remove the .lock file attached to the file that was saved during process()
     * This should happen if the form fails validation/processing
     */
    public function confirm()
    {
        $this->container->delete($this->name . '.lock');
    }

    /**
     * File attribute getter
     *
     * @param $name
     * @return mixed
     */
    public function __get($name)
    {
        if (isset($this->file[$name])) {
            return $this->file[$name];
        }

        return null;
    }
}


<?php namespace YAAP\Theme;

use Illuminate\Support\ServiceProvider;

/**
 * Class ThemeServiceProvider
 * @package YAAP\Theme
 */
class ThemeServiceProvider extends ServiceProvider {

	/**
	 * Indicates if loading of the provider is deferred.
	 *
	 * @var bool
	 */
	protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        $this->publishes([
            __DIR__.'/../../config/config.php' => config_path('theme.php'),
        ], 'config');
    }

    /**
	 * Register the service provider.
	 *
	 * @return void
	 */
	public function register()
	{

        // init theme with default finder
        $this->app->singleton('theme', function($app) {
            $theme = new Theme($app, $this->app['view']->getFinder());
            return $theme;
        });

        // merge & publish config
        $configPath = __DIR__ . '/../../config/config.php';
        $this->mergeConfigFrom($configPath, 'theme');
        $this->publishes([$configPath => config_path('theme.php')]);

        $this->app->singleton('theme.create',function ($app) {
            return new Commands\ThemeGeneratorCommand($app['config'], $app['files']);
        });

        $this->app->singleton('theme.destroy',function ($app) {
            return new Commands\ThemeDestroyCommand($app['config'], $app['files']);
        });

        $this->commands(
            'theme.create',
            'theme.destroy'
        );

	}

	/**
	 * Get the services provided by the provider.
	 *
	 * @return array
	 */
	public function provides()
	{
		return array();
	}

}


<?php namespace YAAP\Theme;

use Illuminate\Container\Container;
use Illuminate\View\ViewFinderInterface;
use YAAP\Theme\Exceptions\ThemeException;

/**
 * Class Theme
 * @package YAAP\Theme
 */
class Theme
{

    /**
     * The IoC Container
     *
     * @var Container
     */
    protected $app;

    /**
     * Default view finder
     *
     */

    protected $finder;

    /**
     * theme config
     *
     */
    protected $config;

    /**
     * parent themes
     *
     */
    protected $parents;

    /**
     *  cache for paths
     */
    protected $cache;

    /**
     *  current theme
     */
    protected $theme;

    /**
     * Build a new Theme manager
     *
     * @param Container $app
     * @param ViewFinderInterface $finder
     */
    public function __construct(Container $app, ViewFinderInterface $finder)
    {

        $this->app = $app;

        $this->finder = $finder;
    }

    /**
     *
     * Initialize a theme by name
     * @param $theme
     * @throws ThemeException
     */
    public function init($theme)
    {
        if (empty($theme)) throw new ThemeException('Theme name should not be empty');

        $this->theme = $theme;

        // read theme path
        $path = $this->app['config']->get('theme.path', base_path('resources/themes'));

        //init config
        $this->config = $this->_readConfig( $path . '/' . $theme . '/config.php');

        // theme parents
        $this->parents = array();

        while (!empty($theme)) {

            if (!is_dir($path . '/' . $theme)) throw new ThemeException('Theme '.$theme.' not found.');

            // add theme's root folder
            $this->finder->addLocation($path . '/' . $theme);

            // add folder with views
            $this->finder->addLocation($path . '/' . $theme . '/' . config('theme.containerDir.view'));

            // read theme config
            $current_theme_config = $this->_readConfig($path . '/' . $theme . '/config.php');

            $theme = array_get($current_theme_config, 'inherit');

            if (!empty($theme)) {
                $this->parents[] = $theme;
            }
        }

    }

    /**
     * Returns the list of available themes names in an array.
     *
     * @return array
     */
    public function getList()
    {
        // read theme path
        $path = $this->app['config']->get('theme.path', base_path('resources/themes'));

        if (file_exists($path))
        {
            $dir_list = dir($path);
            while (false !== ($entry = $dir_list->read())) {
                if (file_exists($path . '/' . $entry . '/' . 'config.php'))
                    $list[] = $entry;
            }
        }

        return $list;

    }

    /**
     * Generate an asset path for current theme.
     *
     * @param $path
     * @param null $secure
     * @param bool $version
     * @throws Exceptions\ThemeException
     * @return mixed
     */
    public function asset($path, $secure = null, $version = false)
    {

        if (!$this->theme) throw new ThemeException('Theme should be init first');

        $full_path = $this->_assetFullpath($path);

        $asset = $this->app['url']->asset($full_path, $secure);

        if ($version) {

            if (is_bool($version)) {
                $asset .= '?v=' . $this->_assetVersion($path);
            } else {
                $asset .= '?v=' . $version;
            }
        }

        return $asset;

    }

    /**
     * Return filemtime of given asset or null if asset doesn't exists
     *
     * @param $path
     * @return int|null
     */
    private function _assetVersion($path)
    {

        $full_path = $this->_assetFullpath($path);

        $file_path = public_path($full_path);

        if (file_exists($file_path)) {
            return filemtime($file_path);
        }

        return null;
    }

    /**
     *
     * Try to find asset file in current theme or in parents
     *
     * @param $path
     * @return string
     */
    private function _assetFullpath($path)
    {

        $path = trim($path, '/');

        // alredy processed
        if (isset($this->cache[$path])) {
            return $this->cache[$path];
        }

        $assets_path = trim($this->app['config']->get('theme.assets_path', 'assets/themes'), '/');

        $full_path = $assets_path . '/' . $this->theme . '/' . $path;

        // theme has this asset
        if (!file_exists(public_path($full_path))) {

            $found = false;

            // loop over parents
            foreach ($this->parents as $parent) {

                $full_path = $assets_path . '/' . $parent . '/' . $path;

                if (file_exists(public_path($full_path))) {
                    $found = true;
                    break;
                }
            }

            // in case of failure to find asset - return default theme asset
            // (404 error will be signal of promlems)
            if (!$found) {
                $full_path = $assets_path . '/' . $this->theme . '/' . $path;
            }

        }

        $this->cache[$path] = $full_path;

        return $full_path;
    }

    /**
     * @param $path
     * @return array|mixed
     */
    private function _readConfig($path){
        if (file_exists($path))
                return include($path);

        return array();
    }

}


<?php namespace YAAP\Theme\Commands;

use Illuminate\Console\Command;
use Illuminate\Config\Repository;
use Illuminate\Filesystem\Filesystem as File;
use Symfony\Component\Console\Input\InputArgument;

/**
 * Class ThemeDestroyCommand
 * @package YAAP\Theme\Commands
 */
class ThemeDestroyCommand extends Command {

	/**
	 * The console command name.
	 *
	 * @var string
	 */
	protected $name = 'theme:destroy';

	/**
	 * The console command description.
	 *
	 * @var string
	 */
	protected $description = 'Remove exsisting theme';

	/**
	 * Repository config.
	 *
	 * @var Illuminate\Config\Repository
	 */
	protected $config;

	/**
	 * Filesystem
	 *
	 * @var Illuminate\Filesystem\Filesystem
	 */
	protected $files;

    /**
     * Create a new command instance.
     *
     * @param \Illuminate\Config\Repository $config
     * @param \Illuminate\Filesystem\Filesystem $files
     * @return \YAAP\Theme\Commands\ThemeDestroyCommand
     */
	public function __construct(Repository $config, File $files)
	{
		$this->config = $config;

		$this->files = $files;

		parent::__construct();
	}

	/**
	 * Execute the console command.
	 *
	 * @return void
	 */
	public function handle()
	{
		// The theme is not exists.
		if ( ! $this->files->isDirectory($this->getPath(null)))
		{
			return $this->error('Theme "'.$this->getTheme().'" is not exists.');
		}

		$themePath = $this->getPath(null);

		$assetsPath = $this->getAssetsPath(null);

		if ($this->confirm('Are you sure you want to permanently delete? [yes|no]'))
		{
			// Delete permanent.
			$this->files->deleteDirectory($themePath, false);
			$this->files->deleteDirectory($assetsPath, false);

			$this->info('Theme "'.$this->getTheme().'" has been destroyed.');
		}
	}

	/**
	 * Get root writable path.
	 *
	 * @param  string $path
	 * @return string
	 */
	protected function getPath($path)
	{
		$rootPath = $this->config->get('theme::path', base_path('resources/themes'));

        return $rootPath.'/'.strtolower($this->getTheme()).'/' . $path;
	}

    /**
     * Get assets writable path.
     *
     * @param  string $path
     * @param bool $absolute
     * @return string
     */
    protected function getAssetsPath($path, $absolute = true)
    {
        $rootPath = $this->config->get('theme::assets_path', 'assets/themes');

        if ($absolute)
            $rootPath = public_path($rootPath);

        return $rootPath.'/'.strtolower($this->getTheme()).'/' . $path;
    }

	/**
	 * Get the theme name.
	 *
	 * @return string
	 */
	protected function getTheme()
	{
		return strtolower($this->argument('name'));
	}

	/**
	 * Get the console command arguments.
	 *
	 * @return array
	 */
	protected function getArguments()
	{
		return array(
			array('name', InputArgument::REQUIRED, 'Name of the theme to generate.'),
		);
	}

}


<?php namespace YAAP\Theme\Commands;

use Illuminate\Console\Command;
use Illuminate\Config\Repository;
use Illuminate\Filesystem\Filesystem as File;
use Symfony\Component\Console\Input\InputArgument;

/**
 * Class ThemeGeneratorCommand
 * @package YAAP\Theme\Commands
 */
class ThemeGeneratorCommand extends Command {

	/**
	 * The console command name.
	 *
	 * @var string
	 */
	protected $name = 'theme:create';

	/**
	 * The console command description.
	 *
	 * @var string
	 */
	protected $description = 'Generate theme structure';

	/**
	 * Repository config.
	 *
	 * @var Illuminate\Config\Repository
	 */
	protected $config;

	/**
	 * Filesystem
	 *
	 * @var Illuminate\Filesystem\Filesystem
	 */
	protected $files;

    /**
     * Create a new command instance.
     *
     * @param \Illuminate\Config\Repository $config
     * @param \Illuminate\Filesystem\Filesystem $files
     * @return \YAAP\Theme\Commands\ThemeGeneratorCommand
     */
	public function __construct(Repository $config, File $files)
	{
		$this->config = $config;

		$this->files = $files;

		parent::__construct();
	}

	/**
	 * Execute the console command.
	 *
	 * @return void
	 */
	public function handle()
	{
		// The theme is already exists.
		if ($this->files->isDirectory($this->getPath(null)))
		{
			return $this->error('Theme "'.$this->getTheme().'" is already exists.');
		}

		// Directories.
		$container = $this->config->get('theme.containerDir');

        $this->makeDir($this->getPath($container['layout']));
        $this->makeDir($this->getPath($container['partial']));
        $this->makeDir($this->getPath($container['view']));

        $this->makeFile($container['layout'].'/master.blade.php', $this->getTemplate('layout.blade'));
        $this->makeFile($container['partial'].'/header.blade.php', $this->getTemplate('header.blade'));
        $this->makeFile($container['partial'].'/footer.blade.php', $this->getTemplate('footer.blade'));
        $this->makeFile($container['view'].'/hello.blade.php', $this->getTemplate('view.blade'));

        //assets
        $this->makeDir($this->getAssetsPath('css'));
        $this->makeAssetsFile('css/.gitkeep', '');
        $this->makeAssetsFile('css/styles.css', $this->getTemplate('styles.css'));

        $this->makeDir($this->getAssetsPath('js'));
        $this->makeAssetsFile('js/.gitkeep', '');

        $this->makeDir($this->getAssetsPath('img'));
        $this->makeAssetsFile('img/.gitkeep', '');

		// Generate inside config.
		$this->makeFile('config.php', $this->getTemplate('config', ['%theme_name%' => $this->getTheme()]));

		$this->info('Theme "'.$this->getTheme().'" has been created.');
	}

    /**
     * Make directory.
     *
     * @param $path
     * @return void
     */
	protected function makeDir($path)
	{
		if ( ! $this->files->isDirectory($path))
		{
            $this->files->makeDirectory($path, 0777, true);
		}
	}

    /**
     * Make file.
     *
     * @param  string $file
     * @param  string $template
     * @param bool $assets
     * @return void
     */
	protected function makeFile($file, $template = null, $assets = false)
	{
		if ( ! $this->files->exists($this->getPath($file)))
		{
			$content = $assets ? $this->getAssetsPath($file, true) : $this->getPath($file);

			$this->files->put($content, $template);
		}
	}

    /**
     * Make file.
     *
     * @param  string $file
     * @param  string $template
     * @internal param bool $assets
     * @return void
     */
	protected function makeAssetsFile($file, $template = null)
	{
		$this->makeFile($file, $template, true);
	}

	/**
	 * Get root writable path.
	 *
	 * @param  string $path
	 * @return string
	 */
	protected function getPath($path)
	{
		$rootPath = $this->config->get('theme.path', base_path('resources/themes'));

		return $rootPath.'/'.strtolower($this->getTheme()).'/' . $path;
	}

    /**
     * Get assets writable path.
     *
     * @param  string $path
     * @param bool $absolute
     * @return string
     */
	protected function getAssetsPath($path, $absolute = true)
	{
        $rootPath = $this->config->get('theme.assets_path', 'assets/themes');

        if ($absolute)
            $rootPath = public_path($rootPath);

		return $rootPath.'/'.strtolower($this->getTheme()).'/' . $path;
	}

	/**
	 * Get the theme name.
	 *
	 * @return string
	 */
	protected function getTheme()
	{
		return strtolower($this->argument('name'));
	}

    /**
     * Get default template.
     *
     * @param  string $template
     * @param array $replacements
     * @return string
     */
    protected function getTemplate($template, $replacements = array())
    {

        $path = realpath(__DIR__ . '/../templates/' . $template . '.txt');

        $content = $this->files->get($path);

        if (!empty($replacements)) {
            $content = str_replace(array_keys($replacements), array_values($replacements), $content);
        }

        return $content;
    }

	/**
	 * Get the console command arguments.
	 *
	 * @return array
	 */
	protected function getArguments()
	{
		return array(
			array('name', InputArgument::REQUIRED, 'Name of the theme to generate.'),
		);
	}

}


<?php

namespace Happyr\TranslationBundle\Controller;

use Happyr\TranslationBundle\Model\Message;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Translation\DataCollectorTranslator;
use Symfony\Component\VarDumper\Cloner\Data;

/**
 * @author Tobias Nyholm
 */
class ProfilerController extends Controller
{
    /**
     * @param Request $request
     * @param string  $token
     *
     * @Route("/{token}/translation/edit", name="_profiler_translations_edit")
     *
     * @return Response
     */
    public function editAction(Request $request, $token)
    {
        if (!$this->getParameter('translation.toolbar.allow_edit')) {
            return new Response('You are not allowed to edit the translations.');
        }

        if (!$request->isXmlHttpRequest()) {
            return $this->redirectToRoute('_profiler', ['token' => $token]);
        }

        $message = $this->getMessage($request, $token);
        $trans = $this->get('happyr.translation');

        if ($request->isMethod('GET')) {
            $trans->fetchTranslation($message);

            return $this->render('HappyrTranslationBundle:Profiler:edit.html.twig', [
                'message' => $message,
                'key' => $request->query->get('message_id'),
            ]);
        }

        //Assert: This is a POST request
        $message->setTranslation($request->request->get('translation'));
        $trans->updateTranslation($message);

        return new Response($message->getTranslation());
    }

    /**
     * @param Request $request
     * @param string  $token
     *
     * @Route("/{token}/translation/flag", name="_profiler_translations_flag")
     * @Method("POST")
     *
     * @return Response
     */
    public function flagAction(Request $request, $token)
    {
        if (!$request->isXmlHttpRequest()) {
            return $this->redirectToRoute('_profiler', ['token' => $token]);
        }

        $message = $this->getMessage($request, $token);

        $saved = $this->get('happyr.translation')->flagTranslation($message);

        return new Response($saved ? 'OK' : 'ERROR');
    }

    /**
     * @param Request $request
     * @param string  $token
     *
     * @Route("/{token}/translation/sync", name="_profiler_translations_sync")
     * @Method("POST")
     *
     * @return Response
     */
    public function syncAction(Request $request, $token)
    {
        if (!$request->isXmlHttpRequest()) {
            return $this->redirectToRoute('_profiler', ['token' => $token]);
        }

        $message = $this->getMessage($request, $token);
        $translation = $this->get('happyr.translation')->fetchTranslation($message, true);

        if ($translation !== null) {
            return new Response($translation);
        }

        return new Response('Asset not found', 404);
    }

    /**
     * @param Request $request
     * @param         $token
     *
     * @Route("/{token}/translation/sync-all", name="_profiler_translations_sync_all")
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
     */
    public function syncAllAction(Request $request, $token)
    {
        if (!$request->isXmlHttpRequest()) {
            return $this->redirectToRoute('_profiler', ['token' => $token]);
        }

        $this->get('happyr.translation')->synchronizeAllTranslations();

        return new Response('Started synchronization of all translations');
    }

    /**
     * Save the selected translation to resources.
     *
     * @author Damien Alexandre (damienalexandre)
     *
     * @param Request $request
     * @param string  $token
     *
     * @Route("/{token}/translation/create-asset", name="_profiler_translations_create_assets")
     * @Method("POST")
     *
     * @return Response
     */
    public function createAssetsAction(Request $request, $token)
    {
        if (!$request->isXmlHttpRequest()) {
            return $this->redirectToRoute('_profiler', ['token' => $token]);
        }

        $messages = $this->getSelectedMessages($request, $token);

        if ($messages instanceof Data) {
            $messages = $messages->getValue(true);
        }

        if (empty($messages)) {
            return new Response('No translations selected.');
        }

        $uploaded = array();
        $trans = $this->get('happyr.translation');
        foreach ($messages as $message) {
            if ($trans->createAsset($message)) {
                $uploaded[] = $message;
            }
        }

        $saved = count($uploaded);
        if ($saved > 0) {
            $this->get('happyr.translation.filesystem')->updateMessageCatalog($uploaded);
        }

        return new Response(sprintf('%s new assets created!', $saved));
    }

    /**
     * @param Request $request
     * @param string  $token
     *
     * @return Message
     */
    protected function getMessage(Request $request, $token)
    {
        $profiler = $this->get('profiler');
        $profiler->disable();

        $messageId = $request->request->get('message_id', $request->query->get('message_id'));

        $profile = $profiler->loadProfile($token);
        $messages = $profile->getCollector('translation')->getMessages();
        if (!isset($messages[$messageId])) {
            throw $this->createNotFoundException(sprintf('No message with key "%s" was found.', $messageId));
        }
        $message = new Message($messages[$messageId]);

        if ($message->getState() === DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK) {
            $message->setLocale($profile->getCollector('request')->getLocale())
                ->setTranslation(sprintf('[%s]', $message->getTranslation()));
        }

        return $message;
    }

    /**
     * @param Request $request
     * @param string  $token
     *
     * @return array
     */
    protected function getSelectedMessages(Request $request, $token)
    {
        $profiler = $this->get('profiler');
        $profiler->disable();

        $selected = $request->request->get('selected');
        if (!$selected || count($selected) == 0) {
            return array();
        }

        $profile = $profiler->loadProfile($token);
        $dataCollector = $profile->getCollector('translation');
        $messages = $dataCollector->getMessages();

        if ($messages instanceof Data) {
            $messages = $messages->getValue(true);
        }

        $toSave = array_intersect_key($messages, array_flip($selected));

        $messages = array();
        foreach ($toSave as $data) {
            //We do not want do add the placeholder to Loco. That messes up the stats.
            $data['translation'] = '';

            $messages[] = new Message($data);
        }

        return $messages;
    }
}