<?php
/**
 * PHP Exif Exiftool Mapper
 *
 * @link        http://github.com/miljar/PHPExif for the canonical source repository
 * @copyright   Copyright (c) 2015 Tom Van Herreweghe <tom@theanalogguy.be>
 * @license     http://github.com/miljar/PHPExif/blob/master/LICENSE MIT License
 * @category    PHPExif
 * @package     Mapper
 */

namespace PHPExif\Mapper;

use PHPExif\Exif;
use DateTime;

/**
 * PHP Exif Exiftool Mapper
 *
 * Maps Exiftool raw data to valid data for the \PHPExif\Exif class
 *
 * @category    PHPExif
 * @package     Mapper
 */
class Exiftool implements MapperInterface
{
    const APERTURE                 = 'Composite:Aperture';
    const APPROXIMATEFOCUSDISTANCE = 'XMP-aux:ApproximateFocusDistance';
    const ARTIST                   = 'IFD0:Artist';
    const CAPTION                  = 'XMP-acdsee';
    const CAPTIONABSTRACT          = 'IPTC:Caption-Abstract';
    const COLORSPACE               = 'ExifIFD:ColorSpace';
    const COPYRIGHT                = 'IFD0:Copyright';
    const DATETIMEORIGINAL         = 'ExifIFD:DateTimeOriginal';
    const CREDIT                   = 'IPTC:Credit';
    const EXPOSURETIME             = 'ExifIFD:ExposureTime';
    const FILESIZE                 = 'System:FileSize';
    const FOCALLENGTH              = 'ExifIFD:FocalLength';
    const HEADLINE                 = 'IPTC:Headline';
    const IMAGEHEIGHT              = 'File:ImageHeight';
    const IMAGEWIDTH               = 'File:ImageWidth';
    const ISO                      = 'ExifIFD:ISO';
    const JOBTITLE                 = 'IPTC:By-lineTitle';
    const KEYWORDS                 = 'IPTC:Keywords';
    const MIMETYPE                 = 'File:MIMEType';
    const MODEL                    = 'IFD0:Model';
    const ORIENTATION              = 'IFD0:Orientation';
    const SOFTWARE                 = 'IFD0:Software';
    const SOURCE                   = 'IPTC:Source';
    const TITLE                    = 'IPTC:ObjectName';
    const XRESOLUTION              = 'IFD0:XResolution';
    const YRESOLUTION              = 'IFD0:YResolution';
    const GPSLATITUDE              = 'GPS:GPSLatitude';
    const GPSLONGITUDE             = 'GPS:GPSLongitude';

    /**
     * Maps the ExifTool fields to the fields of
     * the \PHPExif\Exif class
     *
     * @var array
     */
    protected $map = array(
        self::APERTURE                 => Exif::APERTURE,
        self::ARTIST                   => Exif::AUTHOR,
        self::MODEL                    => Exif::CAMERA,
        self::CAPTION                  => Exif::CAPTION,
        self::COLORSPACE               => Exif::COLORSPACE,
        self::COPYRIGHT                => Exif::COPYRIGHT,
        self::DATETIMEORIGINAL         => Exif::CREATION_DATE,
        self::CREDIT                   => Exif::CREDIT,
        self::EXPOSURETIME             => Exif::EXPOSURE,
        self::FILESIZE                 => Exif::FILESIZE,
        self::FOCALLENGTH              => Exif::FOCAL_LENGTH,
        self::APPROXIMATEFOCUSDISTANCE => Exif::FOCAL_DISTANCE,
        self::HEADLINE                 => Exif::HEADLINE,
        self::IMAGEHEIGHT              => Exif::HEIGHT,
        self::XRESOLUTION              => Exif::HORIZONTAL_RESOLUTION,
        self::ISO                      => Exif::ISO,
        self::JOBTITLE                 => Exif::JOB_TITLE,
        self::KEYWORDS                 => Exif::KEYWORDS,
        self::MIMETYPE                 => Exif::MIMETYPE,
        self::ORIENTATION              => Exif::ORIENTATION,
        self::SOFTWARE                 => Exif::SOFTWARE,
        self::SOURCE                   => Exif::SOURCE,
        self::TITLE                    => Exif::TITLE,
        self::YRESOLUTION              => Exif::VERTICAL_RESOLUTION,
        self::IMAGEWIDTH               => Exif::WIDTH,
        self::CAPTIONABSTRACT          => Exif::CAPTION,
        self::GPSLATITUDE              => Exif::GPS,
        self::GPSLONGITUDE             => Exif::GPS,
    );

    /**
     * @var bool
     */
    protected $numeric = true;

    /**
     * Mutator method for the numeric property
     *
     * @param bool $numeric
     * @return \PHPExif\Mapper\Exiftool
     */
    public function setNumeric($numeric)
    {
        $this->numeric = (bool) $numeric;

        return $this;
    }

    /**
     * Maps the array of raw source data to the correct
     * fields for the \PHPExif\Exif class
     *
     * @param array $data
     * @return array
     */
    public function mapRawData(array $data)
    {
        $mappedData = array();
        $gpsData = array();
        foreach ($data as $field => $value) {
            if (!array_key_exists($field, $this->map)) {
                // silently ignore unknown fields
                continue;
            }

            $key = $this->map[$field];

            // manipulate the value if necessary
            switch ($field) {
                case self::APERTURE:
                    $value = sprintf('f/%01.1f', $value);
                    break;
                case self::APPROXIMATEFOCUSDISTANCE:
                    $value = sprintf('%1$sm', $value);
                    break;
                case self::DATETIMEORIGINAL:
                    try {
                        $value = new DateTime($value);
                    } catch (\Exception $exception) {
                        continue 2;
                    }
                    break;
                case self::EXPOSURETIME:
                    // Based on the source code of Exiftool (PrintExposureTime subroutine):
                    // http://cpansearch.perl.org/src/EXIFTOOL/Image-ExifTool-9.90/lib/Image/ExifTool/Exif.pm
                    if ($value < 0.25001 && $value > 0) {
                        $value = sprintf('1/%d', intval(0.5 + 1 / $value));
                    } else {
                        $value = sprintf('%.1f', $value);
                        $value = preg_replace('/.0$/', '', $value);
                    }
                    break;
                case self::FOCALLENGTH:
                    if (!$this->numeric || strpos($value, ' ') !== false) {
                        $focalLengthParts = explode(' ', $value);
                        $value = reset($focalLengthParts);
                    }
                    break;
                case self::GPSLATITUDE:
                    $gpsData['lat']  = $this->extractGPSCoordinates($value);
                    break;
                case self::GPSLONGITUDE:
                    $gpsData['lon']  = $this->extractGPSCoordinates($value);
                    break;
            }

            // set end result
            $mappedData[$key] = $value;
        }

        // add GPS coordinates, if available
        if (count($gpsData) === 2 && $gpsData['lat'] !== false && $gpsData['lon'] !== false) {
            $latitudeRef = empty($data['GPS:GPSLatitudeRef'][0]) ? 'N' : $data['GPS:GPSLatitudeRef'][0];
            $longitudeRef = empty($data['GPS:GPSLongitudeRef'][0]) ? 'E' : $data['GPS:GPSLongitudeRef'][0];

            $gpsLocation = sprintf(
                '%s,%s',
                (strtoupper($latitudeRef) === 'S' ? -1 : 1) * $gpsData['lat'],
                (strtoupper($longitudeRef) === 'W' ? -1 : 1) * $gpsData['lon']
            );

            $mappedData[Exif::GPS] = $gpsLocation;
        } else {
            unset($mappedData[Exif::GPS]);
        }

        return $mappedData;
    }

    /**
     * Extract GPS coordinates from formatted string
     *
     * @param string $coordinates
     * @return array
     */
    protected function extractGPSCoordinates($coordinates)
    {
        if ($this->numeric === true) {
            return abs((float) $coordinates);
        } else {
            if (!preg_match('!^([0-9.]+) deg ([0-9.]+)\' ([0-9.]+)"!', $coordinates, $matches)) {
                return false;
            }

            return floatval($matches[1]) + (floatval($matches[2]) / 60) + (floatval($matches[3]) / 3600);
        }
    }
}


<?php
/**
 * PHP Exif Reader Adapter Abstract: Common functionality for adapters
 *
 * @link        http://github.com/miljar/PHPExif for the canonical source repository
 * @copyright   Copyright (c) 2013 Tom Van Herreweghe <tom@theanalogguy.be>
 * @license     http://github.com/miljar/PHPExif/blob/master/LICENSE MIT License
 * @category    PHPExif
 * @package     Reader
 */

namespace PHPExif\Adapter;

use PHPExif\Mapper\MapperInterface;
use PHPExif\Hydrator\HydratorInterface;

/**
 * PHP Exif Reader Adapter Abstract
 *
 * Implements common functionality for the reader adapters
 *
 * @category    PHPExif
 * @package     Reader
 */
abstract class AdapterAbstract implements AdapterInterface
{
    /**
     * @var string
     */
    protected $hydratorClass = '\\PHPExif\\Hydrator\\Mutator';

    /**
     * @var \PHPExif\Mapper\MapperInterface
     */
    protected $mapper;

    /**
     * @var \PHPExif\Hydrator\HydratorInterface
     */
    protected $hydrator;

    /**
     * @var string
     */
    protected $mapperClass = '';

    /**
     * Class constructor
     *
     * @param array $options Optional array of data to initialize the object with
     */
    public function __construct(array $options = array())
    {
        if (!empty($options)) {
            $this->setOptions($options);
        }
    }

    /**
     * Mutator for the data mapper
     *
     * @param \PHPExif\Mapper\MapperInterface $mapper
     * @return \PHPExif\Adapter\AdapterInterface
     */
    public function setMapper(MapperInterface $mapper)
    {
        $this->mapper = $mapper;

        return $this;
    }

    /**
     * Accessor for the data mapper
     *
     * @return \PHPExif\Mapper\MapperInterface
     */
    public function getMapper()
    {
        if (null === $this->mapper) {
            // lazy load one
            $mapper = new $this->mapperClass;

            $this->setMapper($mapper);
        }

        return $this->mapper;
    }

