Images_Via_Imgix::replace_images_in_content()   D
last analyzed

Complexity

Conditions 10
Paths 17

Size

Total Lines 34
Code Lines 18

Duplication

Lines 15
Ratio 44.12 %

Importance

Changes 0
Metric Value
cc 10
eloc 18
nc 17
nop 1
dl 15
loc 34
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		// Change filter load order to ensure it loads after other CDN url transformations i.e. Amazon S3 which loads at position 99.
33
		add_filter( 'wp_get_attachment_url', [ $this, 'replace_image_url' ], 100 );
34
		add_filter( 'imgix/add-image-url', [ $this, 'replace_image_url' ] );
35
36
		add_filter( 'image_downsize', [ $this, 'image_downsize' ], 10, 3 );
37
38
		add_filter( 'wp_calculate_image_srcset', [ $this, 'calculate_image_srcset' ], 10, 5 );
39
40
		add_filter( 'the_content', [ $this, 'replace_images_in_content' ] );
41
		add_action( 'wp_head', [ $this, 'prefetch_cdn' ], 1 );
42
43
		add_action( 'after_setup_theme', [ $this, 'buffer_start_for_retina' ] );
44
		add_action( 'shutdown', [ $this, 'buffer_end_for_retina' ], 0 );
45
	}
46
47
	/**
48
	 * Plugin loader instance.
49
	 *
50
	 * @return Images_Via_Imgix
51
	 */
52
	public static function instance() {
53
		if ( ! isset( self::$instance ) ) {
54
			self::$instance = new self;
55
		}
56
57
		return self::$instance;
58
	}
59
60
	/**
61
	 * Set a single option.
62
	 *
63
	 * @param string $key
64
	 * @param mixed $value
65
	 */
66
	public function set_option( $key, $value ) {
67
		$this->options[ $key ] = $value;
68
	}
69
70
	/**
71
	 * Get a single option.
72
	 *
73
	 * @param  string $key
74
	 * @param  mixed $default
75
	 * @return mixed
76
	 */
77
	public function get_option( $key, $default = '' ) {
78
		return array_key_exists( $key, $this->options ) ? $this->options[ $key ] : $default;
79
	}
80
81
	/**
82
	 * Override options from settings.
83
	 * Used in unit tests.
84
	 *
85
	 * @param array $options
86
	 */
87
	public function set_options( $options ) {
88
		$this->options = $options;
89
	}
90
91
	/**
92
	 * Find all img tags with sources matching "imgix.net" without the parameter
93
	 * "srcset" and add the "srcset" parameter to all those images, appending a new
94
	 * source using the "dpr=2" modifier.
95
	 *
96
	 * @param $content
97
	 *
98
	 * @return string Content with retina-enriched image tags.
99
	 */
100
	public function add_retina( $content ) {
101
		$pattern = '/<img((?![^>]+srcset )([^>]*)';
102
		$pattern .= 'src=[\'"]([^\'"]*imgix.net[^\'"]*\?[^\'"]*w=[^\'"]*)[\'"]([^>]*)*?)>/i';
103
		$repl    = '<img$2src="$3" srcset="${3}, ${3}&amp;dpr=2 2x, ${3}&amp;dpr=3 3x,"$4>';
104
		$content = preg_replace( $pattern, $repl, $content );
105
106
		return preg_replace( $pattern, $repl, $content );
107
	}
108
109
	/**
110
	 * Modify image urls for attachments to use imgix host.
111
	 *
112
	 * @param string $url
113
	 *
114
	 * @return string
115
	 */
