managed_columns = array( 'as3cf_bucket' => _x( 'Bucket', 'Media Library column heading', 'amazon-s3-and-cloudfront' ), 'as3cf_access' => _x( 'Access', 'Media Library column heading', 'amazon-s3-and-cloudfront' ), ); } // Update media counts cache with current provider and bucket usage. add_filter( 'as3cf_media_counts_for_blog', array( $this, 'add_providers_and_buckets_to_media_counts' ), 10, 3 ); } /** * Load the media Pro assets */ public function load_media_pro_assets() { if ( ! $this->as3cf->is_plugin_setup() ) { return; } $this->as3cf->enqueue_script( 'as3cf-pro-media-script', 'assets/js/pro/media', array( 'jquery', 'media-views', 'media-grid', 'wp-util', ), false ); $nonces = array( 'get_attachment_provider_details' => wp_create_nonce( 'get-attachment-s3-details' ), ); foreach ( $this->get_available_media_actions() as $action => $scopes ) { foreach ( $scopes as $scope ) { $nonces[ $scope . '_' . $action ] = wp_create_nonce( "$scope-$action" ); } } wp_localize_script( 'as3cf-pro-media-script', 'as3cfpro_media', array( 'strings' => $this->get_media_action_strings(), 'actions' => array( 'bulk' => $this->get_available_media_actions( 'bulk' ), 'singular' => $this->get_available_media_actions( 'singular' ), ), 'nonces' => $nonces, 'settings' => array( 'default_acl' => $this->as3cf->get_storage_provider()->get_default_acl(), 'private_acl' => $this->as3cf->get_storage_provider()->get_private_acl(), ), 'filters' => $this->get_media_library_filters_config(), ) ); } /** * Load the attachment JS only when editing an attachment. */ public function load_attachment_js() { $this->as3cf->enqueue_script( 'as3cf-pro-attachment-script', 'assets/js/pro/attachment', array( 'jquery', 'wp-util', ), false ); $actions = $this->get_available_media_actions( 'singular' ); $nonces = array(); foreach ( $actions as $action ) { $nonces[ 'singular_' . $action ] = wp_create_nonce( "singular-$action" ); } wp_localize_script( 'as3cf-pro-attachment-script', 'as3cfpro_media', array( 'strings' => array( 'local_warning' => $this->get_media_action_strings( 'local_warning' ), 'updating_acl' => $this->get_media_action_strings( 'updating_acl' ), 'change_acl_error' => $this->get_media_action_strings( 'change_acl_error' ), ), 'actions' => $actions, 'nonces' => $nonces, 'settings' => array( 'post_id' => get_the_ID(), 'default_acl' => $this->as3cf->get_storage_provider()->get_default_acl(), 'private_acl' => $this->as3cf->get_storage_provider()->get_private_acl(), ), ) ); } /** * Handle updating the ACL for an attachment */ public function ajax_update_acl() { check_ajax_referer( 'singular-update_acl' ); $id = $this->as3cf->filter_input( 'id', INPUT_POST, FILTER_VALIDATE_INT ); // input var ok $acl = sanitize_text_field( $_REQUEST['acl'] ); // input var ok $title = $this->get_media_action_strings( 'change_to_public' ); $is_private = true; if ( empty( $id ) || empty( $acl ) ) { wp_send_json_error(); } if ( $this->as3cf->get_storage_provider()->get_private_acl() !== $acl ) { $acl = $this->as3cf->get_storage_provider()->get_default_acl(); $title = $this->get_media_action_strings( 'change_to_private' ); $is_private = false; } // Update on provider. $as3cf_item = Media_Library_Item::get_by_source_id( $id ); /** @var Update_Acl_Handler $updace_acl_handler */ $update_acl_handler = $this->as3cf->get_item_handler( Update_Acl_Handler::get_item_handler_key_name() ); $update = $update_acl_handler->handle( $as3cf_item, array( 'set_private' => $is_private ) ); $data = array( 'acl' => $acl, 'acl_display' => $this->as3cf->get_acl_display_name( $acl ), 'title' => $title, 'url' => wp_get_attachment_url( $id ), ); if ( is_wp_error( $update ) ) { wp_send_json_error(); } wp_send_json_success( $data ); } /** * Enrich the attachment model attributes used in JS * * @param array $response Array of prepared attachment data. * @param int|object $attachment Attachment ID or object. * * @return array */ public function enrich_attachment_model( $response, $attachment ) { $file = get_attached_file( $attachment->ID, true ); // flag if the attachment file doesn't exist locally // so we can ask for confirmation when removing from S3 $response['bulk_local_warning'] = ! file_exists( $file ); return $response; } /** * Add bulk media actions to a list table's bulk actions dropdown. * * @param array $actions * * @return array */ public function add_list_table_bulk_actions( $actions ) { $strings = $this->get_media_action_strings(); foreach ( $this->get_available_media_actions( 'bulk' ) as $action ) { $actions[ 'bulk_as3cfpro_' . $action ] = $strings[ $action ]; } return $actions; } /** * Add Pro media action strings. * * @param array $strings * * @return array */ public function media_action_strings( $strings ) { $strings['copy'] = __( 'Copy to Bucket', 'amazon-s3-and-cloudfront' ); $strings['remove'] = __( 'Remove from Bucket', 'amazon-s3-and-cloudfront' ); $strings['remove_local'] = __( 'Remove from Server', 'amazon-s3-and-cloudfront' ); $strings['download'] = __( 'Copy from Bucket to Server', 'amazon-s3-and-cloudfront' ); $strings['private_acl'] = __( 'Make Private in Bucket', 'amazon-s3-and-cloudfront' ); $strings['public_acl'] = __( 'Make Public in Bucket', 'amazon-s3-and-cloudfront' ); $strings['local_warning'] = __( 'This file does not exist locally so removing it from the bucket will result in broken links on your site. Are you sure you want to continue?', 'amazon-s3-and-cloudfront' ); $strings['bulk_local_warning'] = __( 'Some files do not exist locally so removing them from the bucket will result in broken links on your site. Are you sure you want to continue?', 'amazon-s3-and-cloudfront' ); $strings['change_to_private'] = __( 'Click to set as Private in the bucket', 'amazon-s3-and-cloudfront' ); $strings['change_to_public'] = __( 'Click to set as Public in the bucket', 'amazon-s3-and-cloudfront' ); $strings['updating_acl'] = __( 'Updating…', 'amazon-s3-and-cloudfront' ); $strings['change_acl_error'] = __( 'There was an error changing the ACL. Make sure the IAM user has permission to change the ACL and try again.', 'amazon-s3-and-cloudfront' ); return $strings; } /** * Conditionally adds media action links for an attachment on the Media library list view. * * @param array $actions * @param WP_Post|int $post * * @return array */ public function add_media_row_actions( array $actions, $post ) { $available_actions = $this->get_available_media_actions( 'singular' ); $this->bucket_col_actions = array(); $this->access_col_actions = array(); if ( ! $available_actions ) { return $actions; } $post_id = ( is_object( $post ) ) ? $post->ID : $post; $file = get_attached_file( $post_id, true ); $file_exists = file_exists( $file ); $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); // If offloaded to another provider can not do anything. if ( $as3cf_item && ! $as3cf_item->served_by_provider( true ) ) { $this->bucket_col_actions['as3cfpro_wrong_provider'] = sprintf( '%s', __( 'Offloaded to a different provider than currently configured.', 'amazon-s3-and-cloudfront' ), 'as3cf-warning', __( 'Disconnected', 'amazon-s3-and-cloudfront' ) ); return $actions; } // If not offloaded at all, or offloaded to current provider, can use copy. if ( in_array( 'copy', $available_actions ) && $file_exists && ( ! $as3cf_item || $as3cf_item->served_by_provider( true ) ) ) { $this->add_media_row_action( $actions, $post_id, 'copy' ); } // Actions beyond this point are for items on provider only if ( ! $as3cf_item || ! $as3cf_item->served_by_provider( true ) ) { return $actions; } if ( in_array( 'remove', $available_actions ) ) { if ( 'grid' === $this->render_context ) { $this->add_media_row_action( $actions, $post_id, 'remove' ); } else { $this->add_media_row_action( $this->bucket_col_actions, $post_id, 'remove' ); } } if ( in_array( 'download', $available_actions ) && ! $file_exists ) { $this->add_media_row_action( $actions, $post_id, 'download' ); } if ( in_array( 'private_acl', $available_actions ) && ! $as3cf_item->is_private() ) { if ( 'grid' === $this->render_context ) { $this->add_media_row_action( $actions, $post_id, 'private_acl' ); } else { $this->add_media_row_action( $this->access_col_actions, $post_id, 'private_acl', __( 'Make Private', 'amazon-s3-and-cloudfront' ) ); } } if ( in_array( 'public_acl', $available_actions ) && $as3cf_item->is_private() ) { if ( 'grid' === $this->render_context ) { $this->add_media_row_action( $actions, $post_id, 'public_acl' ); } else { $this->add_media_row_action( $this->access_col_actions, $post_id, 'public_acl', __( 'Make Public', 'amazon-s3-and-cloudfront' ) ); } } if ( in_array( 'remove_local', $available_actions ) && $file_exists ) { $this->add_media_row_action( $actions, $post_id, 'remove_local' ); } return $actions; } /** * Display notices after processing media actions */ public function maybe_display_media_action_message() { global $pagenow; if ( ! in_array( $pagenow, array( 'upload.php', 'post.php' ) ) ) { return; } if ( isset( $_GET['as3cfpro-action'] ) && isset( $_GET['errors'] ) && isset( $_GET['count'] ) ) { $action = sanitize_key( $_GET['as3cfpro-action'] ); // input var okay $error_count = absint( $_GET['errors'] ); // input var okay $count = absint( $_GET['count'] ); // input var okay $message_html = $this->get_media_action_result_message( $action, $count, $error_count ); if ( false !== $message_html ) { echo $message_html; } } } /** * Handler for single and bulk media actions */ public function process_media_actions() { if ( AS3CF_Utils::is_ajax() ) { return; } global $pagenow; if ( 'upload.php' != $pagenow ) { return; } if ( ! isset( $_GET['action'] ) ) { // input var okay return; } if ( ! empty( $_REQUEST['action2'] ) && '-1' != $_REQUEST['action2'] ) { // Handle bulk actions from the footer bulk action select $action = sanitize_key( $_REQUEST['action2'] ); // input var okay } else { $action = sanitize_key( $_REQUEST['action'] ); // input var okay } if ( false === strpos( $action, 'bulk_as3cfpro_' ) ) { $available_actions = $this->get_available_media_actions( 'singular' ); $referrer = 'as3cfpro-' . $action; $doing_bulk_action = false; if ( ! isset( $_GET['ids'] ) ) { return; } $ids = explode( ',', $_GET['ids'] ); // input var okay } else { $available_actions = $this->get_available_media_actions( 'bulk' ); $action = str_replace( 'bulk_as3cfpro_', '', $action ); $referrer = 'bulk-media'; $doing_bulk_action = true; if ( ! isset( $_REQUEST['media'] ) ) { return; } $ids = $_REQUEST['media']; // input var okay } if ( ! in_array( $action, $available_actions ) ) { return; } $ids = array_map( 'intval', $ids ); $id_count = count( $ids ); check_admin_referer( $referrer ); $sendback = isset( $_GET['sendback'] ) ? $_GET['sendback'] : wp_get_referer(); $args = array( 'as3cfpro-action' => $action, ); $result = $this->maybe_do_provider_action( $action, $ids, $doing_bulk_action ); if ( ! $result ) { unset( $args['as3cfpro-action'] ); $result = array(); } // If we're uploading a single file, add the id to the `$args` array. if ( 'copy' === $action && 1 === $id_count && ! empty( $result ) && 1 === ( $result['count'] + $result['errors'] ) ) { $args['as3cf_id'] = array_shift( $ids ); } $args = array_merge( $args, $result ); $url = add_query_arg( $args, $sendback ); wp_redirect( esc_url_raw( $url ) ); $this->_exit(); } /** * Add an action link to the media actions array * * @param array $actions * @param int $post_id * @param string $action * @param string $text * @param bool $show_warning */ public function add_media_row_action( &$actions, $post_id, $action, $text = '', $show_warning = false ) { $url = $this->get_media_action_url( $action, $post_id ); $text = $text ? $text : $this->get_media_action_strings( $action ); $class = $action; if ( $show_warning ) { $class .= ' local-warning'; } $actions[ 'as3cfpro_' . $action ] = '' . esc_html( $text ) . ''; } /** * Check we can do the media actions * * @return bool */ public function verify_media_actions() { if ( ! $this->as3cf->is_pro_plugin_setup( true ) ) { return false; } return $this->user_can_use_media_actions(); } /** * Get a list of available media actions which can be performed according to plugin and user capability requirements. * * @param string|null $scope * * @return array */ public function get_available_media_actions( $scope = null ) { $actions = array(); if ( ! $this->as3cf->is_plugin_setup( true ) || ! $this->user_can_use_media_actions() ) { return $actions; } // We've already tested provider credentials, but is license ok? if ( $this->as3cf->is_pro_plugin_setup( true ) ) { $actions['copy'] = array( 'singular', 'bulk' ); $actions['download'] = array( 'singular', 'bulk' ); $actions['update_acl'] = array( 'singular' ); $actions['private_acl'] = array( 'singular', 'bulk' ); $actions['public_acl'] = array( 'singular', 'bulk' ); $actions['remove_local'] = array( 'singular', 'bulk' ); } // Remove from Bucket should still be available even if license exceeded/invalid. $actions['remove'] = array( 'singular', 'bulk' ); if ( $scope ) { $in_scope = array_filter( $actions, function ( $scopes ) use ( $scope ) { return in_array( $scope, $scopes ); } ); return array_keys( $in_scope ); } return $actions; } /** * Check if the given user can use on-demand S3 media actions. * * @param null|int|WP_User $user User to check. Defaults to current user. * * @return bool */ public function user_can_use_media_actions( $user = null ) { $user = $user ? $user : wp_get_current_user(); if ( is_object( $user ) ) { $user = $user->ID; } if ( ! is_null( $this->user_can_use_media_actions ) && isset( $this->user_can_use_media_actions[ $user ] ) ) { return $this->user_can_use_media_actions[ $user ]; } $this->user_can_use_media_actions[ $user ] = false; if ( user_can( $user, 'use_as3cf_media_actions' ) ) { $this->user_can_use_media_actions[ $user ] = true; } else { /** * The default capability for using on-demand S3 media actions. * * @param string $capability Registered capability identifier */ $capability = apply_filters( 'as3cfpro_media_actions_capability', 'manage_options' ); $this->user_can_use_media_actions[ $user ] = user_can( $user, $capability ); } return $this->user_can_use_media_actions[ $user ]; } /** * Generate the URL for performing S3 media actions * * @param string $action * @param int $post_id * @param null|string $sendback_path * * @return string */ public function get_media_action_url( $action, $post_id, $sendback_path = null ) { $args = array( 'action' => $action, 'ids' => $post_id, ); if ( ! is_null( $sendback_path ) ) { $args['sendback'] = urlencode( admin_url( $sendback_path ) ); } $url = add_query_arg( $args, admin_url( 'upload.php' ) ); $url = wp_nonce_url( $url, 'as3cfpro-' . $action ); return esc_url( $url ); } /** * Handle S3 actions applied to attachments via the Backbone JS * in the media grid and edit attachment modal */ public function ajax_process_media_action() { if ( ! isset( $_POST['s3_action'] ) && ! isset( $_POST['ids'] ) ) { return; } $scope = filter_input( INPUT_POST, 'scope' ); $action = filter_input( INPUT_POST, 's3_action' ); check_ajax_referer( "{$scope}-{$action}" ); $ids = array_map( 'intval', $_POST['ids'] ); // input var okay // process the S3 action for the attachments $return = $this->maybe_do_provider_action( $action, $ids, true ); $message_html = ''; if ( $return ) { $message_html = $this->get_media_action_result_message( $action, $return['count'], $return['errors'] ); } wp_send_json_success( $message_html ); } /** * Wrapper for media actions * * @param string $action type of media action, copy, remove, download, remove_local * @param array $ids attachment IDs * @param bool $doing_bulk_action flag for multiple attachments, if true then we need to * perform a check for each attachment * * @return bool|array on success array with success count and error count * @throws Exception */ public function maybe_do_provider_action( $action, $ids, $doing_bulk_action ) { switch ( $action ) { case 'copy': $result = $this->maybe_upload_attachments( $ids, $doing_bulk_action ); break; case 'remove': $result = $this->maybe_delete_attachments_from_provider( $ids, $doing_bulk_action ); break; case 'download': $result = $this->maybe_download_attachments_from_provider( $ids, $doing_bulk_action ); break; case 'private_acl': $result = $this->maybe_update_acls_to_private( $ids, $doing_bulk_action ); break; case 'public_acl': $result = $this->maybe_update_acls_to_public( $ids, $doing_bulk_action ); break; case 'remove_local': $result = $this->maybe_remove_local_files_for_attachments( $ids, $doing_bulk_action ); break; default: // not one of our actions, remove $result = false; break; } return $result; } /** * Get the result message after an S3 action has been performed * * @param string $action type of S3 action * @param int $count count of successful processes * @param int $error_count count of errors * * @return bool|string */ public function get_media_action_result_message( $action, $count = 0, $error_count = 0 ) { $class = 'updated'; $type = 'success'; if ( 0 === $count && 0 === $error_count ) { // don't show any message if no attachments processed // i.e. they haven't met the checks for bulk actions return false; } if ( $error_count > 0 ) { $type = 'error'; $class = $type; // We have processed some successfully. if ( $count > 0 ) { $type = 'partial'; } } $message = $this->get_message( $action, $type ); // can't find a relevant message, abort if ( ! $message ) { return false; } $id = $this->as3cf->filter_input( 'as3cf_id', INPUT_GET, FILTER_VALIDATE_INT ); // If we're uploading a single item, add an edit link. if ( 1 === ( $count + $error_count ) && ! empty( $id ) ) { $url = esc_url( get_edit_post_link( $id ) ); // Only add the link if we have a URL. if ( ! empty( $url ) ) { $text = esc_html__( 'Edit attachment', 'amazon-s3-and-cloudfront' ); $message .= sprintf( ' %2$s', $url, $text ); } } $message = sprintf( '