    /**
     * Mutator for the hydrator
     *
     * @param \PHPExif\Hydrator\HydratorInterface $hydrator
     * @return \PHPExif\Adapter\AdapterInterface
     */
    public function setHydrator(HydratorInterface $hydrator)
    {
        $this->hydrator = $hydrator;

        return $this;
    }

    /**
     * Accessor for the data hydrator
     *
     * @return \PHPExif\Hydrator\HydratorInterface
     */
    public function getHydrator()
    {
        if (null === $this->hydrator) {
            // lazy load one
            $hydrator = new $this->hydratorClass;

            $this->setHydrator($hydrator);
        }

        return $this->hydrator;
    }

    /**
     * Set array of options in the current object
     *
     * @param array $options
     * @return \PHPExif\Reader\AdapterAbstract
     */
    public function setOptions(array $options)
    {
        $hydrator = $this->getHydrator();
        $hydrator->hydrate($this, $options);

        return $this;
    }
}


<?php
/**
 * PHP Exif Native Reader Adapter
 *
 * @link        http://github.com/miljar/PHPExif for the canonical source repository
 * @copyright   Copyright (c) 2013 Tom Van Herreweghe <tom@theanalogguy.be>
 * @license     http://github.com/miljar/PHPExif/blob/master/LICENSE MIT License
 * @category    PHPExif
 * @package     Reader
 */

namespace PHPExif\Adapter;

use PHPExif\Exif;

/**
 * PHP Exif Native Reader Adapter
 *
 * Uses native PHP functionality to read data from a file
 *
 * @category    PHPExif
 * @package     Reader
 */
class Native extends AdapterAbstract
{
    const INCLUDE_THUMBNAIL = true;
    const NO_THUMBNAIL      = false;

    const SECTIONS_AS_ARRAYS    = true;
    const SECTIONS_FLAT         = false;

    const SECTION_FILE      = 'FILE';
    const SECTION_COMPUTED  = 'COMPUTED';
    const SECTION_IFD0      = 'IFD0';
    const SECTION_THUMBNAIL = 'THUMBNAIL';
    const SECTION_COMMENT   = 'COMMENT';
    const SECTION_EXIF      = 'EXIF';
    const SECTION_ALL       = 'ANY_TAG';
    const SECTION_IPTC      = 'IPTC';

    /**
     * List of EXIF sections
     *
     * @var array
     */
    protected $requiredSections = array();

    /**
     * Include the thumbnail in the EXIF data?
     *
     * @var boolean
     */
    protected $includeThumbnail = self::NO_THUMBNAIL;

    /**
     * Parse the sections as arrays?
     *
     * @var boolean
     */
    protected $sectionsAsArrays = self::SECTIONS_FLAT;

    /**
     * @var string
     */
    protected $mapperClass = '\\PHPExif\\Mapper\\Native';

    /**
     * Contains the mapping of names to IPTC field numbers
     *
     * @var array
     */
    protected $iptcMapping = array(
        'title'     => '2#005',
        'keywords'  => '2#025',
        'copyright' => '2#116',
        'caption'   => '2#120',
        'headline'  => '2#105',
        'credit'    => '2#110',
        'source'    => '2#115',
        'jobtitle'  => '2#085'
    );

    /**
     * Getter for the EXIF sections
     *
     * @return array
     */
    public function getRequiredSections()
    {
        return $this->requiredSections;
    }

    /**
     * Setter for the EXIF sections
     *
     * @param array $sections List of EXIF sections
     * @return \PHPExif\Adapter\Native Current instance for chaining
     */
    public function setRequiredSections(array $sections)
    {
        $this->requiredSections = $sections;

        return $this;
    }

    /**
     * Adds an EXIF section to the list
     *
     * @param string $section
     * @return \PHPExif\Adapter\Native Current instance for chaining
     */
    public function addRequiredSection($section)
    {
        if (!in_array($section, $this->requiredSections)) {
            array_push($this->requiredSections, $section);
        }

        return $this;
    }

    /**
     * Define if the thumbnail should be included into the EXIF data or not
     *
     * @param boolean $value
     * @return \PHPExif\Adapter\Native Current instance for chaining
     */
    public function setIncludeThumbnail($value)
    {
        $this->includeThumbnail = $value;

        return $this;
    }

    /**
     * Returns if the thumbnail should be included into the EXIF data or not
     *
     * @return boolean
     */
    public function getIncludeThumbnail()
    {
        return $this->includeThumbnail;
    }

    /**
     * Define if the sections should be parsed as arrays
     *
     * @param boolean $value
     * @return \PHPExif\Adapter\Native Current instance for chaining
     */
    public function setSectionsAsArrays($value)
    {
        $this->sectionsAsArrays = (bool) $value;

        return $this;
    }

    /**
     * Returns if the sections should be parsed as arrays
     *
     * @return boolean
     */
    public function getSectionsAsArrays()
    {
        return $this->sectionsAsArrays;
    }

    /**
     * Reads & parses the EXIF data from given file
     *
     * @param string $file
     * @return \PHPExif\Exif|boolean Instance of Exif object with data
     */
    public function getExifFromFile($file)
    {
        $sections   = $this->getRequiredSections();
        $sections   = implode(',', $sections);
        $sections   = (empty($sections)) ? null : $sections;

        $data = @exif_read_data(
            $file,
            $sections,
            $this->getSectionsAsArrays(),
            $this->getIncludeThumbnail()
        );

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

        $xmpData = $this->getIptcData($file);
        $data = array_merge($data, array(self::SECTION_IPTC => $xmpData));

        // map the data:
        $mapper = $this->getMapper();
        $mappedData = $mapper->mapRawData($data);

        // hydrate a new Exif object
        $exif = new Exif();
        $hydrator = $this->getHydrator();
        $hydrator->hydrate($exif, $mappedData);
        $exif->setRawData($data);

        return $exif;
    }

    /**
     * Returns an array of IPTC data
     *
     * @param string $file The file to read the IPTC data from
     * @return array
     */
    public function getIptcData($file)
    {
        getimagesize($file, $info);
        $arrData = array();
        if (isset($info['APP13'])) {
            $iptc = iptcparse($info['APP13']);

            foreach ($this->iptcMapping as $name => $field) {
                if (!isset($iptc[$field])) {
                    continue;
                }

                if (count($iptc[$field]) === 1) {
                    $arrData[$name] = reset($iptc[$field]);
                } else {
                    $arrData[$name] = $iptc[$field];
                }
            }
        }

        return $arrData;
    }
}


<?php
/**
 * PHP Exif Reader: Reads EXIF metadata from a file
 *
 * @link        http://github.com/miljar/PHPExif for the canonical source repository
 * @copyright   Copyright (c) 2013 Tom Van Herreweghe <tom@theanalogguy.be>
 * @license     http://github.com/miljar/PHPExif/blob/master/LICENSE MIT License
 * @category    PHPExif
 * @package     Reader
 */

namespace PHPExif\Reader;

use PHPExif\Adapter\AdapterInterface;
use PHPExif\Adapter\NoAdapterException;
use PHPExif\Adapter\Exiftool as ExiftoolAdapter;
use PHPExif\Adapter\Native as NativeAdapter;

/**
 * PHP Exif Reader
 *
 * Responsible for all the read operations on a file's EXIF metadata
 *
 * @category    PHPExif
 * @package     Reader
 * @
 */
class Reader implements ReaderInterface
{
    const TYPE_NATIVE   = 'native';
    const TYPE_EXIFTOOL = 'exiftool';

    /**
     * The current adapter
     *
     * @var \PHPExif\Adapter\AdapterInterface
     */
    protected $adapter;

    /**
     * Reader constructor
     *
     * @param \PHPExif\Adapter\AdapterInterface $adapter
     */
    public function __construct(AdapterInterface $adapter)
    {
        $this->adapter = $adapter;
    }

    /**
     * Getter for the reader adapter
     *
     * @return \PHPExif\Adapter\AdapterInterface
     * @throws NoAdapterException When no adapter is set
     */
    public function getAdapter()
    {
        if (empty($this->adapter)) {
            throw new NoAdapterException('No adapter set in the reader');
        }

        return $this->adapter;
    }

    /**
     * Factory for the reader
     *
     * @param string $type
     * @return $this
     * @throws \InvalidArgumentException When given type is invalid
     */
    public static function factory($type)
    {
        $classname = get_called_class();
        switch ($type) {
            case self::TYPE_NATIVE:
                $adapter = new NativeAdapter();
                break;
            case self::TYPE_EXIFTOOL:
                $adapter = new ExiftoolAdapter();
                break;
            default:
                throw new \InvalidArgumentException(
                    sprintf('Unknown type "%1$s"', $type)
                );
        }
        return new $classname($adapter);
    }

    /**
     * Reads & parses the EXIF data from given file
     *
     * @param string $file
     * @return \PHPExif\Exif Instance of Exif object with data
     */
    public function read($file)
    {
        return $this->getAdapter()->getExifFromFile($file);
    }

    /**
     * alias to read method
     *
     * @param string $file
     * @return \PHPExif\Exif Instance of Exif object with data
     */
    public function getExifFromFile($file)
    {
        return $this->read($file);
    }
}


<?php

/**
 * Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

namespace CakeDC\Enum\Model\Behavior\Strategy;

use CakeDC\Enum\Model\Behavior\Exception\MissingEnumStrategyPrefixException;
use Cake\Core\InstanceConfigTrait;
use Cake\ORM\Table;
use Cake\Utility\Inflector;

abstract class AbstractStrategy implements StrategyInterface
{

    use InstanceConfigTrait;

    /**
     * Default configuration.
     *
     * @var array
     */
    protected $_defaultConfig = [];

    /**
     * Target table.
     *
     * @var \Cake\ORM\Table
     */
    protected $_table;

    /**
     * Constructor.
     *
     * @param string $alias Alias assigned to the strategy in the behavior.
     * @param \Cake\ORM\Table $table Target table.
     */
    public function __construct($alias, Table $table)
    {
        $this->_alias = $alias;
        $this->_table = $table;
    }

    /**
     * {@inheritdoc}
     *
     * @param mixed $config Strategy's configuration.
     * @return $this
     */
    public function initialize($config)
    {
        if (is_string($config)) {
            $config = ['prefix' => $config];
        }

        $prefix = $this->getConfig('prefix');
        if (empty($config['prefix']) && empty($prefix)) {
            $config['prefix'] = $this->_generatePrefix();
        }

        if (empty($config['field'])) {
            $config['field'] = Inflector::underscore($this->_alias);
        }

        if (empty($config['errorMessage'])) {
            $config['errorMessage'] = __d('cake', 'The provided value is invalid');
        }

        $this->setConfig($config);

        return $this;
    }

    /**
     * Generates default prefix for strategy.
     *
     * @return string
     */
    protected function _generatePrefix()
    {
        $prefix = Inflector::underscore(Inflector::singularize($this->_table->getAlias()));
        $prefix .= '_' . $this->_alias;

        return strtoupper($prefix);
    }
}


