From d254551dd712ebff4bce2c98f9530e72352ea7b9 Mon Sep 17 00:00:00 2001 From: Daryl Koopersmith Date: Thu, 15 Mar 2012 04:14:05 +0000 Subject: [PATCH] Theme Customizer: First pass for upload controls, using background image as an example. Add a wrapper for Plupload that allows for custom upload UIs. see #19910. wp.Uploader is a wrapper that provides a simple way to upload an attachment (using the wp_ajax_upload_attachment handler). It is intentionally decoupled from the UI. When an upload succeeds, it will receive the attachment information (id, url, meta, etc) as a JSON response. If the upload fails, the wrapper handles both WordPress and plupload errors through a single handler. As todos, we should add drag classes for the uploader dropzone and account for the rough 100mb filesize limit in most browsers. The UI for the customizer upload controls could be improved as well. git-svn-id: https://develop.svn.wordpress.org/trunk@20179 602fd350-edb4-49c9-b593-d223f7449a82 --- wp-admin/admin-ajax.php | 2 +- wp-admin/includes/ajax-actions.php | 44 ++++++ wp-includes/class-wp-customize-setting.php | 12 ++ wp-includes/class-wp-customize.php | 8 +- wp-includes/js/customize-controls.dev.js | 26 +++- wp-includes/js/plupload/wp-plupload.dev.js | 156 +++++++++++++++++++++ wp-includes/js/plupload/wp-plupload.js | 0 wp-includes/media.php | 43 ++++++ wp-includes/script-loader.php | 3 + 9 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 wp-includes/js/plupload/wp-plupload.dev.js create mode 100644 wp-includes/js/plupload/wp-plupload.js diff --git a/wp-admin/admin-ajax.php b/wp-admin/admin-ajax.php index d9060ceb94..ad2fdae669 100644 --- a/wp-admin/admin-ajax.php +++ b/wp-admin/admin-ajax.php @@ -45,7 +45,7 @@ $core_actions_post = array( 'menu-locations-save', 'menu-quick-search', 'meta-box-order', 'get-permalink', 'sample-permalink', 'inline-save', 'inline-save-tax', 'find_posts', 'widgets-order', 'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post', - 'wp-remove-post-lock', 'dismiss-wp-pointer', + 'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', ); // Register core Ajax calls. diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index 93d1b4a848..be77857844 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -1544,6 +1544,50 @@ function wp_ajax_save_widget() { wp_die(); } +function wp_ajax_upload_attachment() { + check_ajax_referer( 'media-form' ); + + if ( ! current_user_can( 'upload_files' ) ) + wp_die( -1 ); + + if ( isset( $_REQUEST['post_id'] ) ) { + $post_id = $_REQUEST['post_id']; + if ( ! current_user_can( 'edit_post', $post_id ) ) + wp_die( -1 ); + } else { + $post_id = null; + } + + $post_data = is_array( $_REQUEST['post_data'] ) ? $_REQUEST['post_data'] : array(); + + $attachment_id = media_handle_upload( 'async-upload', $post_id, $post_data ); + + if ( is_wp_error( $attachment_id ) ) { + echo json_encode( array( + 'type' => 'error', + 'data' => array( + 'message' => $attachment_id->get_error_message(), + 'filename' => $_FILES['async-upload']['name'], + ), + ) ); + wp_die(); + } + + $post = get_post( $attachment_id ); + + echo json_encode( array( + 'type' => 'success', + 'data' => array( + 'id' => $attachment_id, + 'title' => esc_attr( $post->post_title ), + 'filename' => esc_html( basename( $post->guid ) ), + 'url' => wp_get_attachment_url( $attachment_id ), + 'meta' => wp_get_attachment_metadata( $attachment_id ), + ), + ) ); + wp_die(); +} + function wp_ajax_image_editor() { $attachment_id = intval($_POST['postid']); if ( empty($attachment_id) || !current_user_can('edit_post', $attachment_id) ) diff --git a/wp-includes/class-wp-customize-setting.php b/wp-includes/class-wp-customize-setting.php index 710026c221..54b1eac7a7 100644 --- a/wp-includes/class-wp-customize-setting.php +++ b/wp-includes/class-wp-customize-setting.php @@ -70,6 +70,9 @@ class WP_Customize_Setting { wp_enqueue_script( 'farbtastic' ); wp_enqueue_style( 'farbtastic' ); break; + case 'upload': + wp_enqueue_script( 'wp-plupload' ); + break; } } @@ -394,6 +397,15 @@ class WP_Customize_Setting { + + control, $this ); diff --git a/wp-includes/class-wp-customize.php b/wp-includes/class-wp-customize.php index 68576c5011..1763a7f282 100644 --- a/wp-includes/class-wp-customize.php +++ b/wp-includes/class-wp-customize.php @@ -523,7 +523,13 @@ final class WP_Customize { 'section' => 'background', 'control' => 'color', 'default' => defined( 'BACKGROUND_COLOR' ) ? BACKGROUND_COLOR : '', - 'sanitize_callback' => 'sanitize_hexcolor' + 'sanitize_callback' => 'sanitize_hexcolor', + ) ); + + $this->add_setting( 'background_image', array( + 'label' => 'Background Image', + 'section' => 'background', + 'control' => 'upload', ) ); /* Nav Menus */ diff --git a/wp-includes/js/customize-controls.dev.js b/wp-includes/js/customize-controls.dev.js index 5787499bb9..0b4baf1ccb 100644 --- a/wp-includes/js/customize-controls.dev.js +++ b/wp-includes/js/customize-controls.dev.js @@ -70,6 +70,26 @@ } }); + api.UploadControl = api.Control.extend({ + initialize: function( id, value, options ) { + var control = this; + + api.Control.prototype.initialize.call( this, id, value, options ); + + this.uploader = new wp.Uploader({ + browser: this.container.find('.upload'), + success: function( attachment ) { + control.set( attachment.url ); + } + }); + + this.container.on( 'click', '.remove', function( event ) { + control.set(''); + event.preventDefault(); + }); + } + }); + // Change objects contained within the main customize object to Settings. api.defaultConstructor = api.Setting; @@ -194,7 +214,8 @@ * ===================================================================== */ api.controls = { - color: api.ColorControl + color: api.ColorControl, + upload: api.UploadControl }; $( function() { @@ -229,6 +250,9 @@ // Background color uses postMessage by default api('background_color').method = 'postMessage'; + + // api('background_image').method = 'postMessage'; + api('background_image').uploader.param( 'post_data', { context: 'custom-background' }); }); })( wp, jQuery ); \ No newline at end of file diff --git a/wp-includes/js/plupload/wp-plupload.dev.js b/wp-includes/js/plupload/wp-plupload.dev.js new file mode 100644 index 0000000000..8ad2738606 --- /dev/null +++ b/wp-includes/js/plupload/wp-plupload.dev.js @@ -0,0 +1,156 @@ +if ( typeof wp === 'undefined' ) + var wp = {}; + +(function( exports, $ ) { + /* + * An object that helps create a WordPress uploader using plupload. + * + * @param options - object - The options passed to the new plupload instance. + * Requires the following parameters: + * - container - The id of uploader container. + * - browser - The id of button to trigger the file select. + * - dropzone - The id of file drop target. + * - plupload - An object of parameters to pass to the plupload instance. + * - params - An object of parameters to pass to $_POST when uploading the file. + * Extends this.plupload.multipart_params under the hood. + * + * @param attributes - object - Attributes and methods for this specific instance. + */ + var Uploader = function( options ) { + var self = this, + elements = { + container: 'container', + browser: 'browse_button', + dropzone: 'drop_element' + }, + key; + + this.plupload = $.extend( { multipart_params: {} }, wpPluploadDefaults ); + this.container = document.body; // Set default container. + + // Extend the instance with options + // + // Use deep extend to allow options.plupload to override individual + // default plupload keys. + $.extend( true, this, options ); + + // Proxy all methods so this always refers to the current instance. + for ( key in this ) { + if ( $.isFunction( this[ key ] ) ) + this[ key ] = $.proxy( this[ key ], this ); + } + + // Ensure all elements are jQuery elements and have id attributes + // Then set the proper plupload arguments to the ids. + for ( key in elements ) { + if ( ! this[ key ] ) + continue; + + this[ key ] = $( this[ key ] ).first(); + if ( ! this[ key ].prop('id') ) + this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ ); + this.plupload[ elements[ key ] ] = this[ key ].prop('id'); + } + + this.uploader = new plupload.Uploader( this.plupload ); + delete this.plupload; + + // Set default params and remove this.params alias. + this.param( this.params || {} ); + delete this.params; + + this.uploader.bind( 'Init', this.init ); + + this.uploader.init(); + + this.uploader.bind( 'UploadProgress', this.progress ); + + this.uploader.bind( 'FileUploaded', function( up, file, response ) { + response = JSON.parse( response.response ); + + if ( ! response || ! response.type || ! response.data ) + return self.error( pluploadL10n.default_error ); + + if ( 'error' === response.type ) + return self.error( response.data.message, response.data ); + + if ( 'success' === response.type ) + return self.success( response.data ); + + }); + + this.uploader.bind( 'Error', function( up, error ) { + var message = pluploadL10n.default_error, + key; + + // Check for plupload errors. + for ( key in Uploader.errorMap ) { + if ( error.code === plupload[ key ] ) { + message = Uploader.errorMap[ key ]; + break; + } + } + + self.error( message, error ); + up.refresh(); + }); + + this.uploader.bind( 'FilesAdded', function( up, files ) { + $.each( files, function() { + self.added( this ); + }); + + up.refresh(); + up.start(); + }); + }; + + Uploader.uuid = 0; + + Uploader.errorMap = { + 'FAILED': pluploadL10n.upload_failed, + 'FILE_EXTENSION_ERROR': pluploadL10n.invalid_filetype, + // 'FILE_SIZE_ERROR': '', + 'IMAGE_FORMAT_ERROR': pluploadL10n.not_an_image, + 'IMAGE_MEMORY_ERROR': pluploadL10n.image_memory_exceeded, + 'IMAGE_DIMENSIONS_ERROR': pluploadL10n.image_dimensions_exceeded, + 'GENERIC_ERROR': pluploadL10n.upload_failed, + 'IO_ERROR': pluploadL10n.io_error, + 'HTTP_ERROR': pluploadL10n.http_error, + 'SECURITY_ERROR': pluploadL10n.security_error + }; + + $.extend( Uploader.prototype, { + /** + * Acts as a shortcut to extending the uploader's multipart_params object. + * + * param( key ) + * Returns the value of the key. + * + * param( key, value ) + * Sets the value of a key. + * + * param( map ) + * Sets values for a map of data. + */ + param: function( key, value ) { + if ( arguments.length === 1 && typeof key === 'string' ) + return this.uploader.settings.multipart_params[ key ]; + + if ( arguments.length > 1 ) { + this.uploader.settings.multipart_params[ key ] = value; + } else { + $.extend( this.uploader.settings.multipart_params, key ); + } + }, + + init: function() {}, + error: function() {}, + success: function() {}, + added: function() {}, + progress: function() {}, + complete: function() {} + }); + + exports.Uploader = Uploader; +})( wp, jQuery ); \ No newline at end of file diff --git a/wp-includes/js/plupload/wp-plupload.js b/wp-includes/js/plupload/wp-plupload.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wp-includes/media.php b/wp-includes/media.php index e3b9008e34..e67563b87f 100644 --- a/wp-includes/media.php +++ b/wp-includes/media.php @@ -1441,3 +1441,46 @@ function wp_embed_handler_googlevideo( $matches, $attr, $url, $rawattr ) { return apply_filters( 'embed_googlevideo', '', $matches, $attr, $url, $rawattr ); } + +/** + * Prints default plupload arguments. + * + * @since 3.4.0 + */ +function wp_plupload_default_settings() { + global $wp_scripts; + + $max_upload_size = wp_max_upload_size(); + + $params = array( + 'action' => 'upload-attachment', + ); + $params = apply_filters( 'plupload_default_params', $params ); + + $params['_wpnonce'] = wp_create_nonce( 'media-form' ); + + $settings = array( + 'runtimes' => 'html5,silverlight,flash,html4', + 'file_data_name' => 'async-upload', // key passed to $_FILE. + 'multiple_queues' => true, + 'max_file_size' => $max_upload_size . 'b', + 'url' => admin_url( 'admin-ajax.php' ), + 'flash_swf_url' => includes_url( 'js/plupload/plupload.flash.swf' ), + 'silverlight_xap_url' => includes_url( 'js/plupload/plupload.silverlight.xap' ), + 'filters' => array( array( 'title' => __( 'Allowed Files' ), 'extensions' => '*') ), + 'multipart' => true, + 'urlstream_upload' => true, + 'multipart_params' => $params, + ); + + $settings = apply_filters( 'plupload_default_settings', $settings ); + + $script = 'var wpPluploadDefaults = ' . json_encode( $settings ) . ';'; + + $data = $wp_scripts->get_data( 'wp-plupload', 'data' ); + if ( $data ) + $script = "$data\n$script"; + + $wp_scripts->add_data( 'wp-plupload', 'data', $script ); +} +add_action( 'admin_init', 'wp_plupload_default_settings' ); diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php index 14f86bcb38..f1b9073345 100644 --- a/wp-includes/script-loader.php +++ b/wp-includes/script-loader.php @@ -228,6 +228,9 @@ function wp_default_scripts( &$scripts ) { $scripts->add( 'plupload-handlers', "/wp-includes/js/plupload/handlers$suffix.js", array('plupload-all', 'jquery') ); $scripts->localize( 'plupload-handlers', 'pluploadL10n', $uploader_l10n ); + $scripts->add( 'wp-plupload', "/wp-includes/js/plupload/wp-plupload$suffix.js", array('plupload-all', 'jquery', 'json2') ); + $scripts->localize( 'wp-plupload', 'pluploadL10n', $uploader_l10n ); + // keep 'swfupload' for back-compat. $scripts->add( 'swfupload', '/wp-includes/js/swfupload/swfupload.js', array(), '2201-20110113'); $scripts->add( 'swfupload-swfobject', '/wp-includes/js/swfupload/plugins/swfupload.swfobject.js', array('swfupload', 'swfobject'), '2201a');