open( $path ) ) { return false; } $has_plugin_header = false; for ( $i = 0; $i < $zip->numFiles; $i++ ) { $name = $zip->getNameIndex( $i ); if ( ! is_string( $name ) || substr( $name, -4 ) !== '.php' ) { continue; } // Only `/.php`. WordPress's own `Plugin_Upgrader::check_package()` // looks for the header at the top level of the extracted folder and rejects an // archive whose only headed file is nested deeper — so accepting one here would // pass the validator and still hard-fail the install. $parts = explode( '/', trim( $name, '/' ) ); if ( 2 !== count( $parts ) ) { continue; } // Right shape, wrong product — see the docblock. if ( is_string( $expected_dir ) && '' !== $expected_dir && $parts[0] !== $expected_dir ) { continue; } // Plugin headers must be within the first 8KB for WordPress to read them, so // there is no reason to pull a whole file into memory to look for one. $contents = $zip->getFromIndex( $i, 8192 ); if ( is_string( $contents ) && preg_match( '/^[ \t\/*#@]*Plugin Name:\s*\S/mi', $contents ) ) { $has_plugin_header = true; break; } } $zip->close(); return $has_plugin_header; } /** * Delete a transient archive, ignoring a file that is already gone. * * Nothing licensed may survive the request that downloaded it (FR-024), so this is * called on BOTH the success and the failure branch of installation. * * @param string|null $path */ public static function discard( $path ): void { if ( is_string( $path ) && '' !== $path && file_exists( $path ) ) { @unlink( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a vanished temp file is not an error. } } }