<?php

/**
 * Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

namespace CakeDC\Enum\Model\Behavior\Strategy;

use Cake\ORM\Table;
use ReflectionClass;

class ConstStrategy extends AbstractStrategy
{

    /**
     * Constants list
     *
     * @var array
     */
    protected $_constants;

    /**
     * {@inheritdoc}
     *
     * @param string $alias Strategy's alias.
     * @param \Cake\ORM\Table $table Table object.
     */
    public function __construct($alias, Table $table)
    {
        parent::__construct($alias, $table);
        $this->_defaultConfig['prefix'] = strtoupper($alias);
        $this->_defaultConfig['lowercase'] = false;
    }

    /**
     * {@inheritdoc}
     *
     * @param array $config List of callable filters to limit items generated from list.
     * @return array
     */
    public function enum(array $config = [])
    {
        $constants = $this->_getConstants();
        $keys = array_keys($constants);

        foreach ($config as $callable) {
            if (is_callable($callable)) {
                $keys = array_filter($keys, $callable);
            }
        }

        $values = array_map(function ($v) use ($constants) {
            return $constants[$v];
        }, $keys);

        return array_combine($keys, $values);
    }

    /**
     * Returns defined constants for the current `$_table`.
     *
     * @return array
     */
    protected function _getConstants()
    {
        if (isset($this->_constants)) {
            return $this->_constants;
        }

        $prefix = $this->getConfig('prefix');
        $lowercase = $this->getConfig('lowercase');
        $className = $this->getConfig('className') ?: get_class($this->_table);
        $length = strlen($prefix) + 1;
        $classConstants = (new ReflectionClass($className))->getConstants();
        $constants = [];

        foreach ($classConstants as $key => $value) {
            if (strpos($key, $prefix) === 0) {
                $listKey = substr($key, $length);
                if ($lowercase) {
                    $listKey = strtolower($listKey);
                }
                $constants[$listKey] = $value;
            }
        }

        return $this->_constants = $constants;
    }
}


<?php

/**
 * Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

namespace CakeDC\Enum\Model\Behavior\Strategy;

use Cake\Datasource\ModelAwareTrait;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;

class LookupStrategy extends AbstractStrategy
{

    use ModelAwareTrait;

    /**
     * {@inheritdoc}
     *
     * @param string $alias Strategy's alias.
     * @param \Cake\ORM\Table $table Table object.
     */
    public function __construct($alias, Table $table)
    {
        parent::__construct($alias, $table);
        $this->modelClass = 'CakeDC/Enum.Lookups';
        $this->modelFactory('Table', ['Cake\ORM\TableRegistry', 'get']);
    }

    /**
     * {@inheritdoc}
     *
     * @param array $config (unused in this case).
     * @return array
     */
    public function enum(array $config = [])
    {
        $query = $this->loadModel()
            ->find('list', [
                'keyField' => 'name',
                'valueField' => 'label',
            ])
            ->where([
                'prefix' => $this->getConfig('prefix'),
            ]);

        foreach ($config as $method => $args) {
            if (method_exists($query, $method)) {
                $query = call_user_func_array([$query, $method], $args);
            }
        }

        return $query->toArray();
    }

    /**
     * {@inheritdoc}
     *
     * @param array $config Strategy's configuration.
     * @return $this
     */
    public function initialize($config)
    {
        $config = parent::initialize($config)->getConfig();
        $assocName = Inflector::pluralize(Inflector::classify($this->_alias));

        $this->_table->belongsTo($assocName, [
            'className' => $this->modelClass,
            'foreignKey' => $config['field'],
            'bindingKey' => 'name',
            'conditions' => [$assocName . '.prefix' => $config['prefix']],
        ]);

        return $this;
    }
}


<?php

/**
 * Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2015 - 2018, Cake Development Corporation (http://cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

namespace CakeDC\Enum\Model\Behavior;

use BadMethodCallException;
use CakeDC\Enum\Model\Behavior\Exception\MissingEnumConfigurationException;
use CakeDC\Enum\Model\Behavior\Exception\MissingEnumStrategyException;
use CakeDC\Enum\Model\Behavior\Strategy\StrategyInterface;
use Cake\Event\Event;
use Cake\ORM\Behavior;
use Cake\ORM\RulesChecker;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;

class EnumBehavior extends Behavior
{
    /**
     * Default configuration.
     *
     * - `defaultStrategy`: the default strategy to use.
     * - `translate`: Whether values of lists returned by enum() method should
     *   be translated. Defaults to `false`.
     * - `translationDomain`: Domain to use when translating list value.
     *   Defaults to "default".
     * - `nested`: (bool) If `true` the array returned by enum() method will be of form
     *   `[['value' => 'v1', 'text' => 't1'], ['value' => 'v2', 'text' => 't2']`
     *   instead of default `['v1' => 't1', 'v2' => 't2']`.
     * - `implementedMethods`: custom table methods made accessible by this behavior.
     * - `lists`: the defined enumeration lists. Lists can use different strategies,
     *   use prefixes to differentiate them (defaults to the uppercased list name) and
     *   are persisted into a table's field (default to the underscored list name).
     *
     *   Example:
     *
     *   ```php
     *   $lists = [
     *       'priority' => [
     *           'strategy' => 'lookup',
     *           'prefix' => 'PRIORITY',
     *           'field' => 'priority',
     *           'errorMessage' => 'Invalid priority',
     *           // Create application rule to ensure only valid enum value can be saved.
     *           'applicationRules' => true,
     *           // Allow saving field without any enum value.
     *           'allowEmpty' => false
     *       ],
     *   ];
     *   ```
     *
     * @var array
     */
    protected $_defaultConfig = [
        'defaultStrategy' => 'lookup',
        'translate' => false,
        'translationDomain' => 'default',
        'implementedMethods' => [
            'enum' => 'enum',
        ],
        'classMap' => [],
        'lists' => [],
    ];

    /**
     * Class map.
     *
     * @var array
     */
    protected $_classMap = [
        'lookup' => 'CakeDC\Enum\Model\Behavior\Strategy\LookupStrategy',
        'const' => 'CakeDC\Enum\Model\Behavior\Strategy\ConstStrategy',
        'config' => 'CakeDC\Enum\Model\Behavior\Strategy\ConfigStrategy',
    ];

    /**
     * Stack of strategies in use.
     *
     * @var array
     */
    protected $_strategies = [];

    /**
     * Initializes the behavior.
     *
     * @param array $config Strategy's configuration.
     * @return void
     */
    public function initialize(array $config)
    {
        parent::initialize($config);
        $this->_normalizeConfig();
    }

    /**
     * Getter/setter for strategies.
     *
     * @param string $alias Strategy's alias.
     * @param mixed $strategy Strategy name from the class map or some strategy instance.
     * @return \CakeDC\Enum\Model\Behavior\Strategy\StrategyInterface
     * @throws \CakeDC\Enum\Model\Behavior\Exception\MissingEnumStrategyException
     */
    public function strategy($alias, $strategy)
    {
        if (!empty($this->_strategies[$alias])) {
            return $this->_strategies[$alias];
        }

        $this->_strategies[$alias] = $strategy;

        if ($strategy instanceof StrategyInterface) {
            return $strategy;
        }

        if (isset($this->_classMap[$strategy])) {
            $class = $this->_classMap[$strategy];
        }

        if (!class_exists($class)) {
            throw new MissingEnumStrategyException([$class]);
        }

        return $this->_strategies[$alias] = new $class($alias, $this->_table);
    }

    /**
     * Normalizes the strategies configuration and initializes the strategies.
     *
     * @return void
     */
    protected function _normalizeConfig()
    {
        $classMap = $this->getConfig('classMap');
        $this->_classMap = array_merge($this->_classMap, $classMap);

        $lists = $this->getConfig('lists');
        $defaultStrategy = $this->getConfig('defaultStrategy');

        foreach ($lists as $alias => $config) {
            if (is_numeric($alias)) {
                unset($lists[$alias]);
                $alias = $config;
                $config = [];
                $lists[$alias] = $config;
            }

            if (is_string($config)) {
                $config = ['prefix' => strtoupper($config)];
            }

            if (empty($config['strategy'])) {
                $config['strategy'] = $defaultStrategy;
            }

            $lists[$alias] = $this->strategy($alias, $config['strategy'])
                ->initialize($config)
                ->getConfig();
        }

        $this->setConfig('lists', $lists, false);
    }

    /**
     * @param string|array|null $alias Defined list's alias/name.
     * @return array
     * @throws \CakeDC\Enum\Model\Behavior\Exception\MissingEnumConfigurationException
     */
    public function enum($alias = null)
    {
        if (is_string($alias)) {
            $config = $this->getConfig('lists.' . $alias);
            if (empty($config)) {
                throw new MissingEnumConfigurationException([$alias]);
            }

            return $this->_enumList($alias, $config);
        }

        $lists = $this->getConfig('lists');
        if (!empty($alias)) {
            $lists = array_intersect_key($lists, array_flip($alias));
        }

        $return = [];
        foreach ($lists as $alias => $config) {
            $return[$alias] = $this->_enumList($alias, $config);
        }

        return $return;
    }

    /**
     * @param string $alias List alias.
     * @param array $config Config
     * @return array
     */
    protected function _enumList($alias, array $config)
    {
        $return = $this->strategy($alias, $config['strategy'])->enum($config);
        if ($this->getConfig('translate')) {
            $return = $this->_translate($return);
        }

        if ($this->getConfig('nested')) {
            array_walk(
                $return,
                function (&$item, $val) {
                    $item = ['value' => $val, 'text' => $item];
                }
            );

            $return = array_values($return);
        }

        return $return;
    }

    /**
     * Translate list values.
     *
     * @param array $list List.
     * @return array
     */
    protected function _translate(array $list)
    {
        $domain = $this->getConfig('translationDomain');

        return array_map(function ($value) use ($domain) {
            return __d($domain, $value);
        }, $list);
    }

    /**
     * @param \Cake\Event\Event $event Event.
     * @param \Cake\ORM\RulesChecker $rules Rules checker.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(Event $event, RulesChecker $rules)
    {
        foreach ($this->getConfig('lists') as $alias => $config) {
            if (Hash::get($config, 'applicationRules') === false) {
                continue;
            }

            $ruleName = 'isValid' . Inflector::camelize($alias);
            $rules->add([$this, $ruleName], $ruleName, [
                'errorField' => $config['field'],
                'message' => $config['errorMessage']
            ]);
        }

        return $rules;
    }

    /**
     * Universal validation rule for lists.
     *
     * @param string $method Method name.
     * @param array $args Method's arguments.
     * @return bool
     * @throws \BadMethodCallException
     * @throws \CakeDC\Enum\Model\Behavior\Exception\MissingEnumConfigurationException
     */
    public function __call($method, $args)
    {
        if (strpos($method, 'isValid') !== 0) {
            throw new BadMethodCallException(sprintf('Call to undefined method (%s)', $method));
        }

        $alias = Inflector::underscore(str_replace('isValid', '', $method));
        list($entity, ) = $args;

        if (!$config = $this->getConfig('lists.' . $alias)) {
            throw new MissingEnumConfigurationException([$alias]);
        }

        if (!$entity->has($config['field']) && Hash::get($config, 'allowEmpty') === true) {
            return true;
        }

        return array_key_exists($entity->{$config['field']}, $this->enum($alias));
    }
}


