Completed
Pull Request — master (#214)
by Dominik
05:45 queued 01:46
created

TimberDynamicResize::addHooks()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 12
c 3
b 0
f 1
dl 0
loc 18
rs 9.8666
cc 4
nc 4
nop 0
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 = 'dynamic-images';
14
    const IMAGE_PATH_SEPARATOR = 'dynamic';
15
16
    public $flyntResizedImages = [];
17
18
    protected $enabled = false;
19
    protected $webpEnabled = false;
20
21
    public function __construct()
22
    {
23
        $this->enabled = !(apply_filters('Flynt/TimberDynamicResize/disableDynamic', false));
24
        $this->webpEnabled = !(apply_filters('Flynt/TimberDynamicResize/disableWebp', false));
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', function ($wp) {
66
            if (isset($wp->query_vars[static::IMAGE_QUERY_VAR])) {
67
                $this->checkAndGenerateImage($wp->query_vars[static::IMAGE_QUERY_VAR]);
68
            }
69
        });
70
    }
71
72
    protected function addHooks()
73
    {
74
        add_action('timber/twig/filters', function ($twig) {
75
            $twig->addFilter(
76
                new TwigFilter('resizeDynamic', [$this, 'resizeDynamic'])
77
            );
78
            return $twig;
79
        });
80
        if ($this->webpEnabled) {
81
            add_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
82
        }
83
        if ($this->enabled || $this->webpEnabled) {
84
            add_action('after_switch_theme', function () {
85
                add_action('shutdown', 'flush_rewrite_rules');
86
            });
87
            add_action('switch_theme', function () {
88
                remove_filter('mod_rewrite_rules', [$this, 'addWebpRewriteRule']);
89
                flush_rewrite_rules();
90
            });
91
        }
92
    }
93
94
    public function getTableName()
95
    {
96
        global $wpdb;
97
        return $wpdb->prefix . static::TABLE_NAME;
98
    }
99
100
    public function getRelativeUploadDir()
101
    {
102
        require_once(ABSPATH . 'wp-admin/includes/file.php');
103
        $uploadDir = wp_upload_dir();
104
        $homePath = get_home_path();
105
        if (!empty($homePath) && $homePath !== '/') {
106
            $relativeUploadDir = str_replace($homePath, '', $uploadDir['basedir']);
107
        } else {
108
            $relativeUploadDir = $uploadDir['relative'];
109
        }
110
        return apply_filters('Flynt/TimberDynamicResize/relativeUploadDir', $relativeUploadDir);
111
    }
112
113
    public function getUploadsBaseurl()
114
    {
115
        $uploadDir = wp_upload_dir();
116
        return $uploadDir['baseurl'];
117
    }
118
119
    public function getUploadsBasedir()
120
    {
121
        $uploadDir = wp_upload_dir();
122
        return $uploadDir['basedir'];
123
    }
124
125
    public function resizeDynamic(
126
        $src,
127
        $w,
128
        $h = 0,
129
        $crop = 'default',
130
        $force = false
131
    ) {
132
        if ($this->enabled) {
133
            $resizeOp = new Resize($w, $h, $crop);
134
            $fileinfo = pathinfo($src);
135
            $resizedUrl = $resizeOp->filename(
136
                $fileinfo['dirname'] . '/' . $fileinfo['filename'],
137
                $fileinfo['extension']
138
            );
139
140
            if (empty($this->flyntResizedImages)) {
141
                add_action('shutdown', [$this, 'storeResizedUrls'], -1);
142
            }
143
            $this->flyntResizedImages[$w . '-' . $h . '-' . $crop] = [$w, $h, $crop];
144
145
            return $this->addImageSeparatorToUploadUrl($resizedUrl);
146
        } else {
147
            return $this->generateImage($src, $w, $h, $crop, $force);
148
        }
149
    }
150
151
    public function registerRewriteRule($wpRewrite)
152
    {
153
        $routeName = static::IMAGE_QUERY_VAR;
154
        $relativeUploadDir = $this->getRelativeUploadDir();
155
        $relativeUploadDir = trailingslashit($relativeUploadDir) . static::IMAGE_PATH_SEPARATOR;
156
        $wpRewrite->rules = array_merge(
157
            ["^{$relativeUploadDir}/?(.*?)/?$" => "index.php?{$routeName}=\$matches[1]"],
158
            $wpRewrite->rules
159
        );
160
    }
161
162
    public function addRewriteTag()
163
    {
164
        $routeName = static::IMAGE_QUERY_VAR;
165
        add_rewrite_tag("%{$routeName}%", "([^&]+)");
166
    }
167
168
    public function checkAndGenerateImage($relativePath)
