<?php

namespace AvtoDev\B2BApiLaravel;

use Illuminate\Container\Container;
use AvtoDev\B2BApi\Tokens\AuthToken;
use AvtoDev\B2BApi\Clients\v1\Client;
use AvtoDev\B2BApi\References\QueryTypes;
use AvtoDev\B2BApi\Exceptions\B2BApiException;
use AvtoDev\B2BApiLaravel\Traits\InstanceableTrait;
use AvtoDev\B2BApi\Responses\DataTypes\Report\ReportData;
use AvtoDev\B2BApiLaravel\ReportTypes\ReportTypeInterface;
use AvtoDev\B2BApiLaravel\Exceptions\B2BApiServiceException;
use AvtoDev\B2BApiLaravel\ReportTypes\ReportTypesRepository;
use AvtoDev\B2BApi\Exceptions\B2BApiInvalidArgumentException;
use AvtoDev\B2BApi\Responses\DataTypes\Report\ReportStatusData;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use AvtoDev\B2BApiLaravel\Exceptions\InvalidReportTypeException;

/**
 * Клиент, реализующий работу с B2B API.
 */
class B2BApiService
{
    use InstanceableTrait;

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

    /**
     * @var Client
     */
    protected $client;

    /**
     * @var AuthToken
     */
    protected $auth_token;

    /**
     * @var ReportTypesRepository
     */
    protected $report_types_repository;

    /**
     * @var QueryTypes
     */
    protected $query_types;

    /**
     * Конфиг (должен соответствовать структуре конфига пакета).
     *
     * Описываем значения полностью для полноценной работы type-hint.
     *
     * @var array
     */
    protected $config = [
        'api_base_uri' => null,
        'domain'       => null,
        'username'     => null,
        'password'     => null,
        'webhooks'     => null,
        'is_test'      => false,
    ];

    /**
     * B2BApiService constructor.
     *
     * @param Container $app
     * @param array     $config
     */
    public function __construct(Container $app, $config = [])
    {
        $this->app                     = $app;
        $this->config                  = \array_replace_recursive($this->config, (array) $config);
        $this->auth_token              = new AuthToken;
        $this->query_types             = new QueryTypes;
        $this->report_types_repository = $app->make(ReportTypesRepository::class);

        // И инициализируем клиента B2B API
        $this->client = new Client([
            'api' => [
                'versions' => [
                    'default' => [
                        'base_uri' => $this->config['api_base_uri'],
                    ],
                ],
            ],

            'use_api_version' => 'default',

            'is_test' => $this->config['is_test'],
        ]);
    }

    /**
     * Возвращает инстанс клиента B2B API.
     *
     * @return Client
     */
    public function client()
    {
        return $this->client;
    }

    /**
     * Возвращает справочник типов идентификаторов запросов.
     *
     * @return QueryTypes
     */
    public function queryTypes()
    {
        return $this->query_types;
    }

    /**
     * Создает (заказывает) отчет у сервиса B2B API.
     *
     * Если методу не передать значение $report_type - то он будет автоматически извлечен из конфигурации.
     *
     * @param string                          $query_type  Тип идентификатора запрашиваемой сущности
     * @param string                          $query_id    Идентификатор запрашиваемой сущности (VIN, GRZ и так далее)
     * @param ReportTypeInterface|string|null $report_type UID типа отчета либо его **имя** (из конфигурации)
     * @param bool                            $is_force    Нужно ли перегенерировать отчет если он уже существует?
     *
     * @throws B2BApiServiceException
     * @throws B2BApiInvalidArgumentException
     * @throws B2BApiException
     *
     * @return ReportStatusData
     */
    public function makeReport($query_type, $query_id, $report_type = null, $is_force = false)
    {
        // Если методу передан пустой $report_type, то пытаемся извлечь ReportType из репозитория, который установлен
        // по как используемый по умолчанию
        if ($report_type === null) {
            $default = $this->report_types_repository->getDefaultReportType();

            if ($default instanceof ReportTypeInterface) {
                $report_type = $default;
            }
        }

        $response = $this->client->user()->report()->make(
            $this->generateAuthToken(),
            $query_type,
            $query_id,
            $this->getReportTypeUid($report_type),
            $is_force,
            $this->generateWebHooksOptions()
        );

        if (($status = $response->data()->first()) && $status instanceof ReportStatusData) {
            return $status;
        }

        throw new B2BApiServiceException(sprintf('Invalid response type: "%s"', get_class($status)));
    }

    /**
     * Генерирует новый токен авторизации на сервисе B2B API.
     *
     * @param int      $age       Время жизни токена (unix-time, в секундах)
     * @param int|null $timestamp Временная метка (unix-time, начала жизни токена)
     *
     * @return string
     */
    public function generateAuthToken($age = 172800, $timestamp = null)
    {
        $auth_token = $this->auth_token; // PHP 5.6

        return $auth_token::generate(
            $this->config['username'],
            $this->config['password'],
            $this->config['domain'],
            $age,
            $timestamp
        );
    }

    /**
     * Возвращает UID ипа отчета по переданной сущностью. В роли сущности может выступать как объект типа отчета, так и
     * строковое значение любого известного имени типа отчета, либо непосредственно его UID.
     *
     * @param ReportTypeInterface|string $input
     *
     * @throws InvalidReportTypeException
     *
     * @return null|string
     */
    public function getReportTypeUid($input)
    {
        if ($input instanceof ReportTypeInterface) {
            return $input->getUid();
        }

        if (is_string($input) && ! empty($input)) {
            $report_type = $this->report_types_repository->getByName($input);

            if ($report_type instanceof ReportTypeInterface) {
                return $report_type->getUid();
            }

            $report_type = $this->report_types_repository->getByUid($input);

            if ($report_type instanceof ReportTypeInterface) {
                return $report_type->getUid();
            }
        }

        throw new InvalidReportTypeException(sprintf('Invalid report type identifier: "%s"', $input));
    }

    /**
     * Возвращает объект отчета B2B API.
     *
     * @param string $report_uid UID отчета
     *
     * @throws B2BApiServiceException
     * @throws B2BApiException
     *
     * @return ReportData
     */
    public function getReport($report_uid)
    {
        $response = $this->client->user()->report()->get($this->generateAuthToken(), $report_uid);

        if (($report = $response->data()->first()) && $report instanceof ReportData) {
            return $report;
        }

        throw new B2BApiServiceException(sprintf('Invalid response type: "%s"', get_class($report)));
    }

    /**
     * Обновляет имеющийся отчет у сервиса B2B API по его UID.
     *
     * @param string $report_uid UID отчета
     *
     * @throws B2BApiServiceException
     * @throws B2BApiException
     *
     * @return ReportStatusData
     */
    public function refreshReport($report_uid)
    {
        $response = $this->client->user()->report()->refresh(
            $this->generateAuthToken(),
            $report_uid,
            $this->generateWebHooksOptions()
        );

        if (($report = $response->data()->first()) && $report instanceof ReportStatusData) {
            return $report;
        }

        throw new B2BApiServiceException(sprintf('Invalid response type: "%s"', get_class($report)));
    }

    /**
     * Генерирует набор опций, отправляемых вместе с запросами, отвечающий за указание webhook-ов.
     *
     * @return array|array[]
     */
    protected function generateWebHooksOptions()
    {
        /** @var ConfigRepository $config */
        $config = $this->app->make('config');
        $root   = B2BApiServiceProvider::getConfigRootKeyName();

        $options = [
            'webhook' => (object) array_filter([
                'on_complete' => $config->get("$root.webhooks.on.complete"),
                'on_update'   => $config->get("$root.webhooks.on.update"),
            ]),
        ];

        return $options;
    }
}


<?php

namespace AvtoDev\B2BApiLaravel\ReportTypes;

use Traversable;
use Illuminate\Support\Str;

class ReportType implements ReportTypeInterface
{
    /**
     * UID типа отчета.
     *
     * @var string|null
     */
    protected $uid;

    /**
     * Описание типа отчета.
     *
     * @var string|null
     */
    protected $description;

    /**
     * Имя типа отчета.
     *
     * @var string|null
     */
    protected $name;

    /**
     * ReportType constructor.
     *
     * @param null|string|array|Traversable|ReportTypeInterface $input
     */
    public function __construct($input = null)
    {
        $this->configure($input);
    }

    /**
     * При попытке преобразовать объект в строку - вернется значение его UID-а.
     *
     * @return string
     */
    public function __toString()
    {
        return (string) $this->uid;
    }

