plugin_slug = ( is_null( $slug ) ) ? 'amazon-s3-and-cloudfront' : $slug; parent::__construct( $plugin_file_path ); $this->aws = $aws; $this->init( $plugin_file_path ); } /** * Abstract class constructor * * @param string $plugin_file_path */ function init( $plugin_file_path ) { self::$plugin_page = $this->plugin_slug; $this->plugin_title = __( 'Offload S3', 'as3cf' ); $this->plugin_menu_title = __( 'S3 and CloudFront', 'as3cf' ); new AS3CF_Upgrade_Region_Meta( $this ); new AS3CF_Upgrade_File_Sizes( $this ); new AS3CF_Upgrade_Meta_WP_Error( $this ); add_action( 'aws_admin_menu', array( $this, 'admin_menu' ) ); add_action( 'wp_ajax_as3cf-get-buckets', array( $this, 'ajax_get_buckets' ) ); add_action( 'wp_ajax_as3cf-save-bucket', array( $this, 'ajax_save_bucket' ) ); add_action( 'wp_ajax_as3cf-create-bucket', array( $this, 'ajax_create_bucket' ) ); add_action( 'wp_ajax_as3cf-manual-save-bucket', array( $this, 'ajax_save_bucket' ) ); add_action( 'wp_ajax_as3cf-get-url-preview', array( $this, 'ajax_get_url_preview' ) ); // Admin notices add_action( 'admin_notices', array( $this, 'maybe_show_admin_notices' ) ); add_action( 'network_admin_notices', array( $this, 'maybe_show_admin_notices' ) ); add_action( 'shutdown', array( $this, 'save_admin_notices' ) ); add_filter( 'wp_get_attachment_url', array( $this, 'wp_get_attachment_url' ), 99, 2 ); add_filter( 'wp_handle_upload_prefilter', array( $this, 'wp_handle_upload_prefilter' ), 1 ); add_filter( 'wp_update_attachment_metadata', array( $this, 'wp_update_attachment_metadata' ), 110, 2 ); add_filter( 'wp_get_attachment_metadata', array( $this, 'wp_get_attachment_metadata' ), 10, 2 ); add_filter( 'delete_attachment', array( $this, 'delete_attachment' ), 20 ); add_filter( 'update_attached_file', array( $this, 'update_attached_file' ), 100, 2 ); add_filter( 'get_attached_file', array( $this, 'get_attached_file' ), 10, 2 ); add_filter( 'plugin_action_links', array( $this, 'plugin_actions_settings_link' ), 10, 2 ); add_filter( 'pre_get_space_used', array( $this, 'multisite_get_spaced_used' ) ); // include compatibility code for other plugins new AS3CF_Plugin_Compatibility( $this ); load_plugin_textdomain( 'as3cf', false, dirname( plugin_basename( $plugin_file_path ) ) . '/languages/' ); // Register modal scripts and styles $this->register_modal_assets(); } /** * Get the plugin title to be used in page headings * * @return string */ function get_plugin_page_title() { return apply_filters( 'as3cf_settings_page_title', $this->plugin_title ); } /** * Get the plugin prefix in slug format, ie. replace underscores with hyphens * * @return string */ function get_plugin_prefix_slug() { return str_replace( '_', '-', $this->plugin_prefix ); } /** * Get the nonce key for the settings form of the plugin * * @return string */ function get_settings_nonce_key() { return $this->get_plugin_prefix_slug() . '-save-settings'; } /** * Accessor for a plugin setting with conditions to defaults and upgrades * * @param string $key * @param mixed $default * * @return int|mixed|string|WP_Error */ function get_setting( $key, $default = '' ) { // use settings from $_POST when generating URL preview via AJAX if ( isset( $_POST['action'] ) && 'as3cf-get-url-preview' == sanitize_key( $_POST['action'] ) ) { // input var okay $value = 0; if ( isset( $_POST[ $key ] ) ) { // input var okay $value = $_POST[ $key ]; // input var okay if ( is_array( $value ) ) { // checkbox is checked $value = 1; } } return $value; } $settings = $this->get_settings(); // If legacy setting set, migrate settings if ( isset( $settings['wp-uploads'] ) && $settings['wp-uploads'] && in_array( $key, array( 'copy-to-s3', 'serve-from-s3' ) ) ) { return '1'; } // Turn on object versioning by default if ( 'object-versioning' == $key && ! isset( $settings['object-versioning'] ) ) { return '1'; } // Default object prefix if ( 'object-prefix' == $key && ! isset( $settings['object-prefix'] ) ) { return $this->get_default_object_prefix(); } // Default use year and month folders if ( 'use-yearmonth-folders' == $key && ! isset( $settings['use-yearmonth-folders'] ) ) { return get_option( 'uploads_use_yearmonth_folders' ); } // Default enable object prefix - enabled unless path is empty if ( 'enable-object-prefix' == $key ) { if ( isset( $settings['enable-object-prefix'] ) && '0' == $settings['enable-object-prefix'] ) { return 0; } if ( isset( $settings['object-prefix'] ) && '' == trim( $settings['object-prefix'] ) ) { return 0; } else { return 1; } } // Region of bucket if not already retrieved if ( 'region' == $key && ! isset( $settings['region'] ) ) { $bucket = $this->get_setting( 'bucket' ); $region = $default; if ( $bucket ) { $region = $this->get_bucket_region( $bucket ); } // Store the region for future use parent::set_setting( 'region', $region ); $this->save_settings(); return $region; } // Region of bucket translation if ( 'region' == $key && isset( $settings['region'] ) ) { return $this->translate_region( $settings['region'] ); } // Domain setting since 0.8 if ( 'domain' == $key && ! isset( $settings['domain'] ) ) { if ( $this->get_setting( 'cloudfront' ) ) { $domain = 'cloudfront'; } elseif ( $this->get_setting( 'virtual-host' ) ) { $domain = 'virtual-host'; } elseif ( $this->use_ssl() ) { $domain = 'path'; } else { $domain = 'subdomain'; } return $domain; } // SSL radio buttons since 0.8 if ( 'ssl' == $key && ! isset( $settings['ssl'] ) ) { if ( $this->get_setting( 'force-ssl', false ) ) { $ssl = 'https'; } else { $ssl = 'request'; } return $ssl; } $value = parent::get_setting( $key, $default ); if ( 'bucket' == $key && defined( 'AS3CF_BUCKET' ) ) { $bucket = AS3CF_BUCKET; if ( $bucket !== $value ) { // Save the defined bucket parent::set_setting( 'bucket', $bucket ); // Clear region $this->remove_setting( 'region' ); $this->save_settings(); } return $bucket; } return apply_filters( 'as3cf_setting_' . $key, $value ); } /** * Setter for a plugin setting with custom hooks * * @param string $key * @param mixed $value */ function set_setting( $key, $value ) { // Run class specific hooks before the setting is saved $this->pre_set_setting( $key, $value ); $value = apply_filters( 'as3cf_set_setting_' . $key, $value ); parent::set_setting( $key, $value ); } /** * Return the default object prefix * * @return string */ function get_default_object_prefix() { if ( is_multisite() ) { return 'wp-content/uploads/'; } $uploads = wp_upload_dir(); $parts = parse_url( $uploads['baseurl'] ); $path = ltrim( $parts['path'], '/' ); return trailingslashit( $path ); } /** * Allowed mime types array that can be edited for specific S3 uploading * * @return array */ function get_allowed_mime_types() { return apply_filters( 'as3cf_allowed_mime_types', get_allowed_mime_types() ); } /** * Wrapper for scheduling cron jobs * * @param string $hook * @param null|string $interval Defaults to hook if not supplied * @param array $args */ function schedule_event( $hook, $interval = null, $args = array() ) { if ( is_null( $interval ) ) { $interval = $hook; } if ( ! wp_next_scheduled( $hook ) ) { wp_schedule_event( current_time( 'timestamp' ), $interval, $hook, $args ); } } /** * Wrapper for clearing scheduled events for a specific cron job * * @param string $hook */ function clear_scheduled_event( $hook ) { $timestamp = wp_next_scheduled( $hook ); if ( $timestamp ) { wp_unschedule_event( $timestamp, $hook ); } } /** * Generate a preview of the URL of files uploaded to S3 * * @param bool $escape * @param string $suffix * * @return string */ function get_url_preview( $escape = true, $suffix = 'photo.jpg' ) { $scheme = $this->get_s3_url_scheme(); $bucket = $this->get_setting( 'bucket' ); $path = $this->get_file_prefix(); $region = $this->get_setting( 'region' ); if ( is_wp_error( $region ) ) { $region = ''; } $domain = $this->get_s3_url_domain( $bucket, $region ); $url = $scheme . '://' . $domain . '/' . $path . $suffix; // replace hyphens with non breaking hyphens for formatting if ( $escape ) { $url = str_replace( '-', '‑', $url ); } return $url; } /** * AJAX handler for get_url_preview() */ function ajax_get_url_preview() { $this->verify_ajax_request(); $url = $this->get_url_preview(); $out = array( 'success' => '1', 'url' => $url, ); $this->end_ajax( $out ); } /** * Delete bulk objects from an S3 bucket * * @param string $region * @param string $bucket * @param array $objects * @param bool $log_error * @param bool $return_on_error * @param bool $force_new_s3_client if we are deleting in bulk, force new S3 client * to cope with possible different regions */ function delete_s3_objects( $region, $bucket, $objects, $log_error = false, $return_on_error = false, $force_new_s3_client = false ) { try { $this->get_s3client( $region, $force_new_s3_client )->deleteObjects( array( 'Bucket' => $bucket, 'Objects' => $objects, ) ); } catch ( Exception $e ) { if ( $log_error ) { error_log( 'Error removing files from S3: ' . $e->getMessage() ); } if ( $return_on_error ) { return; } } } /** * Removes an attachment's files from S3. * * @param int $post_id * @param array $s3object * @param bool $remove_backup_sizes remove previous edited image versions * @param bool $log_error * @param bool $return_on_error * @param bool $force_new_s3_client if we are deleting in bulk, force new S3 client * to cope with possible different regions */ function remove_attachment_files_from_s3( $post_id, $s3object, $remove_backup_sizes = true, $log_error = false, $return_on_error = false, $force_new_s3_client = false ) { $prefix = trailingslashit( dirname( $s3object['key'] ) ); $bucket = $s3object['bucket']; $region = $this->get_s3object_region( $s3object ); $paths = $this->get_attachment_file_paths( $post_id, false, false, $remove_backup_sizes ); if ( is_wp_error( $region ) ) { $region = ''; } $objects_to_remove = array(); foreach ( $paths as $path ) { $objects_to_remove[] = array( 'Key' => $prefix . basename( $path ), ); } // finally delete the objects from S3 $this->delete_s3_objects( $region, $bucket, $objects_to_remove, $log_error, $return_on_error, $force_new_s3_client ); } /** * Removes an attachment and intermediate image size files from S3 * * @param int $post_id * @param bool $force_new_s3_client if we are deleting in bulk, force new S3 client * to cope with possible different regions */ function delete_attachment( $post_id, $force_new_s3_client = false ) { if ( ! $this->is_plugin_setup() ) { return; } if ( ! ( $s3object = $this->get_attachment_s3_info( $post_id ) ) ) { return; } $this->remove_attachment_files_from_s3( $post_id, $s3object, true, true, true, $force_new_s3_client ); delete_post_meta( $post_id, 'amazonS3_info' ); } /** * Handles the upload of the attachment to S3 when an attachment is updated using * the 'wp_update_attachment_metadata' filter * * @param array $data meta data for attachment * @param int $post_id * * @return array */ function wp_update_attachment_metadata( $data, $post_id ) { if ( ! $this->is_plugin_setup() ) { return $data; } if ( ! ( $old_s3object = $this->get_attachment_s3_info( $post_id ) ) && ! $this->get_setting( 'copy-to-s3' ) ) { // abort if not already uploaded to S3 and the copy setting is off return $data; } // allow S3 upload to be cancelled for any reason $pre = apply_filters( 'as3cf_pre_update_attachment_metadata', false, $data, $post_id, $old_s3object ); if ( false !== $pre ) { return $data; } // upload attachment to S3 $this->upload_attachment_to_s3( $post_id, $data ); return $data; } /** * Upload attachment to S3 * * @param int $post_id * @param array|null $data * @param string|null $file_path * @param bool $force_new_s3_client if we are uploading in bulk, force new S3 client * to cope with possible different regions * @param bool $remove_local_files * * @return array|WP_Error $s3object|$meta If meta is supplied, return it. Else return S3 meta */ function upload_attachment_to_s3( $post_id, $data = null, $file_path = null, $force_new_s3_client = false, $remove_local_files = true ) { if ( is_null( $data ) ) { $data = wp_get_attachment_metadata( $post_id, true ); } if ( is_null( $file_path ) ) { $file_path = get_attached_file( $post_id, true ); } // Check file exists locally before attempting upload if ( ! file_exists( $file_path ) ) { return new WP_Error( 'exception', sprintf( __( 'File %s does not exist', 'as3cf' ), $file_path ) ); } $file_name = basename( $file_path ); $type = get_post_mime_type( $post_id ); $allowed_types = $this->get_allowed_mime_types(); // check mime type of file is in allowed S3 mime types if ( ! in_array( $type, $allowed_types ) ) { return new WP_Error( 'exception', sprintf( __( 'Mime type %s is not allowed', 'as3cf' ), $type ) ); } $acl = self::DEFAULT_ACL; // check the attachment already exists in S3, eg. edit or restore image if ( ( $old_s3object = $this->get_attachment_s3_info( $post_id ) ) ) { // use existing non default ACL if attachment already exists if ( isset( $old_s3object['acl'] ) ) { $acl = $old_s3object['acl']; } // use existing prefix $prefix = trailingslashit( dirname( $old_s3object['key'] ) ); // use existing bucket $bucket = $old_s3object['bucket']; // get existing region if ( isset( $old_s3object['region'] ) ) { $region = $old_s3object['region']; }; } else { // derive prefix from various settings if ( isset( $data['file'] ) ) { $time = $this->get_folder_time_from_url( $data['file'] ); } else { $time = $this->get_attachment_folder_time( $post_id ); $time = date( 'Y/m', $time ); } $prefix = $this->get_file_prefix( $time, $post_id ); // use bucket from settings $bucket = $this->get_setting( 'bucket' ); $region = $this->get_setting( 'region' ); if ( is_wp_error( $region ) ) { $region = ''; } } $acl = apply_filters( 'wps3_upload_acl', $acl, $type, $data, $post_id, $this ); // Old naming convention, will be deprecated soon $acl = apply_filters( 'as3cf_upload_acl', $acl, $data, $post_id ); $s3object = array( 'bucket' => $bucket, 'key' => $prefix . $file_name, 'region' => $region, ); // store acl if not default if ( $acl != self::DEFAULT_ACL ) { $s3object['acl'] = $acl; } $s3client = $this->get_s3client( $region, $force_new_s3_client ); $args = array( 'Bucket' => $bucket, 'Key' => $prefix . $file_name, 'SourceFile' => $file_path, 'ACL' => $acl, ); // If far future expiration checked (10 years) if ( $this->get_setting( 'expires' ) ) { $args['Expires'] = date( 'D, d M Y H:i:s O', time() + 315360000 ); } $args = apply_filters( 'as3cf_object_meta', $args, $post_id ); do_action( 'as3cf_upload_attachment_pre_remove', $post_id, $s3object, $prefix, $args ); $files_to_remove = array(); if ( file_exists( $file_path ) ) { $files_to_remove[] = $file_path; try { $s3client->putObject( $args ); } catch ( Exception $e ) { $error_msg = sprintf( __( 'Error uploading %s to S3: %s', 'as3cf' ), $file_path, $e->getMessage() ); error_log( $error_msg ); return new WP_Error( 'exception', $error_msg ); } } delete_post_meta( $post_id, 'amazonS3_info' ); add_post_meta( $post_id, 'amazonS3_info', $s3object ); $file_paths = $this->get_attachment_file_paths( $post_id, true, $data ); $additional_images = array(); $filesize_total = 0; $remove_local_files_setting = $this->get_setting( 'remove-local-file' ); if ( $remove_local_files_setting ) { $bytes = filesize( $file_path ); if ( false !== $bytes ) { // Store in the attachment meta data for use by WP $data['filesize'] = $bytes; // Update metadata with filesize update_post_meta( $post_id, '_wp_attachment_metadata', $data ); // Add to the file size total $filesize_total += $bytes; } } foreach ( $file_paths as $file_path ) { if ( ! in_array( $file_path, $files_to_remove ) ) { $additional_images[] = array( 'Key' => $prefix . basename( $file_path ), 'SourceFile' => $file_path, ); $files_to_remove[] = $file_path; if ( $remove_local_files_setting ) { // Record the file size for the additional image $bytes = filesize( $file_path ); if ( false !== $bytes ) { $filesize_total += $bytes; } } } } foreach ( $additional_images as $image ) { try { $args = array_merge( $args, $image ); $args['ACL'] = self::DEFAULT_ACL; $s3client->putObject( $args ); } catch ( Exception $e ) { error_log( 'Error uploading ' . $args['SourceFile'] . ' to S3: ' . $e->getMessage() ); } } if ( $remove_local_files ) { if ( $remove_local_files_setting ) { // Allow other functions to remove files after they have processed $files_to_remove = apply_filters( 'as3cf_upload_attachment_local_files_to_remove', $files_to_remove, $post_id, $file_path ); // Remove duplicates $files_to_remove = array_unique( $files_to_remove ); // Delete the files $this->remove_local_files( $files_to_remove ); } } // Store the file size in the attachment meta if we are removing local file if ( $remove_local_files_setting ) { if ( $filesize_total > 0 ) { // Add the total file size for all image sizes update_post_meta( $post_id, 'wpos3_filesize_total', $filesize_total ); } } else { if ( isset( $data['filesize'] ) ) { // Make sure we don't have a cached file sizes in the meta unset( $data['filesize'] ); // Remove the filesize from the metadata update_post_meta( $post_id, '_wp_attachment_metadata', $data ); delete_post_meta( $post_id, 'wpos3_filesize_total' ); } } return $s3object; } /** * Remove files from the local site * * @param array $file_paths array of files to remove */ function remove_local_files( $file_paths ) { foreach ( $file_paths as $path ) { if ( ! @unlink( $path ) ) { error_log( 'Error removing local file ' . $path ); } } } function get_hidpi_file_path( $orig_path ) { $hidpi_suffix = apply_filters( 'as3cf_hidpi_suffix', '@2x' ); $pathinfo = pathinfo( $orig_path ); return $pathinfo['dirname'] . '/' . $pathinfo['filename'] . $hidpi_suffix . '.' . $pathinfo['extension']; } /** * Get the object versioning string prefix * * @param int $post_id * * @return string */ function get_object_version_string( $post_id ) { if ( $this->get_setting( 'use-yearmonth-folders' ) ) { $date_format = 'dHis'; } else { $date_format = 'YmdHis'; } $time = $this->get_attachment_folder_time( $post_id ); $object_version = date( $date_format, $time ) . '/'; $object_version = apply_filters( 'as3cf_get_object_version_string', $object_version ); return $object_version; } /** * Get the upload folder time from given URL * * @param string $url * * @return null|string */ function get_folder_time_from_url( $url ) { preg_match( '@[0-9]{4}/[0-9]{2}@', $url, $matches ); if ( isset( $matches[0] ) ) { return $matches[0]; } return null; } /** * Get the time of attachment upload. * * Use post datetime if attached. * * @param int $post_id * * @return int|string */ function get_attachment_folder_time( $post_id ) { $time = current_time( 'timestamp' ); if ( ! ( $attach = get_post( $post_id ) ) ) { return $time; } if ( ! $attach->post_parent ) { return $time; } if ( ! ( $post = get_post( $attach->post_parent ) ) ) { return $time; } if ( substr( $post->post_date_gmt, 0, 4 ) > 0 ) { return strtotime( $post->post_date_gmt . ' +0000' ); } return $time; } /** * Create unique names for file to be uploaded to AWS * This only applies when the remove local file option is enabled * * @param array $file An array of data for a single file. * * @return array $file The altered file array with AWS unique filename. */ function wp_handle_upload_prefilter( $file ) { if ( ! $this->get_setting( 'copy-to-s3' ) || ! $this->is_plugin_setup() ) { return $file; } // only do this when we are removing local versions of files if ( ! $this->get_setting( 'remove-local-file' ) ) { return $file; } $filename = $file['name']; // sanitize the file name before we begin processing $filename = sanitize_file_name( $filename ); // separate the filename into a name and extension $info = pathinfo( $filename ); $ext = ! empty( $info['extension'] ) ? '.' . $info['extension'] : ''; $name = basename( $filename, $ext ); // edge case: if file is named '.ext', treat as an empty name if ( $name === $ext ) { $name = ''; } // rebuild filename with lowercase extension as S3 will have converted extension on upload $ext = strtolower( $ext ); $filename = $info['filename'] . $ext; $time = current_time( 'timestamp' ); $time = date( 'Y/m', $time ); $prefix = ltrim( trailingslashit( $this->get_object_prefix() ), '/' ); $prefix .= ltrim( trailingslashit( $this->get_dynamic_prefix( $time ) ), '/' ); $bucket = $this->get_setting( 'bucket' ); $region = $this->get_setting( 'region' ); if ( is_wp_error( $region ) ) { return $file; } $s3client = $this->get_s3client( $region ); $number = ''; while ( $s3client->doesObjectExist( $bucket, $prefix . $filename ) !== false ) { $previous = $number; ++$number; if ( '' == $previous ) { $filename = $name . $number . $ext; } else { $filename = str_replace( "$previous$ext", $number . $ext, $filename ); } } $file['name'] = $filename; return $file; } function wp_get_attachment_url( $url, $post_id ) { $new_url = $this->get_attachment_url( $post_id ); if ( false === $new_url ) { return $url; } $new_url = apply_filters( 'wps3_get_attachment_url', $new_url, $post_id, $this ); // Old naming convention, will be deprecated soon $new_url = apply_filters( 'as3cf_wp_get_attachment_url', $new_url, $post_id ); return $new_url; } function get_attachment_s3_info( $post_id ) { return get_post_meta( $post_id, 'amazonS3_info', true ); } /** * Check the plugin is correctly setup * * @return bool */ function is_plugin_setup() { if ( is_wp_error( $this->aws->get_client() ) ) { // AWS not configured return false; } if ( false === (bool) $this->get_setting( 'bucket' ) ) { // No bucket selected return false; } if ( is_wp_error( $this->get_setting( 'region' ) ) ) { // Region error when retrieving bucket location return false; } // All good, let's do this return true; } /** * Generate a link to download a file from Amazon S3 using query string * authentication. This link is only valid for a limited amount of time. * * @param int $post_id Post ID of the attachment * @param int|null $expires Seconds for the link to live * @param string|null $size Size of the image to get * @param array $headers Header overrides for request * * @return mixed|void|WP_Error */ function get_secure_attachment_url( $post_id, $expires = null, $size = null, $headers = array() ) { if ( is_null( $expires ) ) { $expires = self::DEFAULT_EXPIRES; } return $this->get_attachment_url( $post_id, $expires, $size, null, $headers ); } /** * Return the scheme to be used in URLs * * @param string|null $ssl * * @return string */ function get_s3_url_scheme( $ssl = null ) { if ( $this->use_ssl( $ssl ) ) { $scheme = 'https'; } else { $scheme = 'http'; } return $scheme; } /** * Determine when to use https in URLS * * @param string|null $ssl * * @return bool */ function use_ssl( $ssl = null ) { $use_ssl = false; if ( is_null( $ssl ) ) { $ssl = $this->get_setting( 'ssl' ); } if ( 'request' == $ssl && is_ssl() ) { $use_ssl = true; } else if ( 'https' == $ssl ) { $use_ssl = true; } return apply_filters( 'as3cf_use_ssl', $use_ssl ); } /** * Get the custom object prefix if enabled * * @return string */ function get_object_prefix() { if ( $this->get_setting( 'enable-object-prefix' ) ) { $prefix = trim( $this->get_setting( 'object-prefix' ) ); } else { $prefix = ''; } return $prefix; } /** * Get the file prefix * * @param null|string $time * @param null|int $post_id * * @return string */ function get_file_prefix( $time = null, $post_id = null ) { $prefix = ltrim( trailingslashit( $this->get_object_prefix() ), '/' ); $prefix .= ltrim( trailingslashit( $this->get_dynamic_prefix( $time ) ), '/' ); if ( $this->get_setting( 'object-versioning' ) ) { $prefix .= $this->get_object_version_string( $post_id ); } return $prefix; } /** * Get the region specific prefix for S3 URL * * @param string $region * @param null|int $expires * * @return string */ function get_s3_url_prefix( $region = '', $expires = null ) { $prefix = 's3'; if ( '' !== $region ) { $delimiter = '-'; if ( 'eu-central-1' == $region && ! is_null( $expires ) ) { // if we are creating a secure URL for a Frankfurt base file use the alternative delimiter // http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region $delimiter = '.'; } $prefix .= $delimiter . $region; } return $prefix; } /** * Get the S3 url for the files * * @param string $bucket * @param string $region * @param int $expires * @param array $args Allows you to specify custom URL settings * * @return mixed|string|void */ function get_s3_url_domain( $bucket, $region = '', $expires = null, $args = array() ) { if ( ! isset( $args['cloudfront'] ) ) { $args['cloudfront'] = $this->get_setting( 'cloudfront' ); } if ( ! isset( $args['domain'] ) ) { $args['domain'] = $this->get_setting( 'domain' ); } if ( ! isset( $args['ssl'] ) ) { $args['ssl'] = $this->get_setting( 'ssl' ); } $prefix = $this->get_s3_url_prefix( $region, $expires ); if ( 'cloudfront' === $args['domain'] && is_null( $expires ) && $args['cloudfront'] ) { $s3_domain = $args['cloudfront']; } elseif ( 'virtual-host' === $args['domain'] ) { $s3_domain = $bucket; } elseif ( 'path' === $args['domain'] || $this->use_ssl( $args['ssl'] ) ) { $s3_domain = $prefix . '.amazonaws.com/' . $bucket; } else { $s3_domain = $bucket . '.' . $prefix . '.amazonaws.com'; } return $s3_domain; } /** * Get the url of the file from Amazon S3 * * @param int $post_id Post ID of the attachment * @param int|null $expires Seconds for the link to live * @param string|null $size Size of the image to get * @param array|null $meta Pre retrieved _wp_attachment_metadata for the attachment * @param array $headers Header overrides for request * * @return bool|mixed|void|WP_Error */ function get_attachment_url( $post_id, $expires = null, $size = null, $meta = null, $headers = array() ) { if ( ! $this->get_setting( 'serve-from-s3' ) ) { return false; } // check that the file has been uploaded to S3 if ( ! ( $s3object = $this->get_attachment_s3_info( $post_id ) ) ) { return false; } $scheme = $this->get_s3_url_scheme(); // We don't use $this->get_s3object_region() here because we don't want // to make an AWS API call and slow down page loading if ( isset( $s3object['region'] ) && self::DEFAULT_REGION !== $s3object['region'] ) { $region = $this->translate_region( $s3object['region'] ); } else { $region = ''; } // force use of secured url when ACL has been set to private if ( is_null( $expires ) && isset( $s3object['acl'] ) && self::PRIVATE_ACL == $s3object['acl'] ) { $expires = self::DEFAULT_EXPIRES; } $domain_bucket = $this->get_s3_url_domain( $s3object['bucket'], $region, $expires ); if ( ! is_null( $size ) ) { if ( is_null( $meta ) ) { $meta = get_post_meta( $post_id, '_wp_attachment_metadata', true ); } if ( isset( $meta['sizes'][ $size ]['file'] ) ) { $s3object['key'] = dirname( $s3object['key'] ) . '/' . $meta['sizes'][ $size ]['file']; } } if ( ! is_null( $expires ) ) { try { $expires = time() + $expires; $secure_url = $this->get_s3client( $region )->getObjectUrl( $s3object['bucket'], $s3object['key'], $expires, $headers ); return apply_filters( 'as3cf_get_attachment_secure_url', $secure_url, $s3object, $post_id, $expires, $headers ); } catch ( Exception $e ) { return new WP_Error( 'exception', $e->getMessage() ); } } $file = $this->encode_filename_in_path( $s3object['key'] ); $url = $scheme . '://' . $domain_bucket . '/' . $file; return apply_filters( 'as3cf_get_attachment_url', $url, $s3object, $post_id, $expires, $headers ); } /** * Override the attachment metadata * * @param unknown $data * @param unknown $post_id * * @return mixed */ function wp_get_attachment_metadata( $data, $post_id ) { return $this->maybe_encoded_file_of_resized_images( $data, $post_id ); } /** * Encodes the file names for resized image files for an attachment where necessary * * @param array $data * @param int $post_id * * @return mixed Attachment meta data */ function maybe_encoded_file_of_resized_images( $data, $post_id ) { if ( ! $this->get_setting( 'serve-from-s3' ) ) { return $data; } if ( ! ( $s3object = $this->get_attachment_s3_info( $post_id ) ) ) { return $data; } // we only need to encode the file name if url encoding is needed $filename = basename( $s3object['key'] ); if ( $filename == rawurlencode( $filename ) ) { return $data; } // we only need to encode resized image files if ( ! isset( $data['sizes'] ) ) { return $data; } foreach ( $data['sizes'] as $key => $size ) { $data['sizes'][ $key ]['file'] = $this->encode_filename_in_path( $data['sizes'][ $key ]['file'] ); } return $data; } /** * Encode file names according to RFC 3986 when generating urls * As per Amazon https://forums.aws.amazon.com/thread.jspa?threadID=55746#jive-message-244233 * * @param string $file * * @return string Encoded filename with path prefix untouched */ function encode_filename_in_path( $file ) { $file_path = dirname( $file ); $file_path = ( '.' != $file_path ) ? trailingslashit( $file_path ) : ''; $file_name = rawurlencode( basename( $file ) ); return $file_path . $file_name; } /** * Allow processes to update the file on S3 via update_attached_file() * * @param string $file * @param int $attachment_id * * @return string */ function update_attached_file( $file, $attachment_id ) { if ( ! $this->is_plugin_setup() ) { return $file; } if ( ! ( $s3object = $this->get_attachment_s3_info( $attachment_id ) ) ) { return $file; } $file = apply_filters( 'as3cf_update_attached_file', $file, $attachment_id, $s3object ); return $file; } /** * Return the S3 URL when the local file is missing * unless we know the calling process is and we are happy * to copy the file back to the server to be used * * @param string $file * @param int $attachment_id * * @return string */ function get_attached_file( $file, $attachment_id ) { if ( file_exists( $file ) || ! $this->get_setting( 'serve-from-s3' ) ) { return $file; } if ( ! ( $s3object = $this->get_attachment_s3_info( $attachment_id ) ) ) { return $file; } $url = $this->get_attachment_url( $attachment_id ); // return the URL by default $file = apply_filters( 'as3cf_get_attached_file', $url, $file, $attachment_id, $s3object ); return $file; } /** * Helper method for returning data to AJAX call * * @param array $return */ function end_ajax( $return = array() ) { echo json_encode( $return ); exit; } function verify_ajax_request() { if ( ! is_admin() || ! wp_verify_nonce( sanitize_key( $_POST['_nonce'] ), sanitize_key( $_POST['action'] ) ) ) { // input var okay wp_die( __( 'Cheatin’ eh?', 'as3cf' ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( __( 'You do not have sufficient permissions to access this page.', 'as3cf' ) ); } } function ajax_check_bucket() { if ( ! isset( $_POST['bucket_name'] ) || ! ( $bucket = sanitize_text_field( $_POST['bucket_name'] ) ) ) { // input var okay $out = array( 'error' => __( 'No bucket name provided.', 'as3cf' ) ); $this->end_ajax( $out ); } return strtolower( $bucket ); } /** * Handler for AJAX callback to create a bucket in S3 */ function ajax_create_bucket() { $this->verify_ajax_request(); $bucket = $this->ajax_check_bucket(); if ( defined( 'AS3CF_REGION' ) ) { // Are we defining the region? $region = AS3CF_REGION; } else { // Are we specifying the region via the form? $region = isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : null; // input var okay } $result = $this->create_bucket( $bucket, $region ); if ( is_wp_error( $result ) ) { $out = $this->prepare_bucket_error( $result, false ); $this->end_ajax( $out ); } // check if we were previously selecting a bucket manually via the input $previous_manual_bucket_select = $this->get_setting( 'manual_bucket', false ); $args = array( '_nonce' => wp_create_nonce( 'as3cf-create-bucket' ) ); $this->save_bucket_for_ajax( $bucket, $previous_manual_bucket_select, $region, $args ); } /** * Create an S3 bucket * * @param string $bucket_name * @param null|string $region option location constraint * * @return bool|WP_Error */ function create_bucket( $bucket_name, $region = null ) { try { $args = array( 'Bucket' => $bucket_name ); if ( defined( 'AS3CF_REGION' ) ) { // Make sure we always use the defined region $region = AS3CF_REGION; } if ( ! is_null( $region ) && self::DEFAULT_REGION !== $region ) { $args['LocationConstraint'] = $region; } $this->get_s3client()->createBucket( $args ); } catch ( Exception $e ) { return new WP_Error( 'exception', $e->getMessage() ); } return true; } /** * Handler for AJAX callback to save the selection of a bucket */ function ajax_save_bucket() { $this->verify_ajax_request(); $bucket = $this->ajax_check_bucket(); $manual = false; // are we inputting the bucket manually? if ( isset( $_POST['action'] ) && false !== strpos( $_POST['action'], 'manual-save-bucket' ) ) { $manual = true; } $this->save_bucket_for_ajax( $bucket, $manual ); } /** * Wrapper method for saving a bucket when creating or selecting * * @param string $bucket * @param bool|false $manual_select * @param null|string $region * @param array $defaults */ function save_bucket_for_ajax( $bucket, $manual_select = false, $region = null, $defaults = array() ) { $region = $this->save_bucket( $bucket, $manual_select, $region ); if ( ! is_wp_error( $region ) ) { $out = array( 'success' => '1', 'region' => $region, ); $out = wp_parse_args( $out, $defaults ); $can_write = $this->check_write_permission( $bucket, $region ); if ( is_wp_error( $can_write ) ) { $out = $this->prepare_bucket_error( $can_write ); } else { $out['can_write'] = $can_write; } } else { $out = $this->prepare_bucket_error( $region ); } $this->end_ajax( $out ); } /** * Prepare the bucket error before returning to JS * * @param WP_Error $object * @param bool $single Are we dealing with a single bucket? * * @return array */ function prepare_bucket_error( $object, $single = true ) { if ( 'Access Denied' === $object->get_error_message() ) { // If the bucket error is access denied, show our notice message $out = array( 'error' => $this->get_access_denied_notice_message( $single ) ); } else { $out = array( 'error' => $object->get_error_message() ); } return $out; } /** * Perform custom actions before the setting is saved * * @param string $key * @param string $value */ function pre_set_setting( $key, $value ) { if ( 'bucket' === $key && ! $this->get_setting( 'bucket' ) ) { // first time bucket select - enable main options by default $this->set_setting( 'copy-to-s3', '1' ); $this->set_setting( 'serve-from-s3', '1' ); } } /** * Save bucket and bucket's region * * @param string $bucket_name * @param bool $manual if we are entering the bucket via the manual input form * @param null|string $region * * @return string|bool region on success */ function save_bucket( $bucket_name, $manual = false, $region = null ) { if ( $bucket_name ) { $this->get_settings(); $this->set_setting( 'bucket', $bucket_name ); if ( is_null( $region ) ) { // retrieve the bucket region if not supplied $region = $this->get_bucket_region( $bucket_name ); if ( is_wp_error( $region ) ) { return $region; } } if ( self::DEFAULT_REGION === $region ) { $region = ''; } $this->set_setting( 'region', $region ); if ( $manual ) { // record that we have entered the bucket via the manual form $this->set_setting( 'manual_bucket', true ); } else { $this->remove_setting( 'manual_bucket' ); } $this->save_settings(); return $region; } return false; } /** * Get all AWS regions * * @return array */ function get_aws_regions() { $regionEnum = new ReflectionClass( 'Aws\Common\Enum\Region' ); $all_regions = $regionEnum->getConstants(); $regions = array(); foreach ( $all_regions as $label => $region ) { // Nicely format region name if ( self::DEFAULT_REGION === $region ) { $label = 'US Standard'; } else { $label = strtolower( $label ); $label = str_replace( '_', ' ', $label ); $label = ucwords( $label ); } $regions[ $region ] = $label; } return $regions; } /** * Add the settings menu item * * @param Amazon_Web_Services $aws */ function admin_menu( $aws ) { $hook_suffix = $aws->add_page( $this->get_plugin_page_title(), $this->plugin_menu_title, 'manage_options', $this->plugin_slug, array( $this, 'render_page' ) ); if ( false !== $hook_suffix ) { $this->hook_suffix = $hook_suffix; add_action( 'load-' . $this->hook_suffix, array( $this, 'plugin_load' ) ); } } /** * Get the S3 client * * @param bool|string $region specify region to client for signature * @param bool $force force return of new S3 client when swapping regions * * @return mixed */ function get_s3client( $region = false, $force = false ) { if ( is_null( $this->s3client ) || $force ) { if ( $region ) { $args = array( 'region' => $this->translate_region( $region ), 'signature' => 'v4', ); } else { $args = array(); } $this->s3client = $this->aws->get_client()->get( 's3', $args ); } return $this->s3client; } /** * Get the region of a bucket * * @param string $bucket * * @return string|WP_Error */ function get_bucket_region( $bucket ) { try { $region = $this->get_s3client()->getBucketLocation( array( 'Bucket' => $bucket ) ); } catch ( Exception $e ) { $error_msg_title = '' . __( 'Error Getting Bucket Region', 'as3cf' ) . ' —'; $error_msg = sprintf( __( 'There was an error attempting to get the region of the bucket %s: %s', 'as3cf' ), $bucket, $e->getMessage() ); error_log( $error_msg ); return new WP_Error( 'exception', $error_msg_title . $error_msg ); } $region = $this->translate_region( $region['Location'] ); return $region; } /** * Get the region of the bucket stored in the S3 metadata. * * * @param array $s3object * @param int $post_id - if supplied will update the s3 meta if no region found * * @return string|WP_Error - region name */ function get_s3object_region( $s3object, $post_id = null ) { if ( ! isset( $s3object['region'] ) ) { // if region hasn't been stored in the s3 metadata retrieve using the bucket $region = $this->get_bucket_region( $s3object['bucket'] ); if ( is_wp_error( $region ) ) { return $region; } $s3object['region'] = $region; if ( ! is_null( $post_id ) ) { // retrospectively update s3 metadata with region update_post_meta( $post_id, 'amazonS3_info', $s3object ); } } return $s3object['region']; } /** * Translate older bucket locations to newer S3 region names * http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region * * @param $region * * @return string */ function translate_region( $region ) { if ( ! is_string( $region ) ) { // Don't translate any region errors return $region; } $region = strtolower( $region ); switch ( $region ) { case 'eu': $region = 'eu-west-1'; break; } return $region; } /** * AJAX handler for get_buckets() */ function ajax_get_buckets() { $this->verify_ajax_request(); $result = $this->get_buckets(); if ( is_wp_error( $result ) ) { $out = $this->prepare_bucket_error( $result, false ); } else { $out = array( 'success' => '1', 'buckets' => $result, ); } $this->end_ajax( $out ); } /** * Get a list of buckets from S3 * * @return array|WP_Error - list of buckets */ function get_buckets() { try { $result = $this->get_s3client()->listBuckets(); } catch ( Exception $e ) { return new WP_Error( 'exception', $e->getMessage() ); } return $result['Buckets']; } /** * Checks the user has write permission for S3 * * @param string $bucket * @param string $region * * @return bool|WP_Error */ function check_write_permission( $bucket = null, $region = null ) { if ( is_null( $bucket ) ) { if ( ! ( $bucket = $this->get_setting( 'bucket' ) ) ) { // if no bucket set then no need check return true; } } if ( isset( self::$buckets_check[ $bucket ] ) ) { return self::$buckets_check[ $bucket ]; } $file_name = 'as3cf-permission-check.txt'; $file_contents = __( 'This is a test file to check if the user has write permission to S3. Delete me if found.', 'as3cf' ); $path = $this->get_object_prefix(); $key = $path . $file_name; $args = array( 'Bucket' => $bucket, 'Key' => $key, 'Body' => $file_contents, 'ACL' => 'public-read', ); try { // need to set region for buckets in non default region if ( is_null( $region ) ) { $region = $this->get_setting( 'region' ); if ( is_wp_error( $region ) ) { return $region; } } // attempt to create the test file $this->get_s3client( $region, true )->putObject( $args ); // delete it straight away if created $this->get_s3client()->deleteObject( array( 'Bucket' => $bucket, 'Key' => $key, ) ); $can_write = true; } catch ( Exception $e ) { // if we encounter an error that isn't access denied, throw that error if ( ! $e instanceof Aws\Common\Exception\ServiceResponseException || 'AccessDenied' !== $e->getExceptionCode() ) { $error_msg = sprintf( __( 'There was an error attempting to check the permissions of the bucket %s: %s', 'as3cf' ), $bucket, $e->getMessage() ); error_log( $error_msg ); return new WP_Error( 'exception', $error_msg ); } // write permission not found $can_write = false; } self::$buckets_check[ $bucket ] = $can_write; return $can_write; } /** * Render error messages in a view for bucket permission and access issues */ function render_bucket_permission_errors() { $can_write = $this->check_write_permission(); // catch any checking issues if ( is_wp_error( $can_write ) ) { $this->render_view( 'error-fatal', array( 'message' => $can_write->get_error_message() ) ); $can_write = true; } // display a error message if the user does not have write permission to S3 bucket $this->render_view( 'error-access', array( 'can_write' => $can_write ) ); } /** * Register modal scripts and styles so they can be enqueued later */ function register_modal_assets() { $version = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? time() : $this->plugin_version; $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; $src = plugins_url( 'assets/css/modal.css', $this->plugin_file_path ); wp_register_style( 'as3cf-modal', $src, array(), $version ); $src = plugins_url( 'assets/js/modal' . $suffix . '.js', $this->plugin_file_path ); wp_register_script( 'as3cf-modal', $src, array( 'jquery' ), $version, true ); } function plugin_load() { $version = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? time() : $this->plugin_version; $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; $src = plugins_url( 'assets/css/styles.css', $this->plugin_file_path ); wp_enqueue_style( 'as3cf-styles', $src, array( 'as3cf-modal' ), $version ); $src = plugins_url( 'assets/js/script' . $suffix . '.js', $this->plugin_file_path ); wp_enqueue_script( 'as3cf-script', $src, array( 'jquery', 'as3cf-modal' ), $version, true ); wp_localize_script( 'as3cf-script', 'as3cf', array( 'strings' => array( 'create_bucket_error' => __( 'Error creating bucket', 'as3cf' ), 'create_bucket_name_short' => __( 'Bucket name too short.', 'as3cf' ), 'create_bucket_name_long' => __( 'Bucket name too long.', 'as3cf' ), 'create_bucket_invalid_chars' => __( 'Invalid character. Bucket names can contain lowercase letters, numbers, periods and hyphens.', 'as3cf' ), 'save_bucket_error' => __( 'Error saving bucket', 'as3cf' ), 'get_buckets_error' => __( 'Error fetching buckets', 'as3cf' ), 'get_url_preview_error' => __( 'Error getting URL preview: ', 'as3cf' ), 'save_alert' => __( 'The changes you made will be lost if you navigate away from this page', 'as3cf' ) ), 'nonces' => array( 'create_bucket' => wp_create_nonce( 'as3cf-create-bucket' ), 'manual_bucket' => wp_create_nonce( 'as3cf-manual-save-bucket' ), 'get_buckets' => wp_create_nonce( 'as3cf-get-buckets' ), 'save_bucket' => wp_create_nonce( 'as3cf-save-bucket' ), 'get_url_preview' => wp_create_nonce( 'as3cf-get-url-preview' ), ), 'is_pro' => $this->is_pro(), ) ); $this->handle_post_request(); $this->http_prepare_download_log(); do_action( 'as3cf_plugin_load' ); } /** * Whitelist of settings allowed to be saved * * @return array */ function get_settings_whitelist() { return array( 'bucket', 'region', 'domain', 'virtual-host', 'expires', 'permissions', 'cloudfront', 'object-prefix', 'copy-to-s3', 'serve-from-s3', 'remove-local-file', 'ssl', 'hidpi-images', 'object-versioning', 'use-yearmonth-folders', 'enable-object-prefix', ); } /** * Handle the saving of the settings page */ function handle_post_request() { if ( empty( $_POST['plugin'] ) || $this->get_plugin_slug() != sanitize_key( $_POST['plugin'] ) ) { // input var okay return; } if ( empty( $_POST['action'] ) || 'save' != sanitize_key( $_POST['action'] ) ) { // input var okay return; } if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), $this->get_settings_nonce_key() ) ) { // input var okay die( __( "Cheatin' eh?", 'as3cf' ) ); } do_action( 'as3cf_pre_save_settings' ); $post_vars = $this->get_settings_whitelist(); foreach ( $post_vars as $var ) { $this->remove_setting( $var ); if ( ! isset( $_POST[ $var ] ) ) { // input var okay continue; } $value = sanitize_text_field( $_POST[ $var ] ); // input var okay $this->set_setting( $var, $value ); } $this->save_settings(); $url = $this->get_plugin_page_url( array( 'updated' => '1' ) ); wp_redirect( $url ); exit; } /** * Helper method to return the settings page URL for the plugin * * @param array $args * @param string $url_method To prepend to admin_url() * @param bool $escape Should we escape the URL * * @return string */ function get_plugin_page_url( $args = array(), $url_method = 'network', $escape = true ) { $default_args = array( 'page' => self::$plugin_page, ); $args = array_merge( $args, $default_args ); switch ( $url_method ) { case 'self': $base_url = self_admin_url( 'admin.php' ); break; case '': $base_url = admin_url( 'admin.php' ); break; default: $base_url = network_admin_url( 'admin.php' ); } // Add a hash to the URL $hash = false; if ( isset( $args['hash'] ) ) { $hash = $args['hash']; unset( $args['hash'] ); } else if ( $this->default_tab ) { $hash = $this->default_tab; } $url = add_query_arg( $args, $base_url ); if ( $hash ) { $url .= '#' . $hash; } if ( $escape ) { $url = esc_url_raw( $url ); } return $url; } /** * Display the main settings page for the plugin */ function render_page() { $this->aws->render_view( 'header', array( 'page_title' => $this->get_plugin_page_title(), 'page' => 'as3cf' ) ); $aws_client = $this->aws->get_client(); if ( is_wp_error( $aws_client ) ) { $this->render_view( 'error-fatal', array( 'message' => $aws_client->get_error_message() ) ); } else { $this->render_view( 'settings-tabs' ); do_action( 'as3cf_pre_settings_render' ); $this->render_view( 'settings' ); do_action( 'as3cf_post_settings_render' ); } $this->aws->render_view( 'footer' ); } /** * Get the tabs available for the plugin settings page * * @return array */ function get_settings_tabs() { $tabs = array( 'media' => _x( 'Media Library', 'Show the media library tab', 'as3cf' ), 'support' => _x( 'Support', 'Show the support tab', 'as3cf' ) ); return apply_filters( 'as3cf_settings_tabs', $tabs ); } /** * Get the prefix path for the files * * @param string $time * * @return string */ function get_dynamic_prefix( $time = null ) { $prefix = ''; if ( $this->get_setting( 'use-yearmonth-folders' ) ) { $uploads = wp_upload_dir( $time ); $prefix = str_replace( $this->get_base_upload_path(), '', $uploads['path'] ); } // support legacy MS installs (<3.5 since upgraded) for subsites if ( is_multisite() && ! ( is_main_network() && is_main_site() ) && false === strpos( $prefix, 'sites/' ) ) { $details = get_blog_details( get_current_blog_id() ); $legacy_ms_prefix = 'sites/' . $details->blog_id . '/'; $legacy_ms_prefix = apply_filters( 'as3cf_legacy_ms_subsite_prefix', $legacy_ms_prefix, $details ); $prefix = '/' . trailingslashit( ltrim( $legacy_ms_prefix, '/' ) ) . ltrim( $prefix, '/' ); } return $prefix; } /** * Get the base upload path * without the multisite subdirectory * * @return string */ function get_base_upload_path() { if ( defined( 'UPLOADS' ) && ! ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) ) { return ABSPATH . UPLOADS; } $upload_path = trim( get_option( 'upload_path' ) ); if ( empty( $upload_path ) || 'wp-content/uploads' == $upload_path ) { return WP_CONTENT_DIR . '/uploads'; } elseif ( 0 !== strpos( $upload_path, ABSPATH ) ) { // $dir is absolute, $upload_path is (maybe) relative to ABSPATH return path_join( ABSPATH, $upload_path ); } else { return $upload_path; } } /** * Get all the blog IDs for the multisite network used for table prefixes * * @return array */ function get_blog_ids() { $args = array( 'limit' => false, 'spam' => 0, 'deleted' => 0, 'archived' => 0, ); $blogs = wp_get_sites( $args ); $blog_ids = array(); foreach ( $blogs as $blog ) { $blog_ids[] = $blog['blog_id']; } return $blog_ids; } /** * Check whether the pro addon is installed. * * @return bool */ function is_pro() { if ( ! class_exists( 'Amazon_S3_And_CloudFront_Pro' ) ) { return false; } return true; } /** * Apply ACL to an attachment and associated files * * @param int $post_id * @param array $s3object * @param string $acl */ function set_attachment_acl_on_s3( $post_id, $s3object, $acl ) { // set ACL as private $args = array( 'ACL' => $acl, 'Bucket' => $s3object['bucket'], 'Key' => $s3object['key'], ); $region = ( isset( $s3object['region'] ) ) ? $s3object['region'] : false; $s3client = $this->get_s3client( $region, true ); try { $s3client->PutObjectAcl( $args ); $s3object['acl'] = $acl; // Add attachment to ACL update notice $message = $this->make_acl_admin_notice_text( $s3object ); $this->set_admin_notice( $message ); // update S3 meta data if ( $acl == self::DEFAULT_ACL ) { unset( $s3object['acl'] ); } update_post_meta( $post_id, 'amazonS3_info', $s3object ); } catch ( Exception $e ) { error_log( 'Error setting ACL to ' . $acl . ' for ' . $s3object['key'] . ': ' . $e->getMessage() ); } } /** * Make admin notice text for when object ACL has changed * * @param array $s3object * * @return string */ function make_acl_admin_notice_text( $s3object ) { $filename = basename( $s3object['key'] ); $acl = $this->get_acl_display_name( $s3object['acl'] ); return sprintf( __( 'The file %s has been given %s permissions on Amazon S3.', 'as3cf' ), "{$filename}", "{$acl}" ); } /** * Set admin notice * * @param string $message * @param string $type info, updated, error * @param bool $dismissible * @param bool $inline */ function set_admin_notice( $message, $type = 'info', $dismissible = true, $inline = false ) { self::$admin_notices[] = array( 'message' => $message, 'type' => $type, 'dismissible' => $dismissible, 'inline' => $inline, ); } /** * Save admin notices to transients before shutdown */ function save_admin_notices() { if ( ! empty( self::$admin_notices ) ) { set_site_transient( 'as3cf_notices', self::$admin_notices ); } } /** * Maybe show notices on admin page */ function maybe_show_admin_notices() { if ( $notices = get_site_transient( 'as3cf_notices' ) ) { foreach ( $notices as $notice ) { if ( 'info' === $notice['type'] ) { $notice['type'] = 'notice-info'; } $args = array( 'message' => $notice['message'], 'type' => $notice['type'], 'dismissible' => $notice['dismissible'], 'inline' => $notice['inline'], ); $this->render_view( 'notice', $args ); } delete_site_transient( 'as3cf_notices' ); } } /** * Diagnostic information for the support tab * * @param bool $escape */ function output_diagnostic_info( $escape = true ) { global $table_prefix; global $wpdb; echo 'site_url(): '; echo esc_html( site_url() ); echo "\r\n"; echo 'home_url(): '; echo esc_html( home_url() ); echo "\r\n"; echo 'Database Name: '; echo esc_html( $wpdb->dbname ); echo "\r\n"; echo 'Table Prefix: '; echo esc_html( $table_prefix ); echo "\r\n"; echo 'WordPress: '; echo bloginfo( 'version' ); if ( is_multisite() ) { echo ' Multisite'; } echo "\r\n"; echo 'Web Server: '; echo esc_html( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '' ); echo "\r\n"; echo 'PHP: '; if ( function_exists( 'phpversion' ) ) { echo esc_html( phpversion() ); } echo "\r\n"; echo 'MySQL: '; echo esc_html( empty( $wpdb->use_mysqli ) ? mysql_get_server_info() : mysqli_get_server_info( $wpdb->dbh ) ); echo "\r\n"; echo 'ext/mysqli: '; echo empty( $wpdb->use_mysqli ) ? 'no' : 'yes'; echo "\r\n"; echo 'WP Memory Limit: '; echo esc_html( WP_MEMORY_LIMIT ); echo "\r\n"; echo 'Blocked External HTTP Requests: '; if ( ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL ) { echo 'None'; } else { $accessible_hosts = ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) ? WP_ACCESSIBLE_HOSTS : ''; if ( empty( $accessible_hosts ) ) { echo 'ALL'; } else { echo 'Partially (Accessible Hosts: ' . esc_html( $accessible_hosts ) . ')'; } } echo "\r\n"; echo 'WP Locale: '; echo esc_html( get_locale() ); echo "\r\n"; echo 'Debug Mode: '; echo esc_html( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No' ); echo "\r\n"; echo 'WP Max Upload Size: '; echo esc_html( size_format( wp_max_upload_size() ) ); echo "\r\n"; echo 'PHP Time Limit: '; if ( function_exists( 'ini_get' ) ) { echo esc_html( ini_get( 'max_execution_time' ) ); } echo "\r\n"; echo 'PHP Error Log: '; if ( function_exists( 'ini_get' ) ) { echo esc_html( ini_get( 'error_log' ) ); } echo "\r\n"; echo 'fsockopen: '; if ( function_exists( 'fsockopen' ) ) { echo 'Enabled'; } else { echo 'Disabled'; } echo "\r\n"; echo 'OpenSSL: '; if ( $this->open_ssl_enabled() ) { echo esc_html( OPENSSL_VERSION_TEXT ); } else { echo 'Disabled'; } echo "\r\n"; echo 'cURL: '; if ( function_exists( 'curl_init' ) ) { echo 'Enabled'; } else { echo 'Disabled'; } echo "\r\n"; echo 'Zlib Compression: '; if ( function_exists( 'gzcompress' ) ) { echo 'Enabled'; } else { echo 'Disabled'; } echo "\r\n\r\n"; $media_counts = $this->diagnostic_media_counts(); echo 'Media Files: '; echo number_format_i18n( $media_counts['all'] ); echo "\r\n"; echo 'Media Files on S3: '; echo number_format_i18n( $media_counts['s3'] ); echo "\r\n"; echo 'Number of Image Sizes: '; $sizes = count( get_intermediate_image_sizes() ); echo number_format_i18n( $sizes ); echo "\r\n\r\n"; echo 'Bucket: '; echo $this->get_setting( 'bucket' ); echo "\r\n"; echo 'Region: '; $region = $this->get_setting( 'region' ); if ( ! is_wp_error( $region ) ) { echo $region; } echo "\r\n"; echo 'Copy Files to S3: '; echo $this->on_off( 'copy-to-s3' ); echo "\r\n"; echo 'Rewrite File URLs: '; echo $this->on_off( 'serve-from-s3' ); echo "\r\n"; echo "\r\n"; echo 'URL Preview: '; echo $this->get_url_preview( $escape ); echo "\r\n"; echo "\r\n"; echo 'Domain: '; echo $this->get_setting( 'domain' ); echo "\r\n"; echo 'Enable Path: '; echo $this->on_off( 'enable-object-prefix' ); echo "\r\n"; echo 'Custom Path: '; echo $this->get_setting( 'object-prefix' ); echo "\r\n"; echo 'Use Year/Month: '; echo $this->on_off( 'use-yearmonth-folders' ); echo "\r\n"; echo 'SSL: '; echo $this->get_setting( 'ssl' ); echo "\r\n"; echo 'Remove Files From Server: '; echo $this->on_off( 'remove-local-file' ); echo "\r\n"; echo 'Object Versioning: '; echo $this->on_off( 'object-versioning' ); echo "\r\n"; echo 'Far Future Expiration Header: '; echo $this->on_off( 'expires' ); echo "\r\n"; echo 'Copy HiDPI (@2x) Images: '; echo $this->on_off( 'hidpi-images' ); echo "\r\n\r\n"; do_action( 'as3cf_diagnostic_info' ); if ( has_action( 'as3cf_diagnostic_info' ) ) { echo "\r\n"; } echo "Active Plugins:\r\n"; $active_plugins = (array) get_option( 'active_plugins', array() ); if ( is_multisite() ) { $network_active_plugins = wp_get_active_network_plugins(); $active_plugins = array_map( array( $this, 'remove_wp_plugin_dir' ), $network_active_plugins ); } foreach ( $active_plugins as $plugin ) { $this->print_plugin_details( WP_PLUGIN_DIR . '/' . $plugin ); } } /** * Helper for displaying settings * * @param string $key setting key * * @return string */ function on_off( $key ) { $value = $this->get_setting( $key, 0 ); return ( 1 == $value ) ? 'On' : 'Off'; } /** * Helper to display plugin details * * @param string $plugin_path * @param string $suffix */ function print_plugin_details( $plugin_path, $suffix = '' ) { $plugin_data = get_plugin_data( $plugin_path ); if ( empty( $plugin_data['Name'] ) ) { return; } printf( "%s%s (v%s) by %s\r\n", $plugin_data['Name'], $suffix, $plugin_data['Version'], strip_tags( $plugin_data['AuthorName'] ) ); } /** * Helper to remove the plugin directory from the plugin path * * @param string $path * * @return string */ function remove_wp_plugin_dir( $path ) { $plugin = str_replace( WP_PLUGIN_DIR, '', $path ); return substr( $plugin, 1 ); } /** * Check for as3cf-download-log and related nonce and if found begin the * download of the diagnostic log * * @return void */ function http_prepare_download_log() { if ( isset( $_GET['as3cf-download-log'] ) && wp_verify_nonce( $_GET['nonce'], 'as3cf-download-log' ) ) { ob_start(); $this->output_diagnostic_info( false ); $log = ob_get_clean(); $url = parse_url( home_url() ); $host = sanitize_file_name( $url['host'] ); $filename = sprintf( '%s-diagnostic-log-%s.txt', $host, date( 'YmdHis' ) ); header( 'Content-Description: File Transfer' ); header( 'Content-Type: application/octet-stream' ); header( 'Content-Length: ' . strlen( $log ) ); header( 'Content-Disposition: attachment; filename=' . $filename ); echo $log; exit; } } /** * Return human friendly ACL name * * @param string $acl * * @return string */ function get_acl_display_name( $acl ) { return ucwords( str_replace( '-', ' ', $acl ) ); } /** * Detect if OpenSSL is enabled * * @return bool */ function open_ssl_enabled() { if ( defined( 'OPENSSL_VERSION_TEXT' ) ) { return true; } else { return false; } } /** * Is the current blog ID that specified in wp-config.php * * @param int $blog_id * * @return bool */ function is_current_blog( $blog_id ) { $default = defined( 'BLOG_ID_CURRENT_SITE' ) ? BLOG_ID_CURRENT_SITE : 1; if ( $default === $blog_id ) { return true; } return false; } /** * Helper to switch to a Multisite blog * - If the site is MS * - If the blog is not the current blog defined * * @param $blog_id */ function switch_to_blog( $blog_id ) { if ( is_multisite() && ! $this->is_current_blog( $blog_id ) ) { switch_to_blog( $blog_id ); } } /** * Helper to restore to the current Multisite blog * - If the site is MS * - If the blog is not the current blog defined * * @param $blog_id */ function restore_current_blog( $blog_id ) { if ( is_multisite() && ! $this->is_current_blog( $blog_id ) ) { restore_current_blog(); } } /** * Get all the table prefixes for the blogs in the site. MS compatible * * @param array $exclude_blog_ids blog ids to exclude * * @return array associative array with blog ID as key, prefix as value */ function get_all_blog_table_prefixes( $exclude_blog_ids = array() ) { global $wpdb; $prefix = $wpdb->prefix; $table_prefixes = array(); if ( ! in_array( 1, $exclude_blog_ids ) ) { $table_prefixes[1] = $prefix; } if ( is_multisite() ) { $blog_ids = $this->get_blog_ids(); foreach ( $blog_ids as $blog_id ) { if ( in_array( $blog_id, $exclude_blog_ids ) ) { continue; } $table_prefixes[ $blog_id ] = $wpdb->get_blog_prefix( $blog_id ); } } return $table_prefixes; } /** * Get file paths for all attachment versions. * * @param int $attachment_id * @param bool $exists_locally * @param array|bool $meta * @param bool $include_backups * * @return array */ public function get_attachment_file_paths( $attachment_id, $exists_locally = true, $meta = false, $include_backups = true ) { $paths = array(); $file_path = get_attached_file( $attachment_id, true ); $file_name = basename( $file_path ); $backups = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true ); if ( ! $meta ) { $meta = get_post_meta( $attachment_id, '_wp_attachment_metadata', true ); } $original_file = $file_path; // Not all attachments will have meta if ( isset( $meta['file'] ) ) { $original_file = str_replace( $file_name, basename( $meta['file'] ), $file_path ); } // Original file $paths[] = $original_file; // Sizes if ( isset( $meta['sizes'] ) ) { foreach ( $meta['sizes'] as $size ) { if ( isset( $size['file'] ) ) { $paths[] = str_replace( $file_name, $size['file'], $file_path ); } } } // Thumb if ( isset( $meta['thumb'] ) ) { $paths[] = str_replace( $file_name, $meta['thumb'], $file_path ); } // Backups if ( $include_backups && is_array( $backups ) ) { foreach ( $backups as $backup ) { $paths[] = str_replace( $file_name, $backup['file'], $file_path ); } } // HiDPI if ( $this->get_setting( 'hidpi-images' ) ) { foreach ( $paths as $path ) { $paths[] = $this->get_hidpi_file_path( $path ); } } // Remove duplicates $paths = array_unique( $paths ); // Remove paths that don't exist if ( $exists_locally ) { foreach ( $paths as $key => $path ) { if ( ! file_exists( $path ) ) { unset( $paths[ $key ] ); } } } return $paths; } /** * Get the access denied bucket error notice message * * @param bool $single * * @return string */ function get_access_denied_notice_message( $single = true ) { $quick_start = sprintf( '%s', 'https://deliciousbrains.com/wp-offload-s3/doc/quick-start-guide/', __( 'Quick Start Guide', 'as3cf' ) ); $message = sprintf( __( "Looks like we don't have write access to this bucket. It's likely that the user you've provided access keys for hasn't been granted the correct permissions. Please see our %s for instructions on setting up permissions correctly.", 'as3cf' ), $quick_start ); if ( ! $single ) { $message = sprintf( __( "Looks like we don't have access to the buckets. It's likely that the user you've provided access keys for hasn't been granted the correct permissions. Please see our %s for instructions on setting up permissions correctly.", 'as3cf' ), $quick_start ); } return $message; } /** * Used to give a realistic total of storage space used on a Multisite subsite, * when there have been attachments uploaded to S3 but removed from server * * @param $space_used bool * * @return float|int */ function multisite_get_spaced_used( $space_used ) { if ( false === ( $space_used = get_transient( 'wpos3_site_space_used' ) ) ) { global $wpdb; // Sum the total file size (including image sizes) for all S3 attachments $sql = "SELECT SUM( meta_value ) AS bytes_total FROM {$wpdb->postmeta} WHERE meta_key = 'wpos3_filesize_total'"; $space_used = $wpdb->get_var( $sql ); // Get local upload sizes $upload_dir = wp_upload_dir(); $space_used += get_dirsize( $upload_dir['basedir'] ); if ( $space_used > 0 ) { // Convert to bytes to MB $space_used = $space_used / 1024 / 1024; } set_transient( 'wpos3_site_space_used', $space_used, HOUR_IN_SECONDS ); } return $space_used; } /** * Memory exceeded * * Ensures the a process never exceeds 90% of the maximum WordPress memory. * * @param null|string $filter_name Name of filter to apply to the return * * @return bool */ public function memory_exceeded( $filter_name = null ) { $current_memory = memory_get_usage( true ); $memory_limit = ( intval( WP_MEMORY_LIMIT ) * 1024 * 1024 ) * 0.9; // 90% of max memory $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } if ( is_null( $filter_name ) || ! is_string( $filter_name ) ) { return $return; } return apply_filters( $filter_name, $return ); } /** * Count attachments on a site * * @param string $prefix * @param null|bool $uploaded_to_s3 * null - All attachments * true - Attachments only uploaded to S3 * false - Attachments not uploaded to S3 * * @return null|string */ public function count_attachments( $prefix, $uploaded_to_s3 = null ) { global $wpdb; $sql = "SELECT COUNT(*) FROM `{$prefix}posts` p"; $where = "WHERE p.post_type = 'attachment'"; if ( ! is_null( $uploaded_to_s3 ) && is_bool( $uploaded_to_s3 ) ) { $sql .= " LEFT OUTER JOIN `{$prefix}postmeta` pm ON p.`ID` = pm.`post_id` AND pm.`meta_key` = 'amazonS3_info'"; $operator = $uploaded_to_s3 ? 'not ' : ''; $where .= " AND pm.`post_id` is {$operator}null"; } $sql .= ' ' . $where; return $wpdb->get_var( $sql ); } /** * Get the total attachment and total S3 attachment counts for the diagnostic log * * @return array */ protected function diagnostic_media_counts() { if ( false === ( $attachment_counts = get_site_transient( 'wpos3_attachment_counts' ) ) ) { $table_prefixes = $this->get_all_blog_table_prefixes(); $all_media = 0; $all_media_s3 = 0; foreach ( $table_prefixes as $blog_id => $table_prefix ) { $count = $this->count_attachments( $table_prefix ); $all_media += $count; $s3_count = $this->count_attachments( $table_prefix, true ); $all_media_s3 += $s3_count; } $attachment_counts = array( 'all' => $all_media, 's3' => $all_media_s3, ); set_site_transient( 'wpos3_attachment_counts', $attachment_counts, 2 * HOUR_IN_SECONDS ); } return $attachment_counts; } }