Completed
Pull Request — master (#18)
by Rasmus
02:38
created

Images_Via_Imgix   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 325
Duplicated Lines 4.31 %

Coupling/Cohesion

Components 2
Dependencies 0

Importance

Changes 0
Metric Value
dl 14
loc 325
rs 8.295
c 0
b 0
f 0
wmc 42
lcom 2
cbo 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A instance() 0 7 2
A set_options() 0 3 1
A add_retina() 0 8 1
D replace_image_url() 0 33 9
C image_downsize() 0 36 8
A replace_host_in_srcset() 0 7 2
A replace_images_in_content() 0 10 2
A prefetch_cdn() 0 10 2
A buffer_start_for_retina() 0 6 2
A buffer_end_for_retina() 0 5 2
A get_global_params() 0 19 4
A convert_filename_to_size_args() 0 14 1
B get_all_defined_sizes() 14 34 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Images_Via_Imgix often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Images_Via_Imgix, and based on these observations, apply Extract Interface, too.

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, 'replace_host_in_srcset' ], 10 );
38
		add_filter( 'the_content', [ $this, 'replace_images_in_content' ] );
39
		add_action( 'wp_head', [ $this, 'prefetch_cdn' ], 1 );
40
41
		add_action( 'after_setup_theme', [ $this, 'buffer_start_for_retina' ] );
42
		add_action( 'shutdown', [ $this, 'buffer_end_for_retina' ] );
43
	}
44
45
	/**
46
	 * Plugin loader instance.
47
	 *
48
	 * @return Images_Via_Imgix
49
	 */
50
	public static function instance() {
51
		if ( ! isset( self::$instance ) ) {
52
			self::$instance = new self;
53
		}
54
55
		return self::$instance;
56
	}
57
58
	/**
59
	 * Override options from settings.
60
	 * Used in unit tests.
61
	 *
62
	 * @param array $options
63
	 */
64
	public function set_options( $options ) {
65
		$this->options = $options;
66
	}
67
68
	/**
69
	 * Find all img tags with sources matching "imgix.net" without the parameter
70
	 * "srcset" and add the "srcset" parameter to all those images, appending a new
71
	 * source using the "dpr=2" modifier.
72
	 *
73
	 * @param $content
74
	 *
75
	 * @return string Content with retina-enriched image tags.
76
	 */
77
	public function add_retina( $content ) {
78
		$pattern = '/<img((?![^>]+srcset )([^>]*)';
79
		$pattern .= 'src=[\'"]([^\'"]*imgix.net[^\'"]*\?[^\'"]*w=[^\'"]*)[\'"]([^>]*)*?)>/i';
80
		$repl    = '<img$2src="$3" srcset="${3}, ${3}&amp;dpr=2 2x, ${3}&amp;dpr=3 3x,"$4>';
81
		$content = preg_replace( $pattern, $repl, $content );
82
83
		return preg_replace( $pattern, $repl, $content );
84
	}
85
86
	/**
87
	 * Modify image urls for attachments to use imgix host.
88
	 *
89
	 * @param string $url
90
	 *
91
	 * @return string
92
	 */
93
	public function replace_image_url( $url ) {
94
		if ( ! empty ( $this->options['cdn_link'] ) ) {
95
			$pathinfo = pathinfo( $url );
96
97
			if ( in_array( $pathinfo['extension'], [ 'jpg', 'gif', 'png', 'jpeg' ] ) ) {
98
				$parsed_url = parse_url( $url );
99
				if ( isset( $parsed_url['host'] ) && $parsed_url['host'] === parse_url( home_url( '/' ), PHP_URL_HOST ) ) {
100
					$cdn = parse_url( $this->options['cdn_link'] );
101
					foreach ( [ 'scheme', 'host', 'port' ] as $url_part ) {
102
						if ( isset( $cdn[ $url_part ] ) ) {
103
							$parsed_url[ $url_part ] = $cdn[ $url_part ];
104
						} else {
105
							unset( $parsed_url[ $url_part ] );
106
						}
107
					}
108
109
					list( $filename, $arguments ) = $this->convert_filename_to_size_args( $pathinfo['basename'] );
110
111
					$arguments = array_merge( $arguments, $this->get_global_params() );
112
113
					$parsed_url['path'] = trailingslashit( dirname( $parsed_url['path'] ) ) . $filename;
114
115
					if ( ! empty( $arguments ) ) {
116
						$parsed_url['query'] = empty( $parsed_url['query'] ) ? build_query( $arguments ) : $parsed_url['query'] . '&' . build_query( $arguments );
117
					}
118
119
					$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 98 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...
120
				}
121
			}
122
		}
123
124
		return $url;
125
	}