<?php

namespace SilverStripe\StaticPublishQueue;

use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPApplication;
use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\StaticPublishQueue\Contract\StaticPublisher;
use SilverStripe\View\ArrayData;
use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend;
use SilverStripe\View\SSViewer;

abstract class Publisher implements StaticPublisher
{
    use Injectable;
    use Configurable;

    /**
     * @var array
     * @config
     */
    private static $static_publisher_themes = [];

    /**
     * @config
     *
     * @var Boolean Use domain based caching (put cache files into a domain subfolder)
     * This must be true if you are using this with the "subsites" module.
     * Please note that this form of caching requires all URLs to be provided absolute
     * (not relative to the webroot) via {@link SiteTree->AbsoluteLink()}.
     */
    private static $domain_based_caching = false;

    /**
     * @config
     *
     * @var Boolean Add a timestamp to the statically published output for HTML files
     */
    private static $add_timestamp = false;

    /**
     * @param string $url
     *
     * @return HTTPResponse
     */
    public function generatePageResponse($url)
    {
        if (Director::is_relative_url($url)) {
            $url = Director::absoluteURL($url);
        }
        $urlParts = parse_url($url);
        if (!empty($urlParts['query'])) {
            parse_str($urlParts['query'], $getVars);
        } else {
            $getVars = [];
        }
        // back up requirements backend
        $origRequirements = Requirements::backend();
        Requirements::set_backend(Requirements_Backend::create());

        $origThemes = SSViewer::get_themes();
        $staticThemes = self::config()->get('static_publisher_themes');
        if ($staticThemes) {
            SSViewer::set_themes($staticThemes);
        } else {
            // get the themes raw from config to prevent the "running from the CMS" problem where no themes are live
            $rawThemes = SSViewer::config()->uninherited('themes');
            SSViewer::set_themes($rawThemes);
        }
        try {
            // try to add all the server vars that would be needed to create a static cache
            $request = HTTPRequestBuilder::createFromVariables(
                [
                    '_SERVER' => [
                        'REQUEST_URI' => isset($urlParts['path']) ? $urlParts['path'] : '',
                        'REQUEST_METHOD' => 'GET',
                        'REMOTE_ADDR' => '127.0.0.1',
                        'HTTPS' => $urlParts['scheme'] == 'https' ? 'on' : 'off',
                        'QUERY_STRING' => isset($urlParts['query']) ? $urlParts['query'] : '',
                        'REQUEST_TIME' => DBDatetime::now()->getTimestamp(),
                        'REQUEST_TIME_FLOAT' => (float)DBDatetime::now()->getTimestamp(),
                        'HTTP_HOST' => $urlParts['host'],
                        'HTTP_USER_AGENT' => 'silverstripe/staticpublishqueue',
                    ],
                    '_GET' => $getVars,
                    '_POST' => [],
                ],
                ''
            );
            $app = $this->getHTTPApplication();
            $response = $app->handle($request);

            if ($this->config()->get('add_timestamp') === true) {
                $response->setBody(
                    str_replace(
                        '',
                        "\n",
                        $response->getBody()
                    )
                );
            }
        } catch (HTTPResponse_Exception $e) {
            $response = $e->getResponse();
        } finally {
            // restore backends
            SSViewer::set_themes($origThemes);
            Requirements::set_backend($origRequirements);
            DataObject::singleton()->flushCache();
        }
        return $response;
    }

    /**
     * @return HTTPApplication
     */
    protected function getHTTPApplication()
    {
        $kernel = new CoreKernel(BASE_PATH);
        return new HTTPApplication($kernel);
    }

    /**
     * Generate the templated content for a PHP script that can serve up the
     * given piece of content with the given age and expiry.
     *
     * @param HTTPResponse $response
     *
     * @return string
     */
    protected function generatePHPCacheFile($response)
    {
        $cacheConfig = [
            'responseCode' => $response->getStatusCode(),
            'headers' => [],
        ];

        foreach ($response->getHeaders() as $header => $value) {
            if (!in_array($header, ['cache-control'])) {
                $cacheConfig['headers'][] = sprintf('%s: %s', $header, $value);
            }
        }

        return "<?php\n\nreturn " . var_export($cacheConfig, true) . ';';
    }

    /**
     * @param string $destination
     *
     * @return string
     */
    protected function generateHTMLCacheRedirection($destination)
    {
        return SSViewer::execute_template(
            'SilverStripe\\StaticPublishQueue\\HTMLRedirection',
            ArrayData::create([
                'URL' => DBField::create_field('Varchar', $destination),
            ])
        );
    }
}


<?php

namespace SilverStripe\StaticPublishQueue\Task;

use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Dev\BuildTask;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\StaticPublishQueue\Job\StaticCacheFullBuildJob;
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor;
use Symbiote\QueuedJobs\Services\QueuedJob;
use Symbiote\QueuedJobs\Services\QueuedJobService;

class StaticCacheFullBuildTask extends BuildTask
{

    /**
     * Queue up a StaticCacheFullBuildJob
     * Check for startAfter param and do some sanity checking
     *
     * @param HTTPRequest $request
     * @return
     */
    public function run($request)
    {
        $job = new StaticCacheFullBuildJob();
        $signature = $job->getSignature();

        // see if we already have this job in a queue
        $filter = [
            'Signature' => $signature,
            'JobStatus' => [
                QueuedJob::STATUS_NEW,
                QueuedJob::STATUS_INIT,
            ]
        ];

        $existing = DataList::create(QueuedJobDescriptor::class)->filter($filter)->first();

        if ($existing && $existing->ID) {
            $this->log(sprintf(
                'There is already a %s in the queue, added %s %s',
                StaticCacheFullBuildJob::class,
                $existing->Created,
                $existing->StartAfter ? 'and set to start after ' . $existing->StartAfter : ''
            ));
            return false;
        }

        if ($request->getVar('startAfter')) {
            $now = DBDatetime::now();
            $today = $now->Date();
            $startTime = $request->getVar('startAfter');

            // move to tomorrow if the starttime has passed today
            if ($now->Time24() > $startTime) {
                $timestamp = strtotime($today . ' ' . $startTime . ' +1 day');
                $dayWord = 'tomorrow';
            } else {
                $timestamp = strtotime($today . ' ' . $startTime);
                $dayWord = 'today';
            }
            $startAfter = (new \DateTime())->setTimestamp($timestamp);
            $thisTimeTomorrow = (new \DateTime())->setTimestamp(strtotime($now . ' +1 day'))->getTimestamp();

            // sanity check that we are in the next 24 hours - prevents some weird stuff sneaking through
            if ($startAfter->getTimestamp() > $thisTimeTomorrow || $startAfter->getTimestamp() < $now->getTimestamp()) {
                $this->log('Invalid startAfter parameter passed. Please ensure the time format is HHmm e.g. 1300');
                return false;
            }

            $this->log(sprintf(
                '%s queued for %s %s.',
                StaticCacheFullBuildJob::class,
                $startAfter->format('H:m'),
                $dayWord
            ));
        } else {
            $startAfter = null;
            $this->log(StaticCacheFullBuildJob::class . ' added to the queue for immediate processing');
        }

        $job->setJobData(0, 0, false, new \stdClass(), [
            'Building static cache for full site'
        ]);
        QueuedJobService::singleton()->queueJob($job, $startAfter ? $startAfter->format('Y-m-d H:i:s') : null);

        return true;
    }

    protected function log($message)
    {
        $newLine = Director::is_cli() ? PHP_EOL : '<br>';
        echo $message . $newLine;
    }
}


<?php

namespace SilverStripe\StaticPublishQueue\Job;

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\StaticPublishQueue\Contract\StaticallyPublishable;
use SilverStripe\StaticPublishQueue\Extension\Publishable\PublishableSiteTree;
use SilverStripe\StaticPublishQueue\Job;
use SilverStripe\StaticPublishQueue\Publisher;
use SilverStripe\Versioned\Versioned;

/**
 * Adds all pages to the queue for caching. Best implemented on a cron via StaticCacheFullBuildTask.
 */
class StaticCacheFullBuildJob extends Job
{
    /**
     * @return string
     */
    public function getTitle()
    {
        return 'Generate static pages for all URLs';
    }

    /**
     * @return string
     */
    public function getSignature()
    {
        return md5(static::class);
    }

