TimberDynamicResize::addWebpRewriteRule()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 32
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 30
c 1
b 0
f 0
dl 0
loc 32
rs 9.44
cc 1
nc 1
nop 1
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
            $baseDir = str_replace('\\', '/', $uploadDir['basedir']);
109
            $relativeUploadDir = str_replace($homePath, '', $baseDir);
110
        } else {
111
            $relativeUploadDir = $uploadDir['relative'];
112
        }
113
        return $relativeUploadDir;
114
    }
115
116
    public function getRelativeUploadDir()
117
    {
118
        $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

118
        $relativeUploadPath = /** @scrutinizer ignore-call */ get_field('field_global_TimberDynamicResize_relativeUploadPath', 'option');
Loading history...
119
        if (empty($relativeUploadPath)) {
120
            return static::getDefaultRelativeUploadDir();
121
        } else {
122
            return $relativeUploadPath;
123
        }
124
    }
125
126
    public function getUploadsBaseurl()
127
    {
128
        $uploadDir = wp_upload_dir();
129
        return $uploadDir['baseurl'];
130
    }
131
132
    public function getUploadsBasedir()
133
    {
134
        $uploadDir = wp_upload_dir();
135
        return $uploadDir['basedir'];
136
    }
137
138
    public function resizeDynamic(
139
        $src,
140
        $w,
141
        $h = 0,
142
        $crop = 'default',
143
        $force = false
144
    ) {
145
        if ($this->enabled) {
146
            $resizeOp = new Resize($w, $h, $crop);
147
            $fileinfo = pathinfo($src);
148
            $resizedUrl = $resizeOp->filename(
149
                $fileinfo['dirname'] . '/' . $fileinfo['filename'],
150
                $fileinfo['extension']
151
            );
152
153
            if (empty($this->flyntResizedImages)) {
154
                add_action('shutdown', [$this, 'storeResizedUrls'], -1);
155
            }
156
            $this->flyntResizedImages[$w . '-' . $h . '-' . $crop] = [$w, $h, $crop];
157
158
            return $this->addImageSeparatorToUploadUrl($resizedUrl);
159
        } else {
160
            return $this->generateImage($src, $w, $h, $crop, $force);
161
        }
162
    }
163
164
    public function registerRewriteRule($wpRewrite)
165
    {
166
        $routeName = static::IMAGE_QUERY_VAR;
167
        $relativeUploadDir = $this->getRelativeUploadDir();
168
        $relativeUploadDir = trailingslashit($relativeUploadDir) . static::IMAGE_PATH_SEPARATOR;
169
        $wpRewrite->rules = array_merge(
170
            ["^{$relativeUploadDir}/?(.*?)/?$" => "index.php?{$routeName}=\$matches[1]"],
171
            $wpRewrite->rules
172
        );
173
    }
174
175
    public function addRewriteTag()
176
    {
177
        $routeName = static::IMAGE_QUERY_VAR;
178
        add_rewrite_tag("%{$routeName}%", "([^&]+)");
179
    }
180
181
    public function removeRewriteTag()
182
    {
183
        $routeName = static::IMAGE_QUERY_VAR;
184
        remove_rewrite_tag("%{$routeName}%");
185
    }
186
187
    public function checkAndGenerateImage($relativePath)
188
    {
189
        $matched = preg_match('/(.+)-(\d+)x(\d+)-c-(.+)(\..*)$/', $relativePath, $matchedSrc);
190
        $exists = false;
191
        if ($matched) {
192
            $originalRelativePath = $matchedSrc[1] . $matchedSrc[5];
193
            $originalPath = trailingslashit($this->getUploadsBasedir()) . $originalRelativePath;
194
            $originalUrl = trailingslashit($this->getUploadsBaseurl()) . $originalRelativePath;
195
            $exists = file_exists($originalPath);
196
            $w = (int) $matchedSrc[2];
197
            $h = (int) $matchedSrc[3];
198
            $crop = $matchedSrc[4];
199
        }
200
201
        if ($exists) {
202
            global $wpdb;
203
            $tableName = $this->getTableName();
204
            $resizedImage = $wpdb->get_row(
205
                $wpdb->prepare("SELECT * FROM {$tableName} WHERE width = %d AND height = %d AND crop = %s", [
206
                    $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...
207
                    $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...
208
                    $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...
209
                ])
210
            );
211
        }
212
213
        if (empty($resizedImage)) {
214
            global $wp_query;
215
            $wp_query->set_404();
216
            status_header(404);
217
            nocache_headers();
218
            include get_404_template();
219
            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...
220
        } else {
221
            $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...
222
223
            wp_redirect($resizedUrl);
224
            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...
225
        }
226
    }
