>> $checksums Validated official checksums. * @return array */ public function inspect( string $root_path, array $checksums ): array { $root = realpath( $root_path ); if ( false === $root || ! is_dir( $root ) || is_link( $root_path ) ) { throw new \InvalidArgumentException( 'plugin_checksum_root_invalid' ); } $root = rtrim( wp_normalize_path( $root ), '/' ); $expected = array(); $invalid = 0; foreach ( $checksums as $relative => $algorithms ) { if ( ! $this->safe_relative_path( $relative ) || ! is_array( $algorithms ) ) { ++$invalid; continue; } $expected[ $relative ] = $algorithms; } ksort( $expected ); $matched = 0; $missing = array(); $mismatched = array(); $missing_count = 0; $mismatched_count = 0; $skipped = $invalid; foreach ( array_slice( $expected, 0, self::MAX_FILES, true ) as $relative => $algorithms ) { $path = $root . '/' . $relative; if ( is_link( $path ) ) { ++$skipped; continue; } if ( ! file_exists( $path ) ) { ++$missing_count; $this->append( $missing, $relative ); continue; } $real = realpath( $path ); if ( false === $real || 0 !== strpos( wp_normalize_path( $real ), $root . '/' ) || ! is_file( $real ) || ! is_readable( $real ) ) { ++$skipped; continue; } if ( $this->matches( $real, $algorithms ) ) { ++$matched; } else { ++$mismatched_count; $this->append( $mismatched, $relative ); } } if ( count( $expected ) > self::MAX_FILES ) { $skipped += count( $expected ) - self::MAX_FILES; } $additional = $this->additional_files( $root, $expected ); return array( 'matched' => $matched, 'missing' => $missing, 'mismatched' => $mismatched, 'additional' => $additional['paths'], 'missing_count' => $missing_count, 'mismatched_count' => $mismatched_count, 'additional_count' => $additional['count'], 'skipped_count' => $skipped + $additional['skipped'], 'expected_count' => min( count( $expected ), self::MAX_FILES ), 'lists_truncated' => $missing_count > count( $missing ) || $mismatched_count > count( $mismatched ) || $additional['count'] > count( $additional['paths'] ), ); } /** * @param array> $algorithms Checksums by algorithm. */ private function matches( string $path, array $algorithms ): bool { if ( isset( $algorithms['sha256'] ) ) { // Read-only hash of a validated path inside the selected plugin root. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_hash_file $value = hash_file( 'sha256', $path ); return is_string( $value ) && in_array( strtolower( $value ), $algorithms['sha256'], true ); } if ( isset( $algorithms['md5'] ) ) { // Read-only hash of a validated path inside the selected plugin root. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_md5_file $value = md5_file( $path ); return is_string( $value ) && in_array( strtolower( $value ), $algorithms['md5'], true ); } return false; } /** * @param array>> $expected Expected files. * @return array{paths:list,count:int,skipped:int} */ private function additional_files( string $root, array $expected ): array { $paths = array(); $count = 0; $skipped = 0; $seen = 0; try { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::LEAVES_ONLY ); foreach ( $iterator as $entry ) { if ( ++$seen > self::MAX_FILES ) { ++$skipped; break; } if ( $entry->isLink() || ! $entry->isFile() ) { ++$skipped; continue; } $path = wp_normalize_path( $entry->getPathname() ); if ( 0 !== strpos( $path, $root . '/' ) ) { ++$skipped; continue; } $relative = substr( $path, strlen( $root ) + 1 ); if ( ! isset( $expected[ $relative ] ) ) { ++$count; $this->append( $paths, $relative ); } } } catch ( \Throwable $exception ) { unset( $exception ); ++$skipped; } return array( 'paths' => $paths, 'count' => $count, 'skipped' => $skipped, ); } private function safe_relative_path( string $path ): bool { $normalized = str_replace( '\\', '/', $path ); return strlen( $path ) <= 512 && '' !== $path && $normalized === $path && '/' !== $path[0] && false === strpos( '/' . $normalized . '/', '/../' ) && false === strpos( '/' . $normalized . '/', '/./' ) && false === strpos( $normalized, "\0" ); } /** * @param list $items Bounded relative paths. */ private function append( array &$items, string $value ): void { if ( count( $items ) < self::MAX_LIST ) { $bounded = substr( $value, 0, 512 ); if ( false !== $bounded ) { $items[] = $bounded; } } } }