    public function setup()
    {
        parent::setup();
        $this->URLsToProcess = $this->getAllLivePageURLs();
        $this->URLsToCleanUp = [];
        $this->totalSteps = ceil(count($this->URLsToProcess) / self::config()->get('chunk_size'));
        $this->addMessage(sprintf('Building %s URLS', count($this->URLsToProcess)));
        $this->addMessage(var_export(array_keys($this->URLsToProcess), true));
    }

    /**
     * Do some processing yourself!
     */
    public function process()
    {
        $chunkSize = self::config()->get('chunk_size');
        $count = 0;

        // Remove any URLs which have already been processed
        if ($this->jobData->ProcessedURLs) {
            $this->jobData->URLsToProcess = array_diff_key(
                $this->jobData->URLsToProcess,
                $this->jobData->ProcessedURLs
            );
        }

        foreach ($this->jobData->URLsToProcess as $url => $priority) {
            if (++$count > $chunkSize) {
                break;
            }
            $meta = Publisher::singleton()->publishURL($url, true);
            if (!empty($meta['success'])) {
                $this->jobData->ProcessedURLs[$url] = $url;
                unset($this->jobData->URLsToProcess[$url]);
            }
        }
        if (empty($this->jobData->URLsToProcess)) {
            $trimSlashes = function ($value) {
                return trim($value, '/');
            };
            $this->jobData->publishedURLs = array_map($trimSlashes, Publisher::singleton()->getPublishedURLs());
            $this->jobData->ProcessedURLs = array_map($trimSlashes, $this->jobData->ProcessedURLs);
            $this->jobData->URLsToCleanUp = array_diff($this->jobData->publishedURLs, $this->jobData->ProcessedURLs);

            foreach ($this->jobData->URLsToCleanUp as $staleURL) {
                $purgeMeta = Publisher::singleton()->purgeURL($staleURL);
                if (!empty($purgeMeta['success'])) {
                    unset($this->jobData->URLsToCleanUp[$staleURL]);
                }
            }
        };
        $this->isComplete = empty($this->jobData->URLsToProcess) && empty($this->jobData->URLsToCleanUp);
    }

    /**
     *
     * @return array
     */
    protected function getAllLivePageURLs()
    {
        $urls = [];
        $this->extend('beforeGetAllLivePageURLs', $urls);
        $livePages = Versioned::get_by_stage(SiteTree::class, Versioned::LIVE);
        foreach ($livePages as $page) {
            if ($page->hasExtension(PublishableSiteTree::class) || $page instanceof StaticallyPublishable) {
                $urls = array_merge($urls, $page->urlsToCache());
            }
        }

        $this->extend('afterGetAllLivePageURLs', $urls);
        // @TODO look here when integrating subsites
        // if (class_exists(Subsite::class)) {
        //     Subsite::disable_subsite_filter(true);
        // }
        return $urls;
    }
}


<?php

namespace SilverStripe\StaticPublishQueue\Extension\Engine;

use SilverStripe\CMS\Model\SiteTreeExtension;
use SilverStripe\Core\Environment;
use SilverStripe\StaticPublishQueue\Contract\StaticPublishingTrigger;
use SilverStripe\StaticPublishQueue\Extension\Publishable\PublishableSiteTree;
use SilverStripe\StaticPublishQueue\Job\DeleteStaticCacheJob;
use SilverStripe\StaticPublishQueue\Job\GenerateStaticCacheJob;
use Symbiote\QueuedJobs\Services\QueuedJobService;
use SilverStripe\Core\Injector\Injector;

/**
 * This extension couples to the StaticallyPublishable and StaticPublishingTrigger implementations
 * on the SiteTree objects and makes sure the actual change to SiteTree is triggered/enqueued.
 *
 * Provides the following information as a context to StaticPublishingTrigger:
 * * action - name of the executed action: publish or unpublish
 *
 * @see PublishableSiteTree
 */
class SiteTreePublishingEngine extends SiteTreeExtension
{
    /**
     * Queues the urls to be flushed into the queue.
     *
     * @var array
     */
    private $toUpdate = array();

    /**
     * Queues the urls to be deleted as part of a next flush operation.
     *
     * @var array
     */
    private $toDelete = array();

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

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

    /**
     * @param array $toUpdate
     * @return $this
     */
    public function setToUpdate($toUpdate)
    {
        $this->toUpdate = $toUpdate;
        return $this;
    }

    /**
     * @param $toDelete
     * @return $this
     */
    public function setToDelete($toDelete)
    {
        $this->toDelete = $toDelete;
        return $this;
    }

    /**
     * @param \SilverStripe\CMS\Model\SiteTree|null $original
     */
    public function onAfterPublish(&$original)
    {
        // if the site tree has been "reorganised" (ie: the parentID has changed)
        // then this is eht equivalent of an unpublish and publish as far as the
        // static publisher is concerned
        if ($original && (
                $original->ParentID != $this->getOwner()->ParentID
                || $original->URLSegment != $this->getOwner()->URLSegment
            )
        ) {
            $context = [
                'action' => 'unpublish',
            ];
            $original->collectChanges($context);
            $original->flushChanges();
        }
        $context = [
            'action' => 'publish',
        ];
        $this->collectChanges($context);
        $this->flushChanges();
    }

    public function onBeforeUnpublish()
    {
        $context = array(
            'action' => 'unpublish'
        );
        $this->collectChanges($context);
    }

    public function onAfterUnpublish()
    {
        $this->flushChanges();
    }

    /**
     * Collect all changes for the given context.
     *
     * @param array $context
     */
    public function collectChanges($context)
    {
        Environment::increaseMemoryLimitTo();
        Environment::increaseTimeLimitTo();

        if ($this->getOwner()->hasExtension(PublishableSiteTree::class)
            || $this->getOwner() instanceof StaticPublishingTrigger
        ) {
            $toUpdate = $this->getOwner()->objectsToUpdate($context);
            $this->setToUpdate($toUpdate);

            $toDelete = $this->getOwner()->objectsToDelete($context);
            $this->setToDelete($toDelete);
        }
    }

