Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 105
AutoloadGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
1722
0.00% covered (danger)
0.00%
0 / 105
 __construct
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 7
 setExcludePaths
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 setPsr4Namespaces
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 shouldExclude
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 4
 forceClassPath
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 readFile
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 23
 readDir
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 7
 generateJsonAutoload
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 10
 generatePHPAutoload
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 23
 getAutoload
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getTargetFileinfo
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 normalizePathSeparator
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 initMediaWikiDefault
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
<?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */
/**
 * Accepts a list of files and directories to search for
 * php files and generates $wgAutoloadLocalClasses or $wgAutoloadClasses
 * lines for all detected classes. These lines are written out
 * to an autoload.php file in the projects provided basedir.
 *
 * Usage:
 *
 *     $gen = new AutoloadGenerator( __DIR__ );
 *     $gen->readDir( __DIR__ . '/includes' );
 *     $gen->readFile( __DIR__ . '/foo.php' )
 *     $gen->getAutoload();
 */
class AutoloadGenerator {
    private const FILETYPE_JSON = 'json';
    private const FILETYPE_PHP = 'php';
    /**
     * @var string Root path of the project being scanned for classes
     */
    protected $basepath;
    /**
     * @var ClassCollector Helper class extracts class names from php files
     */
    protected $collector;
    /**
     * @var array Map of file shortpath to list of FQCN detected within file
     */
    protected $classes = [];
    /**
     * @var string The global variable to write output to
     */
    protected $variableName = 'wgAutoloadClasses';
    /**
     * @var array Map of FQCN to relative path(from self::$basepath)
     */
    protected $overrides = [];
    /**
     * Directories that should be excluded
     *
     * @var string[]
     */
    protected $excludePaths = [];
    /**
     * Configured PSR4 namespaces
     *
     * @var string[] namespace => path
     */
    protected $psr4Namespaces = [];
    /**
     * @param string $basepath Root path of the project being scanned for classes
     * @param array|string $flags
     *
     *  local - If this flag is set $wgAutoloadLocalClasses will be build instead
     *          of $wgAutoloadClasses
     */
    public function __construct( $basepath, $flags = [] ) {
        if ( !is_array( $flags ) ) {
            $flags = [ $flags ];
        }
        $this->basepath = self::normalizePathSeparator( realpath( $basepath ) );
        $this->collector = new ClassCollector;
        if ( in_array( 'local', $flags ) ) {
            $this->variableName = 'wgAutoloadLocalClasses';
        }
    }
    /**
     * Directories that should be excluded
     *
     * @since 1.31
     * @param string[] $paths
     */
    public function setExcludePaths( array $paths ) {
        foreach ( $paths as $path ) {
            $this->excludePaths[] = self::normalizePathSeparator( $path );
        }
    }
    /**
     * Unlike self::setExcludePaths(), this will only skip outputting the
     * autoloader entry when the namespace matches the path.
     *
     * @since 1.32
     * @param string[] $namespaces Associative array mapping namespace to path
     */
    public function setPsr4Namespaces( array $namespaces ) {
        foreach ( $namespaces as $ns => $path ) {
            $ns = rtrim( $ns, '\\' ) . '\\';
            $this->psr4Namespaces[$ns] = rtrim( self::normalizePathSeparator( $path ), '/' );
        }
    }
    /**
     * Whether the file should be excluded
     *
     * @param string $path File path
     * @return bool
     */
    private function shouldExclude( $path ) {
        foreach ( $this->excludePaths as $dir ) {
            if ( strpos( $path, $dir ) === 0 ) {
                return true;
            }
        }
        return false;
    }
    /**
     * Force a class to be autoloaded from a specific path, regardless of where
     * or if it was detected.
     *
     * @param string $fqcn FQCN to force the location of
     * @param string $inputPath Full path to the file containing the class
     * @throws Exception
     */
    public function forceClassPath( $fqcn, $inputPath ) {
        $path = self::normalizePathSeparator( realpath( $inputPath ) );
        if ( !$path ) {
            throw new \Exception( "Invalid path: $inputPath" );
        }
        $len = strlen( $this->basepath );
        if ( substr( $path, 0, $len ) !== $this->basepath ) {
            throw new \Exception( "Path is not within basepath: $inputPath" );
        }
        $shortpath = substr( $path, $len );
        $this->overrides[$fqcn] = $shortpath;
    }
    /**
     * @param string $inputPath Path to a php file to find classes within
     * @throws Exception
     */
    public function readFile( $inputPath ) {
        // NOTE: do NOT expand $inputPath using realpath(). It is perfectly
        // reasonable for LocalSettings.php and similiar files to be symlinks
        // to files that are outside of $this->basepath.
        $inputPath = self::normalizePathSeparator( $inputPath );
        $len = strlen( $this->basepath );
        if ( substr( $inputPath, 0, $len ) !== $this->basepath ) {
            throw new \Exception( "Path is not within basepath: $inputPath" );
        }
        if ( $this->shouldExclude( $inputPath ) ) {
            return;
        }
        $result = $this->collector->getClasses(
            file_get_contents( $inputPath )
        );
        // Filter out classes that will be found by PSR4
        $result = array_filter( $result, function ( $class ) use ( $inputPath ) {
            $parts = explode( '\\', $class );
            for ( $i = count( $parts ) - 1; $i > 0; $i-- ) {
                $ns = implode( '\\', array_slice( $parts, 0, $i ) ) . '\\';
                if ( isset( $this->psr4Namespaces[$ns] ) ) {
                    $expectedPath = $this->psr4Namespaces[$ns] . '/'
                        . implode( '/', array_slice( $parts, $i ) )
                        . '.php';
                    if ( $inputPath === $expectedPath ) {
                        return false;
                    }
                }
            }
            return true;
        } );
        if ( $result ) {
            $shortpath = substr( $inputPath, $len );
            $this->classes[$shortpath] = $result;
        }
    }
    /**
     * @param string $dir Path to a directory to recursively search for php files
     */
    public function readDir( $dir ) {
        $it = new RecursiveDirectoryIterator(
            self::normalizePathSeparator( realpath( $dir ) ) );
        $it = new RecursiveIteratorIterator( $it );
        foreach ( $it as $path => $file ) {
            if ( pathinfo( $path, PATHINFO_EXTENSION ) === 'php' ) {
                $this->readFile( $path );
            }
        }
    }
    /**
     * Updates the AutoloadClasses field at the given
     * filename.
     *
     * @param string $filename Filename of JSON
     *  extension/skin registration file
     * @return string Updated Json of the file given as the $filename parameter
     */
    protected function generateJsonAutoload( $filename ) {
        $key = 'AutoloadClasses';
        $json = FormatJson::decode( file_get_contents( $filename ), true );
        unset( $json[$key] );
        // Inverting the key-value pairs so that they become of the
        // format class-name : path when they get converted into json.
        foreach ( $this->classes as $path => $contained ) {
            foreach ( $contained as $fqcn ) {
                // Using substr to remove the leading '/'
                $json[$key][$fqcn] = substr( $path, 1 );
            }
        }
        foreach ( $this->overrides as $path => $fqcn ) {
            // Using substr to remove the leading '/'
            $json[$key][$fqcn] = substr( $path, 1 );
        }
        // Sorting the list of autoload classes.
        ksort( $json[$key] );
        // Return the whole JSON file
        return FormatJson::encode( $json, "\t", FormatJson::ALL_OK ) . "\n";
    }
    /**
     * Generates a PHP file setting up autoload information.
     *
     * @param string $commandName Command name to include in comment
     * @param string $filename of PHP file to put autoload information in.
     * @return string
     */
    protected function generatePHPAutoload( $commandName, $filename ) {
        // No existing JSON file found; update/generate PHP file
        $content = [];
        // We need to generate a line each rather than exporting the
        // full array so __DIR__ can be prepended to all the paths
        $format = "%s => __DIR__ . %s,";
        foreach ( $this->classes as $path => $contained ) {
            $exportedPath = var_export( $path, true );
            foreach ( $contained as $fqcn ) {
                $content[$fqcn] = sprintf(
                    $format,
                    var_export( $fqcn, true ),
                    $exportedPath
                );
            }
        }
        foreach ( $this->overrides as $fqcn => $path ) {
            $content[$fqcn] = sprintf(
                $format,
                var_export( $fqcn, true ),
                var_export( $path, true )
            );
        }
        // sort for stable output
        ksort( $content );
        // extensions using this generator are appending to the existing
        // autoload.
        if ( $this->variableName === 'wgAutoloadClasses' ) {
            $op = '+=';
        } else {
            $op = '=';
        }
        $output = implode( "\n\t", $content );
        return <<<EOD
<?php
// This file is generated by $commandName, do not adjust manually
// phpcs:disable Generic.Files.LineLength
global \${$this->variableName};
\${$this->variableName} {$op} [
    {$output}
];
EOD;
    }
    /**
     * Returns all known classes as a string, which can be used to put into a target
     * file (e.g. extension.json, skin.json or autoload.php)
     *
     * @param string $commandName Value used in file comment to direct
     *  developers towards the appropriate way to update the autoload.
     * @return string
     */
    public function getAutoload( $commandName = 'AutoloadGenerator' ) {
        // We need to check whether an extension.json or skin.json exists or not, and
        // incase it doesn't, update the autoload.php file.
        $fileinfo = $this->getTargetFileinfo();
        if ( $fileinfo['type'] === self::FILETYPE_JSON ) {
            return $this->generateJsonAutoload( $fileinfo['filename'] );
        }
        return $this->generatePHPAutoload( $commandName, $fileinfo['filename'] );
    }
    /**
     * Returns the filename of the extension.json of skin.json, if there's any, or
     * otherwise the path to the autoload.php file in an array as the "filename"
     * key and with the type (AutoloadGenerator::FILETYPE_JSON or AutoloadGenerator::FILETYPE_PHP)
     * of the file as the "type" key.
     *
     * @return array
     */
    public function getTargetFileinfo() {
        if ( file_exists( $this->basepath . '/extension.json' ) ) {
            return [
                'filename' => $this->basepath . '/extension.json',
                'type' => self::FILETYPE_JSON
            ];
        }
        if ( file_exists( $this->basepath . '/skin.json' ) ) {
            return [
                'filename' => $this->basepath . '/skin.json',
                'type' => self::FILETYPE_JSON
            ];
        }
        return [
            'filename' => $this->basepath . '/autoload.php',
            'type' => self::FILETYPE_PHP
        ];
    }
    /**
     * Ensure that Unix-style path separators ("/") are used in the path.
     *
     * @param string $path
     * @return string
     */
    protected static function normalizePathSeparator( $path ) {
        return str_replace( '\\', '/', $path );
    }
    /**
     * Initialize the source files and directories which are used for the MediaWiki default
     * autoloader in {mw-base-dir}/autoload.php including:
     *  * includes/
     *  * languages/
     *  * maintenance/
     *  * mw-config/
     *  * any `*.php` file in the base directory
     */
    public function initMediaWikiDefault() {
        foreach ( [ 'includes', 'languages', 'maintenance', 'mw-config' ] as $dir ) {
            $this->readDir( $this->basepath . '/' . $dir );
        }
        foreach ( glob( $this->basepath . '/*.php' ) as $file ) {
            $this->readFile( $file );
        }
    }
}