,version:string,slug:string}> */ public static function discover( Roots $roots, string $version ): array { $units = array(); if ( '' !== $roots->path( Roots::CORE ) ) { $units[] = self::unit( self::CORE, Roots::CORE, array( array( '', false, false ), array( 'wp-admin', true, true ), array( 'wp-includes', true, true ), ), $version, '' ); } $plugins = $roots->path( Roots::PLUGINS ); if ( '' !== $plugins ) { foreach ( self::directories( $plugins ) as $slug ) { $units[] = self::unit( 'plugin:' . $slug, Roots::PLUGINS, array( array( $slug, true, true ) ), self::plugin_version( $plugins . $slug ), $slug ); } $units[] = self::unit( self::PLUGINS_ROOT, Roots::PLUGINS, array( array( '', false, true ) ), '', '' ); } $themes = $roots->path( Roots::THEMES ); if ( '' !== $themes ) { foreach ( self::directories( $themes ) as $slug ) { $units[] = self::unit( 'theme:' . $slug, Roots::THEMES, array( array( $slug, true, true ) ), self::header( $themes . $slug . '/style.css', 'Version' ), $slug ); } } if ( '' !== $roots->path( Roots::UPLOADS ) ) { $units[] = self::unit( self::UPLOADS, Roots::UPLOADS, array( array( '', true, true ) ), '', '' ); } return $units; } /** * Whether a unit's files may come from wordpress.org's plugin checksums. * * @param string $key Unit key. */ public static function is_plugin( string $key ): bool { return 0 === strpos( $key, 'plugin:' ); } /** * Whether a unit is compared against a baseline the site recorded * (rather than only against wordpress.org or only for executables). * * @param string $key Unit key. */ public static function may_baseline( string $key ): bool { return self::CORE !== $key && self::UPLOADS !== $key; } /** * @param string $key Unit key. * @param string $root Root key. * @param array $start Walk start. * @param string $version Version. * @param string $slug Package slug. * @return array{key:string,root:string,start:array,version:string,slug:string} */ private static function unit( string $key, string $root, array $start, string $version, string $slug ): array { return array( 'key' => $key, 'root' => $root, 'start' => $start, 'version' => $version, 'slug' => $slug, ); } /** * Real (non-link) subdirectories, sorted, capped. A directory name is a * path segment of every row the unit writes, so one that is not a safe * segment is not a unit (it cannot be confined). * * @param string $root Directory with a trailing slash. * @return array */ private static function directories( string $root ): array { $names = scandir( $root ); $dirs = array(); foreach ( is_array( $names ) ? $names : array() as $name ) { // Hidden directories are units too: WordPress will not load a // plugin from one, but a web server will happily serve a script // out of it. if ( '.' === $name || '..' === $name || in_array( $name, Comparison::SKIPPED_DIRECTORIES, true ) ) { continue; } if ( Comparison::is_safe_relative( $name ) && is_dir( $root . $name ) && ! is_link( $root . $name ) ) { $dirs[] = $name; } if ( count( $dirs ) >= self::MAX_PACKAGES ) { break; } } return $dirs; } /** * A plugin directory's version: the `Version:` header of the first * top-level PHP file carrying a `Plugin Name:` header, as WordPress * finds a plugin's main file. * * @param string $dir Plugin directory, no trailing slash. */ private static function plugin_version( string $dir ): string { $files = glob( $dir . '/*.php' ); foreach ( array_slice( is_array( $files ) ? $files : array(), 0, 50 ) as $file ) { if ( is_link( $file ) || ! is_file( $file ) ) { continue; } $data = get_file_data( $file, array( 'name' => 'Plugin Name', 'version' => 'Version', ) ); if ( '' !== (string) $data['name'] ) { return self::clean_version( (string) $data['version'] ); } } return ''; } /** * @param string $file File. * @param string $header Header name. */ private static function header( string $file, string $header ): string { if ( ! is_file( $file ) || is_link( $file ) ) { return ''; } $data = get_file_data( $file, array( 'value' => $header ) ); return self::clean_version( (string) $data['value'] ); } /** * A version as it may appear in a URL path segment: wordpress.org's own * version strings are digits, dots and a few letters. * * @param string $version Raw header value. */ public static function clean_version( string $version ): string { $version = trim( $version ); return 1 === preg_match( '/^[0-9A-Za-z][0-9A-Za-z.\-+_]{0,39}$/', $version ) ? $version : ''; } }