diff --git a/src/wp-admin/css/site-icon.css b/src/wp-admin/css/site-icon.css
new file mode 100644
index 0000000000..8a4d428d8a
--- /dev/null
+++ b/src/wp-admin/css/site-icon.css
@@ -0,0 +1,57 @@
+/*------------------------------------------------------------------------------
+ 28.0 - Site Icon
+------------------------------------------------------------------------------*/
+
+.site-icon-image {
+ float: left;
+ margin: 0 20px 0 0;
+}
+
+.site-icon-content {
+ overflow: hidden;
+ padding: 10px;
+ position: relative;
+}
+
+.site-icon-crop-shell {
+ max-width: 720px;
+}
+
+.site-icon-crop-preview-shell {
+ float: right;
+ overflow: hidden;
+}
+
+.site-icon-crop-preview-shell h3 {
+ margin-top: 0;
+}
+
+.site-icon-crop-favicon-preview-shell {
+ margin-bottom: 20px;
+ position: relative;
+}
+
+.site-icon-crop-preview-favicon,
+.site-icon-browser-title {
+ height: 16px;
+ left: 88px;
+ overflow: hidden;
+ position: absolute;
+ top: 16px;
+}
+
+.site-icon-crop-preview-favicon {
+ width: 16px;
+}
+
+.site-icon-browser-title {
+ left: 109px;
+}
+
+.site-icon-crop-preview-homeicon {
+ -webkit-border-radius: 16px;
+ border-radius: 16px;
+ height: 64px;
+ overflow: hidden;
+ width: 64px;
+}
diff --git a/src/wp-admin/css/wp-admin.css b/src/wp-admin/css/wp-admin.css
index 13d66e20de..14c10f9cd9 100644
--- a/src/wp-admin/css/wp-admin.css
+++ b/src/wp-admin/css/wp-admin.css
@@ -10,4 +10,5 @@
@import url(about.css);
@import url(nav-menus.css);
@import url(widgets.css);
+@import url(site-icon.css);
@import url(l10n.css);
diff --git a/src/wp-admin/images/browser.png b/src/wp-admin/images/browser.png
new file mode 100644
index 0000000000..0747bdf206
Binary files /dev/null and b/src/wp-admin/images/browser.png differ
diff --git a/src/wp-admin/includes/admin.php b/src/wp-admin/includes/admin.php
index 426bb412dc..cee0a207bc 100644
--- a/src/wp-admin/includes/admin.php
+++ b/src/wp-admin/includes/admin.php
@@ -64,6 +64,9 @@ require_once(ABSPATH . 'wp-admin/includes/theme.php');
/** WordPress User Administration API */
require_once(ABSPATH . 'wp-admin/includes/user.php');
+/** WordPress Site Icon API */
+require_once(ABSPATH . 'wp-admin/includes/site-icon.php');
+
/** WordPress Update Administration API */
require_once(ABSPATH . 'wp-admin/includes/update.php');
diff --git a/src/wp-admin/includes/site-icon.php b/src/wp-admin/includes/site-icon.php
new file mode 100644
index 0000000000..8255b4debf
--- /dev/null
+++ b/src/wp-admin/includes/site-icon.php
@@ -0,0 +1,636 @@
+ 'site-icon-upload',
+ ) );
+ }
+
+ /**
+ * Removes site icon.
+ *
+ * @since 4.3.0
+ */
+ public function remove_site_icon() {
+ check_admin_referer( 'remove-site-icon' );
+
+ $this->delete_site_icon();
+
+ add_settings_error( 'site-icon', 'icon-removed', __( 'Site Icon removed.' ), 'updated' );
+ }
+
+ /**
+ * Uploading a site_icon is a 3 step process
+ *
+ * 1. Select the file to upload
+ * 2. Crop the file
+ * 3. Confirmation
+ *
+ * @since 4.3.0
+ */
+ public function upload_site_icon_page() {
+ $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'select_site_icon';
+
+ switch ( $action ) {
+ case 'select_site_icon':
+ $this->select_page();
+ break;
+
+ case 'crop_site_icon':
+ $this->crop_page();
+ break;
+
+ default:
+ wp_safe_redirect( admin_url( 'options-general.php#site-icon' ) );
+ exit;
+ }
+ }
+
+ /**
+ * Displays the site_icon form to upload the image.
+ *
+ * @since 4.3.0
+ */
+ public function select_page() {
+ ?>
+
+
+
+
+
+ 'site-icon',
+ 'action' => 'crop_site_icon',
+ ), wp_nonce_url( admin_url( 'options-general.php' ), 'crop-site-icon' ) ) );
+ ?>
+
+
+
+
+
+ handle_upload();
+ $attachment_id = $upload['attachment_id'];
+ $file = $upload['file'];
+ $url = $upload['url'];
+ }
+
+ $image_size = getimagesize( $file );
+
+ if ( $image_size[0] < $this->min_size ) {
+ add_settings_error( 'site-icon', 'too-small', sprintf( __( 'The selected image is smaller than %upx in width.' ), $this->min_size ) );
+
+ // back to step one
+ $this->select_page();
+
+ return;
+ }
+
+ if ( $image_size[1] < $this->min_size ) {
+ add_settings_error( 'site-icon', 'too-small', sprintf( __( 'The selected image is smaller than %upx in height.' ), $this->min_size ) );
+
+ // Back to step one.
+ $this->select_page();
+
+ return;
+ }
+
+ // Let's resize the image so that the user can easier crop a image that in the admin view.
+ $crop_height = absint( $this->page_crop * $image_size[1] / $image_size[0] );
+ $cropped = wp_crop_image( $attachment_id, 0, 0, 0, 0, $this->page_crop, $crop_height );
+ if ( ! $cropped || is_wp_error( $cropped ) ) {
+ wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) );
+ }
+ $cropped_size = getimagesize( $cropped );
+
+ // set default values (in case of no JS)
+ $crop_ratio = $image_size[0] / $cropped_size[0];
+ if ( $cropped_size[0] < $cropped_size[1] ) {
+ $crop_x = 0;
+ $crop_y = absint( ( $cropped_size[1] - $cropped_size[0] ) / 2 );
+ $crop_size = $cropped_size[0];
+ } elseif ( $cropped_size[0] > $cropped_size[1] ) {
+ $crop_x = absint( ( $cropped_size[0] - $cropped_size[1] ) / 2 );
+ $crop_y = 0;
+ $crop_size = $cropped_size[1];
+ } else {
+ $crop_x = 0;
+ $crop_y = 0;
+ $crop_size = $cropped_size[0];
+ }
+
+ wp_delete_file( $cropped );
+
+ wp_localize_script( 'site-icon-crop', 'wpSiteIconCropData', $this->initial_crop_data( $crop_ratio, $cropped_size ) );
+ ?>
+
+
+ delete_site_icon();
+
+ $attachment_id = absint( $_POST['attachment_id'] );
+
+ // TODO
+ if ( empty( $_POST['skip-cropping'] ) ) {
+ $crop_ratio = (float) $_POST['crop_ratio'];
+ $crop_data = $this->convert_coordinates_from_resized_to_full( $_POST['crop-x'], $_POST['crop-y'], $_POST['crop-w'], $_POST['crop-h'], $crop_ratio );
+ $cropped = wp_crop_image( $attachment_id, $crop_data['crop_x'], $crop_data['crop_y'], $crop_data['crop_width'], $crop_data['crop_height'], $this->min_size, $this->min_size );
+ } elseif ( ! empty( $_POST['create-new-attachment'] ) ) {
+ $cropped = _copy_image_file( $attachment_id );
+ } else {
+ $cropped = get_attached_file( $attachment_id );
+ }
+
+ if ( ! $cropped || is_wp_error( $cropped ) ) {
+ wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) );
+ }
+
+ $object = $this->create_attachment_object( $cropped, $attachment_id );
+
+ if ( ! empty( $_POST['create-new-attachment'] ) ) {
+ unset( $object['ID'] );
+ }
+
+ // Update the attachment
+ add_filter( 'intermediate_image_sizes_advanced', array( $this, 'additional_sizes' ) );
+ $attachment_id = $this->insert_attachment( $object, $cropped );
+ remove_filter( 'intermediate_image_sizes_advanced', array( $this, 'additional_sizes' ) );
+
+ // Save the site_icon data into option
+ update_option( 'site_icon', $attachment_id );
+
+ add_settings_error( 'site-icon', 'icon-updated', __( 'Site Icon updated.' ), 'updated' );
+ }
+
+ /**
+ * This function is used to pass data to the localize script
+ * so that we can center the cropper and also set the minimum
+ * cropper.
+ *
+ * @since 4.3.0
+ *
+ * @param float $ratio
+ * @param array $cropped_size
+ * @return array
+ */
+ public function initial_crop_data( $ratio, $cropped_size ) {
+ $init_x = $init_y = $init_size = 0;
+
+ $min_crop_size = ( $this->min_size / $ratio );
+ $resized_width = $cropped_size[0];
+ $resized_height = $cropped_size[1];
+
+ // Landscape format ( width > height )
+ if ( $resized_width > $resized_height ) {
+ $init_x = ( $this->page_crop - $resized_height ) / 2;
+ $init_size = $resized_height;
+ }
+
+ // Portrait format ( height > width )
+ if ( $resized_width < $resized_height ) {
+ $init_y = ( $this->page_crop - $resized_width ) / 2;
+ $init_size = $resized_height;
+ }
+
+ // Square height == width
+ if ( $resized_width == $resized_height ) {
+ $init_size = $resized_height;
+ }
+
+ return array(
+ 'init_x' => $init_x,
+ 'init_y' => $init_y,
+ 'init_size' => $init_size,
+ 'min_size' => $min_crop_size,
+ );
+ }
+
+ /**
+ * Converts the coordinates from the downsized image to the original image for accurate cropping.
+ *
+ * @since 4.3.0
+ *
+ * @param int $crop_x
+ * @param int $crop_y
+ * @param int $crop_width
+ * @param int $crop_height
+ * @param float $ratio
+ * @return array
+ */
+ public function convert_coordinates_from_resized_to_full( $crop_x, $crop_y, $crop_width, $crop_height, $ratio ) {
+ return array(
+ 'crop_x' => floor( $crop_x * $ratio ),
+ 'crop_y' => floor( $crop_y * $ratio ),
+ 'crop_width' => floor( $crop_width * $ratio ),
+ 'crop_height' => floor( $crop_height * $ratio ),
+ );
+ }
+
+ /**
+ * Upload the file to be cropped in the second step.
+ *
+ * @since 4.3.0
+ */
+ public function handle_upload() {
+ $uploaded_file = $_FILES['site-icon'];
+ $file_type = wp_check_filetype_and_ext( $uploaded_file['tmp_name'], $uploaded_file['name'] );
+ if ( ! wp_match_mime_types( 'image', $file_type['type'] ) ) {
+ wp_die( __( 'The uploaded file is not a valid image. Please try again.' ) );
+ }
+
+ $file = wp_handle_upload( $uploaded_file, array( 'test_form' => false ) );
+
+ if ( isset( $file['error'] ) ) {
+ wp_die( $file['error'], __( 'Image Upload Error' ) );
+ }
+
+ $url = $file['url'];
+ $type = $file['type'];
+ $file = $file['file'];
+ $filename = basename( $file );
+
+ // Construct the object array
+ $object = array(
+ 'post_title' => $filename,
+ 'post_content' => $url,
+ 'post_mime_type' => $type,
+ 'guid' => $url,
+ 'context' => 'site-icon',
+ );
+
+ // Save the data
+ $attachment_id = wp_insert_attachment( $object, $file );
+
+ return compact( 'attachment_id', 'file', 'filename', 'url', 'type' );
+ }
+
+ /**
+ * Create an attachment 'object'.
+ *
+ * @since 4.3.0
+ *
+ * @param string $cropped Cropped image URL.
+ * @param int $parent_attachment_id Attachment ID of parent image.
+ * @return array Attachment object.
+ */
+ public function create_attachment_object( $cropped, $parent_attachment_id ) {
+ $parent = get_post( $parent_attachment_id );
+ $parent_url = $parent->guid;
+ $url = str_replace( basename( $parent_url ), basename( $cropped ), $parent_url );
+
+ $size = @getimagesize( $cropped );
+ $image_type = ( $size ) ? $size['mime'] : 'image/jpeg';
+
+ $object = array(
+ 'ID' => $parent_attachment_id,
+ 'post_title' => basename( $cropped ),
+ 'post_content' => $url,
+ 'post_mime_type' => $image_type,
+ 'guid' => $url,
+ 'context' => 'site-icon'
+ );
+
+ return $object;
+ }
+
+ /**
+ * Insert an attachment and its metadata.
+ *
+ * @since 4.3.0
+ *
+ * @param array $object Attachment object.
+ * @param string $cropped Cropped image URL.
+ * @return int Attachment ID.
+ */
+ public function insert_attachment( $object, $cropped ) {
+ $attachment_id = wp_insert_attachment( $object, $cropped );
+ $metadata = wp_generate_attachment_metadata( $attachment_id, $cropped );
+
+ /**
+ * Filter the site icon attachment metadata.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_generate_attachment_metadata()
+ *
+ * @param array $metadata Attachment metadata.
+ */
+ $metadata = apply_filters( 'site_icon_attachment_metadata', $metadata );
+ wp_update_attachment_metadata( $attachment_id, $metadata );
+
+ return $attachment_id;
+ }
+
+ /**
+ * Add additional sizes to be made when creating the site_icon images.
+ *
+ * @since 4.3.0
+ *
+ * @param array $sizes
+ * @return array
+ */
+ public function additional_sizes( $sizes = array() ) {
+ $only_crop_sizes = array();
+
+ /**
+ * Filter the different dimensions that a site icon is saved in.
+ *
+ * @since 4.3.0
+ *
+ * @param array $site_icon_sizes Sizes available for the Site Icon.
+ */
+ $this->site_icon_sizes = apply_filters( 'site_icon_image_sizes', $this->site_icon_sizes );
+
+ // Use a natural sort of numbers.
+ natsort( $this->site_icon_sizes );
+ $this->site_icon_sizes = array_reverse( $this->site_icon_sizes );
+
+ // ensure that we only resize the image into
+ foreach ( $sizes as $name => $size_array ) {
+ if ( $size_array['crop'] ) {
+ $only_crop_sizes[ $name ] = $size_array;
+ }
+ }
+
+ foreach ( $this->site_icon_sizes as $size ) {
+ if ( $size < $this->min_size ) {
+ $only_crop_sizes[ 'site_icon-' . $size ] = array(
+ 'width ' => $size,
+ 'height' => $size,
+ 'crop' => true,
+ );
+ }
+ }
+
+ return $only_crop_sizes;
+ }
+
+ /**
+ * Add Site Icon sizes to the array of image sizes on demand.
+ *
+ * @since 4.3.0
+ *
+ * @param array $sizes
+ * @return array
+ */
+ public function intermediate_image_sizes( $sizes = array() ) {
+ /** This filter is documented in wp-admin/includes/site-icon.php */
+ $this->site_icon_sizes = apply_filters( 'site_icon_image_sizes', $this->site_icon_sizes );
+ foreach ( $this->site_icon_sizes as $size ) {
+ $sizes[] = 'site_icon-' . $size;
+ }
+
+ return $sizes;
+ }
+
+ /**
+ * Deletes all additional image sizes, used for site icons.
+ *
+ * @since 4.3.0
+ *
+ * @return bool
+ */
+ public function delete_site_icon() {
+ // We add the filter to make sure that we also delete all the added images.
+ add_filter( 'intermediate_image_sizes', array( $this, 'intermediate_image_sizes' ) );
+ wp_delete_attachment( get_option( 'site_icon' ), true );
+ remove_filter( 'intermediate_image_sizes', array( $this, 'intermediate_image_sizes' ) );
+
+ return delete_option( 'site_icon' );
+ }
+
+ /**
+ * Deletes the Site Icon when the image file is deleted.
+ *
+ * @since 4.3.0
+ *
+ * @param int $post_id Attachment ID.
+ */
+ public function delete_attachment_data( $post_id ) {
+ $site_icon_id = get_option( 'site_icon' );
+
+ if ( $site_icon_id && $post_id == $site_icon_id ) {
+ delete_option( 'site_icon' );
+ }
+ }
+
+ /**
+ * Adds custom image sizes when meta data for an image is requested, that happens to be used as Site Icon.
+ *
+ * @since 4.3.0
+ *
+ * @param null|array|string $value The value get_metadata() should
+ * return - a single metadata value,
+ * or an array of values.
+ * @param int $post_id Post ID.
+ * @param string $meta_key Meta key.
+ * @param string|array $single Meta value, or an array of values.
+ * @return array|null|string
+ */
+ public function get_post_metadata( $value, $post_id, $meta_key, $single ) {
+ $site_icon_id = get_option( 'site_icon' );
+
+ if ( $post_id == $site_icon_id && '_wp_attachment_backup_sizes' == $meta_key && $single ) {
+ add_filter( 'intermediate_image_sizes', array( $this, 'intermediate_image_sizes' ) );
+ }
+
+ return $value;
+ }
+}
+
+$GLOBALS['wp_site_icon'] = new WP_Site_Icon;
diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php
index 82155cfdbc..302e9b48dc 100644
--- a/src/wp-admin/includes/template.php
+++ b/src/wp-admin/includes/template.php
@@ -1778,13 +1778,17 @@ function _media_states( $post ) {
$media_states[] = __( 'Background Image' );
}
+ if ( $post->ID == get_option( 'site_icon' ) ) {
+ $media_states[] = __( 'Site Icon' );
+ }
+
/**
* Filter the default media display states for items in the Media list table.
*
* @since 3.2.0
*
* @param array $media_states An array of media states. Default 'Header Image',
- * 'Background Image'.
+ * 'Background Image', 'Site Icon'.
*/
$media_states = apply_filters( 'display_media_states', $media_states );
diff --git a/src/wp-admin/js/site-icon-crop.js b/src/wp-admin/js/site-icon-crop.js
new file mode 100644
index 0000000000..ebe727454e
--- /dev/null
+++ b/src/wp-admin/js/site-icon-crop.js
@@ -0,0 +1,54 @@
+/* global wpSiteIconCropData, jQuery */
+(function($) {
+ var jcrop_api = {},
+ siteIconCrop = {
+
+ updateCoords : function ( coords ) {
+ $( '#crop-x' ).val( coords.x );
+ $( '#crop-y' ).val( coords.y );
+ $( '#crop-width' ).val( coords.w );
+ $( '#crop-height' ).val( coords.h );
+
+ siteIconCrop.showPreview( coords );
+ },
+
+ showPreview : function( coords ){
+ var rx = 64 / coords.w,
+ ry = 64 / coords.h,
+ preview_rx = 16 / coords.w,
+ preview_ry = 16 / coords.h,
+ $cropImage = $( '#crop-image' ),
+ $homeIcon = $( '#preview-homeicon' ),
+ $favicon = $( '#preview-favicon' );
+
+ $homeIcon.css({
+ width: Math.round(rx * $cropImage.attr( 'width' ) ) + 'px',
+ height: Math.round(ry * $cropImage.attr( 'height' ) ) + 'px',
+ marginLeft: '-' + Math.round(rx * coords.x) + 'px',
+ marginTop: '-' + Math.round(ry * coords.y) + 'px'
+ });
+
+ $favicon.css({
+ width: Math.round( preview_rx * $cropImage.attr( 'width' ) ) + 'px',
+ height: Math.round( preview_ry * $cropImage.attr( 'height' ) ) + 'px',
+ marginLeft: '-' + Math.round( preview_rx * coords.x ) + 'px',
+ marginTop: '-' + Math.floor( preview_ry* coords.y ) + 'px'
+ });
+ },
+
+ ready: function() {
+ jcrop_api = $.Jcrop( '#crop-image' );
+ jcrop_api.setOptions({
+ aspectRatio: 1,
+ onSelect: siteIconCrop.updateCoords,
+ onChange: siteIconCrop.updateCoords,
+ minSize: [ wpSiteIconCropData.min_size, wpSiteIconCropData.min_size ]
+ });
+ jcrop_api.animateTo([wpSiteIconCropData.init_x, wpSiteIconCropData.init_y, wpSiteIconCropData.init_size, wpSiteIconCropData.init_size]);
+ }
+
+ };
+
+ siteIconCrop.ready();
+
+})(jQuery);
\ No newline at end of file
diff --git a/src/wp-admin/js/site-icon.js b/src/wp-admin/js/site-icon.js
new file mode 100644
index 0000000000..2328d529f6
--- /dev/null
+++ b/src/wp-admin/js/site-icon.js
@@ -0,0 +1,49 @@
+(function($) {
+ var frame;
+
+ $( function() {
+ // Build the choose from library frame.
+ $( '#choose-from-library-link' ).click( function( event ) {
+ var $el = $(this);
+ event.preventDefault();
+
+ // If the media frame already exists, reopen it.
+ if ( frame ) {
+ frame.open();
+ return;
+ }
+
+ // Create the media frame.
+ frame = wp.media.frames.customHeader = wp.media({
+ // Set the title of the modal.
+ title: $el.data('choose'),
+
+ // Tell the modal to show only images.
+ library: {
+ type: 'image'
+ },
+
+ // Customize the submit button.
+ button: {
+ // Set the text of the button.
+ text: $el.data('update'),
+ // Tell the button not to close the modal, since we're
+ // going to refresh the page when the image is selected.
+ close: false
+ }
+ });
+
+ // When an image is selected, run a callback.
+ frame.on( 'select', function() {
+ // Grab the selected attachment.
+ var attachment = frame.state().get('selection').first(),
+ link = $el.data('updateLink');
+
+ // Tell the browser to navigate to the crop step.
+ window.location = link + '&file=' + attachment.id;
+ });
+
+ frame.open();
+ });
+ });
+}(jQuery));
diff --git a/src/wp-admin/options-general.php b/src/wp-admin/options-general.php
index 7f2a14ea4f..b8175fbb09 100644
--- a/src/wp-admin/options-general.php
+++ b/src/wp-admin/options-general.php
@@ -124,6 +124,48 @@ include( ABSPATH . 'wp-admin/admin-header.php' );
|
+
+ |
+
+ 'site-icon',
+ 'action' => 'crop_site_icon',
+ ), wp_nonce_url( admin_url( 'options-general.php' ), 'crop-site-icon' ) ) );
+
+ wp_enqueue_media();
+ wp_enqueue_script( 'site-icon' );
+
+ if ( has_site_icon() ) :
+ $remove_url = add_query_arg( array(
+ 'action' => 'remove_site_icon',
+ ), wp_nonce_url( admin_url( 'options-general.php' ), 'remove-site-icon' ) );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
|
diff --git a/src/wp-admin/options.php b/src/wp-admin/options.php
index 752d7b46ff..3dc5d2dd9b 100644
--- a/src/wp-admin/options.php
+++ b/src/wp-admin/options.php
@@ -72,7 +72,7 @@ if ( is_multisite() && !is_super_admin() && 'update' != $action )
wp_die( __( 'Cheatin’ uh?' ), 403 );
$whitelist_options = array(
- 'general' => array( 'blogname', 'blogdescription', 'gmt_offset', 'date_format', 'time_format', 'start_of_week', 'timezone_string', 'WPLANG' ),
+ 'general' => array( 'blogname', 'blogdescription', 'site_icon', 'gmt_offset', 'date_format', 'time_format', 'start_of_week', 'timezone_string', 'WPLANG' ),
'discussion' => array( 'default_pingback_flag', 'default_ping_status', 'default_comment_status', 'comments_notify', 'moderation_notify', 'comment_moderation', 'require_name_email', 'comment_whitelist', 'comment_max_links', 'moderation_keys', 'blacklist_keys', 'show_avatars', 'avatar_rating', 'avatar_default', 'close_comments_for_old_posts', 'close_comments_days_old', 'thread_comments', 'thread_comments_depth', 'page_comments', 'comments_per_page', 'default_comments_page', 'comment_order', 'comment_registration' ),
'media' => array( 'thumbnail_size_w', 'thumbnail_size_h', 'thumbnail_crop', 'medium_size_w', 'medium_size_h', 'large_size_w', 'large_size_h', 'image_default_size', 'image_default_align', 'image_default_link_type' ),
'reading' => array( 'posts_per_page', 'posts_per_rss', 'rss_use_excerpt', 'show_on_front', 'page_on_front', 'page_for_posts', 'blog_public' ),
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 257cbfa3ad..b1f69ea98c 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -222,8 +222,9 @@ add_action( 'wp_head', 'wp_print_styles', 8 );
add_action( 'wp_head', 'wp_print_head_scripts', 9 );
add_action( 'wp_head', 'wp_generator' );
add_action( 'wp_head', 'rel_canonical' );
-add_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
add_action( 'wp_head', 'wp_shortlink_wp_head', 10, 0 );
+add_action( 'wp_head', 'wp_site_icon', 99 );
+add_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 );
add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
add_action( 'init', 'check_theme_switched', 99 );
@@ -243,6 +244,11 @@ foreach ( array( 'rss2_head', 'commentsrss2_head', 'rss_head', 'rdf_header', 'at
add_action( $action, 'the_generator' );
}
+// Feed Site Icon
+add_action( 'atom_head', 'atom_site_icon' );
+add_action( 'rss2_head', 'rss2_site_icon' );
+
+
// WP Cron
if ( !defined( 'DOING_CRON' ) )
add_action( 'init', 'wp_cron' );
diff --git a/src/wp-includes/feed.php b/src/wp-includes/feed.php
index 4554d0e461..93a6ddf268 100644
--- a/src/wp-includes/feed.php
+++ b/src/wp-includes/feed.php
@@ -587,6 +587,42 @@ function prep_atom_text_construct($data) {
}
}
+/**
+ * Display Site Icon in atom feeds.
+ *
+ * @since 4.3.0
+ */
+function atom_site_icon() {
+ $url = get_site_icon_url( null, 32 );
+ if ( $url ) {
+ echo "$url\n";
+ }
+}
+
+/**
+ * Display Site Icon in RSS2.
+ *
+ * @since 4.3.0
+ */
+function rss2_site_icon() {
+ $rss_title = get_wp_title_rss();
+ if ( empty( $rss_title ) ) {
+ $rss_title = get_bloginfo_rss( 'name' );
+ }
+
+ $url = get_site_icon_url( null, 32 );
+ if ( $url ) {
+ echo '
+
+ ' . convert_chars( $url ) . '
+ ' . $rss_title . '
+ ' . get_bloginfo_rss( 'url' ) . '
+ 32
+ 32
+ ' . "\n";
+ }
+}
+
/**
* Display the link for the currently displayed feed in a XSS safe way.
*
diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
index aa8f398e22..8578ad1e6c 100644
--- a/src/wp-includes/formatting.php
+++ b/src/wp-includes/formatting.php
@@ -3421,6 +3421,7 @@ function sanitize_option( $option, $value ) {
case 'thread_comments_depth':
case 'users_can_register':
case 'start_of_week':
+ case 'site_icon':
$value = absint( $value );
break;
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index 3c3f88f02c..a95da70f79 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -721,6 +721,58 @@ function get_bloginfo( $show = '', $filter = 'raw' ) {
return $output;
}
+/**
+ * Returns the Site Icon URL.
+ *
+ * @param null|int $blog_id Id of the blog to get the site icon for.
+ * @param int $size Size of the site icon.
+ * @param string $url Fallback url if no site icon is found.
+ * @return string Site Icon URL.
+ */
+function get_site_icon_url( $blog_id = null, $size = 512, $url = '' ) {
+ if ( function_exists( 'get_blog_option' ) ) {
+ if ( ! is_int( $blog_id ) ) {
+ $blog_id = get_current_blog_id();
+ }
+ $site_icon_id = get_blog_option( $blog_id, 'site_icon' );
+ } else {
+ $site_icon_id = get_option( 'site_icon' );
+ }
+
+ if ( $site_icon_id ) {
+ if ( $size >= 512 ) {
+ $size_data = 'full';
+ } else {
+ $size_data = array( $size, $size );
+ }
+ $url_data = wp_get_attachment_image_src( $site_icon_id, $size_data );
+ $url = $url_data[0];
+ }
+
+ return $url;
+}
+
+/**
+ * Displays the Site Icon URL.
+ *
+ * @param null|int $blog_id Id of the blog to get the site icon for.
+ * @param int $size Size of the site icon.
+ * @param string $url Fallback url if no site icon is found.
+ */
+function site_icon_url( $blog_id = null, $size = 512, $url = '' ) {
+ echo esc_url( get_site_icon_url( $blog_id, $size, $url ) );
+}
+
+/**
+ * Whether the site has a Site Icon.
+ *
+ * @param int|null $blog_id Optional. Blog ID. Default: Current blog.
+ * @return bool
+ */
+function has_site_icon( $blog_id = null ) {
+ return !! get_site_icon_url( $blog_id, 512 );
+}
+
/**
* Display title tag with contents.
*
@@ -2385,6 +2437,39 @@ function wp_no_robots() {
echo "\n";
}
+/**
+ * Display site icon meta tags.
+ *
+ * @since 4.3.0
+ *
+ * @link http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon HTML5 specification link icon.
+ */
+function wp_site_icon() {
+ if ( ! has_site_icon() ) {
+ return;
+ }
+
+ $meta_tags = array(
+ sprintf( '', esc_url( get_site_icon_url( null, 32 ) ) ),
+ sprintf( '', esc_url( get_site_icon_url( null, 180 ) ) ),
+ sprintf( '', esc_url( get_site_icon_url( null, 270 ) ) ),
+ );
+
+ /**
+ * Filters the site icon meta tags, so Plugins can add their own.
+ *
+ * @since 4.3.0
+ *
+ * @param array $meta_tags Site Icon meta elements.
+ */
+ $meta_tags = apply_filters( 'site_icon_meta_tags', $meta_tags );
+ $meta_tags = array_filter( $meta_tags );
+
+ foreach ( $meta_tags as $meta_tag ) {
+ echo "$meta_tag\n";
+ }
+}
+
/**
* Whether the user should have a WYSIWIG editor.
*
diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
index 287a23ed06..79d060136d 100644
--- a/src/wp-includes/script-loader.php
+++ b/src/wp-includes/script-loader.php
@@ -612,6 +612,9 @@ function wp_default_scripts( &$scripts ) {
'untitled' => _x( '(no label)', 'missing menu item navigation label' )
) );
+ $scripts->add( 'site-icon', '/wp-admin/js/site-icon.js', array( 'jquery' ), false, 1 );
+ $scripts->add( 'site-icon-crop', '/wp-admin/js/site-icon-crop.js', array( 'jcrop' ), false, 1 );
+
$scripts->add( 'custom-header', "/wp-admin/js/custom-header.js", array( 'jquery-masonry' ), false, 1 );
$scripts->add( 'custom-background', "/wp-admin/js/custom-background$suffix.js", array( 'wp-color-picker', 'media-views' ), false, 1 );
$scripts->add( 'media-gallery', "/wp-admin/js/media-gallery$suffix.js", array('jquery'), false, 1 );