169
    {
170
        $matched = preg_match('/(.+)-(\d+)x(\d+)-c-(.+)(\..*)$/', $relativePath, $matchedSrc);
171
        $exists = false;
172
        if ($matched) {
173
            $originalRelativePath = $matchedSrc[1] . $matchedSrc[5];
174
            $originalPath = trailingslashit($this->getUploadsBasedir()) . $originalRelativePath;
175
            $originalUrl = trailingslashit($this->getUploadsBaseurl()) . $originalRelativePath;
176
            $exists = file_exists($originalPath);
177
            $w = (int) $matchedSrc[2];
178
            $h = (int) $matchedSrc[3];
179
            $crop = $matchedSrc[4];
180
        }
181
182
        if ($exists) {
183
            global $wpdb;
184
            $tableName = $this->getTableName();
185
            $resizedImage = $wpdb->get_row(
186
                $wpdb->prepare("SELECT * FROM {$tableName} WHERE width = %d AND height = %d AND crop = %s", [
187
                    $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...
188
                    $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...
189
                    $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...
190
                ])
191
            );
192
        }
193
194
        if (empty($resizedImage)) {
195
            global $wp_query;
196
            $wp_query->set_404();
197
            status_header(404);
198
            nocache_headers();
199
            include get_404_template();
200
            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...
201
        } else {
202
            $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...
203
204
            wp_redirect($resizedUrl);
205
            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...
206
        }
207
    }
208
209
    protected function generateImage($url, $w, $h, $crop, $force = false)
210
    {
211
        add_filter('timber/image/new_url', [$this, 'addImageSeparatorToUploadUrl']);
212
        add_filter('timber/image/new_path', [$this, 'addImageSeparatorToUploadPath']);
213
214
        $resizedUrl = ImageHelper::resize(
215
            $url,
216
            $w,
217
            $h,
218
            $crop,
219
            $force
220
        );
221
222
        remove_filter('timber/image/new_url', [$this, 'addImageSeparatorToUploadUrl']);
223
        remove_filter('timber/image/new_path', [$this, 'addImageSeparatorToUploadPath']);
224
225
        if ($this->webpEnabled) {
226
            $fileinfo = pathinfo($resizedUrl);
227
            if (in_array($fileinfo['extension'], [
228
                'jpeg',
229
                'jpg',
230
                'png',
231
            ])) {
232
                ImageHelper::img_to_webp($resizedUrl);
233
            }
234
        }
235
236
        return $resizedUrl;
237
    }
238
239
    public function addImageSeparatorToUploadUrl($url)
240
    {
241
        $baseurl = $this->getUploadsBaseurl();
242
        return str_replace(
243
            $baseurl,
244
            trailingslashit($baseurl) . static::IMAGE_PATH_SEPARATOR,
245
            $url
246
        );
247
    }
248
249
    public function addImageSeparatorToUploadPath($path)
250
    {
251
        $basepath = $this->getUploadsBasedir();
252
        return str_replace(
253
            $basepath,
254
            trailingslashit($basepath) . static::IMAGE_PATH_SEPARATOR,
255
            $path
256
        );
257
    }
258
259
    public function addWebpRewriteRule($rules)
260
    {
261
        $dynamicImageRule = <<<EOD
262
\n# BEGIN Flynt dynamic images
263
<IfModule mod_setenvif.c>
264
# Vary: Accept for all the requests to jpeg and png
265
SetEnvIf Request_URI "\.(jpe?g|png)$" REQUEST_image
266
</IfModule>
267
268
<IfModule mod_rewrite.c>
269
RewriteEngine On
270
271
# Check if browser supports WebP images
272
RewriteCond %{HTTP_ACCEPT} image/webp
273
274
# Check if WebP replacement image exists
275
RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
276
277
# Serve WebP image instead
278
RewriteRule (.+)\.(jpe?g|png)$ $1.webp [T=image/webp]
279
</IfModule>
280
281
<IfModule mod_headers.c>
282
Header append Vary Accept env=REQUEST_image
283
</IfModule>
284
285
<IfModule mod_mime.c>
286
AddType image/webp .webp
287
</IfModule>\n
288
# END Flynt dynamic images\n\n
289
EOD;
290
        return $dynamicImageRule . $rules;
291
    }
292
293
    public function storeResizedUrls()
294
    {
295
        global $wpdb;
296
        $tableName = $this->getTableName();
297
        $values = array_values($this->flyntResizedImages);
298
        $placeholders = array_fill(0, count($values), '(%d, %d, %s)');
299
        $placeholdersString = implode(', ', $placeholders);
300
        $wpdb->query(
301
            $wpdb->prepare(
302
                "INSERT IGNORE INTO {$tableName} (width, height, crop) VALUES {$placeholdersString}",
303
                call_user_func_array('array_merge', $values)
304
            )
305
        );
306
    }
307
}
308