    /**
     * Execute URL deletions, enqueue URL updates.
     */
    public function flushChanges()
    {
        $queue = QueuedJobService::singleton();
        if (!empty($this->toUpdate)) {
            foreach ($this->toUpdate as $queueItem) {
                $job = Injector::inst()->create(GenerateStaticCacheJob::class);

                $jobData = new \stdClass();
                $urls = $queueItem->urlsToCache();
                ksort($urls);
                $jobData->URLsToProcess = $urls;

                $job->setJobData(0, 0, false, $jobData, [
                    'Building URLs: ' . var_export(array_keys($jobData->URLsToProcess), true)
                ]);

                $queue->queueJob($job);
            }
            $this->toUpdate = array();
        }

        if (!empty($this->toDelete)) {
            foreach ($this->toDelete as $queueItem) {
                $job = Injector::inst()->create(DeleteStaticCacheJob::class);

                $jobData = new \stdClass();
                $urls = $queueItem->urlsToCache();
                ksort($urls);
                $jobData->URLsToProcess = $urls;

                $job->setJobData(0, 0, false, $jobData, [
                    'Purging URLs: ' . var_export(array_keys($jobData->URLsToProcess), true)
                ]);

                $queue->queueJob($job);
            }
            $this->toDelete = array();
        }
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: keywan
 * Date: 23.11.18
 * Time: 11:37
 */

namespace OxidCommunity\ModuleInternals\Core;

use OxidEsales\Eshop\Core\Module\ModuleExtensionsCleaner;
use OxidEsales\Eshop\Core\Registry;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;

class ModuleExtensionCleanerDebug extends ModuleExtensionsCleaner
{
    protected $_debugOutput;

    public function __construct()
    {
        $this->_debugOutput= new NullLogger();
    }

    public function setOutput(LoggerInterface $out){

        $this->_debugOutput = $out;
    }

    /**
     * Removes garbage ( module not used extensions ) from all installed extensions list.
     * For example: some classes were renamed, so these should be removed.
     *
     * @param array                                $installedExtensions
     * @param \OxidEsales\Eshop\Core\Module\Module $module
     *
     * @return array
     */
    public function cleanExtensions($installedExtensions, \OxidEsales\Eshop\Core\Module\Module $module)
    {
        $moduleExtensions = $module->getExtensions();

        $installedModuleExtensions = $this->filterExtensionsByModule($installedExtensions, $module);

        if (count($installedModuleExtensions)) {
            $garbage = $this->getModuleExtensionsGarbage($moduleExtensions, $installedModuleExtensions);

            if (count($garbage)) {
                $installedExtensions = $this->removeGarbage($installedExtensions, $garbage);
            }
        }

        $oModules = oxNew( \OxidEsales\EshopCommunity\Core\Module\ModuleList::class );
        //ids will include garbage in case there are files that not registered by any module
        $ids = $oModules->getModuleIds();

        $config = Registry::getConfig();
        $knownIds = array_keys($config->getConfigParam('aModulePaths'));
        $diff = array_diff($ids,$knownIds);
        if ($diff) {
            foreach ($diff as $item) {
                foreach ($installedExtensions as &$coreClassExtension) {
                    foreach ($coreClassExtension as $i => $ext) {
                        if ($ext === $item) {
                            $this->_debugOutput->debug("$item will be removed");
                            unset($coreClassExtension[$i]);
                        }
                    }
                }
            }
        }

        return $installedExtensions;
    }

    protected function removeGarbage($aInstalledExtensions, $aarGarbage)
    {
        foreach ($aarGarbage as $moduleId => $aExt) {
            $this->_debugOutput->info("removing garbage for module $moduleId: " . join(',', $aExt));
        }
        return parent::removeGarbage($aInstalledExtensions, $aarGarbage);
    }

    /**
     * Returns extension which is no longer in metadata - garbage
     *
     * @param array $moduleMetaDataExtensions  extensions defined in metadata.
     * @param array $moduleInstalledExtensions extensions which are installed
     *
     * @return array
     */
    protected function getModuleExtensionsGarbage($moduleMetaDataExtensions, $moduleInstalledExtensions)
    {

        $garbage = parent::getModuleExtensionsGarbage($moduleMetaDataExtensions, $moduleInstalledExtensions);

        foreach ($moduleInstalledExtensions as $coreClassName => $listOfExtensions) {
            foreach ($listOfExtensions as $extensions) {
                if (! (isset($moduleMetaDataExtensions[$coreClassName]) && $moduleMetaDataExtensions[$coreClassName] == $extensions)) {
                    $garbage[$coreClassName][] = $extensions;
                }
            }
        }

        return $garbage;
    }

    /**
     * Returns extensions list by module id.
     *
     * @param array  $modules  Module array (nested format)
     * @param string $module Module id/folder name
     *
     * @return array
     */
    protected function filterExtensionsByModule($modules, $module)
    {
        if ($this->isMetadataVersionGreaterEqual($module, '2.0')) {
            $moduleHelper = Registry::get(ModuleHelper::class);
            $moduleHelper->setModule($module);
            $path = $moduleHelper->getModuleNameSpace();
        } else {

            $modulePaths = \OxidEsales\Eshop\Core\Registry::getConfig()->getConfigParam('aModulePaths');

            $moduleId = $module->getId();
            $path = '';
            if (isset($modulePaths[$moduleId])) {
                $path = $modulePaths[$moduleId] . '/';
            }

            if (!$path) {
                $path = $moduleId . "/";
            }
        }
        $filteredModules = [];

        if (!$path) {
            return $filteredModules;
        }

       foreach ($modules as $class => $extend) {
            foreach ($extend as $extendPath) {
                if (strpos($extendPath, $path) === 0) {
                    $filteredModules[$class][] = $extendPath;
                }
            }
        }

        return $filteredModules;
    }

    public function isMetadataVersionGreaterEqual($module, $sVersion)
    {
        return version_compare($module->getMetaDataVersion(), $sVersion) >= 0;
    }
}


<?php
/**
 * @package   moduleinternals
 * @category  OXID Module
 * @license   GPL3 License http://opensource.org/licenses/GPL
 * @author    Alfonsas Cirtautas / OXID Community
 * @link      https://github.com/OXIDprojects/ocb_cleartmp
 * @see       https://github.com/acirtautas/oxid-module-internals
 */

namespace OxidCommunity\ModuleInternals\Core;

use \OxidEsales\Eshop\Core\DatabaseProvider as DatabaseProvider;
use \OxidEsales\Eshop\Core\Registry as Registry;
use \OxidEsales\Eshop\Core\Module\ModuleList as ModuleList;

/**
 * Class InternalModule: chain extends OxidEsales\Eshop\Core\Module\Module
 * @package OxidCommunity\ModuleInternals\Core
 */
class InternalModule extends InternalModule_parent
{
    protected $state = self::FINE;
    protected $checked = false;
    /** @var ModuleStateFixer */
    protected $_oModuleFixHelper;

    const FINE = 0;
    const NEED_MANUAL_FIXED = 1;
    const MAY_NEED_MANUAL_FIX = 2;

    const OK = 'sok';
    const MODULE_FILE_NOT_FOUND = 'sfatalm';
    const SHOP_FILE_NOT_FOUND = 'sfatals';

    public function load($id){
        $this->metaDataVersion = 0;
        $this->checked = false;
        $this->state = self::FINE;
        $res = parent::load($id);
        return $res;
    }

    /**
     * @param ModuleStateFixer $oModuleFixHelper
     */
    public function setModuleFixHelper($oModuleFixHelper)
    {
        $this->_oModuleFixHelper = $oModuleFixHelper;
    }

    public function getModuleHelper(){
        return Registry::get(ModuleHelper::class);
    }

    /**
     * @return ModuleStateFixer
     */
    public function getModuleStateFixer()
    {
        if ($this->_oModuleFixHelper === null) {
            $this->_oModuleFixHelper = Registry::get(ModuleStateFixer::class);
	    //$this->_oModuleFixHelper->disableInitialCacheClear();
        }

        return $this->_oModuleFixHelper;
    }

    /**
     * Get template blocks defined in database.
     *
     * @return array
     * @throws \OxidEsales\Eshop\Core\Exception\DatabaseErrorException
     */
    public function getModuleBlocks()
    {
        $config = Registry::getConfig();
        $activeThemeIds = oxNew(\OxidEsales\Eshop\Core\Theme::class)->getActiveThemesList();
        $activeThemeIds[] = '';

        $themeIdsSql = join(', ', \OxidEsales\Eshop\Core\DatabaseProvider::getDb()->quoteArray($activeThemeIds));

        $aResults = DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->select(
            "SELECT OXID as id,
                           OXACTIVE as active,
                           OXTHEME as theme,
                           OXTEMPLATE as template,
                           OXBLOCKNAME as block,
                           OXFILE as file,
                           OXPOS
                    FROM oxtplblocks
                    WHERE oxModule = ? AND oxshopid = ? AND OXTHEME IN ($themeIdsSql)
                    ORDER BY OXTHEME, OXTEMPLATE, OXBLOCKNAME",
            [$this->getId(), $config->getShopId()]
        );

        return $aResults->fetchAll();
    }

    /**
     * Get module settings defined in database.
     *
     * @return array
     */
    public function getModuleSettings()
    {
        $aResult = DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->select(
            'SELECT * FROM oxconfig WHERE oxModule = ? AND oxshopid = ?',
            [sprintf('module:%s', $this->getId()), Registry::getConfig()->getShopId()]
        );

        return $aResult->fetchAll();
    }

    /**
     * Check supported metadata version
     *
     * @return bool
     */
    public function isMetadataSupported()
    {
        $sMetadataVersion = $this->getMetaDataVersion();

        $sLatestVersion = '1.0';

        if (method_exists('oxModuleList', 'getModuleVersions') || method_exists('oxModule', 'getModuleEvents')) {
            $sLatestVersion = '1.1';
        }

        if (method_exists(ModuleList::class, 'getModuleConfigParametersByKey')) {
            $sLatestVersion = '2.0';
        }

        if (method_exists(ModuleList::class, 'getSmartyPluginDirectories')) {
            $sLatestVersion = '2.1';
        }

        return version_compare($sLatestVersion, $sMetadataVersion) >= 0;
    }

    /**
     * check if current metadata version is $sVersion
     *
     * @param $sVersion
     *
     * @return bool
     */
    public function isMetadataVersionGreaterEqual($sVersion)
    {
        return version_compare($this->getMetaDataVersion(), $sVersion) >= 0;
    }

    /**
     * check if current metadata version is $sVersion
     *
     * @param $sVersion
     *
     * @return bool
     */
    public function checkMetadataVersion($sVersion)
    {
        return version_compare($this->getMetaDataVersion(), $sVersion) == 0;
    }

    /**
     * Returns array of module files / entries in metadata.php
     * like 'template' => / 'blocks' => ....
     * possible entries
     *
     * ModuleList::MODULE_KEY_FILES = 'files'
     * ModuleList::MODULE_KEY_CONTROLLERS = 'controllers'
     * ModuleList::MODULE_KEY_EVENTS => 'events'
     * ModuleList::MODULE_KEY_VERSIONS => module version
     *
     * @return array
     */
    public function getModuleEntries($sType)
    {
        $aReturn = [];
        $aList = oxNew(ModuleList::class)->getModuleConfigParametersByKey($sType);

        if (isset($aList[ $this->getId() ])) {
            $aReturn = $aList[ $this->getId() ];
        }

        return $aReturn;
    }

    /**
     * checks if module file exists on directory
     * switches between metadata version for checking
     * checks also for namespace class if metadata version = 2.0
     *
     * @param string $sClassName
     * @param string $sExtention
     *
     * @return bool
     */
    public function checkPhpFileExists($sClassName)
    {
        if ($this->isMetadataVersionGreaterEqual('2.0')) {
            $composerClassLoader = $this->getModuleHelper()->getAutoloader();

            return $composerClassLoader->findFile($sClassName);
        } else {
            $sExtensionPath = $sClassName . '.php';
            $res = $this->checkFileExists($sExtensionPath);
            return $res;
        }
    }

    /**
     * Analyze versions in metadata
     * checks if metadata version is same as database entry for metadata
     *
     * @return array
     */
    public function checkModuleVersions()
    {
        $sDatabaseVersion = $this->getModuleEntries(ModuleList::MODULE_KEY_VERSIONS);

        /**
         * Set version for disabled module
         */
        if (!count($sDatabaseVersion)) {
            $iLang = \OxidEsales\Eshop\Core\Registry::getLang()->getTplLanguage();
            $sDatabaseVersion = $this->getInfo("version", $iLang);
        }

        $aResult = $this->toResult(['version'=>$sDatabaseVersion]);

        return $aResult;
    }

    public function getTitle() {
        if ($this->checked !== false) {
            return $this->checked['title'];
        }
        $title = parent::getTitle();
        $request = Registry::getRequest();
        $controller = $request->getRequestParameter('cl');

        if ($controller == 'module_list' || $controller == 'checkconsistency') {
            $fixed = $this->getModuleStateFixer()->fix($this);
            if ($fixed) {
                $title .= ' State fixed';
            }
            $this->checked = $this->checkState();
            if ($this->hasIssue()) {
                $title .= ' Issue found!';
            }

            $this->checked['title'] = $title;
        }
        return $title;
    }

    public function hasIssue(){
        return ($this->state & self::NEED_MANUAL_FIXED) == self::NEED_MANUAL_FIXED;
    }

    /**
     * Analyze extended class information in metadata and database.
     *
     * @return array
     */
    public function checkExtendedClasses()
    {
        $aMetadataExtend = $this->getInfo('extend');
        $aMetadataExtend = is_array($aMetadataExtend) ? $aMetadataExtend : [];

        $aResult = [];

        /**
         * only convert class names to lower if we don't use namespace
         */
        if (!$this->isMetadataVersionGreaterEqual('2.0')) {
            $aMetadataExtend = array_change_key_case($aMetadataExtend, CASE_LOWER);
            //convert legacy classnames because $aAllModules dos not contain legacy classes
            if (method_exists(Registry::class ,'getBackwardsCompatibilityClassMap')) {
                $map = Registry::getBackwardsCompatibilityClassMap();
                foreach ($aMetadataExtend as $legacyName => $file) {
                    if (isset($map[$legacyName])) {
                        $namespaceName = $map[$legacyName];
                        $aMetadataExtend[$namespaceName] = $file;
                        unset($aMetadataExtend[$legacyName]);
                    }
                }
            }
        }

        foreach ($aMetadataExtend as $sClassName => $sModuleName) {
            $key_state = $iState = self::OK;

            if (strpos($sClassName,'OxidEsales\\EshopCommunity\\') === 0 ||
                strpos($sClassName,'OxidEsales\\EshopEnterprise\\') === 0 ||
                strpos($sClassName,'OxidEsales\\EshopProfessional\\') === 0 ||
                !class_exists($sClassName)
            ) {
                if (strpos($sClassName,'oxerp') === 0) {
                    //AS ERP module does still use a own autoloader
                    //classes of oxerp will not be found with class_exists
                    $erp_dir = Registry::getConfig()->getModulesDir() .'erp/';
                    if (strpos($sClassName,'oxerptype_') === 0) {
                        $dir = $erp_dir.'objects/';
                    } else {
                        $dir = $erp_dir;
                    }

                    $sFullPath  = $dir.$sClassName.'.php';

                    if (!file_exists($sFullPath)) {
                        $key_state = self::SHOP_FILE_NOT_FOUND;
                        $this->state |= self::NEED_MANUAL_FIXED;
                    }
                } else {
                    $key_state = self::SHOP_FILE_NOT_FOUND;
                    $this->state |= self::NEED_MANUAL_FIXED;
                }
            }

            $aResult[ $sClassName ]['key_state'] = $key_state;

            if (!$this->checkPhpFileExists($sModuleName)) {
                $iState = self::MODULE_FILE_NOT_FOUND;
                $this->state |= self::NEED_MANUAL_FIXED;
            }

            $aResult[ $sClassName ][ 'data' ] = $sModuleName;
            $aResult[ $sClassName ][ 'state' ] = $iState;
        }

        return $aResult;
    }

    public function setModuleData($aModule)
    {
        $moduleId = $aModule['id'];
        if (!isset($aModule['version'])) {
            $package = $this->getComposerPackage($moduleId);

            if ($package) {
                $version = $package->getVersion();
                $aModule['version'] = $version;
            }
        }
        parent::setModuleData($aModule);
    }

    /**
     * Analyze template block information in metadata and database.
     *
     * @return array
     */
    public function checkTemplateBlocks()
    {
        $sModulePath = $this->getModulePath();
        $aDatabaseBlocks = $this->getModuleBlocks();
        $aDatabaseBlocks = is_array($aDatabaseBlocks) ? $aDatabaseBlocks : [];
        $aMetadataTemplates = $this->getInfo('templates');

        $config = Registry::getConfig();
        $sModulesDir = $config->getModulesDir();

        // Check if all blocks are injected.
        foreach ($aDatabaseBlocks as &$aBlock) {
            $iState = self::OK;

            $file = $aBlock['file'];
            if (!$this->checkFileExists( $sModulePath . '/' . $file) &&
                !$this->checkFileExists( $sModulePath . '/out/blocks/' . basename($file)) &&
                !$this->checkFileExists( $sModulePath . '/out/blocks/' . basename($file) . '.tpl')
            ) {
                $iState = self::MODULE_FILE_NOT_FOUND;
                $this->state |= self::NEED_MANUAL_FIXED;
            }

            $block = $aBlock['block'];
            $template = $aBlock['template'];
            $aBlock['state'] = $iState;

            // Check if template file exists and block is defined.

            // Get template from shop..
            $sTemplate = $config->getTemplatePath($template, false);

            // Get template from shop admin ..
            if (!$sTemplate) {
                $sTemplate = $config->getTemplatePath($template, true);
            }

            // Get template from module ..
            if (!$sTemplate && isset($aMetadataTemplates[$template])) {

                if ($this->checkFileExists($aMetadataTemplates[$template])) {
                    $sTemplate = $sModulesDir . '/' . $aMetadataTemplates[$template];
                }
            }

            if (empty($sTemplate)) {
                $aBlock['t_state'] = self::SHOP_FILE_NOT_FOUND;
                $this->state |= self::MAY_NEED_MANUAL_FIX;
            } else {
                $aBlock['t_state'] = self::OK;
                $aBlock['b_state'] = self::OK;
                $sContent = file_get_contents($sTemplate);
                if (!preg_match('/\[{\s*block[^}]+?name\s*=\s*["\']' . $block . '[\'"].*?}\]/', $sContent)) {
                    $aBlock['b_state'] = self::SHOP_FILE_NOT_FOUND;
                    $this->state |= self::MAY_NEED_MANUAL_FIX;
                }
            }

        }

        return $aDatabaseBlocks;
    }

    /**
     * Analyze settings in metadata ans settings.
     *
     * @return array
     */
    public function checkModuleSettings()
    {
        $aDatabaseSettings = $this->getModuleSettings();
        $aDatabaseSettings = is_array($aDatabaseSettings) ? $aDatabaseSettings : [];
        $list = [];
        foreach ($aDatabaseSettings as $v) {
            $list[$v['OXVARNAME']] = $v['OXVARTYPE'];
        }

        $aResult = $this->toResult($list);
        return $aResult;
    }

    /**
     * Analyze templates in metadata ans settings.
     *
     * @return array
     */
    public function checkModuleTemplates()
    {
        $aDatabaseTemplates = $this->getModuleEntries(ModuleList::MODULE_KEY_TEMPLATES);
        $aResult = $this->checkFiles($aDatabaseTemplates, false);

        return $aResult;
    }

    /**
     * Analyze controller in metadata
     *
     * @return array
     */
    public function checkModuleController()
    {
        $oModuleStateFixer = Registry::get(ModuleStateFixer::class);
        $controllers =  $oModuleStateFixer->getModuleControllerEntries($this->getId());
        return $this->checkFiles($controllers, true);
    }

    /**
     * Analyze file in metadata
     *
     * @return array
     */
    public function checkModuleFiles()
    {
        $aDatabaseFiles = $this->getModuleEntries(ModuleList::MODULE_KEY_FILES);
        return $this->checkFiles($aDatabaseFiles, false);
    }

    protected function toResult($array){
        $result = [];
        foreach ($array as $key => $data) {
            $result[$key]['data'] = $data;
            $result[$key]['state'] = self::OK;
        }
        return $result;
    }

    protected function checkFiles($files , $php){
        $result = [];
        foreach ($files as $key => $file) {
            $result[$key]['data'] = $file;
            $s = self::OK;
            if (($php && !$this->checkPhpFileExists($file))
                || ((!$php) && !$this->checkFileExists($file))) {
                $s = self::MODULE_FILE_NOT_FOUND;
                $this->state |= self::NEED_MANUAL_FIXED;
            }
            $result[$key]['state'] = $s;
        }
        return $result;
    }

    /**
     * Analyze events in metadata ans settings.
     *
     * @return array
     */
    public function checkModuleEvents()
    {
        $aDatabaseEvents = $this->getModuleEntries(ModuleList::MODULE_KEY_EVENTS);

        $aDatabaseEvents = is_array($aDatabaseEvents) ? $aDatabaseEvents : [];
        $aDatabaseEvents = array_map(function ($value){return print_r($value,true);}, $aDatabaseEvents);
        $aResult = $this->toResult($aDatabaseEvents);
        foreach ($aResult as $eventName => &$data){
            $data['key_state'] = ($eventName == 'onActivate' || $eventName == 'onDeactivate') ? self::OK : self::SHOP_FILE_NOT_FOUND;
        }
        return $aResult;
    }

    /**
     * @param $oModule
     * @param $sModId
     * @param $sTitle
     * @return array
     */
    public function checkState($sTitle = '')
    {
        if ($this->checked !== false) {
            return $this->checked;
        }
        $oModule = $this;
        $aModule = array();
        $aModule['oxid'] = $sId = $oModule->getId();
        $aModule['title'] = $aModule['oxid'] . " - " . $sTitle;

        $aModule['aExtended'] = $oModule->checkExtendedClasses();
        $aModule['aBlocks'] = $oModule->checkTemplateBlocks();
        $aModule['aSettings'] = $oModule->checkModuleSettings();
        $aModule['aTemplates'] = $oModule->checkModuleTemplates();

        $aModule['aFiles'] = array();
        $aModule['aEvents'] = array();
        $aModule['aVersions'] = array();
        $aModule['aControllers'] = array();

        // files are valid for  metadata version < 2.0
        if ($oModule->isMetadataVersionGreaterEqual('2.0')) {
            /**
             * @todo check if files is set - should'nt be
             */
        } else {
            $aModule['aFiles'] = $oModule->checkModuleFiles();
        }

        // valid  for  metadata version >= 1.1
        if ($oModule->isMetadataVersionGreaterEqual('1.1')) {
            $aModule['aEvents'] = $oModule->checkModuleEvents();
        }

        $aModule['aVersions'] = $oModule->checkModuleVersions();

        if ($oModule->isMetadataVersionGreaterEqual('2.0')) {
            $aModule['aControllers'] = $oModule->checkModuleController();
        }
        return $aModule;
    }

    protected $filelist = [];
    /**
     * @param $filePath
     * @return bool
     */
    protected function checkFileExists($filePath)
    {
        $sModulesDir = Registry::getConfig()->getModulesDir();
        $filePath = $sModulesDir . $filePath;

        $dir = dirname($filePath);
        $file = basename($filePath);
        $res = true;
        //check if filename case sensitive so we will see errors
        //also on case insensitive filesystems
        $filelist = $this->filelist[$dir];
        if (!isset($filelist)) {
            $filelist = $this->filelist[$dir] = scandir($dir);
        }
        if (!in_array($file, $filelist)) {
            $res = false;
        }
        return $res;
    }

    /**
     * @param $moduleId
     * @return bool|\Composer\Package\PackageInterface
     */
    protected function getComposerPackage($moduleId = null)
    {
        if ($moduleId == null){
            $moduleId = $this->getId();
        }
        /**
         * @var $packageService OxidComposerModulesService
         */
        $packageService = Registry::get(OxidComposerModulesService::class);
        $package = $packageService->getPackage($moduleId);
        return $package;
    }

}


<?php

namespace OxidCommunity\ModuleInternals\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\NullOutput;
use OxidEsales\Eshop\Core\Registry;
use OxidEsales\Eshop\Core\Module\Module;
use OxidEsales\Eshop\Core\Module\ModuleList;
use OxidEsales\Eshop\Core\Exception\InputException;
use OxidCommunity\ModuleInternals\Core\ModuleStateFixer;
use OxidProfessionalServices\OxidConsole\Core\ShopConfig;

/**
 * Fix States command
 */
class ModuleFixCommand extends Command
{

    /**
     * @var array|null Available module ids
     */
    protected $_aAvailableModuleIds = null;

    /** @var InputInterface */
    private $input;

    /**
     * {@inheritdoc}
     */
    public function configure()
    {
        $this
            ->setName('module:fix')
            ->setAliases(['fix:states'])
            ->setDescription('Fixes modules metadata states')
            ->addOption('all', 'a', InputOption::VALUE_NONE, 'Includes all modules')
            ->addOption('base-shop', 'b', InputOption::VALUE_NONE, 'Apply changes to base shop only')
            ->addOption('shop', 's', InputOption::VALUE_REQUIRED, 'Apply changes to given shop only')
            ->addArgument('module-id', InputArgument::IS_ARRAY, 'Module id/ids to use');
    }

    /**
     * {@inheritdoc}
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $this->input = $input;

        $verboseOutput = $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE
            ? $output
            : new NullOutput();

        try {
            $aModuleIds = $this->_parseModuleIds();
            $aShopConfigs = $this->_parseShopConfigs();
        } catch (InputException $oEx) {
            $output->writeLn($oEx->getMessage());
            exit(1);
        }

        /** @var ModuleStateFixer $oModuleStateFixer */
        $oModuleStateFixer = Registry::get(ModuleStateFixer::class);
        $oModuleStateFixer->setOutput($output);
        $oModuleStateFixer->setDebugOutput($verboseOutput);

        /** @var Module $oModule */
        $oModule = oxNew(Module::class);

        foreach ($aShopConfigs as $oConfig) {
            $moduleCount = count($aModuleIds);
            $verboseOutput->writeLn('[DEBUG] Working on shop id ' . $oConfig->getShopId() . " fixing $moduleCount modules");
            $oModuleStateFixer->setConfig($oConfig);
            $oModuleStateFixer->cleanUp();
            foreach ($aModuleIds as $sModuleId) {
                $oModule->setMetaDataVersion(null);
                if (!$oModule->load($sModuleId)) {
                    $verboseOutput->writeLn("[DEBUG] {$sModuleId} does not exist - skipping");
                    continue;
                }

                $verboseOutput->writeLn("[DEBUG] Fixing {$sModuleId} module");
                $oModuleStateFixer->fix($oModule);
            }

            $verboseOutput->writeLn('');
        }

        $output->writeLn('Fixed module states successfully');
    }

    /**
     * Parse and return module ids from input
     *
     * @return array
     *
     * @throws InputException
     */
    protected function _parseModuleIds()
    {
        if ($this->input->getOption('all')) {
            return $this->_getAvailableModuleIds();
        }

        if (count($this->input->getArguments()['module-id']) === 0) {
            throw oxNew(
                InputException::class,
                'Please specify at least one module if as argument or use --all (-a) option'
            );
        }

        $requestedModuleIds = $this->input->getArguments()['module-id'];
        $availableModuleIds = $this->_getAvailableModuleIds();

        // Checking if all provided module ids exist
        foreach ($requestedModuleIds as $moduleId) {
            if (!in_array($moduleId, $availableModuleIds)) {
                throw oxNew(
                    InputException::class,
                    "{$moduleId} module does not exist"
                );
            }
        }

        return $requestedModuleIds;
    }

    /**
     * Parse and return shop config objects from input
     *
     * @return ShopConfig[]
     *
     * @throws InputException
     */
    protected function _parseShopConfigs()
    {
        if ($this->input->getOption('base-shop')) {
            return array(Registry::getConfig());
        }

        if ($shopId = $this->input->getOption('shop')) {
            if ($oConfig = ShopConfig::get($shopId)) {
                return array($oConfig);
            }

            throw oxNew(
                InputException::class,
                'Shop id does not exist'
            );
        }

        return ShopConfig::getAll();
    }

    /**
     * Get all available module ids
     *
     * @return array
     */
    protected function _getAvailableModuleIds()
    {
        if ($this->_aAvailableModuleIds === null) {
            $oConfig = Registry::getConfig();

            // We are calling getModulesFromDir() because we want to refresh
            // the list of available modules. This is a workaround for OXID
            // bug.
            oxNew(ModuleList::class)->getModulesFromDir($oConfig->getModulesDir());
            $this->_aAvailableModuleIds = array_keys($oConfig->getConfigParam('aModulePaths'));
        }

        return $this->_aAvailableModuleIds;
    }

    /**
     * important method for oxrun to bootstrap oxid
     * @return bool
     */
    public function isEnabled()
    {
        $app = $this->getApplication();
        if (method_exists($app,'bootstrapOxid') ) {
                return $app->bootstrapOxid(true);
        }
        return true;
    }

}


<?php
/**
 * @package   moduleinternals
 * @category  OXID Module
 * @version   1.0.1
 * @license   GPL3 License http://opensource.org/licenses/GPL
 * @author    Alfonsas Cirtautas / OXID Community
 * @link      https://github.com/OXIDprojects/ocb_cleartmp
 * @see       https://github.com/acirtautas/oxid-module-internals
 */

namespace OxidCommunity\ModuleInternals\Controller\Admin;

use \OxidEsales\Eshop\Core\Module\Module as Module;
use \OxidEsales\Eshop\Core\Module\ModuleCache as ModuleCache;
use \OxidEsales\Eshop\Core\Module\ModuleInstaller as ModuleInstaller;

/**
 * Module internals tools.
 *
 * @author Oxid Community
 */

/**
 * Internal module utilities.
 */
class UtilsController extends \OxidEsales\Eshop\Application\Controller\Admin\AdminController
{

