array( '' ), 'dir' => null, 'offset' => 0, 'files' => 0, ); } /** * Walks until the deadline (at least one file) or the end. * * @param array $walk Walk state, updated. * @param Context $ctx Context. * @param PassAccumulator $acc Accumulator. * @param float $deadline microtime(true) limit. * @param array $batch Collected items for ahcl_batch_collected, appended. * @return bool True when the walk is over. */ public static function run( array &$walk, Context $ctx, PassAccumulator $acc, $deadline, array &$batch ) { $root = untrailingslashit( Site::abspath() ); $processed = 0; $listeners = has_action( 'ahcl_batch_collected' ); while ( true ) { if ( null === $walk['dir'] ) { if ( ! $walk['stack'] ) { return true; } $walk['dir'] = array_pop( $walk['stack'] ); $walk['offset'] = 0; } $dir = $walk['dir']; $abs = '' === $dir ? $root : $root . '/' . $dir; $entries = is_readable( $abs ) ? scandir( $abs ) : false; if ( false === $entries ) { $acc->addNotTraversed( '' === $dir ? '/' : $dir ); $walk['dir'] = null; continue; } $entries = array_values( array_diff( $entries, array( '.', '..' ) ) ); $count = count( $entries ); for ( $i = $walk['offset']; $i < $count; $i++ ) { if ( $processed > 0 && microtime( true ) >= $deadline ) { $walk['offset'] = $i; return false; } $name = $entries[ $i ]; $rel = '' === $dir ? $name : $dir . '/' . $name; $full = $abs . '/' . $name; if ( is_link( $full ) ) { if ( is_dir( $full ) ) { $acc->addNotTraversed( $rel ); } else { $acc->addFile( FileEntry::fromArray( array( 'path' => $rel, 'readable' => false, ) ), $ctx ); } continue; } if ( is_dir( $full ) ) { $walk['stack'][] = $rel; continue; } if ( ! is_file( $full ) ) { continue; } $entry = self::read( $full, $rel, $ctx ); $acc->addFile( $entry, $ctx ); ++$processed; ++$walk['files']; if ( $listeners ) { $batch[] = self::batchItem( $entry, $full ); } } $walk['dir'] = null; } } /** * Item of ahcl_batch_collected for one file: the md5 already read, else computed here, * except above Assumptions::HASH_MAX_BYTES, where md5 stays null (D-05 holds with a listener). * * @param FileEntry $entry File as read. * @param string $full Absolute path. * @return array{path:string, md5:?string, size:int} */ private static function batchItem( FileEntry $entry, $full ) { $md5 = $entry->md5; if ( null === $md5 && $entry->readable && $entry->size <= Assumptions::HASH_MAX_BYTES ) { $md5 = md5_file( $full ); } return array( 'path' => $entry->path, 'md5' => false === $md5 ? null : $md5, 'size' => $entry->size, ); } /** * Reads what the domain asks for this file (FileInspector::needs). * * @param string $full Absolute path. * @param string $rel Path relative to ABSPATH. * @param Context $ctx Context. * @return FileEntry */ private static function read( $full, $rel, Context $ctx ) { $size = (int) filesize( $full ); $mtime = filemtime( $full ); $a = array( 'path' => $rel, 'size' => $size, 'readable' => is_readable( $full ), 'mtime' => false === $mtime ? null : $mtime, ); if ( ! $a['readable'] ) { return FileEntry::fromArray( $a ); } $needs = FileInspector::needs( $rel, $size, $ctx ); if ( $needs['md5'] ) { $md5 = md5_file( $full ); if ( false === $md5 ) { $a['readable'] = false; return FileEntry::fromArray( $a ); } $a['md5'] = $md5; } if ( $needs['sha256'] ) { $sha = hash_file( 'sha256', $full ); if ( false !== $sha ) { $a['sha256'] = $sha; } } if ( $needs['content'] && $size <= Assumptions::CONTENT_MAX_BYTES ) { $content = file_get_contents( $full ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file of the site, read only. if ( false === $content ) { $a['readable'] = false; return FileEntry::fromArray( $a ); } $a['content'] = $content; } elseif ( $needs['head'] ) { $head = file_get_contents( $full, false, null, 0, Assumptions::HEAD_BYTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file of the site, read only. if ( false === $head ) { $a['readable'] = false; return FileEntry::fromArray( $a ); } $a['head'] = $head; } return FileEntry::fromArray( $a ); } }