Completed
Push — master ( 6ef803...e4b824 )
by LA
13s
created

Images_Via_Imgix::replace_image_url()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 3
nop 1
dl 0
loc 21
rs 7.551
c 0
b 0
f 0
1
<?php
2
3
class Images_Via_Imgix {
4
5
	/**
6
	 * The instance of the class.
7
	 *
8
	 * @var Images_Via_Imgix
9
	 */
10
	protected static $instance;
11
12
	/**
13
	 * Plugin options
14
	 *
15
	 * @var array
16
	 */
17
	protected $options = [];
18
19
	/**
20
	 * Buffer is started by plugin and should be ended on shutdown.
21
	 *
22
	 * @var bool
23
	 */
24
	protected $buffer_started = false;
25
26
	/**
27
	 * ImagesViaImgix constructor.
28
	 */
29
	public function __construct() {
30
		$this->options = get_option( 'imgix_settings', [] );
31
32
		add_filter( 'wp_get_attachment_url', [ $this, 'replace_image_url' ] );
33
		add_filter( 'imgix/add-image-url', [ $this, 'replace_image_url' ] );
34
35
		add_filter( 'image_downsize', [ $this, 'image_downsize' ], 10, 3 );
36
37
		add_filter( 'wp_calculate_image_srcset', [ $this, 'calculate_image_srcset' ], 10, 5 );
38
39
		add_filter( 'the_content', [ $this, 'replace_images_in_content' ] );
40
		add_action( 'wp_head', [ $this, 'prefetch_cdn' ], 1 );
41
42
		add_action( 'after_setup_theme', [ $this, 'buffer_start_for_retina' ] );
43
		add_action( 'shutdown', [ $this, 'buffer_end_for_retina' ] );
44
	}
45
46
	/**
47
	 * Plugin loader instance.
48
	 *
49
	 * @return Images_Via_Imgix
50
	 */
51
	public static function instance() {
52
		if ( ! isset( self::$instance ) ) {
53
			self::$instance = new self;
54
		}
55
56
		return self::$instance;
57
	}
58
59
	/**
60
	 * Override options from settings.
61
	 * Used in unit tests.
62
	 *
63
	 * @param array $options
64
	 */
65
	public function set_options( $options ) {
66
		$this->options = $options;
67
	}
68
69
	/**
70
	 * Find all img tags with sources matching "imgix.net" without the parameter
71
	 * "srcset" and add the "srcset" parameter to all those images, appending a new
72
	 * source using the "dpr=2" modifier.
73
	 *
74
	 * @param $content
75
	 *
76
	 * @return string Content with retina-enriched image tags.
77
	 */
78
	public function add_retina( $content ) {
79
		$pattern = '/<img((?![^>]+srcset )([^>]*)';
80
		$pattern .= 'src=[\'"]([^\'"]*imgix.net[^\'"]*\?[^\'"]*w=[^\'"]*)[\'"]([^>]*)*?)>/i';
81
		$repl    = '<img$2src="$3" srcset="${3}, ${3}&amp;dpr=2 2x, ${3}&amp;dpr=3 3x,"$4>';
82
		$content = preg_replace( $pattern, $repl, $content );
83
84
		return preg_replace( $pattern, $repl, $content );
85
	}
86
87
	/**
88
	 * Modify image urls for attachments to use imgix host.
89
	 *
90
	 * @param string $url
91
	 *
92
	 * @return string
93
	 */
94
	public function replace_image_url( $url ) {
95
		if ( ! empty ( $this->options['cdn_link'] ) ) {
96
			$parsed_url = parse_url( $url );
97
98
			if ( isset( $parsed_url['host'], $parsed_url['path'] ) && $parsed_url['host'] === parse_url( home_url( '/' ), PHP_URL_HOST ) && preg_match( '/\.(jpg|jpeg|gif|png)$/', $parsed_url['path'] ) ) {
99
				$cdn = parse_url( $this->options['cdn_link'] );
100
				foreach ( [ 'scheme', 'host', 'port' ] as $url_part ) {
101
					if ( isset( $cdn[ $url_part ] ) ) {
102
						$parsed_url[ $url_part ] = $cdn[ $url_part ];
103
					} else {
104
						unset( $parsed_url[ $url_part ] );
105
					}
106
				}
107
				$url = http_build_url( $parsed_url );
0 ignored issues
show
Security Bug introduced by
It seems like $parsed_url defined by parse_url($url) on line 96 can also be of type false; however, http_build_url() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
108
109
				$url = add_query_arg( $this->get_global_params(), $url );
110
			}
111
		}
112
113
		return $url;
114
	}
115
116
	/**
117
	 * Set params when running image_downsize
118
	 *
119
	 * @param false|array  $return
120
	 * @param int          $attachment_id
121
	 * @param string|array $size
122
	 *
123
	 * @return false|array
124
	 */
125
	public function image_downsize( $return, $attachment_id, $size ) {
126
		if ( ! empty ( $this->options['cdn_link'] ) ) {
127
			$img_url = wp_get_attachment_url( $attachment_id );
128
129
			$params = [];
130
			if ( is_array( $size ) ) {
131
				$params['w'] = $width = isset( $size[0] ) ? $size[0] : 0;
132
				$params['h'] = $height = isset( $size[1] ) ? $size[1] : 0;
133
			} else {
134
				$available_sizes = $this->get_all_defined_sizes();
135
				if ( isset( $available_sizes[ $size ] ) ) {
136
					$size        = $available_sizes[ $size ];
137
					$params['w'] = $width = $size['width'];
138
					$params['h'] = $height = $size['height'];
139
				}
140
			}
141
142
			$params = array_filter( $params );
143
144
			$img_url = add_query_arg( $params, $img_url );
145
146
			if ( ! isset( $width ) || ! isset( $height ) ) {
147
				// any other type: use the real image
148
				$meta   = wp_get_attachment_metadata( $attachment_id );
149
				$width  = isset( $width ) ? $width : $meta['width'];
150
				$height = isset( $height ) ? $height : $meta['height'];
151
			}
152
153
			$return = [ $img_url, $width, $height, true ];
154
		}
155
156
		return $return;
157
	}
158
159
	/**
160
	 * Change url for images in srcset
161
	 *
162
	 * @param array  $sources
163
	 * @param array  $size_array
164
	 * @param string $image_src
165
	 * @param array  $image_meta
166
	 * @param int    $attachment_id
167
	 *
168
	 * @return array
169
	 */
170
	public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
171
		if ( ! empty ( $this->options['cdn_link'] ) ) {
172
			foreach ( $sources as $i => $image_size ) {
173
				if ( $image_size['descriptor'] === 'w' ) {
174
					if ( $attachment_id ) {
175
						$image_src = wp_get_attachment_url( $attachment_id );
176
					}
177
178
					$image_src            = remove_query_arg( 'h', $image_src );
179
					$sources[ $i ]['url'] = add_query_arg( 'w', $image_size['value'], $image_src );
180
				}
181
			}
182
		}
183
184
		return $sources;
185
	}
