get_upload_dir(); return trailingslashit( $upload_dir['basedir'] ) . self::FEED_DIR . '/'; } /** * Get the absolute path for a specific feed file. * * @param string $filename The feed filename. * * @return string Absolute file path. */ public function get_feed_path( string $filename ): string { return $this->get_feed_dir() . sanitize_file_name( $filename ); } /** * Get the public URL to the feed directory. * * @return string Public URL (with trailing slash). */ public function get_feed_url( string $filename = '' ): string { $upload_dir = $this->get_upload_dir(); $base = trailingslashit( $upload_dir['baseurl'] ) . self::FEED_DIR . '/'; if ( '' !== $filename ) { return $base . sanitize_file_name( $filename ); } return $base; } /** * Ensure the feed directory exists and is protected. * * Creates .htaccess with Options -Indexes and index.php. * * @return bool True if directory is ready. */ public function ensure_directory(): bool { $dir = $this->get_feed_dir(); if ( ! file_exists( $dir ) ) { wp_mkdir_p( $dir ); } $filesystem = $this->filesystem(); if ( null === $filesystem ) { return is_dir( $dir ) && wp_is_writable( $dir ); } // Add index.php to prevent directory listing. $index_file = $dir . 'index.php'; if ( ! file_exists( $index_file ) ) { $filesystem->put_contents( $index_file, "put_contents( $htaccess_file, "Options -Indexes\n", FS_CHMOD_FILE ); } return is_dir( $dir ) && wp_is_writable( $dir ); } /** * Initialize and return the WordPress filesystem abstraction. * * @return \WP_Filesystem_Base|null The filesystem, or null if unavailable. */ private function filesystem(): ?\WP_Filesystem_Base { global $wp_filesystem; if ( ! $wp_filesystem instanceof \WP_Filesystem_Base ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } return $wp_filesystem instanceof \WP_Filesystem_Base ? $wp_filesystem : null; } /** * Write feed content to a file with LOCK_EX. * * Uses atomic write: writes to a temp file first, then renames. * * @param string $filename The filename (e.g., 'google-feed.xml'). * @param string $content The feed content. * * @return array{path: string, url: string, size: int}|false File info or false on failure. */ public function write_feed( string $filename, string $content ) { if ( ! $this->ensure_directory() ) { return false; } $filesystem = $this->filesystem(); if ( null === $filesystem ) { return false; } $file_path = $this->get_feed_path( $filename ); $temp_path = $file_path . '.tmp'; // Atomic write: write to a temp file, then move it into place. if ( ! $filesystem->put_contents( $temp_path, $content, FS_CHMOD_FILE ) ) { return false; } if ( ! $filesystem->move( $temp_path, $file_path, true ) ) { $filesystem->delete( $temp_path ); return false; } return array( 'path' => $file_path, 'url' => $this->get_feed_url( $filename ), 'size' => (int) $filesystem->size( $file_path ), ); } /** * Delete a feed file. * * @param string $filename The filename to delete. * * @return bool True if deleted or didn't exist. */ public function delete_feed( string $filename ): bool { $file_path = $this->get_feed_path( $filename ); if ( file_exists( $file_path ) ) { return wp_delete_file( $file_path ) !== false; } return true; } /** * Get the filename for a feed type. * * @param string $feed_type The feed type identifier. * @param string $extension File extension (default: xml). * * @return string The filename. */ public function get_filename( string $feed_type, string $extension = 'xml' ): string { return sanitize_file_name( $feed_type . '-feed.' . $extension ); } /** * Get file hash for integrity checks. * * @param string $filename The filename. * * @return string|null SHA-256 hash or null if file doesn't exist. */ public function get_file_hash( string $filename ): ?string { $file_path = $this->get_feed_path( $filename ); if ( ! file_exists( $file_path ) ) { return null; } return hash_file( 'sha256', $file_path ); } /** * List all feed files in the directory. * * @return array Array of file info arrays. */ public function list_feeds(): array { $dir = $this->get_feed_dir(); if ( ! is_dir( $dir ) ) { return array(); } $feeds = array(); $files = glob( $dir . '*-feed.*' ); if ( ! is_array( $files ) ) { return array(); } foreach ( $files as $file ) { $filename = basename( $file ); $feeds[] = array( 'filename' => $filename, 'path' => $file, 'url' => $this->get_feed_url( $filename ), 'size' => filesize( $file ), 'modified' => gmdate( 'c', filemtime( $file ) ), ); } return $feeds; } }