    /** @var oxModule */
    protected $_oModule;

    /** @var oxModuleCache */
    protected $_oModuleCache;

    /** @var oxModuleInstaller */
    protected $_oModuleInstaller;

    /**
     * @var string
     */
    public $sTemplate = 'utils.tpl';

    /**
     * Get active module object.
     *
     * @return oxModule
     */
    public function getModule()
    {
        if ($this->_oModule === null) {
            $sModuleId = $this->getEditObjectId();

            $this->addTplParam('oxid', $sModuleId);
            $this->_oModule = oxNew(Module::class);
            $this->_oModule->load($sModuleId);
        }

        return $this->_oModule;
    }

    /**
     * Returns initialized cache instance
     *
     * @return oxModuleCache
     */
    public function getModuleCache()
    {
        if ($this->_oModuleCache === null) {
            $this->_oModuleCache = oxNew(ModuleCache::class, $this->getModule());
        }

        return $this->_oModuleCache;
    }

    /**
     * Returns initialized module installer instance
     *
     * @return oxModuleInstaller
     */
    public function getModuleInstaller()
    {
        if ($this->_oModuleInstaller === null) {
            $this->_oModuleInstaller = oxNew(ModuleInstaller::class, $this->getModuleCache());
        }

        return $this->_oModuleInstaller;
    }