    /**
     * {@inheritdoc}
     */
    public function configure($input)
    {
        if ($input instanceof self) {
            $this->setUid($input->getUid());
            $this->setDescription($input->getDescription());
            $this->setName($input->getName());
        } elseif (is_array($input) || $input instanceof Traversable) {
            foreach ($input as $key => $value) {
                switch (Str::lower(trim((string) $key))) {
                    case 'value':
                    case 'id':
                    case 'uid':
                        $this->setUid($value);
                        break;

                    case 'desc':
                    case 'description':
                        $this->setDescription($value);
                        break;

                    case 'name':
                        $this->setName($value);
                        break;
                }
            }
        } elseif (\is_string($input) && ! empty($input)) {
            $this->setUid($input);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function toJson($options = 0)
    {
        return \json_encode($this->toArray(), $options);
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        return [
            'uid'         => $this->getUid(),
            'description' => $this->getDescription(),
            'name'        => $this->getName(),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getUid()
    {
        return $this->uid;
    }

    /**
     * {@inheritdoc}
     */
    public function setUid($uid)
    {
        $this->uid = $uid === null
            ? null
            : trim((string) $uid);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * {@inheritdoc}
     */
    public function setDescription($description)
    {
        $this->description = $description === null
            ? null
            : trim((string) $description);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * {@inheritdoc}
     */
    public function setName($name)
    {
        $this->name = $name === null
            ? null
            : trim((string) $name);

        return $this;
    }
}


<?php

namespace AvtoDev\B2BApiLaravel\ReportTypes;

use Traversable;
use Illuminate\Support\Collection;
use AvtoDev\B2BApiLaravel\Traits\InstanceableTrait;

class ReportTypesRepository extends Collection implements ReportTypesRepositoryInterface
{
    use InstanceableTrait;

    /**
     * Стек объектов - типов отчетов.
     *
     * @var ReportType[]|array
     */
    protected $items = [];

    /**
     * Тип отчета, используемый по умолчанию.
     *
     * @var ReportType|null
     */
    protected $default_report_type;

    /**
     * ReportTypesRepository constructor.
     *
     * @param array|Traversable|mixed $items
     */
    public function __construct($items = [])
    {
        $this->items = $this->toArrayOfReportTypes($items);
    }

    /**
     * {@inheritdoc}
     */
    public function setDefaultReportType($report_type)
    {
        $report_type = $this->toReportType($report_type);

        if ($report_type instanceof ReportType) {
            $this->default_report_type = $report_type;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultReportType()
    {
        return $this->default_report_type;
    }

    /**
     * {@inheritdoc}
     */
    public function getByName($report_type_name)
    {
        foreach ($this->all() as $item) {
            if ($item instanceof ReportTypeInterface && $item->getName() === $report_type_name) {
                return $item;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function hasName($name)
    {
        return \in_array($name, $this->getAllNames(), true);
    }

    /**
     * {@inheritdoc}
     */
    public function getAllNames()
    {
        $result = [];

        foreach ($this->all() as $item) {
            if ($item instanceof ReportTypeInterface && ($name = $item->getName()) !== null) {
                $result[] = $name;
            }
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function getByUid($report_type_uid)
    {
        foreach ($this->all() as $item) {
            if ($item instanceof ReportTypeInterface && $item->getUid() === $report_type_uid) {
                return $item;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function hasUid($uid)
    {
        return \in_array($uid, $this->getAllUids(), true);
    }

    /**
     * {@inheritdoc}
     */
    public function getAllUids()
    {
        $result = [];

        foreach ($this->all() as $item) {
            if ($item instanceof ReportTypeInterface && ($uid = $item->getUid()) !== null) {
                $result[] = $uid;
            }
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($key, $value)
    {
        // Пытаемся преобразовать значение в объект типа ReportType, если в этом есть необходимость
        $value = $value instanceof ReportTypeInterface
            ? $value
            : $this->toReportType($value);

        if ($value instanceof ReportTypeInterface) {
            if ($key === null) {
                $this->items[] = $value;
            } else {
                $this->items[$key] = $value;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function toReportType($some_value)
    {
        $report_types_array = $this->toArrayOfReportTypes($some_value);

        return isset($report_types_array[0]) && (($report_type = $report_types_array[0]) instanceof ReportType)
            ? $report_type
            : null;
    }

    /**
     * Преобразует входящее значение в массив объектов типа ReportType.
     *
     * Принимает на вход массив данных о типах отчетов, и в их соответствии формирует коллекцию данных. Формат
     * массивов может быть следующим:
     *
     * <code>
     * new ReportType('uid_0')
     *
     * 'uid_0'
     *
     * 'uid_0,uid_1'
     *
     * [
     *   'uid_1',
     *   'uid_2',
     * ]
     *
     * [
     *   'uid_1' => ['uid' => 'report_1_uid', 'description' => 'Some 1 description'],
     *   'uid_2' => ['id' => 'report_2_uid',  'desc' => 'Some 2 description'],
     *  ['name' => 'some_3_name', 'id' => 'report_3_uid', 'description' => 'Some 3 description'],
     * ]
     * </code>
     *
     * @param string|array|array[]|object $items
     *
     * @return ReportType[]|array
     */
    protected function toArrayOfReportTypes($items)
    {
        $result = [];

        // Если влетело некоторое скалярное выражение - то преобразуем его в массив по разделителю
        if (is_scalar($items) && ! empty($items)) {
            $items = explode(',', trim((string) $items));
        }

        // Если элементом является объект, умеющим себя преобразовывать в массив - то преобразуем
        if (\is_object($items) && ! ($items instanceof ReportType) && \method_exists($items, 'toArray')) {
            $items = $items->toArray();
        }

        foreach ((array) $items as $key => $value) {
            if ($value instanceof ReportType) {
                // Если элементом уже является объект типа ReportType - то сразу его пушим в стек
                $result[] = $value;
            } elseif (\is_scalar($value) && ! empty($value)) {
                // Если влетела строка - то пушим в стек объект, у которого и имя, и UID равны этой строке
                $report_type = new ReportType;
                $report_type->setUid($value);
                $report_type->setName($value);

                $result[] = $report_type;
            } elseif (\is_array($value) || $value instanceof Traversable) {
                // Если массив или перебираемое дерьмо - то работаем с ним
                $report_type = new ReportType;
                $report_type->setName($key);
                $report_type->configure($value);

                $result[] = $report_type;
            }
        }

        return $result;
    }
}


<?php

namespace AvtoDev\B2BApiLaravel;

use Illuminate\Container\Container;
use Psr\Http\Message\ResponseInterface;
use AvtoDev\B2BApiLaravel\Events\AfterRequestSending;
use AvtoDev\B2BApiLaravel\Events\BeforeRequestSending;
use AvtoDev\B2BApiLaravel\ReportTypes\ReportTypesRepository;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;

class B2BApiServiceProvider extends IlluminateServiceProvider
{
    /**
     * Возвращает путь до файла-конфигурации пакета (данный метод можно переопределять).
     *
     * @return string
     */
    public static function getConfigFilePath()
    {
        return __DIR__ . '/../config/b2b-api-client.php';
    }

    /**
     * Get config root key name.
     *
     * @return string
     */
    public static function getConfigRootKeyName()
    {
        return \basename(static::getConfigFilePath(), '.php');
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->initializeConfigs();

        $this->registerReportTypesRepository();

        $this->registerClientInstance();
    }

    /**
     * Выполнение после-регистрационной загрузки сервисов.
     *
     * @return void
     */
    public function boot()
    {
        $this->publishes([
            \realpath($config_path = static::getConfigFilePath()) => config_path(\basename($config_path)),
        ], 'config');
    }

    /**
     * Initialize configs.
     *
     * @return void
     */
    protected function initializeConfigs()
    {
        $this->mergeConfigFrom(static::getConfigFilePath(), static::getConfigRootKeyName());
    }

    /**
     * Регистрирует контейнер-коллекцию типов отчетов.
     *
     * @return void
     */
    protected function registerReportTypesRepository()
    {
        $this->app->singleton(ReportTypesRepository::class, function (Container $app) {
            /** @var ConfigRepository $config */
            $config = $app->make('config');

            $repository = new ReportTypesRepository($config->get(
                sprintf('%s.report_types.uids', static::getConfigRootKeyName())
            ));

            if ($repository->hasName($name = $config->get(sprintf(
                '%s.report_types.use_as_default', static::getConfigRootKeyName()
            )))) {
                $repository->setDefaultReportType($repository->getByName($name));
            }

            return $repository;
        });

        $this->app->bind('b2b-api.report-types.repository', ReportTypesRepository::class);
    }

    /**
     * Регистрирует контейнер-реализацию клиента.
     *
     * @return void
     */
    protected function registerClientInstance()
    {
        $this->app->singleton(B2BApiService::class, function (Container $app) {
            /** @var ConfigRepository $config */
            $config = $app->make('config');

            $client = new B2BApiService($app, $config->get(static::getConfigRootKeyName()));

            // "Вешаем" на события внутри клиента Laravel-обработчики
            $client->client()->httpClient()->on('before_request',
                function (&$method, &$uri, &$data, &$headers) {
                    event(new BeforeRequestSending($method, $uri, $data, $headers));
                }
            );
            $client->client()->httpClient()->on('after_request',
                function (ResponseInterface $response) {
                    event(new AfterRequestSending(clone $response));
                }
            );

            return $client;
        });

        $this->app->bind('b2b-api.service', B2BApiService::class);
    }
}


<?php
/**
 * Part of the Joomla Framework String Package
 *
 * @copyright  Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\String;

use InvalidArgumentException;

/**
 * Joomla Framework String Inflector Class
 *
 * The Inflector transforms words
 *
 * @since  1.0
 */
class Inflector
{
	/**
	 * The singleton instance.
	 *
	 * @var    Inflector
	 * @since  1.0
	 */
	private static $instance;

	/**
	 * The inflector rules for singularisation, pluralisation and countability.
	 *
	 * @var    array
	 * @since  1.0
	 */
	private $rules = array(
		'singular' => array(
			'/(matr)ices$/i'                                                          => '\1ix',
			'/(vert|ind)ices$/i'                                                      => '\1ex',
			'/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
			'/([ftw]ax)es/i'                                                          => '\1',
			'/(cris|ax|test)es$/i'                                                    => '\1is',
			'/(shoe|slave)s$/i'                                                       => '\1',
			'/(o)es$/i'                                                               => '\1',
			'/([^aeiouy]|qu)ies$/i'                                                   => '\1y',
			'/$1ses$/i'                                                               => '\s',
			'/ses$/i'                                                                 => '\s',
			'/eaus$/'                                                                 => 'eau',
			'/^(.*us)$/'                                                              => '\\1',
			'/s$/i'                                                                   => '',
		),
		'plural' => array(
			'/([m|l])ouse$/i'                                                        => '\1ice',
			'/(matr|vert|ind)(ix|ex)$/i'                                             => '\1ices',
			'/(x|ch|ss|sh)$/i'                                                       => '\1es',
			'/([^aeiouy]|qu)y$/i'                                                    => '\1ies',
			'/([^aeiouy]|qu)ies$/i'                                                  => '\1y',
			'/(?:([^f])fe|([lr])f)$/i'                                               => '\1\2ves',
			'/sis$/i'                                                                => 'ses',
			'/([ti])um$/i'                                                           => '\1a',
			'/(buffal|tomat)o$/i'                                                    => '\1\2oes',
			'/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i',
			'/us$/i'                                                                 => 'uses',
			'/(ax|cris|test)is$/i'                                                   => '\1es',
			'/s$/i'                                                                  => 's',
			'/$/'                                                                    => 's',
		),
		'countable' => array(
			'id',
			'hits',
			'clicks',
		),
	);

	/**
	 * Cached inflections.
	 *
	 * The array is in the form [singular => plural]
	 *
	 * @var    string[]
	 * @since  1.0
	 */
	private $cache = array();

	/**
	 * Protected constructor.
	 *
	 * @since  1.0
	 */
	protected function __construct()
	{
		// Pre=populate the irregual singular/plural.
		$this
			->addWord('deer')
			->addWord('moose')
			->addWord('sheep')
			->addWord('bison')
			->addWord('salmon')
			->addWord('pike')
			->addWord('trout')
			->addWord('fish')
			->addWord('swine')

			->addWord('alias', 'aliases')
			->addWord('bus', 'buses')
			->addWord('foot', 'feet')
			->addWord('goose', 'geese')
			->addWord('hive', 'hives')
			->addWord('louse', 'lice')
			->addWord('man', 'men')
			->addWord('mouse', 'mice')
			->addWord('ox', 'oxen')
			->addWord('quiz', 'quizes')
			->addWord('status', 'statuses')
			->addWord('tooth', 'teeth')
			->addWord('woman', 'women');
	}

	/**
	 * Adds inflection regex rules to the inflector.
	 *
	 * @param   mixed   $data      A string or an array of strings or regex rules to add.
	 * @param   string  $ruleType  The rule type: singular | plural | countable
	 *
	 * @return  void
	 *
	 * @since   1.0
	 * @throws  InvalidArgumentException
	 */
	private function addRule($data, $ruleType)
	{
		if (\is_string($data))
		{
			$data = array($data);
		}
		elseif (!\is_array($data))
		{
			// Do not translate.
			throw new InvalidArgumentException('Invalid inflector rule data.');
		}

		foreach ($data as $rule)
		{
			// Ensure a string is pushed.
			array_push($this->rules[$ruleType], (string) $rule);
		}
	}

	/**
	 * Gets an inflected word from the cache where the singular form is supplied.
	 *
	 * @param   string  $singular  A singular form of a word.
	 *
	 * @return  string|boolean  The cached inflection or false if none found.
	 *
	 * @since   1.0
	 */
	private function getCachedPlural($singular)
	{
		$singular = StringHelper::strtolower($singular);

		// Check if the word is in cache.
		if (isset($this->cache[$singular]))
		{
			return $this->cache[$singular];
		}

		return false;
	}

	/**
	 * Gets an inflected word from the cache where the plural form is supplied.
	 *
	 * @param   string  $plural  A plural form of a word.
	 *
	 * @return  string|boolean  The cached inflection or false if none found.
	 *
	 * @since   1.0
	 */
	private function getCachedSingular($plural)
	{
		$plural = StringHelper::strtolower($plural);

		return array_search($plural, $this->cache);
	}

	/**
	 * Execute a regex from rules.
	 *
	 * The 'plural' rule type expects a singular word.
	 * The 'singular' rule type expects a plural word.
	 *
	 * @param   string  $word      The string input.
	 * @param   string  $ruleType  String (eg, singular|plural)
	 *
	 * @return  string|boolean  An inflected string, or false if no rule could be applied.
	 *
	 * @since   1.0
	 */
	private function matchRegexRule($word, $ruleType)
	{
		// Cycle through the regex rules.
		foreach ($this->rules[$ruleType] as $regex => $replacement)
		{
			$matches     = 0;
			$matchedWord = preg_replace($regex, $replacement, $word, -1, $matches);

			if ($matches > 0)
			{
				return $matchedWord;
			}
		}

		return false;
	}

	/**
	 * Sets an inflected word in the cache.
	 *
	 * @param   string  $singular  The singular form of the word.
	 * @param   string  $plural    The plural form of the word. If omitted, it is assumed the singular and plural are identical.
	 *
	 * @return  void
	 *
	 * @since   1.0
	 */
	private function setCache($singular, $plural = null)
	{
		$singular = StringHelper::strtolower($singular);

		if ($plural === null)
		{
			$plural = $singular;
		}
		else
		{
			$plural = StringHelper::strtolower($plural);
		}

		$this->cache[$singular] = $plural;
	}

	/**
	 * Adds a countable word.
	 *
	 * @param   mixed  $data  A string or an array of strings to add.
	 *
	 * @return  Inflector  Returns this object to support chaining.
	 *
	 * @since   1.0
	 */
	public function addCountableRule($data)
	{
		$this->addRule($data, 'countable');

		return $this;
	}

	/**
	 * Adds a specific singular-plural pair for a word.
	 *
	 * @param   string  $singular  The singular form of the word.
	 * @param   string  $plural    The plural form of the word. If omitted, it is assumed the singular and plural are identical.
	 *
	 * @return  Inflector  Returns this object to support chaining.
	 *
	 * @since   1.0
	 */
	public function addWord($singular, $plural =null)
	{
		$this->setCache($singular, $plural);

		return $this;
	}

	/**
	 * Adds a pluralisation rule.
	 *
	 * @param   mixed  $data  A string or an array of regex rules to add.
	 *
	 * @return  Inflector  Returns this object to support chaining.
	 *
	 * @since   1.0
	 */
	public function addPluraliseRule($data)
	{
		$this->addRule($data, 'plural');

		return $this;
	}

	/**
	 * Adds a singularisation rule.
	 *
	 * @param   mixed  $data  A string or an array of regex rules to add.
	 *
	 * @return  Inflector  Returns this object to support chaining.
	 *
	 * @since   1.0
	 */
	public function addSingulariseRule($data)
	{
		$this->addRule($data, 'singular');

		return $this;
	}

	/**
	 * Gets an instance of the JStringInflector singleton.
	 *
	 * @param   boolean  $new  If true (default is false), returns a new instance regardless if one exists.
	 *                         This argument is mainly used for testing.
	 *
	 * @return  Inflector
	 *
	 * @since   1.0
	 */
	public static function getInstance($new = false)
	{
		if ($new)
		{
			return new static;
		}

		if (!\is_object(self::$instance))
		{
			self::$instance = new static;
		}

		return self::$instance;
	}

	/**
	 * Checks if a word is countable.
	 *
	 * @param   string  $word  The string input.
	 *
	 * @return  boolean  True if word is countable, false otherwise.
	 *
	 * @since   1.0
	 */
	public function isCountable($word)
	{
		return \in_array($word, $this->rules['countable']);
	}

	/**
	 * Checks if a word is in a plural form.
	 *
	 * @param   string  $word  The string input.
	 *
	 * @return  boolean  True if word is plural, false if not.
	 *
	 * @since   1.0
	 */
	public function isPlural($word)
	{
		// Try the cache for a known inflection.
		$inflection = $this->getCachedSingular($word);

		if ($inflection !== false)
		{
			return true;
		}

		$singularWord = $this->toSingular($word);

		if ($singularWord === false)
		{
			return false;
		}

		// Compute the inflection to cache the values, and compare.
		return $this->toPlural($singularWord) == $word;
	}

	/**
	 * Checks if a word is in a singular form.
	 *
	 * @param   string  $word  The string input.
	 *
	 * @return  boolean  True if word is singular, false if not.
	 *
	 * @since   1.0
	 */
	public function isSingular($word)
	{
		// Try the cache for a known inflection.
		$inflection = $this->getCachedPlural($word);

		if ($inflection !== false)
		{
			return true;
		}

		$pluralWord = $this->toPlural($word);

		if ($pluralWord === false)
		{
			return false;
		}

		// Compute the inflection to cache the values, and compare.
		return $this->toSingular($pluralWord) == $word;
	}

	/**
	 * Converts a word into its plural form.
	 *
	 * @param   string  $word  The singular word to pluralise.
	 *
	 * @return  string|boolean  An inflected string, or false if no rule could be applied.
	 *
	 * @since   1.0
	 */
	public function toPlural($word)
	{
		// Try to get the cached plural form from the singular.
		$cache = $this->getCachedPlural($word);

		if ($cache !== false)
		{
			return $cache;
		}

		// Check if the word is a known singular.
		if ($this->getCachedSingular($word))
		{
			return false;
		}

		// Compute the inflection.
		$inflected = $this->matchRegexRule($word, 'plural');

		if ($inflected !== false)
		{
			$this->setCache($word, $inflected);

			return $inflected;
		}

		// Dead code
		return false;
	}

	/**
	 * Converts a word into its singular form.
	 *
	 * @param   string  $word  The plural word to singularise.
	 *
	 * @return  string|boolean  An inflected string, or false if no rule could be applied.
	 *
	 * @since   1.0
	 */
	public function toSingular($word)
	{
		// Try to get the cached singular form from the plural.
		$cache = $this->getCachedSingular($word);

		if ($cache !== false)
		{
			return $cache;
		}

		// Check if the word is a known plural.
		if ($this->getCachedPlural($word))
		{
			return false;
		}

		// Compute the inflection.
		$inflected = $this->matchRegexRule($word, 'singular');

		if ($inflected !== false)
		{
			$this->setCache($inflected, $word);

			return $inflected;
		}

		return false;
	}
}


<?php
/**
 * Part of the Joomla Framework String Package
 *
 * @copyright  Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\String;

// PHP mbstring and iconv local configuration
if (version_compare(PHP_VERSION, '5.6', '>='))
{
	@ini_set('default_charset', 'UTF-8');
}
else
{
	// Check if mbstring extension is loaded and attempt to load it if not present except for windows
	if (\extension_loaded('mbstring'))
	{
		@ini_set('mbstring.internal_encoding', 'UTF-8');
		@ini_set('mbstring.http_input', 'UTF-8');
		@ini_set('mbstring.http_output', 'UTF-8');
	}

	// Same for iconv
	if (\function_exists('iconv'))
	{
		iconv_set_encoding('internal_encoding', 'UTF-8');
		iconv_set_encoding('input_encoding', 'UTF-8');
		iconv_set_encoding('output_encoding', 'UTF-8');
	}
}

/**
 * String handling class for UTF-8 data wrapping the phputf8 library. All functions assume the validity of UTF-8 strings.
 *
 * @since  1.3.0
 */
abstract class StringHelper
{
	/**
	 * Increment styles.
	 *
	 * @var    array
	 * @since  1.3.0
	 */
	protected static $incrementStyles = array(
		'dash' => array(
			'#-(\d+)$#',
			'-%d',
		),
		'default' => array(
			array('#\((\d+)\)$#', '#\(\d+\)$#'),
			array(' (%d)', '(%d)'),
		),
	);

	/**
	 * Increments a trailing number in a string.
	 *
	 * Used to easily create distinct labels when copying objects. The method has the following styles:
	 *
	 * default: "Label" becomes "Label (2)"
	 * dash:    "Label" becomes "Label-2"
	 *
	 * @param   string   $string  The source string.
	 * @param   string   $style   The the style (default|dash).
	 * @param   integer  $n       If supplied, this number is used for the copy, otherwise it is the 'next' number.
	 *
	 * @return  string  The incremented string.
	 *
	 * @since   1.3.0
	 */
	public static function increment($string, $style = 'default', $n = 0)
	{
		$styleSpec = isset(static::$incrementStyles[$style]) ? static::$incrementStyles[$style] : static::$incrementStyles['default'];

		// Regular expression search and replace patterns.
		if (\is_array($styleSpec[0]))
		{
			$rxSearch  = $styleSpec[0][0];
			$rxReplace = $styleSpec[0][1];
		}
		else
		{
			$rxSearch = $rxReplace = $styleSpec[0];
		}

		// New and old (existing) sprintf formats.
		if (\is_array($styleSpec[1]))
		{
			$newFormat = $styleSpec[1][0];
			$oldFormat = $styleSpec[1][1];
		}
		else
		{
			$newFormat = $oldFormat = $styleSpec[1];
		}

		// Check if we are incrementing an existing pattern, or appending a new one.
		if (preg_match($rxSearch, $string, $matches))
		{
			$n      = empty($n) ? ($matches[1] + 1) : $n;
			$string = preg_replace($rxReplace, sprintf($oldFormat, $n), $string);
		}
		else
		{
			$n = empty($n) ? 2 : $n;
			$string .= sprintf($newFormat, $n);
		}

		return $string;
	}

	/**
	 * Tests whether a string contains only 7bit ASCII bytes.
	 *
	 * You might use this to conditionally check whether a string needs handling as UTF-8 or not, potentially offering performance
	 * benefits by using the native PHP equivalent if it's just ASCII e.g.;
	 *
	 * <code>
	 * if (StringHelper::is_ascii($someString))
	 * {
	 *     // It's just ASCII - use the native PHP version
	 *     $someString = strtolower($someString);
	 * }
	 * else
	 * {
	 *     $someString = StringHelper::strtolower($someString);
	 * }
	 * </code>
	 *
	 * @param   string  $str  The string to test.
	 *
	 * @return  boolean True if the string is all ASCII
	 *
	 * @since   1.3.0
	 */
	public static function is_ascii($str)
	{
		return utf8_is_ascii($str);
	}

	/**
	 * UTF-8 aware alternative to ord()
	 *
	 * Returns the unicode ordinal for a character.
	 *
	 * @param   string  $chr  UTF-8 encoded character
	 *
	 * @return  integer Unicode ordinal for the character
	 *
	 * @link    https://www.php.net/ord
	 * @since   1.4.0
	 */
	public static function ord($chr)
	{
		return utf8_ord($chr);
	}

	/**
	 * UTF-8 aware alternative to strpos()
	 *
	 * Find position of first occurrence of a string.
	 *
	 * @param   string   $str     String being examined
	 * @param   string   $search  String being searched for
	 * @param   integer  $offset  Optional, specifies the position from which the search should be performed
	 *
	 * @return  integer|boolean  Number of characters before the first match or FALSE on failure
	 *
	 * @link    https://www.php.net/strpos
	 * @since   1.3.0
	 */
	public static function strpos($str, $search, $offset = false)
	{
		if ($offset === false)
		{
			return utf8_strpos($str, $search);
		}

		return utf8_strpos($str, $search, $offset);
	}

	/**
	 * UTF-8 aware alternative to strrpos()
	 *
	 * Finds position of last occurrence of a string.
	 *
	 * @param   string   $str     String being examined.
	 * @param   string   $search  String being searched for.
	 * @param   integer  $offset  Offset from the left of the string.
	 *
	 * @return  integer|boolean  Number of characters before the last match or false on failure
	 *
	 * @link    https://www.php.net/strrpos
	 * @since   1.3.0
	 */
	public static function strrpos($str, $search, $offset = 0)
	{
		return utf8_strrpos($str, $search, $offset);
	}

	/**
	 * UTF-8 aware alternative to substr()
	 *
	 * Return part of a string given character offset (and optionally length).
	 *
	 * @param   string   $str     String being processed
	 * @param   integer  $offset  Number of UTF-8 characters offset (from left)
	 * @param   integer  $length  Optional length in UTF-8 characters from offset
	 *
	 * @return  string|boolean
	 *
	 * @link    https://www.php.net/substr
	 * @since   1.3.0
	 */
	public static function substr($str, $offset, $length = false)
	{
		if ($length === false)
		{
			return utf8_substr($str, $offset);
		}

		return utf8_substr($str, $offset, $length);
	}

	/**
	 * UTF-8 aware alternative to strtolower()
	 *
	 * Make a string lowercase
	 *
	 * Note: The concept of a characters "case" only exists is some alphabets such as Latin, Greek, Cyrillic, Armenian and archaic Georgian - it does
	 * not exist in the Chinese alphabet, for example. See Unicode Standard Annex #21: Case Mappings
	 *
	 * @param   string  $str  String being processed
	 *
	 * @return  string|boolean  Either string in lowercase or FALSE is UTF-8 invalid
	 *
	 * @link    https://www.php.net/strtolower
	 * @since   1.3.0
	 */
	public static function strtolower($str)
	{
		return utf8_strtolower($str);
	}

	/**
	 * UTF-8 aware alternative to strtoupper()
	 *
	 * Make a string uppercase
	 *
	 * Note: The concept of a characters "case" only exists is some alphabets such as Latin, Greek, Cyrillic, Armenian and archaic Georgian - it does
	 * not exist in the Chinese alphabet, for example. See Unicode Standard Annex #21: Case Mappings
	 *
	 * @param   string  $str  String being processed
	 *
	 * @return  string|boolean  Either string in uppercase or FALSE is UTF-8 invalid
	 *
	 * @link    https://www.php.net/strtoupper
	 * @since   1.3.0
	 */
	public static function strtoupper($str)
	{
		return utf8_strtoupper($str);
	}

	/**
	 * UTF-8 aware alternative to strlen()
	 *
	 * Returns the number of characters in the string (NOT THE NUMBER OF BYTES).
	 *
	 * @param   string  $str  UTF-8 string.
	 *
	 * @return  integer  Number of UTF-8 characters in string.
	 *
	 * @link    https://www.php.net/strlen
	 * @since   1.3.0
	 */
	public static function strlen($str)
	{
		return utf8_strlen($str);
	}

	/**
	 * UTF-8 aware alternative to str_ireplace()
	 *
	 * Case-insensitive version of str_replace()
	 *
	 * @param   string   $search   String to search
	 * @param   string   $replace  Existing string to replace
	 * @param   string   $str      New string to replace with
	 * @param   integer  $count    Optional count value to be passed by referene
	 *
	 * @return  string  UTF-8 String
	 *
	 * @link    https://www.php.net/str_ireplace
	 * @since   1.3.0
	 */
	public static function str_ireplace($search, $replace, $str, $count = null)
	{
		if ($count === false)
		{
			return utf8_ireplace($search, $replace, $str);
		}

		return utf8_ireplace($search, $replace, $str, $count);
	}

	/**
	 * UTF-8 aware alternative to str_pad()
	 *
	 * Pad a string to a certain length with another string.
	 * $padStr may contain multi-byte characters.
	 *
	 * @param   string   $input   The input string.
	 * @param   integer  $length  If the value is negative, less than, or equal to the length of the input string, no padding takes place.
	 * @param   string   $padStr  The string may be truncated if the number of padding characters can't be evenly divided by the string's length.
	 * @param   integer  $type    The type of padding to apply
	 *
	 * @return  string
	 *
	 * @link    https://www.php.net/str_pad
	 * @since   1.4.0
	 */
	public static function str_pad($input, $length, $padStr = ' ', $type = STR_PAD_RIGHT)
	{
		return utf8_str_pad($input, $length, $padStr, $type);
	}

	/**
	 * UTF-8 aware alternative to str_split()
	 *
	 * Convert a string to an array.
	 *
	 * @param   string   $str       UTF-8 encoded string to process
	 * @param   integer  $splitLen  Number to characters to split string by
	 *
	 * @return  array
	 *
	 * @link    https://www.php.net/str_split
	 * @since   1.3.0
	 */
	public static function str_split($str, $splitLen = 1)
	{
		return utf8_str_split($str, $splitLen);
	}

	/**
	 * UTF-8/LOCALE aware alternative to strcasecmp()
	 *
	 * A case insensitive string comparison.
	 *
	 * @param   string  $str1    string 1 to compare
	 * @param   string  $str2    string 2 to compare
	 * @param   mixed   $locale  The locale used by strcoll or false to use classical comparison
	 *
	 * @return  integer   < 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.
	 *
	 * @link    https://www.php.net/strcasecmp
	 * @link    https://www.php.net/strcoll
	 * @link    https://www.php.net/setlocale
	 * @since   1.3.0
	 */
	public static function strcasecmp($str1, $str2, $locale = false)
	{
		if ($locale)
		{
			// Get current locale
			$locale0 = setlocale(LC_COLLATE, 0);

			if (!$locale = setlocale(LC_COLLATE, $locale))
			{
				$locale = $locale0;
			}

			// See if we have successfully set locale to UTF-8
			if (!stristr($locale, 'UTF-8') && stristr($locale, '_') && preg_match('~\.(\d+)$~', $locale, $m))
			{
				$encoding = 'CP' . $m[1];
			}
			elseif (stristr($locale, 'UTF-8') || stristr($locale, 'utf8'))
			{
				$encoding = 'UTF-8';
			}
			else
			{
				$encoding = 'nonrecodable';
			}

			// If we successfully set encoding it to utf-8 or encoding is sth weird don't recode
			if ($encoding == 'UTF-8' || $encoding == 'nonrecodable')
			{
				return strcoll(utf8_strtolower($str1), utf8_strtolower($str2));
			}

			return strcoll(
				static::transcode(utf8_strtolower($str1), 'UTF-8', $encoding),
				static::transcode(utf8_strtolower($str2), 'UTF-8', $encoding)
			);
		}

		return utf8_strcasecmp($str1, $str2);
	}

	/**
	 * UTF-8/LOCALE aware alternative to strcmp()
	 *
	 * A case sensitive string comparison.
	 *
	 * @param   string  $str1    string 1 to compare
	 * @param   string  $str2    string 2 to compare
	 * @param   mixed   $locale  The locale used by strcoll or false to use classical comparison
	 *
	 * @return  integer  < 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.
	 *
	 * @link    https://www.php.net/strcmp
	 * @link    https://www.php.net/strcoll
	 * @link    https://www.php.net/setlocale
	 * @since   1.3.0
	 */
	public static function strcmp($str1, $str2, $locale = false)
	{
		if ($locale)
		{
			// Get current locale
			$locale0 = setlocale(LC_COLLATE, 0);

			if (!$locale = setlocale(LC_COLLATE, $locale))
			{
				$locale = $locale0;
			}

			// See if we have successfully set locale to UTF-8
			if (!stristr($locale, 'UTF-8') && stristr($locale, '_') && preg_match('~\.(\d+)$~', $locale, $m))
			{
				$encoding = 'CP' . $m[1];
			}
			elseif (stristr($locale, 'UTF-8') || stristr($locale, 'utf8'))
			{
				$encoding = 'UTF-8';
			}
			else
			{
				$encoding = 'nonrecodable';
			}

			// If we successfully set encoding it to utf-8 or encoding is sth weird don't recode
			if ($encoding == 'UTF-8' || $encoding == 'nonrecodable')
			{
				return strcoll($str1, $str2);
			}

			return strcoll(static::transcode($str1, 'UTF-8', $encoding), static::transcode($str2, 'UTF-8', $encoding));
		}

		return strcmp($str1, $str2);
	}

	/**
	 * UTF-8 aware alternative to strcspn()
	 *
	 * Find length of initial segment not matching mask.
	 *
	 * @param   string   $str     The string to process
	 * @param   string   $mask    The mask
	 * @param   integer  $start   Optional starting character position (in characters)
	 * @param   integer  $length  Optional length
	 *
	 * @return  integer  The length of the initial segment of str1 which does not contain any of the characters in str2
	 *
	 * @link    https://www.php.net/strcspn
	 * @since   1.3.0
	 */
	public static function strcspn($str, $mask, $start = null, $length = null)
	{
		if ($start === false && $length === false)
		{
			return utf8_strcspn($str, $mask);
		}

		if ($length === false)
		{
			return utf8_strcspn($str, $mask, $start);
		}

		return utf8_strcspn($str, $mask, $start, $length);
	}

	/**
	 * UTF-8 aware alternative to stristr()
	 *
	 * Returns all of haystack from the first occurrence of needle to the end. Needle and haystack are examined in a case-insensitive manner to
	 * find the first occurrence of a string using case insensitive comparison.
	 *
	 * @param   string  $str     The haystack
	 * @param   string  $search  The needle
	 *
	 * @return string the sub string
	 *
	 * @link    https://www.php.net/stristr
	 * @since   1.3.0
	 */
	public static function stristr($str, $search)
	{
		return utf8_stristr($str, $search);
	}

	/**
	 * UTF-8 aware alternative to strrev()
	 *
	 * Reverse a string.
	 *
	 * @param   string  $str  String to be reversed
	 *
	 * @return  string   The string in reverse character order
	 *
	 * @link    https://www.php.net/strrev
	 * @since   1.3.0
	 */
	public static function strrev($str)
	{
		return utf8_strrev($str);
	}

	/**
	 * UTF-8 aware alternative to strspn()
	 *
	 * Find length of initial segment matching mask.
	 *
	 * @param   string   $str     The haystack
	 * @param   string   $mask    The mask
	 * @param   integer  $start   Start optional
	 * @param   integer  $length  Length optional
	 *
	 * @return  integer
	 *
	 * @link    https://www.php.net/strspn
	 * @since   1.3.0
	 */
	public static function strspn($str, $mask, $start = null, $length = null)
	{
		if ($start === null && $length === null)
		{
			return utf8_strspn($str, $mask);
		}

		if ($length === null)
		{
			return utf8_strspn($str, $mask, $start);
		}

		return utf8_strspn($str, $mask, $start, $length);
	}

	/**
	 * UTF-8 aware alternative to substr_replace()
	 *
	 * Replace text within a portion of a string.
	 *
	 * @param   string   $str     The haystack
	 * @param   string   $repl    The replacement string
	 * @param   integer  $start   Start
	 * @param   integer  $length  Length (optional)
	 *
	 * @return  string
	 *
	 * @link    https://www.php.net/substr_replace
	 * @since   1.3.0
	 */
	public static function substr_replace($str, $repl, $start, $length = null)
	{
		// Loaded by library loader
		if ($length === false)
		{
			return utf8_substr_replace($str, $repl, $start);
		}

		return utf8_substr_replace($str, $repl, $start, $length);
	}

	/**
	 * UTF-8 aware replacement for ltrim()
	 *
	 * Strip whitespace (or other characters) from the beginning of a string. You only need to use this if you are supplying the charlist
	 * optional arg and it contains UTF-8 characters. Otherwise ltrim will work normally on a UTF-8 string.
	 *
	 * @param   string  $str       The string to be trimmed
	 * @param   string  $charlist  The optional charlist of additional characters to trim
	 *
	 * @return  string  The trimmed string
	 *
	 * @link    https://www.php.net/ltrim
	 * @since   1.3.0
	 */
	public static function ltrim($str, $charlist = false)
	{
		if (empty($charlist) && $charlist !== false)
		{
			return $str;
		}

		if ($charlist === false)
		{
			return utf8_ltrim($str);
		}

		return utf8_ltrim($str, $charlist);
	}

	/**
	 * UTF-8 aware replacement for rtrim()
	 *
	 * Strip whitespace (or other characters) from the end of a string. You only need to use this if you are supplying the charlist
	 * optional arg and it contains UTF-8 characters. Otherwise rtrim will work normally on a UTF-8 string.
	 *
	 * @param   string  $str       The string to be trimmed
	 * @param   string  $charlist  The optional charlist of additional characters to trim
	 *
	 * @return  string  The trimmed string
	 *
	 * @link    https://www.php.net/rtrim
	 * @since   1.3.0
	 */
	public static function rtrim($str, $charlist = false)
	{
		if (empty($charlist) && $charlist !== false)
		{
			return $str;
		}

		if ($charlist === false)
		{
			return utf8_rtrim($str);
		}

		return utf8_rtrim($str, $charlist);
	}

	/**
	 * UTF-8 aware replacement for trim()
	 *
	 * Strip whitespace (or other characters) from the beginning and end of a string. You only need to use this if you are supplying the charlist
	 * optional arg and it contains UTF-8 characters. Otherwise trim will work normally on a UTF-8 string
	 *
	 * @param   string  $str       The string to be trimmed
	 * @param   string  $charlist  The optional charlist of additional characters to trim
	 *
	 * @return  string  The trimmed string
	 *
	 * @link    https://www.php.net/trim
	 * @since   1.3.0
	 */
	public static function trim($str, $charlist = false)
	{
		if (empty($charlist) && $charlist !== false)
		{
			return $str;
		}

		if ($charlist === false)
		{
			return utf8_trim($str);
		}

		return utf8_trim($str, $charlist);
	}

	/**
	 * UTF-8 aware alternative to ucfirst()
	 *
	 * Make a string's first character uppercase or all words' first character uppercase.
	 *
	 * @param   string  $str           String to be processed
	 * @param   string  $delimiter     The words delimiter (null means do not split the string)
	 * @param   string  $newDelimiter  The new words delimiter (null means equal to $delimiter)
	 *
	 * @return  string  If $delimiter is null, return the string with first character as upper case (if applicable)
	 *                  else consider the string of words separated by the delimiter, apply the ucfirst to each words
	 *                  and return the string with the new delimiter
	 *
	 * @link    https://www.php.net/ucfirst
	 * @since   1.3.0
	 */
	public static function ucfirst($str, $delimiter = null, $newDelimiter = null)
	{
		if ($delimiter === null)
		{
			return utf8_ucfirst($str);
		}

		if ($newDelimiter === null)
		{
			$newDelimiter = $delimiter;
		}

		return implode($newDelimiter, array_map('utf8_ucfirst', explode($delimiter, $str)));
	}

	/**
	 * UTF-8 aware alternative to ucwords()
	 *
	 * Uppercase the first character of each word in a string.
	 *
	 * @param   string  $str  String to be processed
	 *
	 * @return  string  String with first char of each word uppercase
	 *
	 * @link    https://www.php.net/ucwords
	 * @since   1.3.0
	 */
	public static function ucwords($str)
	{
		return utf8_ucwords($str);
	}

	/**
	 * Transcode a string.
	 *
	 * @param   string  $source        The string to transcode.
	 * @param   string  $fromEncoding  The source encoding.
	 * @param   string  $toEncoding    The target encoding.
	 *
	 * @return  mixed  The transcoded string, or null if the source was not a string.
	 *
	 * @link    https://bugs.php.net/bug.php?id=48147
	 *
	 * @since   1.3.0
	 */
	public static function transcode($source, $fromEncoding, $toEncoding)
	{
		if (\is_string($source))
		{
			switch (ICONV_IMPL)
			{
				case 'glibc':
					return @iconv($fromEncoding, $toEncoding . '//TRANSLIT,IGNORE', $source);

				case 'libiconv':
				default:
					return iconv($fromEncoding, $toEncoding . '//IGNORE//TRANSLIT', $source);
			}
		}
	}

	/**
	 * Tests a string as to whether it's valid UTF-8 and supported by the Unicode standard.
	 *
	 * Note: this function has been modified to simple return true or false.
	 *
	 * @param   string  $str  UTF-8 encoded string.
	 *
	 * @return  boolean  true if valid
	 *
	 * @author  <hsivonen@iki.fi>
	 * @link    https://hsivonen.fi/php-utf8/
	 * @see     compliant
	 * @since   1.3.0
	 */
	public static function valid($str)
	{
		return utf8_is_valid($str);
	}

	/**
	 * Tests whether a string complies as UTF-8.
	 *
	 * This will be much faster than StringHelper::valid() but will pass five and six octet UTF-8 sequences, which are not supported by Unicode and
	 * so cannot be displayed correctly in a browser. In other words it is not as strict as StringHelper::valid() but it's faster. If you use it to
	 * validate user input, you place yourself at the risk that attackers will be able to inject 5 and 6 byte sequences (which may or may not be a
	 * significant risk, depending on what you are are doing).
	 *
	 * @param   string  $str  UTF-8 string to check
	 *
	 * @return  boolean  TRUE if string is valid UTF-8
	 *
	 * @see     StringHelper::valid
	 * @link    https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php#54805
	 * @since   1.3.0
	 */
	public static function compliant($str)
	{
		return utf8_compliant($str);
	}

	/**
	 * Converts Unicode sequences to UTF-8 string.
	 *
	 * @param   string  $str  Unicode string to convert
	 *
	 * @return  string  UTF-8 string
	 *
	 * @since   1.3.0
	 */
	public static function unicode_to_utf8($str)
	{
		if (\extension_loaded('mbstring'))
		{
			return preg_replace_callback(
				'/\\\\u([0-9a-fA-F]{4})/',
				function ($match)
				{
					return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
				},
				$str
			);
		}

		return $str;
	}

	/**
	 * Converts Unicode sequences to UTF-16 string.
	 *
	 * @param   string  $str  Unicode string to convert
	 *
	 * @return  string  UTF-16 string
	 *
	 * @since   1.3.0
	 */
	public static function unicode_to_utf16($str)
	{
		if (\extension_loaded('mbstring'))
		{
			return preg_replace_callback(
				'/\\\\u([0-9a-fA-F]{4})/',
				function ($match)
				{
					return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UTF-16BE');
				},
				$str
			);
		}

		return $str;
	}
}


<?php

namespace Salva\JshrinkBundle\Twig\TokenParser;

use Salva\JshrinkBundle\Twig\Node\JshrinkNode;
use Twig_Token;
use Twig_TokenParser;

/**
 * Jshrink.
 */
class JshrinkTokenParser extends Twig_TokenParser
{
    /**
     * @var bool
     */
    private $enabled;

    /**
     * JShrink configuration.
     *
     * @var array
     */
    private $config;

    /**
     * Twig extension based on bundle configuration.
     *
     * @param array $config
     * @param bool  $enabled
     */
    public function __construct(array $config = array(), $enabled = true)
    {
        $this->config = $config;
        $this->enabled = (bool) $enabled;
    }

    /**
     * {@inheritdoc}
     */
    public function parse(Twig_Token $token)
    {
        $lineNumber = $token->getLine();

        $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
        $body = $this->parser->subparse(function (Twig_Token $token) {
            return $token->test('endjshrink');
        }, true);
        $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);

        if ($this->enabled) {
            $node = new JshrinkNode($body, $lineNumber, $this->getTag());
            $node->setConfig($this->config);

            return $node;
        }

        return $body;
    }

    /**
     * {@inheritdoc}
     */
    public function getTag()
    {
        return 'jshrink';
    }
}


<?php

namespace Salva\JshrinkBundle\Cache;

/**
 * Class CachedMinifier.
 *
 * Cached minified content
 */
class CachedMinifier implements CacheInterface
{
    /**
     * @var string Cache dir
     */
    private $cacheDir;

    /**
     * @param $cacheDir
     */
    public function __construct($cacheDir)
    {
        $this->cacheDir = $cacheDir;
    }

    /**
     * {@inheritdoc}
     */
    public function minify($content, $options)
    {
        if ($this->cacheDir) {
            if (!file_exists($this->cacheDir)) {
                mkdir($this->cacheDir, 0777, true);
            }

            $hash = md5($content);
            $file = implode(DIRECTORY_SEPARATOR, array($this->cacheDir, $hash));
            if (!file_exists($file)) {
                file_put_contents($file, \JShrink\Minifier::minify($content, $options));
            }

            return file_get_contents($file);
        } else {
            return \JShrink\Minifier::minify($content, $options);
        }
    }
}


<?php

namespace Salva\JshrinkBundle\Twig\Node;

use Twig_Compiler;
use Twig_Node;

/**
 * Jshrink Twig Extension.
 */
class JshrinkNode extends Twig_Node
{
    /**
     * JShrink configuration.
     *
     * @var array
     */
    private $config;

    /**
     * {@inheritdoc}
     */
    public function __construct(Twig_Node $body, $lineNumber, $tag = 'jshrink')
    {
        parent::__construct(array('body' => $body), array(), $lineNumber, $tag);
    }

    /**
     * Twig extension based on bundle configuration.
     *
     * @param array $config
     */
    public function setConfig(array $config = array())
    {
        $this->config = $config;
    }

    /**
     * {@inheritdoc}
     */
    public function compile(Twig_Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write("ob_start();\n")
            ->subcompile($this->getNode('body'))
            ->write("echo \$context['_jshrink_cached_minifier']->minify(")
            ->raw('trim(ob_get_clean()), '.var_export($this->config, true).'')
            ->raw(");\n");
    }
}


<?php

namespace Salva\JshrinkBundle\Assetic\Filter;

use Assetic\Asset\AssetInterface;
use Assetic\Filter\FilterInterface;
use JShrink\Minifier;

class JshrinkFilter implements FilterInterface
{
    /**
     * Array containing app configurations.
     *
     * @var array
     */
    protected $options = array();

    /**
     * @param array $options
     */
    public function __construct(array $options = array())
    {
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public function filterLoad(AssetInterface $asset)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function filterDump(AssetInterface $asset)
    {
        $asset->setContent(Minifier::minify($asset->getContent(), $this->options));
    }
}


<?php

class EduAdminAPIHelper {
	protected static $_instance = null;

	/**
	 * @param $course_id
	 * @param $fetch_months
	 * @param $group_by_city
	 *
	 * @return string
	 */
	public function GetCourseDetailInfo( $course_id, $fetch_months, $group_by_city ) {
		return EDU()->get_transient( 'eduadmin-object', function() use ( $course_id, $fetch_months, $group_by_city ) {
			$expands = array();
			$selects = array();

			$selects[] = 'CourseTemplateId';
			$selects[] = 'CourseName';
			$selects[] = 'InternalCourseName';
			$selects[] = 'ImageUrl';
			$selects[] = 'CourseDescription';
			$selects[] = 'CourseDescriptionShort';
			$selects[] = 'CourseGoal';
			$selects[] = 'TargetGroup';
			$selects[] = 'Prerequisites';
			$selects[] = 'CourseAfter';
			$selects[] = 'Quote';
			$selects[] = 'Days';
			$selects[] = 'StartTime';
			$selects[] = 'EndTime';
			$selects[] = 'RequireCivicRegistrationNumber';

			$expands['Subjects']   = '$select=SubjectName;';
			$expands['Categories'] = '$select=CategoryName;';
			$expands['PriceNames'] = '$filter=PublicPriceName;';
			$expands['Events']     =
				'$filter=' .
				'HasPublicPriceName' .
				' and StatusId eq 1' .
				' and CustomerId eq null' .
				' and CompanySpecific eq false' .
				' and LastApplicationDate ge ' . date( 'c', strtotime('now 23:59:59') ) .
				' and StartDate le ' . date( 'c', strtotime( 'now 23:59:59 +' . $fetch_months . ' months' ) ) .
				' and EndDate ge ' . date( 'c', strtotime( 'now' ) ) .
				';' .
				'$expand=PriceNames($filter=PublicPriceName;$select=PriceNameId,PriceNameDescription,Price,MaxParticipantNumber,NumberOfParticipants,DiscountPercent;),EventDates($orderby=StartDate;$select=StartDate,EndDate;),Sessions($expand=PriceNames($filter=PublicPriceName;);$filter=HasPublicPriceName;),PaymentMethods' .
				';' .
				'$orderby=StartDate asc' . ( $group_by_city ? ', City asc' : '' ) .
				';' .
				'$select=EventId,City,ParticipantNumberLeft,MaxParticipantNumber,StartDate,EndDate,AddressName,LocationId';

			$expands['CustomFields'] = '$filter=ShowOnWeb;$select=CustomFieldId,CustomFieldName,CustomFieldType,CustomFieldValue,CustomFieldChecked,CustomFieldDate,CustomFieldAlternativeId,CustomFieldAlternativeValue;';

			$expand_arr = array();
			foreach ( $expands as $key => $value ) {
				if ( empty( $value ) ) {
					$expand_arr[] = $key;
				} else {
					$expand_arr[] = $key . '(' . $value . ')';
				}
			}

			return wp_json_encode( EDUAPI()->OData->CourseTemplates->GetItem(
				$course_id,
				join( ',', $selects ),
				join( ',', $expand_arr )
			) );
		}, 10, $course_id, $fetch_months, $group_by_city );
	}

	public function GetCourseList( $attributes, $category_id, $city, $subjectid, $courselevel, $custom_order_by, $custom_order_by_order ) {
		$fetch_months = get_option( 'eduadmin-monthsToFetch', 6 );
		if ( ! is_numeric( $fetch_months ) ) {
			$fetch_months = 6;
		}

		$filters = array();
		$expands = array();
		$selects = array();
		$sorting = array();

		$selects[] = 'CourseTemplateId';
		$selects[] = 'CourseName';
		$selects[] = 'CategoryId';
		$selects[] = 'CategoryName';
		$selects[] = 'InternalCourseName';
		$selects[] = 'ImageUrl';
		$selects[] = 'CourseDescription';
		$selects[] = 'CourseDescriptionShort';
		$selects[] = 'CourseGoal';
		$selects[] = 'TargetGroup';
		$selects[] = 'Prerequisites';
		$selects[] = 'CourseAfter';
		$selects[] = 'Quote';
		$selects[] = 'Days';
		$selects[] = 'StartTime';
		$selects[] = 'EndTime';
		$selects[] = 'RequireCivicRegistrationNumber';

		$expands['Subjects']   = '$select=SubjectName;';
		$expands['Categories'] = '$select=CategoryName;';
		$expands['PriceNames'] = '$filter=PublicPriceName';
		$expands['Events']     =
			'$filter=' .
			'HasPublicPriceName' .
			' and StatusId eq 1' .
			' and CustomerId eq null' .
			' and CompanySpecific eq false' .
			' and LastApplicationDate ge ' . date( 'c', strtotime('now 23:59:59') ) .
			' and StartDate le ' . date( 'c', strtotime( 'now 23:59:59 +' . $fetch_months . ' months' ) ) .
			' and EndDate ge ' . date( 'c', strtotime( 'now' ) ) .
			';' .
			'$expand=PriceNames($filter=PublicPriceName;$select=PriceNameId,PriceNameDescription,Price,MaxParticipantNumber,NumberOfParticipants,DiscountPercent;)' .
			';' .
			'$orderby=StartDate asc' .
			';' .
			'$select=EventId,City,ParticipantNumberLeft,MaxParticipantNumber,StartDate,EndDate,AddressName,EventName';

		$expands['CustomFields'] = '$filter=ShowOnWeb;$select=CustomFieldId,CustomFieldName,CustomFieldType,CustomFieldValue,CustomFieldChecked,CustomFieldDate,CustomFieldAlternativeId,CustomFieldAlternativeValue;';

		$filters[] = 'ShowOnWeb';

		$show_events_with_events_only    = $attributes['onlyevents'];
		$show_events_without_events_only = $attributes['onlyempty'];

		if ( !empty($category_id) ) {
			$filters[]              = 'CategoryId eq ' . $category_id;
		}

		if ( ! empty( $city ) ) {
			$filters[]          = 'Events/any(e:e/LocationId eq ' . intval( $city ) . ')';
		}

		if ( isset( $attributes['subject'] ) && ! empty( $attributes['subject'] ) ) {
			$filters[] = 'Subjects/any(s:s/SubjectName eq \'' . sanitize_text_field( $attributes['subject'] ) . '\')';
		}

		if ( ! empty( $subjectid ) ) {
			$filters[]               = 'Subjects/any(s:s/SubjectId eq ' . $subjectid . ')';
			$attributes['subjectid'] = $subjectid;
		}

		if ( ! empty( $courselevel ) ) {
			$filters[] = 'CourseLevelId eq ' . $courselevel;
		}

		$sort_order = get_option( 'eduadmin-listSortOrder', 'SortIndex' );

		if ( null !== $custom_order_by ) {
			$orderby   = explode( ' ', $custom_order_by );
			$sortorder = explode( ' ', $custom_order_by_order );
			foreach ( $orderby as $od => $v ) {
				if ( isset( $sortorder[ $od ] ) ) {
					$or = $sortorder[ $od ];
				} else {
					$or = 'asc';
				}

				if ( edu_validate_column( 'course', $v ) !== false ) {
					$sorting[] = $v . ' ' . strtolower( $or );
				}
			}
		}
		if ( edu_validate_column( 'course', $sort_order ) !== false ) {
			$sorting[] = $sort_order . ' asc';
		}

		$expand_arr = array();
		foreach ( $expands as $key => $value ) {
			if ( empty( $value ) ) {
				$expand_arr[] = $key;
			} else {
				$expand_arr[] = $key . '(' . $value . ')';
			}
		}

		return EDU()->get_transient( 'eduadmin-listcourses-courses', function() use ( $selects, $filters, $expand_arr, $sorting ) {
			return EDUAPI()->OData->CourseTemplates->Search(
				join( ',', $selects ),
				join( ' and ', $filters ),
				join( ',', $expand_arr ),
				join( ',', $sorting )
			);
		}, 300, $selects, $filters, $sorting );
	}

	public function GetEventList( $attributes, $category_id, $city, $subjectid, $courselevel, $custom_order_by, $custom_order_by_order ) {
		$fetch_months = get_option( 'eduadmin-monthsToFetch', 6 );
		if ( ! is_numeric( $fetch_months ) ) {
			$fetch_months = 6;
		}

		$filters = array();
		$expands = array();
		$selects = array();

		$selects[] = 'CourseTemplateId';
		$selects[] = 'CourseName';
		$selects[] = 'InternalCourseName';
		$selects[] = 'ImageUrl';
		$selects[] = 'CourseDescription';
		$selects[] = 'CourseGoal';
		$selects[] = 'TargetGroup';
		$selects[] = 'Prerequisites';
		$selects[] = 'CourseAfter';
		$selects[] = 'Quote';
		$selects[] = 'Days';
		$selects[] = 'StartTime';
		$selects[] = 'EndTime';
		$selects[] = 'RequireCivicRegistrationNumber';

		$expands['Subjects']   = '$select=SubjectName;';
		$expands['Categories'] = '$select=CategoryName;';
		$expands['PriceNames'] = '$filter=PublicPriceName';
		$expands['Events']     =
			'$filter=' .
			'HasPublicPriceName' .
			' and StatusId eq 1' .
			' and CustomerId eq null' .
			' and CompanySpecific eq false' .
			' and LastApplicationDate ge ' . date( 'c', strtotime('now 23:59:59') ) .
			' and StartDate le ' . date( 'c', strtotime( 'now 23:59:59 +' . $fetch_months . ' months' ) ) .
			' and EndDate ge ' . date( 'c', strtotime( 'now' ) ) .
			';' .
			'$expand=PriceNames($filter=PublicPriceName;$select=PriceNameId,PriceNameDescription,Price,MaxParticipantNumber,NumberOfParticipants,DiscountPercent;),EventDates($orderby=StartDate;$select=StartDate,EndDate;)' .
			';' .
			'$orderby=StartDate asc' .
			';' .
			'$select=EventId,City,ParticipantNumberLeft,MaxParticipantNumber,StartDate,EndDate,AddressName,EventName';

		$expands['CustomFields'] = '$filter=ShowOnWeb;$select=CustomFieldId,CustomFieldName,CustomFieldType,CustomFieldValue,CustomFieldChecked,CustomFieldDate,CustomFieldAlternativeId,CustomFieldAlternativeValue;';

		$filters[] = 'ShowOnWeb';

		$showEventsWithEventsOnly    = $attributes['onlyevents'];
		$showEventsWithoutEventsOnly = $attributes['onlyempty'];

		if ( !empty($category_id) ) {
			$filters[]              = 'CategoryId eq ' . $category_id;
		}

		if ( ! empty( $city ) ) {
			$filters[]          = 'Events/any(e:e/LocationId eq ' . intval( $city ) . ')';
		}

		if ( isset( $attributes['subject'] ) && ! empty( $attributes['subject'] ) ) {
			$filters[] = 'Subjects/any(s:s/SubjectName eq \'' . sanitize_text_field( $attributes['subject'] ) . '\')';
		}

		if ( ! empty( $subjectid ) ) {
			$filters[]               = 'Subjects/any(s:s/SubjectId eq ' . $subjectid . ')';
			$attributes['subjectid'] = $subjectid;
		}

		if ( ! empty( $courselevel ) ) {
			$filters[] = 'CourseLevelId eq ' . $courselevel;
		}

		$order_by     = array();
		$order        = array( 1 );
		$order_option = get_option( 'eduadmin-listSortOrder', 'SortIndex' );

		if ( null !== $custom_order_by ) {
			$order_by = explode( ' ', $custom_order_by );
			if ( null !== $custom_order_by_order ) {
				$order        = array();
				$custom_order = explode( ' ', $custom_order_by_order );
				foreach ( $custom_order as $coVal ) {
					! isset( $coVal ) || $coVal === "asc" ? array_push( $order, 1 ) : array_push( $order, -1 );
				}
			}
		} else {
			if ( $order_option === "SortIndex" ) {
				$order_option = "StartDate";
			}
			array_push( $order_by, $order_option );
			array_push( $order, 1 );
		}

		$expand_arr = array();
		foreach ( $expands as $key => $value ) {
			if ( empty( $value ) ) {
				$expand_arr[] = $key;
			} else {
				$expand_arr[] = $key . '(' . $value . ')';
			}
		}

		return EDU()->get_transient( 'eduadmin-listevent-courses', function() use ( $selects, $filters, $expand_arr ) {
			return EDUAPI()->OData->CourseTemplates->Search(
				join( ',', $selects ),
				join( ' and ', $filters ),
				join( ',', $expand_arr )
			);
		}, 300, $selects, $filters );
	}

	/**
	 * @return mixed
	 */
	public function GetOrganization() {
		return EDU()->get_transient( 'eduadmin-organization', function() {
			return EDUAPI()->REST->Organisation->GetOrganisation();
		}, DAY_IN_SECONDS );
	}

	public function GetRegions() {
		return EDU()->get_transient( 'eduadmin-regions', function() {
			return EDUAPI()->OData->Regions->Search(
				'RegionId,RegionName',
				null,
				'Locations($filter=PublicLocation;$expand=LocationAddresses;$select=LocationId,City,PublicLocation;)',
				'RegionName asc'
			);
		}, DAY_IN_SECONDS );
	}

	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}

		return self::$_instance;
	}
}

function EDUAPIHelper() {
	return EduAdminAPIHelper::instance();
}

$GLOBALS['eduadminapihelper'] = EDUAPIHelper();


<?php
/**
 * WordPress session managment.
 *
 * Standardizes WordPress session data using database-backed options for storage.
 * for storing user session information.
 *
 * @package    WordPress
 * @subpackage Session
 * @since      3.7.0
 */
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WordPress Session class for managing user session data.
 *
 * @package WordPress
 * @since   3.7.0
 */
final class WP_Session extends Recursive_ArrayAccess implements Iterator, Countable {
	/**
	 * ID of the current session.
	 *
	 * @var string
	 */
	public $session_id;
	/**
	 * Unix timestamp when session expires.
	 *
	 * @var int
	 */
	protected $expires;
	/**
	 * Unix timestamp indicating when the expiration time needs to be reset.
	 *
	 * @var int
	 */
	protected $exp_variant;
	/**
	 * Singleton instance.
	 *
	 * @var bool|WP_Session
	 */
	private static $instance = false;

	/**
	 * Retrieve the current session instance.
	 *
	 * @param bool $session_id Session ID from which to populate data.
	 *
	 * @return bool|WP_Session
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Default constructor.
	 * Will rebuild the session collection from the given session ID if it exists. Otherwise, will
	 * create a new session with that ID.
	 *
	 * @param $session_id
	 *
	 * @uses apply_filters Calls `wp_session_expiration` to determine how long until sessions expire.
	 */
	protected function __construct() {
		if ( isset( $_COOKIE[ WP_SESSION_COOKIE ] ) ) {
			$cookie        = stripslashes( $_COOKIE[ WP_SESSION_COOKIE ] );
			$cookie_crumbs = explode( '||', $cookie );
			if ( $this->is_valid_md5( $cookie_crumbs[0] ) ) {
				$this->session_id = $cookie_crumbs[0];
			} else {
				$this->regenerate_id( true );
			}
			$this->expires     = $cookie_crumbs[1];
			$this->exp_variant = $cookie_crumbs[2];
			// Update the session expiration if we're past the variant time
			if ( time() > $this->exp_variant ) {
				$this->set_expiration();
				delete_option( "_wp_session_expires_{$this->session_id}" );
				add_option( "_wp_session_expires_{$this->session_id}", $this->expires, '', 'no' );
			}
		} else {
			$this->session_id = $this->generate_id();
			$this->set_expiration();
		}
		$this->read_data();
		$this->set_cookie();
	}

	/**
	 * Set both the expiration time and the expiration variant.
	 *
	 * If the current time is below the variant, we don't update the session's expiration time. If it's
	 * greater than the variant, then we update the expiration time in the database.  This prevents
	 * writing to the database on every page load for active sessions and only updates the expiration
	 * time if we're nearing when the session actually expires.
	 *
	 * By default, the expiration time is set to 30 minutes.
	 * By default, the expiration variant is set to 24 minutes.
	 *
	 * As a result, the session expiration time - at a maximum - will only be written to the database once
	 * every 24 minutes.  After 30 minutes, the session will have been expired. No cookie will be sent by
	 * the browser, and the old session will be queued for deletion by the garbage collector.
	 *
	 * @uses apply_filters Calls `wp_session_expiration_variant` to get the max update window for session data.
	 * @uses apply_filters Calls `wp_session_expiration` to get the standard expiration time for sessions.
	 */
	protected function set_expiration() {
		$this->exp_variant = time() + (int) apply_filters( 'wp_session_expiration_variant', 24 * 60 );
		$this->expires     = time() + (int) apply_filters( 'wp_session_expiration', 30 * 60 );
	}

	/**
	 * Set the session cookie
	 */
	protected function set_cookie() {
		@setcookie( WP_SESSION_COOKIE, $this->session_id . '||' . $this->expires . '||' . $this->exp_variant, $this->expires, COOKIEPATH, COOKIE_DOMAIN );
	}

	/**
	 * Generate a cryptographically strong unique ID for the session token.
	 *
	 * @return string
	 */
	protected function generate_id() {
		require_once( ABSPATH . 'wp-includes/class-phpass.php' );
		$hasher = new PasswordHash( 8, false );

		return md5( $hasher->get_random_bytes( 32 ) );
	}

	/**
	 * Checks if is valid md5 string
	 *
	 * @param string $md5
	 *
	 * @return int
	 */
	protected function is_valid_md5( $md5 = '' ) {
		return preg_match( '/^[a-f0-9]{32}$/', $md5 );
	}

	/**
	 * Read data from a transient for the current session.
	 *
	 * Automatically resets the expiration time for the session transient to some time in the future.
	 *
	 * @return array
	 */
	protected function read_data() {
		$this->container = get_option( "_wp_session_{$this->session_id}", array() );

		return $this->container;
	}

	/**
	 * Write the data from the current session to the data storage system.
	 */
	public function write_data() {
		$option_key = "_wp_session_{$this->session_id}";
		// Only write the collection to the DB if it's changed.
		if ( $this->dirty ) {
			if ( false === get_option( $option_key ) ) {
				add_option( "_wp_session_{$this->session_id}", $this->container, '', 'no' );
				add_option( "_wp_session_expires_{$this->session_id}", $this->expires, '', 'no' );
			} else {
				delete_option( "_wp_session_{$this->session_id}" );
				add_option( "_wp_session_{$this->session_id}", $this->container, '', 'no' );
			}
		}
	}

	/**
	 * Output the current container contents as a JSON-encoded string.
	 *
	 * @return string
	 */
	public function json_out() {
		return json_encode( $this->container );
	}

	/**
	 * Decodes a JSON string and, if the object is an array, overwrites the session container with its contents.
	 *
	 * @param string $data
	 *
	 * @return bool
	 */
	public function json_in( $data ) {
		$array = json_decode( $data );
		if ( is_array( $array ) ) {
			$this->container = $array;

			return true;
		}

		return false;
	}

	/**
	 * Regenerate the current session's ID.
	 *
	 * @param bool $delete_old Flag whether or not to delete the old session data from the server.
	 */
	public function regenerate_id( $delete_old = false ) {
		if ( $delete_old ) {
			delete_option( "_wp_session_{$this->session_id}" );
		}
		$this->session_id = $this->generate_id();
		$this->set_cookie();
	}

	/**
	 * Check if a session has been initialized.
	 *
	 * @return bool
	 */
	public function session_started() {
		return ! ! self::$instance;
	}

	/**
	 * Return the read-only cache expiration value.
	 *
	 * @return int
	 */
	public function cache_expiration() {
		return $this->expires;
	}

	/**
	 * Flushes all session variables.
	 */
	public function reset() {
		$this->container = array();
	}
	/*****************************************************************/
	/*                     Iterator Implementation                   */
	/*****************************************************************/
	/**
	 * Current position of the array.
	 *
	 * @link http://php.net/manual/en/iterator.current.php
	 *
	 * @return mixed
	 */
	public function current() {
		return current( $this->container );
	}

	/**
	 * Key of the current element.
	 *
	 * @link http://php.net/manual/en/iterator.key.php
	 *
	 * @return mixed
	 */
	public function key() {
		return key( $this->container );
	}

	/**
	 * Move the internal point of the container array to the next item
	 *
	 * @link http://php.net/manual/en/iterator.next.php
	 *
	 * @return void
	 */
	public function next() {
		next( $this->container );
	}

	/**
	 * Rewind the internal point of the container array.
	 *
	 * @link http://php.net/manual/en/iterator.rewind.php
	 *
	 * @return void
	 */
	public function rewind() {
		reset( $this->container );
	}

	/**
	 * Is the current key valid?
	 *
	 * @link http://php.net/manual/en/iterator.rewind.php
	 *
	 * @return bool
	 */
	public function valid() {
		return $this->offsetExists( $this->key() );
	}
	/*****************************************************************/
	/*                    Countable Implementation                   */
	/*****************************************************************/
	/**
	 * Get the count of elements in the container array.
	 *
	 * @link http://php.net/manual/en/countable.count.php
	 *
	 * @return int
	 */
	public function count() {
		return count( $this->container );
	}
}


<?php

/**
 * Class EduAdmin_REST_ProgrammeStart
 */
class EduAdmin_REST_ProgrammeStart extends EduAdminRESTClient {
	protected $api_url = '/v1/ProgrammeStart';

	/**
	 * @param integer   $programme_start_id
	 * @param bool|null $show_external
	 *
	 * @return mixed
	 */
	public function BookingQuestions( $programme_start_id, $show_external = null ) {
		$params = array();
		if ( isset( $show_external ) ) {
			$params['showExternal'] = $show_external ? 'true' : 'false';
		}

		return parent::GET(
			"/$programme_start_id/BookingQuestions",
			$params,
			get_called_class() . '|' . __FUNCTION__
		);
	}
}


<?php

/**
 * Class EduAdmin_REST_Customer
 */
class EduAdmin_REST_Customer extends EduAdminRESTClient {
	protected $api_url = '/v1/Customer';

	/**
	 * @param EduAdmin_Data_Customer|stdClass|object $customer
	 *
	 * @return mixed
	 */
	public function Create( $customer ) {
		return parent::POST( '/',
			$customer,
			get_called_class() . '|' . __FUNCTION__
		);
	}

	/**
	 * @param integer $customer_id
	 * @param EduAdmin_Data_Customer|stdClass|object $customer
	 *
	 * @return mixed
	 */
	public function Update( $customer_id, $customer ) {
		return parent::PATCH( "/$customer_id",
			$customer,
			get_called_class() . '|' . __FUNCTION__
		);
	}

	/**
	 * @param integer $customer_id
	 * @param integer $event_id
	 * @param integer|null $contact_person_id
	 *
	 * @return mixed
	 */
	public function GetValidVouchers( $customer_id, $event_id, $contact_person_id = null ) {
		$params = array();
		if ( isset( $contact_person_id ) ) {
			$params['contactPersonId'] = $contact_person_id;
		}

		return parent::GET(
			"/$customer_id/ValidVouchers/$event_id",
			$params,
			get_called_class() . '|' . __FUNCTION__
		);
	}
}


<?php

/**
 * Class EduAdminRESTClient
 */
class EduAdminRESTClient {
	protected $user_agent = 'EduAdmin REST API Client';
	/**
	 * @var string API User
	 */
	static $api_user = null;
	/**
	 * @var string API Password
	 */
	static $api_pass = null;
	static $root_url = 'https://api.eduadmin.se';
	protected $api_url = '';

	/**
	 * @param resource $curl
	 *
	 * @return mixed
	 */
	private function execute_request( $curl ) {
		$r   = curl_exec( $curl );
		$i   = curl_getinfo( $curl );
		$obj = array();
		if ( false === $r || ( json_decode( $r ) && isset( json_decode( $r )->error ) ) || ( $i['http_code'] < 200 || $i['http_code'] > 299 ) ) {
			curl_close( $curl );
			if ( null !== json_decode( $r ) ) {
				$obj = json_decode( $r, true );
			} else {
				$obj['data'] = $r;
			}
			$obj['@curl']  = $i;
			$obj['@error'] = $r;

			return $obj;
		}
		curl_close( $curl );
		if ( ( substr( $r, 0, 1 ) === '{' || substr( $r, 0, 1 ) === '[' )
		     && null !== json_decode( $r, true ) ) {
			$obj = json_decode( $r, true );
		} else {
			if ( substr( $r, 0, 1 ) === '"' ) {
				$obj['data'] = json_decode( $r, true );
			} else {
				$obj['data'] = $r;
			}
		}
		$obj['@curl'] = $i;

		return $obj;
	}

	/**
	 * @param      $endpoint   string Where are we going with this request?
	 * @param      $params     string|object|array Contains all parameters that we want to pass to the API
	 * @param      $method_name string Which method called us?
	 * @param bool $is_json Decides if this is a post with JSON
	 *
	 * @return mixed
	 */
	public function POST( $endpoint, $params, $method_name, $is_json = true ) {
		return $this->make_request( 'POST', $endpoint, $params, $method_name, $is_json );
	}

	/**
	 * @param      $endpoint   string Where are we going with this request?
	 * @param      $params     string|object|array Contains all parameters that we want to pass to the API
	 * @param      $method_name string Which method called us?
	 * @param bool $is_json Decides if this is a post with JSON
	 *
	 * @return mixed
	 */
	public function PUT( $endpoint, $params, $method_name, $is_json = true ) {
		return $this->make_request( 'PUT', $endpoint, $params, $method_name, $is_json );
	}

	/**
	 * @param      $endpoint   string Where are we going with this request?
	 * @param      $params     string|object|array Contains all parameters that we want to pass to the API
	 * @param      $method_name string Which method called us?
	 * @param bool $is_json Decides if this is a post with JSON
	 *
	 * @return mixed
	 */
	public function DELETE( $endpoint, $params, $method_name, $is_json = true ) {
		return $this->make_request( 'DELETE', $endpoint, $params, $method_name, $is_json );
	}

	/**
	 * @param      $endpoint   string Where are we going with this request?
	 * @param      $params     string|object|array Contains all parameters that we want to pass to the API
	 * @param      $method_name string Which method called us?
	 * @param bool $is_json Decides if this is a post with JSON
	 *
	 * @return mixed
	 */
	public function PATCH( $endpoint, $params, $method_name, $is_json = true ) {
		return $this->make_request( 'PATCH', $endpoint, $params, $method_name, $is_json );
	}

	/**
	 * @param string $type
	 * @param string $endpoint
	 * @param string|array|object $params
	 * @param string $method_name
	 * @param bool $is_json
	 *
	 * @return mixed
	 */
	private function make_request( $type, $endpoint, $params, $method_name, $is_json = true ) {
		$t = EDUAPI()->start_timer( $method_name . ' - ' . $type );
		$c = $this->get_curl_object( $endpoint );

		$headers = array();
		$data    = null;

		if ( $is_json ) {
			$headers[] = 'Content-Type: application/json';
			$data      = json_encode( $params );
			$headers[] = 'Content-Length: ' . strlen( $data );
		} else {
			$data = http_build_query( $params );
		}
		$this->set_headers( $c, $headers );

		curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $type );
		curl_setopt( $c, CURLOPT_POSTFIELDS, $data );

		$result = $this->execute_request( $c );
		EDUAPI()->stop_timer( $t );

		return $result;
	}

	/**
	 * @param string $endpoint
	 * @param object|array $params
	 * @param string $method_name
	 *
	 * @return mixed
	 */
	public function GET( $endpoint, $params, $method_name ) {
		$t = EDUAPI()->start_timer( $method_name . ' - GET' );
		$c = $this->get_curl_object( $endpoint . '?' . http_build_query( $params ) );
		$this->set_headers( $c, array() );
		$result = $this->execute_request( $c );
		EDUAPI()->stop_timer( $t );

		return $result;
	}

	/**
	 * @param string $endpoint
	 *
	 * @return resource
	 */
	private function get_curl_object( $endpoint ) {
		if ( ! strpos( $endpoint, '/' ) === 0 ) {
			$endpoint = '/' . $endpoint;
		}
		$c = curl_init( self::$root_url . $this->api_url . $endpoint );
		curl_setopt( $c, CURLOPT_RETURNTRANSFER, true );

		return $c;
	}

	/**
	 * @param resource $curl_object
	 * @param array $array
	 */
	private function set_headers( $curl_object, array $array = array() ) {
		if ( isset( EDUAPI()->api_token ) ) {
			$std_headers = array(
				'Authorization: bearer ' . EDUAPI()->api_token->AccessToken,
			);
		} else {
			$std_headers = array();
		}
		if ( ! empty( $array ) ) {
			$std_headers = array_merge( $std_headers, $array );
		}

		curl_setopt( $curl_object, CURLOPT_HTTPHEADER, $std_headers );
	}
}


<?php

/**
 * Class EduAdmin_REST_Person
 */
class EduAdmin_REST_Person extends EduAdminRESTClient {
	protected $api_url = '/v1/Person';

	/**
	 * @param EduAdmin_Data_Person|stdClass|object $person
	 * @param bool $skip_duplicate_match
	 *
	 * @return mixed
	 */
	public function Create( $person, $skip_duplicate_match = false ) {
		$query = array();
		if ( $skip_duplicate_match ) {
			$query['skipDuplicateMatch'] = 'true';
		}

		return parent::POST( '?' . http_build_query( $query ),
			$person,
			get_called_class() . '|' . __FUNCTION__
		);
	}

	/**
	 * @param integer $person_id
	 * @param EduAdmin_Data_Person|stdClass|object $person
	 *
	 * @return mixed
	 */
	public function Update( $person_id, $person ) {
		return parent::PATCH( "/$person_id",
			$person,
			get_called_class() . '|' . __FUNCTION__
		);
	}

	/**
	 * @param EduAdmin_Data_Login|stdClass|object $login
	 *
	 * @return mixed
	 */
	public function Login( $login ) {
		return parent::POST( '/Login',
			$login,
			get_called_class() . '|' . __FUNCTION__
		);
	}

	/**
	 * @param integer $person_id
	 * @param string $password
	 *
	 * @return mixed
	 */
	public function LoginById( $person_id, $password ) {
		return parent::POST( "/$person_id/Login",
			array( '' => $password ),
			get_called_class() . '|' . __FUNCTION__,
			false
		);
	}

	/**
	 * @param integer $person_id
	 *
	 * @return mixed
	 */
	public function SendResetPasswordEmailById( $person_id ) {
		return parent::POST( "/$person_id/SendResetPasswordEmail",
			array(),
			get_called_class() . '|' . __FUNCTION__
		);
	}

	/**
	 * @param string $email
	 *
	 * @return mixed
	 */
	public function SendResetPasswordEmail( $email ) {
		return parent::POST( '/SendResetPasswordEmail',
			array(
				'email' => $email,
			),
			get_called_class() . '|' . __FUNCTION__
		);
	}
}


<?php

/**
 * Class EduAdminODataClient
 */
class EduAdminODataClient extends EduAdminRESTClient {
	protected $user_agent = 'EduAdmin OData API Client';

	/**
	 * @param string|null $select
	 * @param string|null $filter
	 * @param string|null $expand
	 * @param string|null $orderby
	 * @param int|null $top
	 * @param int|null $skip
	 * @param bool $count
	 * @param bool $use_cache
	 *
	 * @return mixed
	 */
	public function Search( $select = null, $filter = null, $expand = null, $orderby = null, $top = null, $skip = null, $count = false, $use_cache = true ) {
		$params = array();
		if ( isset( $select ) && ! empty( $select ) ) {
			$params['$select'] = $select;
		}
		if ( isset( $filter ) && ! empty( $filter ) ) {
			$params['$filter'] = $filter;
		}
		if ( isset( $expand ) && ! empty( $expand ) ) {
			$params['$expand'] = $expand;
		}
		if ( isset( $orderby ) && ! empty( $orderby ) ) {
			$params['$orderby'] = $orderby;
		}
		if ( isset( $top ) && ! empty( $top ) ) {
			$params['$top'] = $top;
		}
		if ( isset( $skip ) && ! empty( $skip ) ) {
			$params['$skip'] = $skip;
		}

		if ( ! empty( $use_cache ) && ! $use_cache ) {
			$params['cache'] = 'false';
		}

		$params['$count'] = $count ? 'true' : 'false';

		return parent::GET( '', $params, get_called_class() . '|' . __FUNCTION__ );
	}

	/**
	 * @param int $id
	 * @param string|null $select
	 * @param string|null $expand
	 * @param bool $use_cache
	 *
	 * @return mixed
	 */
	public function GetItem( $id, $select = null, $expand = null, $use_cache = true ) {
		$params = array();
		if ( isset( $select ) && ! empty( $select ) ) {
			$params['$select'] = $select;
		}
		if ( isset( $expand ) && ! empty( $expand ) ) {
			$params['$expand'] = $expand;
		}

		if ( ! empty( $use_cache ) && ! $use_cache ) {
			$params['cache'] = 'false';
		}

		return parent::GET( "($id)", $params, get_called_class() . '|' . __FUNCTION__ );
	}

	/**
	 * @deprecated Not allowed in OData
	 *
	 * @param string $endpoint
	 * @param array|object $params
	 * @param string $method_name
	 * @param bool $is_json
	 *
	 * @return mixed|void
	 * @throws Exception
	 */
	final public function GET( $endpoint, $params, $method_name, $is_json = true ) {
		throw new Exception( 'This is OData, not REST, use Search or GetItem' );
	}

	/**
	 * @deprecated Not allowed in OData
	 *
	 * @param string $endpoint
	 * @param array|object $params
	 * @param string $method_name
	 * @param bool $is_json
	 *
	 * @return mixed|void
	 * @throws Exception
	 */
	final public function POST( $endpoint, $params, $method_name, $is_json = true ) {
		throw new Exception( 'This is OData, not REST, use Search or GetItem' );
	}

	/**
	 * @deprecated Not allowed in OData
	 *
	 * @param string $endpoint
	 * @param array|object $params
	 * @param string $method_name
	 * @param bool $is_json
	 *
	 * @return mixed|void
	 * @throws Exception
	 */
	final public function PATCH( $endpoint, $params, $method_name, $is_json = true ) {
		throw new Exception( 'This is OData, not REST, use Search or GetItem' );
	}

	/**
	 * @deprecated Not allowed in OData
	 *
	 * @param string $endpoint
	 * @param array|object $params
	 * @param string $method_name
	 * @param bool $is_json
	 *
	 * @return mixed|void
	 * @throws Exception
	 */
	final public function DELETE( $endpoint, $params, $method_name, $is_json = true ) {
		throw new Exception( 'This is OData, not REST, use Search or GetItem' );
	}

	/**
	 * @deprecated Not allowed in OData
	 *
	 * @param string $endpoint
	 * @param array|object $params
	 * @param string $method_name
	 * @param bool $is_json
	 *
	 * @return mixed|void
	 * @throws Exception
	 */
	final public function PUT( $endpoint, $params, $method_name, $is_json = true ) {
		throw new Exception( 'This is OData, not REST, use Search or GetItem' );
	}
}


<?php
/**
 * Multidimensional ArrayAccess
 *
 * Allows ArrayAccess-like functionality with multidimensional arrays.  Fully supports
 * both sets and unsets.
 *
 * @package    WordPress
 * @subpackage Session
 * @since      3.6.0
 */
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Recursive array class to allow multidimensional array access.
 *
 * @package WordPress
 * @since   3.6.0
 */
class Recursive_ArrayAccess implements ArrayAccess {
	/**
	 * Internal data collection.
	 *
	 * @var array
	 */
	protected $container = array();
	/**
	 * Flag whether or not the internal collection has been changed.
	 *
	 * @var bool
	 */
	protected $dirty = false;

	/**
	 * Default object constructor.
	 *
	 * @param array $data
	 */
	protected function __construct( $data = array() ) {
		foreach ( $data as $key => $value ) {
			$this[ $key ] = $value;
		}
	}

	/**
	 * Allow deep copies of objects
	 */
	public function __clone() {
		foreach ( $this->container as $key => $value ) {
			if ( $value instanceof self ) {
				$this[ $key ] = clone $value;
			}
		}
	}

	/**
	 * Output the data container as a multidimensional array.
	 *
	 * @return array
	 */
	public function toArray() {
		$data = $this->container;
		foreach ( $data as $key => $value ) {
			if ( $value instanceof self ) {
				$data[ $key ] = $value->toArray();
			}
		}

		return $data;
	}
	/**
	 * ArrayAccess Implementation
	 **/
	/**
	 * Whether a offset exists
	 *
	 * @link http://php.net/manual/en/arrayaccess.offsetexists.php
	 *
	 * @param mixed $offset An offset to check for.
	 *
	 * @return boolean true on success or false on failure.
	 */
	public function offsetExists( $offset ) {
		return isset( $this->container[ $offset ] );
	}

	/**
	 * Offset to retrieve
	 *
	 * @link http://php.net/manual/en/arrayaccess.offsetget.php
	 *
	 * @param mixed $offset The offset to retrieve.
	 *
	 * @return mixed Can return all value types.
	 */
	public function offsetGet( $offset ) {
		return isset( $this->container[ $offset ] ) ? $this->container[ $offset ] : null;
	}

	/**
	 * Offset to set
	 *
	 * @link http://php.net/manual/en/arrayaccess.offsetset.php
	 *
	 * @param mixed $offset The offset to assign the value to.
	 * @param mixed $value  The value to set.
	 *
	 * @return void
	 */
	public function offsetSet( $offset, $data ) {
		if ( is_array( $data ) ) {
			$data = new self( $data );
		}
		if ( $offset === null ) { // don't forget this!
			$this->container[] = $data;
		} else {
			$this->container[ $offset ] = $data;
		}
		$this->dirty = true;
	}

	/**
	 * Offset to unset
	 *
	 * @link http://php.net/manual/en/arrayaccess.offsetunset.php
	 *
	 * @param mixed $offset The offset to unset.
	 *
	 * @return void
	 */
	public function offsetUnset( $offset ) {
		unset( $this->container[ $offset ] );
	}
}


<?php

namespace Bob;

use Bob;
use itertools;

# Internal: Represents a single task.
class Task
{
    public
        # Internal: The task's actions as SplDoublyLinkedList, can be empty.
        $actions,

        # Public: Name of the task. Used to invoke the task and used in prerequisites.
        $name,

        # Public: The task's dependencies. When a task name is encountered then this
        # task gets run before this task.
        $prerequisites,

        # Public: The description.
        $description = '',

        # Public: An application instance which holds references
        # to all tasks.
        $application,
        $enable = true;

    protected $reenable = false;

    # Public: Initializes the task instance.
    #
    # name        - The task name, used to refer to the task in the CLI and
    #               when declaring dependencies.
    # application - The application object, to which this task belongs to.
    function __construct($name, $application)
    {
        $this->name        = $name;
        $this->application = $application;

        $this->description = TaskRegistry::$lastDescription;
        TaskRegistry::$lastDescription = '';

        $this->clear();
    }

    # Child classes of Task should put here their custom logic to determine
    # if the task should do something. See the FileTask class for an
    # example of this.
    #
    # Returns TRUE if the task should be run, FALSE otherwise.
    function isNeeded()
    {
        return true;
    }

    # Public: Collects all dependencies and invokes the task if it's
    # needed.
    #
    # Returns the callback's return value.
    function invoke()
    {
        if (!$this->enable) {
            if ($this->application->trace) {
                $this->application['log']->debug("{$this->inspect()} is not enabled");
            }
            return;
        }

        if (!$this->reenable and $this->application['invocation_chain']->has($this)) {
            return;
        }

        if (!$this->application->forceRun and !$this->isNeeded()) {
            $this->application->trace and $this->application['log']->debug("Skipping {$this->inspect()}");
            return;
        }

        $this->application['invocation_chain']->push($this);

        if ($this->application->trace) {
            $this->application['log']->debug("Invoke {$this->inspect()}");
        }

        $app = $this->application;

        itertools\walk(
            itertools\filter(itertools\to_iterator($this->prerequisites), function($p) use ($app) {
                return $app->taskDefined((string) $p);
            }),
            function($p) use ($app) {
                $app['tasks'][(string) $p]->invoke();
            }
        );

        if ($this->prerequisites !== null) {
            $this->prerequisites->rewind();
        }

        $this->execute();
        $this->reenable = false;
    }

    # Internal: Executes all actions.
    #
    # Returns nothing.
    function execute()
    {
        foreach ($this->actions as $action) {
            call_user_func($action, $this);
        }
    }

    # Clears all actions and prerequisites.
    #
    # Returns nothing.
    function clear()
    {
        $this->actions = new \SplDoublyLinkedList;
        $this->prerequisites = null;
    }

    function reenable()
    {
        $this->reenable = true;
    }

    function enhance($deps = null, $action = null)
    {
        if ($deps) {
            if ($this->prerequisites instanceof \Iterator) {
                $p = new \AppendIterator;
                $p->append($this->prerequisites);
                $p->append(itertools\to_iterator($deps));
                $this->prerequisites = $p;
            } else {
                $this->prerequisites = itertools\to_iterator($deps);
            }
        }

        if (is_callable($action)) {
            $this->actions->push($action);
        }
    }

    function getTaskPrerequisites()
    {
        $app = $this->application;

        return itertools\filter(
            itertools\to_iterator($this->prerequisites),
            function($task) use ($app) {
                return $app->taskDefined($task);
            }
        );
    }

    function inspect()
    {
        $prereqs = join(', ', iterator_to_array($this->getTaskPrerequisites()));
        $class = get_class($this);
        return "<$class {$this->name} => $prereqs>";
    }

    function __toString()
    {
        return $this->name;
    }
}