%s

', $class, $message ); return $message; } /** * Retrieve all the media action related notice messages * * @return array */ public function get_messages() { if ( is_null( $this->messages ) ) { $this->messages = array( 'copy' => array( 'success' => __( 'Media successfully copied to bucket.', 'amazon-s3-and-cloudfront' ), 'partial' => __( 'Media copied to bucket with some errors.', 'amazon-s3-and-cloudfront' ), 'error' => __( 'There were errors when copying the media to bucket.', 'amazon-s3-and-cloudfront' ), ), 'remove' => array( 'success' => __( 'Media successfully removed from bucket.', 'amazon-s3-and-cloudfront' ), 'partial' => __( 'Media removed from bucket, with some errors.', 'amazon-s3-and-cloudfront' ), 'error' => __( 'There were errors when removing the media from bucket.', 'amazon-s3-and-cloudfront' ), ), 'download' => array( 'success' => __( 'Media successfully downloaded from bucket.', 'amazon-s3-and-cloudfront' ), 'partial' => __( 'Media downloaded from bucket, with some errors.', 'amazon-s3-and-cloudfront' ), 'error' => __( 'There were errors when downloading the media from bucket.', 'amazon-s3-and-cloudfront' ), ), 'private_acl' => array( 'success' => __( 'Media successfully set as private in bucket.', 'amazon-s3-and-cloudfront' ), 'partial' => __( 'Media set as private in bucket, with some errors.', 'amazon-s3-and-cloudfront' ), 'error' => __( 'There were errors when setting the media as private in bucket.', 'amazon-s3-and-cloudfront' ), ), 'public_acl' => array( 'success' => __( 'Media successfully set as public in bucket.', 'amazon-s3-and-cloudfront' ), 'partial' => __( 'Media set as public in bucket, with some errors.', 'amazon-s3-and-cloudfront' ), 'error' => __( 'There were errors when setting the media as public in bucket.', 'amazon-s3-and-cloudfront' ), ), 'remove_local' => array( 'success' => __( 'Media successfully removed from server.', 'amazon-s3-and-cloudfront' ), 'partial' => __( 'Media removed from server, with some errors.', 'amazon-s3-and-cloudfront' ), 'error' => __( 'There were errors when removing the media from server.', 'amazon-s3-and-cloudfront' ), ), ); } return $this->messages; } /** * Get a specific media action notice message * * @param string $action type of action, e.g. copy, remove, download * @param string $type if the action has resulted in success, error, partial (errors) * * @return string|bool */ public function get_message( $action = 'copy', $type = 'success' ) { $messages = $this->get_messages(); if ( isset( $messages[ $action ][ $type ] ) ) { return $messages[ $action ][ $type ]; } return false; } /** * Wrapper for uploading multiple attachments to S3 * * @param array $post_ids attachment IDs * @param bool $doing_bulk_action flag for multiple attachments, if true then we need to * perform a check for each attachment to make sure the * file exists locally before uploading to S3 * * @return array|WP_Error * @throws Exception */ public function maybe_upload_attachments( $post_ids, $doing_bulk_action = false ) { $result = array( 'errors' => 0, 'count' => 0, ); $upload_handler = $this->as3cf->get_item_handler( Upload_Handler::get_item_handler_key_name() ); foreach ( $post_ids as $post_id ) { if ( $doing_bulk_action ) { // If bulk action check the file exists. $file = get_attached_file( $post_id, true ); // If the file doesn't exist locally we can't copy. if ( ! file_exists( $file ) ) { continue; } } $offloaded_files = array(); $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); if ( empty( $as3cf_item ) ) { $as3cf_item = Media_Library_Item::create_from_source_id( $post_id ); } else { // Refresh the list of expected objects from attachment's expected thumbnail sizes. $offloaded_files = $as3cf_item->offloaded_files(); $metadata = wp_get_attachment_metadata( $post_id, true ); if ( empty( $metadata ) || is_wp_error( $metadata ) ) { $result['errors']++; continue; } $this->update_item_from_new_metadata( $as3cf_item, $metadata ); } if ( ! empty( $as3cf_item ) && ! is_wp_error( $as3cf_item ) ) { $upload_result = $upload_handler->handle( $as3cf_item, $offloaded_files ); // Even if an upload was cancelled via a filter, // if there was no error, then it was successfully processed. if ( is_wp_error( $upload_result ) ) { AS3CF_Error::log( $upload_result->get_error_messages() ); } else { $result['count']++; continue; } } $result['errors']++; } return $result; } /** * Wrapper for removing multiple attachments from provider. * * @param array $post_ids Attachment IDs * @param bool $doing_bulk_action Flag for multiple attachments. If true then we need to * perform a check for each attachment to make sure it has * been uploaded to provider before trying to delete it. * This flag currently has no effect, but is retained for * consistency with similar functions in this class. * * @return array * @throws Exception */ public function maybe_delete_attachments_from_provider( $post_ids, $doing_bulk_action = false ) { $result = array( 'errors' => 0, 'count' => 0, ); /** @var Remove_Provider_Handler $remove_handler */ $remove_handler = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() ); foreach ( $post_ids as $post_id ) { $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); if ( empty( $as3cf_item ) ) { $result['count']++; continue; } // Delete attachment from provider. $remove_result = $remove_handler->handle( $as3cf_item ); if ( is_wp_error( $remove_result ) ) { $result['errors']++; AS3CF_Error::log( $remove_result->get_error_messages() ); continue; } // There was no error, so either objects deleted or skipped due to duplicates. $as3cf_item->delete(); if ( Media_Library_Item::get_by_source_id( $post_id ) ) { $result['errors']++; continue; } $result['count']++; } return $result; } /** * Wrapper for downloading multiple attachments from provider. * * @param array $post_ids Attachment IDs * @param bool $doing_bulk_action Flag for multiple attachments. If true then we need to * perform a check for each attachment to make sure it has * been uploaded to provider and does not exist locally before * trying to download it. * * @return array * @throws Exception */ public function maybe_download_attachments_from_provider( $post_ids, $doing_bulk_action = false ) { $result = array( 'errors' => 0, 'count' => 0, ); $download_handler = $this->as3cf->get_item_handler( Download_Handler::get_item_handler_key_name() ); foreach ( $post_ids as $post_id ) { $file = get_attached_file( $post_id, true ); $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); if ( empty( $as3cf_item ) || is_wp_error( $as3cf_item ) ) { continue; } // If bulk action, check whether original file exists locally before trying to download. $file_exists_locally = $doing_bulk_action && file_exists( $file ); if ( ! $file_exists_locally ) { // Download the attachment from provider. $download_handler->handle( $as3cf_item ); if ( ! file_exists( $file ) ) { $result['errors']++; continue; } } $result['count']++; } return $result; } /** * Wrapper for removing multiple attachments from server if offloaded * * @param array $post_ids attachment IDs * @param bool $doing_bulk_action flag for multiple attachments, if true then we need to * perform a check for each attachment to make sure it has * been uploaded to bucket before trying to delete it * * @return array * @throws Exception */ public function maybe_remove_local_files_for_attachments( $post_ids, $doing_bulk_action = false ) { $result = array( 'errors' => 0, 'count' => 0, ); $as3cf_items = array(); foreach ( $post_ids as $key => $post_id ) { $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); if ( empty( $as3cf_item ) ) { $result['errors']++; unset( $post_ids[ $key ] ); continue; } $as3cf_items[ $post_id ] = $as3cf_item; } if ( ! empty( $post_ids ) ) { /** @var Remove_Local_Handler $remove_local_handler */ $remove_local_handler = $this->as3cf->get_item_handler( Remove_Local_Handler::get_item_handler_key_name() ); // Set up to check provider keys before removing. $options = array( 'verify_exists_on_provider' => true, 'provider_keys' => $this->as3cf->get_provider_keys( $post_ids ), ); foreach ( $post_ids as $post_id ) { $as3cf_item = $as3cf_items[ $post_id ]; $remove_local_handler->handle( $as3cf_item, $options ); if ( ! $this->attachment_exists_locally( $post_id ) ) { $result['count']++; } else { $result['errors']++; } } } return $result; } /** * Wrapper for updating the ACLs for multiple attachments on provider * * @param array $post_ids Attachment IDs * @param bool $doing_bulk_action Flag for multiple attachments, if true then we need to * perform a check for each attachment to make sure it has * been uploaded to the current provider * @param bool $private Setting to private ACL? Default is public. * * @return array|WP_Error * @throws Exception */ private function maybe_update_acls( $post_ids, $doing_bulk_action = false, $private = false ) { $result = array( 'errors' => 0, 'count' => 0, ); $provider_key = $this->as3cf->get_storage_provider()->get_provider_key_name(); /** @var Update_Acl_Handler $update_acl_handler */ $update_acl_handler = $this->as3cf->get_item_handler( Update_Acl_Handler::get_item_handler_key_name() ); $options = array( 'set_private' => $private ); foreach ( $post_ids as $post_id ) { $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); if ( ! $as3cf_item ) { $result['errors']++; continue; } if ( $doing_bulk_action ) { if ( $provider_key !== $as3cf_item->provider() ) { $result['errors']++; continue; } } $update_result = $update_acl_handler->handle( $as3cf_item, $options ); if ( is_wp_error( $update_result ) ) { $result['errors']++; AS3CF_Error::log( $update_result->get_error_messages() ); continue; } $result['count']++; } return $result; } /** * Wrapper for updating the ACLs to private for multiple attachments on provider * * @param array $post_ids Attachment IDs * @param bool $doing_bulk_action Flag for multiple attachments, if true then we need to * perform a check for each attachment to make sure it has * been uploaded to the current provider * * @return array|WP_Error * @throws Exception */ private function maybe_update_acls_to_private( $post_ids, $doing_bulk_action = false ) { return $this->maybe_update_acls( $post_ids, $doing_bulk_action, true ); } /** * Wrapper for updating the ACLs to public for multiple attachments on provider * * @param array $post_ids Attachment IDs * @param bool $doing_bulk_action Flag for multiple attachments, if true then we need to * perform a check for each attachment to make sure it has * been uploaded to the current provider * * @return array|WP_Error * @throws Exception */ private function maybe_update_acls_to_public( $post_ids, $doing_bulk_action = false ) { return $this->maybe_update_acls( $post_ids, $doing_bulk_action, false ); } /** * Get attachment local paths. * * @param int $attachment_id * * @return array */ private function get_attachment_local_paths( $attachment_id ) { static $local_paths = array(); $blog_id = get_current_blog_id(); if ( isset( $local_paths[ $blog_id ][ $attachment_id ] ) ) { return $local_paths[ $blog_id ][ $attachment_id ]; } $paths = AS3CF_Utils::get_attachment_file_paths( $attachment_id, false ); $local_paths[ $blog_id ][ $attachment_id ] = array_unique( $paths ); return $paths; } /** * Does attachment exist locally? * * @param int $attachment_id * * @return bool */ private function attachment_exists_locally( $attachment_id ) { $paths = $this->get_attachment_local_paths( $attachment_id ); foreach ( $paths as $path ) { if ( file_exists( $path ) ) { return true; } } return false; } /** * Get ACL value string. * * @param array $acl * @param int $post_id * * @return string */ public function get_acl_value_string( $acl, $post_id ) { $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); if ( ! in_array( 'update_acl', $this->get_available_media_actions( 'singular' ) ) || ! $this->as3cf || ! $as3cf_item->served_by_provider( true ) ) { return parent::get_acl_value_string( $acl, $post_id ); } return sprintf( '%s', $acl['title'], $acl['acl'], $acl['name'] ); } /** * Helper function for terminating script execution. Easily testable. * * @param int|string $exit_code * * @return void * * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore */ public function _exit( $exit_code = 0 ) { exit( $exit_code ); } /** * Add column with as3cf item details to the media library list view. * * @handles manage_media_columns * * @param array $columns * * @return array */ public function add_media_library_columns( array $columns ): array { return array_merge( $columns, $this->managed_columns ); } /** * Add our columns as sortable. * * @handles manage_upload_sortable_columns * * @param array $columns * * @return array */ public function add_media_library_sortable_columns( array $columns ): array { foreach ( $this->managed_columns as $key => $caption ) { $columns[ $key ] = $key; } return $columns; } /** * Render column content for individual media library item. * * @handles manage_media_custom_column * * @param string $column_name * @param string $post_id */ public function get_media_library_column_content( string $column_name, string $post_id ) { if ( ! in_array( $column_name, array_keys( $this->managed_columns ) ) ) { return; } $provider_object = $this->get_formatted_provider_info( $post_id ); if ( empty( $provider_object ) ) { echo '—'; return; } // Bucket column. if ( $column_name === 'as3cf_bucket' ) { echo sprintf( '%s
%s
%s', $provider_object['provider_name'], $provider_object['region'], $provider_object['bucket'] ); if ( ! empty( $this->bucket_col_actions ) ) { echo '
' . join( ' | ', $this->bucket_col_actions ) . '
'; } } // Access column. if ( $column_name === 'as3cf_access' ) { echo sprintf( '%s', $provider_object['acl']['name'] ); if ( ! empty( $this->access_col_actions ) ) { echo '
' . join( ' | ', $this->access_col_actions ) . '
'; } } } /** * Renders the filter dropdowns in the media library list view. * * @handles restrict_manage_posts */ public function add_media_library_filters() { $screen = get_current_screen(); if ( 'upload' !== $screen->base ) { return; } $filters = $this->get_media_library_filters_config(); echo AS3CF_Utils::make_dropdown( $filters['as3cf_location'] ); echo AS3CF_Utils::make_dropdown( $filters['as3cf_access'] ); } /** * Filter the posts query when one of our filters are in use on the * media library screen. * * @handles pre_get_posts * * @param WP_Query $query * * @return WP_Query */ public function media_library_filter_query( WP_Query $query ): WP_Query { global $pagenow; if ( empty( $query->query_vars["post_type"] ) || 'attachment' !== $query->query_vars["post_type"] ) { return $query; } if ( ! in_array( $pagenow, array( 'upload.php', 'admin-ajax.php' ) ) ) { return $query; } $location = $this->selected_filter_value( 'as3cf_location', 'all' ); $access = $this->selected_filter_value( 'as3cf_access', 'all' ); $orderby = $query->get( 'orderby' ); // If none of our filters are in play and sorting isn't by one of our columns, exit out. if ( 'all' === $location && 'all' === $access && ! in_array( $orderby, array_keys( $this->managed_columns ) ) ) { return $query; } add_filter( 'posts_join', array( $this, 'query_join_items_table' ), 10, 2 ); add_filter( 'posts_where', array( $this, 'query_add_where' ), 10, 2 ); add_filter( 'posts_orderby', array( $this, 'query_add_orderby' ), 10, 2 ); return $query; } /** * Join in the items table in WP_Query. * * @handles posts_join * * @param string $join * @param WP_Query $query * * @return string */ public function query_join_items_table( string $join, WP_Query $query ): string { global $wpdb; $table = $wpdb->get_blog_prefix() . Item::ITEMS_TABLE; $join .= " LEFT JOIN {$table} i ON (i.source_type = 'media-library' AND i.source_id = {$wpdb->posts}.ID) "; return $join; } /** * Add where clauses to posts query. * * @handles posts_where * * @param string $where * @param WP_Query $query * * @return string */ public function query_add_where( string $where, WP_Query $query ): string { global $wpdb; $location = $this->selected_filter_value( 'as3cf_location', 'all' ); switch ( $location ) { case 'all': // intentionally empty break; case 'offloaded': $where .= ' AND i.id IS NOT NULL '; break; case 'not_offloaded': $where .= ' AND i.id IS NULL '; break; default: $parts = explode( '|', $location ); if ( count( $parts ) === 2 ) { array_walk( $parts, 'sanitize_key' ); $where .= $wpdb->prepare( ' AND (i.provider = %s AND i.bucket = %s) ', $parts[0], $parts[1] ); } } $access = $this->selected_filter_value( 'as3cf_access', 'all' ); switch ( $access ) { case 'all': // intentionally empty break; case 'private': $where .= ' AND i.is_private = 1 '; break; case 'public': $where .= ' AND i.is_private = 0 '; break; } return $where; } /** * Add ORDER BY clause to posts query. * * @handles posts_orderby * * @param string $orderby * @param WP_Query $query * * @return string */ public function query_add_orderby( string $orderby, WP_Query $query ): string { $direction = $query->get( 'order', 'ASC' ); switch ( $query->get( 'orderby' ) ) { case 'as3cf_access': $orderby = sprintf( 'i.is_private %s', $direction ); break; case 'as3cf_bucket': $orderby = sprintf( 'i.bucket %1$s, i.provider %1$s', $direction ); break; } return $orderby; } /** * Get all providers and buckets currently in use. * * @return array */ private function get_providers_and_buckets(): array { $blog_id = get_current_blog_id(); $counts = $this->as3cf->media_counts(); if ( ! empty( $counts['providers'][ $blog_id ] ) ) { return $counts['providers'][ $blog_id ]; } return array(); } /** * Update media counts cache with current provider and bucket usage. * * @param array $media_counts * @param int $blog_id * @param string $table_prefix * * @return array */ public function add_providers_and_buckets_to_media_counts( $media_counts, $blog_id, $table_prefix ): array { global $wpdb; if ( ! is_array( $media_counts ) || ! is_int( $blog_id ) || empty( $table_prefix ) || ! is_string( $table_prefix ) ) { return $media_counts; } $table = $table_prefix . Item::ITEMS_TABLE; $rows = $wpdb->get_results( "SELECT DISTINCT provider, bucket FROM $table ORDER BY provider, bucket" ); $providers = array_unique( array_map( function ( $row ) { return $row->provider; }, $rows ) ); $all_buckets = array(); foreach ( $providers as $provider_key ) { /** @var Provider|null $provider_class */ $provider_class = $this->as3cf->get_provider_class( $provider_key ); $provider_name = is_null( $provider_class ) ? $provider_key : $provider_class::get_provider_service_name(); foreach ( $rows as $row ) { if ( $row->provider !== $provider_key ) { continue; } $bucket_key = $row->provider . '|' . $row->bucket; $all_buckets[ $provider_name ][ $bucket_key ] = $row->bucket; } } $media_counts['providers'][ $blog_id ] = $all_buckets; return $media_counts; } /** * Safely retrieve the selected filter value from a dropdown. * * @param string $key * @param string $default * * @return string */ private function selected_filter_value( string $key, string $default ): string { if ( wp_doing_ajax() ) { if ( isset( $_REQUEST['query'][ $key ] ) ) { $value = sanitize_text_field( $_REQUEST['query'][ $key ] ); } } else { if ( ! isset( $_REQUEST['filter_action'] ) || $_REQUEST['filter_action'] !== 'Filter' ) { return $default; } if ( ! isset( $_REQUEST[ $key ] ) ) { return $default; } $value = sanitize_text_field( $_REQUEST[ $key ] ); } return ! empty( $value ) ? $value : $default; } /** * Return values for the media filters. * * @return array */ private function get_media_library_filters_config(): array { if ( ! empty( $this->media_library_filters ) ) { return $this->media_library_filters; } // Locations dropdown. $options = array( 'all' => _x( 'All locations', 'Media Library Filter option', 'amazon-s3-and-cloudfront' ), 'offloaded' => _x( 'Offloaded', 'Media Library Filter option', 'amazon-s3-and-cloudfront' ), 'not_offloaded' => _x( 'Not offloaded', 'Media Library Filter option', 'amazon-s3-and-cloudfront' ), ); $options = array_merge( $options, $this->get_providers_and_buckets() ); $this->media_library_filters['as3cf_location'] = array( 'id' => 'as3cf_location', 'name' => 'as3cf_location', 'label' => __( 'Filter by Offload Media location', 'amazon-s3-and-cloudfront' ), 'selected' => $this->selected_filter_value( 'as3cf_location', 'all' ), 'options' => $options, ); // Access dropdown. $this->media_library_filters['as3cf_access'] = array( 'id' => 'as3cf_access', 'name' => 'as3cf_access', 'label' => __( 'Filter by Offload Media access level', 'amazon-s3-and-cloudfront' ), 'selected' => $this->selected_filter_value( 'as3cf_access', 'all' ), 'options' => array( 'all' => _x( 'All access', 'Media Library Filter option', 'amazon-s3-and-cloudfront' ), 'public' => _x( 'Public', 'Media Library Filter option', 'amazon-s3-and-cloudfront' ), 'private' => _x( 'Private', 'Media Library Filter option', 'amazon-s3-and-cloudfront' ), ), ); return $this->media_library_filters; } /** * Add URL entries for our summary counts. * * @handles as3cf_get_summary_counts * * @param array $summaries * * @return array */ public function add_urls_to_summary_counts( array $summaries ): array { $attachments_url = add_query_arg( array( 'mode' => 'list', 'filter_action' => 'Filter', ), admin_url( 'upload.php' ) ); foreach ( $summaries as $idx => $summary ) { if ( ! empty( $summary['type'] ) && Media_Library_Item::summary_type() === $summary['type'] ) { $summaries[ $idx ]['offloaded_url'] = add_query_arg( 'as3cf_location', 'offloaded', $attachments_url ); $summaries[ $idx ]['not_offloaded_url'] = add_query_arg( 'as3cf_location', 'not_offloaded', $attachments_url ); break; } } return $summaries; } }