126
127
	/**
128
	 * Set params when running image_downsize
129
	 *
130
	 * @param false|array  $return
131
	 * @param int          $attachment_id
132
	 * @param string|array $size
133
	 *
134
	 * @return false|array
135
	 */
136
	public function image_downsize( $return, $attachment_id, $size ) {
137
		if ( ! empty ( $this->options['cdn_link'] ) ) {
138
			$img_url    = wp_get_attachment_url( $attachment_id );
139
			$parsed_url = parse_url( $img_url );
140
			parse_str( $parsed_url['query'], $params );
141
142
			if ( is_array( $size ) ) {
143
				$params['w'] = $width = isset( $size[0] ) ? $size[0] : 0;
144
				$params['h'] = $height = isset( $size[1] ) ? $size[1] : 0;
145
			} else {
146
				$available_sizes = $this->get_all_defined_sizes();
147
				if ( isset( $available_sizes[ $size ] ) ) {
148
					$size        = $available_sizes[ $size ];
149
					$params['w'] = $width = $size['width'];
150
					$params['h'] = $height = $size['height'];
151
				}
152
			}
153
154
			$params = array_filter( $params );
155
156
			$parsed_url['query'] = build_query( $params );
157
158
			$img_url = http_build_url( $parsed_url );
0 ignored issues
show
Security Bug introduced by
It seems like $parsed_url defined by parse_url($img_url) on line 139 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...
159
160
			if ( ! isset( $width ) && ! isset( $height ) ) {
161
				// any other type: use the real image
162
				$meta   = wp_get_attachment_metadata( $attachment_id );
163
				$width  = $meta['width'];
164
				$height = $meta['height'];
165
			}
166
167
			$return = [ $img_url, $width, $height, true ];
0 ignored issues
show
Bug introduced by
The variable $height does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
168
		}
169
170
		return $return;
171
	}
172
173
	/**
174
	 * Modify image urls in srcset to use imgix host.
175
	 *
176
	 * @param array $sources
177
	 *
178
	 * @return array $sources
179
	 */
180
	public function replace_host_in_srcset( $sources ) {
181
		foreach ( $sources as $source ) {
182
			$sources[ $source['value'] ]['url'] = apply_filters( 'imgix/add-image-url', $sources[ $source['value'] ]['url'] );
183
		}
184
185
		return $sources;
186
	}
187
188
	/**
189
	 * Modify image urls in content to use imgix host.
190
	 *
191
	 * @param $content
192
	 *
193
	 * @return string
194
	 */
195
	public function replace_images_in_content( $content ) {
196
		if ( ! empty ( $this->options['cdn_link'] ) ) {
197
			$content = preg_replace_callback( '/(?<=\shref="|\ssrc="|\shref=\'|\ssrc=\').*(?=\'|")/', function ( $match ) {
198
				return esc_url( apply_filters( 'imgix/add-image-url', $match[0] ) );
199
			}, $content );
200
201
		}
202
203
		return $content;
204
	}
205
206
	/**
207
	 * Add tag to dns prefetch cdn host
208
	 */
209
	public function prefetch_cdn() {
210
		if ( ! empty ( $this->options['cdn_link'] ) ) {
211
			$host = parse_url( $this->options['cdn_link'], PHP_URL_HOST );
212
213
			printf(
214
				'<link rel="dns-prefetch" href="%s"/>',
215
				esc_attr( '//' . $host )
216
			);
217
		}
218
	}
