has_client_config() && $oauth->is_connected() && '' !== Settings::instance()->get_folder_id( 'incoming' ); } /** * @inheritDoc */ public function resolve_folders( string $root_id ) { $client = new Google_Drive_Client( Google_OAuth::instance() ); $incoming = $client->find_or_create_folder( $root_id, 'incoming' ); if ( is_wp_error( $incoming ) ) { return $incoming; } $published = $client->find_or_create_folder( $root_id, 'published' ); if ( is_wp_error( $published ) ) { return $published; } $failed = $client->find_or_create_folder( $root_id, 'failed' ); if ( is_wp_error( $failed ) ) { return $failed; } return array( 'root' => $root_id, 'incoming' => $incoming, 'published' => $published, 'failed' => $failed, ); } /** * Browse incoming / published / failed — one listing per role folder (lazy children). * * @return array{folders: array>, files: array>}|WP_Error */ public function browse_tree() { if ( ! $this->is_ready() ) { return new WP_Error( 'forwp_drive_not_ready', __( 'Google Drive is not configured.', '4wp-drive' ) ); } $settings = Settings::instance(); $client = new Google_Drive_Client( Google_OAuth::instance() ); $root = array( 'folders' => array(), 'files' => array(), ); foreach ( array( 'incoming', 'published', 'failed' ) as $role ) { $id = $settings->get_folder_id( $role ); $root['folders'][ $role ] = $this->list_browse_level( $client, $id, $role, $role, $role ); } return $root; } /** * One folder listing for lazy tree expand. * * @param string $folder_id Drive folder id. * @param string $path Tree path (incoming/…). * @param string $name Display name. * @return array|WP_Error */ public function browse_folder( string $folder_id, string $path = '', string $name = '' ) { if ( ! $this->is_ready() ) { return new WP_Error( 'forwp_drive_not_ready', __( 'Google Drive is not configured.', '4wp-drive' ) ); } $folder_id = sanitize_text_field( $folder_id ); if ( '' === $folder_id ) { return new WP_Error( 'forwp_drive_browse_folder', __( 'Missing folder id.', '4wp-drive' ) ); } $path = trim( str_replace( '\\', '/', $path ), '/' ); $name = '' !== $name ? $name : ( '' !== $path ? basename( $path ) : $folder_id ); $client = new Google_Drive_Client( Google_OAuth::instance() ); return $this->list_browse_level( $client, $folder_id, $name, '', $path ); } /** * @return array */ private function list_browse_level( Google_Drive_Client $client, string $folder_id, string $name, string $role, string $path ): array { $path = '' !== $path ? $path : $name; $node = array( 'name' => $name, 'path' => $path, 'role' => $role, 'id' => $folder_id, 'lazy' => false, 'loaded' => true, 'folders' => array(), 'files' => array(), ); if ( '' === $folder_id ) { return $node; } $entries = $client->list_folder_entries( $folder_id ); if ( is_wp_error( $entries ) ) { $node['error'] = $entries->get_error_message(); return $node; } foreach ( (array) ( $entries['folders'] ?? array() ) as $folder ) { if ( ! is_array( $folder ) ) { continue; } $child_id = (string) ( $folder['id'] ?? '' ); $child_name = (string) ( $folder['name'] ?? '' ); if ( '' === $child_id || '' === $child_name ) { continue; } $child_path = '' !== $path ? $path . '/' . $child_name : $child_name; $node['folders'][ $child_name ] = array( 'name' => $child_name, 'path' => $child_path, 'role' => '', 'id' => $child_id, 'lazy' => true, 'loaded' => false, 'folders' => array(), 'files' => array(), ); } foreach ( (array) ( $entries['files'] ?? array() ) as $file ) { if ( ! is_array( $file ) ) { continue; } $node['files'][] = array( 'id' => (string) ( $file['id'] ?? '' ), 'name' => (string) ( $file['name'] ?? '' ), 'kind' => (string) ( $file['kind'] ?? 'file' ), ); } return $node; } /** * @inheritDoc */ public function scan_incoming() { if ( ! $this->is_ready() ) { return new WP_Error( 'forwp_drive_not_ready', __( 'Google Drive is not configured.', '4wp-drive' ) ); } $settings = Settings::instance(); $incoming_id = $settings->get_folder_id( 'incoming' ); $client = new Google_Drive_Client( Google_OAuth::instance() ); $parser = new Template_Parser(); $results = array(); $seen_docs = array(); if ( '' === $incoming_id ) { return array(); } $incoming_folders = $client->list_child_folders( $incoming_id, array() ); if ( is_wp_error( $incoming_folders ) ) { return $incoming_folders; } foreach ( $incoming_folders as $folder ) { $item = $this->scan_package_folder( $client, $parser, (string) $folder['id'], (string) $folder['name'] ); if ( null === $item ) { continue; } $file_id = (string) ( $item['file_id'] ?? '' ); if ( '' === $file_id || isset( $seen_docs[ $file_id ] ) ) { continue; } $seen_docs[ $file_id ] = true; $results[] = $item; } $flat = $client->list_documents_in_folder( $incoming_id ); if ( is_wp_error( $flat ) ) { return $flat; } foreach ( $flat as $file ) { $file_id = (string) ( $file['id'] ?? '' ); if ( '' === $file_id || isset( $seen_docs[ $file_id ] ) ) { continue; } $item = $this->scan_document_file( $client, $parser, $file_id, (string) ( $file['name'] ?? '' ), '', null, (string) ( $file['mimeType'] ?? '' ) ); if ( null !== $item ) { $seen_docs[ $file_id ] = true; $results[] = $item; } } return $results; } /** * @inheritDoc */ public function move_after_import( string $file_id, string $target_role, array $metadata = array() ) { $folders = Settings::instance()->get_folder_ids(); $to = $folders[ $target_role ] ?? ''; if ( '' === $to ) { return new WP_Error( 'forwp_drive_folders', __( 'Folder mapping is incomplete.', '4wp-drive' ) ); } $client = new Google_Drive_Client( Google_OAuth::instance() ); $package_folder_id = (string) ( $metadata['package_folder_id'] ?? '' ); $image_file_id = (string) ( $metadata['image_file_id'] ?? '' ); $slug = (string) ( $metadata['slug'] ?? '' ); if ( '' !== $image_file_id && '' !== $slug && 'published' === $target_role ) { $image_name = (string) ( $metadata['image_file_name'] ?? '' ); $drive_name = Featured_Image_Importer::build_filename( $slug, $image_name ); $renamed = $client->update_file_name( $image_file_id, $drive_name ); if ( is_wp_error( $renamed ) ) { return $renamed; } } $from = $folders['incoming'] ?? ''; if ( '' === $from ) { return new WP_Error( 'forwp_drive_folders', __( 'Folder mapping is incomplete.', '4wp-drive' ) ); } // Reject/fail: move only the rejected document, never the whole package folder. if ( 'failed' === $target_role ) { return $client->move_file( $file_id, $to, $from ); } if ( '' !== $package_folder_id && $package_folder_id !== ( $folders['incoming'] ?? '' ) ) { $remove_from = $this->resolve_remove_parent( $client, $package_folder_id, $folders ); return is_wp_error( $remove_from ) ? $remove_from : $client->move_file( $package_folder_id, $to, $remove_from ); } $moved = $client->move_file( $file_id, $to, $from ); if ( is_wp_error( $moved ) ) { return $moved; } if ( '' !== $image_file_id && $from ) { $image_moved = $client->move_file( $image_file_id, $to, $from ); if ( is_wp_error( $image_moved ) ) { return $image_moved; } } return true; } /** * Scan one article package folder (document + optional image). * * @param Google_Drive_Client $client Drive client. * @param Template_Parser $parser Template parser. * @param string $folder_id Package folder id. * @param string $folder_name Folder label for display. * @return array|null */ private function scan_package_folder( Google_Drive_Client $client, Template_Parser $parser, string $folder_id, string $folder_name ): ?array { $docs = $client->list_documents_in_folder( $folder_id ); if ( is_wp_error( $docs ) || empty( $docs ) ) { return null; } $doc = Importable_Document::pick_default( $docs ); if ( ! is_array( $doc ) ) { $doc = $docs[0]; } $doc_id = (string) ( $doc['id'] ?? '' ); if ( '' === $doc_id ) { return null; } $images = $client->list_images_in_folder( $folder_id ); if ( is_wp_error( $images ) ) { $images = array(); } $package_files = array(); foreach ( $docs as $row ) { if ( ! is_array( $row ) ) { continue; } $name = (string) ( $row['name'] ?? '' ); $id = (string) ( $row['id'] ?? '' ); if ( '' === $name || '' === $id ) { continue; } $package_files[] = array( 'id' => $id, 'name' => $name, 'kind' => 'document', 'mime' => (string) ( $row['mimeType'] ?? '' ), ); } $image_rows = array(); foreach ( $images as $row ) { if ( ! is_array( $row ) ) { continue; } $name = (string) ( $row['name'] ?? '' ); $id = (string) ( $row['id'] ?? '' ); if ( '' === $name || '' === $id ) { continue; } $image_rows[] = array( 'id' => $id, 'name' => $name, ); $package_files[] = array( 'id' => $id, 'name' => $name, 'kind' => 'image', ); } $suggested = Featured_Image_Chooser::suggest( $image_rows ); $image = null; if ( null !== $suggested ) { $image = array( 'id' => $suggested['id'], 'name' => $suggested['name'], ); } return $this->scan_document_file( $client, $parser, $doc_id, (string) ( $doc['name'] ?? $folder_name ), $folder_id, $image, (string) ( $doc['mimeType'] ?? '' ), $package_files, $folder_name ); } /** * Export and parse a document; attach package/image metadata when present. * * @param Google_Drive_Client $client Drive client. * @param Template_Parser $parser Template parser. * @param string $file_id Document file id. * @param string $file_name Document filename. * @param string $package_folder_id Article subfolder in incoming (empty for flat docs). * @param array|null $image Drive image file row. * @param string $mime_type Document mime type. * @param array> $package_files Sibling files in the package folder. * @param string $package_folder_name Package folder label. * @return array|null */ private function scan_document_file( Google_Drive_Client $client, Template_Parser $parser, string $file_id, string $file_name, string $package_folder_id, ?array $image, string $mime_type = '', array $package_files = array(), string $package_folder_name = '' ): ?array { $raw = $client->fetch_document_content( $file_id, $mime_type, $file_name ); $meta_extra = array( 'image_file_id' => is_array( $image ) ? (string) ( $image['id'] ?? '' ) : '', 'image_file_name' => is_array( $image ) ? (string) ( $image['name'] ?? '' ) : '', 'selected_file_id' => $file_id, ); if ( '' !== $package_folder_id ) { $meta_extra['package_folder_id'] = $package_folder_id; } if ( '' !== $package_folder_name ) { $meta_extra['package_folder_name'] = $package_folder_name; } if ( ! empty( $package_files ) ) { $meta_extra['package_files'] = $package_files; } if ( is_wp_error( $raw ) ) { return array( 'file_id' => $file_id, 'file_name' => $file_name, 'content_hash' => 'export-error-' . md5( $file_id ), 'export_failed' => true, 'metadata' => array_merge( array( 'title' => $file_name ?: __( 'Untitled', '4wp-drive' ), 'scan_error' => $raw->get_error_message(), ), $meta_extra ), ); } $hash = md5( (string) $raw ); if ( is_array( $image ) && ! empty( $image['modifiedTime'] ) ) { $hash = md5( $hash . (string) $image['modifiedTime'] ); } if ( ! empty( $package_files ) ) { $hash = md5( $hash . wp_json_encode( $package_files ) ); } $meta = $parser->parse( (string) $raw ); if ( '' === $meta['title'] ) { $meta['title'] = $file_name ?: __( 'Untitled', '4wp-drive' ); } $meta = array_merge( $meta, $meta_extra ); return array( 'file_id' => $file_id, 'file_name' => $file_name, 'content_hash' => $hash, 'metadata' => $meta, ); } /** * Parent folder to remove when moving a package folder into published/failed. * * @param Google_Drive_Client $client Drive client. * @param string $folder_id Package folder id. * @param array $folders Configured folder ids. * @return string|WP_Error */ private function resolve_remove_parent( Google_Drive_Client $client, string $folder_id, array $folders ) { $file = $client->get_file( $folder_id ); if ( is_wp_error( $file ) ) { return $file; } $parents = isset( $file['parents'] ) && is_array( $file['parents'] ) ? $file['parents'] : array(); if ( ! empty( $parents[0] ) ) { return (string) $parents[0]; } $root = $folders['root'] ?? ''; if ( '' !== $root ) { return $root; } return new WP_Error( 'forwp_drive_parent', __( 'Could not determine Drive folder parent.', '4wp-drive' ) ); } /** * Re-parse a package using a chosen article file. * * @param array $item Original scan-shaped item (needs metadata.package_*). * @param string $file_id Selected Drive file id. * @return array|null */ public function rescan_source_file( array $item, string $file_id ): ?array { $meta = isset( $item['metadata'] ) && is_array( $item['metadata'] ) ? $item['metadata'] : array(); $files = isset( $meta['package_files'] ) && is_array( $meta['package_files'] ) ? $meta['package_files'] : array(); $found = null; foreach ( $files as $file ) { if ( is_array( $file ) && (string) ( $file['id'] ?? '' ) === $file_id && 'document' === ( $file['kind'] ?? '' ) ) { $found = $file; break; } } if ( ! is_array( $found ) ) { return null; } $image = null; if ( ! empty( $meta['image_file_id'] ) ) { $image = array( 'id' => (string) $meta['image_file_id'], 'name' => (string) ( $meta['image_file_name'] ?? '' ), ); } $client = new Google_Drive_Client( Google_OAuth::instance() ); $parser = new Template_Parser(); return $this->scan_document_file( $client, $parser, $file_id, (string) ( $found['name'] ?? '' ), (string) ( $meta['package_folder_id'] ?? '' ), $image, (string) ( $found['mime'] ?? '' ), $files, (string) ( $meta['package_folder_name'] ?? '' ) ); } }