Passed
Push — master ( 904877...40e2f6 )
by Dominik
06:14
created

TimberDynamicResize   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Importance

Changes 8
Bugs 1 Features 2
Metric Value
wmc 43
eloc 203
c 8
b 1
f 2
dl 0
loc 355
rs 8.96

23 Methods

Rating   Name   Duplication   Size   Complexity  
A createTable() 0 26 3
A getRelativeUploadDir() 0 7 2
A addDynamicHooks() 0 5 1
A resizeDynamic() 0 23 3
A getDefaultRelativeUploadDir() 0 11 3
A checkAndGenerateImage() 0 38 4
A registerRewriteRule() 0 8 1
A storeResizedUrls() 0 11 1
A getTableName() 0 4 1
A addRewriteTag() 0 4 1
A addImageSeparatorToUploadUrl() 0 7 1
A getUploadsBasedir() 0 4 1
A __construct() 0 9 2
A getUploadsBaseurl() 0 4 1
A addWebpRewriteRule() 0 32 1
A generateImage() 0 30 3
A toggleDynamic() 0 13 2
A changeRelativeUploadPath() 0 4 1
A removeRewriteTag() 0 4 1
A parseRequest() 0 4 2
A toggleWebp() 0 13 2
A addHooks() 0 17 4
A addImageSeparatorToUploadPath() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like TimberDynamicResize 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.

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 TimberDynamicResize, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Flynt\Utils;
4
5
use Twig\TwigFilter;
6
use Timber\ImageHelper;
7
use Timber\Image\Operation\Resize;
8
9
class TimberDynamicResize
10
{
11
    const DB_VERSION = '2.0';
12
    const TABLE_NAME = 'resized_images';
13
    const IMAGE_QUERY_VAR = 'resized-images';
14
    const IMAGE_PATH_SEPARATOR = 'resized';
15
16
    public $flyntResizedImages = [];
17
18
    protected $enabled = false;
19
    protected $webpEnabled = false;
20
21
    public function __construct()
22
    {
23
        $this->enabled = get_field('field_global_TimberDynamicResize_dynamicImageGeneration', 'option');
0 ignored issues
show
Bug introduced by
The function get_field was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

23
        $this->enabled = /** @scrutinizer ignore-call */ get_field('field_global_TimberDynamicResize_dynamicImageGeneration', 'option');
Loading history...
24
        $this->webpEnabled = get_field('field_global_TimberDynamicResize_webpSupport', 'option');
25
        if ($this->enabled) {
26
            $this->createTable();
27
            $this->addDynamicHooks();
28
        }
29
        $this->addHooks();
30
    }
31
32
    protected function createTable()
33
    {
34
        $optionName = static::TABLE_NAME . '_db_version';
35
36
        $installedVersion = get_option($optionName);
37
38
        if ($installedVersion !== static::DB_VERSION) {
39
            global $wpdb;
40
            $tableName = self::getTableName();
0 ignored issues
show
Bug Best Practice introduced by
The method Flynt\Utils\TimberDynamicResize::getTableName() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

40
            /** @scrutinizer ignore-call */ 
41
            $tableName = self::getTableName();
Loading history...
41
42
            $charsetCollate = $wpdb->get_charset_collate();
43
44
            $sql = "CREATE TABLE $tableName (
45
                width int(11) NOT NULL,
46
                height int(11) NOT NULL,
47
                crop varchar(32) NOT NULL
48
            ) $charsetCollate;";
49
50
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
51
            dbDelta($sql);
52
53
            if (version_compare($installedVersion, '2.0', '<=')) {
0 ignored issues
show
Bug introduced by
It seems like $installedVersion can also be of type false; however, parameter $version1 of version_compare() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

53
            if (version_compare(/** @scrutinizer ignore-type */ $installedVersion, '2.0', '<=')) {
Loading history...
54
                $wpdb->query("ALTER TABLE {$tableName} ADD PRIMARY KEY(`width`, `height`, `crop`);");
55
            }
56
57
            update_option($optionName, static::DB_VERSION);
58
        }
59
    }
60
61
    protected function addDynamicHooks()
62
    {
63
        add_filter('init', [$this, 'addRewriteTag']);
64
        add_action('generate_rewrite_rules', [$this, 'registerRewriteRule']);
65
        add_action('parse_request', [$this, 'parseRequest']);
66
    }
67
68
    public function parseRequest($wp)
69
    {
70
        if (isset($wp->query_vars[static::IMAGE_QUERY_VAR])) {
71
            $this->checkAndGenerateImage($wp->query_vars[static::IMAGE_QUERY_VAR]);
72
        }
73
    }
74
75
    protected function addHooks()
76
    {
77
        add_action('timber/twig/filters', function ($twig) {
78
            $twig->addFilter(
79
                new TwigFilter('resizeDynamic', [$this, 'resizeDynamic'])
80
            );
81
            return $twig;
82
        });
83
        if ($this->webpEnabled) {
84
            add_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
85
        }
86
        if ($this->enabled || $this->webpEnabled) {
87
            add_action('after_switch_theme', function () {
88
                add_action('shutdown', 'flush_rewrite_rules');
89
            });
90
            add_action('switch_theme', function () {
91
                flush_rewrite_rules(true);
92
            });
93
        }
94
    }
95
96
    public function getTableName()
97
    {
98
        global $wpdb;
99
        return $wpdb->prefix . static::TABLE_NAME;
100
    }
101
102
    public static function getDefaultRelativeUploadDir()
103
    {
104
        require_once(ABSPATH . 'wp-admin/includes/file.php');
105
        $uploadDir = wp_upload_dir();
106
        $homePath = get_home_path();
107
        if (!empty($homePath) && $homePath !== '/') {
108
            $relativeUploadDir = str_replace($homePath, '', $uploadDir['basedir']);
109
        } else {
110
            $relativeUploadDir = $uploadDir['relative'];
111
        }
112
        return $relativeUploadDir;
113
    }
114
115
    public function getRelativeUploadDir()
116
    {
117
        $relativeUploadPath = get_field('field_global_TimberDynamicResize_relativeUploadPath', 'option');
0 ignored issues
show
Bug introduced by
The function get_field was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
        $relativeUploadPath = /** @scrutinizer ignore-call */ get_field('field_global_TimberDynamicResize_relativeUploadPath', 'option');
Loading history...
118
        if (empty($relativeUploadPath)) {
119
            return static::getDefaultRelativeUploadDir();
120
        } else {
121
            return $relativeUploadPath;
122
        }
123
    }
124
125
    public function getUploadsBaseurl()
126
    {
127
        $uploadDir = wp_upload_dir();
128
        return $uploadDir['baseurl'];
129
    }
130
131
    public function getUploadsBasedir()
132
    {
133
        $uploadDir = wp_upload_dir();
134
        return $uploadDir['basedir'];
135
    }
136
137
    public function resizeDynamic(
138
        $src,
139
        $w,
140
        $h = 0,
141
        $crop = 'default',
142
        $force = false
143
    ) {
144
        if ($this->enabled) {
145
            $resizeOp = new Resize($w, $h, $crop);
146
            $fileinfo = pathinfo($src);
147
            $resizedUrl = $resizeOp->filename(
148
                $fileinfo['dirname'] . '/' . $fileinfo['filename'],
149
                $fileinfo['extension']
150
            );
151
152
            if (empty($this->flyntResizedImages)) {
153
                add_action('shutdown', [$this, 'storeResizedUrls'], -1);
154
            }
155
            $this->flyntResizedImages[$w . '-' . $h . '-' . $crop] = [$w, $h, $crop];
156
157
            return $this->addImageSeparatorToUploadUrl($resizedUrl);
158
        } else {
159
            return $this->generateImage($src, $w, $h, $crop, $force);
160
        }
161
    }
162
163
    public function registerRewriteRule($wpRewrite)
164
    {
165
        $routeName = static::IMAGE_QUERY_VAR;
166
        $relativeUploadDir = $this->getRelativeUploadDir();
167
        $relativeUploadDir = trailingslashit($relativeUploadDir) . static::IMAGE_PATH_SEPARATOR;
168
        $wpRewrite->rules = array_merge(
169
            ["^{$relativeUploadDir}/?(.*?)/?$" => "index.php?{$routeName}=\$matches[1]"],
170
            $wpRewrite->rules
171
        );
172
    }
173
174
    public function addRewriteTag()
175
    {
176
        $routeName = static::IMAGE_QUERY_VAR;
177
        add_rewrite_tag("%{$routeName}%", "([^&]+)");
178
    }
179
180
    public function removeRewriteTag()
181
    {
182
        $routeName = static::IMAGE_QUERY_VAR;
183
        remove_rewrite_tag("%{$routeName}%");
184
    }
185
186
    public function checkAndGenerateImage($relativePath)
187
    {
188
        $matched = preg_match('/(.+)-(\d+)x(\d+)-c-(.+)(\..*)$/', $relativePath, $matchedSrc);
189
        $exists = false;
190
        if ($matched) {
191
            $originalRelativePath = $matchedSrc[1] . $matchedSrc[5];
192
            $originalPath = trailingslashit($this->getUploadsBasedir()) . $originalRelativePath;
193
            $originalUrl = trailingslashit($this->getUploadsBaseurl()) . $originalRelativePath;
194
            $exists = file_exists($originalPath);
195
            $w = (int) $matchedSrc[2];
196
            $h = (int) $matchedSrc[3];
197
            $crop = $matchedSrc[4];
198
        }
199
200
        if ($exists) {
201
            global $wpdb;
202
            $tableName = $this->getTableName();
203
            $resizedImage = $wpdb->get_row(
204
                $wpdb->prepare("SELECT * FROM {$tableName} WHERE width = %d AND height = %d AND crop = %s", [
205
                    $w,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $w does not seem to be defined for all execution paths leading up to this point.
Loading history...
206
                    $h,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $h does not seem to be defined for all execution paths leading up to this point.
Loading history...
207
                    $crop,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $crop does not seem to be defined for all execution paths leading up to this point.
Loading history...
208
                ])
209
            );
210
        }
211
212
        if (empty($resizedImage)) {
213
            global $wp_query;
214
            $wp_query->set_404();
215
            status_header(404);
216
            nocache_headers();
217
            include get_404_template();
218
            exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
219
        } else {
220
            $resizedUrl = $this->generateImage($originalUrl, $w, $h, $crop);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $originalUrl does not seem to be defined for all execution paths leading up to this point.
Loading history...
221
222
            wp_redirect($resizedUrl);
223
            exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
224
        }
225
    }
226
227
    protected function generateImage($url, $w, $h, $crop, $force = false)
228
    {
229
        add_filter('timber/image/new_url', [$this, 'addImageSeparatorToUploadUrl']);
230
        add_filter('timber/image/new_path', [$this, 'addImageSeparatorToUploadPath']);
231
232
        $resizedUrl = ImageHelper::resize(
233
            $url,
234
            $w,
235
            $h,
236
            $crop,
237
            $force
238
        );
239
240
        remove_filter('timber/image/new_url', [$this, 'addImageSeparatorToUploadUrl']);
241
        remove_filter('timber/image/new_path', [$this, 'addImageSeparatorToUploadPath']);
242
243
        if ($this->webpEnabled) {
244
            $fileinfo = pathinfo($resizedUrl);
245
            if (
246
                in_array($fileinfo['extension'], [
247
                'jpeg',
248
                'jpg',
249
                'png',
250
                ])
251
            ) {
252
                ImageHelper::img_to_webp($resizedUrl);
253
            }
254
        }
255
256
        return $resizedUrl;
257
    }
258
259
    public function addImageSeparatorToUploadUrl($url)
260
    {
261
        $baseurl = $this->getUploadsBaseurl();
262
        return str_replace(
263
            $baseurl,
264
            trailingslashit($baseurl) . static::IMAGE_PATH_SEPARATOR,
265
            $url
266
        );
267
    }
268
269
    public function addImageSeparatorToUploadPath($path = '')
270
    {
271
        $basepath = $this->getUploadsBasedir();
272
        return str_replace(
273
            $basepath,
274
            trailingslashit($basepath) . static::IMAGE_PATH_SEPARATOR,
275
            empty($path) ? $basepath : $path
276
        );
277
    }
278
279
    public function addWebpRewriteRule($rules)
280
    {
281
        $dynamicImageRule = <<<EOD
282
\n# BEGIN Flynt dynamic images
283
<IfModule mod_setenvif.c>
284
# Vary: Accept for all the requests to jpeg and png
285
SetEnvIf Request_URI "\.(jpe?g|png)$" REQUEST_image
286
</IfModule>
287
288
<IfModule mod_rewrite.c>
289
RewriteEngine On
290
291
# Check if browser supports WebP images
292
RewriteCond %{HTTP_ACCEPT} image/webp
293
294
# Check if WebP replacement image exists
295
RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
296
297
# Serve WebP image instead
298
RewriteRule (.+)\.(jpe?g|png)$ $1.webp [T=image/webp]
299
</IfModule>
300
301
<IfModule mod_headers.c>
302
Header merge Vary Accept env=REQUEST_image
303
</IfModule>
304
305
<IfModule mod_mime.c>
306
AddType image/webp .webp
307
</IfModule>\n
308
# END Flynt dynamic images\n\n
309
EOD;
310
        return $dynamicImageRule . $rules;
311
    }
312
313
    public function storeResizedUrls()
314
    {
315
        global $wpdb;
316
        $tableName = $this->getTableName();
317
        $values = array_values($this->flyntResizedImages);
318
        $placeholders = array_fill(0, count($values), '(%d, %d, %s)');
319
        $placeholdersString = implode(', ', $placeholders);
320
        $wpdb->query(
321
            $wpdb->prepare(
322
                "INSERT IGNORE INTO {$tableName} (width, height, crop) VALUES {$placeholdersString}",
323
                call_user_func_array('array_merge', $values)
324
            )
325
        );
326
    }
327
328
    public function toggleDynamic($enable)
329
    {
330
        if ($enable) {
331
            $this->addRewriteTag();
332
            add_action('generate_rewrite_rules', [$this, 'registerRewriteRule']);
333
            add_action('parse_request', [$this, 'parseRequest']);
334
        } else {
335
            $this->removeRewriteTag();
336
            remove_action('generate_rewrite_rules', [$this, 'registerRewriteRule']);
337
            remove_action('parse_request', [$this, 'parseRequest']);
338
        }
339
        add_action('shutdown', function () {
340
            flush_rewrite_rules(false);
341
        });
342
    }
343
344
    public function toggleWebp($enable)
345
    {
346
        if ($enable) {
347
            add_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
348
        } else {
349
            remove_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
350
        }
351
        add_action('shutdown', function () {
352
            require_once(ABSPATH . 'wp-admin/includes/file.php');
353
            WP_Filesystem();
354
            global $wp_filesystem;
355
            flush_rewrite_rules(true);
356
            @$wp_filesystem->rmdir($this->addImageSeparatorToUploadPath(), true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rmdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

356
            /** @scrutinizer ignore-unhandled */ @$wp_filesystem->rmdir($this->addImageSeparatorToUploadPath(), true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
357
        });
358
    }
359
360
    public function changeRelativeUploadPath($relativeUploadPath)
0 ignored issues
show
Unused Code introduced by
The parameter $relativeUploadPath is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

360
    public function changeRelativeUploadPath(/** @scrutinizer ignore-unused */ $relativeUploadPath)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
361
    {
362
        add_action('shutdown', function () {
363
            flush_rewrite_rules(false);
364
        });
365
    }
366
}
367