1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 
<?php
/**
 * MIT License
 *
 * Copyright (c) 2018, ArrayIterator
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

declare(strict_types=1);

namespace ArrayIterator\Extension;

/**
 * Class Parser
 * @package ArrayIterator\Extension
 *
 * Extension Parser to parse and detect path of extension directory.
 */
class Parser implements ParserInterface
{
    /**
     * {@inheritdoc}
     */
    public function parse(
        string $directory,
        bool $strict = false,
        array $existingClass = [],
        array &$duplication = []
    ) {
        $parser = $this->scanDirectory(
            $directory,
            $strict,
            $existingClass,
            $duplication
        );
        if ($parser) {
            $parser = new ExtensionInfo($parser, $strict);
        }

        return $parser;
    }

    /**
     * Scan directory extensions placed.
     *
     * @param string $directory <p>
     * Extension directory to be crawl.
     * </p>
     * @param bool $strict      <p>
     * Determine if on <b>Strict Mode</b> or not
     * </p>
     * @param array $existingClass <p>List of existing class.</p>
     * @param array $duplication <p>
     * array for injection reference duplications.
     * </p>
     * @return null|\ReflectionClass returning \ReflectionClass or NULL if invalid.
     */
    protected function scanDirectory(
        string $directory,
        bool $strict,
        array $existingClass = [],
        array &$duplication = []
    ) {
        $directory = rtrim(
            Utility::normalizeDirectorySeparator($directory),
            DIRECTORY_SEPARATOR
        );

        $baseName  = basename($directory);
        $ref       = null;
        $className = Utility::parseClassName($baseName);
        if ($className) {
            $className = $directory .  DIRECTORY_SEPARATOR . $className . '.php';
            if (is_file($className) && is_readable($className)) {
                $ref = $this->parseFile($className);
            }
            clearstatcache(true, $className);
            // check if in strict mode
            if ($strict && ($ref === null || strtolower($ref->getShortName()) !== strtolower($className))) {
                unset($ref);
                return null;
            }
        } elseif ($strict) {
            return null;
        }

        if ($ref) {
            if (in_array($ref->getName(), $existingClass)) {
                $duplication[$baseName] = [$ref->getName()];
                $ref = null;
            }

            return $ref;
        }

        /**
         * @var \SplFileInfo $item
         */
        foreach (new \FilesystemIterator(
            $directory,
            \FilesystemIterator::CURRENT_AS_FILEINFO
             | \FilesystemIterator::KEY_AS_FILENAME
             | \FilesystemIterator::SKIP_DOTS
        ) as $fileName => $item) {
            if ($fileName === $className
                || $item->isFile() === false
                || $item->getExtension() !== 'php'
                || $item->isReadable() === false
            ) {
                continue;
            }

            if (($ref = $this->parseFile($item->getRealPath()))) {
                if (!in_array($ref->getName(), $existingClass)) {
                    break;
                }
                if (!isset($duplication[$baseName])) {
                    $duplication[$baseName] = [];
                }
                $duplication[$baseName][] = $ref->getName();
                $ref = null;
            }
        }

        return $ref;
    }

    /**
     * Doing process parse of extension file.
     *
     * @param string $target <p>
     * Target file to be parse.
     * </p>
     * @return null|\ReflectionClass returning \ReflectionClass or NULL if invalid.
     */
    protected function parseFile(string $target)
    {
        if (substr($target, -4) !== '.php' || !is_file($target)) {
            return null;
        }

        $data = php_strip_whitespace($target);
        if (strtolower(substr($data, 0, 5)) !== '<?php'
            || preg_replace('~\<\?php\s*|[\s]|\s*\?\>~i', '', $data) === ''
        ) {
            return null;
        }
        $data = preg_replace(
            '/^\<\?php\s+declare[^;]+\;\s*/smi',
            "<?php\n",
            $data
        );
        preg_match(
            '~^\<\?php
                \s+namespace\s+
                (
                    [a-z\_][a-z0-9\_]{0,}
                    (?:[\\\]?[a-z\_][a-z0-9\_]{0,}){0,}
                )\s*[;]+
            ~smix',
            $data,
            $namespace
        );

        $namespace = empty($namespace[1])  ? '' : $namespace[1].'\\';
        if ($namespace === '' && preg_match('~^\<\?php\s*namespace\s+~i', $data)) {
            unset($data);
            return null;
        }

        preg_match(
            '~
                \s+
                class\s+([a-z\_][a-z0-9\_]{0,})
                (?:
                    \s+
                    (?:
                        implements\s+
                        \\\?[a-z\_][a-z0-9\_]{0,}
                        (?:\s*[\,]\s*[\\\]?[a-z\_][a-z0-9\_]{0,}){0,}
                        | extends\s+\\\?[a-z\_][a-z0-9\_]{0,}
                            (?:[\\\][a-z\_][a-z0-9\_]{0,}){0,}
                    )
                )
            ~xi',
            $data,
            $class
        );
        unset($data);

        if (empty($class[1])) {
            return null;
        }

        $class = $namespace . $class[1];
        try {
            $ref = new \ReflectionClass($class);
            if (! $ref->isAnonymous()
                && $ref->isInstantiable()
                && $ref->isSubclassOf(ExtensionInterface::class)
            ) {
                return $ref;
            }
        } catch (\Exception $e) {
        }

        set_error_handler(function () {
            error_clear_last();
            throw new \Exception();
        });

        $ref = null;
        try {
            /** @noinspection PhpIncludeInspection */
            include_once $target;
            $ref = new \ReflectionClass($class);
            if ($ref->isAnonymous()
                || ! $ref->isInstantiable()
                || ! $ref->isSubclassOf(ExtensionInterface::class)
            ) {
                $ref = null;
            }
        } catch (\Exception $e) {
        } catch (\Throwable $e) {
        } finally {
            unset($e);
            // pass
            restore_error_handler();
        }

        return $ref;
    }

    /**
     * Implementation of interface \Serializable, when object serialize.
     *
     * @return string serialized data.
     */
    public function serialize() : string
    {
        return serialize([]);
    }

    /**
     * Implementation of interface \Serializable, when object serialized unserialize.
     *
     * @param string $serialized <p>
     * Serialized data.
     * </p>
     *
     * @return void
     */
    public function unserialize($serialized)
    {
        return;
    }
}