227
228
    protected function generateImage($url, $w, $h, $crop, $force = false)
229
    {
230
        add_filter('timber/image/new_url', [$this, 'addImageSeparatorToUploadUrl']);
231
        add_filter('timber/image/new_path', [$this, 'addImageSeparatorToUploadPath']);
232
233
        $resizedUrl = ImageHelper::resize(
234
            $url,
235
            $w,
236
            $h,
237
            $crop,
238
            $force
239
        );
240
241
        remove_filter('timber/image/new_url', [$this, 'addImageSeparatorToUploadUrl']);
242
        remove_filter('timber/image/new_path', [$this, 'addImageSeparatorToUploadPath']);
243
244
        if ($this->webpEnabled) {
245
            $fileinfo = pathinfo($resizedUrl);
246
            if (
247
                in_array($fileinfo['extension'], [
248
                'jpeg',
249
                'jpg',
250
                'png',
251
                ])
252
            ) {
253
                ImageHelper::img_to_webp($resizedUrl);
254
            }
255
        }
256
257
        return $resizedUrl;
258
    }
259
260
    public function addImageSeparatorToUploadUrl($url)
261
    {
262
        $baseurl = $this->getUploadsBaseurl();
263
        return str_replace(
264
            $baseurl,
265
            trailingslashit($baseurl) . static::IMAGE_PATH_SEPARATOR,
266
            $url
267
        );
268
    }
269
270
    public function addImageSeparatorToUploadPath($path = '')
271
    {
272
        $basepath = $this->getUploadsBasedir();
273
        return str_replace(
274
            $basepath,
275
            trailingslashit($basepath) . static::IMAGE_PATH_SEPARATOR,
276
            empty($path) ? $basepath : $path
277
        );
278
    }
279
280
    public function addWebpRewriteRule($rules)
281
    {
282
        $dynamicImageRule = <<<EOD
283
\n# BEGIN Flynt dynamic images
284
<IfModule mod_setenvif.c>
285
# Vary: Accept for all the requests to jpeg and png
286
SetEnvIf Request_URI "\.(jpe?g|png)$" REQUEST_image
287
</IfModule>
288
289
<IfModule mod_rewrite.c>
290
RewriteEngine On
291
292
# Check if browser supports WebP images
293
RewriteCond %{HTTP_ACCEPT} image/webp
294
295
# Check if WebP replacement image exists
296
RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
297
298
# Serve WebP image instead
299
RewriteRule (.+)\.(jpe?g|png)$ $1.webp [T=image/webp]
300
</IfModule>
301
302
<IfModule mod_headers.c>
303
Header merge Vary Accept env=REQUEST_image
304
</IfModule>
305
306
<IfModule mod_mime.c>
307
AddType image/webp .webp
308
</IfModule>\n
309
# END Flynt dynamic images\n\n
310
EOD;
311
        return $dynamicImageRule . $rules;
312
    }
313
314
    public function storeResizedUrls()
315
    {
316
        global $wpdb;
317
        $tableName = $this->getTableName();
318
        $values = array_values($this->flyntResizedImages);
319
        $placeholders = array_fill(0, count($values), '(%d, %d, %s)');
320
        $placeholdersString = implode(', ', $placeholders);
321
        $wpdb->query(
322
            $wpdb->prepare(
323
                "INSERT IGNORE INTO {$tableName} (width, height, crop) VALUES {$placeholdersString}",
324
                call_user_func_array('array_merge', $values)
325
            )
326
        );
327
    }
328
329
    public function toggleDynamic($enable)
330
    {
331
        if ($enable) {
332
            $this->addRewriteTag();
333
            add_action('generate_rewrite_rules', [$this, 'registerRewriteRule']);
334
            add_action('parse_request', [$this, 'parseRequest']);
335
        } else {
336
            $this->removeRewriteTag();
337
            remove_action('generate_rewrite_rules', [$this, 'registerRewriteRule']);
338
            remove_action('parse_request', [$this, 'parseRequest']);
339
        }
340
        add_action('shutdown', function () {
341
            flush_rewrite_rules(false);
342
        });
343
    }
344
345
    public function toggleWebp($enable)
346
    {
347
        if ($enable) {
348
            add_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
349
        } else {
350
            remove_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
351
        }
352
        add_action('shutdown', function () {
353
            require_once(ABSPATH . 'wp-admin/includes/file.php');
354
            WP_Filesystem();
355
            global $wp_filesystem;
356
            flush_rewrite_rules(true);
357
            @$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

357
            /** @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...
358
        });
359
    }
360
361
    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

361
    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...
362
    {
363
        add_action('shutdown', function () {
364
            flush_rewrite_rules(false);
365
        });
366
    }
367
}
368