list_objects(
array(
'prefix' => $prefix,
'continuation_token' => $token,
'max_keys' => 100,
)
);
?>
' . esc_html( $result->get_error_message() ) . '
';
return; }
?>
'r2clouder-files',
'prefix' => $prefix,
'token' => $result['next_token'],
),
admin_url( 'upload.php' )
)
) . '">' . esc_html__( 'Next page', 'devsyed-media-offloader' ) . ''; }
?>
100 ) {
wp_die( esc_html__( 'Select between 1 and 100 files.', 'devsyed-media-offloader' ) ); }
foreach ( $keys as $key ) {
if ( ! is_string( $key ) || is_wp_error( Object_Key::sanitize( $key ) ) ) {
wp_die( esc_html__( 'Invalid object key.', 'devsyed-media-offloader' ) ); }
}
$keys = array_unique( $keys );
$client = Storage::client();
if ( is_wp_error( $client ) ) {
wp_die( esc_html( $client->get_error_message() ) ); }
$operation = isset( $_POST['operation'] ) && is_string( $_POST['operation'] ) ? sanitize_key( $_POST['operation'] ) : '';
if ( 'zip' === $operation ) {
self::zip( $client, array_values( $keys ) );
return; }
if ( empty( $_POST['confirm'] ) ) {
wp_die( esc_html__( 'Confirm the move or deletion first.', 'devsyed-media-offloader' ) ); }
if ( 'delete' === $operation ) {
$attachments = array();
$standalone = array();
// Preflight every owner and permission before deleting anything.
foreach ( $keys as $key ) {
$owners = Managed_Files::owners( $client->get_bucket(), $key );
if ( is_wp_error( $owners ) ) {
wp_die( esc_html( $owners->get_error_message() ) ); }
if ( ! $owners ) {
$standalone[] = $key; }
foreach ( $owners as $id ) {
if ( empty( $_POST['delete_attachments'] ) || ! current_user_can( 'delete_post', $id ) || 'attachment' !== get_post_type( $id ) ) {
wp_die( esc_html__( 'Confirm deletion of entire managed attachments and ensure you can delete each attachment.', 'devsyed-media-offloader' ) );
}
$attachments[ $id ] = $id;
}
}
// Refuse shared variants that would leave another attachment broken.
foreach ( $attachments as $id ) {
foreach ( Offload_Meta::keys( Offload_Meta::get( $id ), true ) as $key ) {
$owners = Managed_Files::owners( $client->get_bucket(), $key );
if ( is_wp_error( $owners ) || array_diff( $owners, $attachments ) ) {
wp_die( esc_html__( 'An attachment shares cloud files with another attachment. Select all owners before deleting.', 'devsyed-media-offloader' ) );
}
}
}
foreach ( $attachments as $id ) {
if ( ! wp_delete_attachment( $id, true ) ) {
wp_die( esc_html__( 'WordPress could not delete an attachment. Remaining files were not deleted.', 'devsyed-media-offloader' ) );
}
}
foreach ( $standalone as $key ) {
$result = $client->delete( $key );
if ( is_wp_error( $result ) ) {
wp_die( esc_html( $result->get_error_message() ) ); }
}
} elseif ( 'move_attachment' === $operation && count( $keys ) === 1 && $client instanceof S3_Client ) {
$source = reset( $keys );
$owners = Managed_Files::owners( $client->get_bucket(), $source );
if ( is_wp_error( $owners ) || count( $owners ) !== 1 || ! in_array( $source, Offload_Meta::keys( Offload_Meta::get( $owners[0] ) ), true ) ) {
wp_die( esc_html__( 'Select one current file belonging to a single attachment.', 'devsyed-media-offloader' ) );
}
if ( isset( $_POST['folder'] ) && ! is_string( $_POST['folder'] ) ) {
wp_die( esc_html__( 'Invalid destination folder.', 'devsyed-media-offloader' ) );
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Object_Key validates the literal folder in move_attachment.
$folder = isset( $_POST['folder'] ) && is_string( $_POST['folder'] ) ? wp_unslash( $_POST['folder'] ) : '';
$result = Managed_Files::move_attachment( $owners[0], $client, $folder );
if ( is_wp_error( $result ) ) {
wp_die( esc_html( $result->get_error_message() ) );
}
} elseif ( 'move' === $operation && count( $keys ) === 1 && $client instanceof S3_Client ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated immediately below by Object_Key.
$destination = isset( $_POST['destination'] ) && is_string( $_POST['destination'] ) ? wp_unslash( $_POST['destination'] ) : '';
if ( is_wp_error( Object_Key::sanitize( $destination ) ) ) {
wp_die( esc_html__( 'Invalid destination.', 'devsyed-media-offloader' ) ); }
$exists = $client->exists( $destination );
if ( is_wp_error( $exists ) || $exists ) {
wp_die( esc_html__( 'Destination exists or cannot be checked. Nothing was moved.', 'devsyed-media-offloader' ) ); }
$source = reset( $keys );
$owners = Managed_Files::owners( $client->get_bucket(), $source );
$destination_owners = Managed_Files::owners( $client->get_bucket(), $destination );
if ( is_wp_error( $owners ) || is_wp_error( $destination_owners ) || $destination_owners || count( $owners ) > 1 ) {
wp_die( esc_html__( 'Ownership could not be verified, the source is shared, or the destination belongs to an attachment.', 'devsyed-media-offloader' ) );
}
if ( $owners && ! current_user_can( 'edit_post', $owners[0] ) ) {
wp_die( esc_html__( 'You cannot edit this attachment.', 'devsyed-media-offloader' ) );
}
if ( $owners && ! in_array( $source, Offload_Meta::keys( Offload_Meta::get( $owners[0] ) ), true ) ) {
wp_die( esc_html__( 'Retained versions cannot be moved. Move the current attachment file instead.', 'devsyed-media-offloader' ) );
}
$result = $client->copy_object( $source, $destination );
if ( is_wp_error( $result ) ) {
wp_die( esc_html( $result->get_error_message() ) ); }
if ( $owners ) {
$result = Managed_Files::relocate( $owners[0], $client->get_bucket(), $source, $destination );
if ( is_wp_error( $result ) ) {
wp_die( esc_html( $result->get_error_message() ) ); }
} else {
$result = $client->delete( $source );
if ( is_wp_error( $result ) ) {
wp_die( esc_html__( 'Copied successfully, but the source could not be deleted. Both files remain.', 'devsyed-media-offloader' ) ); }
}
} else {
wp_die( esc_html__( 'Unsupported operation or select exactly one file to move.', 'devsyed-media-offloader' ) ); }
wp_safe_redirect( admin_url( 'upload.php?page=r2clouder-files' ) );
exit;
}
/**
* Streams selected files as a ZIP.
*
* @param \R2Clouder\Storage\Storage_Adapter $client Storage client.
* @param string[] $keys Object keys.
* @throws \RuntimeException When archive creation fails (handled locally).
*/
private static function zip( $client, array $keys ): void {
if ( ! class_exists( '\ZipArchive' ) ) {
wp_die( esc_html__( 'ZIP downloads require the PHP ZIP extension.', 'devsyed-media-offloader' ) ); }
$archive = wp_tempnam( 'clouder.zip' );
$files = array();
$zip = new \ZipArchive();
$opened = false;
try {
if ( ! $archive || true !== $zip->open( $archive, \ZipArchive::OVERWRITE ) ) {
throw new \RuntimeException( 'Could not create ZIP.' ); }
$opened = true;
$total = 0;
foreach ( $keys as $index => $key ) {
$file = wp_tempnam( 'clouder-object' );
if ( ! $file ) {
throw new \RuntimeException( 'Could not create temporary file.' ); }
$files[] = $file;
$result = $client->get_to_file( $key, $file );
if ( is_wp_error( $result ) ) {
throw new \RuntimeException( $result->get_error_message() ); }
clearstatcache( true, $file );
if ( ! is_file( $file ) || ! is_readable( $file ) ) {
throw new \RuntimeException( 'Could not read downloaded file.' ); }
$total += (int) filesize( $file );
if ( $total > 100 * MB_IN_BYTES ) {
throw new \RuntimeException( 'ZIP selection exceeds 100 MB. Select fewer files.' ); }
// Flat numbered names prevent traversal and duplicate archive entries.
if ( ! $zip->addFile( $file, ( $index + 1 ) . '-' . sanitize_file_name( basename( $key ) ) ) ) {
throw new \RuntimeException( 'Could not add file to ZIP.' ); }
}
$opened = false;
if ( ! $zip->close() ) {
throw new \RuntimeException( 'Could not finish ZIP.' ); }
$opened = false;
nocache_headers();
header( 'Content-Type: application/zip' );
header( 'Content-Disposition: attachment; filename="cloud-files.zip"' );
header( 'Content-Length: ' . filesize( $archive ) );
readfile( $archive ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
} catch ( \Throwable $error ) {
if ( $opened ) {
try {
$zip->close();
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Cleanup must preserve the original error even when libzip throws.
} catch ( \Throwable $close_error ) {
// Preserve the original failure and continue temporary-file cleanup.
}
}
foreach ( $files as $file ) {
wp_delete_file( $file ); }
if ( $archive ) {
wp_delete_file( $archive ); }
wp_die( esc_html( $error->getMessage() ) );
} finally {
foreach ( $files as $file ) {
wp_delete_file( $file ); }
if ( $archive ) {
wp_delete_file( $archive ); }
}
exit;
}
}