219
220
	/**
221
	 * Start output buffer if auto retina is enabled
222
	 */
223
	public function buffer_start_for_retina() {
224
		if ( ! empty ( $this->options['add_dpi2_srcset'] ) ) {
225
			$this->buffer_started = true;
226
			ob_start( [ $this, 'add_retina' ] );
227
		}
228
	}
229
230
	/**
231
	 * Stop output buffer if it was enabled by the plugin
232
	 */
233
	public function buffer_end_for_retina() {
234
		if ( $this->buffer_started === true ) {
235
			ob_end_flush();
236
		}
237
	}
238
239
	/**
240
	 * Returns a array of global parameters to be applied in all images,
241
	 * according to plugin's settings.
242
	 *
243
	 * @return array Global parameters to be appened at the end of each img URL.
244
	 */
245
	protected function get_global_params() {
246
		$params = [];
247
248
		// For now, only "auto" is supported.
249
		$auto = [];
250
		if ( ! empty ( $this->options['auto_format'] ) ) {
251
			array_push( $auto, 'format' );
252
		}
253
254
		if ( ! empty ( $this->options['auto_enhance'] ) ) {
255
			array_push( $auto, 'enhance' );
256
		}
257
258
		if ( ! empty( $auto ) ) {
259
			$params['auto'] = implode( ',', $auto );
260
		}
261
262
		return $params;
263
	}
264
265
	/**
266
	 * Convert sizes in filename to parameters and returns origina filename without sizes.
267
	 * If no size is found the original filename is returned.
268
	 *
269
	 * @param string $filename
270
	 *
271
	 * @return array with filename and size arguments.
272
	 */
273
	protected function convert_filename_to_size_args( $filename ) {
274
		$arguments = [];
275
276
		$filename = preg_replace_callback( '/-(?<width>\d+)x(?<height>\d+)(?<extension>\.\w{3,4}$)/', function ( $match ) use ( &$arguments ) {
277
			$arguments = [
278
				'w' => $match['width'],
279
				'h' => $match['height']
280
			];
281
282
			return $match['extension'];
283
		}, $filename );
284
285
		return [ $filename, $arguments ];
286
	}
287
288
	/**
289
	 * Get all defined image sizes
290
	 *
291
	 * @return array
292
	 */
293
	protected function get_all_defined_sizes() {
294
		// Make thumbnails and other intermediate sizes.
295
		$theme_image_sizes = wp_get_additional_image_sizes();
296
297
		$sizes = [];
298
		foreach ( get_intermediate_image_sizes() as $s ) {
299
			$sizes[ $s ] = [ 'width' => '', 'height' => '', 'crop' => false ];
300 View Code Duplication
			if ( isset( $theme_image_sizes[ $s ]['width'] ) ) {
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...
301
				// For theme-added sizes
302
				$sizes[ $s ]['width'] = intval( $theme_image_sizes[ $s ]['width'] );
303
			} else {
304
				// For default sizes set in options
305
				$sizes[ $s ]['width'] = get_option( "{$s}_size_w" );
306
			}
307
308 View Code Duplication
			if ( isset( $theme_image_sizes[ $s ]['height'] ) ) {
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...
309
				// For theme-added sizes
310
				$sizes[ $s ]['height'] = intval( $theme_image_sizes[ $s ]['height'] );
311
			} else {
312
				// For default sizes set in options
313
				$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
314
			}
315
316
			if ( isset( $theme_image_sizes[ $s ]['crop'] ) ) {
317
				// For theme-added sizes
318
				$sizes[ $s ]['crop'] = $theme_image_sizes[ $s ]['crop'];
319
			} else {
320
				// For default sizes set in options
321
				$sizes[ $s ]['crop'] = get_option( "{$s}_crop" );
322
			}
323
		}
324
325
		return $sizes;
326
	}
327
}
328
329
Images_Via_Imgix::instance();
330