116
	public function replace_image_url( $url ) {
117
		if ( ! empty ( $this->options['cdn_link'] ) ) {
118
			$parsed_url = parse_url( $url );
119
120
			//Check if image is hosted on current site url -OR- the CDN url specified. Using strpos because we're comparing the host to a full CDN url.
121
			if (
122
				isset( $parsed_url['host'], $parsed_url['path'] )
123
				&& ($parsed_url['host'] === parse_url( home_url( '/' ), PHP_URL_HOST ) || ( isset($this->options['external_cdn_link']) && ! empty($this->options['external_cdn_link']) && strpos( $this->options['external_cdn_link'], $parsed_url['host']) !== false ) )
124
				&& preg_match( '/\.(jpg|jpeg|gif|png)$/i', $parsed_url['path'] )
125
			) {
126
				$cdn = parse_url( $this->options['cdn_link'] );
127
128
				foreach ( [ 'scheme', 'host', 'port' ] as $url_part ) {
129
					if ( isset( $cdn[ $url_part ] ) ) {
130
						$parsed_url[ $url_part ] = $cdn[ $url_part ];
131
					} else {
132
						unset( $parsed_url[ $url_part ] );
133
					}
134
				}
135
136
				if ( ! empty( $this->options['external_cdn_link'] ) ) {
137
					$cdn_path = parse_url( $this->options['external_cdn_link'],  PHP_URL_PATH );
138
139
					if ( isset( $cdn_path, $parsed_url['path'] ) && $cdn_path !== '/' && ! empty( $parsed_url['path'] ) ) {
140
						$parsed_url['path'] = str_replace( $cdn_path, '', $parsed_url['path'] );
141
					}
142
				}
143
144
				$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 118 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...
145
146
				$url = add_query_arg( $this->get_global_params(), $url );
147
			}
148
		}
149
150
		return $url;
151
	}
152
153
	/**
154
	 * Set params when running image_downsize
155
	 *
156
	 * @param false|array  $return
157
	 * @param int          $attachment_id
158
	 * @param string|array $size
159
	 *
160
	 * @return false|array
161
	 */
162
	public function image_downsize( $return, $attachment_id, $size ) {
163
		if ( ! empty ( $this->options['cdn_link'] ) ) {
164
			$img_url = wp_get_attachment_url( $attachment_id );
165
166
			$params = [];
167
			if ( is_array( $size ) ) {
168
				$params['w'] = $width = isset( $size[0] ) ? $size[0] : 0;
169
				$params['h'] = $height = isset( $size[1] ) ? $size[1] : 0;
170
			} else {
171
				$available_sizes = $this->get_all_defined_sizes();
172
				if ( isset( $available_sizes[ $size ] ) ) {
173
					$size        = $available_sizes[ $size ];
174
					$params['w'] = $width = $size['width'];
175
					$params['h'] = $height = $size['height'];
176
				}
177
			}
178
179
			$params = array_filter( $params );
180
181
			$img_url = add_query_arg( $params, $img_url );
182
183
			if ( ! isset( $width ) || ! isset( $height ) ) {
184
				// any other type: use the real image
185
				$meta   = wp_get_attachment_metadata( $attachment_id );
186
187
				// Image sizes is missing for pdf thumbnails
188
				$meta['width']  = isset( $meta['width'] ) ? $meta['width'] : 0;
189
				$meta['height'] = isset( $meta['height'] ) ? $meta['height'] : 0;
190
191
				$width  = isset( $width ) ? $width : $meta['width'];
192
				$height = isset( $height ) ? $height : $meta['height'];
193
			}
194
195
			$return = [ $img_url, $width, $height, true ];
196
		}
197
198
		return $return;
199
	}
200
201
	/**
202
	 * Change url for images in srcset
203
	 *
204
	 * @param array  $sources
205
	 * @param array  $size_array
206
	 * @param string $image_src
207
	 * @param array  $image_meta
208
	 * @param int    $attachment_id
209
	 *
210
	 * @return array
211
	 */
212
	public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
213
		if ( ! empty ( $this->options['cdn_link'] ) ) {
214
			foreach ( $sources as $i => $image_size ) {
215
				if ( $image_size['descriptor'] === 'w' ) {
216
					if ( $attachment_id ) {
217
						$image_src = wp_get_attachment_url( $attachment_id );
218
					}
219
220
					$image_src            = remove_query_arg( 'h', $image_src );
221
					$sources[ $i ]['url'] = add_query_arg( 'w', $image_size['value'], $image_src );
222
				}
223
			}
224
		}
225
226
		return $sources;
227
	}
228
229
	/**
230
	 * Modify image urls in content to use imgix host.
231
	 *
232
	 * @param $content
233
	 *
234
	 * @return string
235
	 */