186
187
	/**
188
	 * Modify image urls in content to use imgix host.
189
	 *
190
	 * @param $content
191
	 *
192
	 * @return string
193
	 */
194
	public function replace_images_in_content( $content ) {
195
		if ( ! empty ( $this->options['cdn_link'] ) ) {
196 View Code Duplication
			if ( preg_match_all( '/<img\s[^>]*src=([\"\']??)([^\" >]*?)\1[^>]*>/iU', $content, $matches ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197
				foreach ( $matches[2] as $image_src ) {
198
					$content = str_replace( $image_src, apply_filters( 'wp_get_attachment_url', $image_src ), $content );
199
				}
200
			}
201
202
			if ( preg_match_all( '/<img\s[^>]*srcset=([\"\']??)([^\">]*?)\1[^>]*\/?>/iU', $content, $matches ) ) {
203
204
				foreach ( $matches[2] as $image_srcset ) {
205
					$new_image_srcset = preg_replace_callback( '/(\S+)(\s\d+\w)/', function ( $srcset_matches ) {
206
						return apply_filters( 'wp_get_attachment_url', $srcset_matches[1] ) . $srcset_matches[2];
207
					}, $image_srcset );
208
209
					$content = str_replace( $image_srcset, $new_image_srcset, $content );
210
				}
211
			}
212
213 View Code Duplication
			if ( preg_match_all( '/<a\s[^>]*href=([\"\']??)([^\" >]*?)\1[^>]*>(.*)<\/a>/iU', $content, $matches ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214
				foreach ( $matches[0] as $link ) {
215
					$content = str_replace( $link[2], apply_filters( 'wp_get_attachment_url', $link[2] ), $content );
216
				}
217
			}
218
		}
219
220
		return $content;
221
	}
222
223
	/**
224
	 * Add tag to dns prefetch cdn host
225
	 */
226
	public function prefetch_cdn() {
227
		if ( ! empty ( $this->options['cdn_link'] ) ) {
228
			$host = parse_url( $this->options['cdn_link'], PHP_URL_HOST );
229
230
			printf(
231
				'<link rel="dns-prefetch" href="%s"/>',
232
				esc_attr( '//' . $host )
233
			);
234
		}
235
	}
236
237
	/**
238
	 * Start output buffer if auto retina is enabled
239
	 */
240
	public function buffer_start_for_retina() {
241
		if ( ! empty ( $this->options['add_dpi2_srcset'] ) ) {
242
			$this->buffer_started = true;
243
			ob_start( [ $this, 'add_retina' ] );
244
		}
245
	}
246
247
	/**
248
	 * Stop output buffer if it was enabled by the plugin
249
	 */
250
	public function buffer_end_for_retina() {
251
		if ( $this->buffer_started === true ) {
252
			ob_end_flush();
253
		}
254
	}
255
256
	/**
257
	 * Returns a array of global parameters to be applied in all images,
258
	 * according to plugin's settings.
259
	 *
260
	 * @return array Global parameters to be appened at the end of each img URL.
261
	 */
262
	protected function get_global_params() {
263
		$params = [];
264
265
		// For now, only "auto" is supported.
266
		$auto = [];
267
		if ( ! empty ( $this->options['auto_format'] ) ) {
268
			array_push( $auto, 'format' );
269
		}
270
271
		if ( ! empty ( $this->options['auto_enhance'] ) ) {
272
			array_push( $auto, 'enhance' );
273
		}
274
275
		if ( ! empty ( $this->options['auto_compress'] ) ) {
276
			array_push( $auto, 'compress' );
277
		}
278
279
		if ( ! empty( $auto ) ) {
280
			$params['auto'] = implode( '%2C', $auto );
281
		}
282
283
		return $params;
284
	}
285
286
	/**
287
	 * Get all defined image sizes
288
	 *
289
	 * @return array
290
	 */
291
	protected function get_all_defined_sizes() {
292
		// Make thumbnails and other intermediate sizes.
293
		$theme_image_sizes = wp_get_additional_image_sizes();
294
295
		$sizes = [];
296
		foreach ( get_intermediate_image_sizes() as $s ) {
297
			$sizes[ $s ] = [ 'width' => '', 'height' => '', 'crop' => false ];
298
			if ( isset( $theme_image_sizes[ $s ] ) ) {
299
				// For theme-added sizes
300
				$sizes[ $s ]['width']  = intval( $theme_image_sizes[ $s ]['width'] );
301
				$sizes[ $s ]['height'] = intval( $theme_image_sizes[ $s ]['height'] );
302
				$sizes[ $s ]['crop']   = $theme_image_sizes[ $s ]['crop'];
303
			} else {
304
				// For default sizes set in options
305
				$sizes[ $s ]['width']  = get_option( "{$s}_size_w" );
306
				$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
307
				$sizes[ $s ]['crop']   = get_option( "{$s}_crop" );
308
			}
309
		}
310
311
		return $sizes;
312
	}
313
}
314
315
Images_Via_Imgix::instance();
316