1<?php
2/**
3 * Widget API: WP_Widget_Media_Image class
4 *
5 * @package WordPress
6 * @subpackage Widgets
7 * @since 4.8.0
8 */
9
10/**
11 * Core class that implements an image widget.
12 *
13 * @since 4.8.0
14 *
15 * @see WP_Widget_Media
16 * @see WP_Widget
17 */
18class WP_Widget_Media_Image extends WP_Widget_Media {
19
20	/**
21	 * Constructor.
22	 *
23	 * @since 4.8.0
24	 */
25	public function __construct() {
26		parent::__construct(
27			'media_image',
28			__( 'Image' ),
29			array(
30				'description' => __( 'Displays an image.' ),
31				'mime_type'   => 'image',
32			)
33		);
34
35		$this->l10n = array_merge(
36			$this->l10n,
37			array(
38				'no_media_selected'          => __( 'No image selected' ),
39				'add_media'                  => _x( 'Add Image', 'label for button in the image widget' ),
40				'replace_media'              => _x( 'Replace Image', 'label for button in the image widget; should preferably not be longer than ~13 characters long' ),
41				'edit_media'                 => _x( 'Edit Image', 'label for button in the image widget; should preferably not be longer than ~13 characters long' ),
42				'missing_attachment'         => sprintf(
43					/* translators: %s: URL to media library. */
44					__( 'We can&#8217;t find that image. Check your <a href="%s">media library</a> and make sure it wasn&#8217;t deleted.' ),
45					esc_url( admin_url( 'upload.php' ) )
46				),
47				/* translators: %d: Widget count. */
48				'media_library_state_multi'  => _n_noop( 'Image Widget (%d)', 'Image Widget (%d)' ),
49				'media_library_state_single' => __( 'Image Widget' ),
50			)
51		);
52	}
53
54	/**
55	 * Get schema for properties of a widget instance (item).
56	 *
57	 * @since 4.8.0
58	 *
59	 * @see WP_REST_Controller::get_item_schema()
60	 * @see WP_REST_Controller::get_additional_fields()
61	 * @link https://core.trac.wordpress.org/ticket/35574
62	 *
63	 * @return array Schema for properties.
64	 */
65	public function get_instance_schema() {
66		return array_merge(
67			array(
68				'size'              => array(
69					'type'        => 'string',
70					'enum'        => array_merge( get_intermediate_image_sizes(), array( 'full', 'custom' ) ),
71					'default'     => 'medium',
72					'description' => __( 'Size' ),
73				),
74				'width'             => array( // Via 'customWidth', only when size=custom; otherwise via 'width'.
75					'type'        => 'integer',
76					'minimum'     => 0,
77					'default'     => 0,
78					'description' => __( 'Width' ),
79				),
80				'height'            => array( // Via 'customHeight', only when size=custom; otherwise via 'height'.
81					'type'        => 'integer',
82					'minimum'     => 0,
83					'default'     => 0,
84					'description' => __( 'Height' ),
85				),
86
87				'caption'           => array(
88					'type'                  => 'string',
89					'default'               => '',
90					'sanitize_callback'     => 'wp_kses_post',
91					'description'           => __( 'Caption' ),
92					'should_preview_update' => false,
93				),
94				'alt'               => array(
95					'type'              => 'string',
96					'default'           => '',
97					'sanitize_callback' => 'sanitize_text_field',
98					'description'       => __( 'Alternative Text' ),
99				),
100				'link_type'         => array(
101					'type'                  => 'string',
102					'enum'                  => array( 'none', 'file', 'post', 'custom' ),
103					'default'               => 'custom',
104					'media_prop'            => 'link',
105					'description'           => __( 'Link To' ),
106					'should_preview_update' => true,
107				),
108				'link_url'          => array(
109					'type'                  => 'string',
110					'default'               => '',
111					'format'                => 'uri',
112					'media_prop'            => 'linkUrl',
113					'description'           => __( 'URL' ),
114					'should_preview_update' => true,
115				),
116				'image_classes'     => array(
117					'type'                  => 'string',
118					'default'               => '',
119					'sanitize_callback'     => array( $this, 'sanitize_token_list' ),
120					'media_prop'            => 'extraClasses',
121					'description'           => __( 'Image CSS Class' ),
122					'should_preview_update' => false,
123				),
124				'link_classes'      => array(
125					'type'                  => 'string',
126					'default'               => '',
127					'sanitize_callback'     => array( $this, 'sanitize_token_list' ),
128					'media_prop'            => 'linkClassName',
129					'should_preview_update' => false,
130					'description'           => __( 'Link CSS Class' ),
131				),
132				'link_rel'          => array(
133					'type'                  => 'string',
134					'default'               => '',
135					'sanitize_callback'     => array( $this, 'sanitize_token_list' ),
136					'media_prop'            => 'linkRel',
137					'description'           => __( 'Link Rel' ),
138					'should_preview_update' => false,
139				),
140				'link_target_blank' => array(
141					'type'                  => 'boolean',
142					'default'               => false,
143					'media_prop'            => 'linkTargetBlank',
144					'description'           => __( 'Open link in a new tab' ),
145					'should_preview_update' => false,
146				),
147				'image_title'       => array(
148					'type'                  => 'string',
149					'default'               => '',
150					'sanitize_callback'     => 'sanitize_text_field',
151					'media_prop'            => 'title',
152					'description'           => __( 'Image Title Attribute' ),
153					'should_preview_update' => false,
154				),
155
156				/*
157				 * There are two additional properties exposed by the PostImage modal
158				 * that don't seem to be relevant, as they may only be derived read-only
159				 * values:
160				 * - originalUrl
161				 * - aspectRatio
162				 * - height (redundant when size is not custom)
163				 * - width (redundant when size is not custom)
164				 */
165			),
166			parent::get_instance_schema()
167		);
168	}
169
170	/**
171	 * Render the media on the frontend.
172	 *
173	 * @since 4.8.0
174	 *
175	 * @param array $instance Widget instance props.
176	 */
177	public function render_media( $instance ) {
178		$instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance );
179		$instance = wp_parse_args(
180			$instance,
181			array(
182				'size' => 'thumbnail',
183			)
184		);
185
186		$attachment = null;
187
188		if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) {
189			$attachment = get_post( $instance['attachment_id'] );
190		}
191
192		if ( $attachment ) {
193			$caption = '';
194			if ( ! isset( $instance['caption'] ) ) {
195				$caption = $attachment->post_excerpt;
196			} elseif ( trim( $instance['caption'] ) ) {
197				$caption = $instance['caption'];
198			}
199
200			$image_attributes = array(
201				'class' => sprintf( 'image wp-image-%d %s', $attachment->ID, $instance['image_classes'] ),
202				'style' => 'max-width: 100%; height: auto;',
203			);
204			if ( ! empty( $instance['image_title'] ) ) {
205				$image_attributes['title'] = $instance['image_title'];
206			}
207
208			if ( $instance['alt'] ) {
209				$image_attributes['alt'] = $instance['alt'];
210			}
211
212			$size = $instance['size'];
213
214			if ( 'custom' === $size || ! in_array( $size, array_merge( get_intermediate_image_sizes(), array( 'full' ) ), true ) ) {
215				$size  = array( $instance['width'], $instance['height'] );
216				$width = $instance['width'];
217			} else {
218				$caption_size = _wp_get_image_size_from_meta( $instance['size'], wp_get_attachment_metadata( $attachment->ID ) );
219				$width        = empty( $caption_size[0] ) ? 0 : $caption_size[0];
220			}
221
222			$image_attributes['class'] .= sprintf( ' attachment-%1$s size-%1$s', is_array( $size ) ? implode( 'x', $size ) : $size );
223
224			$image = wp_get_attachment_image( $attachment->ID, $size, false, $image_attributes );
225
226		} else {
227			if ( empty( $instance['url'] ) ) {
228				return;
229			}
230
231			$instance['size'] = 'custom';
232			$caption          = $instance['caption'];
233			$width            = $instance['width'];
234			$classes          = 'image ' . $instance['image_classes'];
235			if ( 0 === $instance['width'] ) {
236				$instance['width'] = '';
237			}
238			if ( 0 === $instance['height'] ) {
239				$instance['height'] = '';
240			}
241
242			$image = sprintf(
243				'<img class="%1$s" src="%2$s" alt="%3$s" width="%4$s" height="%5$s" />',
244				esc_attr( $classes ),
245				esc_url( $instance['url'] ),
246				esc_attr( $instance['alt'] ),
247				esc_attr( $instance['width'] ),
248				esc_attr( $instance['height'] )
249			);
250		} // End if().
251
252		$url = '';
253		if ( 'file' === $instance['link_type'] ) {
254			$url = $attachment ? wp_get_attachment_url( $attachment->ID ) : $instance['url'];
255		} elseif ( $attachment && 'post' === $instance['link_type'] ) {
256			$url = get_attachment_link( $attachment->ID );
257		} elseif ( 'custom' === $instance['link_type'] && ! empty( $instance['link_url'] ) ) {
258			$url = $instance['link_url'];
259		}
260
261		if ( $url ) {
262			$link = sprintf( '<a href="%s"', esc_url( $url ) );
263			if ( ! empty( $instance['link_classes'] ) ) {
264				$link .= sprintf( ' class="%s"', esc_attr( $instance['link_classes'] ) );
265			}
266			if ( ! empty( $instance['link_rel'] ) ) {
267				$link .= sprintf( ' rel="%s"', esc_attr( $instance['link_rel'] ) );
268			}
269			if ( ! empty( $instance['link_target_blank'] ) ) {
270				$link .= ' target="_blank"';
271			}
272			$link .= '>';
273			$link .= $image;
274			$link .= '</a>';
275			$image = wp_targeted_link_rel( $link );
276		}
277
278		if ( $caption ) {
279			$image = img_caption_shortcode(
280				array(
281					'width'   => $width,
282					'caption' => $caption,
283				),
284				$image
285			);
286		}
287
288		echo $image;
289	}
290
291	/**
292	 * Loads the required media files for the media manager and scripts for media widgets.
293	 *
294	 * @since 4.8.0
295	 */
296	public function enqueue_admin_scripts() {
297		parent::enqueue_admin_scripts();
298
299		$handle = 'media-image-widget';
300		wp_enqueue_script( $handle );
301
302		$exported_schema = array();
303		foreach ( $this->get_instance_schema() as $field => $field_schema ) {
304			$exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) );
305		}
306		wp_add_inline_script(
307			$handle,
308			sprintf(
309				'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;',
310				wp_json_encode( $this->id_base ),
311				wp_json_encode( $exported_schema )
312			)
313		);
314
315		wp_add_inline_script(
316			$handle,
317			sprintf(
318				'
319					wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s;
320					wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s );
321				',
322				wp_json_encode( $this->id_base ),
323				wp_json_encode( $this->widget_options['mime_type'] ),
324				wp_json_encode( $this->l10n )
325			)
326		);
327	}
328
329	/**
330	 * Render form template scripts.
331	 *
332	 * @since 4.8.0
333	 */
334	public function render_control_template_scripts() {
335		parent::render_control_template_scripts();
336
337		?>
338		<script type="text/html" id="tmpl-wp-media-widget-image-fields">
339			<# var elementIdPrefix = 'el' + String( Math.random() ) + '_'; #>
340			<# if ( data.url ) { #>
341			<p class="media-widget-image-link">
342				<label for="{{ elementIdPrefix }}linkUrl"><?php esc_html_e( 'Link to:' ); ?></label>
343				<input id="{{ elementIdPrefix }}linkUrl" type="text" class="widefat link" value="{{ data.link_url }}" placeholder="https://" pattern="((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#).*">
344			</p>
345			<# } #>
346		</script>
347		<script type="text/html" id="tmpl-wp-media-widget-image-preview">
348			<# if ( data.error && 'missing_attachment' === data.error ) { #>
349				<div class="notice notice-error notice-alt notice-missing-attachment">
350					<p><?php echo $this->l10n['missing_attachment']; ?></p>
351				</div>
352			<# } else if ( data.error ) { #>
353				<div class="notice notice-error notice-alt">
354					<p><?php _e( 'Unable to preview media due to an unknown error.' ); ?></p>
355				</div>
356			<# } else if ( data.url ) { #>
357				<img class="attachment-thumb" src="{{ data.url }}" draggable="false" alt="{{ data.alt }}"
358					<# if ( ! data.alt && data.currentFilename ) { #>
359						aria-label="
360						<?php
361						echo esc_attr(
362							sprintf(
363								/* translators: %s: The image file name. */
364								__( 'The current image has no alternative text. The file name is: %s' ),
365								'{{ data.currentFilename }}'
366							)
367						);
368						?>
369						"
370					<# } #>
371				/>
372			<# } #>
373		</script>
374		<?php
375	}
376}
377