    /**
     * @return string
     */
    public function render()
    {
        $oModule = $this->getModule();
        $sModuleId = $oModule->getId();

        $this->addTplParam('oxid', $sModuleId);
        $this->addTplParam('blIsActive', $oModule->isActive());

        return $this->sTemplate;
    }

    /**
     * Reset module cache.
     */
    public function resetModuleCache()
    {
        $this->getModuleCache()->resetCache();
    }

    /**
     * Activate module.
     */
    public function activateModule()
    {
        $this->getModuleInstaller()->activate($this->getModule());
    }

    /**
     * Deactivate module.
     */
    public function deactivateModule()
    {
        $this->getModuleInstaller()->deactivate($this->getModule());
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: keywan
 * Date: 15.01.19
 * Time: 22:06
 */

namespace OxidCommunity\ModuleInternals\Core;

use OxidEsales\Eshop\Core\Registry;

class ModuleHelper
{
    protected $module;

    public function setModule($module){
        $this->module = $module;
    }

    /**
     * @param Module $module
     *
     * @return string
     */
    public function getModuleNameSpace()
    {
        $module = $this->module;
        $package = $this->getComposerPackage();
        $sModulePath = $module->getModulePath();
        if ($package) {
            $autoload = $package->getAutoload();
        } else {
            $sModulesDir = Registry::getConfig()->getModulesDir();
            $file = $sModulesDir . $sModulePath . '/composer.json';
            if (file_exists($file)) {
                $data = json_decode(file_get_contents($file), true);
                $autoload = $data["autoload"];
            }
        }

        if ($autoload) {
            $namesspaces = $autoload["psr-4"];
            $prefix = array_keys($namesspaces);
            $moduleNameSpace = $prefix[0];
            return $moduleNameSpace;
        }

        $moduleNameSpace = '';
        $composerClassLoader = $this->getAutoloader();
        $nameSpacePrefixes = $composerClassLoader->getPrefixesPsr4();
        foreach ($nameSpacePrefixes as $nameSpacePrefix => $paths) {
            foreach ($paths as $path) {
                if (strpos($path, $sModulePath) !== false) {
                    $moduleNameSpace = $nameSpacePrefix;

                    return $moduleNameSpace;
                }
            }
        }

        return $moduleNameSpace;
    }

    /**
     * @param Module $module
     * @return bool|\Composer\Package\PackageInterface
     */
    protected function getComposerPackage()
    {
        $moduleId = $this->module->getId();

        /**
         * @var $packageService OxidComposerModulesService
         */
        $packageService = Registry::get(OxidComposerModulesService::class);
        $package = $packageService->getPackage($moduleId);
        return $package;
    }

    public function getAutoloader(){
        if (Registry::instanceExists('autoloader')){
            return Registry::get('autoloader');
        }
        $composerClassLoader = include VENDOR_PATH . 'autoload.php';
        Registry::set('autoloader', $composerClassLoader);
        return $composerClassLoader;
    }

}