236
	public function replace_images_in_content( $content ) {
237
		// Added null to apply filters wp_get_attachment_url to improve compatibility with https://en-gb.wordpress.org/plugins/amazon-s3-and-cloudfront/ - does not break wordpress if the plugin isn't present.
238
		if ( ! empty ( $this->options['cdn_link'] ) ) {
239 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...
240
				foreach ( $matches[2] as $image_src ) {
241
					$content = str_replace( $image_src, apply_filters( 'wp_get_attachment_url', $image_src, null ), $content );
242
				}
243
			}
244
245
			if ( preg_match_all( '/<img\s[^>]*srcset=([\"\']??)([^\">]*?)\1[^>]*\/?>/iU', $content, $matches ) ) {
246
247
				foreach ( $matches[2] as $image_srcset ) {
248
					$new_image_srcset = preg_replace_callback( '/(\S+)(\s\d+\w)/', function ( $srcset_matches ) {
249
						return apply_filters( 'wp_get_attachment_url', $srcset_matches[1], null ) . $srcset_matches[2];
250
					}, $image_srcset );
251
252
					$content = str_replace( $image_srcset, $new_image_srcset, $content );
253
				}
254
			}
255
256 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...
257
				foreach ( $matches[0] as $link ) {
258
					$content = str_replace( $link[2], apply_filters( 'wp_get_attachment_url', $link[2], null ), $content );
259
				}
260
			}
261
262 View Code Duplication
      if ( preg_match_all('/url\(([\s])?([\"|\'])?(.*?)([\"|\'])?([\s])?\)/i', $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...
263
        foreach ( $matches[3] as $image_src ) {
264
          $content = str_replace( $image_src, apply_filters( 'wp_get_attachment_url', $image_src, null ), $content );
265
        }
266
      }
267
		}
268
		return $content;
269
	}
270
271
	/**
272
	 * Add tag to dns prefetch cdn host
273
	 */
274
	public function prefetch_cdn() {
275
		if ( ! empty ( $this->options['cdn_link'] ) ) {
276
			$host = parse_url( $this->options['cdn_link'], PHP_URL_HOST );
277
278
			printf(
279
				'<link rel="dns-prefetch" href="%s"/>',
280
				esc_attr( '//' . $host )
281
			);
282
		}
283
	}
284
285
	/**
286
	 * Start output buffer if auto retina is enabled
287
	 */
288
	public function buffer_start_for_retina() {
289
		if ( ! empty ( $this->options['add_dpi2_srcset'] ) ) {
290
			$this->buffer_started = ob_start( [ $this, 'add_retina' ] );
291
		}
292
	}
293
294
	/**
295
	 * Stop output buffer if it was enabled by the plugin
296
	 */
297
	public function buffer_end_for_retina() {
298
		if ( $this->buffer_started && ob_get_level() ) {
299
			ob_end_flush();
300
		}
301
	}
302
303
	/**
304
	 * Returns a array of global parameters to be applied in all images,
305
	 * according to plugin's settings.
306
	 *
307
	 * @return array Global parameters to be appened at the end of each img URL.
308
	 */
309
	protected function get_global_params() {
310
		$params = [];
311
312
		// For now, only "auto" is supported.
313
		$auto = [];
314
		if ( ! empty ( $this->options['auto_format'] ) ) {
315
			array_push( $auto, 'format' );
316
		}
317
318
		if ( ! empty ( $this->options['auto_enhance'] ) ) {
319
			array_push( $auto, 'enhance' );
320
		}
321
322
		if ( ! empty ( $this->options['auto_compress'] ) ) {
323
			array_push( $auto, 'compress' );
324
		}
325
326
		if ( ! empty( $auto ) ) {
327
			$params['auto'] = implode( '%2C', $auto );
328
		}
329
330
		return $params;
331
	}
332
333
	/**
334
	 * Get all defined image sizes
335
	 *
336
	 * @return array
337
	 */
338
	protected function get_all_defined_sizes() {
339
		// Make thumbnails and other intermediate sizes.
340
		$theme_image_sizes = wp_get_additional_image_sizes();
341
342
		$sizes = [];
343
		foreach ( get_intermediate_image_sizes() as $s ) {
344
			$sizes[ $s ] = [ 'width' => '', 'height' => '', 'crop' => false ];
345
			if ( isset( $theme_image_sizes[ $s ] ) ) {
346
				// For theme-added sizes
347
				$sizes[ $s ]['width']  = intval( $theme_image_sizes[ $s ]['width'] );
348
				$sizes[ $s ]['height'] = intval( $theme_image_sizes[ $s ]['height'] );
349
				$sizes[ $s ]['crop']   = $theme_image_sizes[ $s ]['crop'];
350
			} else {
351
				// For default sizes set in options
352
				$sizes[ $s ]['width']  = get_option( "{$s}_size_w" );
353
				$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
354
				$sizes[ $s ]['crop']   = get_option( "{$s}_crop" );
355
			}
356
		}
357
358
		return $sizes;
359
	}
360
}
361
362
Images_Via_Imgix::instance();
363