WebP Express - Version 0.15.0

Version Description

(released: 17 sep 2019)

  • Provided test-buttons for checking if the redirects works.
  • You can now choose which folders WebP Express is active in. Ie "Uploads and Themes".
  • You can now choose an alternative file structure for the webps which does not rely on DOCUMENT_ROOT being available.
  • WebP Express can now handle when wp-content is symlinked.
  • The .htaccess rules are now divided across folders. Some rules are needed where the source files are located, some where the webp files are located.
  • Added option to convert only PNG files
  • And a couple of bugfixes.

For more info, see the closed issues on the 0.15.0 milestone on the github repository: https://github.com/rosell-dk/webp-express/milestone/22?closed=1

Download this release

Release Info

Developer rosell.dk
Plugin Icon 128x128 WebP Express
Version 0.15.0
Comparing to
See all releases

Code changes from version 0.14.22 to 0.15.0

Files changed (86) hide show
  1. BACKERS.md +1 -0
  2. README.md +95 -106
  3. README.txt +109 -98
  4. changelog.txt +161 -0
  5. composer.lock +5 -5
  6. docs/development.md +2 -15
  7. lib/classes/AdminInit.php +3 -2
  8. lib/classes/AlterHtmlHelper.php +37 -32
  9. lib/classes/BulkConvert.php +52 -27
  10. lib/classes/CacheMover.php +57 -5
  11. lib/classes/CachePurge.php +4 -0
  12. lib/classes/CapabilityTest.php +6 -0
  13. lib/classes/Config.php +75 -10
  14. lib/classes/Convert.php +40 -8
  15. lib/classes/ConvertHelperIndependent.php +306 -71
  16. lib/classes/FileHelper.php +31 -0
  17. lib/classes/HTAccess.php +120 -565
  18. lib/classes/HTAccessRules.php +855 -0
  19. lib/classes/HandleUploadHooks.php +1 -1
  20. lib/classes/ImageRoot.php +50 -0
  21. lib/classes/ImageRoots.php +49 -0
  22. lib/classes/Mime.php +15 -8
  23. lib/classes/PathHelper.php +344 -0
  24. lib/classes/Paths.php +351 -95
  25. lib/classes/PlatformInfo.php +64 -15
  26. lib/classes/PluginActivate.php +3 -2
  27. lib/classes/PluginDeactivate.php +9 -6
  28. lib/classes/SanityCheck.php +123 -43
  29. lib/classes/SelfTest.php +118 -0
  30. lib/classes/SelfTestHelper.php +535 -0
  31. lib/classes/SelfTestRedirectAbstract.php +115 -0
  32. lib/classes/SelfTestRedirectToConverter.php +208 -0
  33. lib/classes/SelfTestRedirectToExisting.php +253 -0
  34. lib/classes/SelfTestRedirectToWebPRealizer.php +228 -0
  35. lib/classes/WebPOnDemand.php +229 -0
  36. lib/classes/WebPRealizer.php +253 -0
  37. lib/classes/WodConfigLoader.php +180 -0
  38. lib/dismissable-messages/0.15.0/new-scope-setting-content.php +12 -0
  39. lib/dismissable-messages/0.15.0/new-scope-setting-index.php +13 -0
  40. lib/dismissable-messages/0.15.0/new-scope-setting-no-uploads.php +12 -0
  41. lib/migrate/migrate11.php +77 -0
  42. lib/migrate/migrate7.php +1 -1
  43. lib/options/css/webp-express-options-page.css +56 -2
  44. lib/options/enqueue_scripts.php +29 -11
  45. lib/options/js/0.14.9/escapeHTML.js +0 -16
  46. lib/options/js/{0.14.9 → 0.15.0}/authorized_sites_bak.js +0 -0
  47. lib/options/js/{0.14.9 → 0.15.0}/bulk-convert.js +18 -13
  48. lib/options/js/{0.14.9 → 0.15.0}/converters.js +0 -45
  49. lib/options/js/{0.14.9 → 0.15.0}/das-popup.js +0 -0
  50. lib/options/js/0.15.0/escapeHTML.js +34 -0
  51. lib/options/js/{0.14.9 → 0.15.0}/image-comparison-slider.js +0 -0
  52. lib/options/js/{0.14.9 → 0.15.0}/page.js +20 -7
  53. lib/options/js/{0.14.9 → 0.15.0}/purge-cache.js +1 -1
  54. lib/options/js/0.15.0/self-test.js +156 -0
  55. lib/options/js/{0.14.9 → 0.15.0}/sortable.min.js +0 -0
  56. lib/options/js/{0.14.9 → 0.15.0}/test-convert.js +8 -2
  57. lib/options/js/{0.14.9 → 0.15.0}/whitelist.js +0 -0
  58. lib/options/options/conversion-options/jpeg.inc +1 -1
  59. lib/options/options/general/destination-extension.inc +2 -1
  60. lib/options/options/general/destination-structure.inc +38 -0
  61. lib/options/options/general/general.inc +11 -0
  62. lib/options/options/general/image-types.inc +1 -0
  63. lib/options/options/general/scope.inc +59 -0
  64. lib/options/options/redirection-rules/enable-redirection-to-converter.inc +3 -0
  65. lib/options/options/redirection-rules/enable-redirection-to-webp-realizer.inc +4 -1
  66. lib/options/options/redirection-rules/redirect-to-existing.inc +4 -0
  67. lib/options/options/redirection-rules/redirection-rules.inc +9 -3
  68. lib/options/page-messages.php +2 -2
  69. lib/options/page.php +5 -1
  70. lib/options/submit.php +111 -18
  71. vendor/composer/autoload_classmap.php +0 -83
  72. vendor/composer/autoload_static.php +0 -87
  73. vendor/composer/installed.json +6 -6
  74. vendor/rosell-dk/webp-convert/.php_cs.dist +0 -19
  75. vendor/rosell-dk/webp-convert/BACKERS.md +1 -0
  76. vendor/rosell-dk/webp-convert/README.md +12 -7
  77. vendor/rosell-dk/webp-convert/composer.json +1 -18
  78. vendor/rosell-dk/webp-convert/phpdox.xml +0 -9
  79. vendor/rosell-dk/webp-convert/phpstan.neon +0 -28
  80. vendor/rosell-dk/webp-convert/phpunit.xml.dist +0 -41
  81. vendor/rosell-dk/webp-convert/src/Convert/Converters/Cwebp.php +6 -0
  82. vendor/rosell-dk/webp-convert/src/Helpers/Sanitize.php +30 -0
  83. vendor/rosell-dk/webp-convert/src/Options/Options.php +11 -2
  84. webp-express.php +1 -1
  85. wod/webp-on-demand.php +11 -219
  86. wod/webp-realizer.php +12 -207
BACKERS.md CHANGED
@@ -13,6 +13,7 @@ To become a backer, yourself, [go to my page at patreon.com](https://www.patreon
13
  | Name | Date | Message (max 70 chars, plain text only) |
14
  | --------------------- | ---------- | ----------------------------------------------------------------------- |
15
  | Tammy Valgardson | 2018-12-27 | |
 
16
 
17
  <sub>I reserve the right to disallow inappropriate messages. No Trump hooting or bashing here, please. And don't be aggressive, obscene or anything unpleasant. But I don't have to point that out, do I?</sub>
18
 
13
  | Name | Date | Message (max 70 chars, plain text only) |
14
  | --------------------- | ---------- | ----------------------------------------------------------------------- |
15
  | Tammy Valgardson | 2018-12-27 | |
16
+ | Max Kreminsky | 2019-08-02 | |
17
 
18
  <sub>I reserve the right to disallow inappropriate messages. No Trump hooting or bashing here, please. And don't be aggressive, obscene or anything unpleasant. But I don't have to point that out, do I?</sub>
19
 
README.md CHANGED
@@ -12,12 +12,8 @@ But well, it is developed ([here on github](https://github.com/rosell-dk/webp-ex
12
  ## Description
13
  Almost 4 out of 5 mobile users use a browser that is able to display webp images. Yet, on most websites, they are served jpeg images, which are typically double the size of webp images for a given quality. What a waste of bandwidth! This plugin was created to help remedy that situation. With little effort, Wordpress admins can have their site serving autogenerated webp images to browsers that supports it, while still serving jpeg and png files to browsers that does not support webp.
14
 
15
- **Security notice**
16
- Security issues has been found and fixed. I urge you to upgrade to the latest release (at least 0.14.11, but go with 0.14.15, as there are important bug fixes)
17
-
18
-
19
  ### The image converter
20
- The plugin uses the [WebP Convert](https://github.com/rosell-dk/webp-convert) library to convert images to webp. *WebP Convert* is able to convert images using multiple methods. There are the "local" conversion methods: `cwebp`, `gd`, `imagick`. If none of these works on your host, there are the cloud alternatives: `ewww` (paid) or connecting to a Wordpress site where you got WebP Express installed and you enabled the "web service" functionality.
21
 
22
  ### The "Serving webp to browsers that supports it" part.
23
 
@@ -27,16 +23,21 @@ The plugin supports different ways of delivering webps to browsers that supports
27
  2. By altering the HTML, replacing image tags with *picture* tags. Missing webps are auto generated upon visit.
28
  3. By altering the HTML, replacing image URLs so all points to webp. The replacements only being made for browsers that supports webp. Again, missing webps are auto generated upon visit.
29
  4. In combination with *Cache Enabler*, the same as above can be achieved, but with page caching.
30
- 5. You can also deliver webp to *all* browsers and add the [webpjs](http://webpjs.appspot.com) javascript, which provides webp support for browsers that doesn't support webp natively. You currently have to add the javascript yourself, but I expect to add an option for it in the next release.
31
 
32
- The plugin builds on [WebPConvert](https://github.com/rosell-dk/webp-convert) and its "WebP On Demand" solution described [here](https://github.com/rosell-dk/webp-convert/blob/master/docs/webp-on-demand/webp-on-demand.md)
 
 
 
 
33
 
34
  ### Benefits
35
  - Much faster load time for images in browsers that supports webp. The converted images are typically *less than half the size* (for jpeg), while maintaining the same quality. Bear in mind that for most web sites, images are responsible for the largest part of the waiting time.
36
  - Better user experience (whether performance goes from terrible to bad, or from good to impressive, it is a benefit)
37
  - Better ranking in Google searches (performance is taken into account by Google)
38
  - Less bandwidth consumption - makes a huge difference in the parts of the world where the internet is slow and costly (you know, ~80% of the world population lives under these circumstances).
39
- - Currently ~73% of all traffic, and ~79% of mobile browsing traffic are done with browsers supporting webp. With Mozilla and Microsoft [finally on board](https://medium.com/@richard_90141/webp-image-support-an-8-year-saga-7aa2bedb8d02), these numbers are bound to increase. Check current numbers on [caniuse.com](https://caniuse.com/webp)).
 
40
 
41
  ### Recent news
42
  Feb 2019: Multisite is now supported (0.12.0)
@@ -68,20 +69,23 @@ Here you have all options available.
68
 
69
 
70
  #### Conversion methods
71
- WebP Express has a bunch of methods available for converting images: Executing cwebp binary, Gd extension, Imagick extension, ewww cloud converter and remote WebP express. Each requires *something*. In many cases, one of the conversion methods will be available. You can quickly identify which converters are working - there is a green icon next to them. Hovering conversion methods that are not working will show you what is wrong.
72
 
73
  In case no conversion methods are working out of the box, you have several options:
74
  - You can install this plugin on another website, which supports a local conversion method and connect to that using the "Remote WebP Express" conversion method
75
  - You can [purchase a key](https://ewww.io/plans/) for the ewww cloud converter. They do not charge credits for webp conversions, so all you ever have to pay is the one dollar start-up fee :)
76
  - You can set up [webp-convert-cloud-service](https://github.com/rosell-dk/webp-convert-cloud-service) on another server and connect to that. Its open source.
77
- - You can try to meet the server requirements of cwebp, gd, imagick or gmagick. Check out [this wiki page](https://github.com/rosell-dk/webp-convert/wiki/Meeting-the-requirements-of-the-converters) on how to do that
 
 
 
78
 
79
- ### The auto quality
80
- If your server has imagick og gmagick installed, the plugin will be able to detect the quality of a jpeg, and use the same quality for the converted webp. You can tell if it does, by looking at the quality option. If it allows you to select "auto" quality, it is available, otherwise it is not, and you will only have the option to set a specific quality for all conversions. *Auto* should be chosen, if available, as this ensures that each conversion are converted with an appropriate quality. Say you have a jpeg with low quality (say 30). The best result, is achieved by converting it to the same quality. Converting it with high quality (say 80), will not get you better quality, only a larger file.
81
 
82
- If you do not the "auto" option available:
83
- - Install imagick or gmagick, if you can
84
- - Use "Remote WebP Express" converter to connect to a site, that *does* have the auto option available
 
85
  - If you have cwebp converter available, you can configure it to aim for a certain reduction, rather than using the quality parameter. Set this to for example 50%, or even 45%.
86
 
87
  ### Verifying that it works.
@@ -146,72 +150,36 @@ Easy enough. Browsers looks at the *content type* header rather than the URL to
146
 
147
  I am btw considering making an option to have the plugin redirect to the webp instead of serving immediately. That would remove the apparent mismatch between file extension and content type header. However, the cost of doing that will be an extra request for each image, which means extra time and worse performance. I believe you'd be ill advised to use that option, so I guess I will not implement it. But perhaps you have good reasons to use it? If you do, please let me know!
148
 
149
- ### I am on NGINX / OpenResty
 
 
150
  The easy solution is simply to use the plugin in "CDN friendly" mode, do a bulk conversion (takes care of converting existing images) and activate the "Convert on upload" option (takes care of converting new images in the media library).
151
 
152
- This however does not cover images in external CSS and images being dynamically added with javascript. And it does not handle new theme images.
 
 
153
 
154
- To get this working, requires manually inserting redirection rules in the NGINX configuration file (nginx.conf or the configuration file for the site, found in `/etc/nginx/sites-available`).
 
155
 
156
- There are two different approaches to achieve the redirections. One based on *rewrite* and one based on *try_files*. As *try_files* performs best, I shall recommend that.
157
 
158
  For multisite on NGINX, read [here](https://github.com/rosell-dk/webp-express/issues/8)
159
 
160
- #### method 1 (try_files)
161
-
162
- Lets take this step by step.
163
- First step is to redirect images to the script. Second step is redirecting directly to existing webp images. Finally, as an optional third step, you can add extra rules for enabling the *Convert non-existing webp-files upon request* functionality
164
 
165
- **Step 1**: *Redirecting images to the script*
166
-
167
- The following will redirect all images under wp-content to the script, but only for webp-enabled browsers.
168
 
 
169
  Insert the following in the `server` context of your configuration file (usually found in `/etc/nginx/sites-available`). "The `server` context" refers to the part of the configuration that starts with "server {" and ends with the matching "}".
170
 
171
- ```nginx
172
- location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
173
- add_header Vary Accept;
174
- if ($http_accept !~* "webp"){
175
- break;
176
- }
177
- try_files
178
- /nonexisting-because-try-files-needs-fallback
179
- /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content
180
- ;
181
- }
182
- ```
183
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
184
-
185
- If you have moved wp-content to a non-standard place, you must change accordingly. Especially note that you must also change the wp-content parameter to the script. It expects a relative path to wp-content (from document root) and is needed so the script can find the configuration file.
186
-
187
- The `xsource` parameter helps the script finding the source file. It is only needed on some setups. You can try deleting it and see if it still works.
188
-
189
- Beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g", so the first line becomes:
190
- ```nginx
191
- location ~* ^/?wp-content/.*\.jpe?g$ {
192
- ```
193
-
194
- If you cannot get this to work then perhaps you need to add the following to your `mime.types` configuration file:
195
- `image/webp webp;`
196
-
197
- If you still cannot get it to work, you can instead try *method 2*
198
-
199
- Note: There is no longer any reason to add "&$args" to the line begining with "/wp-content". It was there to enable debugging a single image by appending "?debug" to the url. I however removed that functionality from `webp-on-demand.php`.
200
-
201
- **Step 2**: *Redirecting directly to existing webp images.*
202
-
203
- Once you got this working, lets improve performance by redirecting directly to existing webp images. This step isn't necessary, as the script also does that - but invoking the php script takes more resources that the direct redirect. Also, a direct redirect will produce *ETag* response header, which is increases caching performance.
204
-
205
- The rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that.
206
-
207
- The following rules works both when WebP Express are configured to store images in the same folder as the originals ("mingled") and when or in a separate folder. Nginx will first see if there is a corresponding webp in the separate folder and then if there is a corresponding webp in the same folder and finally fall back to calling the conversion script.
208
-
209
  ```nginx
210
  # WebP Express rules
211
  # --------------------
212
  location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
213
  add_header Vary Accept;
214
- # expires 365d;
215
  if ($http_accept !~* "webp"){
216
  break;
217
  }
@@ -221,53 +189,51 @@ location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
221
  /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content
222
  ;
223
  }
 
 
 
 
 
 
 
 
224
  # ------------------- (WebP Express rules ends here)
225
  ```
226
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
227
 
228
- If you have configured WebP Express to store images in separate folder, you do not need the "$uri.webp" line. But it doesn't hurt to have it. But the reverse is not true. If configured to store images in the same folder ("mingled"), you still need the line that looks for a webp in the separate folder. The reason for this is that the "mingled" only applies to the images in the upload folder - other images - such as theme images are always stored in a separate folder.
 
229
 
230
- Again, beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
231
 
232
- Credits: This second step builds on [Eugene Lazutkins solution](http://www.lazutkin.com/blog/2014/02/23/serve-files-with-nginx-conditionally/).
233
 
234
- **Step 3:**: *Caching*
235
- In most cases you can and should allow images to be cached for a long period. To do that, simply uncomment the "expires 365d;" line, by removing the "#" in front of it.
236
 
237
- **Step 4:**: *Routing requests for non-existing webps to the converter*
238
 
239
- Simply add the following rules below the ones you added in step 2:
240
 
241
- ```nginx
242
- location ~* ^/?wp-content/.*\.(png|jpe?g)\.webp$ {
243
- try_files
244
- $uri
245
- /wp-content/plugins/webp-express/wod/webp-realizer.php?wp-content=wp-content&$args
246
- ;
247
- }
248
- ```
249
 
250
- Again, beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
 
251
 
 
252
 
253
- #### method 2 (rewrite)
254
 
255
- **Step 1**: *Redirecting images to the script*
256
 
257
- Insert the following in the `server` context:
258
 
259
- ```nginx
260
- # WebP Express rules
261
- # --------------------
262
- if ($http_accept ~* "webp"){
263
- rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content break;
264
- }
265
- # ------------------- (WebP Express rules ends here)
266
- ```
267
 
268
- **Step 2**: *Redirecting directly to existing webp images.*
 
269
 
270
- Make sure that WebP Express is configured with "Destination" set to "Mingled". And then insert the following in the `server` context (replacing the rules you inserted in step 1)
271
  ```nginx
272
  # WebP Express rules
273
  # --------------------
@@ -295,17 +261,22 @@ if ($whattodo = A) {
295
  }
296
  # ------------------- (WebP Express rules ends here)
297
  ```
298
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
299
 
300
- Again, `wp-content` argument must point to the wp-content folder (relative to document root). In most installations, it is 'wp-content'.
 
 
 
 
 
 
 
 
301
 
302
- And again, the rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that:
303
- 1. Set *Destination folder* to *mingled*
304
- 2. Set *File extension* to *Append ".webp"*
305
 
306
- The "expires 365d;" lines set caching to one year. You can remove these lines if you wish.
307
 
308
- I have not set any expire on the webp-on-demand.php request. This is not needed, as the script sets this according to what you set up in WebP Express settings. Also, trying to do it would require a new location block matching webp-on-demand.php, but that would override the location block handling php files, and thus break the functionality.
309
 
310
  It is possible to put this stuff inside a `location` directive. However, having `if` directives inside `location` directives [is considered evil](https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/). But it seems that in our case, it works. If you wish to do that, use the following rules instead:
311
 
@@ -338,12 +309,22 @@ location ~* ^/wp-content/.*\.webp$ {
338
  }
339
  # ------------------- (WebP Express rules ends here)
340
  ```
341
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
 
 
 
 
 
 
 
 
 
 
342
 
343
  Discussion on this topic [here](https://wordpress.org/support/topic/nginx-rewrite-rules-4/)
344
  And here: https://github.com/rosell-dk/webp-express/issues/166
345
 
346
- Here are rules if you need to replace the file extension with ".webp" rather than appending ".webp" to it: https://www.keycdn.com/support/optimus/configuration-to-deliver-webp
347
 
348
  ### I am on a Windows server
349
  Good news! It should work now, thanks to a guy that calls himself lwxbr. At least on XAMPP 7.3.1, Windows 10. https://github.com/rosell-dk/webp-express/pull/213.
@@ -526,7 +507,7 @@ If you do not want to use serve varied images:
526
  - Open WebP Express options
527
  - Switch to *CDN friendly* mode.
528
  - Set *File extension* to "Set to .webp"
529
- - Enable *Alter HTML* and select *Replace image URLs*. It is not absolutely neccessary, as Cache Enabler also alters HTML - but there are several reasons to do it. Firstly, *Cache Enabler* doesn't get as many URLs replaced as we do. WebP Express for example also replaces background urls in inline styles. Secondly, *Cache enabler* has [problems in edge cases](https://regexr.com/46isf). Thirdly, WebP Express can be configured to alter HTML to point to corresponding webp images, *before they even exists* which can be used in conjunction with the the *Convert non-existing webp-files upon request* option. And this is smart, because then you don't have trouble with *Cache Enabler* caching HTML which references the original images due to that some images hasn't been converted yet.
530
  - If you enabled *Alter HTML*, also enable *Reference webps that hasn't been converted yet* and *Convert non-existing webp-files upon request*
531
  - If you did not enable *Alter HTML*, enable *Convert non-existing webp-files upon request to original image*
532
 
@@ -610,6 +591,15 @@ Here are my current plans ahead: 0.15 will probably be a file manager-like inter
610
 
611
  If you wish to affect priorities, it is certainly possible. You can try to argue your case in the forum or you can simply let the money do the talking. By donating as little as a cup of coffee on [ko-fi.com/rosell](https://ko-fi.com/rosell), you can leave a wish. I shall take these wishes into account when prioritizing between new features.
612
 
 
 
 
 
 
 
 
 
 
613
  ## Changes in 0.14.12 - 0.14.15
614
  - Fixed errors with "redirect to conversion script" on systems with symlinked folders
615
  - Fixed errors with "redirect to conversion script" on systems where the filename cannot be passed through an environment variable
@@ -624,7 +614,6 @@ It is urged that you upgrade all of you WebP Express installations!
624
  - Security fix: Added checks for file paths and directories.
625
  - Security fix: Nonces and capability checks for AJAX calls.
626
 
627
-
628
  ## Changes in 0.14.4
629
  - Now bundles with multiple cwebp binaries for linux for systems where 1.0.2 fails.
630
 
12
  ## Description
13
  Almost 4 out of 5 mobile users use a browser that is able to display webp images. Yet, on most websites, they are served jpeg images, which are typically double the size of webp images for a given quality. What a waste of bandwidth! This plugin was created to help remedy that situation. With little effort, Wordpress admins can have their site serving autogenerated webp images to browsers that supports it, while still serving jpeg and png files to browsers that does not support webp.
14
 
 
 
 
 
15
  ### The image converter
16
+ The plugin uses the [WebP Convert](https://github.com/rosell-dk/webp-convert) library to convert images to webp. *WebP Convert* is able to convert images using multiple methods. There are the "local" conversion methods: `imagick`, `cwebp`, `vips`, `gd`. If none of these works on your host, there are the cloud alternatives: `ewww` (paid) or connecting to a Wordpress site where you got WebP Express installed and you enabled the "web service" functionality.
17
 
18
  ### The "Serving webp to browsers that supports it" part.
19
 
23
  2. By altering the HTML, replacing image tags with *picture* tags. Missing webps are auto generated upon visit.
24
  3. By altering the HTML, replacing image URLs so all points to webp. The replacements only being made for browsers that supports webp. Again, missing webps are auto generated upon visit.
25
  4. In combination with *Cache Enabler*, the same as above can be achieved, but with page caching.
26
+ 5. You can also deliver webp to *all* browsers and add the [webpjs](http://webpjs.appspot.com) javascript, which provides webp support for browsers that doesn't support webp natively. However, beware that the javascript doesn't support srcset attributes, which is why I haven't added that method to the plugin (yet).
27
 
28
+ The plugin implements the "WebP On Demand" solution described [here](https://github.com/rosell-dk/webp-convert/blob/master/docs/v2.0/webp-on-demand/webp-on-demand.md) and builds on a bunch of open source libraries (all maintained by me):
29
+ - [WebPConvert](https://github.com/rosell-dk/webp-convert): For converting images to webp
30
+ - [WebP Convert Cloud Service](https://github.com/rosell-dk/webp-convert-cloud-service): For the Web Service functionality
31
+ - [DOM Util for WebP](https://github.com/rosell-dk/dom-util-for-webp): For the Alter HTML functionality
32
+ - [Image MimeType Guesser](https://github.com/rosell-dk/image-mime-type-guesser): For detecting mime types of images.
33
 
34
  ### Benefits
35
  - Much faster load time for images in browsers that supports webp. The converted images are typically *less than half the size* (for jpeg), while maintaining the same quality. Bear in mind that for most web sites, images are responsible for the largest part of the waiting time.
36
  - Better user experience (whether performance goes from terrible to bad, or from good to impressive, it is a benefit)
37
  - Better ranking in Google searches (performance is taken into account by Google)
38
  - Less bandwidth consumption - makes a huge difference in the parts of the world where the internet is slow and costly (you know, ~80% of the world population lives under these circumstances).
39
+ - Currently ~83% of all traffic, and ~80% of mobile browsing traffic are done with browsers supporting webp. With Mozilla and Microsoft [finally on board](https://medium.com/@richard_90141/webp-image-support-an-8-year-saga-7aa2bedb8d02), these numbers are bound to increase. Check current numbers on [caniuse.com](https://caniuse.com/webp)).
40
+ - It's great for the environment too! Reducing network traffic reduces electricity consumption which reduces CO2 emissions.
41
 
42
  ### Recent news
43
  Feb 2019: Multisite is now supported (0.12.0)
69
 
70
 
71
  #### Conversion methods
72
+ WebP Express has a bunch of methods available for converting images: Executing cwebp binary, Gd extension, Imagick extension, Vips extension, ewww cloud converter and remote WebP express etc. Each requires *something*. In many cases, one of the conversion methods will be available. You can quickly identify which converters are working - there is a green icon next to them. Hovering conversion methods that are not working will show you what is wrong.
73
 
74
  In case no conversion methods are working out of the box, you have several options:
75
  - You can install this plugin on another website, which supports a local conversion method and connect to that using the "Remote WebP Express" conversion method
76
  - You can [purchase a key](https://ewww.io/plans/) for the ewww cloud converter. They do not charge credits for webp conversions, so all you ever have to pay is the one dollar start-up fee :)
77
  - You can set up [webp-convert-cloud-service](https://github.com/rosell-dk/webp-convert-cloud-service) on another server and connect to that. Its open source.
78
+ - You can try to meet the server requirements of cwebp, Gd, Imagick, Gmagick or Vips. Check out [this wiki page](https://github.com/rosell-dk/webp-convert/wiki/Meeting-the-requirements-of-the-converters) on how to do that
79
+
80
+ ### Quality detection of jpegs
81
+ If your server has Imagick extension or is able to execute imagemagick binary, the plugin will be able to detect the quality of a jpeg, and use that quality for the converted webp. You can tell if the quality detection is available by hovering the help icon in Conversion > Jpeg options > Quality for lossy. The last line in that help text tells you.
82
 
83
+ This auto quality has benefits over fixed quality as it ensures that each conversion are converted with an appropriate quality. Encoding low quality jpegs to high quality webps does not magically increase the visual quality so that your webp looks better than the original. But it does result in a much larger filesize than if the jpeg where converting to a webp with the same quality setting as the original.
 
84
 
85
+ If you do not have quality detection working, you can try one of the following:
86
+ - Install Imagick on the server (for this purpose, it is not required that it is compiled with WebP support)
87
+ - Install imagemagick on the server and grant permission for PHP to use the "exec" function.
88
+ - Use "Remote WebP Express" converter to connect to a site, that *does* have quality detection working
89
  - If you have cwebp converter available, you can configure it to aim for a certain reduction, rather than using the quality parameter. Set this to for example 50%, or even 45%.
90
 
91
  ### Verifying that it works.
150
 
151
  I am btw considering making an option to have the plugin redirect to the webp instead of serving immediately. That would remove the apparent mismatch between file extension and content type header. However, the cost of doing that will be an extra request for each image, which means extra time and worse performance. I believe you'd be ill advised to use that option, so I guess I will not implement it. But perhaps you have good reasons to use it? If you do, please let me know!
152
 
153
+ ### I am on NGINX or OpenResty
154
+
155
+ #### The simple way (no redirecting rules)
156
  The easy solution is simply to use the plugin in "CDN friendly" mode, do a bulk conversion (takes care of converting existing images) and activate the "Convert on upload" option (takes care of converting new images in the media library).
157
 
158
+ *PRO*: Very easy to set up.
159
+ *CON*: Images in external CSS and images being dynamically added with javascript will not be served as webp.
160
+ *CON*: New new theme images will not be converted until you run a new Bulk conversion
161
 
162
+ #### The advanced way (creating NGINX redirecting rules)
163
+ Creating NGINX rules requires manually inserting redirection rules in the NGINX configuration file (nginx.conf or the configuration file for the site, found in `/etc/nginx/sites-available`). If you do not have access to do that, you will have to settle with the "simple way" described above.
164
 
165
+ There are two different approaches to achieve the redirections. The one that I recommend is based on a *try_files* directive. If that doesn't work for you, you can try the alternative rules that are based on the *rewrite* directive. The rules are described in the next couple of sections.
166
 
167
  For multisite on NGINX, read [here](https://github.com/rosell-dk/webp-express/issues/8)
168
 
169
+ #### Recommended rules (using "try_files")
 
 
 
170
 
171
+ __Preparational step:__
172
+ The rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that by setting *General > File extension* to *Append ".webp"*
 
173
 
174
+ __The rules:__
175
  Insert the following in the `server` context of your configuration file (usually found in `/etc/nginx/sites-available`). "The `server` context" refers to the part of the configuration that starts with "server {" and ends with the matching "}".
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  ```nginx
178
  # WebP Express rules
179
  # --------------------
180
  location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
181
  add_header Vary Accept;
182
+ expires 365d;
183
  if ($http_accept !~* "webp"){
184
  break;
185
  }
189
  /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content
190
  ;
191
  }
192
+
193
+ # Route requests for non-existing webps to the converter
194
+ location ~* ^/?wp-content/.*\.(png|jpe?g)\.webp$ {
195
+ try_files
196
+ $uri
197
+ /wp-content/plugins/webp-express/wod/webp-realizer.php?wp-content=wp-content
198
+ ;
199
+ }
200
  # ------------------- (WebP Express rules ends here)
201
  ```
 
202
 
203
+ __BEWARE:__
204
+ - Beware that when copy/pasting you might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded (in the last line in the try_files block)
205
 
206
+ - Beware that the rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you __must__ configure *WebP Express* to store the converted files like that.
207
 
208
+ - Beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
209
 
210
+ - Beware that if you have moved wp-content to a non-standard place, you must change accordingly. Note that you must then also change the "wp-content" parameter to the script. It expects a relative path to wp-content (from document root) and is needed so the script can find the configuration file.
 
211
 
212
+ - I have put in an expires statement for caching. You might want to modify or disable that.
213
 
214
+ - The rules contains all redirections (as if you enabled all three redirection options in settings). If you do not wish to redirect to converter, remove the last line in the try_files block. If you do not wish to create webp files upon request, remove the last location block.
215
 
216
+ - If you have configured WebP Express to store images in separate folder, you do not need the "$uri.webp" line in the first "try_files" block. But it doesn't hurt to have it. And beware that the reverse is not true. If configured to store images in the same folder ("mingled"), you still need the line that looks for a webp in the separate folder. The reason for this is that the "mingled" only applies to the images in the upload folder - other images - such as theme images are always stored in a separate folder.
 
 
 
 
 
 
 
217
 
218
+ If you cannot get this to work then perhaps you need to add the following to your *mime.types* configuration file:
219
+ `image/webp webp;`
220
 
221
+ If you still cannot get it to work, you can instead try the alternative rules below.
222
 
223
+ Credits: These rules are builds upon [Eugene Lazutkins solution](http://www.lazutkin.com/blog/2014/02/23/serve-files-with-nginx-conditionally/).
224
 
225
+ #### Alternative rules (using "rewrite")
226
 
227
+ In case the recommended rules does not work for you, you can try these alternative rules.
228
 
229
+ The reason I recommend the *try_files* approach above over these alternative rules is that it is a bit simpler and it is supposed to perform marginally better. These alternative rules are in no way inferior to the other. Choose whatever works!
230
+
231
+ __Preparational step:__
232
+ The rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that by setting *General > File extension* to *Append ".webp"*. Also make sure that WebP Express is configured with "Destination" set to "Mingled".
 
 
 
 
233
 
234
+ __The rules:__
235
+ Insert the following in the `server` context of your configuration file (usually found in `/etc/nginx/sites-available`). "The `server` context" refers to the part of the configuration that starts with "server {" and ends with the matching "}".
236
 
 
237
  ```nginx
238
  # WebP Express rules
239
  # --------------------
261
  }
262
  # ------------------- (WebP Express rules ends here)
263
  ```
 
264
 
265
+ __BEWARE:__
266
+
267
+ - Beware that when copy/pasting you might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded (in the last line in the try_files block)
268
+
269
+ - Beware that the rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you __must__ configure *WebP Express* to store the converted files like that.
270
+
271
+ - Beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
272
+
273
+ - Beware that if you have moved wp-content to a non-standard place, you must change accordingly. Note that you must then also change the "wp-content" parameter to the script. It expects a relative path to wp-content (from document root) and is needed so the script can find the configuration file.
274
 
275
+ - I have put in an expires statement for caching. You might want to modify or disable that.
 
 
276
 
277
+ - I have not set any expire on the webp-on-demand.php request. This is not needed, as the script sets this according to what you set up in WebP Express settings. Also, trying to do it would require a new location block matching webp-on-demand.php, but that would override the location block handling php files, and thus break the functionality.
278
 
279
+ - There is no longer any reason to add "&$args" to the line begining with "/wp-content". It was there to enable debugging a single image by appending "?debug" to the url. I however removed that functionality from `webp-on-demand.php`.
280
 
281
  It is possible to put this stuff inside a `location` directive. However, having `if` directives inside `location` directives [is considered evil](https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/). But it seems that in our case, it works. If you wish to do that, use the following rules instead:
282
 
309
  }
310
  # ------------------- (WebP Express rules ends here)
311
  ```
312
+
313
+ PS: In case you only want to redirect images to the script (and not to existing), the rules becomes much simpler:
314
+
315
+ ```nginx
316
+ # WebP Express rules
317
+ # --------------------
318
+ if ($http_accept ~* "webp"){
319
+ rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content break;
320
+ }
321
+ # ------------------- (WebP Express rules ends here)
322
+ ```
323
 
324
  Discussion on this topic [here](https://wordpress.org/support/topic/nginx-rewrite-rules-4/)
325
  And here: https://github.com/rosell-dk/webp-express/issues/166
326
 
327
+ Here are rules if you need to *replace* the file extension with ".webp" rather than appending ".webp" to it: https://www.keycdn.com/support/optimus/configuration-to-deliver-webp
328
 
329
  ### I am on a Windows server
330
  Good news! It should work now, thanks to a guy that calls himself lwxbr. At least on XAMPP 7.3.1, Windows 10. https://github.com/rosell-dk/webp-express/pull/213.
507
  - Open WebP Express options
508
  - Switch to *CDN friendly* mode.
509
  - Set *File extension* to "Set to .webp"
510
+ - Enable *Alter HTML* and select *Replace image URLs*. It is not absolutely necessary, as Cache Enabler also alters HTML - but there are several reasons to do it. Firstly, *Cache Enabler* doesn't get as many URLs replaced as we do. WebP Express for example also replaces background urls in inline styles. Secondly, *Cache enabler* has [problems in edge cases](https://regexr.com/46isf). Thirdly, WebP Express can be configured to alter HTML to point to corresponding webp images, *before they even exists* which can be used in conjunction with the the *Convert non-existing webp-files upon request* option. And this is smart, because then you don't have trouble with *Cache Enabler* caching HTML which references the original images due to that some images hasn't been converted yet.
511
  - If you enabled *Alter HTML*, also enable *Reference webps that hasn't been converted yet* and *Convert non-existing webp-files upon request*
512
  - If you did not enable *Alter HTML*, enable *Convert non-existing webp-files upon request to original image*
513
 
591
 
592
  If you wish to affect priorities, it is certainly possible. You can try to argue your case in the forum or you can simply let the money do the talking. By donating as little as a cup of coffee on [ko-fi.com/rosell](https://ko-fi.com/rosell), you can leave a wish. I shall take these wishes into account when prioritizing between new features.
593
 
594
+ ## Changes in 0.15.0
595
+ - Provided test-buttons for checking if the redirects works.
596
+ - You can now choose which folders WebP Express is active in. Ie "Uploads and Themes".
597
+ - You can now choose an alternative file structure for the webps which does not rely on DOCUMENT_ROOT being available.
598
+ - WebP Express can now handle when wp-content is symlinked.
599
+ - The .htaccess rules are now divided across folders. Some rules are needed where the source files are located, some where the webp files are located.
600
+ - Added option to convert only PNG files
601
+ - And a couple of bugfixes.
602
+
603
  ## Changes in 0.14.12 - 0.14.15
604
  - Fixed errors with "redirect to conversion script" on systems with symlinked folders
605
  - Fixed errors with "redirect to conversion script" on systems where the filename cannot be passed through an environment variable
614
  - Security fix: Added checks for file paths and directories.
615
  - Security fix: Nonces and capability checks for AJAX calls.
616
 
 
617
  ## Changes in 0.14.4
618
  - Now bundles with multiple cwebp binaries for linux for systems where 1.0.2 fails.
619
 
README.txt CHANGED
@@ -4,7 +4,7 @@ Donate link: https://ko-fi.com/rosell
4
  Tags: webp, images, performance
5
  Requires at least: 4.0
6
  Tested up to: 5.2
7
- Stable tag: 0.14.22
8
  Requires PHP: 5.6
9
  License: GPLv3
10
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -15,12 +15,8 @@ Serve autogenerated WebP images instead of jpeg/png to browsers that supports We
15
 
16
  Almost 4 out of 5 mobile users use a browser that is able to display webp images. Yet, on most websites, they are served jpeg images, which are typically double the size of webp images for a given quality. What a waste of bandwidth! This plugin was created to help remedy that situation. With little effort, Wordpress admins can have their site serving autogenerated webp images to browsers that supports it, while still serving jpeg and png files to browsers that does not support webp.
17
 
18
- !! **SECURITY NOTICE** !!
19
- Security issues has recently been found and fixed. I urge you to upgrade to the latest release (at least 0.14.11, but go with 0.14.22, as there are important bug fixes)
20
-
21
-
22
  ### The image converter
23
- The plugin uses the [WebP Convert](https://github.com/rosell-dk/webp-convert) library to convert images to webp. *WebP Convert* is able to convert images using multiple methods. There are the "local" conversion methods: `cwebp`, `gd`, `imagick`. If none of these works on your host, there are the cloud alternatives: `ewww` (paid) or connecting to a Wordpress site where you got WebP Express installed and you enabled the "web service" functionality.
24
 
25
  ### The "Serving webp to browsers that supports it" part.
26
 
@@ -30,9 +26,13 @@ The plugin supports different ways of delivering webps to browsers that supports
30
  2. By altering the HTML, replacing image tags with *picture* tags. Missing webps are auto generated upon visit.
31
  3. By altering the HTML, replacing image URLs so all points to webp. The replacements only being made for browsers that supports webp. Again, missing webps are auto generated upon visit.
32
  4. In combination with *Cache Enabler*, the same as above can be achieved, but with page caching.
33
- 5. You can also deliver webp to *all* browsers and add the [webpjs](http://webpjs.appspot.com) javascript, which provides webp support for browsers that doesn't support webp natively. You currently have to add the javascript yourself, but I expect to add an option for it in the next release.
34
 
35
- The plugin builds on [WebPConvert](https://github.com/rosell-dk/webp-convert) and its "WebP On Demand" solution described [here](https://github.com/rosell-dk/webp-convert/blob/master/docs/webp-on-demand/webp-on-demand.md)
 
 
 
 
36
 
37
  ### Benefits
38
  - Much faster load time for images in browsers that supports webp. The converted images are typically *less than half the size* (for jpeg), while maintaining the same quality. Bear in mind that for most web sites, images are responsible for the largest part of the waiting time.
@@ -153,67 +153,33 @@ Easy enough. Browsers looks at the *content type* header rather than the URL to
153
 
154
  I am btw considering making an option to have the plugin redirect to the webp instead of serving immediately. That would remove the apparent mismatch between file extension and content type header. However, the cost of doing that will be an extra request for each image, which means extra time and worse performance. I believe you'd be ill advised to use that option, so I guess I will not implement it. But perhaps you have good reasons to use it? If you do, please let me know!
155
 
156
- = I am on NGINX / OpenResty =
157
- The easy solution is simply to use the plugin in "CDN friendly" mode, do a bulk conversion (takes care of converting existing images) and activate the "Convert on upload" option (takes care of converting new images in the media library).
158
 
159
- This however does not cover images in external CSS and images being dynamically added with javascript. And it does not handle new theme images.
 
160
 
161
- There are two different approaches to achieve the redirections. One based on *rewrite* and one based on *try_files*. As *try_files* performs best, I shall recommend that.
 
 
162
 
163
- For multisite on NGINX, read [here](https://github.com/rosell-dk/webp-express/issues/8)
 
164
 
165
- **method 1 (try_files)**
166
 
167
- Lets take this step by step.
168
- First step is to redirect images to the script. Second step is redirecting directly to existing webp images. Finally, as an optional third step, you can add extra rules for enabling the *Convert non-existing webp-files upon request* functionality
169
 
170
- **Step 1**: *Redirecting images to the script*
171
 
172
- The following will redirect all images under wp-content to the script, but only for webp-enabled browsers.
 
173
 
 
174
  Insert the following in the `server` context of your configuration file (usually found in `/etc/nginx/sites-available`). "The `server` context" refers to the part of the configuration that starts with "server {" and ends with the matching "}".
175
 
176
  `
177
- location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
178
- add_header Vary Accept;
179
- if ($http_accept !~* "webp"){
180
- break;
181
- }
182
- try_files
183
- /nonexisting-because-try-files-needs-fallback
184
- /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content
185
- ;
186
- }
187
- `
188
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
189
-
190
- If you have moved wp-content to a non-standard place, you must change accordingly. Especially note that you must also change the wp-content parameter to the script. It expects a relative path to wp-content (from document root) and is needed so the script can find the configuration file.
191
-
192
- The `xsource` parameter helps the script finding the source file. It is only needed on some setups. You can try deleting it and see if it still works.
193
-
194
- Beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g", so the first line becomes:
195
- `
196
- location ~* ^/?wp-content/.*\.jpe?g$ {
197
- `
198
-
199
- If you cannot get this to work then perhaps you need to add the following to your `mime.types` configuration file:
200
- `
201
- image/webp webp;
202
- `
203
-
204
- If you still cannot get it to work, you can instead try *method 2*
205
-
206
- Note: There is no longer any reason to add "&$args" to the line begining with "/wp-content". It was there to enable debugging a single image by appending "?debug" to the url. I however removed that functionality from `webp-on-demand.php`.
207
-
208
- **Step 2**: *Redirecting directly to existing webp images.*
209
-
210
- Once you got this working, lets improve performance by redirecting directly to existing webp images. This step isn't necessary, as the script also does that - but invoking the php script takes more resources that the direct redirect. Also, a direct redirect will produce *ETag* response header, which is increases caching performance.
211
-
212
- The rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that.
213
-
214
- The following rules works both when WebP Express are configured to store images in the same folder as the originals ("mingled") and when or in a separate folder. Nginx will first see if there is a corresponding webp in the separate folder and then if there is a corresponding webp in the same folder and finally fall back to calling the conversion script.
215
-
216
- `
217
  location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
218
  add_header Vary Accept;
219
  expires 365d;
@@ -226,51 +192,54 @@ location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
226
  /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content
227
  ;
228
  }
229
- `
230
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
231
-
232
- If you have configured WebP Express to store images in separate folder, you do not need the "$uri.webp" line. But it doesn't hurt to have it. But the reverse is not true. If configured to store images in the same folder ("mingled"), you still need the line that looks for a webp in the separate folder. The reason for this is that the "mingled" only applies to the images in the upload folder - other images - such as theme images are always stored in a separate folder.
233
-
234
- Again, beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
235
-
236
- Credits: This second step builds on [Eugene Lazutkins solution](http://www.lazutkin.com/blog/2014/02/23/serve-files-with-nginx-conditionally/).
237
-
238
- **Step 3:**: *Caching*
239
- In most cases you can and should allow images to be cached for a long period. If you do not want to do that, simply remove the "expires 365d;" line.
240
 
241
- **Step 4:**: *Routing requests for non-existing webps to the converter*
242
-
243
- Simply add the following rules below the ones you added in step 2:
244
-
245
- `
246
  location ~* ^/?wp-content/.*\.(png|jpe?g)\.webp$ {
247
  try_files
248
  $uri
249
  /wp-content/plugins/webp-express/wod/webp-realizer.php?wp-content=wp-content
250
  ;
251
  }
 
252
  `
253
 
254
- Again, beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
 
255
 
 
256
 
257
- **method 2 (rewrite)**
258
 
259
- **Step 1**: *Redirecting images to the script*
260
 
261
- Insert the following in the `server` context:
262
 
263
- `
264
- if ($http_accept ~* "webp"){
265
- rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content break;
266
- }
267
- }
268
- `
269
 
270
- **Step 2**: *Redirecting directly to existing webp images.*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
- Make sure that WebP Express is configured with "Destination" set to "Mingled". And then insert the following in the `server` context (replacing the rules you inserted in step 1)
273
  `
 
 
274
  location ~* ^/wp-content/.*\.(png|jpe?g)$ {
275
  add_header Vary Accept;
276
  expires 365d;
@@ -293,22 +262,30 @@ if ($whattodo = AB) {
293
  if ($whattodo = A) {
294
  rewrite ^/wp-content/.*\.(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content break;
295
  }
296
- `
297
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
 
 
 
 
 
 
 
 
298
 
299
- Again, `wp-content` argument must point to the wp-content folder (relative to document root). In most installations, it is 'wp-content'.
300
 
301
- And again, the rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that:
302
- 1. Set *Destination folder* to *mingled*
303
- 2. Set *File extension* to *Append ".webp"*
304
 
305
- The "expires 365d;" lines set caching to one year. You can remove these lines if you wish.
306
 
307
- I have not set any expire on the webp-on-demand.php request. This is not needed, as the script sets this according to what you set up in WebP Express settings. Also, trying to do it would require a new location block matching webp-on-demand.php, but that would override the location block handling php files, and thus break the functionality.
308
 
309
  It is possible to put this stuff inside a `location` directive. However, having `if` directives inside `location` directives [is considered evil](https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/). But it seems that in our case, it works. If you wish to do that, use the following rules instead:
310
 
311
  `
 
 
312
  location ~* ^/wp-content/.*\.(png|jpe?g)$ {
313
  add_header Vary Accept;
314
  expires 365d;
@@ -333,13 +310,24 @@ location ~* ^/wp-content/.*\.webp$ {
333
  add_header Vary Accept;
334
  }
335
  }
 
 
 
 
 
 
 
 
 
 
 
 
336
  `
337
- *Beware when copy/pasting: You might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded*
338
 
339
  Discussion on this topic [here](https://wordpress.org/support/topic/nginx-rewrite-rules-4/)
340
  And here: https://github.com/rosell-dk/webp-express/issues/166
341
 
342
- Here are rules if you need to replace the file extension with ".webp" rather than appending ".webp" to it: https://www.keycdn.com/support/optimus/configuration-to-deliver-webp
343
 
344
  = I am on a Windows server =
345
  Good news! It should work now, thanks to a guy that calls himself lwxbr. At least on XAMPP 7.3.1, Windows 10. https://github.com/rosell-dk/webp-express/pull/213.
@@ -520,7 +508,7 @@ If you do not want to use serve varied images:
520
  - Open WebP Express options
521
  - Switch to *CDN friendly* mode.
522
  - Set *File extension* to "Set to .webp"
523
- - Enable *Alter HTML* and select *Replace image URLs*. It is not absolutely neccessary, as Cache Enabler also alters HTML - but there are several reasons to do it. Firstly, *Cache Enabler* doesn't get as many URLs replaced as we do. WebP Express for example also replaces background urls in inline styles. Secondly, *Cache enabler* has [problems in edge cases](https://regexr.com/46isf). Thirdly, WebP Express can be configured to alter HTML to point to corresponding webp images, *before they even exists* which can be used in conjunction with the the *Convert non-existing webp-files upon request* option. And this is smart, because then you don't have trouble with *Cache Enabler* caching HTML which references the original images due to that some images hasn't been converted yet.
524
  - If you enabled *Alter HTML*, also enable *Reference webps that hasn't been converted yet* and *Convert non-existing webp-files upon request*
525
  - If you did not enable *Alter HTML*, enable *Convert non-existing webp-files upon request to original image*
526
 
@@ -612,6 +600,19 @@ Easy enough! - [Go here!](https://ko-fi.com/rosell). Or [here](https://buymeacof
612
 
613
  == Changelog ==
614
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
  = 0.14.22 =
616
  *(released: 4 aug 2019)*
617
 
@@ -636,7 +637,14 @@ Easy enough! - [Go here!](https://ko-fi.com/rosell). Or [here](https://buymeacof
636
  * Fixed bug: Ewww api-key was forgot upon saving options
637
 
638
  = 0.14.19 =
639
- *(released: 28 jun 2019)*
 
 
 
 
 
 
 
640
 
641
  * Removed a line that might course Sanity Check to fail ("path not within document root")
642
 
@@ -937,6 +945,9 @@ For older releases, check out changelog.txt
937
 
938
  == Upgrade Notice ==
939
 
 
 
 
940
  = 0.14.22 =
941
  * A bundle of bug fixes and a security fix for Windows
942
 
4
  Tags: webp, images, performance
5
  Requires at least: 4.0
6
  Tested up to: 5.2
7
+ Stable tag: 0.15.0
8
  Requires PHP: 5.6
9
  License: GPLv3
10
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
15
 
16
  Almost 4 out of 5 mobile users use a browser that is able to display webp images. Yet, on most websites, they are served jpeg images, which are typically double the size of webp images for a given quality. What a waste of bandwidth! This plugin was created to help remedy that situation. With little effort, Wordpress admins can have their site serving autogenerated webp images to browsers that supports it, while still serving jpeg and png files to browsers that does not support webp.
17
 
 
 
 
 
18
  ### The image converter
19
+ The plugin uses the [WebP Convert](https://github.com/rosell-dk/webp-convert) library to convert images to webp. *WebP Convert* is able to convert images using multiple methods. There are the "local" conversion methods: `imagick`, `cwebp`, `vips`, `gd`. If none of these works on your host, there are the cloud alternatives: `ewww` (paid) or connecting to a Wordpress site where you got WebP Express installed and you enabled the "web service" functionality.
20
 
21
  ### The "Serving webp to browsers that supports it" part.
22
 
26
  2. By altering the HTML, replacing image tags with *picture* tags. Missing webps are auto generated upon visit.
27
  3. By altering the HTML, replacing image URLs so all points to webp. The replacements only being made for browsers that supports webp. Again, missing webps are auto generated upon visit.
28
  4. In combination with *Cache Enabler*, the same as above can be achieved, but with page caching.
29
+ 5. You can also deliver webp to *all* browsers and add the [webpjs](http://webpjs.appspot.com) javascript, which provides webp support for browsers that doesn't support webp natively. However, beware that the javascript doesn't support srcset attributes, which is why I haven't added that method to the plugin (yet).
30
 
31
+ The plugin implements the "WebP On Demand" solution described [here](https://github.com/rosell-dk/webp-convert/blob/master/docs/v2.0/webp-on-demand/webp-on-demand.md) and builds on a bunch of open source libraries (all maintained by me):
32
+ - [WebPConvert](https://github.com/rosell-dk/webp-convert): For converting images to webp
33
+ - [WebP Convert Cloud Service](https://github.com/rosell-dk/webp-convert-cloud-service): For the Web Service functionality
34
+ - [DOM Util for WebP](https://github.com/rosell-dk/dom-util-for-webp): For the Alter HTML functionality
35
+ - [Image MimeType Guesser](https://github.com/rosell-dk/image-mime-type-guesser): For detecting mime types of images.
36
 
37
  ### Benefits
38
  - Much faster load time for images in browsers that supports webp. The converted images are typically *less than half the size* (for jpeg), while maintaining the same quality. Bear in mind that for most web sites, images are responsible for the largest part of the waiting time.
153
 
154
  I am btw considering making an option to have the plugin redirect to the webp instead of serving immediately. That would remove the apparent mismatch between file extension and content type header. However, the cost of doing that will be an extra request for each image, which means extra time and worse performance. I believe you'd be ill advised to use that option, so I guess I will not implement it. But perhaps you have good reasons to use it? If you do, please let me know!
155
 
156
+ = I am on NGINX or OpenResty =
 
157
 
158
+ **The simple way (no redirecting rules)**
159
+ The easy solution is simply to use the plugin in "CDN friendly" mode, do a bulk conversion (takes care of converting existing images) and activate the "Convert on upload" option (takes care of converting new images in the media library).
160
 
161
+ *PRO*: Very easy to set up.
162
+ *CON*: Images in external CSS and images being dynamically added with javascript will not be served as webp.
163
+ *CON*: New new theme images will not be converted until you run a new Bulk conversion
164
 
165
+ **The advanced way (creating NGINX redirecting rules)**
166
+ Creating NGINX rules requires manually inserting redirection rules in the NGINX configuration file (nginx.conf or the configuration file for the site, found in `/etc/nginx/sites-available`). If you do not have access to do that, you will have to settle with the "simple way" described above.
167
 
168
+ There are two different approaches to achieve the redirections. The one that I recommend is based on a *try_files* directive. If that doesn't work for you, you can try the alternative rules that are based on the *rewrite* directive. The rules are described in the next couple of sections.
169
 
170
+ For multisite on NGINX, read [here](https://github.com/rosell-dk/webp-express/issues/8)
 
171
 
172
+ **Recommended rules (using "try_files")**
173
 
174
+ __Preparational step:__
175
+ The rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that by setting *General > File extension* to *Append ".webp"*
176
 
177
+ __The rules:__
178
  Insert the following in the `server` context of your configuration file (usually found in `/etc/nginx/sites-available`). "The `server` context" refers to the part of the configuration that starts with "server {" and ends with the matching "}".
179
 
180
  `
181
+ &#35; WebP Express rules
182
+ &#35; --------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  location ~* ^/?wp-content/.*\.(png|jpe?g)$ {
184
  add_header Vary Accept;
185
  expires 365d;
192
  /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content
193
  ;
194
  }
 
 
 
 
 
 
 
 
 
 
 
195
 
196
+ &#35; Route requests for non-existing webps to the converter
 
 
 
 
197
  location ~* ^/?wp-content/.*\.(png|jpe?g)\.webp$ {
198
  try_files
199
  $uri
200
  /wp-content/plugins/webp-express/wod/webp-realizer.php?wp-content=wp-content
201
  ;
202
  }
203
+ &#35; ------------------- (WebP Express rules ends here)
204
  `
205
 
206
+ __BEWARE:__
207
+ - Beware that when copy/pasting you might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded (in the last line in the try_files block)
208
 
209
+ - Beware that the rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you __must__ configure *WebP Express* to store the converted files like that.
210
 
211
+ - Beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
212
 
213
+ - Beware that if you have moved wp-content to a non-standard place, you must change accordingly. Note that you must then also change the "wp-content" parameter to the script. It expects a relative path to wp-content (from document root) and is needed so the script can find the configuration file.
214
 
215
+ - I have put in an expires statement for caching. You might want to modify or disable that.
216
 
217
+ - The rules contains all redirections (as if you enabled all three redirection options in settings). If you do not wish to redirect to converter, remove the last line in the try_files block. If you do not wish to create webp files upon request, remove the last location block.
218
+
219
+ - If you have configured WebP Express to store images in separate folder, you do not need the "$uri.webp" line in the first "try_files" block. But it doesn't hurt to have it. And beware that the reverse is not true. If configured to store images in the same folder ("mingled"), you still need the line that looks for a webp in the separate folder. The reason for this is that the "mingled" only applies to the images in the upload folder - other images - such as theme images are always stored in a separate folder.
 
 
 
220
 
221
+ If you cannot get this to work then perhaps you need to add the following to your *mime.types* configuration file:
222
+ `image/webp webp;`
223
+
224
+ If you still cannot get it to work, you can instead try the alternative rules below.
225
+
226
+ Credits: These rules are builds upon [Eugene Lazutkins solution](http://www.lazutkin.com/blog/2014/02/23/serve-files-with-nginx-conditionally/).
227
+
228
+ **Alternative rules (using "rewrite")**
229
+
230
+ In case the recommended rules does not work for you, you can try these alternative rules.
231
+
232
+ The reason I recommend the *try_files* approach above over these alternative rules is that it is a bit simpler and it is supposed to perform marginally better. These alternative rules are in no way inferior to the other. Choose whatever works!
233
+
234
+ __Preparational step:__
235
+ The rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you must configure *WebP Express* to store the converted files like that by setting *General > File extension* to *Append ".webp"*. Also make sure that WebP Express is configured with "Destination" set to "Mingled".
236
+
237
+ __The rules:__
238
+ Insert the following in the `server` context of your configuration file (usually found in `/etc/nginx/sites-available`). "The `server` context" refers to the part of the configuration that starts with "server {" and ends with the matching "}".
239
 
 
240
  `
241
+ &#35; WebP Express rules
242
+ &#35; --------------------
243
  location ~* ^/wp-content/.*\.(png|jpe?g)$ {
244
  add_header Vary Accept;
245
  expires 365d;
262
  if ($whattodo = A) {
263
  rewrite ^/wp-content/.*\.(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content break;
264
  }
265
+ &#35; ------------------- (WebP Express rules ends here)
266
+ ```
267
+
268
+ __BEWARE:__
269
+
270
+ - Beware that when copy/pasting you might get html-encoded characters. Verify that the ampersand before "wp-content" isn't encoded (in the last line in the try_files block)
271
+
272
+ - Beware that the rules looks for existing webp files by appending ".webp" to the URL. So for this to work, you __must__ configure *WebP Express* to store the converted files like that.
273
+
274
+ - Beware that if you haven't enabled *png* conversion, you should replace "(png|jpe?g)" with "jpe?g".
275
 
276
+ - Beware that if you have moved wp-content to a non-standard place, you must change accordingly. Note that you must then also change the "wp-content" parameter to the script. It expects a relative path to wp-content (from document root) and is needed so the script can find the configuration file.
277
 
278
+ - I have put in an expires statement for caching. You might want to modify or disable that.
 
 
279
 
280
+ - I have not set any expire on the webp-on-demand.php request. This is not needed, as the script sets this according to what you set up in WebP Express settings. Also, trying to do it would require a new location block matching webp-on-demand.php, but that would override the location block handling php files, and thus break the functionality.
281
 
282
+ - There is no longer any reason to add "&$args" to the line begining with "/wp-content". It was there to enable debugging a single image by appending "?debug" to the url. I however removed that functionality from `webp-on-demand.php`.
283
 
284
  It is possible to put this stuff inside a `location` directive. However, having `if` directives inside `location` directives [is considered evil](https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/). But it seems that in our case, it works. If you wish to do that, use the following rules instead:
285
 
286
  `
287
+ &#35; WebP Express rules
288
+ &#35; --------------------
289
  location ~* ^/wp-content/.*\.(png|jpe?g)$ {
290
  add_header Vary Accept;
291
  expires 365d;
310
  add_header Vary Accept;
311
  }
312
  }
313
+ &#35; ------------------- (WebP Express rules ends here)
314
+ `
315
+
316
+ PS: In case you only want to redirect images to the script (and not to existing), the rules becomes much simpler:
317
+
318
+ `
319
+ &#35; WebP Express rules
320
+ &#35; --------------------
321
+ if ($http_accept ~* "webp"){
322
+ rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?xsource=x$request_filename&wp-content=wp-content break;
323
+ }
324
+ &#35; ------------------- (WebP Express rules ends here)
325
  `
 
326
 
327
  Discussion on this topic [here](https://wordpress.org/support/topic/nginx-rewrite-rules-4/)
328
  And here: https://github.com/rosell-dk/webp-express/issues/166
329
 
330
+ Here are rules if you need to *replace* the file extension with ".webp" rather than appending ".webp" to it: https://www.keycdn.com/support/optimus/configuration-to-deliver-webp
331
 
332
  = I am on a Windows server =
333
  Good news! It should work now, thanks to a guy that calls himself lwxbr. At least on XAMPP 7.3.1, Windows 10. https://github.com/rosell-dk/webp-express/pull/213.
508
  - Open WebP Express options
509
  - Switch to *CDN friendly* mode.
510
  - Set *File extension* to "Set to .webp"
511
+ - Enable *Alter HTML* and select *Replace image URLs*. It is not absolutely necessary, as Cache Enabler also alters HTML - but there are several reasons to do it. Firstly, *Cache Enabler* doesn't get as many URLs replaced as we do. WebP Express for example also replaces background urls in inline styles. Secondly, *Cache enabler* has [problems in edge cases](https://regexr.com/46isf). Thirdly, WebP Express can be configured to alter HTML to point to corresponding webp images, *before they even exists* which can be used in conjunction with the the *Convert non-existing webp-files upon request* option. And this is smart, because then you don't have trouble with *Cache Enabler* caching HTML which references the original images due to that some images hasn't been converted yet.
512
  - If you enabled *Alter HTML*, also enable *Reference webps that hasn't been converted yet* and *Convert non-existing webp-files upon request*
513
  - If you did not enable *Alter HTML*, enable *Convert non-existing webp-files upon request to original image*
514
 
600
 
601
  == Changelog ==
602
 
603
+ = 0.15.0 =
604
+ *(released: 17 sep 2019)*
605
+
606
+ * Provided test-buttons for checking if the redirects works.
607
+ * You can now choose which folders WebP Express is active in. Ie "Uploads and Themes".
608
+ * You can now choose an alternative file structure for the webps which does not rely on DOCUMENT_ROOT being available.
609
+ * WebP Express can now handle when wp-content is symlinked.
610
+ * The .htaccess rules are now divided across folders. Some rules are needed where the source files are located, some where the webp files are located.
611
+ * Added option to convert only PNG files
612
+ * And a couple of bugfixes.
613
+
614
+ For more info, see the closed issues on the 0.15.0 milestone on the github repository: https://github.com/rosell-dk/webp-express/milestone/22?closed=1
615
+
616
  = 0.14.22 =
617
  *(released: 4 aug 2019)*
618
 
637
  * Fixed bug: Ewww api-key was forgot upon saving options
638
 
639
  = 0.14.19 =
640
+ *(released: 28 jun 2019)** Provided test-buttons for checking if the redirects works.
641
+ * You can now choose which folders WebP Express is active in. Ie "Uploads and Themes".
642
+ * You can now choose an alternative file structure for the webps which does not rely on DOCUMENT_ROOT being available.
643
+ * WebP Express can now handle when wp-content is symlinked.
644
+ * The .htaccess rules are now divided across folders. Some rules are needed where the source files are located, some where the webp files are located.
645
+ * And a couple of bugfixes.
646
+
647
+ For more info, see the closed issues on the 0.15.0 milestone on the github repository: https://github.com/rosell-dk/webp-express/milestone/22?closed=1
648
 
649
  * Removed a line that might course Sanity Check to fail ("path not within document root")
650
 
945
 
946
  == Upgrade Notice ==
947
 
948
+ = 0.15.0 =
949
+ * New "Scope" and "destination structure" options, nice test buttons and a lot of work behind the surface
950
+
951
  = 0.14.22 =
952
  * A bundle of bug fixes and a security fix for Windows
953
 
changelog.txt CHANGED
@@ -1,3 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  = 0.13.0 =
2
  *(released: 21 mar 2019)*
3
  * Bulk Conversion
1
+ = 0.15.0 =
2
+ *(released: 17 sep 2019)*
3
+
4
+ * Provided test-buttons for checking if the redirects works.
5
+ * You can now choose which folders WebP Express is active in. Ie "Uploads and Themes".
6
+ * You can now choose an alternative file structure for the webps which does not rely on DOCUMENT_ROOT being available.
7
+ * WebP Express can now handle when wp-content is symlinked.
8
+ * The .htaccess rules are now divided across folders. Some rules are needed where the source files are located, some where the webp files are located.
9
+ * Added option to convert only PNG files
10
+ * And a couple of bugfixes.
11
+
12
+ For more info, see the closed issues on the 0.15.0 milestone on the github repository: https://github.com/rosell-dk/webp-express/milestone/22?closed=1
13
+
14
+ = 0.14.22 =
15
+ *(released: 4 aug 2019)*
16
+
17
+ * Fixed bug in Nginx rules in the FAQ (they did not take into account that the webp images outside upload folder are never stored "mingled")
18
+ * Fixed bug: The extension setting was not respected - it was always appending ".webp", never setting. Thanks to Florian from Germany and Derrick Hammer from USA for reporting.
19
+ * Fixed bug: It turns out that Imagick gives up quality detection for some images and returns 0. This resulted in very poor quality webps when the quality was set to same as jpeg. The 0 is now treated as a failure and the max-quality will be used for such images. Thanks to @sanjayojha303 from India for reporting that there were quality problems on some images.
20
+ * Fixed bug-like behavior: The conversion scripts no longer requires that the respective setting is on for Nginx. Thanks to Mike from Russia for reporting this.
21
+ * Fixed bug: The error handler in webp-convert for handling warnings could in some cases result in endless recursion. For some the result was that they could no longer upload images. Thanks to Tobias Keller from Germany for reporting this bug.
22
+ * Fixed minor bug: Attempt to call private method in a rare scenario (when accessing one of the php scripts in the "wod" folder directly - which is not allowed). Thanks to Giacomo Lawrance from the U.K. for providing input that led to this discovery.
23
+ * Fixed minor bug: It was not tested whether a corresponding webp existed before trying to deleting it when an image was deleted. This produced warnings in debug.log.
24
+ * Security related: Added sanitizing of paths to avoid false positives on coderisk.com (there where no risk because already test the paths for sanity - but this is not detected by coderisk, as the variable is not modified). This was simply done in order get rid of the warnings at coderisk.
25
+ * Security fix: Paths were not sanitized on Windows.
26
+
27
+ = 0.14.21 =
28
+ *(released: 30 jun 2019)*
29
+
30
+ * Hopefully fixed WebP Express Error: "png" option is Object
31
+
32
+ = 0.14.20 =
33
+ *(released: 29 jun 2019)*
34
+
35
+ * Fixed bug: Ewww api-key was forgot upon saving options
36
+
37
+ = 0.14.19 =
38
+ *(released: 28 jun 2019)*
39
+
40
+ * Removed a line that might course Sanity Check to fail ("path not within document root")
41
+
42
+ = 0.14.18 =
43
+ *(released: 28 jun 2019)*
44
+
45
+ * Fixed Sanity Error: Path is outside allowed path on systems using symlinked folders
46
+ * Updated cache breaking token for javascript in order for the last fix for changing password with Remote WebP Express to take effect
47
+ * Fixed undefined variable error in image_make_intermediate_size hook, which prevented webps thumbnails to be generated upon upload
48
+ * Minor bug fix in cwebp converter (updated to webp-convert v.2.1.4)
49
+
50
+ = 0.14.17 =
51
+ *(released: 28 jun 2019)*
52
+
53
+ * Relaxed abspath sanity check on Windows
54
+ * Fixed updating password for Remote WebP Express
55
+
56
+ = 0.14.16 =
57
+ *(released: 26 jun 2019)*
58
+
59
+ * Fixed conversion errors using Bulk convert or Test convert on systems with symlinked folders
60
+
61
+ = 0.14.15 =
62
+ *(released: 26 jun 2019)*
63
+
64
+ * Fixed errors with "redirect to conversion script" on systems with symlinked folders
65
+ * Fixed errors with "redirect to conversion script" on systems where the filename cannot be passed through an environment variable
66
+
67
+ = 0.14.14 =
68
+ *(released: 26 jun 2019)*
69
+
70
+ * Fixed errors on systems with symlinked folders
71
+
72
+ = 0.14.13 =
73
+ *(released: 26 jun 2019)*
74
+
75
+ * Fixed errors in conversion scripts
76
+
77
+ = 0.14.12 =
78
+ *(released: 26 jun 2019)*
79
+
80
+ * Fixed critical bug
81
+
82
+ = 0.14.11 =
83
+ *(released: 24 jun 2019)*
84
+
85
+ The following security fixes has been applied in 0.14.0 - 0.14.11:
86
+ It is urged that you upgrade all of you WebP Express installations!
87
+
88
+ – Security fix: Closed a security hole that could be used to view the content of any file on the server (provided that the full path is known or guessed). This is a very serious flaw, which unfortunately has been around for quite a while.
89
+ – Security fix: Added capability checks to options page.
90
+ – Security fix: Sanitized user input.
91
+ – Security fix: Added checks for file paths and directories.
92
+ – Security fix: Nonces and capability checks for AJAX calls.
93
+
94
+ = 0.14.10 =
95
+ *(released: 24 jun 2019)*
96
+
97
+ * Security related
98
+
99
+ = 0.14.9 =
100
+ *(released: 22 jun 2019)*
101
+
102
+ * Security related
103
+
104
+ = 0.14.8 =
105
+ *(released: 21 jun 2019)*
106
+
107
+ * Security related
108
+
109
+ = 0.14.7 =
110
+ *(released: 20 jun 2019)*
111
+
112
+ * Security related: Removed unneccesary files from webp-convert library
113
+
114
+ = 0.14.6 =
115
+ *(released: 20 jun 2019)*
116
+
117
+ * Security related
118
+
119
+ = 0.14.5 =
120
+ *(released: 20 jun 2019)*
121
+
122
+ * Security related
123
+
124
+ = 0.14.4 =
125
+ *(released: 18 jun 2019)*
126
+
127
+ * Now bundles with multiple cwebp binaries for linux for systems where 1.0.2 fails.
128
+
129
+ = 0.14.3 =
130
+ *(released: 18 jun 2019)*
131
+
132
+ * Fixed filename of supplied cwebp for linux (bug was introduced in 0.14.2)
133
+
134
+ = 0.14.2 =
135
+ *(released: 17 jun 2019)*
136
+
137
+ * Fixed problem with older versions of cwebp
138
+ * Fixed that images was not deleted
139
+ * Fixed cache problem on options page on systems that disables cache busting (it resulted in "SyntaxError: JSON.parse")
140
+
141
+ = 0.14.1 =
142
+ *(released: 15 jun 2019)*
143
+
144
+ * Security related
145
+
146
+ = 0.14.0 =
147
+ *(released: 15 jun 2019)*
148
+
149
+ * Security fix: Closed a security hole that could be used to view the content of any file on the server (provided that the full path is known or guessed). This is a very serious flaw, which has been around for quite a while. I urge you to upgrade to 0.14.0.
150
+ * Added new "encoding" option, which can be set to auto. This can in some cases dramatically reduce the size of the webp. It is supported by all converters except ewww and gd.
151
+ * Added new "near-lossless" option (only for cwebp and vips). Using this is a good idea for reducing size of lossless webps with an acceptable loss of quality
152
+ * Added new "alpha-quality" option (all converters, except ewww and gd). Using this is a good idea when images with transparency are converted to lossy webp - it has the potential to reduce the size up to 50% (depending on the source material) while keeping an acceptable level of quality
153
+ * Added new conversion methods: Vips and GraphicsMagick
154
+ * Imagick conversion method now supports webp options (finally cracked it!)
155
+ * Using MimeType detection instead of relying on file extensions
156
+ * In "test" converter you now change options and also test PNG files
157
+ * Added conversion logs
158
+ * PNGs are now enabled by default (with the new conversion features especially PNGs are compressed much better)
159
+
160
+ For more info, see the closed issues on the 0.14.0 milestone on the github repository: https://github.com/rosell-dk/webp-express/milestone/9?closed=1
161
+
162
  = 0.13.0 =
163
  *(released: 21 mar 2019)*
164
  * Bulk Conversion
composer.lock CHANGED
@@ -118,16 +118,16 @@
118
  },
119
  {
120
  "name": "rosell-dk/webp-convert",
121
- "version": "2.1.5",
122
  "source": {
123
  "type": "git",
124
  "url": "https://github.com/rosell-dk/webp-convert.git",
125
- "reference": "b28ce7fe71ce4f930bd352f12b4f1591ac21b9f0"
126
  },
127
  "dist": {
128
  "type": "zip",
129
- "url": "https://api.github.com/repos/rosell-dk/webp-convert/zipball/b28ce7fe71ce4f930bd352f12b4f1591ac21b9f0",
130
- "reference": "b28ce7fe71ce4f930bd352f12b4f1591ac21b9f0",
131
  "shasum": ""
132
  },
133
  "require": {
@@ -190,7 +190,7 @@
190
  "png",
191
  "png2webp"
192
  ],
193
- "time": "2019-08-02T14:13:24+00:00"
194
  },
195
  {
196
  "name": "rosell-dk/webp-convert-cloud-service",
118
  },
119
  {
120
  "name": "rosell-dk/webp-convert",
121
+ "version": "2.1.6",
122
  "source": {
123
  "type": "git",
124
  "url": "https://github.com/rosell-dk/webp-convert.git",
125
+ "reference": "5cb744d2786468fb51883d1f3a90562166df3320"
126
  },
127
  "dist": {
128
  "type": "zip",
129
+ "url": "https://api.github.com/repos/rosell-dk/webp-convert/zipball/5cb744d2786468fb51883d1f3a90562166df3320",
130
+ "reference": "5cb744d2786468fb51883d1f3a90562166df3320",
131
  "shasum": ""
132
  },
133
  "require": {
190
  "png",
191
  "png2webp"
192
  ],
193
+ "time": "2019-08-10T22:04:08+00:00"
194
  },
195
  {
196
  "name": "rosell-dk/webp-convert-cloud-service",
docs/development.md CHANGED
@@ -1,6 +1,6 @@
1
  ## Updating the vendor dir:
2
 
3
- 1. Run `composer update` in the root.
4
  2. Run `composer dump-autoload -o`
5
  (for some reason, `vendor/composer/autoload_classmap.php` looses all its mappings on composer update). It also looses them on a `composer dump-autoload` (without the -o option).
6
  It actually seems that the mappings are not needed. It seems to work fine when I alter autoload_real to not use the static loader. But well, I'm reluctant to change anything that works.
@@ -11,21 +11,8 @@ It actually seems that the mappings are not needed. It seems to work fine when I
11
  - cd into the folder
12
 
13
  ```
14
- rm -r tests
15
-
16
- rm -r vendor/rosell-dk/webp-convert/build-scripts
17
- rm -r vendor/rosell-dk/webp-convert/tests
18
- rm -r vendor/rosell-dk/webp-convert/build-tests-webp-convert
19
- rm -r vendor/rosell-dk/webp-convert/build-tests-wod
20
- rm -r vendor/rosell-dk/webp-convert/src-build
21
  rm -r vendor/rosell-dk/webp-convert/docs
22
- rm vendor/rosell-dk/webp-convert/*.sh
23
- rm -r vendor/rosell-dk/webp-convert/src//Helpers/*.txt
24
- rm vendor/rosell-dk/webp-convert/.gitignore
25
-
26
- rm -r vendor/rosell-dk/webp-convert-cloud-service/tests
27
- rm -r vendor/rosell-dk/webp-convert-cloud-service/docs
28
-
29
  rm vendor/rosell-dk/dom-util-for-webp/phpstan.neon
30
 
31
  ```
1
  ## Updating the vendor dir:
2
 
3
+ 1. Run `composer update` in the root (plugin root).
4
  2. Run `composer dump-autoload -o`
5
  (for some reason, `vendor/composer/autoload_classmap.php` looses all its mappings on composer update). It also looses them on a `composer dump-autoload` (without the -o option).
6
  It actually seems that the mappings are not needed. It seems to work fine when I alter autoload_real to not use the static loader. But well, I'm reluctant to change anything that works.
11
  - cd into the folder
12
 
13
  ```
 
 
 
 
 
 
 
14
  rm -r vendor/rosell-dk/webp-convert/docs
15
+ rm -r vendor/rosell-dk/webp-convert/src/Helpers/*.txt
 
 
 
 
 
 
16
  rm vendor/rosell-dk/dom-util-for-webp/phpstan.neon
17
 
18
  ```
lib/classes/AdminInit.php CHANGED
@@ -30,7 +30,7 @@ class AdminInit
30
  public static function runMigrationIfNeeded()
31
  {
32
  // When an update requires a migration, the number should be increased
33
- define('WEBPEXPRESS_MIGRATION_VERSION', '10');
34
 
35
  if (WEBPEXPRESS_MIGRATION_VERSION != Option::getOption('webp-express-migration-version', 0)) {
36
  // run migration logic
@@ -38,7 +38,7 @@ class AdminInit
38
  }
39
 
40
  // uncomment next line to test-run a migration
41
- //include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate10.php';
42
  }
43
 
44
  public static function adminInitHandler()
@@ -62,6 +62,7 @@ class AdminInit
62
  add_action('wp_ajax_webpexpress_view_log', array('\WebPExpress\ConvertLog', 'processAjaxViewLog'));
63
  add_action('wp_ajax_webpexpress_purge_cache', array('\WebPExpress\CachePurge', 'processAjaxPurgeCache'));
64
  add_action('wp_ajax_webpexpress_dismiss_message', array('\WebPExpress\DismissableMessages', 'processAjaxDismissMessage'));
 
65
 
66
 
67
  // Add settings link on the plugins page
30
  public static function runMigrationIfNeeded()
31
  {
32
  // When an update requires a migration, the number should be increased
33
+ define('WEBPEXPRESS_MIGRATION_VERSION', '11');
34
 
35
  if (WEBPEXPRESS_MIGRATION_VERSION != Option::getOption('webp-express-migration-version', 0)) {
36
  // run migration logic
38
  }
39
 
40
  // uncomment next line to test-run a migration
41
+ //include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate11.php';
42
  }
43
 
44
  public static function adminInitHandler()
62
  add_action('wp_ajax_webpexpress_view_log', array('\WebPExpress\ConvertLog', 'processAjaxViewLog'));
63
  add_action('wp_ajax_webpexpress_purge_cache', array('\WebPExpress\CachePurge', 'processAjaxPurgeCache'));
64
  add_action('wp_ajax_webpexpress_dismiss_message', array('\WebPExpress\DismissableMessages', 'processAjaxDismissMessage'));
65
+ add_action('wp_ajax_webpexpress_self_test', array('\WebPExpress\SelfTest', 'processAjax'));
66
 
67
 
68
  // Add settings link on the plugins page
lib/classes/AlterHtmlHelper.php CHANGED
@@ -81,7 +81,7 @@ class AlterHtmlHelper
81
 
82
  /**
83
  * Looks if $imageUrl is rooted in $baseUrl and if the file is there
84
- *
85
  *
86
  * @param $imageUrl (ie http://example.com/wp-content/image.jpg)
87
  * @param $baseUrl (ie http://example.com/wp-content)
@@ -109,7 +109,7 @@ class AlterHtmlHelper
109
 
110
  }
111
 
112
-
113
  public static function isSourceInUpload($src)
114
  {
115
  /* $src is ie http://we0/wp-content-moved/themes/twentyseventeen/assets/images/header.jpg */
@@ -135,13 +135,14 @@ class AlterHtmlHelper
135
  *
136
  * returns false
137
  * - if no source file found in that base
138
- * - or webp file isn't there and the `only-for-webps-that-exists` option is set
139
  *
140
- * @param $imageUrl (ie http://example.com/wp-content/image.jpg)
141
- * @param $baseUrl (ie http://example.com/wp-content)
142
- * @param $baseDir (ie /var/www/example.com/wp-content)
 
143
  */
144
- private static function getWebPUrlInBase($sourceUrl, $baseUrl, $baseDir)
145
  {
146
  //error_log('getWebPUrlInBase:' . $sourceUrl . ':' . $baseUrl . ':' . $baseDir);
147
 
@@ -151,40 +152,44 @@ class AlterHtmlHelper
151
  return false;
152
  }
153
 
154
- // Calculate file path to src
155
  $srcPathAbs = $baseDir . $srcPathRel;
156
 
157
- // Check that src file exists
158
  if (!@file_exists($srcPathAbs)) {
159
  return false;
160
  }
161
 
 
 
162
 
163
- // Calculate $destPathAbs and $destUrl
164
- // -------------------------------------
165
- $inUpload = self::isSourceInUpload($sourceUrl);
166
 
167
- if ((self::$options['destination-folder'] == 'mingled') && $inUpload) {
168
- // mingled
169
- if (self::$options['destination-extension'] == 'append') {
170
- $destPathAbs = $srcPathAbs . '.webp';
171
- $destUrl = $sourceUrl . '.webp';
172
- } else {
173
- $destPathAbs = preg_replace('/\\.(png|jpe?g)$/', '', $srcPathAbs) . '.webp';
174
- $destUrl = preg_replace('/\\.(png|jpe?g)$/', '', $sourceUrl) . '.webp';
175
- }
176
- } else {
177
- // separate (images that are not in upload are always put in separate)
178
-
179
- $relPathFromDocRoot = '/webp-express/webp-images/doc-root/';
180
- $relPathFromDocRoot .= PathHelper::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $baseDir) . $srcPathRel;
181
-
182
- list ($contentDirAbs, $contentUrl) = self::$options['bases']['content'];
183
-
184
- $destPathAbs = $contentDirAbs . $relPathFromDocRoot . '.webp';
185
- $destUrl = $contentUrl . $relPathFromDocRoot . '.webp';
186
  }
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  $webpMustExist = self::$options['only-for-webps-that-exists'];
189
  if ($webpMustExist && (!@file_exists($destPathAbs))) {
190
  return false;
@@ -243,7 +248,7 @@ class AlterHtmlHelper
243
  $baseDir = Paths::getUploadDirAbs();
244
  }
245
 
246
- $result = self::getWebPUrlInBase($sourceUrl, $baseUrl, $baseDir);
247
  if ($result !== false) {
248
  return $result;
249
  }
81
 
82
  /**
83
  * Looks if $imageUrl is rooted in $baseUrl and if the file is there
84
+ * PS: NOT USED ANYMORE!
85
  *
86
  * @param $imageUrl (ie http://example.com/wp-content/image.jpg)
87
  * @param $baseUrl (ie http://example.com/wp-content)
109
 
110
  }
111
 
112
+ // NOT USED ANYMORE
113
  public static function isSourceInUpload($src)
114
  {
115
  /* $src is ie http://we0/wp-content-moved/themes/twentyseventeen/assets/images/header.jpg */
135
  *
136
  * returns false
137
  * - if no source file found in that base
138
+ * - or source file is found but webp file isn't there and the `only-for-webps-that-exists` option is set
139
  *
140
+ * @param string $sourceUrl Url of source image (ie http://example.com/wp-content/image.jpg)
141
+ * @param string $rootId Id (created in Config::updateAutoloadedOptions). Ie "uploads", "content" or any image root id
142
+ * @param string $baseUrl Base url of source image (ie http://example.com/wp-content)
143
+ * @param string $baseDir Base dir of source image (ie /var/www/example.com/wp-content)
144
  */
145
+ public static function getWebPUrlInBase($sourceUrl, $rootId, $baseUrl, $baseDir)
146
  {
147
  //error_log('getWebPUrlInBase:' . $sourceUrl . ':' . $baseUrl . ':' . $baseDir);
148
 
152
  return false;
153
  }
154
 
155
+ // Calculate file path to source
156
  $srcPathAbs = $baseDir . $srcPathRel;
157
 
158
+ // Check that source file exists
159
  if (!@file_exists($srcPathAbs)) {
160
  return false;
161
  }
162
 
163
+ // Calculate destination of webp (both path and url)
164
+ // ----------------------------------------
165
 
166
+ // We are calculating: $destPathAbs and $destUrl.
 
 
167
 
168
+ if (!isset(self::$options['bases'][$rootId])) {
169
+ return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }
171
 
172
+ $destinationRoot = Paths::destinationRoot(
173
+ $rootId,
174
+ self::$options['destination-folder'],
175
+ self::$options['destination-structure']
176
+ );
177
+
178
+ $relPathFromImageRootToSource = PathHelper::getRelDir(
179
+ realpath(Paths::getAbsDirById($rootId)),
180
+ realpath($srcPathAbs)
181
+ );
182
+ $relPathFromImageRootToDest = ConvertHelperIndependent::appendOrSetExtension(
183
+ $relPathFromImageRootToSource,
184
+ self::$options['destination-folder'],
185
+ self::$options['destination-extension'],
186
+ ($rootId == 'uploads')
187
+ );
188
+ $result['destination-url'] = $destinationRoot['url'] . '/' . $relPathFromImageRootToDest;
189
+
190
+ $destPathAbs = $destinationRoot['abs-path'] . '/' . $relPathFromImageRootToDest;
191
+ $destUrl = $destinationRoot['url'] . '/' . $relPathFromImageRootToDest;
192
+
193
  $webpMustExist = self::$options['only-for-webps-that-exists'];
194
  if ($webpMustExist && (!@file_exists($destPathAbs))) {
195
  return false;
248
  $baseDir = Paths::getUploadDirAbs();
249
  }
250
 
251
+ $result = self::getWebPUrlInBase($sourceUrl, $id, $baseUrl, $baseDir);
252
  if ($result !== false) {
253
  return $result;
254
  }
lib/classes/BulkConvert.php CHANGED
@@ -2,26 +2,11 @@
2
 
3
  namespace WebPExpress;
4
 
5
- use \WebPExpress\ConvertHelperIndependent;
6
- use \WebPExpress\Paths;
7
- use \WebPExpress\PathHelper;
8
-
9
  class BulkConvert
10
  {
11
 
12
- public static function getUploadFolder($destinationFolder)
13
- {
14
- switch ($destinationFolder) {
15
- case 'mingled':
16
- return Paths::getUploadDirAbs();
17
- case 'separate':
18
- return Paths::getCacheDirAbs() . '/doc-root/' . Paths::getUploadDirRel();
19
- }
20
- }
21
-
22
  public static function getList($config)
23
  {
24
- //$cacheDir = self::getUploadFolder($config['destination-folder']);
25
 
26
 
27
  /*
@@ -34,11 +19,12 @@ class BulkConvert
34
 
35
  $listOptions = [
36
  //'root' => Paths::getUploadDirAbs(),
37
- //'cache-root' => self::getUploadFolder($config['destination-folder']),
38
  'ext' => $config['destination-extension'],
39
  'destination-folder' => $config['destination-folder'], /* hm, "destination-folder" is a bad name... */
40
  'webExpressContentDirAbs' => Paths::getWebPExpressContentDirAbs(),
41
  'uploadDirAbs' => Paths::getUploadDirAbs(),
 
 
42
  'filter' => [
43
  'only-converted' => false,
44
  'only-unconverted' => true,
@@ -46,13 +32,42 @@ class BulkConvert
46
  ]
47
  ];
48
 
 
 
 
 
49
  $groups = [];
 
 
 
 
 
 
50
 
51
- $groups[] = [
52
- 'groupName' => 'wp-content',
53
- 'root' => Paths::getContentDirAbs(),
54
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
 
56
  if (Paths::isUploadDirMovedOutOfWPContentDir()) {
57
  $groups[] = [
58
  'groupName' => 'uploads',
@@ -60,18 +75,28 @@ class BulkConvert
60
  ];
61
  }
62
 
 
 
 
 
 
 
 
 
 
 
63
  if (Paths::isPluginDirMovedOutOfWpContent()) {
64
  $groups[] = [
65
  'groupName' => 'plugins',
66
  'root' => Paths::getPluginDirAbs(),
67
  ];
68
- }
69
 
70
  foreach ($groups as $i => &$group) {
71
  $listOptions['root'] = $group['root'];
72
  /*
73
  No use, because if uploads is in wp-content, the cache root will be different for the files in uploads (if mingled)
74
- $group['cache-root'] = ConvertHelperIndependent::getDestinationFolder(
75
  $group['root'],
76
  $listOptions['destination-folder'],
77
  $listOptions['ext'],
@@ -79,11 +104,9 @@ class BulkConvert
79
  $listOptions['uploadDirAbs']
80
  );*/
81
  $group['files'] = self::getListRecursively('.', $listOptions);
82
- //'cache-root' => ConvertHelperIndependent::getDestinationFolder()
83
  }
84
 
85
-
86
-
87
  return $groups;
88
  //self::moveRecursively($toDir, $fromDir, $srcDir, $fromExt, $toExt);
89
  }
@@ -133,13 +156,15 @@ class BulkConvert
133
  $addThis = true;
134
 
135
  if (($filter['only-converted']) || ($filter['only-unconverted'])) {
136
- //$cacheDir = $listOptions['cache-root'] . '/' . $relDir;
137
  $destination = ConvertHelperIndependent::getDestination(
138
  $dir . "/" . $filename,
139
  $listOptions['destination-folder'],
140
  $listOptions['ext'],
141
  $listOptions['webExpressContentDirAbs'],
142
- $listOptions['uploadDirAbs']
 
 
143
  );
144
  $webpExists = @file_exists($destination);
145
 
2
 
3
  namespace WebPExpress;
4
 
 
 
 
 
5
  class BulkConvert
6
  {
7
 
 
 
 
 
 
 
 
 
 
 
8
  public static function getList($config)
9
  {
 
10
 
11
 
12
  /*
19
 
20
  $listOptions = [
21
  //'root' => Paths::getUploadDirAbs(),
 
22
  'ext' => $config['destination-extension'],
23
  'destination-folder' => $config['destination-folder'], /* hm, "destination-folder" is a bad name... */
24
  'webExpressContentDirAbs' => Paths::getWebPExpressContentDirAbs(),
25
  'uploadDirAbs' => Paths::getUploadDirAbs(),
26
+ 'useDocRootForStructuringCacheDir' => (($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
27
+ 'imageRoots' => new ImageRoots(Paths::getImageRootsDefForSelectedIds($config['scope'])), // (Paths::getImageRootsDef()
28
  'filter' => [
29
  'only-converted' => false,
30
  'only-unconverted' => true,
32
  ]
33
  ];
34
 
35
+ //$dirs = $config['scope'];
36
+
37
+ $rootIds = Paths::filterOutSubRoots($config['scope']);
38
+
39
  $groups = [];
40
+ foreach ($rootIds as $rootId) {
41
+ $groups[] = [
42
+ 'groupName' => $rootId,
43
+ 'root' => Paths::getAbsDirById($rootId)
44
+ ];
45
+ }
46
 
47
+ /*
48
+ if (in_array('index', $config['scope'])) {
49
+ if (Paths::isWPContentDirMovedOutOfAbsPath()) {
50
+
51
+ }
52
+ } elseif (in_array('wp-content', $config['scope'])) {
53
+ $dirs[] = 'wp-content';
54
+ if (in_array('uploads', $config['scope']) && Paths::isUploadDirMovedOutOfWPContentDir()) {
55
+ $dirs[] = 'uploads';
56
+ }
57
+ if (in_array('plugins', $config['scope']) && Paths::isPluginDirMovedOutOfWpContent()) {
58
+ $dirs[] = 'plugins';
59
+ }
60
+ // ps: themes is always below wp-content
61
+ } else {
62
+ if (in_array('uploads', $config['scope'])) {
63
+ $dirs[] = 'uploads';
64
+ }
65
+ if (in_array('plugins', $config['scope'])) {
66
+ $dirs[] = 'plugins';
67
+ }
68
+ }
69
 
70
+ /*
71
  if (Paths::isUploadDirMovedOutOfWPContentDir()) {
72
  $groups[] = [
73
  'groupName' => 'uploads',
75
  ];
76
  }
77
 
78
+ $groups[] = [
79
+ 'groupName' => 'themes',
80
+ 'root' => Paths::getThemesDirAbs(),
81
+ ];
82
+
83
+ $groups[] = [
84
+ 'groupName' => 'wp-content',
85
+ 'root' => Paths::getContentDirAbs(),
86
+ ];
87
+
88
  if (Paths::isPluginDirMovedOutOfWpContent()) {
89
  $groups[] = [
90
  'groupName' => 'plugins',
91
  'root' => Paths::getPluginDirAbs(),
92
  ];
93
+ }*/
94
 
95
  foreach ($groups as $i => &$group) {
96
  $listOptions['root'] = $group['root'];
97
  /*
98
  No use, because if uploads is in wp-content, the cache root will be different for the files in uploads (if mingled)
99
+ $group['image-root'] = ConvertHelperIndependent::getDestinationFolder(
100
  $group['root'],
101
  $listOptions['destination-folder'],
102
  $listOptions['ext'],
104
  $listOptions['uploadDirAbs']
105
  );*/
106
  $group['files'] = self::getListRecursively('.', $listOptions);
107
+ //'image-root' => ConvertHelperIndependent::getDestinationFolder()
108
  }
109
 
 
 
110
  return $groups;
111
  //self::moveRecursively($toDir, $fromDir, $srcDir, $fromExt, $toExt);
112
  }
156
  $addThis = true;
157
 
158
  if (($filter['only-converted']) || ($filter['only-unconverted'])) {
159
+ //$cacheDir = $listOptions['image-root'] . '/' . $relDir;
160
  $destination = ConvertHelperIndependent::getDestination(
161
  $dir . "/" . $filename,
162
  $listOptions['destination-folder'],
163
  $listOptions['ext'],
164
  $listOptions['webExpressContentDirAbs'],
165
+ $listOptions['uploadDirAbs'],
166
+ $listOptions['useDocRootForStructuringCacheDir'],
167
+ $listOptions['imageRoots']
168
  );
169
  $webpExists = @file_exists($destination);
170
 
lib/classes/CacheMover.php CHANGED
@@ -3,6 +3,7 @@
3
  namespace WebPExpress;
4
 
5
  use \WebPExpress\FileHelper;
 
6
  use \WebPExpress\Paths;
7
 
8
  class CacheMover
@@ -45,15 +46,64 @@ class CacheMover
45
  FileHelper::chmod_r($dir, $dirPerm, $filePerm, $uid, $gid, '#\.webp$#', ($alsoSetOnDirs ? null : '#^$#'));
46
  }
47
 
 
 
 
 
 
48
  /**
49
  * Move cache because of change in options.
50
- * Only move the upload folder
51
  * Only move those that has an original
52
  * Only move those that can be moved.
53
  * @return [$numFilesMoved, $numFilesFailedMoving]
54
  */
55
  public static function move($newConfig, $oldConfig)
56
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  $fromDir = self::getUploadFolder($oldConfig['destination-folder']);
58
  $fromExt = $oldConfig['destination-extension'];
59
 
@@ -62,6 +112,12 @@ class CacheMover
62
 
63
  $srcDir = self::getUploadFolder('mingled');
64
 
 
 
 
 
 
 
65
  // for testing!
66
  /*
67
  $fromDir = self::getUploadFolder('mingled'); // separate | mingled
@@ -77,10 +133,6 @@ class CacheMover
77
 
78
  //error_log('move to:' . $toDir . ' ( ' . (file_exists($toDir) ? 'exists' : 'does not exist ') . ')');
79
 
80
- $result = self::moveRecursively($fromDir, $toDir, $srcDir, $fromExt, $toExt);
81
- self::chmodFixSubDirs($toDir, ($newConfig['destination-folder'] == 'separate'));
82
-
83
- return $result;
84
  //self::moveRecursively($toDir, $fromDir, $srcDir, $fromExt, $toExt);
85
  }
86
 
3
  namespace WebPExpress;
4
 
5
  use \WebPExpress\FileHelper;
6
+ use \WebPExpress\PathHelper;
7
  use \WebPExpress\Paths;
8
 
9
  class CacheMover
46
  FileHelper::chmod_r($dir, $dirPerm, $filePerm, $uid, $gid, '#\.webp$#', ($alsoSetOnDirs ? null : '#^$#'));
47
  }
48
 
49
+ public static function getDestinationFolderForImageRoot($config, $imageRootId)
50
+ {
51
+ return Paths::getCacheDirForImageRoot($config['destination-folder'], $config['destination-structure'], $imageRootId);
52
+ }
53
+
54
  /**
55
  * Move cache because of change in options.
56
+ * If structure is unchanged, only move the upload folder
57
  * Only move those that has an original
58
  * Only move those that can be moved.
59
  * @return [$numFilesMoved, $numFilesFailedMoving]
60
  */
61
  public static function move($newConfig, $oldConfig)
62
  {
63
+ if (!Paths::canUseDocRootForStructuringCacheDir()) {
64
+ if (($oldConfig['destination-structure'] == 'doc-root') || ($newConfig['destination-structure'] == 'doc-root')) {
65
+ // oh, well. Seems document root is not available.
66
+ // so we cannot move from or to that kind of structure
67
+ // This could happen if document root once was available but now is unavailable
68
+ return [0, 0];
69
+ }
70
+ }
71
+
72
+ $changeStructure = ($newConfig['destination-structure'] != $oldConfig['destination-structure']);
73
+
74
+ if ($changeStructure) {
75
+ $rootIds = Paths::getImageRootIds();
76
+ } else {
77
+ $rootIds = ['uploads'];
78
+ }
79
+
80
+ $numFilesMovedTotal = 0;
81
+ $numFilesFailedMovingTotal = 0;
82
+ foreach ($rootIds as $rootId) {
83
+
84
+ $isUploadsMingled = (($newConfig['destination-folder'] == 'mingled') && ($rootId == 'uploads'));
85
+
86
+ $fromDir = self::getDestinationFolderForImageRoot($oldConfig, $rootId);
87
+ $fromExt = $oldConfig['destination-extension'];
88
+
89
+ $toDir = self::getDestinationFolderForImageRoot($newConfig, $rootId);
90
+ $toExt = $newConfig['destination-extension'];
91
+
92
+ $srcDir = Paths::getAbsDirById($rootId);
93
+
94
+ list($numFilesMoved, $numFilesFailedMoving) = self::moveRecursively($fromDir, $toDir, $srcDir, $fromExt, $toExt);
95
+ if (!$isUploadsMingled) {
96
+ FileHelper::removeEmptySubFolders($fromDir);
97
+ }
98
+
99
+ $numFilesMovedTotal += $numFilesMoved;
100
+ $numFilesFailedMovingTotal += $numFilesFailedMoving;
101
+
102
+ $chmodFixFoldersToo = !$isUploadsMingled;
103
+ self::chmodFixSubDirs($toDir, $chmodFixFoldersToo);
104
+ }
105
+ return [$numFilesMovedTotal, $numFilesFailedMovingTotal];
106
+ /*
107
  $fromDir = self::getUploadFolder($oldConfig['destination-folder']);
108
  $fromExt = $oldConfig['destination-extension'];
109
 
112
 
113
  $srcDir = self::getUploadFolder('mingled');
114
 
115
+ $result = self::moveRecursively($fromDir, $toDir, $srcDir, $fromExt, $toExt);
116
+ self::chmodFixSubDirs($toDir, ($newConfig['destination-folder'] == 'separate'));
117
+ */
118
+
119
+ //return $result;
120
+
121
  // for testing!
122
  /*
123
  $fromDir = self::getUploadFolder('mingled'); // separate | mingled
133
 
134
  //error_log('move to:' . $toDir . ' ( ' . (file_exists($toDir) ? 'exists' : 'does not exist ') . ')');
135
 
 
 
 
 
136
  //self::moveRecursively($toDir, $fromDir, $srcDir, $fromExt, $toExt);
137
  }
138
 
lib/classes/CachePurge.php CHANGED
@@ -7,6 +7,8 @@ use \WebPExpress\FileHelper;
7
  use \WebPExpress\DismissableMessages;
8
  use \WebPExpress\Paths;
9
 
 
 
10
  class CachePurge
11
  {
12
 
@@ -27,6 +29,7 @@ class CachePurge
27
  $numFailed = 0;
28
 
29
  list($numDeleted, $numFailed) = self::purgeWebPFilesInDir(Paths::getCacheDirAbs(), $filter, $config);
 
30
 
31
  if ($config['destination-folder'] == 'mingled') {
32
  list($d, $f) = self::purgeWebPFilesInDir(Paths::getUploadDirAbs(), $filter, $config);
@@ -35,6 +38,7 @@ class CachePurge
35
  $numFailed += $f;
36
  }
37
 
 
38
  return [
39
  'delete-count' => $numDeleted,
40
  'fail-count' => $numFailed
7
  use \WebPExpress\DismissableMessages;
8
  use \WebPExpress\Paths;
9
 
10
+ // TODO! Needs to be updated to work with the new "destination-structure" setting
11
+
12
  class CachePurge
13
  {
14
 
29
  $numFailed = 0;
30
 
31
  list($numDeleted, $numFailed) = self::purgeWebPFilesInDir(Paths::getCacheDirAbs(), $filter, $config);
32
+ FileHelper::removeEmptySubFolders(Paths::getCacheDirAbs());
33
 
34
  if ($config['destination-folder'] == 'mingled') {
35
  list($d, $f) = self::purgeWebPFilesInDir(Paths::getUploadDirAbs(), $filter, $config);
38
  $numFailed += $f;
39
  }
40
 
41
+
42
  return [
43
  'delete-count' => $numDeleted,
44
  'fail-count' => $numFailed
lib/classes/CapabilityTest.php CHANGED
@@ -1,5 +1,11 @@
1
  <?php
 
 
2
 
 
 
 
 
3
  namespace WebPExpress;
4
 
5
  use \WebPExpress\FileHelper;
1
  <?php
2
+ /*
3
+ This functionality will be moved to a separate project.
4
 
5
+ Btw:
6
+ Seems someone else got similar idea:
7
+ http://christian.roy.name/blog/detecting-modrewrite-using-php
8
+ */
9
  namespace WebPExpress;
10
 
11
  use \WebPExpress\FileHelper;
lib/classes/Config.php CHANGED
@@ -15,7 +15,7 @@ class Config
15
  {
16
 
17
  /**
18
- * Return object or false, if config file does not exist, or read error
19
  */
20
  public static function loadJSONOptions($filename)
21
  {
@@ -33,7 +33,10 @@ class Config
33
 
34
  public static function saveJSONOptions($filename, $obj)
35
  {
36
- $result = @file_put_contents($filename, json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT));
 
 
 
37
  /*if ($result === false) {
38
  echo 'COULD NOT' . $filename;
39
  }*/
@@ -41,6 +44,9 @@ class Config
41
  }
42
 
43
 
 
 
 
44
  public static function loadConfig()
45
  {
46
  return self::loadJSONOptions(Paths::getConfigFileName());
@@ -61,10 +67,12 @@ class Config
61
  'image-types' => 3,
62
  'destination-folder' => 'separate',
63
  'destination-extension' => 'append',
 
64
  'cache-control' => 'no-header', /* can be "no-header", "set" or "custom" */
65
  'cache-control-custom' => 'public, max-age=31536000, stale-while-revalidate=604800, stale-if-error=604800',
66
  'cache-control-max-age' => 'one-week',
67
  'cache-control-public' => false,
 
68
 
69
  // redirection rules
70
  'enable-redirection-to-converter' => true,
@@ -121,7 +129,13 @@ class Config
121
  ]
122
  */
123
  ]
 
124
 
 
 
 
 
 
125
  ]
126
  ];
127
  }
@@ -170,6 +184,7 @@ class Config
170
  ]);
171
  $config['alter-html']['only-for-webps-that-exists'] = true;
172
  $config['web-service']['enabled'] = false;
 
173
 
174
  }
175
 
@@ -192,15 +207,41 @@ class Config
192
  $defaultConfig = self::getDefaultConfig(true);
193
  $config = array_merge($defaultConfig, $config);
194
 
 
 
195
  $config['alter-html'] = array_replace_recursive($defaultConfig['alter-html'], $config['alter-html']);
 
 
 
196
  }
197
 
198
  if (!isset($config['base-htaccess-on-these-capability-tests'])) {
199
  self::runAndStoreCapabilityTests($config);
200
  }
201
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  $config = self::applyOperationMode($config);
203
 
 
 
 
 
 
 
 
 
 
204
  if (!isset($config['web-service'])) {
205
  $config['web-service'] = [
206
  'enabled' => false
@@ -425,16 +466,44 @@ class Config
425
  unset($obj['enabled']);
426
  $obj['destination-folder'] = $config['destination-folder'];
427
  $obj['destination-extension'] = $config['destination-extension'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  $obj['bases'] = [
429
  'uploads' => [
430
  Paths::getUploadDirAbs(),
431
  Paths::getUploadUrl()
432
  ],
433
- 'content' => [
 
 
 
434
  Paths::getContentDirAbs(),
435
  Paths::getContentUrl()
436
- ],
437
- ];
 
 
 
 
 
 
 
 
 
438
  $obj['image-types'] = $config['image-types']; // 0=none,1=jpg, 2=png, 3=both
439
 
440
  Option::updateOption(
@@ -447,9 +516,7 @@ class Config
447
  public static function saveConfigurationFile($config)
448
  {
449
  $config['paths-used-in-htaccess'] = [
450
- 'existing' => Paths::getPathToExisting(),
451
  'wod-url-path' => Paths::getWodUrlPath(),
452
- 'config-dir-rel' => Paths::getConfigDirRel()
453
  ];
454
 
455
  if (Paths::createConfigDirIfMissing()) {
@@ -609,9 +676,7 @@ class Config
609
  'destination-folder' => $config['destination-folder'],
610
  'forward-query-string' => $config['forward-query-string'],
611
  //'method-for-passing-source' => $config['method-for-passing-source'],
612
- 'paths' => [
613
- 'uploadDirRel' => Paths::getUploadDirRel()
614
- ],
615
  'success-response' => $config['success-response'],
616
  ];
617
 
15
  {
16
 
17
  /**
18
+ * @return object|false Returns parsed file the file exists and can be read. Otherwise it returns false
19
  */
20
  public static function loadJSONOptions($filename)
21
  {
33
 
34
  public static function saveJSONOptions($filename, $obj)
35
  {
36
+ $result = @file_put_contents(
37
+ $filename,
38
+ json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT)
39
+ );
40
  /*if ($result === false) {
41
  echo 'COULD NOT' . $filename;
42
  }*/
44
  }
45
 
46
 
47
+ /**
48
+ * @return object|false Returns config object if config file exists and can be read. Otherwise it returns false
49
+ */
50
  public static function loadConfig()
51
  {
52
  return self::loadJSONOptions(Paths::getConfigFileName());
67
  'image-types' => 3,
68
  'destination-folder' => 'separate',
69
  'destination-extension' => 'append',
70
+ 'destination-structure' => 'doc-root', /* can be "doc-root" or "image-roots" */
71
  'cache-control' => 'no-header', /* can be "no-header", "set" or "custom" */
72
  'cache-control-custom' => 'public, max-age=31536000, stale-while-revalidate=604800, stale-if-error=604800',
73
  'cache-control-max-age' => 'one-week',
74
  'cache-control-public' => false,
75
+ 'scope' => ['themes', 'uploads'],
76
 
77
  // redirection rules
78
  'enable-redirection-to-converter' => true,
129
  ]
130
  */
131
  ]
132
+ ],
133
 
134
+ 'environment-when-config-was-saved' => [
135
+ 'doc-root-available' => null, // null means unavailable
136
+ 'doc-root-resolvable' => null,
137
+ 'doc-root-usable-for-structuring' => null,
138
+ 'image-roots' => null,
139
  ]
140
  ];
141
  }
184
  ]);
185
  $config['alter-html']['only-for-webps-that-exists'] = true;
186
  $config['web-service']['enabled'] = false;
187
+ $config['scope'] = ['uploads'];
188
 
189
  }
190
 
207
  $defaultConfig = self::getDefaultConfig(true);
208
  $config = array_merge($defaultConfig, $config);
209
 
210
+ // Make sure new defaults below "alter-html" are added into the existing array
211
+ // (note that this will not remove old unused properties, if some key should become obsolete)
212
  $config['alter-html'] = array_replace_recursive($defaultConfig['alter-html'], $config['alter-html']);
213
+
214
+ // Make sure new defaults below "environment-when-config-was-saved" are added into the existing array
215
+ $config['environment-when-config-was-saved'] = array_replace_recursive($defaultConfig['environment-when-config-was-saved'], $config['environment-when-config-was-saved']);
216
  }
217
 
218
  if (!isset($config['base-htaccess-on-these-capability-tests'])) {
219
  self::runAndStoreCapabilityTests($config);
220
  }
221
 
222
+ // Apparently, migrate7 did not fix old "operation-mode" values for all.
223
+ // So fix here
224
+ if ($config['operation-mode'] == 'just-redirect') {
225
+ $config['operation-mode'] = 'no-conversion';
226
+ }
227
+ if ($config['operation-mode'] == 'no-varied-responses') {
228
+ $config['operation-mode'] = 'cdn-friendly';
229
+ }
230
+ if ($config['operation-mode'] == 'varied-responses') {
231
+ $config['operation-mode'] = 'varied-image-responses';
232
+ }
233
+
234
  $config = self::applyOperationMode($config);
235
 
236
+ // Fix scope: Remove invalid and put in correct order
237
+ $fixedScope = [];
238
+ foreach (Paths::getImageRootIds() as $rootId) {
239
+ if (in_array($rootId, $config['scope'])) {
240
+ $fixedScope[] = $rootId;
241
+ }
242
+ }
243
+ $config['scope'] = $fixedScope;
244
+
245
  if (!isset($config['web-service'])) {
246
  $config['web-service'] = [
247
  'enabled' => false
466
  unset($obj['enabled']);
467
  $obj['destination-folder'] = $config['destination-folder'];
468
  $obj['destination-extension'] = $config['destination-extension'];
469
+ $obj['destination-structure'] = $config['destination-structure'];
470
+
471
+
472
+ $obj['bases'] = [];
473
+ foreach ($config['scope'] as $rootId) {
474
+ $obj['bases'][$rootId] = [
475
+ Paths::getAbsDirById($rootId),
476
+ Paths::getUrlById($rootId)
477
+ ];
478
+ }
479
+
480
+ /*
481
+ // TODO!
482
+ // Instead of "bases", use image root ids.
483
+ // Its a numeric array and there is no id called "content"
484
+
485
  $obj['bases'] = [
486
  'uploads' => [
487
  Paths::getUploadDirAbs(),
488
  Paths::getUploadUrl()
489
  ],
490
+ ];
491
+
492
+ if ($obj['destination-structure'] == 'doc-root') {
493
+ $obj['bases']['content'] = [
494
  Paths::getContentDirAbs(),
495
  Paths::getContentUrl()
496
+ ];
497
+ } else {
498
+ foreach (Paths::getImageRootIds() as $rootId) {
499
+ $obj['bases'][$rootId] = [
500
+ Paths::getAbsDirById($rootId),
501
+ Paths::getUrlById($rootId)
502
+ ];
503
+ }
504
+ }*/
505
+
506
+
507
  $obj['image-types'] = $config['image-types']; // 0=none,1=jpg, 2=png, 3=both
508
 
509
  Option::updateOption(
516
  public static function saveConfigurationFile($config)
517
  {
518
  $config['paths-used-in-htaccess'] = [
 
519
  'wod-url-path' => Paths::getWodUrlPath(),
 
520
  ];
521
 
522
  if (Paths::createConfigDirIfMissing()) {
676
  'destination-folder' => $config['destination-folder'],
677
  'forward-query-string' => $config['forward-query-string'],
678
  //'method-for-passing-source' => $config['method-for-passing-source'],
679
+ 'image-roots' => Paths::getImageRootsDef(),
 
 
680
  'success-response' => $config['success-response'],
681
  ];
682
 
lib/classes/Convert.php CHANGED
@@ -5,6 +5,7 @@ namespace WebPExpress;
5
  use \WebPExpress\ConvertHelperIndependent;
6
  use \WebPExpress\Config;
7
  use \WebPExpress\ConvertersHelper;
 
8
  use \WebPExpress\SanityCheck;
9
  use \WebPExpress\SanityException;
10
  use \WebPExpress\Validate;
@@ -23,7 +24,9 @@ class Convert
23
  $config['destination-folder'],
24
  $config['destination-extension'],
25
  Paths::getWebPExpressContentDirAbs(),
26
- Paths::getUploadDirAbs()
 
 
27
  );
28
  }
29
 
@@ -32,11 +35,17 @@ class Convert
32
  try {
33
  // Check source
34
  // ---------------
35
- $checking = 'filename!';
36
- $filename = $source;
37
  //$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
38
  // PS: No need to check mime type as the WebPConvert library does that (it only accepts image/jpeg and image/png)
39
 
 
 
 
 
 
 
40
 
41
  // Check config
42
  // --------------
@@ -76,22 +85,38 @@ class Convert
76
  $logDir = SanityCheck::absPathIsInDocRoot(Paths::getWebPExpressContentDirAbs() . '/log');
77
 
78
 
79
- } catch (SanityException $e) {
80
  return [
81
  'success' => false,
82
- 'msg' => 'Sanitation check failed for ' . $checking . ': '. $e->getMessage(),
83
  'log' => '',
84
  ];
85
  }
86
 
87
  // Done with sanitizing, lets get to work!
88
  // ---------------------------------------
89
-
90
  $result = ConvertHelperIndependent::convert($source, $destination, $convertOptions, $logDir, $converter);
91
 
92
  if ($result['success'] === true) {
93
  $result['filesize-original'] = @filesize($source);
94
  $result['filesize-webp'] = @filesize($destination);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
  return $result;
97
  }
@@ -127,7 +152,9 @@ class Convert
127
  $destination,
128
  $config['destination-folder'],
129
  $config['destination-extension'],
130
- Paths::getWebPExpressContentDirAbs()
 
 
131
  );
132
  }
133
 
@@ -145,7 +172,12 @@ class Convert
145
  // Check "filename"
146
  $checking = '"filename" argument';
147
  Validate::postHasKey('filename');
148
- $filename = sanitize_text_field($_POST['filename']);
 
 
 
 
 
149
  //$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($filename);
150
  // PS: No need to check mime version as webp-convert does that.
151
 
5
  use \WebPExpress\ConvertHelperIndependent;
6
  use \WebPExpress\Config;
7
  use \WebPExpress\ConvertersHelper;
8
+ use \WebPExpress\ImageRoots;
9
  use \WebPExpress\SanityCheck;
10
  use \WebPExpress\SanityException;
11
  use \WebPExpress\Validate;
24
  $config['destination-folder'],
25
  $config['destination-extension'],
26
  Paths::getWebPExpressContentDirAbs(),
27
+ Paths::getUploadDirAbs(),
28
+ (($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
29
+ new ImageRoots(Paths::getImageRootsDef())
30
  );
31
  }
32
 
35
  try {
36
  // Check source
37
  // ---------------
38
+ $checking = 'source path';
39
+ $source = SanityCheck::absPathExistsAndIsFile($source);
40
  //$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
41
  // PS: No need to check mime type as the WebPConvert library does that (it only accepts image/jpeg and image/png)
42
 
43
+ // Check that source is within a valid image root
44
+ $activeRootIds = Paths::getImageRootIds(); // Currently, root ids cannot be selected, so all root ids are active.
45
+ $rootId = Paths::findImageRootOfPath($source, $activeRootIds);
46
+ if ($rootId === false) {
47
+ throw new Exception('Path of source is not within a valid image root');
48
+ }
49
 
50
  // Check config
51
  // --------------
85
  $logDir = SanityCheck::absPathIsInDocRoot(Paths::getWebPExpressContentDirAbs() . '/log');
86
 
87
 
88
+ } catch (\Exception $e) {
89
  return [
90
  'success' => false,
91
+ 'msg' => 'Check failed for ' . $checking . ': '. $e->getMessage(),
92
  'log' => '',
93
  ];
94
  }
95
 
96
  // Done with sanitizing, lets get to work!
97
  // ---------------------------------------
98
+ //return false;
99
  $result = ConvertHelperIndependent::convert($source, $destination, $convertOptions, $logDir, $converter);
100
 
101
  if ($result['success'] === true) {
102
  $result['filesize-original'] = @filesize($source);
103
  $result['filesize-webp'] = @filesize($destination);
104
+ $result['destination-path'] = $destination;
105
+
106
+ $rootOfDestination = Paths::destinationRoot($rootId, $config['destination-folder'], $config['destination-structure']);
107
+
108
+ $relPathFromImageRootToSource = PathHelper::getRelDir(
109
+ realpath(Paths::getAbsDirById($rootId)),
110
+ realpath($source)
111
+ );
112
+ $relPathFromImageRootToDest = ConvertHelperIndependent::appendOrSetExtension(
113
+ $relPathFromImageRootToSource,
114
+ $config['destination-folder'],
115
+ $config['destination-extension'],
116
+ ($rootId == 'uploads')
117
+ );
118
+
119
+ $result['destination-url'] = $rootOfDestination['url'] . '/' . $relPathFromImageRootToDest;
120
  }
121
  return $result;
122
  }
152
  $destination,
153
  $config['destination-folder'],
154
  $config['destination-extension'],
155
+ $config['destination-structure'],
156
+ Paths::getWebPExpressContentDirAbs(),
157
+ new ImageRoots(Paths::getImageRootsDef())
158
  );
159
  }
160
 
172
  // Check "filename"
173
  $checking = '"filename" argument';
174
  Validate::postHasKey('filename');
175
+
176
+ $filename = sanitize_text_field(stripslashes($_POST['filename']));
177
+
178
+ // holy moly! Wordpress automatically adds slashes to the global POST vars - https://stackoverflow.com/questions/2496455/why-are-post-variables-getting-escaped-in-php
179
+ $filename = wp_unslash($_POST['filename']);
180
+
181
  //$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($filename);
182
  // PS: No need to check mime version as webp-convert does that.
183
 
lib/classes/ConvertHelperIndependent.php CHANGED
@@ -47,6 +47,31 @@ class ConvertHelperIndependent
47
  return strpos($normalizedSource, $normalizedDocRoot) === 0;
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  /**
52
  * Get destination path corresponding to the source path given (and some configurations)
@@ -54,15 +79,24 @@ class ConvertHelperIndependent
54
  * If for example Operation mode is set to "mingled" and extension is set to "Append .webp",
55
  * the result of finding the destination path that corresponds to "/path/to/logo.jpg" will be "/path/to/logo.jpg.webp".
56
  *
57
- * @param string $source Path to source file
58
- * @param string $destinationFolder 'mingled' or 'separate'
59
- * @param string $destinationExt Extension ('append' or 'set')
60
- * @param string $webExpressContentDirAbs
61
- * @param string $uploadDirAbs
 
 
62
  *
63
  * @return string|false Returns path to destination corresponding to source, or false on failure
64
  */
65
- public static function getDestination($source, $destinationFolder, $destinationExt, $webExpressContentDirAbs, $uploadDirAbs)
 
 
 
 
 
 
 
66
  {
67
  // At this point, everything has already been checked for sanity. But for good meassure, lets
68
  // check the most important parts again. This is after all a public method.
@@ -77,25 +111,113 @@ class ConvertHelperIndependent
77
  // Calculate destination and check that the result is sane
78
  // -------------------------------------------------------
79
  if (self::storeMingledOrNot($source, $destinationFolder, $uploadDirAbs)) {
80
- if ($destinationExt == 'append') {
81
- // TODO: make this check work with symlinks
82
- //$destination = SanityCheck::absPathIsInDocRoot($source . '.webp');
83
- $destination = $source . '.webp';
84
- } else {
85
- $destination = preg_replace('/\\.(jpe?g|png)$/', '', $source) . '.webp';
86
- }
87
  } else {
88
- $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
89
- $imageRoot = $webExpressContentDirAbs . '/webp-images';
90
 
91
- // TODO: make this check work with symlinks
92
- //SanityCheck::absPathIsInDocRoot($imageRoot);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- $sourceRel = substr($source, strlen($docRoot) + 1);
95
- $destination = $imageRoot . '/doc-root/' . $sourceRel . '.webp';
96
 
97
- // TODO: make this check work with symlinks
98
- //$destination = SanityCheck::absPathIsInDocRoot($destination);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
  } catch (SanityException $e) {
@@ -113,47 +235,140 @@ class ConvertHelperIndependent
113
  * Returns false if source file is not found or if a path is not sane. Otherwise returns path to source
114
  * destination does not have to exist.
115
  *
116
- * @param string $destination Path to destination file (does not have to exist)
117
- * @param string $webExpressContentDirAbs
 
 
118
  *
119
  * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
120
  */
121
- private static function findSourceSeparate($destination, $webExpressContentDirAbs)
122
  {
123
  try {
124
 
125
- // Check that destination path is sane and inside document root
126
- // --------------------------
127
- $destination = SanityCheck::absPathIsInDocRoot($destination);
128
 
 
 
 
129
 
130
- // Check that calculated image root is sane and inside document root
131
- // --------------------------
132
- $imageRoot = SanityCheck::absPathIsInDocRoot($webExpressContentDirAbs . '/webp-images/doc-root');
133
 
 
 
 
134
 
135
- // Calculate source and check that it is sane and exists
136
- // -----------------------------------------------------
137
 
138
- // TODO: This does not work on Windows yet.
139
- // NOTE: WE CANNOT DO AS WITH sourceIsInsideDocRoot, because it relies on realpath, which only translates EXISTING paths.
140
- // $destination does not exist yet, when this method is called from webp-realizer.php
141
- if (strpos($destination, $imageRoot . '/') === 0) {
 
142
 
143
- // "Eat" the left part off the $destination parameter. $destination is for example:
144
- // "/var/www/webp-express-tests/we0/wp-content-moved/webp-express/webp-images/doc-root/wordpress/uploads-moved/2018/12/tegning5-300x265.jpg.webp"
145
- // We also eat the slash (+1)
146
- $sourceRel = substr($destination, strlen($imageRoot) + 1);
147
 
148
- $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
149
- $source = $docRoot . '/' . $sourceRel;
150
- $source = preg_replace('/\\.(webp)$/', '', $source);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  return false;
153
  }
154
-
155
- $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
156
-
157
  } catch (SanityException $e) {
158
  return false;
159
  }
@@ -165,19 +380,24 @@ class ConvertHelperIndependent
165
  * Find source corresponding to destination (mingled)
166
  * Returns false if not found. Otherwise returns path to source
167
  *
168
- * @param string $destination Path to destination file (does not have to exist)
169
- * @param string $destinationExt Extension ('append' or 'set')
 
170
  *
171
  * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
172
  */
173
- private static function findSourceMingled($destination, $destinationExt)
174
  {
175
  try {
176
 
177
- // Check that destination path is sane and inside document root
178
- // --------------------------
179
- $destination = SanityCheck::absPathIsInDocRoot($destination);
180
-
 
 
 
 
181
 
182
  // Calculate source and check that it is sane and exists
183
  // -----------------------------------------------------
@@ -192,7 +412,11 @@ class ConvertHelperIndependent
192
  $source = preg_replace('/\\.webp$/', '.png', $destination);
193
  }
194
  }
195
- $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
 
 
 
 
196
 
197
 
198
  } catch (SanityException $e) {
@@ -206,33 +430,41 @@ class ConvertHelperIndependent
206
  * Get source from destination (and some configurations)
207
  * Returns false if not found. Otherwise returns path to source
208
  *
209
- * @param string $destination Path to destination file (does not have to exist)
210
  * @param string $destinationFolder 'mingled' or 'separate'
211
  * @param string $destinationExt Extension ('append' or 'set')
 
212
  * @param string $webExpressContentDirAbs
 
213
  *
214
  * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
215
  */
216
- public static function findSource($destination, $destinationFolder, $destinationExt, $webExpressContentDirAbs)
217
  {
 
218
  try {
219
 
220
- // Check that destination path is sane and inside document root
221
- // --------------------------
222
- $destination = SanityCheck::absPathIsInDocRoot($destination);
 
 
 
 
 
223
 
224
  } catch (SanityException $e) {
225
  return false;
226
  }
227
 
228
  if ($destinationFolder == 'mingled') {
229
- $result = self::findSourceMingled($destination, $destinationExt);
230
  if ($result === false) {
231
- $result = self::findSourceSeparate($destination, $webExpressContentDirAbs);
232
  }
233
  return $result;
234
  } else {
235
- return self::findSourceSeparate($destination, $webExpressContentDirAbs);
236
  }
237
  }
238
 
@@ -326,7 +558,7 @@ APACHE
326
 
327
  $text = preg_replace('#' . preg_quote($_SERVER["DOCUMENT_ROOT"]) . '#', '[doc-root]', $text);
328
 
329
- $text = 'WebP Express 0.14.22. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
330
 
331
  $logFile = self::getLogFilename($source, $logDir);
332
 
@@ -348,8 +580,8 @@ APACHE
348
  *
349
  * PS: To convert with a specific converter, set it in the $converter param.
350
  *
351
- * @param string $source Path to the source file that was converted.
352
- * @param string $destination Path to the destination file (may exist or not).
353
  * @param array $convertOptions Conversion options.
354
  * @param string $logDir The folder where log files are kept.
355
  * @param string $converter (optional) Set it to convert with a specific converter.
@@ -436,20 +668,23 @@ APACHE
436
  // ---------------------------------------------
437
  try {
438
 
439
- // Check that source path is sane, exists, is a file and is inside document root
440
  // -------------------------------------------------------
441
- $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
 
442
 
443
 
444
- // Check that destination path is sane and is inside document root
445
  // -------------------------------------------------------
446
- $destination = SanityCheck::absPathIsInDocRoot($destination);
 
447
  $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Destination does not end with .webp');
448
 
449
 
450
- // Check that log path is sane and inside document root
451
  // -------------------------------------------------------
452
- $logDir = SanityCheck::absPathIsInDocRoot($logDir);
 
453
 
454
 
455
  // PS: No need to check $logMsgTop. Log files are markdown and stored as ".md". They can do no harm.
47
  return strpos($normalizedSource, $normalizedDocRoot) === 0;
48
  }
49
 
50
+ public static function getSource()
51
+ {
52
+
53
+ }
54
+
55
+ /**
56
+ * Append ".webp" to path or replace extension with "webp", depending on what is appropriate.
57
+ *
58
+ * If destination-folder is set to mingled and destination-extension is set to "set" and
59
+ * the path is inside upload folder, the appropriate thing is to SET the extension.
60
+ * Otherwise, it is to APPEND.
61
+ *
62
+ * @param string $path
63
+ * @param string $destinationFolder
64
+ * @param string $destinationExt
65
+ * @param boolean $inUploadFolder
66
+ */
67
+ public static function appendOrSetExtension($path, $destinationFolder, $destinationExt, $inUploadFolder)
68
+ {
69
+ if (($destinationFolder == 'mingled') && ($destinationExt == 'set') && $inUploadFolder) {
70
+ return preg_replace('/\\.(jpe?g|png)$/', '', $path) . '.webp';
71
+ } else {
72
+ return $path . '.webp';
73
+ }
74
+ }
75
 
76
  /**
77
  * Get destination path corresponding to the source path given (and some configurations)
79
  * If for example Operation mode is set to "mingled" and extension is set to "Append .webp",
80
  * the result of finding the destination path that corresponds to "/path/to/logo.jpg" will be "/path/to/logo.jpg.webp".
81
  *
82
+ * @param string $source Path to source file
83
+ * @param string $destinationFolder 'mingled' or 'separate'
84
+ * @param string $destinationExt Extension ('append' or 'set')
85
+ * @param string $webExpressContentDirAbs
86
+ * @param string $uploadDirAbs
87
+ * @param boolean $useDocRootForStructuringCacheDir
88
+ * @param ImageRoots $imageRoots An image roots object
89
  *
90
  * @return string|false Returns path to destination corresponding to source, or false on failure
91
  */
92
+ public static function getDestination(
93
+ $source,
94
+ $destinationFolder,
95
+ $destinationExt,
96
+ $webExpressContentDirAbs,
97
+ $uploadDirAbs,
98
+ $useDocRootForStructuringCacheDir,
99
+ $imageRoots)
100
  {
101
  // At this point, everything has already been checked for sanity. But for good meassure, lets
102
  // check the most important parts again. This is after all a public method.
111
  // Calculate destination and check that the result is sane
112
  // -------------------------------------------------------
113
  if (self::storeMingledOrNot($source, $destinationFolder, $uploadDirAbs)) {
114
+ $destination = self::appendOrSetExtension($source, $destinationFolder, $destinationExt, true);
 
 
 
 
 
 
115
  } else {
 
 
116
 
117
+ if ($useDocRootForStructuringCacheDir) {
118
+ // We must find the relative path from document root to source.
119
+ // However, we dont know if document root is resolved or not.
120
+ // We also do not know if source begins with a resolved or unresolved document root.
121
+ // And we cannot be sure that document root is resolvable.
122
+
123
+ // Lets say:
124
+ // 1. document root is unresolvable.
125
+ // 2. document root is configured to something unresolved ("/my-website")
126
+ // 3. source is resolved and within an image root ("/var/www/my-website/wp-content/uploads/test.jpg")
127
+ // 4. all image roots are resolvable.
128
+ // 5. Paths::canUseDocRootForRelPaths()) returned true
129
+
130
+ // Can the relative path then be found?
131
+ // Actually, yes.
132
+ // We can loop through the image roots.
133
+ // When we get to the "uploads" root, it must neccessarily contain the unresolved document root.
134
+ // It will in other words be: "my-website/wp-content/uploads"
135
+ // It can not be configured to the resolved path because canUseDocRootForRelPaths would have then returned false as
136
+ // It would not be possible to establish that "/var/www/my-website/wp-content/uploads/" is within document root, as
137
+ // document root is "/my-website" and unresolvable.
138
+ // To sum up, we have:
139
+ // If document root is unresolvable while canUseDocRootForRelPaths() succeeded, then the image roots will all begin with
140
+ // the unresolved path.
141
+ // In this method, if $useDocRootForStructuringCacheDir is true, then it is assumed that canUseDocRootForRelPaths()
142
+ // succeeded.
143
+ // OH!
144
+ // I realize that the image root can be passed as well:
145
+ // $imageRoot = $webExpressContentDirAbs . '/webp-images';
146
+ // So the question is: Will $webExpressContentDirAbs also be the unresolved path?
147
+ // That variable is calculated in WodConfigLoader based on various methods available.
148
+ // I'm not digging into it, but would expect it to in some cases be resolved. Which means that relative path can not
149
+ // be found.
150
+ // So. Lets play it safe and require that document root is resolvable in order to use docRoot for structure
151
+
152
+ if (!PathHelper::isDocRootAvailable()) {
153
+ throw new \Exception(
154
+ 'Can not calculate destination using "doc-root" structure as document root is not available. $_SERVER["DOCUMENT_ROOT"] is empty. ' .
155
+ 'This is probably a misconfiguration on the server. ' .
156
+ 'However, WebP Express can function without using documument root. If you resave options and regenerate the .htaccess files, it should ' .
157
+ 'automatically start to structure the webp files in subfolders that are relative the image root folders rather than document-root.'
158
+ );
159
+ }
160
+
161
+ if (!PathHelper::isDocRootAvailableAndResolvable()) {
162
+ throw new \Exception(
163
+ 'Can not calculate destination using "doc-root" structure as document root cannot be resolved for symlinks using "realpath". The ' .
164
+ 'reason for that is probably that open_basedir protection has been set up and that document root is outside outside that open_basedir. ' .
165
+ 'WebP Express can function in that setting, however you will need to resave options and regenerate the .htaccess files. It should then ' .
166
+ 'automatically stop to structure the webp files as relative to document root and instead structure them as relative to image root folders.'
167
+ );
168
+ }
169
+ $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
170
+ $imageRoot = $webExpressContentDirAbs . '/webp-images';
171
+
172
+ // TODO: make this check work with symlinks
173
+ //SanityCheck::absPathIsInDocRoot($imageRoot);
174
+
175
+ $sourceRel = substr(realpath($source), strlen($docRoot) + 1);
176
+ $destination = $imageRoot . '/doc-root/' . $sourceRel;
177
+ $destination = self::appendOrSetExtension($destination, $destinationFolder, $destinationExt, false);
178
 
 
 
179
 
180
+ // TODO: make this check work with symlinks
181
+ //$destination = SanityCheck::absPathIsInDocRoot($destination);
182
+ } else {
183
+ $destination = '';
184
+
185
+ $sourceResolved = realpath($source);
186
+
187
+
188
+ // Check roots until we (hopefully) get a match.
189
+ // (that is: find a root which the source is inside)
190
+ foreach ($imageRoots->getArray() as $i => $imageRoot) {
191
+ // in $obj, "rel-path" is only set when document root can be used for relative paths.
192
+ // So, if it is set, we can use it (beware: we cannot neccessarily use realpath on document root,
193
+ // but we do not need to - see the long comment in Paths::canUseDocRootForRelPaths())
194
+
195
+ $rootPath = $imageRoot->getAbsPath();
196
+ /*
197
+ if (isset($obj['rel-path'])) {
198
+ $docRoot = rtrim($_SERVER["DOCUMENT_ROOT"], '/');
199
+ $rootPath = $docRoot . '/' . $obj['rel-path'];
200
+ } else {
201
+ // If "rel-path" isn't set, then abs-path is, and we can use that.
202
+ $rootPath = $obj['abs-path'];
203
+ }*/
204
+
205
+ // $source may be resolved or not. Same goes for $rootPath.
206
+ // We can assume that $rootPath is resolvable using realpath (it ought to exist and be within open_basedir for WP to function)
207
+ // We can also assume that $source is resolvable (it ought to exist and within open_basedir)
208
+ // So: Resolve both! and test if the resolved source begins with the resolved rootPath.
209
+ if (strpos($sourceResolved, realpath($rootPath)) !== false) {
210
+ $relPath = substr($sourceResolved, strlen(realpath($rootPath)) + 1);
211
+ $relPath = self::appendOrSetExtension($relPath, $destinationFolder, $destinationExt, false);
212
+
213
+ $destination = $webExpressContentDirAbs . '/webp-images/' . $imageRoot->id . '/' . $relPath;
214
+ break;
215
+ }
216
+ }
217
+ if ($destination == '') {
218
+ return false;
219
+ }
220
+ }
221
  }
222
 
223
  } catch (SanityException $e) {
235
  * Returns false if source file is not found or if a path is not sane. Otherwise returns path to source
236
  * destination does not have to exist.
237
  *
238
+ * @param string $destination Path to destination file (does not have to exist)
239
+ * @param string $destinationStructure "doc-root" or "image-roots"
240
+ * @param string $webExpressContentDirAbs
241
+ * @param ImageRoots $imageRoots An image roots object
242
  *
243
  * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
244
  */
245
+ private static function findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots)
246
  {
247
  try {
248
 
249
+ if ($destinationStructure == 'doc-root') {
 
 
250
 
251
+ // Check that destination path is sane and inside document root
252
+ // --------------------------
253
+ $destination = SanityCheck::absPathIsInDocRoot($destination);
254
 
 
 
 
255
 
256
+ // Check that calculated image root is sane and inside document root
257
+ // --------------------------
258
+ $imageRoot = SanityCheck::absPathIsInDocRoot($webExpressContentDirAbs . '/webp-images/doc-root');
259
 
 
 
260
 
261
+ // Calculate source and check that it is sane and exists
262
+ // -----------------------------------------------------
263
+
264
+ // TODO: This does not work on Windows yet.
265
+ if (strpos($destination, $imageRoot . '/') === 0) {
266
 
267
+ // "Eat" the left part off the $destination parameter. $destination is for example:
268
+ // "/var/www/webp-express-tests/we0/wp-content-moved/webp-express/webp-images/doc-root/wordpress/uploads-moved/2018/12/tegning5-300x265.jpg.webp"
269
+ // We also eat the slash (+1)
270
+ $sourceRel = substr($destination, strlen($imageRoot) + 1);
271
 
272
+ $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
273
+ $source = $docRoot . '/' . $sourceRel;
274
+ $source = preg_replace('/\\.(webp)$/', '', $source);
275
+ } else {
276
+ // Try with symlinks resolved
277
+ // This is not trivial as this must also work when the destination path doesn't exist, and
278
+ // realpath can only be used to resolve symlinks for files that exists.
279
+ // But here is how we achieve it anyway:
280
+ //
281
+ // 1. We make sure imageRoot exists (if not, create it) - this ensures that we can resolve it.
282
+ // 2. Find closest folder existing folder (resolved) of destination - using PathHelper::findClosestExistingFolderSymLinksExpanded()
283
+ // 3. Test that resolved closest existing folder starts with resolved imageRoot
284
+ // 4. If it does, we could create a dummy file at the destination to get its real path, but we want to avoid that, so instead
285
+ // we can create the containing directory.
286
+ // 5. We can now use realpath to get the resolved path of the containing directory. The rest is simple enough.
287
+ if (!file_exists($imageRoot)) {
288
+ mkdir($imageRoot, 0777, true);
289
+ }
290
+ $closestExistingResolved = PathHelper::findClosestExistingFolderSymLinksExpanded($destination);
291
+ if ($closestExistingResolved == '') {
292
+ return false;
293
+ } else {
294
+ $imageRootResolved = realpath($imageRoot);
295
+ if (strpos($closestExistingResolved . '/', $imageRootResolved . '/') === 0) {
296
+ // echo $destination . '<br>' . $closestExistingResolved . '<br>' . $imageRootResolved . '/'; exit;
297
+ // Create containing dir for destination
298
+ $containingDir = PathHelper::dirname($destination);
299
+ if (!file_exists($containingDir)) {
300
+ mkdir($containingDir, 0777, true);
301
+ }
302
+ $containingDirResolved = realpath($containingDir);
303
+
304
+ $filename = PathHelper::basename($destination);
305
+ $destinationResolved = $containingDirResolved . '/' . $filename;
306
+
307
+ $sourceRel = substr($destinationResolved, strlen($imageRootResolved) + 1);
308
+
309
+ $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
310
+ $source = $docRoot . '/' . $sourceRel;
311
+ $source = preg_replace('/\\.(webp)$/', '', $source);
312
+ return $source;
313
+ } else {
314
+ return false;
315
+ }
316
+ }
317
+ }
318
+
319
+ return SanityCheck::absPathExistsAndIsFileInDocRoot($source);
320
  } else {
321
+
322
+ // Mission: To find source corresponding to destination (separate) - using the "image-roots" structure.
323
+
324
+ // How can we do that?
325
+ // We got the destination (unresolved) - ie '/website-symlinked/wp-content/webp-express/webp-images/uploads/2018/07/hello.jpg.webp'
326
+ // If we were lazy and unprecise, we could simply:
327
+ // - search for "webp-express/webp-images/"
328
+ // - strip anything before that - result: 'uploads/2018/07/hello.jpg.webp'
329
+ // - the first path component is the root id.
330
+ // - the rest of the path is the relative path to the source - if we strip the ".webp" ending
331
+
332
+ // So, are we lazy? - what is the alternative?
333
+ // - Get closest existing resolved folder of destination (ie "/var/www/website/wp-content-moved/webp-express/webp-images/wp-content")
334
+ // - Check if that folder is below the cache root (resolved) (cache root is the "wp-content" image root + 'webp-express/webp-images')
335
+ // - Create dir for destination (if missing)
336
+ // - We can now resolve destination. With cache root also being resolved, we can get the relative dir.
337
+ // ie 'uploads/2018/07/hello.jpg.webp'.
338
+ // The first path component is the root id, the rest is the relative path to the source.
339
+
340
+ $closestExistingResolved = PathHelper::findClosestExistingFolderSymLinksExpanded($destination);
341
+ $cacheRoot = $webExpressContentDirAbs . '/webp-images';
342
+ if ($closestExistingResolved == '') {
343
+ return false;
344
+ } else {
345
+ $cacheRootResolved = realpath($cacheRoot);
346
+ if (strpos($closestExistingResolved . '/', $cacheRootResolved . '/') === 0) {
347
+
348
+ // Create containing dir for destination
349
+ $containingDir = PathHelper::dirname($destination);
350
+ if (!file_exists($containingDir)) {
351
+ mkdir($containingDir, 0777, true);
352
+ }
353
+ $containingDirResolved = realpath($containingDir);
354
+
355
+ $filename = PathHelper::basename($destination);
356
+ $destinationResolved = $containingDirResolved . '/' . $filename;
357
+ $destinationRelToCacheRoot = substr($destinationResolved, strlen($cacheRootResolved) + 1);
358
+
359
+ $parts = explode('/', $destinationRelToCacheRoot);
360
+ $imageRoot = array_shift($parts);
361
+ $sourceRel = implode('/', $parts);
362
+
363
+ $source = $imageRoots->byId($imageRoot)->getAbsPath() . '/' . $sourceRel;
364
+ $source = preg_replace('/\\.(webp)$/', '', $source);
365
+ return $source;
366
+ } else {
367
+ return false;
368
+ }
369
+ }
370
  return false;
371
  }
 
 
 
372
  } catch (SanityException $e) {
373
  return false;
374
  }
380
  * Find source corresponding to destination (mingled)
381
  * Returns false if not found. Otherwise returns path to source
382
  *
383
+ * @param string $destination Path to destination file (does not have to exist)
384
+ * @param string $destinationExt Extension ('append' or 'set')
385
+ * @param string $destinationStructure "doc-root" or "image-roots"
386
  *
387
  * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
388
  */
389
+ private static function findSourceMingled($destination, $destinationExt, $destinationStructure)
390
  {
391
  try {
392
 
393
+ if ($destinationStructure == 'doc-root') {
394
+ // Check that destination path is sane and inside document root
395
+ // --------------------------
396
+ $destination = SanityCheck::absPathIsInDocRoot($destination);
397
+ } else {
398
+ // The following will fail if path contains directory traversal. TODO: Is that ok?
399
+ $destination = SanityCheck::absPath($destination);
400
+ }
401
 
402
  // Calculate source and check that it is sane and exists
403
  // -----------------------------------------------------
412
  $source = preg_replace('/\\.webp$/', '.png', $destination);
413
  }
414
  }
415
+ if ($destinationStructure == 'doc-root') {
416
+ $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
417
+ } else {
418
+ $source = SanityCheck::absPathExistsAndIsFile($source);
419
+ }
420
 
421
 
422
  } catch (SanityException $e) {
430
  * Get source from destination (and some configurations)
431
  * Returns false if not found. Otherwise returns path to source
432
  *
433
+ * @param string $destination Path to destination file (does not have to exist). May not contain directory traversal
434
  * @param string $destinationFolder 'mingled' or 'separate'
435
  * @param string $destinationExt Extension ('append' or 'set')
436
+ * @param string $destinationStructure "doc-root" or "image-roots"
437
  * @param string $webExpressContentDirAbs
438
+ * @param ImageRoots $imageRoots An image roots object
439
  *
440
  * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
441
  */
442
+ public static function findSource($destination, $destinationFolder, $destinationExt, $destinationStructure, $webExpressContentDirAbs, $imageRoots)
443
  {
444
+
445
  try {
446
 
447
+ if ($destinationStructure == 'doc-root') {
448
+ // Check that destination path is sane and inside document root
449
+ // --------------------------
450
+ $destination = SanityCheck::absPathIsInDocRoot($destination);
451
+ } else {
452
+ // The following will fail if path contains directory traversal. TODO: Is that ok?
453
+ $destination = SanityCheck::absPath($destination);
454
+ }
455
 
456
  } catch (SanityException $e) {
457
  return false;
458
  }
459
 
460
  if ($destinationFolder == 'mingled') {
461
+ $result = self::findSourceMingled($destination, $destinationExt, $destinationStructure);
462
  if ($result === false) {
463
+ $result = self::findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots);
464
  }
465
  return $result;
466
  } else {
467
+ return self::findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots);
468
  }
469
  }
470
 
558
 
559
  $text = preg_replace('#' . preg_quote($_SERVER["DOCUMENT_ROOT"]) . '#', '[doc-root]', $text);
560
 
561
+ $text = 'WebP Express 0.15.0. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
562
 
563
  $logFile = self::getLogFilename($source, $logDir);
564
 
580
  *
581
  * PS: To convert with a specific converter, set it in the $converter param.
582
  *
583
+ * @param string $source Full path to the source file that was converted.
584
+ * @param string $destination Full path to the destination file (may exist or not).
585
  * @param array $convertOptions Conversion options.
586
  * @param string $logDir The folder where log files are kept.
587
  * @param string $converter (optional) Set it to convert with a specific converter.
668
  // ---------------------------------------------
669
  try {
670
 
671
+ // Check that source path is sane, exists, is a file.
672
  // -------------------------------------------------------
673
+ //$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
674
+ $source = SanityCheck::absPathExistsAndIsFile($source);
675
 
676
 
677
+ // Check that destination path is sane
678
  // -------------------------------------------------------
679
+ //$destination = SanityCheck::absPathIsInDocRoot($destination);
680
+ $destination = SanityCheck::absPath($destination);
681
  $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Destination does not end with .webp');
682
 
683
 
684
+ // Check that log path is sane
685
  // -------------------------------------------------------
686
+ //$logDir = SanityCheck::absPathIsInDocRoot($logDir);
687
+ $logDir = SanityCheck::absPath($logDir);
688
 
689
 
690
  // PS: No need to check $logMsgTop. Log files are markdown and stored as ".md". They can do no harm.
lib/classes/FileHelper.php CHANGED
@@ -301,6 +301,37 @@ class FileHelper
301
  return $success;
302
  }
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
  /**
306
  * Verify if OS is Windows
301
  return $success;
302
  }
303
 
304
+ /**
305
+ * Remove empty subfolders.
306
+ *
307
+ * Got it here: https://stackoverflow.com/a/1833681/842756
308
+ *
309
+ * @return boolean If folder is (was) empty
310
+ */
311
+ public static function removeEmptySubFolders($path, $removeEmptySelfToo = false)
312
+ {
313
+ if (!file_exists($path)) {
314
+ return;
315
+ }
316
+ $empty = true;
317
+ foreach (scandir($path) as $file) {
318
+ if (($file == '.') || ($file == '..')) {
319
+ continue;
320
+ }
321
+ $file = $path . DIRECTORY_SEPARATOR . $file;
322
+ if (is_dir($file)) {
323
+ if (!self::removeEmptySubFolders($file, true)) {
324
+ $empty=false;
325
+ }
326
+ } else {
327
+ $empty=false;
328
+ }
329
+ }
330
+ if ($empty && $removeEmptySelfToo) {
331
+ rmdir($path);
332
+ }
333
+ return $empty;
334
+ }
335
 
336
  /**
337
  * Verify if OS is Windows
lib/classes/HTAccess.php CHANGED
@@ -4,470 +4,12 @@ namespace WebPExpress;
4
 
5
  use \WebPExpress\Config;
6
  use \WebPExpress\FileHelper;
 
7
  use \WebPExpress\Paths;
8
  use \WebPExpress\State;
9
 
10
  class HTAccess
11
  {
12
- // (called from this file only. BUT our saveRules methods calls it, and it is called from several classes)
13
- public static function generateHTAccessRulesFromConfigObj($config, $htaccessDir = 'index')
14
- {
15
- // Any option that is newer than ~v.0.2 may not be set yet.
16
- // So, in order to not have to use isset() all over the place, set to values
17
- // that results in same behaviour as before the option was introduced.
18
- // Beware that this may not be same as the default value in the UI (but it is generally)
19
-
20
- // TODO: can we use the new fix method instead?
21
-
22
- $defaults = [
23
- 'enable-redirection-to-converter' => true,
24
- 'forward-query-string' => true,
25
- 'image-types' => 1,
26
- 'do-not-pass-source-in-query-string' => false,
27
- 'redirect-to-existing-in-htaccess' => false,
28
- 'only-redirect-to-converter-on-cache-miss' => false,
29
- 'destination-folder' => 'separate',
30
- 'destination-extension' => 'append',
31
- 'success-response' => 'converted',
32
- ];
33
- $config = array_merge($defaults, $config);
34
-
35
- if ((!$config['enable-redirection-to-converter']) && (!$config['redirect-to-existing-in-htaccess']) && (!$config['enable-redirection-to-webp-realizer'])) {
36
- return '# WebP Express does not need to write any rules (it has not been set up to redirect to converter, nor to existing webp, and the "convert non-existing webp-files upon request" option has not been enabled)';
37
- }
38
-
39
-
40
-
41
- if (isset($config['base-htaccess-on-these-capability-tests'])) {
42
- $capTests = $config['base-htaccess-on-these-capability-tests'];
43
- $modHeaderDefinitelyUnavailable = ($capTests['modHeaderWorking'] === false);
44
- $passThroughHeaderDefinitelyUnavailable = ($capTests['passThroughHeaderWorking'] === false);
45
- $passThroughHeaderDefinitelyAavailable = ($capTests['passThroughHeaderWorking'] === true);
46
- $passThrougEnvVarDefinitelyUnavailable = ($capTests['passThroughEnvWorking'] === false);
47
- $passThrougEnvVarDefinitelyAvailable =($capTests['passThroughEnvWorking'] === true);
48
- } else {
49
- $modHeaderDefinitelyUnavailable = false;
50
- $passThroughHeaderDefinitelyUnavailable = false;
51
- $passThroughHeaderDefinitelyAavailable = false;
52
- $passThrougEnvVarDefinitelyUnavailable = false;
53
- $passThrougEnvVarDefinitelyAvailable = false;
54
- }
55
-
56
- $setEnvVar = !$passThrougEnvVarDefinitelyUnavailable;
57
- $passFullFilePathInQS = false;
58
- $passRelativeFilePathInQS = !($passThrougEnvVarDefinitelyAvailable || $passThroughHeaderDefinitelyAavailable);
59
- $passFullFilePathInQSRealizer = false;
60
- $passRelativeFilePathInQSRealizer = $passRelativeFilePathInQS;
61
-
62
-
63
- $addVary = $config['redirect-to-existing-in-htaccess'];
64
- if ($modHeaderDefinitelyUnavailable) {
65
- $addVary = false;
66
- }
67
-
68
-
69
- /* Calculate $fileExt */
70
- $imageTypes = $config['image-types'];
71
- $fileExtensions = [];
72
- if ($imageTypes & 1) {
73
- $fileExtensions[] = 'jpe?g';
74
- }
75
- if ($imageTypes & 2) {
76
- $fileExtensions[] = 'png';
77
- }
78
- $fileExt = implode('|', $fileExtensions);
79
-
80
- if ($imageTypes == 0) {
81
- return '# WebP Express disabled (no image types have been choosen to be converted/redirected)';
82
- }
83
-
84
-
85
- // Build cache control rules
86
- $ccRules = '';
87
- $cacheControlHeader = Config::getCacheControlHeader($config);
88
- if ($cacheControlHeader != '') {
89
-
90
- if ($config['redirect-to-existing-in-htaccess']) {
91
- $ccRules .= " # Set Cache-Control header so these direct redirections also get the header set\n";
92
- if ($config['enable-redirection-to-webp-realizer']) {
93
- $ccRules .= " # (and also webp-realizer.php)\n";
94
- }
95
- } else {
96
- if ($config['enable-redirection-to-webp-realizer']) {
97
- $ccRules .= " # Set Cache-Control header for requests to webp images\n";
98
- }
99
- }
100
- $ccRules .= " <IfModule mod_headers.c>\n";
101
- $ccRules .= " <FilesMatch \"\.webp$\">\n";
102
- $ccRules .= " Header set Cache-Control \"" . $cacheControlHeader . "\"\n";
103
- $ccRules .= " </FilesMatch>\n";
104
- $ccRules .= " </IfModule>\n\n";
105
-
106
- // Fall back to mod_expires if mod_headers is unavailable
107
-
108
-
109
-
110
- if ($modHeaderDefinitelyUnavailable) {
111
- $cacheControl = $config['cache-control'];
112
-
113
- if ($cacheControl == 'custom') {
114
- $expires = '';
115
-
116
- // Do not add Expire header if private is set
117
- // - because then the user don't want caching in proxies / CDNs.
118
- // the Expires header doesn't differentiate between private/public
119
- if (!(preg_match('/private/', $config['cache-control-custom']))) {
120
- if (preg_match('/max-age=(\d+)/', $config['cache-control-custom'], $matches)) {
121
- if (isset($matches[1])) {
122
- $expires = $matches[1] . ' seconds';
123
- }
124
- }
125
- }
126
-
127
- } elseif ($cacheControl == 'no-header') {
128
- $expires = '';
129
- } elseif ($cacheControl == 'set') {
130
- if ($config['cache-control-public']) {
131
- $cacheControlOptions = [
132
- 'no-header' => '',
133
- 'one-second' => '1 seconds',
134
- 'one-minute' => '1 minutes',
135
- 'one-hour' => '1 hours',
136
- 'one-day' => '1 days',
137
- 'one-week' => '1 weeks',
138
- 'one-month' => '1 months',
139
- 'one-year' => '1 years',
140
- ];
141
- $expires = $cacheControlOptions[$config['cache-control-max-age']];
142
- }
143
- }
144
-
145
- if ($expires != '') {
146
- // in case mod_headers is missing, try mod_expires
147
- $ccRules .= " # Fall back to mod_expires if mod_headers is unavailable\n";
148
- $ccRules .= " <IfModule !mod_headers.c>\n";
149
- $ccRules .= " <IfModule mod_expires.c>\n";
150
- $ccRules .= " ExpiresActive On\n";
151
- $ccRules .= " ExpiresByType image/webp \"access plus " . $expires . "\"\n";
152
- $ccRules .= " </IfModule>\n";
153
- $ccRules .= " </IfModule>\n\n";
154
- }
155
- }
156
- }
157
-
158
-
159
- /* Build rules */
160
-
161
- /* When .htaccess is placed in root (index), the rules needs to be confined only to work in
162
- content folder (if uploads folder is moved, perhaps also that)
163
- Rules needs to start with ie "^/?(wp-content/.+)" rather than "^/?(.+)"
164
- In the case that upload folder is in root too, rules needs to apply to both.
165
- We do it like this: "^/?((?:wp-content|uploads)/.+)" (using non capturing group)
166
- */
167
-
168
- $rewriteRuleStart = '^/?(.+)';
169
- if ($htaccessDir == 'index') {
170
- // Get relative path between index dir and wp-content dir / uploads
171
- // Because we want to restrict the rule so it doesn't work on wp-admin, but only those two.
172
-
173
- $wpContentRel = PathHelper::getRelDir(Paths::getIndexDirAbs(), Paths::getContentDirAbs());
174
- $uploadsRel = PathHelper::getRelDir(Paths::getIndexDirAbs(), Paths::getUploadDirAbs());
175
-
176
- //$rules .= '# rel: ' . $uploadsRel . "\n";
177
-
178
- if (strpos($wpContentRel, '.') !== 0) {
179
-
180
- if (strpos($uploadsRel, $wpContentRel) === 0) {
181
- $rewriteRuleStart = '^/?(' . $wpContentRel . '/.+)';
182
- } else {
183
- $rewriteRuleStart = '^/?((?:' . $wpContentRel . '|' . $uploadsRel . '/.+)';
184
- }
185
- }
186
- }
187
-
188
- $rules = '';
189
-
190
-
191
- /*
192
- // The next line sets an environment variable.
193
- // On the options page, we verify if this is set to diagnose if "AllowOverride None" is presented in 'httpd.conf'
194
- //$rules .= "# The following SetEnv allows to diagnose if .htaccess files are turned off\n";
195
- //$rules .= "SetEnv HTACCESS on\n\n";
196
- */
197
- $rules .= "# The rules below are a result of the WebP Express options, Wordpress configuration and the following .htaccess capability tests:\n" .
198
- "# - mod_header working?: " . ($capTests['modHeaderWorking'] === true ? 'yes' : ($capTests['modHeaderWorking'] === false ? 'no' : 'could not be determined')) . "\n" .
199
- "# - pass variable from .htaccess to script through header working?: " . ($capTests['passThroughHeaderWorking'] === true ? 'yes' : ($capTests['passThroughHeaderWorking'] === false ? 'no' : 'could not be determined')) . "\n" .
200
- "# - pass variable from .htaccess to script through environment variable working?: " . ($capTests['passThroughEnvWorking'] === true ? 'yes' : ($capTests['passThroughEnvWorking'] === false ? 'no' : 'could not be determined')) . "\n";
201
-
202
- $rules .= "<IfModule mod_rewrite.c>\n" .
203
- " RewriteEngine On\n\n";
204
-
205
- $cacheDirRel = Paths::getCacheDirRel() . '/doc-root';
206
-
207
- $htaccessDirRel = '';
208
- switch ($htaccessDir) {
209
- case 'index':
210
- $htaccessDirRel = Paths::getIndexDirRel();
211
- break;
212
- case 'home':
213
- $htaccessDirRel = Paths::getHomeDirRel();
214
- break;
215
- case 'plugin':
216
- $htaccessDirRel = Paths::getPluginDirRel();
217
- break;
218
- case 'uploads':
219
- $htaccessDirRel = Paths::getUploadDirRel();
220
- break;
221
- case 'wp-content':
222
- $htaccessDirRel = Paths::getContentDirRel();
223
- break;
224
- }
225
-
226
-
227
- // TODO: Is it possible to handle when wp-content is outside document root?
228
-
229
- // TODO: It seems $pathToExisting needs to be adjusted, depending on where the .htaccess is located
230
- // Ie, if plugin folder has been moved out of ABSPATH, we should ie set
231
- // $pathToExisting to 'doc-root/plugins-moved/'
232
- // to get: RewriteRule ^\/?(.*)\.(jpe?g)$ /wp-content-moved/webp-express/webp-images/doc-root/plugins-moved/$1.$2.webp [NC,T=image/webp,E=WEBPACCEPT:1,E=EXISTING:1,L]
233
-
234
- // https://stackoverflow.com/questions/34124819/mod-rewrite-set-custom-header-through-htaccess
235
- $mingled = ($config['destination-folder'] == 'mingled');
236
-
237
- if ($config['redirect-to-existing-in-htaccess']) {
238
- if ($mingled) {
239
- $rules .= " # Redirect to existing converted image in same dir (if browser supports webp)\n";
240
- $rules .= " RewriteCond %{HTTP_ACCEPT} image/webp\n";
241
-
242
- if ($config['destination-extension'] == 'append') {
243
- $rules .= " RewriteCond %{DOCUMENT_ROOT}/" . $htaccessDirRel . "/$1.$2.webp -f\n";
244
- $rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . $fileExt . ")$ $1.$2.webp [T=image/webp,E=EXISTING:1," . ($addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
245
- } else {
246
- $rules .= " RewriteCond %{DOCUMENT_ROOT}/" . $htaccessDirRel . "/$1.webp -f\n";
247
- $rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . $fileExt . ")$ $1.webp [T=image/webp,E=EXISTING:1," . ($addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
248
- //$rules .= " RewriteRule ^(.+)\.(" . $fileExt . ")$ $1.webp [T=image/webp,E=EXISTING:1,L]\n\n";
249
- }
250
- }
251
-
252
- $rules .= " # Redirect to existing converted image in cache-dir (if browser supports webp)\n";
253
- $rules .= " RewriteCond %{HTTP_ACCEPT} image/webp\n";
254
- $rules .= " RewriteCond %{REQUEST_FILENAME} -f\n";
255
- $rules .= " RewriteCond %{DOCUMENT_ROOT}/" . $cacheDirRel . "/" . $htaccessDirRel . "/$1.$2.webp -f\n";
256
- $rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . $fileExt . ")$ /" . $cacheDirRel . "/" . $htaccessDirRel . "/$1.$2.webp [NC,T=image/webp,E=EXISTING:1,L]\n\n";
257
- //$rules .= " RewriteRule ^\/?(.*)\.(" . $fileExt . ")$ /" . $cacheDirRel . "/" . $htaccessDirRel . "/$1.$2.webp [NC,T=image/webp,E=EXISTING:1,L]\n\n";
258
-
259
- if ($addVary) {
260
- $rules .= " # Make sure that browsers which does not support webp also gets the Vary:Accept header\n" .
261
- " # when requesting images that would be redirected to existing webp on browsers that does.\n" .
262
- " <IfModule mod_setenvif.c>\n" .
263
- " SetEnvIf Request_URI \"\.(" . $fileExt . ")$\" ADDVARY\n" .
264
- " </IfModule>\n\n";
265
- }
266
-
267
- $rules .= $ccRules;
268
-
269
- }
270
-
271
- // Do not add header magic if passing through env is definitely working
272
- // Do not add either, if we definitily know it isn't working
273
- if ((!$passThrougEnvVarDefinitelyAvailable) && (!$passThroughHeaderDefinitelyUnavailable)) {
274
- if ($config['enable-redirection-to-converter']) {
275
- $rules .= " # Pass REQUEST_FILENAME to webp-on-demand.php in request header\n";
276
- //$rules .= $basicConditions;
277
- //$rules .= " RewriteRule ^(.*)\.(" . $fileExt . ")$ - [E=REQFN:%{REQUEST_FILENAME}]\n" .
278
- $rules .= " <IfModule mod_headers.c>\n" .
279
- " RequestHeader set REQFN \"%{REQFN}e\" env=REQFN\n" .
280
- " </IfModule>\n\n";
281
-
282
- }
283
- if ($config['enable-redirection-to-webp-realizer']) {
284
- // We haven't implemented a clever way to pass through header for webp-realizer yet
285
- }
286
- }
287
-
288
- if ($config['enable-redirection-to-webp-realizer']) {
289
- /*
290
- # Pass REQUEST_FILENAME to PHP in request header
291
- RewriteCond %{REQUEST_FILENAME} !-f
292
- RewriteCond %{DOCUMENT_ROOT}/wordpress/uploads-moved/$1 -f
293
- RewriteRule ^(.*)\.(webp)$ - [E=REQFN:%{REQUEST_FILENAME}]
294
- <IfModule mod_headers.c>
295
- RequestHeader set REQFN "%{REQFN}e" env=REQFN
296
- </IfModule>
297
-
298
- # WebP Realizer: Redirect non-existing webp images to converter when a corresponding jpeg/png is found
299
- RewriteCond %{REQUEST_FILENAME} !-f
300
- RewriteCond %{DOCUMENT_ROOT}/wordpress/uploads-moved/$1 -f
301
- RewriteRule ^(.*)\.(webp)$ /plugins-moved/webp-express/wod/webp-realizer.php?wp-content=wp-content-moved [NC,L]
302
- */
303
-
304
- $basicConditionsRealizer = '';
305
- $basicConditionsRealizer .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
306
- if ($mingled) {
307
- if ($config['destination-extension'] == 'append') {
308
- $basicConditionsRealizer .= " RewriteCond %{DOCUMENT_ROOT}/" . $htaccessDirRel . "/$1 -f\n";
309
- } else {
310
- //$basicConditionsRealizer .= " RewriteCond %{DOCUMENT_ROOT}/" . $htaccessDirRel . "/$1.webp !-f\n";
311
- }
312
- } else {
313
- //$basicConditionsRealizer .= " RewriteCond %{DOCUMENT_ROOT}/" . $cacheDirRel . "/" . $htaccessDirRel . "/$1.$2.webp !-f\n";
314
- }
315
-
316
- $rules .= " # WebP Realizer: Redirect non-existing webp images to webp-realizer.php, which will locate corresponding jpg/png, convert it, and deliver the webp (if possible) \n";
317
- $rules .= $basicConditionsRealizer;
318
-
319
- /*
320
- $rules .= " RewriteRule " . $rewriteRuleStart . "\.(webp)$ " .
321
- "/" . Paths::getWebPRealizerUrlPath() .
322
- ($passFullFilePathInQS ? "?xdestination=x%{SCRIPT_FILENAME}&" : "?") .
323
- "wp-content=" . Paths::getContentDirRel() .
324
- " [" . ($setEnvVar ? ('E=REQFN:%{REQUEST_FILENAME}' . ','): '') . "NC,L]\n\n"; // E=WOD:1
325
- */
326
- $params = [];
327
- if ($passFullFilePathInQSRealizer) {
328
- $params[] = 'xdestination=x%{SCRIPT_FILENAME}';
329
- } elseif ($passRelativeFilePathInQSRealizer) {
330
- $params[] = 'xdestination-rel=x' . $htaccessDirRel . '/$1.$2';
331
- }
332
- if (!$passThrougEnvVarDefinitelyAvailable) {
333
- $params[] = "wp-content=" . Paths::getContentDirRel();
334
- }
335
-
336
- // TODO: When $rewriteRuleStart is empty, we don't need the .*, do we? - test
337
- $rules .= " RewriteRule " . $rewriteRuleStart . "\.(webp)$ " .
338
- "/" . Paths::getWebPRealizerUrlPath() .
339
- ((count($params) > 0) ? "?" . implode('&', $params) : '') .
340
- " [" . ($setEnvVar ? ('E=DESTINATIONREL:' . $htaccessDirRel . '/$0' . ','): '') . (!$passThrougEnvVarDefinitelyUnavailable ? 'E=WPCONTENT:' . Paths::getContentDirRel() . ',' : '') . "NC,L]\n\n"; // E=WOD:1
341
-
342
-
343
- if (!$config['redirect-to-existing-in-htaccess']) {
344
- $rules .= $ccRules;
345
- }
346
- }
347
-
348
- if ($config['enable-redirection-to-converter']) {
349
- $basicConditions = '';
350
- if ($config['only-redirect-to-converter-for-webp-enabled-browsers']) {
351
- $basicConditions = " RewriteCond %{HTTP_ACCEPT} image/webp\n";
352
- }
353
- $basicConditions .= " RewriteCond %{REQUEST_FILENAME} -f\n";
354
- if ($config['only-redirect-to-converter-on-cache-miss']) {
355
- if ($mingled) {
356
- if ($config['destination-extension'] == 'append') {
357
- $basicConditions .= " RewriteCond %{DOCUMENT_ROOT}/" . $htaccessDirRel . "/$1.$2.webp !-f\n";
358
- } else {
359
- $basicConditions .= " RewriteCond %{DOCUMENT_ROOT}/" . $htaccessDirRel . "/$1.webp !-f\n";
360
- }
361
- } else {
362
- $basicConditions .= " RewriteCond %{DOCUMENT_ROOT}/" . $cacheDirRel . "/" . $htaccessDirRel . "/$1.$2.webp !-f\n";
363
- }
364
- }
365
-
366
- $rules .= " # Redirect images to webp-on-demand.php ";
367
- if ($config['only-redirect-to-converter-for-webp-enabled-browsers']) {
368
- $rules .= "(if browser supports webp)\n";
369
- } else {
370
- $rules .= "(regardless whether browser supports webp or not!)\n";
371
- }
372
- if ($config['only-redirect-to-converter-on-cache-miss']) {
373
- $rules .= " # - but only, when no existing converted image is found\n";
374
- }
375
- $rules .= $basicConditions;
376
-
377
- if ($config['forward-query-string']) {
378
- $rules .= " RewriteCond %{QUERY_STRING} (.*)\n";
379
- }
380
- /*
381
- if ($config['forward-query-string']) {
382
- }*/
383
-
384
-
385
- // TODO:
386
- // Add "NE" flag?
387
- // https://github.com/rosell-dk/webp-convert/issues/95
388
- // (and try testing spaces in directory paths)
389
-
390
- $params = [];
391
- if ($passFullFilePathInQS) {
392
- $params[] = 'xsource=x%{SCRIPT_FILENAME}';
393
- } elseif ($passRelativeFilePathInQS) {
394
- $params[] = 'xsource-rel=x' . $htaccessDirRel . '/$1.$2';
395
- }
396
- if (!$passThrougEnvVarDefinitelyAvailable) {
397
- $params[] = "wp-content=" . Paths::getContentDirRel();
398
- }
399
- if ($config['forward-query-string']) {
400
- $params[] = '%1';
401
- }
402
-
403
- // TODO: When $rewriteRuleStart is empty, we don't need the .*, do we? - test
404
- $rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . $fileExt . ")$ " .
405
- "/" . Paths::getWodUrlPath() .
406
- "?" . implode('&', $params) .
407
- " [" . ($setEnvVar ? ('E=REQFN:%{REQUEST_FILENAME},'): '') . (!$passThrougEnvVarDefinitelyUnavailable ? 'E=WPCONTENT:' . Paths::getContentDirRel() . ',' : '') . "NC,L]\n"; // E=WOD:1
408
-
409
- $rules .= "\n";
410
- }
411
-
412
- //$addVary = ($config['enable-redirection-to-converter'] && ($config['success-response'] == 'converted')) || ($config['redirect-to-existing-in-htaccess']);
413
-
414
- if ($addVary) {
415
- $rules .= " <IfModule mod_headers.c>\n";
416
- $rules .= " <IfModule mod_setenvif.c>\n";
417
-
418
- $rules .= " # Apache appends \"REDIRECT_\" in front of the environment variables defined in mod_rewrite, but LiteSpeed does not.\n" .
419
- " # So, the next lines are for Apache, in order to set environment variables without \"REDIRECT_\"\n" .
420
- " SetEnvIf REDIRECT_EXISTING 1 EXISTING=1\n" .
421
- " SetEnvIf REDIRECT_ADDVARY 1 ADDVARY=1\n\n";
422
-
423
- $rules .= " # Set Vary:Accept header for the image types handled by WebP Express.\n" .
424
- " # The purpose is to make proxies and CDNs aware that the response varies with the Accept header. \n" .
425
- " Header append \"Vary\" \"Accept\" env=ADDVARY\n\n";
426
-
427
- if ($config['redirect-to-existing-in-htaccess']) {
428
- $rules .= " # Set X-WebP-Express header for diagnose purposes\n" .
429
- //" SetEnvIf REDIRECT_WOD 1 WOD=1\n\n" .
430
- //" # Set the debug header\n" .
431
- " Header set \"X-WebP-Express\" \"Redirected directly to existing webp\" env=EXISTING\n";
432
- //" Header set \"X-WebP-Express\" \"Redirected to image converter\" env=WOD\n" .
433
- }
434
- $rules .= " </IfModule>\n" .
435
- " </IfModule>\n\n";
436
- }
437
- $rules .="</IfModule>\n";
438
-
439
- /*if ($config['redirect-to-existing-in-htaccess']) {
440
- $rules .=
441
- "<IfModule mod_headers.c>\n" .
442
- " # Append Vary Accept header, when the rules above are redirecting to existing webp\n" .
443
- " # or existing jpg" .
444
-
445
- " # Apache appends \"REDIRECT_\" in front of the environment variables, but LiteSpeed does not.\n" .
446
- " # These next line is for Apache, in order to set environment variables without \"REDIRECT_\"\n" .
447
- " SetEnvIf REDIRECT_WEBPACCEPT 1 WEBPACCEPT=1\n\n" .
448
-
449
- " # Make CDN caching possible.\n" .
450
- " # The effect is that the CDN will cache both the webp image and the jpeg/png image and return the proper\n" .
451
- " # image to the proper clients (for this to work, make sure to set up CDN to forward the \"Accept\" header)\n" .
452
- " Header append Vary Accept env=WEBPACCEPT\n" .
453
- "</IfModule>\n\n";
454
- }*/
455
-
456
- $rules .= "<IfModule mod_mime.c>\n";
457
- $rules .= " AddType image/webp .webp\n";
458
- $rules .= "</IfModule>\n";
459
- return $rules;
460
- }
461
-
462
- /* only called from page-messages.inc, but commented out there... */
463
- public static function generateHTAccessRulesFromConfigFile($htaccessDir = '') {
464
- if (Config::isConfigFileThereAndOk()) {
465
- return self::generateHTAccessRulesFromConfigObj(Config::loadConfig(), $htaccessDir);
466
- } else {
467
- return false;
468
- }
469
- }
470
-
471
  public static function arePathsUsedInHTAccessOutdated() {
472
  if (!Config::isConfigFileThere()) {
473
  // this properly means that rewrite rules have never been generated
@@ -475,9 +17,7 @@ class HTAccess
475
  }
476
 
477
  $pathsGoingToBeUsedInHtaccess = [
478
- 'existing' => Paths::getPathToExisting(),
479
  'wod-url-path' => Paths::getWodUrlPath(),
480
- 'config-dir-rel' => Paths::getConfigDirRel()
481
  ];
482
 
483
  $config = Config::loadConfig();
@@ -487,8 +27,10 @@ class HTAccess
487
  }
488
 
489
  foreach ($config['paths-used-in-htaccess'] as $prop => $value) {
490
- if ($value != $pathsGoingToBeUsedInHtaccess[$prop]) {
491
- return true;
 
 
492
  }
493
  }
494
  }
@@ -516,13 +58,19 @@ class HTAccess
516
  'cache-control-max-age' => 'one-week',
517
  'cache-control-public' => true,
518
  'enable-redirection-to-webp-realizer' => false,
519
- 'enable-redirection-to-converter' => true
 
 
 
 
520
  ];
521
 
 
522
  if (isset($newConfig['redirect-to-existing-in-htaccess']) && $newConfig['redirect-to-existing-in-htaccess']) {
523
  $propsToCompare['destination-folder'] = 'separate';
524
  $propsToCompare['destination-extension'] = 'append';
525
- }
 
526
 
527
  foreach ($propsToCompare as $prop => $behaviourBeforeIntroduced) {
528
  if (!isset($newConfig[$prop])) {
@@ -587,15 +135,13 @@ class HTAccess
587
 
588
 
589
  /**
590
- * Sneak peak into .htaccess to see if we have rules in it
591
- * This may not be possible (it requires read permission)
592
- * Return true, false, or null if we just can't tell
593
  */
594
- public static function haveWeRulesInThisHTAccess($filename) {
595
  if (FileHelper::fileExists($filename)) {
596
  $content = FileHelper::loadFile($filename);
597
  if ($content === false) {
598
- return null;
599
  }
600
 
601
  $pos1 = strpos($content, '# BEGIN WebP Express');
@@ -606,11 +152,30 @@ class HTAccess
606
  if ($pos2 === false) {
607
  return false;
608
  }
 
 
 
 
 
 
609
 
610
- $weRules = substr($content, $pos1, $pos2 - $pos1);
611
-
612
- return (strpos($weRules, '<IfModule mod_rewrite.c>') !== false);
 
 
 
 
 
 
 
 
 
 
 
 
613
 
 
614
  } else {
615
  // the .htaccess isn't even there. So there are no rules.
616
  return false;
@@ -634,6 +199,21 @@ class HTAccess
634
  }
635
  }
636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  public static function saveHTAccessRulesToFile($filename, $rules, $createIfMissing = false) {
638
  if (!@file_exists($filename)) {
639
  if (!$createIfMissing) {
@@ -696,6 +276,11 @@ class HTAccess
696
  return $success;
697
  }
698
 
 
 
 
 
 
699
  /* only called in this file */
700
  public static function saveHTAccessRulesToFirstWritableHTAccessDir($dirs, $rules)
701
  {
@@ -712,35 +297,35 @@ class HTAccess
712
  * Try to deactivate all .htaccess rules.
713
  * If success, we return true.
714
  * If we fail, we return an array of filenames that have problems
 
715
  */
716
- public static function deactivateHTAccessRules() {
717
- //return self::saveHTAccessRules('# Plugin is deactivated');
718
- $indexDir = Paths::getIndexDirAbs();
719
- $homeDir = Paths::getHomeDirAbs();
720
- $wpContentDir = Paths::getContentDirAbs();
721
- $pluginDir = Paths::getPluginDirAbs();
722
- $uploadDir = Paths::getUploadDirAbs();
723
-
724
- $dirsToClean = [$indexDir, $homeDir, $wpContentDir, $pluginDir, $uploadDir];
725
 
 
 
726
  $failures = [];
 
727
 
728
- foreach ($dirsToClean as $dir) {
 
729
  $filename = $dir . '/.htaccess';
730
  if (!FileHelper::fileExists($filename)) {
 
731
  continue;
732
  } else {
733
  if (self::haveWeRulesInThisHTAccessBestGuess($filename)) {
734
- if (!self::saveHTAccessRulesToFile($filename, '# Plugin is deactivated', false)) {
735
- $failures[] = $filename;
 
 
736
  }
 
 
737
  }
738
  }
739
  }
740
- if (count($failures) == 0) {
741
- return true;
742
- }
743
- return $failures;
744
  }
745
 
746
  public static function testLinks($config) {
@@ -789,6 +374,11 @@ class HTAccess
789
  $uploadToo = 'depends';
790
  }
791
 
 
 
 
 
 
792
  return [
793
  $minRequired,
794
  $pluginToo, // 'yes', 'no' or 'depends'
@@ -796,93 +386,58 @@ class HTAccess
796
  ];
797
  }
798
 
799
- /**
800
- * Try to save the rules.
801
- * Returns many details
802
- * (called from migrate1.php, reactivate.php, Config.php and this file)
803
- */
804
  public static function saveRules($config) {
 
805
 
 
 
 
 
806
 
807
- list($minRequired, $pluginToo, $uploadToo) = self::getHTAccessDirRequirements();
 
 
 
 
 
808
 
809
- $rules = HTAccess::generateHTAccessRulesFromConfigObj($config, 'wp-content');
810
- $wpContentDir = Paths::getContentDirAbs();
811
- $wpContentFailed = !(HTAccess::saveHTAccessRulesToFile($wpContentDir . '/.htaccess', $rules, true));
812
 
813
- $overidingRulesInWpContentWarning = false;
814
- if ($wpContentFailed) {
815
- if ($minRequired == 'index') {
816
- $rules = HTAccess::generateHTAccessRulesFromConfigObj($config, 'index');
817
- $indexFailed = !(HTAccess::saveHTAccessRulesToFile(Paths::getIndexDirAbs() . '/.htaccess', $rules, true));
818
 
819
- if ($indexFailed) {
820
- $mainResult = 'failed';
821
- } else {
822
- $mainResult = 'index';
823
- $overidingRulesInWpContentWarning = self::haveWeRulesInThisHTAccessBestGuess($wpContentDir . '/.htaccess');
824
- }
825
- }
826
- } else {
827
- $mainResult = 'wp-content';
828
- // TODO: Change to something like "The rules are placed in the .htaccess file in your wp-content dir."
829
- // BUT! - current text is searched for in page-messages.php
830
- HTAccess::saveHTAccessRulesToFile(Paths::getIndexDirAbs() . '/.htaccess', '# WebP Express has placed its rules in your wp-content dir. Go there.', false);
831
- }
832
 
833
- /* plugin */
834
- if ($pluginToo == 'depends') {
835
- if ($mainResult == 'wp-content') {
836
- $pluginToo = (Paths::isPluginDirMovedOutOfWpContent() ? 'yes' : 'no');
837
- } elseif ($mainResult == 'index') {
838
- $pluginToo = (Paths::isPluginDirMovedOutOfAbsPath() ? 'yes' : 'no');
839
- } else {
840
- // $result must be false. So $pluginToo should still be 'depends'
841
- }
842
- }
843
- $pluginFailed = false;
844
- $pluginFailedBadly = true;
845
- if ($pluginToo == 'yes') {
846
- $rules = HTAccess::generateHTAccessRulesFromConfigObj($config, 'plugin');
847
- $pluginDir = Paths::getPluginDirAbs();
848
- $pluginFailed = !(HTAccess::saveHTAccessRulesToFile($pluginDir . '/.htaccess', $rules, true));
849
- if ($pluginFailed) {
850
- $pluginFailedBadly = self::haveWeRulesInThisHTAccessBestGuess($pluginDir . '/.htaccess');
851
- }
852
- }
853
 
854
- /* upload */
855
- if ($uploadToo == 'depends') {
856
- if ($mainResult == 'wp-content') {
857
- $uploadToo = (Paths::isUploadDirMovedOutOfWPContentDir() ? 'yes' : 'no');
858
- } elseif ($mainResult == 'index') {
859
- $uploadToo = (Paths::isUploadDirMovedOutOfAbsPath() ? 'yes' : 'no');
860
  } else {
861
- // $result must be false. So $uploadToo should still be 'depends'
862
- }
863
- }
864
- $uploadFailed = false;
865
- $uploadFailedBadly = true;
866
- if ($uploadToo == 'yes') {
867
- $uploadDir = Paths::getUploadDirAbs();
868
- $rules = HTAccess::generateHTAccessRulesFromConfigObj($config, 'uploads');
869
- $uploadFailed = !(HTAccess::saveHTAccessRulesToFile($uploadDir . '/.htaccess', $rules, true));
870
- if ($uploadFailed) {
871
- $uploadFailedBadly = self::haveWeRulesInThisHTAccessBestGuess($uploadDir . '/.htaccess');
872
  }
873
  }
874
 
875
- return [
876
- 'mainResult' => $mainResult, // 'index', 'wp-content' or 'failed'
877
- 'minRequired' => $minRequired, // 'index' or 'wp-content'
878
- 'overidingRulesInWpContentWarning' => $overidingRulesInWpContentWarning, // true if main result is 'index' but we cannot remove those in wp-content
879
- 'rules' => $rules, // The rules we generated
880
- 'pluginToo' => $pluginToo, // 'yes', 'no' or 'depends'
881
- 'pluginFailed' => $pluginFailed, // true if failed to write to plugin folder (it only tries that, if pluginToo == 'yes')
882
- 'pluginFailedBadly' => $pluginFailedBadly, // true if plugin failed AND it seems we have rewrite rules there
883
- 'uploadToo' => $uploadToo, // 'yes', 'no' or 'depends'
884
- 'uploadFailed' => $uploadFailed,
885
- 'uploadFailedBadly' => $uploadFailedBadly,
886
- ];
887
  }
 
888
  }
4
 
5
  use \WebPExpress\Config;
6
  use \WebPExpress\FileHelper;
7
+ use \WebPExpress\HTAccessRules;
8
  use \WebPExpress\Paths;
9
  use \WebPExpress\State;
10
 
11
  class HTAccess
12
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  public static function arePathsUsedInHTAccessOutdated() {
14
  if (!Config::isConfigFileThere()) {
15
  // this properly means that rewrite rules have never been generated
17
  }
18
 
19
  $pathsGoingToBeUsedInHtaccess = [
 
20
  'wod-url-path' => Paths::getWodUrlPath(),
 
21
  ];
22
 
23
  $config = Config::loadConfig();
27
  }
28
 
29
  foreach ($config['paths-used-in-htaccess'] as $prop => $value) {
30
+ if (isset($pathsGoingToBeUsedInHtaccess[$prop])) {
31
+ if ($value != $pathsGoingToBeUsedInHtaccess[$prop]) {
32
+ return true;
33
+ }
34
  }
35
  }
36
  }
58
  'cache-control-max-age' => 'one-week',
59
  'cache-control-public' => true,
60
  'enable-redirection-to-webp-realizer' => false,
61
+ 'enable-redirection-to-converter' => true,
62
+ 'destination-folder' => 'separate',
63
+ 'destination-extension' => 'append',
64
+ 'destination-structure' => 'doc-root',
65
+ 'scope' => ['themes', 'uploads']
66
  ];
67
 
68
+ /*
69
  if (isset($newConfig['redirect-to-existing-in-htaccess']) && $newConfig['redirect-to-existing-in-htaccess']) {
70
  $propsToCompare['destination-folder'] = 'separate';
71
  $propsToCompare['destination-extension'] = 'append';
72
+ $propsToCompare['destination-structure'] = 'doc-root';
73
+ }*/
74
 
75
  foreach ($propsToCompare as $prop => $behaviourBeforeIntroduced) {
76
  if (!isset($newConfig[$prop])) {
135
 
136
 
137
  /**
138
+ * @return string|false Rules, or false if no rules found or file does not exist.
 
 
139
  */
140
+ public static function extractWebPExpressRulesFromHTAccess($filename) {
141
  if (FileHelper::fileExists($filename)) {
142
  $content = FileHelper::loadFile($filename);
143
  if ($content === false) {
144
+ return false;
145
  }
146
 
147
  $pos1 = strpos($content, '# BEGIN WebP Express');
152
  if ($pos2 === false) {
153
  return false;
154
  }
155
+ return substr($content, $pos1, $pos2 - $pos1);
156
+ } else {
157
+ // the .htaccess isn't even there. So there are no rules.
158
+ return false;
159
+ }
160
+ }
161
 
162
+ /**
163
+ * Sneak peak into .htaccess to see if we have rules in it
164
+ * This may not be possible (it requires read permission)
165
+ * Return true, false, or null if we just can't tell
166
+ */
167
+ public static function haveWeRulesInThisHTAccess($filename) {
168
+ if (FileHelper::fileExists($filename)) {
169
+ $content = FileHelper::loadFile($filename);
170
+ if ($content === false) {
171
+ return null;
172
+ }
173
+ $weRules = (self::extractWebPExpressRulesFromHTAccess($filename));
174
+ if ($weRules === false) {
175
+ return false;
176
+ }
177
 
178
+ return (strpos($weRules, '<IfModule ') !== false);
179
  } else {
180
  // the .htaccess isn't even there. So there are no rules.
181
  return false;
199
  }
200
  }
201
 
202
+ public static function getRootsWithWebPExpressRulesIn()
203
+ {
204
+ $allIds = Paths::getImageRootIds();
205
+ $allIds[] = 'cache';
206
+ $result = [];
207
+ foreach ($allIds as $imageRootId) {
208
+ $filename = Paths::getAbsDirById($imageRootId) . '/.htaccess';
209
+ if (self::haveWeRulesInThisHTAccessBestGuess($filename)) {
210
+ $result[] = $imageRootId;
211
+ }
212
+
213
+ }
214
+ return $result;
215
+ }
216
+
217
  public static function saveHTAccessRulesToFile($filename, $rules, $createIfMissing = false) {
218
  if (!@file_exists($filename)) {
219
  if (!$createIfMissing) {
276
  return $success;
277
  }
278
 
279
+ public static function saveHTAccessRules($rootId, $rules, $createIfMissing = true) {
280
+ $filename = Paths::getAbsDirById($rootId) . '/.htaccess';
281
+ return self::saveHTAccessRulesToFile($filename, $rules, $createIfMissing);
282
+ }
283
+
284
  /* only called in this file */
285
  public static function saveHTAccessRulesToFirstWritableHTAccessDir($dirs, $rules)
286
  {
297
  * Try to deactivate all .htaccess rules.
298
  * If success, we return true.
299
  * If we fail, we return an array of filenames that have problems
300
+ * @return true|array
301
  */
302
+ public static function deactivateHTAccessRules($comment = '# Plugin is deactivated') {
 
 
 
 
 
 
 
 
303
 
304
+ $rootsToClean = Paths::getImageRootIds();
305
+ $rootsToClean[] = 'home';
306
  $failures = [];
307
+ $successes = [];
308
 
309
+ foreach ($rootsToClean as $imageRootId) {
310
+ $dir = Paths::getAbsDirById($imageRootId);
311
  $filename = $dir . '/.htaccess';
312
  if (!FileHelper::fileExists($filename)) {
313
+ //error_log('exists not:' . $filename);
314
  continue;
315
  } else {
316
  if (self::haveWeRulesInThisHTAccessBestGuess($filename)) {
317
+ if (self::saveHTAccessRulesToFile($filename, $comment, false)) {
318
+ $successes[] = $imageRootId;
319
+ } else {
320
+ $failures[] = $imageRootId;
321
  }
322
+ } else {
323
+ //error_log('no rules:' . $filename);
324
  }
325
  }
326
  }
327
+ $success = (count($failures) == 0);
328
+ return [$success, $failures, $successes];
 
 
329
  }
330
 
331
  public static function testLinks($config) {
374
  $uploadToo = 'depends';
375
  }
376
 
377
+ // We need upload too for rewrite rules when destination structure is image-roots.
378
+ // but it is also good otherwise. So lets always do it.
379
+
380
+ $uploadToo = 'yes';
381
+
382
  return [
383
  $minRequired,
384
  $pluginToo, // 'yes', 'no' or 'depends'
386
  ];
387
  }
388
 
 
 
 
 
 
389
  public static function saveRules($config) {
390
+ list($success, $failedDeactivations, $successfulDeactivations) = self::deactivateHTAccessRules('# The rules have left the building');
391
 
392
+ $rootsToPutRewritesIn = $config['scope'];
393
+ if ($config['destination-structure'] == 'doc-root') {
394
+ $rootsToPutRewritesIn = Paths::filterOutSubRoots($rootsToPutRewritesIn);
395
+ }
396
 
397
+ $dirsContainingWebps = [];
398
+ $mingled = ($config['destination-folder'] == 'mingled');
399
+ if ($mingled) {
400
+ $dirsContainingWebps[] = 'uploads';
401
+ }
402
+ $scopeOtherThanUpload = (str_replace('uploads', '', implode(',', $config['scope'])) != '');
403
 
404
+ if ($scopeOtherThanUpload || (!$mingled)) {
405
+ $dirsContainingWebps[] = 'cache';
406
+ }
407
 
408
+ $dirsToPutRewritesIn = array_merge($rootsToPutRewritesIn, $dirsContainingWebps);
 
 
 
 
409
 
410
+ $failedWrites = [];
411
+ $successfullWrites = [];
412
+ foreach ($dirsToPutRewritesIn as $rootId) {
413
+ $dirContainsSourceImages = in_array($rootId, $rootsToPutRewritesIn);
414
+ $dirContainsWebPImages = in_array($rootId, $dirsContainingWebps);
 
 
 
 
 
 
 
 
415
 
416
+ $rules = HTAccessRules::generateHTAccessRulesFromConfigObj($config, $rootId, $dirContainsSourceImages, $dirContainsWebPImages);
417
+ $success = self::saveHTAccessRules(
418
+ $rootId,
419
+ $rules,
420
+ true
421
+ );
422
+ if ($success) {
423
+ $successfullWrites[] = $rootId;
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
+ // Remove it from $successfulDeactivations (if it is there)
426
+ if (($key = array_search($rootId, $successfulDeactivations)) !== false) {
427
+ unset($successfulDeactivations[$key]);
428
+ }
 
 
429
  } else {
430
+ $failedWrites[] = $rootId;
431
+
432
+ // Remove it from $failedDeactivations (if it is there)
433
+ if (($key = array_search($rootId, $failedDeactivations)) !== false) {
434
+ unset($failedDeactivations[$key]);
435
+ }
 
 
 
 
 
436
  }
437
  }
438
 
439
+ $success = ((count($failedDeactivations) == 0) && (count($failedWrites) == 0));
440
+ return [$success, $successfullWrites, $successfulDeactivations, $failedWrites, $failedDeactivations];
 
 
 
 
 
 
 
 
 
 
441
  }
442
+
443
  }
lib/classes/HTAccessRules.php ADDED
@@ -0,0 +1,855 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ class HTAccessRules
6
+ {
7
+ private static $useDocRootForStructuringCacheDir;
8
+ private static $config;
9
+ private static $mingled;
10
+ private static $appendWebP;
11
+ private static $imageTypes;
12
+ private static $fileExt;
13
+ private static $fileExtensions;
14
+ private static $fileExtIncludingDot;
15
+ private static $htaccessDir;
16
+ private static $htaccessDirRelToDocRoot;
17
+ private static $htaccessDirAbs;
18
+ private static $modHeaderDefinitelyUnavailable;
19
+ private static $passThroughHeaderDefinitelyUnavailable;
20
+ private static $passThroughHeaderDefinitelyAvailable;
21
+ private static $passThroughEnvVarDefinitelyUnavailable;
22
+ private static $passThroughEnvVarDefinitelyAvailable;
23
+ private static $capTests;
24
+ private static $addVary;
25
+ private static $dirContainsSourceImages;
26
+ private static $dirContainsWebPImages;
27
+
28
+
29
+ /**
30
+ * @return string Info in comments.
31
+ */
32
+ private static function infoRules()
33
+ {
34
+
35
+ return "# The rules below is a result of many parameters, including the following:\n" .
36
+ "#\n# WebP Express options:\n" .
37
+ "# - Redirection to existing webp: " .
38
+ (self::$config['redirect-to-existing-in-htaccess'] ? 'enabled' : 'disabled') . "\n" .
39
+ "# - Redirection to converter: " .
40
+ (self::$config['enable-redirection-to-converter'] ? 'enabled' : 'disabled') . "\n" .
41
+ "# - Redirection to converter to create missing webp files upon request for the webp: " .
42
+ (self::$config['enable-redirection-to-webp-realizer'] ? 'enabled' : 'disabled') . "\n" .
43
+
44
+ "# - Destination folder: " . self::$config['destination-folder'] . "\n" .
45
+ "# - Destination extension: " . self::$config['destination-extension'] . "\n" .
46
+ "# - Destination structure: " . self::$config['destination-structure'] . (((self::$config['destination-structure'] == 'doc-root') && (!self::$useDocRootForStructuringCacheDir)) ? ' (overruled!)' : '') . "\n" .
47
+ "# - Image types: " . str_replace('?', '', implode(', ', self::$fileExtensions)) . "\n" .
48
+
49
+ "#\n# Wordpress/Server configuration:\n" .
50
+ '# - Document root availablity: ' . Paths::docRootStatusText() . "\n" .
51
+
52
+ "#\n# .htaccess capability test results:\n" .
53
+ "# - mod_header working?: " .
54
+ (self::$capTests['modHeaderWorking'] === true ? 'yes' : (self::$capTests['modHeaderWorking'] === false ? 'no' : 'could not be determined')) . "\n" .
55
+ "# - pass variable from .htaccess to script through header working?: " .
56
+ (self::$capTests['passThroughHeaderWorking'] === true ? 'yes' : (self::$capTests['passThroughHeaderWorking'] === false ? 'no' : 'could not be determined')) . "\n" .
57
+ "# - pass variable from .htaccess to script through environment variable working?: " .
58
+ (self::$capTests['passThroughEnvWorking'] === true ? 'yes' : (self::$capTests['passThroughEnvWorking'] === false ? 'no' : 'could not be determined')) . "\n" .
59
+
60
+ "#\n# Role of the dir that this .htaccess is located in:\n" .
61
+ '# - Is this .htaccess in a dir containing source images?: ' . (self::$dirContainsSourceImages ? 'yes' : 'no') . "\n" .
62
+ '# - Is this .htaccess in a dir containing webp images?: ' . (self::$dirContainsWebPImages ? 'yes' : 'no') . "\n" .
63
+ "\n";
64
+ }
65
+
66
+ /**
67
+ * @return string rules for cache control
68
+ */
69
+ private static function cacheRules()
70
+ {
71
+ // Build cache control rules
72
+ $ccRules = '';
73
+ $cacheControlHeader = Config::getCacheControlHeader(self::$config);
74
+ if ($cacheControlHeader != '') {
75
+ $ccRules .= "# Set Cache-Control header for requests to webp images\n";
76
+ $ccRules .= "<IfModule mod_headers.c>\n";
77
+ $ccRules .= " <FilesMatch \"\.webp$\">\n";
78
+ $ccRules .= " Header set Cache-Control \"" . $cacheControlHeader . "\"\n";
79
+ $ccRules .= " </FilesMatch>\n";
80
+ $ccRules .= "</IfModule>\n\n";
81
+
82
+ // Fall back to mod_expires if mod_headers is unavailable
83
+ if (self::$modHeaderDefinitelyUnavailable) {
84
+ $cacheControl = self::$config['cache-control'];
85
+
86
+ $expires = '';
87
+ if ($cacheControl == 'custom') {
88
+ $expires = '';
89
+
90
+ // Do not add Expire header if private is set
91
+ // - because then the user don't want caching in proxies / CDNs.
92
+ // the Expires header doesn't differentiate between private/public
93
+ if (!(preg_match('/private/', self::$config['cache-control-custom']))) {
94
+ if (preg_match('/max-age=(\d+)/', self::$config['cache-control-custom'], $matches)) {
95
+ if (isset($matches[1])) {
96
+ $expires = $matches[1] . ' seconds';
97
+ }
98
+ }
99
+ }
100
+
101
+ } elseif ($cacheControl == 'no-header') {
102
+ $expires = '';
103
+ } elseif ($cacheControl == 'set') {
104
+ if (self::$config['cache-control-public']) {
105
+ $cacheControlOptions = [
106
+ 'no-header' => '',
107
+ 'one-second' => '1 seconds',
108
+ 'one-minute' => '1 minutes',
109
+ 'one-hour' => '1 hours',
110
+ 'one-day' => '1 days',
111
+ 'one-week' => '1 weeks',
112
+ 'one-month' => '1 months',
113
+ 'one-year' => '1 years',
114
+ ];
115
+ $expires = $cacheControlOptions[self::$config['cache-control-max-age']];
116
+ }
117
+ }
118
+
119
+ if ($expires != '') {
120
+ // in case mod_headers is missing, try mod_expires
121
+ $ccRules .= "# Fall back to mod_expires if mod_headers is unavailable\n";
122
+ $ccRules .= "<IfModule !mod_headers.c>\n";
123
+ $ccRules .= " <IfModule mod_expires.c>\n";
124
+ $ccRules .= " ExpiresActive On\n";
125
+ $ccRules .= " ExpiresByType image/webp \"access plus " . $expires . "\"\n";
126
+ $ccRules .= " </IfModule>\n";
127
+ $ccRules .= "</IfModule>\n\n";
128
+ }
129
+ }
130
+ }
131
+ return $ccRules;
132
+ }
133
+
134
+ /**
135
+ * @return string rules for redirecting to existing
136
+ */
137
+ private static function redirectToExistingRules()
138
+ {
139
+ $rules = '';
140
+
141
+
142
+ if (self::$mingled) {
143
+ // TODO:
144
+ // Only write mingled rules for "uploads" dir.
145
+ // - UNLESS no .htaccess has been placed in uploads dir (is unwritable) (in that case also write for wp-content / index)
146
+ // (self::$htaccessDir == 'uploads')
147
+ $rules .= " # Redirect to existing converted image in same dir (if browser supports webp)\n";
148
+ $rules .= " RewriteCond %{HTTP_ACCEPT} image/webp\n";
149
+
150
+ if (self::$htaccessDir == 'index') {
151
+ // TODO: Add the following rule if configured to
152
+ if (false) {
153
+ // TODO: Full path to wp-admin from doc-root - if possible
154
+ // (that is: if document root is available).
155
+ // ie: RewriteCond %{REQUEST_URI} ^/?wordpress/wp-admin
156
+ $rules .= " RewriteCond %{REQUEST_URI} !wp-admin\n";
157
+ }
158
+ }
159
+
160
+ // $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(.*)(" . self::$fileExtIncludingDot . ")$\n";
161
+
162
+ // self::$appendWebP cannot be used, we need this:
163
+ // (because we are not sure there are a .htaccess in the uploads folder)
164
+ $appendWebP = !(self::$config['destination-extension'] == 'set');
165
+
166
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(.*)(" . self::$fileExtIncludingDot . ")$\n";
167
+ $rules .= " RewriteCond %1" . ($appendWebP ? "%2" : "") . "\.webp -f\n";
168
+ $rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ %1" . ($appendWebP ? "%2" : "") .
169
+ "\.webp [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
170
+
171
+ /*
172
+ if (self::$config['destination-extension'] == 'append') {
173
+ $rules .= " RewriteCond %{REQUEST_FILENAME}.webp -f\n";
174
+ //$rules .= " RewriteCond %{DOCUMENT_ROOT}/" . self::$htaccessDirRelToDocRoot . "/$1.$2.webp -f\n";
175
+ $rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . self::$fileExt . ")$ $1.$2.webp [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
176
+ } else {
177
+ // extension: set to webp
178
+
179
+ //$rules .= " RewriteCond %{DOCUMENT_ROOT}/" . self::$htaccessDirRelToDocRoot . "/$1.webp -f\n";
180
+ //$rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . self::$fileExt . ")$ $1.webp [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
181
+
182
+ // Got these new rules here: https://www.digitalocean.com/community/tutorials/how-to-create-and-serve-webp-images-to-speed-up-your-website
183
+ $rules .= " RewriteCond %{REQUEST_URI} (?i)(.*)(" . self::$fileExtIncludingDot . ")$\n";
184
+ $rules .= " RewriteCond %{DOCUMENT_ROOT}%1\.webp -f\n";
185
+ $rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ %1\.webp [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
186
+
187
+ // Instead of using REQUEST_URI, I can use REQUEST_FILENAME and remove DOCUMENT_ROOT
188
+ // I suppose REQUEST_URI is what was requested (ie "/wp-content/uploads/image.jpg").
189
+ // REQUEST_FILENAME is the filesystem path. (ie "/var/www/example.com/uploads-moved/image.jpg")
190
+ // But it cant be, because then the digitalocean solution would not work in above case.
191
+ // TODO: investigate
192
+
193
+ // RewriteRule (?i)(.*)(\.jpe?g|\.png)$ %1\.webp [T=image/webp,E=EXISTING:1,E=ADDVARY:1,L]
194
+ }
195
+ */
196
+ }
197
+
198
+ if (self::$htaccessDir != 'uploads') {
199
+ //return '# temporalily disabled';
200
+ }
201
+
202
+ // Redirect to existing converted image in cache-dir.
203
+ // Do not write these rules for uploads in mingled (there are no "uploads" images in cache-dir when in mingled mode)
204
+ if (!(self::$mingled && (self::$htaccessDir == 'uploads'))) {
205
+ $rules .= " # Redirect to existing converted image in cache-dir (if browser supports webp)\n";
206
+ $rules .= " RewriteCond %{HTTP_ACCEPT} image/webp\n";
207
+
208
+ if (self::$useDocRootForStructuringCacheDir) {
209
+ $cacheDirRel = Paths::getCacheDirRelToDocRoot() . '/doc-root';
210
+
211
+ $rules .= " RewriteCond %{REQUEST_FILENAME} -f\n";
212
+ $rules .= " RewriteCond %{DOCUMENT_ROOT}/" . $cacheDirRel . "/" . self::$htaccessDirRelToDocRoot . "/$1.$2.webp -f\n";
213
+ $rules .= " RewriteRule ^/?(.+)\.(" . self::$fileExt . ")$ /" . $cacheDirRel . "/" . self::$htaccessDirRelToDocRoot .
214
+ "/$1.$2.webp [NC,T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
215
+
216
+ } else {
217
+ // Make sure source image exists
218
+ $rules .= " RewriteCond %{REQUEST_FILENAME} -f\n";
219
+
220
+ // Find relative path of source (accessible as %2%3)
221
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(" . self::$htaccessDirAbs . "/)(.*)(" . self::$fileExtIncludingDot . ")$\n";
222
+
223
+ // Make sure there is a webp in the cache-dir
224
+ $cacheDirForThisRoot = Paths::getCacheDirForImageRoot(
225
+ self::$config['destination-folder'],
226
+ self::$config['destination-structure'],
227
+ self::$htaccessDir
228
+ );
229
+ $cacheDirForThisRoot = PathHelper::fixAbsPathToUseUnresolvedDocRoot($cacheDirForThisRoot);
230
+
231
+ $rules .= " RewriteCond " . $cacheDirForThisRoot . "/%2%3.webp -f\n";
232
+ //RewriteCond /var/www/webp-express-tests/we0/wp-content-moved/webp-express/webp-images/uploads/%2%3.webp -f
233
+
234
+ $urlPath = '/' . Paths::getContentUrlPath() . "/webp-express/webp-images/" . self::$htaccessDir . "/%2" . (self::$appendWebP ? "%3" : "") . "\.webp";
235
+ //$rules .= " RewriteCond %1" . (self::$appendWebP ? "%2" : "") . "\.webp -f\n";
236
+ $rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ " . $urlPath .
237
+ " [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
238
+ }
239
+
240
+ //$rules .= " RewriteRule ^\/?(.*)\.(" . self::$fileExt . ")$ /" . $cacheDirRel . "/" . self::$htaccessDirRelToDocRoot . "/$1.$2.webp [NC,T=image/webp,E=EXISTING:1,L]\n\n";
241
+ }
242
+
243
+ return $rules;
244
+ }
245
+
246
+ private static function webpRealizerRules()
247
+ {
248
+ /*
249
+ # Pass REQUEST_FILENAME to PHP in request header
250
+ RewriteCond %{REQUEST_FILENAME} !-f
251
+ RewriteCond %{DOCUMENT_ROOT}/wordpress/uploads-moved/$1 -f
252
+ RewriteRule ^(.*)\.(webp)$ - [E=REQFN:%{REQUEST_FILENAME}]
253
+ <IfModule mod_headers.c>
254
+ RequestHeader set REQFN "%{REQFN}e" env=REQFN
255
+ </IfModule>
256
+
257
+ # WebP Realizer: Redirect non-existing webp images to converter when a corresponding jpeg/png is found
258
+ RewriteCond %{REQUEST_FILENAME} !-f
259
+ RewriteCond %{DOCUMENT_ROOT}/wordpress/uploads-moved/$1 -f
260
+ RewriteRule ^(.*)\.(webp)$ /plugins-moved/webp-express/wod/webp-realizer.php?wp-content=wp-content-moved [NC,L]
261
+ */
262
+
263
+ $rules = '';
264
+ $rules .= "# WebP Realizer: Redirect non-existing webp images to webp-realizer.php, which will locate corresponding jpg/png, \n" .
265
+ "# convert it, and deliver the freshly converted webp\n";
266
+ $rules .= "<IfModule mod_rewrite.c>\n" .
267
+ " RewriteEngine On\n";
268
+
269
+
270
+ if (self::$useDocRootForStructuringCacheDir) {
271
+ /*
272
+ Generate something like this:
273
+
274
+ RewriteCond %{REQUEST_FILENAME} !-f
275
+ RewriteRule ^/?(.+)\.(webp)$ /plugins-moved/webp-express/wod/webp-realizer.php [E=DESTINATIONREL:wp-content-moved/$0,E=WPCONTENT:wp-content-moved,NC,L]
276
+ */
277
+ $rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
278
+ $params = [];
279
+ $flags = [];
280
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
281
+ $flags[] = 'E=DESTINATIONREL:' . self::$htaccessDirRelToDocRoot . '/$0';
282
+ }
283
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
284
+ $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel();
285
+ }
286
+ $flags[] = 'NC';
287
+ $flags[] = 'L';
288
+
289
+ $passRelativePathToSourceInQS = !(self::$passThroughEnvVarDefinitelyAvailable || self::$passThroughHeaderDefinitelyAvailable);
290
+ if ($passRelativePathToSourceInQS) {
291
+ $params[] = 'xdestination-rel=x' . self::$htaccessDirRelToDocRoot . '/$1.$2';
292
+ }
293
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
294
+ $params[] = "wp-content=" . Paths::getContentDirRel();
295
+ }
296
+
297
+ $rewriteRuleStart = '^/?(.+)';
298
+ $rules .= " RewriteRule " . $rewriteRuleStart . "\.(webp)$ " .
299
+ "/" . Paths::getWebPRealizerUrlPath() .
300
+ ((count($params) > 0) ? "?" . implode('&', $params) : '') .
301
+ " [" . implode(',', $flags) . "]\n\n";
302
+ } else {
303
+ /*
304
+ Generate something like this:
305
+ RewriteCond %{REQUEST_FILENAME} !-f
306
+ RewriteRule (?i).*(\.jpe?g|\.png)\.webp$ /plugins-moved/webp-express/wod/webp-realizer.php [E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:../wp-content-moved,E=WE_DESTINATION_REL_HTACCESS:$0,E=WE_HTACCESS_ID:cache,NC,L]
307
+ */
308
+ // Add condition for making sure the webp does not already exist
309
+ $rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
310
+
311
+ $params = [];
312
+ $flags = [];
313
+
314
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
315
+ $flags[] = 'E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:' . Paths::getContentDirRelToPluginDir();
316
+ $flags[] = 'E=WE_DESTINATION_REL_HTACCESS:$0';
317
+ $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir; // this will btw either be "uploads" or "cache"
318
+ }
319
+ $flags[] = 'NC'; // case-insensitive match (so file extension can be jpg, JPG or even jPg)
320
+ $flags[] = 'L';
321
+
322
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
323
+ $params[] = 'xwp-content-rel-to-plugin-dir=x' . Paths::getContentDirRelToPluginDir();
324
+ $params[] = 'xdestination-rel-htaccess=x$0';
325
+ $params[] = 'htaccess-id=' . self::$htaccessDir;
326
+ }
327
+
328
+ // self::$appendWebP cannot be used, we need the following in order for
329
+ // it to work for uploads in: Mingled, "Set to WebP", "Image roots".
330
+ // TODO! Will it work for ie theme images?
331
+ // - well, it should, because the script is passed $0. Not matching the ".png" part of the filename
332
+ // only means it is a bit more greedy than it has to
333
+ $appendWebP = !(self::$config['destination-extension'] == 'set');
334
+
335
+ $rules .= " RewriteRule (?i).*" . ($appendWebP ? "(" . self::$fileExtIncludingDot . ")" : "") . "\.webp$ " .
336
+ "/" . Paths::getWebPRealizerUrlPath() .
337
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
338
+ " [" . implode(',', $flags) . "]\n";
339
+
340
+ /*
341
+ Generate something like this:
342
+ RewriteCond %{REQUEST_FILENAME} !-f
343
+ RewriteRule (?i).*\.webp$ /plugins-moved/webp-express/wod/webp-realizer.php [E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:../wp-content-moved,E=WE_DESTINATION_REL_IMAGE_ROOT:$0,E=WE_IMAGE_ROOT_ID:wp-content,NC,L]
344
+ */
345
+ /*
346
+ // Add condition for making sure the webp does not already exist
347
+ $rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
348
+
349
+ $params = [];
350
+ $flags = [];
351
+
352
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
353
+ $flags[] = 'E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:' . Paths::getContentDirRelToPluginDir();
354
+ $flags[] = 'E=WE_DESTINATION_REL_IMAGE_ROOT:$0';
355
+ $flags[] = 'E=WE_IMAGE_ROOT_ID:' . self::$htaccessDir;
356
+ }
357
+ $flags[] = 'NC'; // case-insensitive match (so file extension can be jpg, JPG or even jPg)
358
+ $flags[] = 'L';
359
+
360
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
361
+ $params[] = 'image-root-id=' . self::$htaccessDir;
362
+ $params[] = 'xdestination-rel-image-root=x$0';
363
+ $params[] = 'xwp-content-rel-to-plugin-dir=x' . Paths::getContentDirRelToPluginDir();
364
+ }
365
+
366
+ // self::$appendWebP cannot be used, we need the following in order for
367
+ // it to work for uploads in: Mingled, "Set to WebP", "Image roots".
368
+ // TODO! Will it work for ie theme images?
369
+ // - well, it should, because the script is passed $0. Not matching the ".png" part of the filename
370
+ // only means it is a bit more greedy than it has to
371
+ $appendWebP = !(self::$config['destination-extension'] == 'set');
372
+
373
+ $rules .= " RewriteRule (?i).*" . ($appendWebP ? "(" . self::$fileExtIncludingDot . ")" : "") . "\.webp$ " .
374
+ "/" . Paths::getWebPRealizerUrlPath() .
375
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
376
+ " [" . implode(',', $flags) . "]\n\n";
377
+ */
378
+
379
+
380
+ /*
381
+ Generate something like this:
382
+
383
+ RewriteCond %{REQUEST_FILENAME} !-f
384
+ RewriteCond %{REQUEST_FILENAME} (?i)(/var/www/webp-express-tests/we0/wordpress/uploads-moved/)(.*)(\.jpe?g|\.png)(\.webp)$
385
+ RewriteRule (?i).*\.webp$ /plugins-moved/webp-express/wod/webp-realizer.php?root-id=uploads&xdest-rel-to-root-id=x%2%3%4 [E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:../wp-content-moved,E=REQFN:%{REQUEST_FILENAME},NC,L]
386
+
387
+ */
388
+ /*
389
+ // Bugger! When the requested file does not exist, %{REQUEST_FILENAME} will always contain the full path.
390
+ // - it is set to the closest existing path plus one path component.
391
+ // So we cannot use %{REQUEST_FILENAME} for webp realizer.
392
+ // It seems we must use REQUEST_URI. But this could get tricky as we may not have access to the resolved document root in the scripts
393
+
394
+ // Add condition for making sure the webp does not already exist
395
+ $rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
396
+
397
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(" .
398
+ self::$htaccessDirAbs . "/)(.*)" . (self::$appendWebP ? "(" . self::$fileExtIncludingDot . ")" : "") . "(\.webp)$\n";
399
+
400
+ $params = [];
401
+ $flags = [];
402
+
403
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
404
+ $flags[] = 'E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:' . Paths::getContentDirRelToPluginDir();
405
+ $flags[] = 'E=WEDESTINATIONABS:%0';
406
+ }
407
+ $flags[] = 'NC'; // case-insensitive match (so file extension can be jpg, JPG or even jPg)
408
+ $flags[] = 'L';
409
+
410
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
411
+ $params[] = 'root-id=' . self::$htaccessDir;
412
+ $params[] = 'xdest-rel-to-root-id=x%2%3' . (self::$appendWebP ? "%4" : "");
413
+ }
414
+
415
+ $rules .= " RewriteRule (?i).*\.webp$ " .
416
+ "/" . Paths::getWebPRealizerUrlPath() .
417
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
418
+ " [" . implode(',', $flags) . "]\n\n";
419
+ */
420
+ }
421
+
422
+ /*if (!self::$config['redirect-to-existing-in-htaccess']) {
423
+ $rules .= self::cacheRules();
424
+ }*/
425
+ $rules .= "</IfModule>\n\n";
426
+
427
+ return $rules;
428
+ }
429
+
430
+ private static function webpOnDemandRules()
431
+ {
432
+ $setEnvVar = !self::$passThroughEnvVarDefinitelyUnavailable;
433
+ $passRelativePathToSourceInQS = !(self::$passThroughEnvVarDefinitelyAvailable || self::$passThroughHeaderDefinitelyAvailable);
434
+
435
+ $rules = '';
436
+
437
+ // Do not add header magic if passing through env is definitely working
438
+ // Do not add either, if we definitily know it isn't working
439
+ /*
440
+ if ((!self::$passThroughEnvVarDefinitelyAvailable) && (!self::$passThroughHeaderDefinitelyUnavailable)) {
441
+ if (self::$config['enable-redirection-to-converter']) {
442
+ $rules .= " # Pass REQUEST_FILENAME to webp-on-demand.php in request header\n";
443
+ //$rules .= $basicConditions;
444
+ //$rules .= " RewriteRule ^(.*)\.(" . self::$fileExt . ")$ - [E=REQFN:%{REQUEST_FILENAME}]\n" .
445
+ $rules .= " <IfModule mod_headers.c>\n" .
446
+ " RequestHeader set REQFN \"%{REQFN}e\" env=REQFN\n" .
447
+ " </IfModule>\n\n";
448
+
449
+ }
450
+ if (self::$config['enable-redirection-to-webp-realizer']) {
451
+ // We haven't implemented a clever way to pass through header for webp-realizer yet
452
+ }
453
+ }*/
454
+ $rules .= " # Redirect images to webp-on-demand.php ";
455
+ if (self::$config['only-redirect-to-converter-for-webp-enabled-browsers']) {
456
+ $rules .= "(if browser supports webp)\n";
457
+ } else {
458
+ $rules .= "(regardless whether browser supports webp or not!)\n";
459
+ }
460
+ if (self::$config['only-redirect-to-converter-for-webp-enabled-browsers']) {
461
+ $rules .= " RewriteCond %{HTTP_ACCEPT} image/webp\n";
462
+ }
463
+
464
+ if (self::$useDocRootForStructuringCacheDir) {
465
+ /*
466
+ Generate something like this:
467
+
468
+ RewriteCond %{HTTP_ACCEPT} image/webp
469
+ RewriteCond %{REQUEST_FILENAME} -f
470
+ RewriteRule ^/?(.+)\.(jpe?g|png)$ /plugins-moved/webp-express/wod/webp-on-demand.php [NC,L,E=REQFN:%{REQUEST_FILENAME},E=WPCONTENT:wp-content-moved]
471
+ */
472
+
473
+ $params = [];
474
+ $flags = ['NC', 'L'];
475
+ if ($setEnvVar) {
476
+ $flags[] = 'E=REQFN:%{REQUEST_FILENAME}';
477
+ }
478
+ $rules .= " RewriteCond %{REQUEST_FILENAME} -f\n";
479
+ if ($passRelativePathToSourceInQS) {
480
+ $params[] = 'xsource-rel=x' . self::$htaccessDirRelToDocRoot . '/$1.$2';
481
+ }
482
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
483
+ $params[] = "wp-content=" . Paths::getContentDirRel();
484
+ }
485
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
486
+ $flags[] = 'E=WPCONTENT:' . Paths::getContentDirRel();
487
+ }
488
+
489
+ // TODO: When $rewriteRuleStart is empty, we don't need the .*, do we? - test
490
+ $rewriteRuleStart = '^/?(.+)';
491
+ $rules .= " RewriteRule " . $rewriteRuleStart . "\.(" . self::$fileExt . ")$ " .
492
+ "/" . Paths::getWodUrlPath() .
493
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
494
+ " [" . implode(',', $flags) . "]\n";
495
+
496
+ } else {
497
+
498
+ /*
499
+ Create something like this:
500
+
501
+ RewriteCond %{REQUEST_FILENAME} -f
502
+ RewriteRule (?i).*(\.jpe?g|\.png)$ /plugins-moved/webp-express/wod/webp-on-demand.php [E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:../../we0-content,E=WE_SOURCE_REL_HTACCESS:$0,E=WE_HTACCESS_ID:themes,NC,L]
503
+ */
504
+
505
+ // Making sure the source exists
506
+ $rules .= " RewriteCond %{REQUEST_FILENAME} -f\n";
507
+
508
+ $params = [];
509
+ $flags = [];
510
+
511
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
512
+ $flags[] = 'E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:' . Paths::getContentDirRelToPluginDir();
513
+ $flags[] = 'E=WE_SOURCE_REL_HTACCESS:$0';
514
+ $flags[] = 'E=WE_HTACCESS_ID:' . self::$htaccessDir; // this will btw be one of the image roots. It will not be "cache"
515
+ }
516
+ $flags[] = 'NC'; // case-insensitive match (so file extension can be jpg, JPG or even jPg)
517
+ $flags[] = 'L';
518
+
519
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
520
+ $params[] = 'xwp-content-rel-to-plugin-dir=x' . Paths::getContentDirRelToPluginDir();
521
+ $params[] = 'xsource-rel-htaccess=x$0';
522
+ $params[] = 'htaccess-id=' . self::$htaccessDir;
523
+ }
524
+
525
+ // self::$appendWebP cannot be used, we need the following in order for
526
+ // it to work for uploads in: Mingled, "Set to WebP", "Image roots".
527
+ // TODO! Will it work for ie theme images?
528
+ // - well, it should, because the script is passed $0. Not matching the ".png" part of the filename
529
+ // only means it is a bit more greedy than it has to
530
+ $appendWebP = !(self::$config['destination-extension'] == 'set');
531
+
532
+ $rules .= " RewriteRule (?i).*" . ($appendWebP ? "(" . self::$fileExtIncludingDot . ")" : "") . "$ " .
533
+ "/" . Paths::getWodUrlPath() .
534
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
535
+ " [" . implode(',', $flags) . "]\n";
536
+
537
+
538
+ /*
539
+ */
540
+
541
+ /*
542
+ Create something like this (for wp-content):
543
+
544
+ # Redirect to existing converted image in cache-dir (if browser supports webp)
545
+ RewriteCond %{HTTP_ACCEPT} image/webp
546
+ RewriteCond %{REQUEST_FILENAME} (?i)(/var/www/webp-express-tests/we0/wp-content-moved/)(.*)(\.jpe?g|\.png)$
547
+ RewriteRule (?i)(.*)(\.jpe?g|\.png)$ /plugins-moved/webp-express/wod/webp-on-demand.php?root-id=wp-content&xsource-rel-to-root-id=%2%3
548
+
549
+ PS: Actually, the whole REQUEST_FILENAME could be passed in querystring by adding "&req-fn=%0" to above.
550
+ */
551
+ /*
552
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(" .
553
+ self::$htaccessDirAbs . "/)(.*)(" . self::$fileExtIncludingDot . ")$\n";
554
+
555
+ $params = [];
556
+ $flags = [];
557
+
558
+ if (!self::$passThroughEnvVarDefinitelyUnavailable) {
559
+ $flags[] = 'E=WE_WP_CONTENT_REL_TO_PLUGIN_DIR:' . Paths::getContentDirRelToPluginDir();
560
+ $flags[] = 'E=REQFN:%{REQUEST_FILENAME}';
561
+ }
562
+ $flags[] = 'NC'; // case-insensitive match (so file extension can be jpg, JPG or even jPg)
563
+ $flags[] = 'L';
564
+
565
+ $params[] = 'root-id=' . self::$htaccessDir;
566
+ $params[] = 'xsource-rel-to-root-id=x%2' . (self::$appendWebP ? "%3" : "");
567
+
568
+ $rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ " .
569
+ "/" . Paths::getWodUrlPath() .
570
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
571
+ " [" . implode(',', $flags) . "]\n";
572
+ */
573
+
574
+ /*
575
+ TODO: NO, this will not do on systems that cannot pass through ENV.
576
+ (Or is REQUEST_FILENAME useable at all? If it is, then we could perhaps
577
+ catch the whole %{REQUEST_FILENAME} and pass it in %1)
578
+
579
+ $params = [];
580
+ $flags = ['NC', 'L'];
581
+
582
+ if ($passRelativePathToSourceInQS) {
583
+ $params[] = 'xsource-rel-to-plugin-dir=x' . self::$htaccessDirRelToDocRoot . '/$1.$2';
584
+ }
585
+ if (!self::$passThroughEnvVarDefinitelyAvailable) {
586
+ $params[] = "xwp-content-rel-to-plugin-dir=x" . Paths::getContentDirRelToPluginDir();
587
+ }
588
+
589
+ // $rules .= " RewriteCond %{REQUEST_FILENAME} -f\n";
590
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(" .
591
+ self::$htaccessDirAbs . "/)(.*)(" . self::$fileExtIncludingDot . ")$\n";
592
+
593
+ $rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ " .
594
+ "/" . Paths::getWodUrlPath() .
595
+ (count($params) > 0 ? "?" . implode('&', $params) : "") .
596
+ " [" . implode(',', $flags) . "]\n";
597
+
598
+ //$urlPath = '/' . Paths::getUrlPathById('plugins') . "/%2" . (self::$appendWebP ? "%3" : "") . "\.webp";
599
+ // $urlPath = '/' . Paths::getUrlPathById(self::$htaccessDir) . "/%2" . (self::$appendWebP ? "%3" : "") . "\.webp";
600
+ //$urlPath = '/' . Paths::getContentUrlPath() . "/webp-express/webp-images/" . self::$htaccessDir . "/%2" . (self::$appendWebP ? "%3" : "") . "\.webp";
601
+ //$rules .= " RewriteCond %1" . (self::$appendWebP ? "%2" : "") . "\.webp -f\n";
602
+ //$rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ " . $urlPath ." [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
603
+ */
604
+
605
+
606
+
607
+
608
+
609
+ }
610
+
611
+
612
+ /*
613
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(" . self::$htaccessDirAbs . "/)(.*)(" . self::$fileExtIncludingDot . ")$\n";
614
+ $urlPath = '/' . Paths::getContentUrlPath() . "/webp-express/webp-images/" . self::$htaccessDir . "/%2" . (self::$appendWebP ? "%3" : "") . "\.webp";
615
+ //$rules .= " RewriteCond %1" . (self::$appendWebP ? "%2" : "") . "\.webp -f\n";
616
+ $rules .= " RewriteRule (?i)(.*)(" . self::$fileExtIncludingDot . ")$ " . $urlPath .
617
+ " [T=image/webp,E=EXISTING:1," . (self::$addVary ? 'E=ADDVARY:1,' : '') . "L]\n\n";
618
+ */
619
+
620
+ /*
621
+ $rules .= " RewriteCond %{REQUEST_FILENAME} (?i)(.*)(\.jpe?g|\.png)$\n";
622
+ $rules .= " RewriteCond %1%2\.webp -f\n";
623
+ $rules .= " RewriteRule (?i)(.*)(\.jpe?g|\.png)$ %1%2\.webp [T=image/webp,E=EXISTING:1,E=ADDVARY:1,L]\n";
624
+ */
625
+
626
+ $rules .= "\n";
627
+ return $rules;
628
+ }
629
+
630
+ private static function setInternalProperties($config, $htaccessDir = 'index')
631
+ {
632
+ self::$useDocRootForStructuringCacheDir = (($config['destination-structure'] == 'doc-root') && Paths::canUseDocRootForStructuringCacheDir());
633
+ self::$htaccessDir = $htaccessDir;
634
+ self::$htaccessDirAbs = Paths::getAbsDirById(self::$htaccessDir);
635
+
636
+ self::$htaccessDirRelToDocRoot = '';
637
+ if (self::$useDocRootForStructuringCacheDir) {
638
+ self::$htaccessDirRelToDocRoot = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(
639
+ self::$htaccessDirAbs
640
+ );
641
+ }
642
+
643
+ // When using the absolute dir, the rewrite rules needs document root and does not work
644
+ // if the symlinks have been resolved.
645
+ // We can fix this - but only if document root is available and resolvable.
646
+ // - which is sad, because the image-roots was introduced in order to get it to work on setups
647
+ // where it isn't.
648
+ self::$htaccessDirAbs = PathHelper::fixAbsPathToUseUnresolvedDocRoot(self::$htaccessDirAbs);
649
+
650
+ // Fix config.
651
+ $defaults = [
652
+ 'enable-redirection-to-converter' => true,
653
+ 'forward-query-string' => true,
654
+ 'image-types' => 1,
655
+ 'do-not-pass-source-in-query-string' => false,
656
+ 'redirect-to-existing-in-htaccess' => false,
657
+ 'only-redirect-to-converter-on-cache-miss' => false,
658
+ 'destination-folder' => 'separate',
659
+ 'destination-extension' => 'append',
660
+ 'destination-structure' => 'doc-root',
661
+ 'success-response' => 'converted',
662
+ ];
663
+ $config = array_merge($defaults, $config);
664
+
665
+ if (!isset($config['base-htaccess-on-these-capability-tests'])) {
666
+ $config['base-htaccess-on-these-capability-tests'] = Config::runAndStoreCapabilityTests();
667
+ }
668
+ self::$config = $config;
669
+
670
+ $capTests = self::$config['base-htaccess-on-these-capability-tests'];
671
+ self::$modHeaderDefinitelyUnavailable = ($capTests['modHeaderWorking'] === false);
672
+ self::$passThroughHeaderDefinitelyUnavailable = ($capTests['passThroughHeaderWorking'] === false);
673
+ self::$passThroughHeaderDefinitelyAvailable = ($capTests['passThroughHeaderWorking'] === true);
674
+ self::$passThroughEnvVarDefinitelyUnavailable = ($capTests['passThroughEnvWorking'] === false);
675
+ self::$passThroughEnvVarDefinitelyAvailable =($capTests['passThroughEnvWorking'] === true);
676
+ self::$capTests = $capTests;
677
+
678
+ self::$imageTypes = self::$config['image-types'];
679
+ self::$fileExtensions = [];
680
+ if (self::$imageTypes & 1) {
681
+ self::$fileExtensions[] = 'jpe?g';
682
+ }
683
+ if (self::$imageTypes & 2) {
684
+ self::$fileExtensions[] = 'png';
685
+ }
686
+ self::$fileExt = implode('|', self::$fileExtensions);
687
+ self::$fileExtIncludingDot = "\." . implode("|\.", self::$fileExtensions);
688
+
689
+ self::$mingled = (self::$config['destination-folder'] == 'mingled');
690
+
691
+ // TODO: If we cannot store all .htaccess files we would like, we need to take into account which dir
692
+ $setWebPExt = ((self::$config['destination-extension'] == 'set') && (self::$htaccessDir == 'uploads'));
693
+ self::$appendWebP = !$setWebPExt;
694
+ }
695
+
696
+ public static function addVaryHeaderEnvRules($indent = 0)
697
+ {
698
+ $rules = [];
699
+ $rules[] = "# Set Vary:Accept header if we came here by way of our redirect, which set the ADDVARY environment variable";
700
+ $rules[] = "# The purpose is to make proxies and CDNs aware that the response varies with the Accept header";
701
+ $rules[] = "<IfModule mod_headers.c>";
702
+ $rules[] = " <IfModule mod_setenvif.c>";
703
+ $rules[] = " # Apache appends \"REDIRECT_\" in front of the environment variables defined in mod_rewrite, but LiteSpeed does not";
704
+ $rules[] = " # So, the next lines are for Apache, in order to set environment variables without \"REDIRECT_\"";
705
+ $rules[] = " SetEnvIf REDIRECT_EXISTING 1 EXISTING=1";
706
+ $rules[] = " SetEnvIf REDIRECT_ADDVARY 1 ADDVARY=1";
707
+ $rules[] = "";
708
+ $rules[] = " Header append \"Vary\" \"Accept\" env=ADDVARY";
709
+ $rules[] = "";
710
+
711
+ if (self::$config['redirect-to-existing-in-htaccess']) {
712
+ $rules[] = " # Set X-WebP-Express header for diagnose purposes";
713
+ //" SetEnvIf REDIRECT_WOD 1 WOD=1\n\n" .
714
+ //" # Set the debug header\n" .
715
+ $rules[] = " Header set \"X-WebP-Express\" \"Redirected directly to existing webp\" env=EXISTING";
716
+ //" Header set \"X-WebP-Express\" \"Redirected to image converter\" env=WOD\n" .
717
+ }
718
+ $rules[] = " </IfModule>";
719
+ $rules[] = "</IfModule>";
720
+ $rules[] = "";
721
+
722
+ if ($indent > 0) {
723
+ $indentStr = '';
724
+ for ($x=0; $x<$indent; $x++) {
725
+ $indentStr .= ' ';
726
+ }
727
+ foreach ($rules as $i => $rule) {
728
+ if ($rule != '') {
729
+ $rules[$i] = $indentStr . $rule;
730
+ }
731
+ }
732
+ }
733
+ return implode("\n", $rules);
734
+ }
735
+
736
+ // https://stackoverflow.com/questions/34124819/mod-rewrite-set-custom-header-through-htaccess
737
+ public static function generateHTAccessRulesFromConfigObj($config, $htaccessDir = 'index', $dirContainsSourceImages = true, $dirContainsWebPImages = true)
738
+ {
739
+ self::setInternalProperties($config, $htaccessDir);
740
+ self::$dirContainsSourceImages = $dirContainsSourceImages;
741
+ self::$dirContainsWebPImages = $dirContainsWebPImages;
742
+
743
+ if (
744
+ (!self::$config['enable-redirection-to-converter']) &&
745
+ (!self::$config['redirect-to-existing-in-htaccess']) &&
746
+ (!self::$config['enable-redirection-to-webp-realizer'])
747
+ ) {
748
+ return '# WebP Express does not need to write any rules (it has not been set up to redirect to converter, nor' .
749
+ ' to existing webp, and the "convert non-existing webp-files upon request" option has not been enabled)';
750
+ }
751
+
752
+ if (self::$imageTypes == 0) {
753
+ return '# WebP Express disabled (no image types has been choosen to be converted/redirected)';
754
+ }
755
+
756
+ self::$addVary = self::$config['redirect-to-existing-in-htaccess'];
757
+ if (self::$modHeaderDefinitelyUnavailable) {
758
+ self::$addVary = false;
759
+ }
760
+
761
+ /* Build rules */
762
+ $rules = '';
763
+ $rules .= self::infoRules();
764
+
765
+ if ($dirContainsSourceImages) {
766
+ $rules .= "# Rules for handling requests for source images\n";
767
+ $rules .= "# ---------------------------------------------\n\n";
768
+ $rules .= "<IfModule mod_rewrite.c>\n" .
769
+ " RewriteEngine On\n\n";
770
+
771
+ if (self::$config['redirect-to-existing-in-htaccess']) {
772
+ $rules .= self::redirectToExistingRules();
773
+ }
774
+
775
+ if (self::$config['enable-redirection-to-converter']) {
776
+ $rules .= self::webpOnDemandRules();
777
+ }
778
+
779
+ //if (self::$addVary) {
780
+ if (
781
+ (self::$config['redirect-to-existing-in-htaccess']) ||
782
+ (self::$config['enable-redirection-to-converter'])
783
+ ) {
784
+ $rules .= " # Make sure that browsers which does not support webp also gets the Vary:Accept header\n" .
785
+ " # when requesting images that would be redirected to webp on browsers that does.\n";
786
+
787
+ $rules .= " <IfModule mod_headers.c>\n";
788
+ $rules .= ' <FilesMatch "\.(jpe?g|png)$">' . "\n";
789
+ $rules .= ' Header append "Vary" "Accept"' . "\n";
790
+ $rules .= " </FilesMatch>\n";
791
+ $rules .= " </IfModule>\n\n";
792
+ }
793
+
794
+ /*
795
+ " <IfModule mod_setenvif.c>\n" .
796
+ " SetEnvIf Request_URI \"\.(" . self::$fileExt . ")$\" ADDVARY\n" .
797
+ " </IfModule>\n\n";
798
+ */
799
+
800
+ //self::$addVary = (self::$config['enable-redirection-to-converter'] && (self::$config['success-response'] == 'converted')) || (self::$config['redirect-to-existing-in-htaccess']);
801
+
802
+ /*
803
+ if (self::$addVary) {
804
+ if ($dirContainsWebPImages) {
805
+ $rules .= self::addVaryHeaderEnvRules(2);
806
+ }
807
+ }*/
808
+ $rules .= "</IfModule>\n";
809
+ } /*else {
810
+ if ($dirContainsWebPImages) {
811
+ $rules .= self::addVaryHeaderEnvRules();
812
+ }
813
+ }*/
814
+ if ($dirContainsWebPImages) {
815
+ $rules .= "\n# Rules for handling requests for webp images\n";
816
+ $rules .= "# ---------------------------------------------\n\n";
817
+ if (self::$config['enable-redirection-to-webp-realizer']) {
818
+ $rules .= self::webpRealizerRules();
819
+ }
820
+ $rules .= self::cacheRules();
821
+
822
+ /*
823
+ if (
824
+ (self::$config['enable-redirection-to-webp-realizer']) ||
825
+ (self::$config['redirect-to-existing-in-htaccess'])
826
+ ) {
827
+ }*/
828
+ $rules .= self::addVaryHeaderEnvRules();
829
+
830
+ $rules .= "\n# Register webp mime type \n";
831
+ $rules .= "<IfModule mod_mime.c>\n";
832
+ $rules .= " AddType image/webp .webp\n";
833
+ $rules .= "</IfModule>\n";
834
+ }
835
+
836
+ /*if (self::$config['redirect-to-existing-in-htaccess']) {
837
+ $rules .=
838
+ "<IfModule mod_headers.c>\n" .
839
+ " # Append Vary Accept header, when the rules above are redirecting to existing webp\n" .
840
+ " # or existing jpg" .
841
+
842
+ " # Apache appends \"REDIRECT_\" in front of the environment variables, but LiteSpeed does not.\n" .
843
+ " # These next line is for Apache, in order to set environment variables without \"REDIRECT_\"\n" .
844
+ " SetEnvIf REDIRECT_WEBPACCEPT 1 WEBPACCEPT=1\n\n" .
845
+
846
+ " # Make CDN caching possible.\n" .
847
+ " # The effect is that the CDN will cache both the webp image and the jpeg/png image and return the proper\n" .
848
+ " # image to the proper clients (for this to work, make sure to set up CDN to forward the \"Accept\" header)\n" .
849
+ " Header append Vary Accept env=WEBPACCEPT\n" .
850
+ "</IfModule>\n\n";
851
+ }*/
852
+
853
+ return $rules;
854
+ }
855
+ }
lib/classes/HandleUploadHooks.php CHANGED
@@ -40,7 +40,7 @@ class HandleUploadHooks
40
  $imageTypes = $config['image-types'];
41
  if ($imageTypes & 1) {
42
  $allowedMimeTypes[] = 'image/jpeg';
43
- $allowedMimeTypes[] = 'image/jpg'; /* don't think "image/jpg" is neccessary, but just in case */
44
  }
45
  if ($imageTypes & 2) {
46
  $allowedMimeTypes[] = 'image/png';
40
  $imageTypes = $config['image-types'];
41
  if ($imageTypes & 1) {
42
  $allowedMimeTypes[] = 'image/jpeg';
43
+ $allowedMimeTypes[] = 'image/jpg'; /* don't think "image/jpg" is necessary, but just in case */
44
  }
45
  if ($imageTypes & 2) {
46
  $allowedMimeTypes[] = 'image/png';
lib/classes/ImageRoot.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ use \WebPExpress\PathHelper;
6
+
7
+ class ImageRoot
8
+ {
9
+ /**
10
+ * Constructor.
11
+ *
12
+ * @param array $imageRootDef assoc array containing "id", "url" and either "abs-path", "rel-path" or both.
13
+ */
14
+ public function __construct($imageRootDef)
15
+ {
16
+ $this->imageRootDef = $imageRootDef;
17
+ $this->id = $imageRootDef['id'];
18
+ }
19
+
20
+ /**
21
+ * Get / calculate abs path.
22
+ *
23
+ * If "rel-path" is set and document root is available, the abs path will be calculated from the relative path.
24
+ * Otherwise the "abs-path" is returned.
25
+ * @throws Exception In case rel-path is not
26
+ */
27
+ public function getAbsPath()
28
+ {
29
+ $def = $this->imageRootDef;
30
+ if (isset($def['rel-path']) && PathHelper::isDocRootAvailable()) {
31
+ return rtrim($_SERVER["DOCUMENT_ROOT"], '/') . '/' . $def['rel-path'];
32
+ } elseif (isset($def['abs-path'])) {
33
+ return $def['abs-path'];
34
+ } else {
35
+ if (!isset($def['rel-path'])) {
36
+ throw new \Exception(
37
+ 'Image root definition in config file is must either have a "rel-path" or "abs-path" property defined. ' .
38
+ 'Probably your system setup has changed. Please re-save WebP Express options and regenerate .htaccess'
39
+ );
40
+ } else {
41
+ throw new \Exception(
42
+ 'Image root definition in config file is defined by "rel-path". However, DOCUMENT_ROOT is unavailable so we ' .
43
+ 'cannot use that (as the rel-path is relative to that. ' .
44
+ 'Probably your system setup has changed. Please re-save WebP Express options and regenerate .htaccess'
45
+ );
46
+ }
47
+ }
48
+ }
49
+
50
+ }
lib/classes/ImageRoots.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ use \WebPExpress\ImageRoot;
6
+
7
+ class ImageRoots
8
+ {
9
+ /**
10
+ * Constructor.
11
+ *
12
+ * @param array $imageRoots Array representation of image roots
13
+ */
14
+ public function __construct($imageRootsDef)
15
+ {
16
+ $this->imageRootsDef = $imageRootsDef;
17
+
18
+ $this->imageRoots = [];
19
+ foreach ($imageRootsDef as $i => $def)
20
+ {
21
+ $this->imageRoots[] = new ImageRoot($def);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Get image root by id.
27
+ *
28
+ * @return \WebPExpress\ImageRoot An image root object
29
+ */
30
+ public function byId($id)
31
+ {
32
+ foreach ($this->imageRoots as $i => $imageRoot) {
33
+ if ($imageRoot->id == $id) {
34
+ return $imageRoot;
35
+ }
36
+ }
37
+ throw new \Exception('Image root not found');
38
+ }
39
+
40
+ /**
41
+ * Get the image roots array
42
+ *
43
+ * @return array An array of ImageRoot objects
44
+ */
45
+ public function getArray()
46
+ {
47
+ return $this->imageRoots;
48
+ }
49
+ }
lib/classes/Mime.php CHANGED
@@ -10,10 +10,15 @@ class Mime
10
 
11
  public static function getMimeTypeOfMedia($filename)
12
  {
13
- // Try the Wordpress function. It tries exif_imagetype and getimagesize and returns false if no methods are available
14
- $mimeType = wp_get_image_mime($filename);
15
- if ($mimeType !== false) {
16
- return $mimeType;
 
 
 
 
 
17
  }
18
 
19
  // Try mime_content_type
@@ -24,10 +29,12 @@ class Mime
24
  }
25
  }
26
 
27
- // Try wordpress method, which simply uses the file extension and a map
28
- $mimeType = wp_check_filetype($filePath)['type'];
29
- if ($mimeType !== false) {
30
- return $mimeType;
 
 
31
  }
32
 
33
  // Don't say we didn't try!
10
 
11
  public static function getMimeTypeOfMedia($filename)
12
  {
13
+ // First try the Wordpress function if available (it was introduced in 4.7.1)
14
+ if (function_exists('wp_get_image_mime')) {
15
+
16
+ // PS: wp_get_image_mime tries exif_imagetype and getimagesize and returns false if no methods are available
17
+ $mimeType = wp_get_image_mime($filename);
18
+ if ($mimeType !== false) {
19
+ return $mimeType;
20
+ }
21
+
22
  }
23
 
24
  // Try mime_content_type
29
  }
30
  }
31
 
32
+ if (function_exists('wp_check_filetype')) { // introduced in 2.0.4
33
+ // Try wordpress method, which simply uses the file extension and a map
34
+ $mimeType = wp_check_filetype($filePath)['type'];
35
+ if ($mimeType !== false) {
36
+ return $mimeType;
37
+ }
38
  }
39
 
40
  // Don't say we didn't try!
lib/classes/PathHelper.php CHANGED
@@ -5,6 +5,333 @@ namespace WebPExpress;
5
  class PathHelper
6
  {
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  /**
9
  * Replace double slash with single slash. ie '/var//www/' => '/var/www/'
10
  * This allows you to lazely concatenate paths with '/' and then call this method to clean up afterwards.
@@ -43,6 +370,23 @@ class PathHelper
43
  return implode('/', $parts);
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  /**
47
  * Returns absolute path from a relative path and root
48
  * The result is canonicalized (dots and double-dots are resolved)
5
  class PathHelper
6
  {
7
 
8
+ public static function isDocRootAvailable() {
9
+
10
+ //return false;
11
+
12
+ if (!isset($_SERVER['DOCUMENT_ROOT'])) {
13
+ return false;
14
+ }
15
+ if ($_SERVER['DOCUMENT_ROOT'] == '') {
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ /**
22
+ * Test if a path exists as is resolvable (will be unless it is outside open_basedir)
23
+ *
24
+ * @param string $absPath The path to test (must be absolute. symlinks allowed)
25
+ * @return boolean The result
26
+ */
27
+ public static function pathExistsAndIsResolvable($absPath) {
28
+ if (!@realpath($absPath)) {
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ /**
35
+ * Test if document root is available, exists and symlinks are resolvable (resolved path is within open basedir)
36
+ *
37
+ * @return boolean The result
38
+ */
39
+ public static function isDocRootAvailableAndResolvable() {
40
+ return (
41
+ self::isDocRootAvailable() &&
42
+ self::pathExistsAndIsResolvable($_SERVER['DOCUMENT_ROOT'])
43
+ );
44
+ }
45
+
46
+ public static function fixAbsPathToUseUnresolvedDocRoot($absPath) {
47
+ if (self::isDocRootAvailableAndResolvable()) {
48
+ if (strpos($absPath, realpath($_SERVER['DOCUMENT_ROOT'])) === 0) {
49
+ return $_SERVER['DOCUMENT_ROOT'] . substr($absPath, strlen(realpath($_SERVER['DOCUMENT_ROOT'])));
50
+ }
51
+ }
52
+ return $absPath;
53
+ }
54
+
55
+ /**
56
+ * Find out if path is below - or equal to a path.
57
+ *
58
+ * "/var/www" below/equal to "/var"? : Yes
59
+ * "/var/www" below/equal to "/var/www"? : Yes
60
+ * "/var/www2" below/equal to "/var/www"? : No
61
+ */
62
+ /*
63
+ public static function isPathBelowOrEqualToPath($path1, $path2)
64
+ {
65
+ return (strpos($path1 . '/', $path2 . '/') === 0);
66
+ //$rel = self::getRelDir($path2, $path1);
67
+ //return (substr($rel, 0, 3) != '../');
68
+ }*/
69
+
70
+ /**
71
+ * Calculate relative path from document root to a given absolute path (must exist and be resolvable) - if possible AND
72
+ * if it can be done without directory traversal.
73
+ *
74
+ * The function is designed with the usual folders in mind (index, uploads, wp-content, plugins), which all presumably
75
+ * exists and are within open_basedir.
76
+ *
77
+ * @param string $dir An absolute path (may contain symlinks). The path must exist and be resolvable.
78
+ * @throws \Exception If it is not possible to get such path (ie if doc-root is unavailable or the dir is outside doc-root)
79
+ * @return string Relative path to document root or empty string if document root is unavailable
80
+ */
81
+ public static function getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($dir)
82
+ {
83
+ if (!self::isDocRootAvailable()) {
84
+ throw new \Exception('Cannot calculate relative path from document root to dir, as document root is not available');
85
+ }
86
+
87
+ // First try unresolved.
88
+ // This will even work when ie wp-content is symlinked to somewhere outside document root, while the symlink itself is within document root)
89
+ $relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], $dir);
90
+ if (strpos($relPath, '../') !== 0) { // Check if relPath starts with "../" (if it does, we cannot use it)
91
+ return $relPath;
92
+ }
93
+
94
+ if (self::isDocRootAvailableAndResolvable()) {
95
+ if (self::pathExistsAndIsResolvable($dir)) {
96
+ // Try with both resolved
97
+ $relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), realpath($dir));
98
+ if (strpos($relPath, '../') !== 0) {
99
+ return $relPath;
100
+ }
101
+ }
102
+
103
+ // Try with just document root resolved
104
+ $relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $dir);
105
+ if (strpos($relPath, '../') !== 0) {
106
+ return $relPath;
107
+ }
108
+ }
109
+
110
+ if (self::pathExistsAndIsResolvable($dir)) {
111
+ // Try with dir resolved
112
+ $relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], realpath($dir));
113
+ if (strpos($relPath, '../') !== 0) {
114
+ return $relPath;
115
+ }
116
+ }
117
+
118
+ // Problem:
119
+ // - dir is already resolved (ie: /disk/the-content)
120
+ // - document root is ie. /var/www/website/wordpress
121
+ // - the unresolved symlink is ie. /var/www/website/wordpress/wp-content
122
+ // - we do not know what the unresolved symlink is
123
+ // The result should be "wp-content". But how do we get to that result?
124
+ // I guess we must check out all folders below document root to see if anyone resolves to dir
125
+ // we could start out trying usual suspects such as "wp-content" and "wp-content/uploads"
126
+ //foreach (glob($dir . DIRECTORY_SEPARATOR . $filePattern) as $filename)
127
+ /*
128
+ $iter = new \RecursiveIteratorIterator(
129
+ new \RecursiveDirectoryIterator($_SERVER['DOCUMENT_ROOT'], \RecursiveDirectoryIterator::SKIP_DOTS),
130
+ \RecursiveIteratorIterator::SELF_FIRST,
131
+ \RecursiveIteratorIterator::CATCH_GET_CHILD // Ignore "Permission denied"
132
+ );
133
+
134
+ foreach ($iter as $path => $dirObj) {
135
+ if ($dirObj->isDir()) {
136
+ if (realpath($path) == $dir) {
137
+ //return $path;
138
+ $relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $path);
139
+ if (strpos($relPath, '../') !== 0) {
140
+ return $relPath;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ */
146
+ // Ok, the above works - but when subfolders to the symlink is referenced. Ie referencing uploads when wp-content is symlinked
147
+ // - dir is already resolved (ie: /disk/the-content/uploads)
148
+ // - document root is ie. /var/www/website/wordpress
149
+ // - the unresolved symlink is ie. /var/www/website/wordpress/wp-content/uploads
150
+ // - we do not know what the unresolved symlink is
151
+ // The result should be "wp-content/uploads". But how do we get to that result?
152
+
153
+ // What if we collect all symlinks below document root in a assoc array?
154
+ // ['/disk/the-content' => 'wp-content']
155
+ // Input is: '/disk/the-content/uploads'
156
+ // 1. We check the symlinks and substitute. We get: 'wp-content/uploads'.
157
+ // 2. We test if realpath($_SERVER['DOCUMENT_ROOT'] . '/' . 'wp-content/uploads') equals input.
158
+ // It seems I have a solution!
159
+ // - I shall continue work soon! - for a 0.15.1 release (test instance #26)
160
+ // PS: cache the result of the symlinks in docroot collector.
161
+
162
+ throw new \Exception(
163
+ 'Cannot get relative path from document root to dir without resolving to directory traversal. ' .
164
+ 'It seems the dir is not below document root'
165
+ );
166
+
167
+ /*
168
+ if (!self::pathExistsAndIsResolvable($dir)) {
169
+ throw new \Exception('Cannot calculate relative path from document root to dir. The path given is not resolvable (realpath fails)');
170
+ }
171
+
172
+
173
+ // Check if relPath starts with "../"
174
+ if (strpos($relPath, '../') === 0) {
175
+
176
+ // Unresolved failed. Try with document root resolved
177
+ $relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $dir);
178
+
179
+ if (strpos($relPath, '../') === 0) {
180
+
181
+ // Try with both resolved
182
+ $relPath = self::getRelDir($dir, $dir);
183
+ throw new \Exception('Cannot calculate relative path from document root to dir. The path given is not within document root');
184
+ }
185
+ }
186
+
187
+
188
+ }
189
+
190
+ return $relPath;
191
+ } else {
192
+ // We cannot get the resolved doc-root.
193
+ // This might be ok as long as the (resolved) path we are examining begins with the configured doc-root.
194
+ $relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], $dir);
195
+
196
+ // Check if relPath starts with "../" (it may not)
197
+ if (strpos($relPath, '../') === 0) {
198
+
199
+ // Well, that did not work. We can try the resolved path instead.
200
+ if (!self::pathExistsAndIsResolvable($dir)) {
201
+ throw new \Exception('Cannot calculate relative path from document root to dir. The path given is not resolvable (realpath fails)');
202
+ }
203
+
204
+ $relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], realpath($dir));
205
+ if (strpos($relPath, '../') === 0) {
206
+
207
+ // That failed too.
208
+ // Either it is in fact outside document root or it is because of a special setup.
209
+ throw new \Exception(
210
+ 'Cannot calculate relative path from document root to dir. Either the path given is not within the configured document root or ' .
211
+ 'it is because of a special setup. The document root is outside open_basedir. If it is also symlinked, but the other Wordpress paths ' .
212
+ 'are not using that same symlink, it will not be possible to calculate the relative path.'
213
+ );
214
+ }
215
+ }
216
+ return $relPath;
217
+ }*/
218
+ }
219
+
220
+ public static function canCalculateRelPathFromDocRootToDir($dir)
221
+ {
222
+ try {
223
+ $relPath = self::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($dir);
224
+ } catch (\Exception $e) {
225
+ return false;
226
+ }
227
+ return true;
228
+ }
229
+
230
+ /**
231
+ * Find closest existing folder with symlinks expandend, using realpath.
232
+ *
233
+ * Note that if the input or the closest existing folder is outside open_basedir, no folder will
234
+ * be found and an empty string will be returned.
235
+ *
236
+ * @return string closest existing path or empty string if none found (due to open_basedir restriction)
237
+ */
238
+ public static function findClosestExistingFolderSymLinksExpanded($input) {
239
+
240
+ // The strategy is to first try the supplied directory. If it fails, try the parent, etc.
241
+ $dir = $input;
242
+
243
+ // We count the levels up to avoid infinite loop - as good practice. It ought not to get that far
244
+ $levelsUp = 0;
245
+
246
+ while ($levelsUp < 100) {
247
+ // We suppress warning because we are aware that we might get a
248
+ // open_basedir restriction warning.
249
+ $realPathResult = @realpath($dir);
250
+ if ($realPathResult !== false) {
251
+ return $realPathResult;
252
+ }
253
+ // Stop at root. This will happen if the original path is outside basedir.
254
+ if (($dir == '/') || (strlen($dir) < 4)) {
255
+ return '';
256
+ }
257
+ // Peal off one directory
258
+ $dir = @dirname($dir);
259
+ $levelsUp++;
260
+ }
261
+ return '';
262
+ }
263
+
264
+ /**
265
+ * Look if filepath is within a dir path (both by string matching and by using realpath, see notes).
266
+ *
267
+ * Note that the naive string match does not resolve '..'. You might want to call ::canonicalize first.
268
+ * Note that the realpath match requires: 1. that the dir exist and is within open_basedir
269
+ * 2. that the closest existing folder within filepath is within open_basedir
270
+ *
271
+ * @param string $filePath Path to file. It may be non-existing.
272
+ * @param string $dirPath Path to dir. It must exist and be within open_basedir in order for the realpath match to execute.
273
+ */
274
+ public static function isFilePathWithinDirPath($filePath, $dirPath)
275
+ {
276
+ // See if $filePath begins with $dirPath + '/'.
277
+ if (strpos($filePath, $dirPath . '/') === 0) {
278
+ return true;
279
+ }
280
+
281
+ if (strpos(self::canonicalize($filePath), self::canonicalize($dirPath) . '/') === 0) {
282
+ return true;
283
+ }
284
+
285
+
286
+ // Also try with symlinks expanded.
287
+ // As symlinks can only be retrieved with realpath and realpath fails with non-existing paths,
288
+ // we settle with checking if closest existing folder in the filepath is within the dir.
289
+ // If that is the case, then surely, the complete filepath is also within the dir.
290
+ // Note however that it might be that the closest existing folder is not within the dir, while the
291
+ // file would be (if it existed)
292
+ // For WebP Express, we are pretty sure that the dirs we are checking against (uploads folder,
293
+ // wp-content, plugins folder) exists. So getting the closest existing folder should be sufficient.
294
+ // but could it be that these are outside open_basedir on some setups? Perhaps on a few systems.
295
+ if (self::pathExistsAndIsResolvable($dirPath)) {
296
+ $closestExistingDirOfFile = PathHelper::findClosestExistingFolderSymLinksExpanded($filePath);
297
+ if (strpos($closestExistingDirOfFile, realpath($dirPath) . '/') === 0) {
298
+ return true;
299
+ }
300
+ }
301
+
302
+ return false;
303
+ }
304
+
305
+ /**
306
+ * Look if path is within a dir path. Also tries expanding symlinks
307
+ *
308
+ * @param string $path Path to examine. It may be non-existing.
309
+ * @param string $dirPath Path to dir. It must exist in order for symlinks to be expanded.
310
+ */
311
+ public static function isPathWithinExistingDirPath($path, $dirPath)
312
+ {
313
+ if ($path == $dirPath) {
314
+ return true;
315
+ }
316
+ // See if $filePath begins with $dirPath + '/'.
317
+ if (strpos($path, $dirPath . '/') === 0) {
318
+ return true;
319
+ }
320
+
321
+ // Also try with symlinks expanded (see comments in ::isFilePathWithinDirPath())
322
+ $closestExistingDir = PathHelper::findClosestExistingFolderSymLinksExpanded($path);
323
+ if (strpos($closestExistingDir . '/', $dirPath . '/') === 0) {
324
+ return true;
325
+ }
326
+ return false;
327
+ }
328
+
329
+ public static function frontslasher($str)
330
+ {
331
+ // TODO: replace backslash with frontslash
332
+ return $str;
333
+ }
334
+
335
  /**
336
  * Replace double slash with single slash. ie '/var//www/' => '/var/www/'
337
  * This allows you to lazely concatenate paths with '/' and then call this method to clean up afterwards.
370
  return implode('/', $parts);
371
  }
372
 
373
+ public static function dirname($path) {
374
+ return self::canonicalize($path . '/..');
375
+ }
376
+
377
+ /**
378
+ * Get base name of a path (the last component of a path - ie the filename).
379
+ *
380
+ * This function operates natively on the string and is not locale aware.
381
+ * It only works with "/" path separators.
382
+ *
383
+ * @return string the last component of a path
384
+ */
385
+ public static function basename($path) {
386
+ $parts = explode('/', $path);
387
+ return array_pop($parts);
388
+ }
389
+
390
  /**
391
  * Returns absolute path from a relative path and root
392
  * The result is canonicalized (dots and double-dots are resolved)
lib/classes/Paths.php CHANGED
@@ -8,6 +8,225 @@ use \WebPExpress\PathHelper;
8
 
9
  class Paths
10
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  public static function createDirIfMissing($dir)
13
  {
@@ -28,44 +247,33 @@ class Paths
28
  return (substr($rel, 0, 3) != '../');
29
  }
30
 
31
- /**
32
- * Return relative dir - relative to realpath(document root)
33
- */
34
- public static function getRelDir($dir)
35
- {
36
- return PathHelper::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $dir);
37
- }
38
-
39
-
40
  /**
41
  * Return absolute dir.
42
- * - realpath() is used to resolve soft links and resolve '../' and './'
 
43
  * - trailing dash is removed - we don't use that around here.
44
  *
45
- * realpath() only works on existing dirs.
46
- * If realpath fails, PathHelper::canonicalize() will be used insead.
47
- * (this takes care of resolving '../' and './', but does NOT resolve soft links)
48
  */
49
  public static function getAbsDir($dir)
50
  {
 
 
 
51
  $result = realpath($dir);
52
  if ($result === false) {
53
  $dir = PathHelper::canonicalize($dir);
54
  } else {
55
  $dir = $result;
56
- }
57
- return rtrim($dir, '/');
58
- }
59
-
60
- // ------------ Document Root -------------
61
 
62
- public static function getDocumentRootAbs()
63
- {
64
- return self::getAbsDir($_SERVER["DOCUMENT_ROOT"]);
65
  }
66
 
67
  // ------------ Home Dir -------------
68
 
 
 
69
  public static function getHomeDirAbs()
70
  {
71
  if (!function_exists('get_home_path')) {
@@ -74,24 +282,24 @@ class Paths
74
  return self::getAbsDir(get_home_path());
75
  }
76
 
77
- public static function getHomeDirRel()
78
- {
79
- return self::getRelDir(self::getHomeDirAbs());
80
- }
81
-
82
- // ------------ Index Dir -------------
83
- // (The Wordpress installation dir)
84
 
85
  public static function getIndexDirAbs()
86
  {
87
- return self::getAbsDir(ABSPATH);
88
- }
 
 
 
 
89
 
90
- public static function getIndexDirRel()
91
- {
92
- return self::getRelDir(self::getIndexDirAbs());
93
- }
94
 
 
95
 
96
  // ------------ .htaccess dir -------------
97
  // (directory containing the relevant .htaccess)
@@ -103,6 +311,10 @@ class Paths
103
  return FileHelper::canEditOrCreateFileHere($dirName . '/.htaccess');
104
  }
105
 
 
 
 
 
106
  public static function returnFirstWritableHTAccessDir($dirs)
107
  {
108
  foreach ($dirs as $dir) {
@@ -121,9 +333,14 @@ class Paths
121
  }
122
  public static function getContentDirRel()
123
  {
124
- return self::getRelDir(self::getContentDirAbs());
 
 
 
 
125
  }
126
 
 
127
  public static function isWPContentDirMoved()
128
  {
129
  return (self::getContentDirAbs() != (ABSPATH . 'wp-content'));
@@ -134,6 +351,12 @@ class Paths
134
  return !(self::isDirInsideDir(self::getContentDirAbs(), ABSPATH));
135
  }
136
 
 
 
 
 
 
 
137
 
138
  // ------------ WebPExpress Content Dir -------------
139
  // (the "webp-express" directory inside wp-content)
@@ -145,7 +368,7 @@ class Paths
145
 
146
  public static function getWebPExpressContentDirRel()
147
  {
148
- return self::getRelDir(self::getWebPExpressContentDirAbs());
149
  }
150
 
151
  public static function createContentDirIfMissing()
@@ -161,7 +384,7 @@ class Paths
161
  }
162
  public static function getUploadDirRel()
163
  {
164
- return self::getRelDir(self::getUploadDirAbs());
165
  }
166
 
167
  /*
@@ -193,7 +416,7 @@ class Paths
193
 
194
  public static function getConfigDirRel()
195
  {
196
- return self::getRelDir(self::getConfigDirAbs());
197
  }
198
 
199
  public static function createConfigDirIfMissing()
@@ -237,9 +460,25 @@ APACHE
237
  return self::getWebPExpressContentDirAbs() . '/webp-images';
238
  }
239
 
240
- public static function getCacheDirRel()
 
 
 
 
 
241
  {
242
- return self::getRelDir(self::getCacheDirAbs());
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
 
245
  public static function createCacheDirIfMissing()
@@ -247,7 +486,7 @@ APACHE
247
  return self::createDirIfMissing(self::getCacheDirAbs());
248
  }
249
 
250
- // ------------ Cache Dir -------------
251
 
252
  public static function getLogDirAbs()
253
  {
@@ -261,10 +500,6 @@ APACHE
261
  return self::getAbsDir(WP_PLUGIN_DIR);
262
  }
263
 
264
- public static function getPluginDirRel()
265
- {
266
- return self::getRelDir(self::getPluginDirAbs());
267
- }
268
 
269
  public static function isPluginDirMovedOutOfAbsPath()
270
  {
@@ -283,39 +518,6 @@ APACHE
283
  return self::getAbsDir(WEBPEXPRESS_PLUGIN_DIR);
284
  }
285
 
286
- public static function getAbsDirId($absDir) {
287
- switch ($absDir) {
288
- case self::getContentDirAbs():
289
- return 'wp-content';
290
- case self::getIndexDirAbs():
291
- return 'index';
292
- case self::getHomeDirAbs():
293
- return 'home';
294
- case self::getPluginDirAbs():
295
- return 'plugins';
296
- case self::getUploadDirAbs():
297
- return 'uploads';
298
- }
299
- return false;
300
- }
301
-
302
- public static function getAbsDirById($dirId) {
303
- switch ($dirId) {
304
- case 'wp-content':
305
- return self::getContentDirAbs();
306
- case 'index':
307
- return self::getIndexDirAbs();
308
- case 'home':
309
- return self::getHomeDirAbs();
310
- case 'plugins':
311
- return self::getPluginDirAbs();
312
- case 'uploads':
313
- return self::getUploadDirAbs();
314
- }
315
- return false;
316
- }
317
-
318
-
319
  // ------------------------------------
320
  // --------- Url paths ----------
321
  // ------------------------------------
@@ -342,11 +544,77 @@ APACHE
342
  return ltrim($path, '/\\');
343
  }
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  // Get complete home url (no trailing slash). Ie: "http://example.com/blog"
346
  public static function getHomeUrl()
347
  {
348
- if (!function_exists('get_home_url')) {
349
  // silence is golden?
 
350
  }
351
  return untrailingslashit(home_url());
352
  }
@@ -381,8 +649,10 @@ APACHE
381
  return self::getUrlPathFromUrl(self::getContentUrl());
382
  }
383
 
384
-
385
-
 
 
386
 
387
  /**
388
  * Get Url to WebP Express plugin (this is in fact an incomplete URL, you need to append ie '/webp-on-demand.php' to get a full URL)
@@ -415,17 +685,6 @@ APACHE
415
  return self::getHomeUrl() . '/webp-express-web-service';
416
  }
417
 
418
- /**
419
- * Calculate path to existing image, excluding
420
- * (relative to document root)
421
- * Ie: "/webp-express-test/wordpress/wp-content/webp-express/webp-images/webp-express-test/wordpress/"
422
- * This is needed for the .htaccess
423
- */
424
- public static function getPathToExisting()
425
- {
426
- return self::getCacheDirRel() . '/doc-root/' . self::getHomeDirRel();
427
- }
428
-
429
  public static function getUrlsAndPathsForTheJavascript()
430
  {
431
  return [
@@ -436,9 +695,6 @@ APACHE
436
  'filePaths' => [
437
  'webpExpressRoot' => self::getWebPExpressPluginDirAbs(),
438
  'destinationRoot' => self::getCacheDirAbs(),
439
- 'configRelToDocRoot' => self::getConfigDirRel(),
440
- 'pluginRelToDocRoot' => self::getPluginDirRel(),
441
-
442
  ]
443
  ];
444
  }
8
 
9
  class Paths
10
  {
11
+ public static function areAllImageRootsWithinDocRoot() {
12
+ if (!PathHelper::isDocRootAvailable()) {
13
+ return false;
14
+ }
15
+
16
+ $roots = self::getImageRootIds();
17
+ foreach ($roots as $dirId) {
18
+ $dir = self::getAbsDirById($dirId);
19
+ if (!PathHelper::canCalculateRelPathFromDocRootToDir($dir)) {
20
+ return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * Check if we can use document root for calculating relative paths (which may not contain "/.." directory traversal)
28
+ *
29
+ * Note that this method allows document root to be outside open_basedir as long as document root is
30
+ * non-empty AND it is possible to calculate relative paths to all image roots (including "index").
31
+ * Here is a case when a relative CAN be calculated:
32
+ * - Document root is configured to "/var/www/website" - which is also the absolute file path.
33
+ * - open_basedir is set to "/var/www/website/wordpress"
34
+ * - uploads is in "/var/www/website/wordpress/wp-content/uploads" (within open_basedir, as it should)
35
+ * - "/wp-uploads" symlinks to "/var/www/website/wordpress")
36
+ * - Wordpress has been configured to use "/wp-uploads" path for uploads.
37
+ *
38
+ * What happens?
39
+ * First, it is tested if the configured upload path ("/wp-uploads") begins with the configured document root ("/var/www/website").
40
+ * This fails.
41
+ * Next, it is tested if the uploads path can be resolved. It can, as it is within the open_basedir.
42
+ * Next, it is tested if the *resolved* the uploads path begins with the configured document root.
43
+ * As "/var/www/website/wordpress/wp-content/uploads" begins with "/var/www/website", we have a match.
44
+ * The relative path can be calculated to be "wordpress/wp-content/uploads".
45
+ * Later, when the relative path is used, it will be used as $docRoot + "/" + $relPath, which
46
+ * will be "/var/www/website/wordpress/wp-content/uploads". All is well.
47
+ *
48
+ * Here is a case where it CAN NOT be calculated:
49
+ * - Document root is configured to "/the-website", which symlinks to "/var/www/website"
50
+ * - open_basedir is set to "/var/www/website/wordpress"
51
+ * - uploads is in "/var/www/website/wordpress/wp-content/uploads" and wordpress is configured to use that upload path.
52
+ *
53
+ * What happens?
54
+ * First, it is tested if the configured upload path begins with the configured document root
55
+ * "/var/www/website/wordpress/wp-content/uploads" does not begin with "/the-website", so it fails.
56
+ * Next, it is tested if the *resolved* the uploads path begins with the configured document root.
57
+ * The resolved uploads path is the same as the configured so it also fails.
58
+ * Next, it is tested if Document root can be resolved. It can not, as the resolved path is not within open_basedir.
59
+ * If it could, it would have been tested if the resolved path begins with the resolved document root and we would have
60
+ * gotten a yes, and the relative path would have been "wordpress/wp-content/uploads" and it would work.
61
+ * However: Document root could not be resolved and we could not get a result.
62
+ * To sum the scenario up:
63
+ * If document root is configured to a symlink which cannot be resolved then it will only be possible to get relative paths
64
+ * when all other configured paths begins are relative to that symlink.
65
+ */
66
+ public static function canUseDocRootForRelPaths() {
67
+ if (!PathHelper::isDocRootAvailable()) {
68
+ return false;
69
+ }
70
+ return self::areAllImageRootsWithinDocRoot();
71
+ }
72
+
73
+ public static function canCalculateRelPathFromDocRootToDir($absPath) {
74
+ }
75
+
76
+ /**
77
+ * Check if we can use document root for structuring the cache dir.
78
+ *
79
+ * In order to structure the images by doc-root, WebP Express needs all images to be within document root.
80
+ * Does WebP Express in addition to this need to be able to resolve document root?
81
+ * Short answer is yes.
82
+ * The long answer is available as a comment inside ConvertHelperIndependent::getDestination()
83
+ *
84
+ */
85
+ public static function canUseDocRootForStructuringCacheDir() {
86
+ return (PathHelper::isDocRootAvailableAndResolvable() && self::canUseDocRootForRelPaths());
87
+ }
88
+
89
+ public static function docRootStatusText()
90
+ {
91
+ if (!PathHelper::isDocRootAvailable()) {
92
+ if (!isset($_SERVER['DOCUMENT_ROOT'])) {
93
+ return 'Unavailable (DOCUMENT_ROOT is not set in the global $_SERVER var)';
94
+ }
95
+ if ($_SERVER['DOCUMENT_ROOT'] == '') {
96
+ return 'Unavailable (empty string)';
97
+ }
98
+ return 'Unavailable';
99
+ }
100
+
101
+ $imageRootsWithin = self::canUseDocRootForRelPaths();
102
+ if (!PathHelper::isDocRootAvailableAndResolvable()) {
103
+ $status = 'Available, but either non-existing or not within open_basedir.' .
104
+ ($imageRootsWithin ? '' : ' And not all image roots are within that document root.');
105
+ } elseif (!$imageRootsWithin) {
106
+ $status = 'Available, but not all image roots are within that document root.';
107
+ } else {
108
+ $status = 'Available and its "realpath" is available too.';
109
+ }
110
+ if (self::canUseDocRootForStructuringCacheDir()) {
111
+ $status .= ' Can be used for structuring cache dir.';
112
+ } else {
113
+ $status .= ' Cannot be used for structuring cache dir.';
114
+ }
115
+ return $status;
116
+ }
117
+
118
+ public static function getAbsDirId($absDir) {
119
+ switch ($absDir) {
120
+ case self::getContentDirAbs():
121
+ return 'wp-content';
122
+ case self::getIndexDirAbs():
123
+ return 'index';
124
+ case self::getHomeDirAbs():
125
+ return 'home';
126
+ case self::getPluginDirAbs():
127
+ return 'plugins';
128
+ case self::getUploadDirAbs():
129
+ return 'uploads';
130
+ case self::getThemesDirAbs():
131
+ return 'themes';
132
+ case self::getCacheDirAbs():
133
+ return 'cache';
134
+ }
135
+ return false;
136
+ }
137
+
138
+ public static function getAbsDirById($dirId) {
139
+ switch ($dirId) {
140
+ case 'wp-content':
141
+ return self::getContentDirAbs();
142
+ case 'index':
143
+ return self::getIndexDirAbs();
144
+ case 'home':
145
+ // "home" is still needed (used in PluginDeactivate.php)
146
+ return self::getHomeDirAbs();
147
+ case 'plugins':
148
+ return self::getPluginDirAbs();
149
+ case 'uploads':
150
+ return self::getUploadDirAbs();
151
+ case 'themes':
152
+ return self::getThemesDirAbs();
153
+ case 'cache':
154
+ return self::getCacheDirAbs();
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Get ids for folders where SOURCE images may reside
161
+ */
162
+ public static function getImageRootIds() {
163
+ return ['uploads', 'themes', 'plugins', 'wp-content', 'index'];
164
+ }
165
+
166
+ public static function findImageRootOfPath($path, $rootIdsToSearch) {
167
+ foreach ($rootIdsToSearch as $rootId) {
168
+ if (PathHelper::isPathWithinExistingDirPath($path, self::getAbsDirById($rootId))) {
169
+ return $rootId;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+
175
+ public static function getImageRootsDefForSelectedIds($ids) {
176
+ $canUseDocRootForRelPaths = self::canUseDocRootForRelPaths();
177
+
178
+ $mapping = [];
179
+ foreach ($ids as $rootId) {
180
+ $obj = [
181
+ 'id' => $rootId,
182
+ ];
183
+ $absPath = self::getAbsDirById($rootId);
184
+ if ($canUseDocRootForRelPaths) {
185
+ $obj['rel-path'] = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($absPath);
186
+ } else {
187
+ $obj['abs-path'] = $absPath;
188
+ }
189
+ $obj['url'] = self::getUrlById($rootId);
190
+ $mapping[] = $obj;
191
+ }
192
+ return $mapping;
193
+ }
194
+
195
+ public static function getImageRootsDef()
196
+ {
197
+ return self::getImageRootsDefForSelectedIds(self::getImageRootIds());
198
+ }
199
+
200
+ public static function filterOutSubRoots($rootIds)
201
+ {
202
+ // Get dirs of enabled roots
203
+ $dirs = [];
204
+ foreach ($rootIds as $rootId) {
205
+ $dirs[] = self::getAbsDirById($rootId);
206
+ }
207
+
208
+ // Filter out dirs which are below other dirs
209
+ $dirsToSkip = [];
210
+ foreach ($dirs as $dirToExamine) {
211
+ foreach ($dirs as $dirToCompareAgainst) {
212
+ if ($dirToExamine == $dirToCompareAgainst) {
213
+ continue;
214
+ }
215
+ if (self::isDirInsideDir($dirToExamine, $dirToCompareAgainst)) {
216
+ $dirsToSkip[] = $dirToExamine;
217
+ break;
218
+ }
219
+ }
220
+ }
221
+ $dirs = array_diff($dirs, $dirsToSkip);
222
+
223
+ // back to ids
224
+ $result = [];
225
+ foreach ($dirs as $dir) {
226
+ $result[] = self::getAbsDirId($dir);
227
+ }
228
+ return $result;
229
+ }
230
 
231
  public static function createDirIfMissing($dir)
232
  {
247
  return (substr($rel, 0, 3) != '../');
248
  }
249
 
 
 
 
 
 
 
 
 
 
250
  /**
251
  * Return absolute dir.
252
+ *
253
+ * - Path is canonicalized (without resolving symlinks)
254
  * - trailing dash is removed - we don't use that around here.
255
  *
256
+ * We do not resolve symlinks anymore. Information was lost that way.
257
+ * And in some cases we needed the unresolved path - for example in the .htaccess.
 
258
  */
259
  public static function getAbsDir($dir)
260
  {
261
+ $dir = PathHelper::canonicalize($dir);
262
+ return rtrim($dir, '/');
263
+ /*
264
  $result = realpath($dir);
265
  if ($result === false) {
266
  $dir = PathHelper::canonicalize($dir);
267
  } else {
268
  $dir = $result;
269
+ }*/
 
 
 
 
270
 
 
 
 
271
  }
272
 
273
  // ------------ Home Dir -------------
274
 
275
+ // PS: Home dir is not the same as index dir.
276
+ // For example, if Wordpress folder has been moved (method 2), the home dir could be below.
277
  public static function getHomeDirAbs()
278
  {
279
  if (!function_exists('get_home_path')) {
282
  return self::getAbsDir(get_home_path());
283
  }
284
 
285
+ // ------------ Index Dir (WP root dir) -------------
286
+ // (The Wordpress installation dir- where index.php and wp-load.php resides)
 
 
 
 
 
287
 
288
  public static function getIndexDirAbs()
289
  {
290
+ // We used to return self::getAbsDir(ABSPATH), which used realpath.
291
+ // It has been changed now, as it seems we do not need realpath for ABSPATH, as it is defined
292
+ // (in wp-load.php) as dirname(__FILE__) . "/" and according to this link, __FILE__ returns resolved paths:
293
+ // https://stackoverflow.com/questions/3221771/how-do-you-get-php-symlinks-and-file-to-work-together-nicely
294
+ // AND a user reported an open_basedir restriction problem thrown by realpath($_SERVER['DOCUMENT_ROOT']),
295
+ // due to symlinking and opendir restriction (see #322)
296
 
297
+ return rtrim(ABSPATH, '/');
298
+
299
+ // TODO: read up on this, regarding realpath:
300
+ // https://github.com/twigphp/Twig/issues/2707
301
 
302
+ }
303
 
304
  // ------------ .htaccess dir -------------
305
  // (directory containing the relevant .htaccess)
311
  return FileHelper::canEditOrCreateFileHere($dirName . '/.htaccess');
312
  }
313
 
314
+ public static function canWriteHTAccessRulesInDir($dirId) {
315
+ return self::canWriteHTAccessRulesHere(self::getAbsDirById($dirId));
316
+ }
317
+
318
  public static function returnFirstWritableHTAccessDir($dirs)
319
  {
320
  foreach ($dirs as $dir) {
333
  }
334
  public static function getContentDirRel()
335
  {
336
+ return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getContentDirAbs());
337
+ }
338
+ public static function getContentDirRelToPluginDir()
339
+ {
340
+ return PathHelper::getRelDir(self::getPluginDirAbs(), self::getContentDirAbs());
341
  }
342
 
343
+
344
  public static function isWPContentDirMoved()
345
  {
346
  return (self::getContentDirAbs() != (ABSPATH . 'wp-content'));
351
  return !(self::isDirInsideDir(self::getContentDirAbs(), ABSPATH));
352
  }
353
 
354
+ // ------------ Themes Dir -------------
355
+
356
+ public static function getThemesDirAbs()
357
+ {
358
+ return self::getContentDirAbs() . '/themes';
359
+ }
360
 
361
  // ------------ WebPExpress Content Dir -------------
362
  // (the "webp-express" directory inside wp-content)
368
 
369
  public static function getWebPExpressContentDirRel()
370
  {
371
+ return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getWebPExpressContentDirAbs());
372
  }
373
 
374
  public static function createContentDirIfMissing()
384
  }
385
  public static function getUploadDirRel()
386
  {
387
+ return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getUploadDirAbs());
388
  }
389
 
390
  /*
416
 
417
  public static function getConfigDirRel()
418
  {
419
+ return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getConfigDirAbs());
420
  }
421
 
422
  public static function createConfigDirIfMissing()
460
  return self::getWebPExpressContentDirAbs() . '/webp-images';
461
  }
462
 
463
+ public static function getCacheDirRelToDocRoot()
464
+ {
465
+ return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getCacheDirAbs());
466
+ }
467
+
468
+ public static function getCacheDirForImageRoot($destinationFolder, $destinationStructure, $imageRootId)
469
  {
470
+ if (($destinationFolder == 'mingled') && ($imageRootId == 'uploads')) {
471
+ return self::getUploadDirAbs();
472
+ }
473
+
474
+ if ($destinationStructure == 'doc-root') {
475
+ $relPath = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(
476
+ self::getAbsDirById($imageRootId)
477
+ );
478
+ return self::getCacheDirAbs() . '/doc-root/' . $relPath;
479
+ } else {
480
+ return self::getCacheDirAbs() . '/' . $imageRootId;
481
+ }
482
  }
483
 
484
  public static function createCacheDirIfMissing()
486
  return self::createDirIfMissing(self::getCacheDirAbs());
487
  }
488
 
489
+ // ------------ Log Dir -------------
490
 
491
  public static function getLogDirAbs()
492
  {
500
  return self::getAbsDir(WP_PLUGIN_DIR);
501
  }
502
 
 
 
 
 
503
 
504
  public static function isPluginDirMovedOutOfAbsPath()
505
  {
518
  return self::getAbsDir(WEBPEXPRESS_PLUGIN_DIR);
519
  }
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  // ------------------------------------
522
  // --------- Url paths ----------
523
  // ------------------------------------
544
  return ltrim($path, '/\\');
545
  }
546
 
547
+ public static function getUrlById($dirId) {
548
+ switch ($dirId) {
549
+ case 'wp-content':
550
+ return self::getContentUrl();
551
+ case 'index':
552
+ return self::getHomeUrl();
553
+ case 'home':
554
+ return self::getHomeUrl();
555
+ case 'plugins':
556
+ return self::getPluginUrl();
557
+ case 'uploads':
558
+ return self::getUploadUrl();
559
+ case 'themes':
560
+ return self::getThemesUrl();
561
+ }
562
+ return false;
563
+ }
564
+
565
+ /**
566
+ * Get destination root url and path, provided rootId and some configuration options
567
+ *
568
+ * This method kind of establishes the overall structure of the cache dir.
569
+ * (but not quite, as the logic is also in ConverterHelper::getDestination).
570
+ *
571
+ * @param string $rootId
572
+ * @param string $destinationFolder ("mingled" or "separate")
573
+ * @param string $destinationStructure ("doc-root" or "image-roots")
574
+ *
575
+ * @return array url and abs-path of destination root
576
+ */
577
+ public static function destinationRoot($rootId, $destinationFolder, $destinationStructure)
578
+ {
579
+ if (($destinationFolder == 'mingled') && ($rootId == 'uploads')) {
580
+ return [
581
+ 'url' => self::getUrlById('uploads'),
582
+ 'abs-path' => self::getUploadDirAbs()
583
+ ];
584
+ } else {
585
+
586
+ // Its within these bases:
587
+ $destUrl = self::getUrlById('wp-content') . '/webp-express/webp-images';
588
+ $destPath = self::getAbsDirById('wp-content') . '/webp-express/webp-images';
589
+
590
+ if (($destinationStructure == 'doc-root') && self::canUseDocRootForStructuringCacheDir()) {
591
+ $relPathFromDocRootToSourceImageRoot = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(
592
+ self::getAbsDirById($rootId)
593
+ );
594
+ return [
595
+ 'url' => $destUrl . '/doc-root/' . $relPathFromDocRootToSourceImageRoot,
596
+ 'abs-path' => $destPath . '/doc-root/' . $relPathFromDocRootToSourceImageRoot
597
+ ];
598
+ } else {
599
+ return [
600
+ 'url' => $destUrl . '/' . $rootId,
601
+ 'abs-path' => $destPath . '/' . $rootId
602
+ ];
603
+ }
604
+ }
605
+ }
606
+
607
+ public static function getUrlPathById($dirId) {
608
+ return self::getUrlPathFromUrl(self::getUrlById($dirId));
609
+ }
610
+
611
+
612
  // Get complete home url (no trailing slash). Ie: "http://example.com/blog"
613
  public static function getHomeUrl()
614
  {
615
+ if (!function_exists('home_url')) {
616
  // silence is golden?
617
+ // bad joke. Need to handle this...
618
  }
619
  return untrailingslashit(home_url());
620
  }
649
  return self::getUrlPathFromUrl(self::getContentUrl());
650
  }
651
 
652
+ public static function getThemesUrl()
653
+ {
654
+ return self::getContentUrl() . '/themes';
655
+ }
656
 
657
  /**
658
  * Get Url to WebP Express plugin (this is in fact an incomplete URL, you need to append ie '/webp-on-demand.php' to get a full URL)
685
  return self::getHomeUrl() . '/webp-express-web-service';
686
  }
687
 
 
 
 
 
 
 
 
 
 
 
 
688
  public static function getUrlsAndPathsForTheJavascript()
689
  {
690
  return [
695
  'filePaths' => [
696
  'webpExpressRoot' => self::getWebPExpressPluginDirAbs(),
697
  'destinationRoot' => self::getCacheDirAbs(),
 
 
 
698
  ]
699
  ];
700
  }
lib/classes/PlatformInfo.php CHANGED
@@ -23,39 +23,88 @@ class PlatformInfo
23
  return ( strpos( $server, 'litespeed') !== false );
24
  }
25
 
 
 
 
 
 
26
  public static function isApacheOrLiteSpeed()
27
  {
28
  return self::isApache() || self::isLiteSpeed();
29
  }
30
 
31
  /**
32
- * It is not always possible to determine if apache has a given module...
33
- * We shall not fool anyone into thinking otherwise by providing a "got" method like Wordpress does...
 
 
 
 
 
 
 
 
34
  */
35
- public static function definitelyNotGotApacheModule($mod)
36
  {
37
- if (function_exists( 'apache_get_modules')) {
38
- $mods = apache_get_modules();
39
- if (!in_array($mod, $mods)) {
40
- return true;
 
 
 
 
 
 
 
41
  }
42
  }
43
- // TODO: Perhaps also try looking at phpinfo, like Wordpress does in apache_mod_loaded
44
 
45
- return false;
 
 
 
 
 
 
46
  }
47
 
 
 
 
 
48
  public static function definitelyGotApacheModule($mod)
49
  {
50
- if (function_exists( 'apache_get_modules')) {
51
- $mods = apache_get_modules();
52
- if (in_array($mod, $mods)) {
53
- return true;
54
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
- return false;
 
 
 
 
 
 
57
  }
58
 
 
59
  public static function definitelyNotGotModRewrite()
60
  {
61
  return self::definitelyNotGotApacheModule('mod_rewrite');
23
  return ( strpos( $server, 'litespeed') !== false );
24
  }
25
 
26
+ public static function isNginx()
27
+ {
28
+ return (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false);
29
+ }
30
+
31
  public static function isApacheOrLiteSpeed()
32
  {
33
  return self::isApache() || self::isLiteSpeed();
34
  }
35
 
36
  /**
37
+ * Check if an Apache module is available.
38
+ *
39
+ * If apache_get_modules() exists, it is used. That function is however only available in mod_php installs.
40
+ * Otherwise the Wordpress function "apache_mod_loaded" is tried, which examines phpinfo() output.
41
+ * However, it seems there is no module output on php-fpm setups.
42
+ * So on php-fpm, we cannot come with an answer.
43
+ * https://stackoverflow.com/questions/9021425/how-to-check-if-mod-rewrite-is-enabled-in-php
44
+ *
45
+ * @param string $mod Name of module - ie "mod_rewrite"
46
+ * @return boolean|null Return if module is available, or null if indeterminate
47
  */
48
+ public static function gotApacheModule($mod)
49
  {
50
+ if (function_exists('apache_get_modules')) {
51
+ return in_array($mod, apache_get_modules());
52
+ }
53
+
54
+ // Revert to Wordpress method, which examines output from phpinfo as well
55
+ if (function_exists('apache_mod_loaded')) {
56
+ $result = apache_mod_loaded($mod, null);
57
+
58
+ // If we got a real result, return it.
59
+ if ($result != null) {
60
+ return $result;
61
  }
62
  }
 
63
 
64
+ // We could run shell_exec("apachectl -l"), as suggested here:
65
+ // https://stackoverflow.com/questions/9021425/how-to-check-if-mod-rewrite-is-enabled-in-php
66
+ // But it does not seem to return all modules in my php-fpm setup.
67
+
68
+ // Currently we got no more tools.
69
+ return null;
70
+
71
  }
72
 
73
+ /**
74
+ * It is not always possible to determine if apache has a given module...
75
+ * We shall not fool anyone into thinking otherwise by providing a "got" method like Wordpress does...
76
+ */
77
  public static function definitelyGotApacheModule($mod)
78
  {
79
+ return (self::gotApacheModule($mod) === true);
80
+ }
81
+
82
+ public static function definitelyNotGotApacheModule($mod)
83
+ {
84
+ return (self::gotApacheModule($mod) === false);
85
+ }
86
+
87
+ /**
88
+ * Check if mod_rewrite or IIS rewrite is available.
89
+ *
90
+ * @return boolean|null Return bool if it can be determined, or null if not
91
+ */
92
+ public static function gotRewriteModule()
93
+ {
94
+ $gotModRewrite = self::gotApacheModule('mod_rewrite');
95
+ if (!is_null($gotModRewrite)) {
96
+ return $gotModRewrite;
97
  }
98
+
99
+ // Got the IIS check here: https://stackoverflow.com/a/21249745/842756
100
+ // but have not tested it...
101
+ if (isset($_SERVER['IIS_UrlRewriteModule'])) {
102
+ return true;
103
+ }
104
+ return null;
105
  }
106
 
107
+
108
  public static function definitelyNotGotModRewrite()
109
  {
110
  return self::definitelyNotGotApacheModule('mod_rewrite');
lib/classes/PluginActivate.php CHANGED
@@ -28,7 +28,8 @@ class PluginActivate
28
 
29
  private static function reactivate()
30
  {
31
- $config = Config::loadConfig();
 
32
  if ($config === false) {
33
  Messenger::addMessage(
34
  'error',
@@ -123,7 +124,7 @@ class PluginActivate
123
  '<a href="' . Paths::getSettingsUrl() . '">configure it here</a>.'
124
  );
125
 
126
- // While not neccessary, lets get those tests copied right away. Some servers are a bit slow to pick up on changes in the filesystem
127
  CapabilityTest::copyCapabilityTestsToWpContent();
128
  }
129
  }
28
 
29
  private static function reactivate()
30
  {
31
+ $config = Config::loadConfigAndFix(false); // false, because we do not need to test if quality detection is working
32
+
33
  if ($config === false) {
34
  Messenger::addMessage(
35
  'error',
124
  '<a href="' . Paths::getSettingsUrl() . '">configure it here</a>.'
125
  );
126
 
127
+ // While not necessary, lets get those tests copied right away. Some servers are a bit slow to pick up on changes in the filesystem
128
  CapabilityTest::copyCapabilityTestsToWpContent();
129
  }
130
  }
lib/classes/PluginDeactivate.php CHANGED
@@ -2,21 +2,24 @@
2
 
3
  namespace WebPExpress;
4
 
5
- use \WebPExpress\HTAccess;
6
- use \WebPExpress\Messenger;
7
-
8
  class PluginDeactivate
9
  {
10
  // The hook was registred in AdminInit
11
  public static function deactivate() {
12
 
13
- $result = HTAccess::deactivateHTAccessRules();
14
- if ($result !== true) {
 
15
  // Oh no. We failed removing the rules
16
  $msg = "<b>Sorry, can't let you disable WebP Express!</b><br>" .
17
  'There are rewrite rules in the <i>.htaccess</i> that could not be removed. If these are not removed, it would break all images.<br>' .
18
  'Please make your <i>.htaccess</i> writable and then try to disable WebPExpress again.<br>Alternatively, remove the rules manually in your <i>.htaccess</i> file and try disabling again.' .
19
- '<br>It concerns the following files:<br>' . implode('<br>', $result);
 
 
 
 
 
20
 
21
  Messenger::addMessage(
22
  'error',
2
 
3
  namespace WebPExpress;
4
 
 
 
 
5
  class PluginDeactivate
6
  {
7
  // The hook was registred in AdminInit
8
  public static function deactivate() {
9
 
10
+ list($success, $failures, $successes) = HTAccess::deactivateHTAccessRules();
11
+
12
+ if (!$success) {
13
  // Oh no. We failed removing the rules
14
  $msg = "<b>Sorry, can't let you disable WebP Express!</b><br>" .
15
  'There are rewrite rules in the <i>.htaccess</i> that could not be removed. If these are not removed, it would break all images.<br>' .
16
  'Please make your <i>.htaccess</i> writable and then try to disable WebPExpress again.<br>Alternatively, remove the rules manually in your <i>.htaccess</i> file and try disabling again.' .
17
+ '<br>It concerns the following files:<br>';
18
+
19
+
20
+ foreach ($failures as $rootId) {
21
+ $msg .= '- ' . Paths::getAbsDirById($rootId) . '/.htaccess<br>';
22
+ }
23
 
24
  Messenger::addMessage(
25
  'error',
lib/classes/SanityCheck.php CHANGED
@@ -2,12 +2,22 @@
2
 
3
  namespace WebPExpress;
4
 
 
5
  use \WebPExpress\Sanitize;
6
  use \WebPExpress\SanityException;
7
 
8
  class SanityCheck
9
  {
10
 
 
 
 
 
 
 
 
 
 
11
  /**
12
  *
13
  * @param string $input string to test for NUL char
@@ -15,7 +25,7 @@ class SanityCheck
15
  public static function mustBeString($input, $errorMsg = 'String expected')
16
  {
17
  if (gettype($input) !== 'string') {
18
- throw new SanityException($errorMsg);
19
  }
20
  return $input;
21
  }
@@ -30,7 +40,7 @@ class SanityCheck
30
  {
31
  self::mustBeString($input);
32
  if (strpos($input, chr(0)) !== false) {
33
- throw new SanityException($errorMsg);
34
  }
35
  return $input;
36
  }
@@ -43,12 +53,12 @@ class SanityCheck
43
  *
44
  * @param string $input string to test for control characters
45
  */
46
- public static function noControlChars($input)
47
  {
48
  self::mustBeString($input);
49
  self::noNUL($input);
50
  if (preg_match('#[\x{0}-\x{1f}]#', $input)) {
51
- throw new SanityException('Control characters are not allowed');
52
  }
53
  return $input;
54
  }
@@ -61,7 +71,7 @@ class SanityCheck
61
  public static function notEmpty($input, $errorMsg = 'Must be non-empty')
62
  {
63
  if (empty($input)) {
64
- throw new SanityException($input);
65
  }
66
  return $input;
67
  }
@@ -73,7 +83,7 @@ class SanityCheck
73
  self::mustBeString($input);
74
  self::noControlChars($input);
75
  if (preg_match('#\.\.\/#', $input)) {
76
- throw new SanityException($errorMsg);
77
  }
78
  return $input;
79
  }
@@ -86,17 +96,16 @@ class SanityCheck
86
  // Prevent stream wrappers ("phar://", "php://" and the like)
87
  // https://www.php.net/manual/en/wrappers.phar.php
88
  if (preg_match('#^\\w+://#', Sanitize::removeNUL($input))) {
89
- throw new SanityException($errorMsg);
90
  }
91
  return $input;
92
  }
93
 
94
- public static function path($input)
95
  {
96
  self::notEmpty($input);
97
  self::mustBeString($input);
98
  self::noControlChars($input);
99
- self::noDirectoryTraversal($input);
100
  self::noStreamWrappers($input);
101
 
102
  // PS: The following sanitize has no effect, as we have just tested that there are no NUL and
@@ -108,32 +117,42 @@ class SanityCheck
108
 
109
  public static function pathWithoutDirectoryTraversal($input)
110
  {
111
- return self::path($input);
 
 
 
 
 
 
 
 
 
112
  }
113
 
 
114
  /**
115
  * Beware: This does not take symlinks into account.
116
  * I should make one that does. Until then, you should probably not call this method from outside this class
117
  */
118
- public static function pathBeginsWith($input, $beginsWith, $errorMsg = 'Path is outside allowed path')
119
  {
120
  self::path($input);
121
  if (!(strpos($input, $beginsWith) === 0)) {
122
- throw new SanityException($errorMsg);
123
  }
124
  return $input;
125
  }
126
 
127
- public static function pathBeginsWithSymLinksExpanded($input, $beginsWith, $errorMsg = 'Path is outside allowed path') {
128
- $closestExistingFolder = self::findClosestExistingFolderSymLinksExpanded($input);
129
  self::pathBeginsWith($closestExistingFolder, $beginsWith, $errorMsg);
130
  }
131
 
132
- public static function absPathMicrosoftStyle($input, $errorMsg = 'Not an fully qualified Windows path')
133
  {
134
  // On microsoft we allow [drive letter]:\
135
  if (!preg_match("#^[A-Z]:\\\\|/#", $input)) {
136
- throw new SanityException($errorMsg);
137
  }
138
  return $input;
139
  }
@@ -169,34 +188,91 @@ class SanityCheck
169
  if (self::isOnMicrosoft()) {
170
  self::absPathMicrosoftStyle($input);
171
  } else {
172
- throw new SanityException($errorMsg);
173
  }
174
  }
175
  return $input;
176
  }
177
 
178
- private static function findClosestExistingFolderSymLinksExpanded($input) {
179
- // Get closest existing folder with symlinks expanded.
180
- // this is a bit complicated, as the input path may not yet exist.
181
- // in case of realpath failure, we must try with one folder pealed off at the time
182
-
183
- $levelsUp = 1;
184
- while (true) {
185
- $dir = dirname($input, $levelsUp);
186
- $realPathResult = realpath($dir);
187
- if ($realPathResult !== false) {
188
- return $realPathResult;
189
- }
190
- if (($dir == '/') || (strlen($dir) < 4)) {
191
- return $dir;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
- $levelsUp++;
194
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  }
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  /**
198
  * Test that absolute path is in document root.
199
  *
 
 
 
200
  * It is acceptable if the absolute path does not exist
201
  */
202
  public static function absPathIsInDocRoot($input, $errorMsg = 'Path is outside document root')
@@ -208,9 +284,10 @@ class SanityCheck
208
  $docRoot = self::absPathExistsAndIsDir($docRoot);
209
 
210
  // Use realpath to expand symbolic links and check if it exists
211
- $docRootSymLinksExpanded = realpath($docRoot);
212
  if ($docRootSymLinksExpanded === false) {
213
- throw new SanityException('Cannot find document root');
 
214
  }
215
  $docRootSymLinksExpanded = rtrim($docRootSymLinksExpanded, '/');
216
  $docRootSymLinksExpanded = self::absPathExists($docRootSymLinksExpanded, 'Document root does not exist!');
@@ -226,11 +303,14 @@ class SanityCheck
226
  return $input;
227
  }
228
 
229
- public static function absPathExists($input, $errorMsg = 'Path does not exist')
230
  {
231
  self::absPath($input);
232
  if (@!file_exists($input)) {
233
- throw new SanityException($errorMsg);
 
 
 
234
  }
235
  return $input;
236
  }
@@ -239,9 +319,9 @@ class SanityCheck
239
  $input,
240
  $errorMsg = 'Path points to a file (it should point to a directory)'
241
  ) {
242
- self::absPathExists($input);
243
  if (!is_dir($input)) {
244
- throw new SanityException($errorMsg);
245
  }
246
  return $input;
247
  }
@@ -250,9 +330,9 @@ class SanityCheck
250
  $input,
251
  $errorMsg = 'Path points to a directory (it should not do that)'
252
  ) {
253
- self::absPathExists($input, 'File does not exist');
254
  if (@is_dir($input)) {
255
- throw new SanityException($errorMsg);
256
  }
257
  return $input;
258
  }
@@ -278,7 +358,7 @@ class SanityCheck
278
  self::noNUL($input);
279
  self::mustBeString($input);
280
  if (!preg_match($pattern, $input)) {
281
- throw new SanityException($errorMsg);
282
  }
283
  return $input;
284
  }
@@ -289,7 +369,7 @@ class SanityCheck
289
  self::mustBeString($input);
290
  self::notEmpty($input);
291
  if ((strpos($input, '[') !== 0) || (!is_array(json_decode($input)))) {
292
- throw new SanityException($errorMsg);
293
  }
294
  return $input;
295
  }
@@ -300,7 +380,7 @@ class SanityCheck
300
  self::mustBeString($input);
301
  self::notEmpty($input);
302
  if ((strpos($input, '{') !== 0) || (!is_object(json_decode($input)))) {
303
- throw new SanityException($errorMsg);
304
  }
305
  return $input;
306
  }
2
 
3
  namespace WebPExpress;
4
 
5
+ use \WebPExpress\PathHelper;
6
  use \WebPExpress\Sanitize;
7
  use \WebPExpress\SanityException;
8
 
9
  class SanityCheck
10
  {
11
 
12
+ private static function fail($errorMsg, $input)
13
+ {
14
+ // sanitize input before calling error_log(), it might be sent to file, mail, syslog etc.
15
+ error_log($errorMsg . '. input:' . Sanitize::removeNUL($input));
16
+ //error_log(get_magic_quotes_gpc() ? 'on' :'off');
17
+ throw new SanityException($errorMsg); // . '. Check debug.log for details (and make sure debugging is enabled)'
18
+ }
19
+
20
+
21
  /**
22
  *
23
  * @param string $input string to test for NUL char
25
  public static function mustBeString($input, $errorMsg = 'String expected')
26
  {
27
  if (gettype($input) !== 'string') {
28
+ self::fail($errorMsg, $input);
29
  }
30
  return $input;
31
  }
40
  {
41
  self::mustBeString($input);
42
  if (strpos($input, chr(0)) !== false) {
43
+ self::fail($errorMsg, $input);
44
  }
45
  return $input;
46
  }
53
  *
54
  * @param string $input string to test for control characters
55
  */
56
+ public static function noControlChars($input, $errorMsg = 'Control characters are not allowed')
57
  {
58
  self::mustBeString($input);
59
  self::noNUL($input);
60
  if (preg_match('#[\x{0}-\x{1f}]#', $input)) {
61
+ self::fail($errorMsg, $input);
62
  }
63
  return $input;
64
  }
71
  public static function notEmpty($input, $errorMsg = 'Must be non-empty')
72
  {
73
  if (empty($input)) {
74
+ self::fail($errorMsg, '');
75
  }
76
  return $input;
77
  }
83
  self::mustBeString($input);
84
  self::noControlChars($input);
85
  if (preg_match('#\.\.\/#', $input)) {
86
+ self::fail($errorMsg, $input);
87
  }
88
  return $input;
89
  }
96
  // Prevent stream wrappers ("phar://", "php://" and the like)
97
  // https://www.php.net/manual/en/wrappers.phar.php
98
  if (preg_match('#^\\w+://#', Sanitize::removeNUL($input))) {
99
+ self::fail($errorMsg, $input);
100
  }
101
  return $input;
102
  }
103
 
104
+ public static function pathDirectoryTraversalAllowed($input)
105
  {
106
  self::notEmpty($input);
107
  self::mustBeString($input);
108
  self::noControlChars($input);
 
109
  self::noStreamWrappers($input);
110
 
111
  // PS: The following sanitize has no effect, as we have just tested that there are no NUL and
117
 
118
  public static function pathWithoutDirectoryTraversal($input)
119
  {
120
+ self::pathDirectoryTraversalAllowed($input);
121
+ self::noDirectoryTraversal($input);
122
+ $input = Sanitize::path($input);
123
+
124
+ return $input;
125
+ }
126
+
127
+ public static function path($input)
128
+ {
129
+ return self::pathWithoutDirectoryTraversal($input);
130
  }
131
 
132
+
133
  /**
134
  * Beware: This does not take symlinks into account.
135
  * I should make one that does. Until then, you should probably not call this method from outside this class
136
  */
137
+ private static function pathBeginsWith($input, $beginsWith, $errorMsg = 'Path is outside allowed path')
138
  {
139
  self::path($input);
140
  if (!(strpos($input, $beginsWith) === 0)) {
141
+ self::fail($errorMsg, $input);
142
  }
143
  return $input;
144
  }
145
 
146
+ private static function pathBeginsWithSymLinksExpanded($input, $beginsWith, $errorMsg = 'Path is outside allowed path') {
147
+ $closestExistingFolder = PathHelper::findClosestExistingFolderSymLinksExpanded($input);
148
  self::pathBeginsWith($closestExistingFolder, $beginsWith, $errorMsg);
149
  }
150
 
151
+ private static function absPathMicrosoftStyle($input, $errorMsg = 'Not an fully qualified Windows path')
152
  {
153
  // On microsoft we allow [drive letter]:\
154
  if (!preg_match("#^[A-Z]:\\\\|/#", $input)) {
155
+ self::fail($errorMsg, $input);
156
  }
157
  return $input;
158
  }
188
  if (self::isOnMicrosoft()) {
189
  self::absPathMicrosoftStyle($input);
190
  } else {
191
+ self::fail($errorMsg, $input);
192
  }
193
  }
194
  return $input;
195
  }
196
 
197
+
198
+
199
+ public static function absPathInOneOfTheseRoots()
200
+ {
201
+
202
+ }
203
+
204
+
205
+ /**
206
+ * Look if filepath is within a dir path.
207
+ * Also tries expanding symlinks
208
+ *
209
+ * @param string $filePath Path to file. It may be non-existing.
210
+ * @param string $dirPath Path to dir. It must exist in order for symlinks to be expanded.
211
+ */
212
+ private static function isFilePathWithinExistingDirPath($filePath, $dirPath)
213
+ {
214
+ // sanity-check input. It must be a valid absolute filepath. It is allowed to be non-existing
215
+ self::absPath($filePath);
216
+
217
+ // sanity-check dir and that it exists.
218
+ self::absPathExistsAndIsDir($dirPath);
219
+
220
+ return PathHelper::isFilePathWithinDirPath($filePath, $dirPath);
221
+ }
222
+
223
+ /**
224
+ * Look if filepath is within multiple dir paths.
225
+ * Also tries expanding symlinks
226
+ *
227
+ * @param string $input Path to file. It may be non-existing.
228
+ * @param array $roots Allowed root dirs. Note that they must exist in order for symlinks to be expanded.
229
+ */
230
+ public static function filePathWithinOneOfTheseRoots($input, $roots, $errorMsg = 'The path is outside allowed roots.')
231
+ {
232
+ self::absPath($input);
233
+
234
+ foreach ($roots as $root) {
235
+ if (self::isFilePathWithinExistingDirPath($input, $root)) {
236
+ return $input;
237
  }
 
238
  }
239
+ self::fail($errorMsg, $input);
240
+ }
241
+
242
+ /*
243
+ public static function sourcePath($input, $errorMsg = 'The source path is outside allowed roots. It is only allowed to convert images that resides in: home dir, content path, upload dir and plugin dir.')
244
+ {
245
+ $validPaths = [
246
+ Paths::getHomeDirAbs(),
247
+ Paths::getIndexDirAbs(),
248
+ Paths::getContentDirAbs(),
249
+ Paths::getUploadDirAbs(),
250
+ Paths::getPluginDirAbs()
251
+ ];
252
+ return self::filePathWithinOneOfTheseRoots($input, $validPaths, $errorMsg);
253
  }
254
 
255
+ public static function destinationPath($input, $errorMsg = 'The destination path is outside allowed roots. The webps may only be stored in the upload folder and in the folder that WebP Express stores converted images in')
256
+ {
257
+ self::absPath($input);
258
+
259
+ // Webp Express only store converted images in upload folder and in its "webp-images" folder
260
+ // Check that destination path is within one of these.
261
+ $validPaths = [
262
+ '/var/www/webp-express-tests/we1'
263
+ //Paths::getUploadDirAbs(),
264
+ //Paths::getWebPExpressContentDirRel() . '/webp-images'
265
+ ];
266
+ return self::filePathWithinOneOfTheseRoots($input, $validPaths, $errorMsg);
267
+ }*/
268
+
269
+
270
  /**
271
  * Test that absolute path is in document root.
272
  *
273
+ * TODO: Instead of this method, we shoud check
274
+ *
275
+ *
276
  * It is acceptable if the absolute path does not exist
277
  */
278
  public static function absPathIsInDocRoot($input, $errorMsg = 'Path is outside document root')
284
  $docRoot = self::absPathExistsAndIsDir($docRoot);
285
 
286
  // Use realpath to expand symbolic links and check if it exists
287
+ $docRootSymLinksExpanded = @realpath($docRoot);
288
  if ($docRootSymLinksExpanded === false) {
289
+ $errorMsg = 'Cannot find document root';
290
+ self::fail($errorMsg, $input);
291
  }
292
  $docRootSymLinksExpanded = rtrim($docRootSymLinksExpanded, '/');
293
  $docRootSymLinksExpanded = self::absPathExists($docRootSymLinksExpanded, 'Document root does not exist!');
303
  return $input;
304
  }
305
 
306
+ public static function absPathExists($input, $errorMsg = 'Path does not exist or it is outside restricted basedir')
307
  {
308
  self::absPath($input);
309
  if (@!file_exists($input)) {
310
+ // TODO: We might be able to detect if the problem is that the path does not exist or if the problem
311
+ // is that it is outside restricted basedir.
312
+ // ie by creating an error handler or inspecting the php ini "open_basedir" setting
313
+ self::fail($errorMsg, $input);
314
  }
315
  return $input;
316
  }
319
  $input,
320
  $errorMsg = 'Path points to a file (it should point to a directory)'
321
  ) {
322
+ self::absPathExists($input, 'Directory does not exist or is outside restricted basedir');
323
  if (!is_dir($input)) {
324
+ self::fail($errorMsg, $input);
325
  }
326
  return $input;
327
  }
330
  $input,
331
  $errorMsg = 'Path points to a directory (it should not do that)'
332
  ) {
333
+ self::absPathExists($input, 'File does not exist or is outside restricted basedir');
334
  if (@is_dir($input)) {
335
+ self::fail($errorMsg, $input);
336
  }
337
  return $input;
338
  }
358
  self::noNUL($input);
359
  self::mustBeString($input);
360
  if (!preg_match($pattern, $input)) {
361
+ self::fail($errorMsg, $input);
362
  }
363
  return $input;
364
  }
369
  self::mustBeString($input);
370
  self::notEmpty($input);
371
  if ((strpos($input, '[') !== 0) || (!is_array(json_decode($input)))) {
372
+ self::fail($errorMsg, $input);
373
  }
374
  return $input;
375
  }
380
  self::mustBeString($input);
381
  self::notEmpty($input);
382
  if ((strpos($input, '{') !== 0) || (!is_object(json_decode($input)))) {
383
+ self::fail($errorMsg, $input);
384
  }
385
  return $input;
386
  }
lib/classes/SelfTest.php ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ class SelfTest
6
+ {
7
+
8
+ private static $next;
9
+
10
+ public static function allInfo()
11
+ {
12
+ self::$next = 'done';
13
+ $config = Config::loadConfigAndFix(false);
14
+ return SelfTestHelper::allInfo($config);
15
+ }
16
+
17
+
18
+ public static function systemInfo()
19
+ {
20
+ self::$next = 'configInfo';
21
+ return SelfTestHelper::systemInfo();
22
+ }
23
+
24
+ public static function configInfo()
25
+ {
26
+ self::$next = 'capabilityTests';
27
+ $config = Config::loadConfigAndFix(false);
28
+ return SelfTestHelper::configInfo($config);
29
+ }
30
+
31
+ public static function capabilityTests()
32
+ {
33
+ self::$next = 'done';
34
+ $config = Config::loadConfigAndFix(false);
35
+ return SelfTestHelper::capabilityTests($config);
36
+ }
37
+
38
+ public static function redirectToExisting()
39
+ {
40
+ self::$next = 'done';
41
+ list ($success, $result) = SelfTestRedirectToExisting::runTest();
42
+ return $result;
43
+ /*
44
+ $result = [];
45
+ $result[] = '# Redirection tests';
46
+ $modRewriteWorking = CapabilityTest::modRewriteWorking();
47
+ $modHeaderWorking = CapabilityTest::modHeaderWorking();
48
+
49
+ if (($modRewriteWorking === false) && ($modHeaderWorking)) {
50
+ //$result[] = 'mod_rewrite is not working';
51
+
52
+ if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) {
53
+
54
+ $result[] = 'You are on Nginx and the rules that WebP Express stores in the .htaccess files does not ' .
55
+ 'have any effect. '
56
+
57
+ }
58
+ // if (stripos($_SERVER["SERVER_SOFTWARE"], 'apache') !== false && stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
59
+
60
+ }
61
+
62
+ return [$result, 'done'];*/
63
+ }
64
+
65
+ public static function redirectToConverter()
66
+ {
67
+ self::$next = 'done';
68
+ list ($success, $result) = SelfTestRedirectToConverter::runTest();
69
+ return $result;
70
+ }
71
+
72
+ public static function redirectToWebPRealizer()
73
+ {
74
+ self::$next = 'done';
75
+ list ($success, $result) = SelfTestRedirectToWebPRealizer::runTest();
76
+ return $result;
77
+ }
78
+
79
+
80
+ public static function processAjax()
81
+ {
82
+ if (!check_ajax_referer('webpexpress-ajax-self-test-nonce', 'nonce', false)) {
83
+ wp_send_json_error('Invalid security nonce (it has probably expired - try refreshing)');
84
+ wp_die();
85
+ }
86
+
87
+ // Check input
88
+ // --------------
89
+ try {
90
+ // Check "testId"
91
+ $checking = '"testId" argument';
92
+ Validate::postHasKey('testId');
93
+
94
+ $testId = sanitize_text_field(stripslashes($_POST['testId']));
95
+
96
+ } catch (Exception $e) {
97
+ wp_send_json_error('Validation failed for ' . $checking . ': '. $e->getMessage());
98
+ wp_die();
99
+ }
100
+ $result = '';
101
+ if (method_exists(__CLASS__, $testId)) {
102
+
103
+ // The following call sets self::$next.
104
+ $result = call_user_func(array(__CLASS__, $testId));
105
+ } else {
106
+ $result = ['Unknown test: ' . $testId];
107
+ self::$next = 'break';
108
+ }
109
+
110
+ $response = [
111
+ 'result' => $result,
112
+ 'next' => self::$next
113
+ ];
114
+ echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
115
+ wp_die();
116
+ }
117
+
118
+ }
lib/classes/SelfTestHelper.php ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ use \WebPExpress\Paths;
6
+
7
+ class SelfTestHelper
8
+ {
9
+
10
+ public static function deleteFilesInDir($dir, $filePattern = "*")
11
+ {
12
+ foreach (glob($dir . DIRECTORY_SEPARATOR . $filePattern) as $filename) {
13
+ unlink($filename);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Remove files in dir and the dir. Does not remove files recursively.
19
+ */
20
+ public static function deleteDir($dir)
21
+ {
22
+ if (@file_exists($dir)) {
23
+ self::deleteFilesInDir($dir);
24
+ rmdir($dir);
25
+ }
26
+ }
27
+
28
+
29
+ public static function deleteTestImagesInFolder($rootId)
30
+ {
31
+ $testDir = Paths::getAbsDirById($rootId) . '/webp-express-test-images';
32
+ self::deleteDir($testDir);
33
+ }
34
+
35
+ public static function cleanUpTestImages($rootId, $config)
36
+ {
37
+ // Clean up test images in source folder
38
+ self::deleteTestImagesInFolder($rootId);
39
+
40
+ // Clean up dummy webp images in cache folder for the root
41
+ $cacheDirForRoot = Paths::getCacheDirForImageRoot(
42
+ $config['destination-folder'],
43
+ $config['destination-structure'],
44
+ $rootId
45
+ );
46
+
47
+ $testDir = $cacheDirForRoot . '/webp-express-test-images';
48
+ self::deleteDir($testDir);
49
+ }
50
+
51
+ public static function copyFile($source, $destination)
52
+ {
53
+ $result = [];
54
+ if (@copy($source, $destination)) {
55
+ return [true, $result];
56
+ } else {
57
+ $result[] = 'Failed to copy *' . $source . '* to *' . $destination . '*';
58
+ if (!@file_exists($source)) {
59
+ $result[] = 'The source file was not found';
60
+ } else {
61
+ if (!@file_exists(dirname($destination))) {
62
+ $result[] = 'The destination folder does not exist!';
63
+ } else {
64
+ $result[] = 'This is probably a permission issue. Check that your webserver has permission to ' .
65
+ 'write files in the directory (*' . dirname($destination) . '*)';
66
+ }
67
+ }
68
+ return [false, $result];
69
+ }
70
+ }
71
+
72
+ public static function randomDigitsAndLetters($length)
73
+ {
74
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
75
+ $charactersLength = strlen($characters);
76
+ $randomString = '';
77
+ for ($i = 0; $i < $length; $i++) {
78
+ $randomString .= $characters[rand(0, $charactersLength - 1)];
79
+ }
80
+ return $randomString;
81
+ }
82
+
83
+ public static function copyTestImageToRoot($rootId, $imageType = 'jpeg')
84
+ {
85
+ // TODO: Copy to a subfolder instead
86
+ // TODO: Use smaller jpeg / pngs please.
87
+ $result = [];
88
+ switch ($imageType) {
89
+ case 'jpeg':
90
+ $fileNameToCopy = 'very-small.jpg';
91
+ break;
92
+ case 'png':
93
+ $fileNameToCopy = 'test.png';
94
+ break;
95
+ }
96
+ $testSource = Paths::getPluginDirAbs() . '/webp-express/test/' . $fileNameToCopy;
97
+ $filenameOfDestination = self::randomDigitsAndLetters(6) . '.' . $imageType;
98
+ $result[] = 'Copying ' . strtoupper($imageType) . ' to ' . $rootId . ' folder (*webp-express-test-images/' . $filenameOfDestination . '*)';
99
+
100
+ $destDir = Paths::getAbsDirById($rootId) . '/webp-express-test-images';
101
+ $destination = $destDir . '/' . $filenameOfDestination;
102
+
103
+ if (!@file_exists($destDir)) {
104
+ if (!@mkdir($destDir)) {
105
+ $result[count($result) - 1] .= '. FAILED';
106
+ $result[] = 'Failed to create folder for test images: ' . $destDir;
107
+ return [$result, false, ''];
108
+ }
109
+ }
110
+
111
+ list($success, $errors) = self::copyFile($testSource, $destination);
112
+ if (!$success) {
113
+ $result[count($result) - 1] .= '. FAILED';
114
+ $result = array_merge($result, $errors);
115
+ return [$result, false, ''];
116
+ } else {
117
+ $result[count($result) - 1] .= '. ok!';
118
+ $result[] = 'We now have a ' . $imageType . ' stored here:';
119
+ $result[] = '*' . $destination . '*';
120
+ }
121
+ return [$result, true, $filenameOfDestination];
122
+ }
123
+
124
+ public static function copyTestImageToUploadFolder($imageType = 'jpeg')
125
+ {
126
+ return self::copyTestImageToRoot('uploads', $imageType);
127
+ }
128
+
129
+ public static function copyDummyWebPToCacheFolder($rootId, $destinationFolder, $destinationExtension, $destinationStructure, $sourceFileName, $imageType = 'jpeg')
130
+ {
131
+ $result = [];
132
+ $dummyWebP = Paths::getPluginDirAbs() . '/webp-express/test/test.jpg.webp';
133
+
134
+ $result[] = 'Copying dummy webp to the cache root for ' . $rootId;
135
+ $destDir = Paths::getCacheDirForImageRoot($destinationFolder, $destinationStructure, $rootId);
136
+ if (!file_exists($destDir)) {
137
+ $result[] = 'The folder did not exist. Creating folder at: ' . $destinationDir;
138
+ if (!mkdir($destDir, 0777, true)) {
139
+ $result[] = 'Failed creating folder!';
140
+ return [$result, false, ''];
141
+ }
142
+ }
143
+ $destDir .= '/webp-express-test-images';
144
+ if (!file_exists($destDir)) {
145
+ if (!mkdir($destDir, 0755, false)) {
146
+ $result[] = 'Failed creating the folder for the test images:';
147
+ $result[] = $destDir;
148
+ $result[] = 'To run this test, you must grant write permissions';
149
+ return [$result, false, ''];
150
+ }
151
+ }
152
+
153
+ $filenameOfDestination = ConvertHelperIndependent::appendOrSetExtension(
154
+ $sourceFileName,
155
+ $destinationFolder,
156
+ $destinationExtension,
157
+ ($rootId == 'uploads')
158
+ );
159
+
160
+ //$filenameOfDestination = $destinationFileNameNoExt . ($destinationExtension == 'append' ? '.' . $imageType : '') . '.webp';
161
+ $destination = $destDir . '/' . $filenameOfDestination;
162
+
163
+ list($success, $errors) = self::copyFile($dummyWebP, $destination);
164
+ if (!$success) {
165
+ $result[count($result) - 1] .= '. FAILED';
166
+ $result = array_merge($result, $errors);
167
+ return [$result, false, ''];
168
+ } else {
169
+ $result[count($result) - 1] .= '. ok!';
170
+ $result[] = 'We now have a webp file stored here:';
171
+ $result[] = '*' . $destination . '*';
172
+ $result[] = '';
173
+ }
174
+ return [$result, true, $destination];
175
+ }
176
+
177
+ public static function remoteGet($requestUrl, $args = [])
178
+ {
179
+ $result = [];
180
+ $return = wp_remote_get($requestUrl, $args);
181
+ if (is_wp_error($return)) {
182
+ $result[] = 'Request URL: ' . $requestUrl;
183
+ $result[] = 'The remote request errored!';
184
+ return [false, $result, [], $return];
185
+ }
186
+ if ($return['response']['code'] != '200') {
187
+ //$result[count($result) - 1] .= '. FAILED';
188
+ $result[] = 'Request URL: ' . $requestUrl;
189
+ $result[] = 'Response: ' . $return['response']['code'] . ' ' . $return['response']['message'];
190
+
191
+ if (isset($return['headers'])) {
192
+ $result = array_merge($result, SelfTestHelper::printHeaders($return['headers']));
193
+ } else {
194
+ $return['headers'] = [];
195
+ }
196
+ return [false, $result, $return['headers'], $return];
197
+ }
198
+ return [true, $result, $return['headers'], $return];
199
+ }
200
+
201
+ public static function hasHeaderContaining($headers, $headerToInspect, $containString)
202
+ {
203
+ if (!isset($headers[$headerToInspect])) {
204
+ return false;
205
+ }
206
+
207
+ // If there are multiple headers, check all
208
+ if (gettype($headers[$headerToInspect]) == 'string') {
209
+ $h = [$headers[$headerToInspect]];
210
+ } else {
211
+ $h = $headers[$headerToInspect];
212
+ }
213
+ foreach ($h as $headerValue) {
214
+ if (stripos($headerValue, $containString) !== false) {
215
+ return true;
216
+ }
217
+ }
218
+ return false;
219
+ }
220
+
221
+ public static function hasVaryAcceptHeader($headers)
222
+ {
223
+ if (!isset($headers['vary'])) {
224
+ return false;
225
+ }
226
+
227
+ // There may be multiple Vary headers. Or they might be combined in one.
228
+ // Both are acceptable, according to https://stackoverflow.com/a/28799169/842756
229
+ if (gettype($headers['vary']) == 'string') {
230
+ $varyHeaders = [$headers['vary']];
231
+ } else {
232
+ $varyHeaders = $headers['vary'];
233
+ }
234
+ foreach ($varyHeaders as $headerValue) {
235
+ $values = explode(',', $headerValue);
236
+ foreach ($values as $value) {
237
+ if (strtolower($value) == 'accept') {
238
+ return true;
239
+ }
240
+ }
241
+ }
242
+ return false;
243
+ }
244
+
245
+ public static function hasCacheControlOrExpiresHeader($headers)
246
+ {
247
+ if (isset($headers['cache-control'])) {
248
+ return true;
249
+ }
250
+ if (isset($headers['expires'])) {
251
+ return true;
252
+ }
253
+ return false;
254
+ }
255
+
256
+
257
+ public static function flattenHeaders($headers)
258
+ {
259
+ $result = [];
260
+ foreach ($headers as $headerName => $headerValue) {
261
+ if (gettype($headerValue) == 'array') {
262
+ foreach ($headerValue as $i => $value) {
263
+ $result[] = [$headerName, $value];
264
+ }
265
+ } else {
266
+ $result[] = [$headerName, $headerValue];
267
+ }
268
+ }
269
+ return $result;
270
+ }
271
+
272
+ public static function printHeaders($headers)
273
+ {
274
+ $result = [];
275
+ $result[] = '#### Response headers:';
276
+
277
+ $headersFlat = self::flattenHeaders($headers);
278
+ //
279
+ foreach ($headersFlat as $i => list($headerName, $headerValue)) {
280
+ if ($headerName == 'x-webp-express-error') {
281
+ $headerValue = '**' . $headerValue . '**{: .error}';
282
+ }
283
+ $result[] = '- ' . $headerName . ': ' . $headerValue;
284
+ }
285
+ $result[] = '';
286
+ return $result;
287
+ }
288
+
289
+ private static function trueFalseNullString($var)
290
+ {
291
+ if ($var === true) {
292
+ return 'yes';
293
+ }
294
+ if ($var === false) {
295
+ return 'no';
296
+ }
297
+ return 'could not be determined';
298
+ }
299
+
300
+ public static function systemInfo()
301
+ {
302
+ $result = [];
303
+ $result[] = '#### System info:';
304
+ $result[] = '- PHP version: ' . phpversion();
305
+ $result[] = '- OS: ' . PHP_OS;
306
+ $result[] = '- Server software: ' . $_SERVER["SERVER_SOFTWARE"];
307
+ $result[] = '- Document Root status: ' . Paths::docRootStatusText();
308
+ if (PathHelper::isDocRootAvailable()) {
309
+ $result[] = '- Document Root: ' . $_SERVER['DOCUMENT_ROOT'];
310
+ }
311
+ if (PathHelper::isDocRootAvailableAndResolvable()) {
312
+ if ($_SERVER['DOCUMENT_ROOT'] != realpath($_SERVER['DOCUMENT_ROOT'])) {
313
+ $result[] = '- Document Root (symlinked resolved): ' . realpath($_SERVER['DOCUMENT_ROOT']);
314
+ }
315
+ }
316
+
317
+ $result[] = '- Document Root: ' . Paths::docRootStatusText();
318
+ $result[] = '- Apache module "mod_rewrite" enabled?: ' . self::trueFalseNullString(PlatformInfo::gotApacheModule('mod_rewrite'));
319
+ $result[] = '- Apache module "mod_headers" enabled?: ' . self::trueFalseNullString(PlatformInfo::gotApacheModule('mod_headers'));
320
+ return $result;
321
+ }
322
+
323
+ public static function wordpressInfo()
324
+ {
325
+ $result = [];
326
+ $result[] = '#### Wordpress info:';
327
+ $result[] = '- Version: ' . get_bloginfo('version');
328
+ $result[] = '- Multisite?: ' . self::trueFalseNullString(is_multisite());
329
+ $result[] = '- Is wp-content moved?: ' . self::trueFalseNullString(Paths::isWPContentDirMoved());
330
+ $result[] = '- Is uploads moved out of wp-content?: ' . self::trueFalseNullString(Paths::isUploadDirMovedOutOfWPContentDir());
331
+ $result[] = '- Is plugins moved out of wp-content?: ' . self::trueFalseNullString(Paths::isPluginDirMovedOutOfWpContent());
332
+
333
+ $result[] = '';
334
+
335
+ $result[] = '#### Image roots:';
336
+ foreach (Paths::getImageRootIds() as $rootId) {
337
+ $absDir = Paths::getAbsDirById($rootId);
338
+
339
+ if (PathHelper::pathExistsAndIsResolvable($absDir) && ($absDir != realpath($absDir))) {
340
+ $result[] = '*' . $rootId . '*: ' . $absDir . ' (resolved for symlinks: ' . realpath($absDir) . ')';
341
+ } else {
342
+ $result[] = '*' . $rootId . '*: ' . $absDir;
343
+
344
+ }
345
+ }
346
+
347
+ $result[] = '#### Image roots (relative to document root)';
348
+ foreach (Paths::getImageRootIds() as $rootId) {
349
+ $absPath = Paths::getAbsDirById($rootId);
350
+ if (PathHelper::canCalculateRelPathFromDocRootToDir($absPath)) {
351
+ $result[] = '*' . $rootId . '*: ' . PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($absPath);
352
+ } else {
353
+ $result[] = '*' . $rootId . '*: ' . 'n/a (not within document root)';
354
+ }
355
+ }
356
+
357
+ return $result;
358
+ }
359
+
360
+ public static function configInfo($config)
361
+ {
362
+ $result = [];
363
+ $result[] = '#### WebP Express configuration info:';
364
+ $result[] = '- Destination folder: ' . $config['destination-folder'];
365
+ $result[] = '- Destination extension: ' . $config['destination-extension'];
366
+ $result[] = '- Destination structure: ' . $config['destination-structure'];
367
+ //$result[] = 'Image types: ' . ;
368
+ //$result[] = '';
369
+ $result[] = '(To view all configuration, take a look at the config file, which is stored in *' . Paths::getConfigFileName() . '*)';
370
+ return $result;
371
+ }
372
+
373
+ public static function htaccessInfo($config, $printRules = true)
374
+ {
375
+ $result = [];
376
+ //$result[] = '*.htaccess info:*';
377
+ //$result[] = '- Image roots with WebP Express rules: ' . implode(', ', HTAccess::getRootsWithWebPExpressRulesIn());
378
+ $result[] = '#### .htaccess files that WebP Express have placed rules in the following files:';
379
+ $rootIds = HTAccess::getRootsWithWebPExpressRulesIn();
380
+ foreach ($rootIds as $imageRootId) {
381
+ $result[] = '- ' . Paths::getAbsDirById($imageRootId) . '/.htaccess';
382
+ }
383
+
384
+ foreach ($rootIds as $imageRootId) {
385
+ $result = array_merge($result, self::rulesInImageRoot($config, $imageRootId));
386
+ }
387
+
388
+ return $result;
389
+ }
390
+
391
+ public static function rulesInImageRoot($config, $imageRootId)
392
+ {
393
+ $result = [];
394
+ $result[] = '#### WebP rules in *' . $imageRootId . '*:';
395
+ $file = Paths::getAbsDirById($imageRootId) . '/.htaccess';
396
+ if (!HTAccess::haveWeRulesInThisHTAccess($file)) {
397
+ $result[] = '**NONE!**{: .warn}';
398
+ } else {
399
+ $weRules = HTAccess::extractWebPExpressRulesFromHTAccess($file);
400
+ // remove unindented comments
401
+ //$weRules = preg_replace('/^\#\s[^\n\r]*[\n\r]+/ms', '', $weRules);
402
+
403
+ // remove comments in the beginning
404
+ $weRulesArr = preg_split("/\r\n|\n|\r/", $weRules); // https://stackoverflow.com/a/11165332/842756
405
+ while ((strlen($weRulesArr[0]) > 0) && ($weRulesArr[0][0] == '#')) {
406
+ array_shift($weRulesArr);
407
+ }
408
+ $weRules = implode("\n", $weRulesArr);
409
+
410
+ $result[] = '```' . $weRules . '```';
411
+ }
412
+ return $result;
413
+ }
414
+
415
+ public static function rulesInUpload($config)
416
+ {
417
+ return self::rulesInImageRoot($config, 'uploads');
418
+ }
419
+
420
+ public static function allInfo($config)
421
+ {
422
+ $result = [];
423
+ $result = array_merge($result, self::systemInfo());
424
+ $result = array_merge($result, self::wordpressInfo());
425
+ $result = array_merge($result, self::configInfo($config));
426
+ $result = array_merge($result, self::capabilityTests($config));
427
+ $result = array_merge($result, self::htaccessInfo($config, true));
428
+ //$result = array_merge($result, self::rulesInImageRoot($config, 'upload'));
429
+ //$result = array_merge($result, self::rulesInImageRoot($config, 'wp-content'));
430
+ return $result;
431
+ }
432
+
433
+ public static function capabilityTests($config)
434
+ {
435
+ $capTests = $config['base-htaccess-on-these-capability-tests'];
436
+ $result = [];
437
+ $result[] = '#### Live tests of .htaccess capabilities:';
438
+ /*$result[] = 'Exactly what you can do in a *.htaccess* depends on the server setup. WebP Express ' .
439
+ 'makes some live tests to verify if a certain feature in fact works. This is done by creating ' .
440
+ 'test files (*.htaccess* files and php files) in a dir inside the content dir and running these. ' .
441
+ 'These test results are used when creating the rewrite rules. Here are the results:';*/
442
+
443
+ // $result[] = '';
444
+ $result[] = '- mod_rewrite working?: ' . self::trueFalseNullString(CapabilityTest::modRewriteWorking());
445
+ $result[] = '- mod_header working?: ' . self::trueFalseNullString($capTests['modHeaderWorking']);
446
+ /*$result[] = '- pass variable from *.htaccess* to script through header working?: ' .
447
+ self::trueFalseNullString($capTests['passThroughHeaderWorking']);*/
448
+ $result[] = '- passing variables from *.htaccess* to PHP script through environment variable working?: ' . self::trueFalseNullString($capTests['passThroughEnvWorking']);
449
+ return $result;
450
+ }
451
+
452
+ public static function diagnoseFailedRewrite($config)
453
+ {
454
+ if (($config['destination-structure'] == 'image-roots') && (!PathHelper::isDocRootAvailableAndResolvable())) {
455
+ $result[] = 'The problem is probably this combination:';
456
+ if (!PathHelper::isDocRootAvailable()) {
457
+ $result[] = '1. Your document root isn`t available';
458
+ } else {
459
+ $result[] = '1. Your document root isn`t resolvable for symlinks (it is probably subject to open_basedir restriction)';
460
+ }
461
+ $result[] = '2. Your document root is symlinked';
462
+ $result[] = '3. The wordpress function that tells the path of the uploads folder returns the symlink resolved path';
463
+
464
+ $result[] = 'I cannot check if your document root is in fact symlinked (as document root isnt resolvable). ' .
465
+ 'But if it is, there you have it. The line beginning with "RewriteCond %{REQUEST_FILENAME}"" points to your resolved root, ' .
466
+ 'but it should point to your symlinked root. WebP Express cannot do that for you because it cannot discover what the symlink is. ' .
467
+ 'Try changing the line manually. When it works, you can move the rules outside the WebP Express block so they dont get ' .
468
+ 'overwritten. OR you can change your server configuration (document root / open_basedir restrictions)';
469
+ }
470
+
471
+ //$result[] = '## Diagnosing';
472
+ if (PlatformInfo::isNginx()) {
473
+ // Nginx
474
+ $result[] = 'Notice that you are on Nginx and the rules that WebP Express stores in the *.htaccess* files probably does not ' .
475
+ 'have any effect. ';
476
+ $result[] = 'Please read the "I am on Nginx" section in the FAQ (https://wordpress.org/plugins/webp-express/)';
477
+ $result[] = 'And did you remember to restart the nginx service after updating the configuration?';
478
+
479
+ $result[] = 'PS: If you cannot get the redirect to work, you can simply rely on Alter HTML as described in the FAQ.';
480
+ return $result;
481
+ }
482
+
483
+ $modRewriteWorking = CapabilityTest::modRewriteWorking();
484
+ if ($modRewriteWorking !== null) {
485
+ $result[] = 'Running a special designed capability test to test if rewriting works with *.htaccess* files';
486
+ }
487
+ if ($modRewriteWorking === true) {
488
+ $result[] = 'Result: Yes, rewriting works.';
489
+ $result[] = 'It seems something is wrong with the *.htaccess* rules then. You could try ' .
490
+ 'to change "Destination structure" - the rules there are quite different.';
491
+ $result[] = 'It could also be that the server has cached the configuration a while. Some servers ' .
492
+ 'does that. In that case, simply give it a few minutes and try again.';
493
+ } elseif ($modRewriteWorking === false) {
494
+ $result[] = 'Result: No, rewriting does not seem to work within *.htaccess* rules.';
495
+ if (PlatformInfo::definitelyNotGotModRewrite()) {
496
+ $result[] = 'It actually seems "mod_write" is disabled on your server. ' .
497
+ '**You must enable mod_rewrite on the server**';
498
+ } elseif (PlatformInfo::definitelyGotApacheModule('mod_rewrite')) {
499
+ $result[] = 'However, "mod_write" *is* enabled on your server. This seems to indicate that ' .
500
+ '*.htaccess* files has been disabled for configuration on your server. ' .
501
+ 'In that case, you need to copy the WebP Express rules from the *.htaccess* files into your virtual host configuration files. ' .
502
+ '(WebP Express generates multiple *.htaccess* files. Look in the upload folder, the wp-content folder, etc).';
503
+ $result[] = 'It could however alse simply be that your server simply needs some time. ' .
504
+ 'Some servers caches the *.htaccess* rules for a bit. In that case, simply give it a few minutes and try again.';
505
+ } else {
506
+ $result[] = 'However, this could be due to your server being a bit slow on picking up changes in *.htaccess*.' .
507
+ 'Give it a few minutes and try again.';
508
+ }
509
+ } else {
510
+ // The mod_rewrite test could not conclude anything.
511
+ if (PlatformInfo::definitelyNotGotApacheModule('mod_rewrite')) {
512
+ $result[] = 'It actually seems "mod_write" is disabled on your server. ' .
513
+ '**You must enable mod_rewrite on the server**';
514
+ } elseif (PlatformInfo::definitelyGotApacheModule('mod_rewrite')) {
515
+ $result[] = '"mod_write" is enabled on your server, so rewriting ought to work. ' .
516
+ 'However, it could be that your server setup has disabled *.htaccess* files for configuration. ' .
517
+ 'In that case, you need to copy the WebP Express rules from the *.htaccess* files into your virtual host configuration files. ' .
518
+ '(WebP Express generates multiple *.htaccess* files. Look in the upload folder, the wp-content folder, etc). ';
519
+ } else {
520
+ $result[] = 'It seems something is wrong with the *.htaccess* rules. ';
521
+ $result[] = 'Or perhaps the server has cached the configuration a while. Some servers ' .
522
+ 'does that. In that case, simply give it a few minutes and try again.';
523
+ }
524
+ }
525
+ $result[] = 'Note that if you cannot get redirection to work, you can switch to "CDN friendly" mode and ' .
526
+ 'rely on the "Alter HTML" functionality to point to the webp images. If you do a bulk conversion ' .
527
+ 'and make sure that "Convert upon upload" is activated, you should be all set. Alter HTML even handles ' .
528
+ 'inline css (unless you select "picture tag" syntax). It does however not handle images in external css or ' .
529
+ 'which is added dynamically with javascript.';
530
+
531
+ $result[] = '## Info for manually diagnosing';
532
+ $result = array_merge($result, self::allInfo($config));
533
+ return $result;
534
+ }
535
+ }
lib/classes/SelfTestRedirectAbstract.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ abstract class SelfTestRedirectAbstract
6
+ {
7
+ protected $config;
8
+
9
+ public function __construct($config) {
10
+ $this->config = $config;
11
+ }
12
+
13
+ /**
14
+ * Run test for either jpeg or png
15
+ *
16
+ * @param string $rootId (ie "uploads" or "themes")
17
+ * @param string $imageType ("jpeg" or "png")
18
+ * @return array [$success, $result, $createdTestFiles]
19
+ */
20
+ abstract protected function runTestForImageType($rootId, $imageType);
21
+
22
+ abstract protected function getSuccessMessage();
23
+
24
+ private function doRunTestForRoot($rootId)
25
+ {
26
+ // return [true, ['hello'], false];
27
+ // return [false, SelfTestHelper::diagnoseFailedRewrite($this->config), false];
28
+
29
+ $result = [];
30
+
31
+ //$result[] = '*hello* with *you* and **you**. ok! FAILED';
32
+ $result[] = '## ' . $rootId;
33
+ //$result[] = 'This test examines image responses "from the outside".';
34
+
35
+ $createdTestFiles = false;
36
+
37
+ if ($this->config['image-types'] & 1) {
38
+ list($success, $subResult, $createdTestFiles) = $this->runTestForImageType($rootId, 'jpeg');
39
+ $result = array_merge($result, $subResult);
40
+
41
+ if ($success) {
42
+ if ($this->config['image-types'] & 2) {
43
+ $result[] = '### Performing same tests for PNG';
44
+ list($success, $subResult, $createdTestFiles2) = $this->runTestForImageType($rootId, 'png');
45
+ $createdTestFiles = $createdTestFiles || $createdTestFiles2;
46
+ if ($success) {
47
+ //$result[count($result) - 1] .= '. **ok**{: .ok}';
48
+ $result[] .= 'All tests passed for PNG as well.';
49
+ $result[] = '(I shall spare you for the report, which is almost identical to the one above)';
50
+ } else {
51
+ $result = array_merge($result, $subResult);
52
+ }
53
+ }
54
+ }
55
+ } else {
56
+ list($success, $subResult, $createdTestFiles) = $this->runTestForImageType($rootId, 'png');
57
+ $result = array_merge($result, $subResult);
58
+ }
59
+
60
+ if ($success) {
61
+ $result[] = '### Results for ' . strtoupper($rootId);
62
+
63
+ $result[] = $this->getSuccessMessage();
64
+ }
65
+ return [true, $result, $createdTestFiles];
66
+ }
67
+
68
+ private function runTestForRoot($rootId)
69
+ {
70
+ // TODO: move that method to here
71
+ SelfTestHelper::cleanUpTestImages($rootId, $this->config);
72
+
73
+ // Run the actual test
74
+ list($success, $result, $createdTestFiles) = $this->doRunTestForRoot($rootId);
75
+
76
+ // Clean up test images again. We are very tidy around here
77
+ if ($createdTestFiles) {
78
+ $result[] = 'Deleting test images';
79
+ SelfTestHelper::cleanUpTestImages($rootId, $this->config);
80
+ }
81
+
82
+ return [$success, $result];
83
+ }
84
+
85
+ abstract protected function startupTests();
86
+
87
+ protected function startTest()
88
+ {
89
+
90
+ list($success, $result) = $this->startupTests();
91
+
92
+ if (!$success) {
93
+ return [false, $result];
94
+ }
95
+
96
+ if (!file_exists(Paths::getConfigFileName())) {
97
+ $result[] = 'Hold on. You need to save options before you can run this test. There is no config file yet.';
98
+ return [true, $result];
99
+ }
100
+
101
+ if ($this->config['image-types'] == 0) {
102
+ $result[] = 'No image types have been activated, nothing to test';
103
+ return [true, $result];
104
+ }
105
+
106
+ foreach ($this->config['scope'] as $rootId) {
107
+ list($success, $subResult) = $this->runTestForRoot($rootId);
108
+ $result = array_merge($result, $subResult);
109
+ }
110
+ //list($success, $result) = self::runTestForRoot('uploads', $this->config);
111
+
112
+ return [$success, $result];
113
+ }
114
+
115
+ }
lib/classes/SelfTestRedirectToConverter.php ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ class SelfTestRedirectToConverter extends SelfTestRedirectAbstract
6
+ {
7
+
8
+ /**
9
+ * Run test for either jpeg or png
10
+ *
11
+ * @param string $rootId (ie "uploads" or "themes")
12
+ * @param string $imageType ("jpeg" or "png")
13
+ * @return array [$success, $result, $createdTestFiles]
14
+ */
15
+ protected function runTestForImageType($rootId, $imageType)
16
+ {
17
+ $result = [];
18
+ $createdTestFiles = false;
19
+ $noWarningsYet = true;
20
+
21
+ // Copy test image (jpeg)
22
+ list($subResult, $success, $sourceFileName) = SelfTestHelper::copyTestImageToRoot($rootId, $imageType);
23
+ $result = array_merge($result, $subResult);
24
+ if (!$success) {
25
+ $result[] = 'The test cannot be completed';
26
+ return [false, $result, $createdTestFiles];
27
+ }
28
+ $createdTestFiles = true;
29
+
30
+ $requestUrl = Paths::getUrlById($rootId) . '/webp-express-test-images/' . $sourceFileName;
31
+
32
+ $result[] = '### Lets check that browsers supporting webp gets a freshly converted WEBP ' .
33
+ 'when the ' . $imageType . ' is requested';
34
+ $result[] = 'Making a HTTP request for the test image (pretending to be a client that supports webp, by setting the "Accept" header to "image/webp")';
35
+ $requestArgs = [
36
+ 'headers' => [
37
+ 'ACCEPT' => 'image/webp'
38
+ ]
39
+ ];
40
+ list($success, $errors, $headers, $return) = SelfTestHelper::remoteGet($requestUrl, $requestArgs);
41
+
42
+ if (!$success) {
43
+ $result[count($result) - 1] .= '. FAILED';
44
+ $result = array_merge($result, $errors);
45
+ $result[] = 'The test cannot be completed';
46
+ //$result[count($result) - 1] .= '. FAILED';
47
+ return [false, $result, $createdTestFiles];
48
+ }
49
+ //$result[count($result) - 1] .= '. ok!';
50
+ $result[] = '*' . $requestUrl . '*';
51
+
52
+ $result = array_merge($result, SelfTestHelper::printHeaders($headers));
53
+
54
+ if (!isset($headers['content-type'])) {
55
+ $result[] = 'Bummer. There is no "content-type" response header. The test FAILED';
56
+ return [false, $result, $createdTestFiles];
57
+ }
58
+
59
+ if ($headers['content-type'] == 'image/' . $imageType) {
60
+ $result[] = 'Bummer. As the "content-type" header reveals, we got the ' . $imageType . '.';
61
+ $result[] = 'The test **failed**{: .error}.';
62
+ $result[] = 'Now, what went wrong?';
63
+
64
+ if (isset($headers['x-webp-convert-log'])) {
65
+ //$result[] = 'Inspect the "x-webp-convert-log" headers above, and you ' .
66
+ // 'should have your answer (it is probably because you do not have any conversion methods working).';
67
+ if (SelfTestHelper::hasHeaderContaining($headers, 'x-webp-convert-log', 'Performing fail action: original')) {
68
+ $result[] = 'The answer lies in the "x-convert-log" response headers: ' .
69
+ '**The conversion failed**{: .error}. ';
70
+ }
71
+ } else {
72
+ $result[] = 'Well, there is indication that the redirection isnt working. ' .
73
+ 'The PHP script should set "x-webp-convert-log" response headers, but there are none. ';
74
+ 'While these headers could have been eaten in a Cloudflare-like setup, the problem is ';
75
+ 'probably that the redirection simply failed';
76
+
77
+ $result[] = '### Diagnosing redirection problems';
78
+ $result = array_merge($result, SelfTestHelper::diagnoseFailedRewrite($this->config));
79
+ }
80
+ return [false, $result, $createdTestFiles];
81
+ }
82
+
83
+ if ($headers['content-type'] != 'image/webp') {
84
+ $result[] = 'However. As the "content-type" header reveals, we did not get a webp' .
85
+ 'Surprisingly we got: "' . $headers['content-type'] . '"';
86
+ $result[] = 'The test FAILED.';
87
+ if (strpos($headers['content-type'], 'text/html') !== false) {
88
+ $result[] = 'Body:';
89
+ $result[] = print_r($return['body'], true);
90
+ }
91
+ return [false, $result, $createdTestFiles];
92
+ }
93
+
94
+ $result[] = 'Alrighty. We got a webp, and we got it from the PHP script. **Great!**{: .ok}';
95
+
96
+ if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
97
+ $result[count($result) - 1] .= '. **BUT!**';
98
+ $result[] = '**Warning: We did not receive a Vary:Accept header. ' .
99
+ 'That header should be set in order to tell proxies that the response varies depending on the ' .
100
+ 'Accept header. Otherwise browsers not supporting webp might get a cached webp and vice versa.**{: .warn}';
101
+ $noWarningsYet = false;
102
+ }
103
+ if (!SelfTestHelper::hasCacheControlOrExpiresHeader($headers)) {
104
+ $result[] = '**Notice: No cache-control or expires header has been set. ' .
105
+ 'It is recommended to do so. Set it nice and big once you are sure the webps have a good quality/compression compromise.**{: .warn}';
106
+ }
107
+ $result[] = '';
108
+
109
+
110
+ // Check browsers NOT supporting webp
111
+ // -----------------------------------
112
+ $result[] = '### Now lets check that browsers *not* supporting webp gets the ' . strtoupper($imageType);
113
+ $result[] = 'Making a HTTP request for the test image (without setting the "Accept" header)';
114
+ list($success, $errors, $headers) = SelfTestHelper::remoteGet($requestUrl);
115
+
116
+ if (!$success) {
117
+ $result[count($result) - 1] .= '. FAILED';
118
+ $result = array_merge($result, $errors);
119
+ $result[] = 'The test cannot be completed';
120
+ //$result[count($result) - 1] .= '. FAILED';
121
+ return [false, $result, $createdTestFiles];
122
+ }
123
+ //$result[count($result) - 1] .= '. ok!';
124
+ $result[] = '*' . $requestUrl . '*';
125
+
126
+ $result = array_merge($result, SelfTestHelper::printHeaders($headers));
127
+
128
+ if (!isset($headers['content-type'])) {
129
+ $result[] = 'Bummer. There is no "content-type" response header. The test FAILED';
130
+ return [false, $result, $createdTestFiles];
131
+ }
132
+
133
+ if ($headers['content-type'] == 'image/webp') {
134
+ $result[] = '**Bummer**{: .error}. As the "content-type" header reveals, we got the webp. ' .
135
+ 'So even browsers not supporting webp gets webp. Not good!';
136
+ $result[] = 'The test FAILED.';
137
+
138
+ $result[] = '### What to do now?';
139
+ // TODO: We could examine the headers for common CDN responses
140
+
141
+ $result[] = 'First, examine the response headers above. Is there any indication that ' .
142
+ 'the image is returned from a CDN cache? ' .
143
+ $result[] = 'If there is: Check out the ' .
144
+ '*How do I configure my CDN in “Varied image responses” operation mode?* section in the FAQ ' .
145
+ '(https://wordpress.org/plugins/webp-express/)';
146
+
147
+ if (PlatformInfo::isApache()) {
148
+ $result[] = 'If not: please report this in the forum, as it seems the .htaccess rules ';
149
+ $result[] = 'just arent working on your system.';
150
+ } elseif (PlatformInfo::isNginx()) {
151
+ $result[] = 'Also, as you are on Nginx, check out the ' .
152
+ ' "I am on Nginx" section in the FAQ (https://wordpress.org/plugins/webp-express/)';
153
+ } else {
154
+ $result[] = 'If not: please report this in the forum, as it seems that there is something ' .
155
+ 'in the *.htaccess* rules generated by WebP Express that are not working.';
156
+ }
157
+
158
+ $result[] = '### System info (for manual diagnosing):';
159
+ $result = array_merge($result, SelfTestHelper::allInfo($this->config));
160
+
161
+
162
+ return [false, $result, $createdTestFiles];
163
+ }
164
+
165
+ if ($headers['content-type'] != 'image/' . $imageType) {
166
+ $result[] = 'Bummer. As the "content-type" header reveals, we did not get the ' . $imageType .
167
+ 'Surprisingly we got: "' . $headers['content-type'] . '"';
168
+ $result[] = 'The test FAILED.';
169
+ return [false, $result, $createdTestFiles];
170
+ }
171
+ $result[] = 'Alrighty. We got the ' . $imageType . '. **Great!**{: .ok}.';
172
+
173
+ if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
174
+ $result[count($result) - 1] .= '. **BUT!**';
175
+ $result[] = '**We did not receive a Vary:Accept header. ' .
176
+ 'That header should be set in order to tell proxies that the response varies depending on the ' .
177
+ 'Accept header. Otherwise browsers not supporting webp might get a cached webp and vice versa.**{: .warn}';
178
+ $noWarningsYet = false;
179
+ }
180
+
181
+ return [$noWarningsYet, $result, $createdTestFiles];
182
+ }
183
+
184
+ protected function getSuccessMessage()
185
+ {
186
+ return 'Everything **seems to work**{: .ok} as it should. ' .
187
+ 'However, a check is on the TODO: ' .
188
+ 'TODO: Check that disabled image types does not get converted. ';
189
+ }
190
+
191
+ public function startupTests()
192
+ {
193
+ $result[] = '# Testing redirection to converter';
194
+ if (!$this->config['enable-redirection-to-converter']) {
195
+ $result[] = 'Turned off, nothing to test (if you just turned it on without saving, remember: this is a live test so you need to save settings)';
196
+ return [false, $result];
197
+ }
198
+ return [true, $result];
199
+ }
200
+
201
+ public static function runTest()
202
+ {
203
+ $config = Config::loadConfigAndFix(false);
204
+ $me = new SelfTestRedirectToConverter($config);
205
+ return $me->startTest();
206
+ }
207
+
208
+ }
lib/classes/SelfTestRedirectToExisting.php ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ class SelfTestRedirectToExisting extends SelfTestRedirectAbstract
6
+ {
7
+ /**
8
+ * Run test for either jpeg or png
9
+ *
10
+ * @param string $rootId (ie "uploads" or "themes")
11
+ * @param string $imageType ("jpeg" or "png")
12
+ * @return array [$success, $result, $createdTestFiles]
13
+ */
14
+ protected function runTestForImageType($rootId, $imageType)
15
+ {
16
+ $result = [];
17
+ $createdTestFiles = false;
18
+ $noWarningsYet = true;
19
+
20
+ $result[] = '### Copying files for testing';
21
+
22
+ // Copy test image
23
+ list($subResult, $success, $sourceFileName) = SelfTestHelper::copyTestImageToRoot($rootId, $imageType);
24
+ $result = array_merge($result, $subResult);
25
+ if (!$success) {
26
+ $result[] = 'The test cannot be completed';
27
+ return [false, $result, $createdTestFiles];
28
+ }
29
+ $createdTestFiles = true;
30
+
31
+ $result[] = '';
32
+
33
+ // Copy dummy webp
34
+ list($subResult, $success, $destinationFile) = SelfTestHelper::copyDummyWebPToCacheFolder(
35
+ $rootId,
36
+ $this->config['destination-folder'],
37
+ $this->config['destination-extension'],
38
+ $this->config['destination-structure'],
39
+ $sourceFileName,
40
+ $imageType
41
+ );
42
+ $result = array_merge($result, $subResult);
43
+ if (!$success) {
44
+ $result[] = 'The test cannot be completed';
45
+ return [false, $result, $createdTestFiles];
46
+ }
47
+
48
+ $requestUrl = Paths::getUrlById($rootId) . '/webp-express-test-images/' . $sourceFileName;
49
+ $result[] = '### Lets check that browsers supporting webp gets the WEBP when the ' . strtoupper($imageType) . ' is requested';
50
+ $result[] = 'Making a HTTP request for the test image (pretending to be a client that supports webp, by setting the "Accept" header to "image/webp")';
51
+ $requestArgs = [
52
+ 'headers' => [
53
+ 'ACCEPT' => 'image/webp'
54
+ ]
55
+ ];
56
+ list($success, $errors, $headers, $return) = SelfTestHelper::remoteGet($requestUrl, $requestArgs);
57
+
58
+ if (!$success) {
59
+ $result[count($result) - 1] .= '. FAILED';
60
+ $result = array_merge($result, $errors);
61
+ $result[] = 'The test cannot be completed';
62
+ //$result[count($result) - 1] .= '. FAILED';
63
+ return [false, $result, $createdTestFiles];
64
+ }
65
+ //$result[count($result) - 1] .= '. ok!';
66
+ $result[] = '*' . $requestUrl . '*';
67
+
68
+ $result = array_merge($result, SelfTestHelper::printHeaders($headers));
69
+
70
+ if (!isset($headers['content-type'])) {
71
+ $result[] = 'Bummer. There is no "content-type" response header. The test FAILED';
72
+ return [false, $result, $createdTestFiles];
73
+ }
74
+
75
+ if ($headers['content-type'] != 'image/webp') {
76
+
77
+ if ($headers['content-type'] == 'image/' . $imageType) {
78
+ $result[] = 'Bummer. As the "content-type" header reveals, we got the ' . $imageType . '. ';
79
+ } else {
80
+ $result[] = 'Bummer. As the "content-type" header reveals, we did not get a webp' .
81
+ 'Surprisingly we got: "' . $headers['content-type'] . '"';
82
+ if (strpos($headers['content-type'], 'text/html') !== false) {
83
+ $result[] = 'Body:';
84
+ $result[] = print_r($return['body'], true);
85
+ }
86
+ }
87
+
88
+ if (isset($headers['content-length'])) {
89
+ if ($headers['content-length'] == '6964') {
90
+ $result[] = 'However, the content-length reveals that we actually GOT the webp ' .
91
+ '(we know that the file we put is exactly 6964 bytes). ' .
92
+ 'So it is "just" the content-type header that was not set correctly.';
93
+
94
+ if (PlatformInfo::isNginx()) {
95
+ $result[] = 'As you are on Nginx, you probably need to add the following line ' .
96
+ 'in your *mime.types* configuration file: ';
97
+ $result[] = '```image/webp webp;```';
98
+ } else {
99
+ $result[] = 'Perhaps you dont have *mod_mime* installed, or the following lines are not in a *.htaccess* ' .
100
+ 'in the folder containing the webp (or a parent):';
101
+ $result[] = "```<IfModule mod_mime.c>\n AddType image/webp .webp\n</IfModule>```";
102
+
103
+ $result[] = '### .htaccess status';
104
+ $result = array_merge($result, SelfTestHelper::htaccessInfo($this->config, true));
105
+ }
106
+
107
+ $result[] = 'The test **FAILED**{: .error}.';
108
+ } else {
109
+ $result[] = 'Additionally, the content-length reveals that we did not get the webp ' .
110
+ '(we know that the file we put is exactly 6964 bytes). ' .
111
+ 'So we can conclude that the rewrite did not happen';
112
+ $result[] = 'The test **FAILED**{: .error}.';
113
+ $result[] = '#### Diagnosing rewrites';
114
+ $result = array_merge($result, SelfTestHelper::diagnoseFailedRewrite($this->config));
115
+ }
116
+ } else {
117
+ $result[] = 'In addition, we did not get a *content-length* header either.' .
118
+ $result[] = 'It seems we can conclude that the rewrite did not happen.';
119
+ $result[] = 'The test **FAILED**{: .error}.';
120
+ $result[] = '#### Diagnosing rewrites';
121
+ $result = array_merge($result, SelfTestHelper::diagnoseFailedRewrite($this->config));
122
+ }
123
+
124
+ return [false, $result, $createdTestFiles];
125
+ }
126
+
127
+ if (isset($headers['x-webp-convert-log'])) {
128
+ $result[] = 'Bummer. Although we did get a webp, we did not get it as a result of a direct ' .
129
+ 'redirection. This webp was returned by the PHP script. Although this works, it takes more ' .
130
+ 'resources to ignite the PHP engine for each image request than redirecting directly to the image.';
131
+ $result[] = 'The test FAILED.';
132
+
133
+ $result[] = 'It seems something went wrong with the redirection.';
134
+ $result[] = '#### Diagnosing redirects';
135
+ $result = array_merge($result, SelfTestHelper::diagnoseFailedRewrite($this->config));
136
+
137
+ return [false, $result, $createdTestFiles];
138
+ } else {
139
+ $result[] = 'Alrighty. We got a webp. Just what we wanted. **Great!**{: .ok}';
140
+ }
141
+ if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
142
+ $result[count($result) - 1] .= '. **BUT!**';
143
+ $result[] = '**Warning: We did not receive a Vary:Accept header. ' .
144
+ 'That header should be set in order to tell proxies that the response varies depending on the ' .
145
+ 'Accept header. Otherwise browsers not supporting webp might get a cached webp and vice versa.**{: .warn}';
146
+ $noWarningsYet = false;
147
+ }
148
+ if (!SelfTestHelper::hasCacheControlOrExpiresHeader($headers)) {
149
+ $result[] = '**Notice: No cache-control or expires header has been set. ' .
150
+ 'It is recommended to do so. Set it nice and big once you are sure the webps have a good quality/compression compromise.**{: .warn}';
151
+ }
152
+ $result[] = '';
153
+
154
+
155
+ // Check browsers NOT supporting webp
156
+ // -----------------------------------
157
+ $result[] = '### Now lets check that browsers *not* supporting webp gets the ' . strtoupper($imageType);
158
+ $result[] = 'Making a HTTP request for the test image (without setting the "Accept" header)';
159
+ list($success, $errors, $headers) = SelfTestHelper::remoteGet($requestUrl);
160
+
161
+ if (!$success) {
162
+ $result[count($result) - 1] .= '. FAILED';
163
+ $result = array_merge($result, $errors);
164
+ $result[] = 'The test cannot be completed';
165
+ //$result[count($result) - 1] .= '. FAILED';
166
+ return [false, $result, $createdTestFiles];
167
+ }
168
+ //$result[count($result) - 1] .= '. ok!';
169
+ $result[] = '*' . $requestUrl . '*';
170
+
171
+ $result = array_merge($result, SelfTestHelper::printHeaders($headers));
172
+
173
+ if (!isset($headers['content-type'])) {
174
+ $result[] = 'Bummer. There is no "content-type" response header. The test FAILED';
175
+ return [false, $result, $createdTestFiles];
176
+ }
177
+
178
+ if ($headers['content-type'] == 'image/webp') {
179
+ $result[] = '**Bummer**{: .error}. As the "content-type" header reveals, we got the webp. ' .
180
+ 'So even browsers not supporting webp gets webp. Not good!';
181
+ $result[] = 'The test FAILED.';
182
+
183
+ $result[] = '#### What to do now?';
184
+ // TODO: We could examine the headers for common CDN responses
185
+
186
+ $result[] = 'First, examine the response headers above. Is there any indication that ' .
187
+ 'the image is returned from a CDN cache? ' .
188
+ $result[] = 'If there is: Check out the ' .
189
+ '*How do I configure my CDN in “Varied image responses” operation mode?* section in the FAQ ' .
190
+ '(https://wordpress.org/plugins/webp-express/)';
191
+
192
+ if (PlatformInfo::isApache()) {
193
+ $result[] = 'If not: please report this in the forum, as it seems the .htaccess rules ';
194
+ $result[] = 'just arent working on your system.';
195
+ } elseif (PlatformInfo::isNginx()) {
196
+ $result[] = 'Also, as you are on Nginx, check out the ' .
197
+ ' "I am on Nginx" section in the FAQ (https://wordpress.org/plugins/webp-express/)';
198
+ } else {
199
+ $result[] = 'If not: please report this in the forum, as it seems that there is something ' .
200
+ 'in the *.htaccess* rules generated by WebP Express that are not working.';
201
+ }
202
+
203
+ $result[] = '### System info (for manual diagnosing):';
204
+ $result = array_merge($result, SelfTestHelper::allInfo($this->config));
205
+
206
+
207
+ return [false, $result, $createdTestFiles];
208
+ }
209
+
210
+ if ($headers['content-type'] != 'image/' . $imageType) {
211
+ $result[] = 'Bummer. As the "content-type" header reveals, we did not get the ' . $imageType .
212
+ 'Surprisingly we got: "' . $headers['content-type'] . '"';
213
+ $result[] = 'The test FAILED.';
214
+ return [false, $result, $createdTestFiles];
215
+ }
216
+ $result[] = 'Alrighty. We got the ' . $imageType . '. **Great!**{: .ok}.';
217
+
218
+ if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
219
+ $result[count($result) - 1] .= '. **BUT!**';
220
+ $result[] = '**We did not receive a Vary:Accept header. ' .
221
+ 'That header should be set in order to tell proxies that the response varies depending on the ' .
222
+ 'Accept header. Otherwise browsers not supporting webp might get a cached webp and vice versa.**{: .warn}';
223
+ $noWarningsYet = false;
224
+ }
225
+
226
+ return [$noWarningsYet, $result, $createdTestFiles];
227
+ }
228
+
229
+ protected function getSuccessMessage()
230
+ {
231
+ return 'Everything **seems to work**{: .ok} as it should. ' .
232
+ 'However, a couple of things were not tested (it is on the TODO). ' .
233
+ 'TODO 1: If one image type is disabled, check that it does not redirect to webp (unless redirection to converter is set up). ' .
234
+ 'TODO 2: Test that redirection to webp only is triggered when the webp exists. ';
235
+ }
236
+
237
+ public function startupTests()
238
+ {
239
+ $result[] = '# Testing redirection to existing webp';
240
+ if (!$this->config['redirect-to-existing-in-htaccess']) {
241
+ $result[] = 'Turned off, nothing to test (if you just turned it on without saving, remember: this is a live test so you need to save settings)';
242
+ return [false, $result];
243
+ }
244
+ return [true, $result];
245
+ }
246
+
247
+ public static function runTest()
248
+ {
249
+ $config = Config::loadConfigAndFix(false);
250
+ $me = new SelfTestRedirectToExisting($config);
251
+ return $me->startTest();
252
+ }
253
+ }
lib/classes/SelfTestRedirectToWebPRealizer.php ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ class SelfTestRedirectToWebPRealizer extends SelfTestRedirectAbstract
6
+ {
7
+
8
+ /**
9
+ * Run test for either jpeg or png
10
+ *
11
+ * @param string $rootId (ie "uploads" or "themes")
12
+ * @param string $imageType ("jpeg" or "png")
13
+ * @return array [$success, $result, $createdTestFiles]
14
+ */
15
+ protected function runTestForImageType($rootId, $imageType)
16
+ {
17
+ $result = [];
18
+ $createdTestFiles = false;
19
+ $noWarningsYet = true;
20
+
21
+ // Copy test image
22
+ list($subResult, $success, $sourceFileName) = SelfTestHelper::copyTestImageToRoot($rootId, $imageType);
23
+ $result = array_merge($result, $subResult);
24
+ if (!$success) {
25
+ $result[] = 'The test cannot be completed';
26
+ return [false, $result, $createdTestFiles];
27
+ }
28
+ $createdTestFiles = true;
29
+
30
+ //$requestUrl = Paths::getUploadUrl() . '/' . $sourceFileName;
31
+
32
+ // Hacky, I know.
33
+ // AlterHtmlHelper was not meant to be used like this, but it is the only place where we currently
34
+ // have logic for finding destination url from source url.
35
+
36
+ //$sourceUrl = Paths::getUploadUrl() . '/' . $sourceFileName;
37
+ $sourceUrl = Paths::getUrlById($rootId) . '/webp-express-test-images/' . $sourceFileName;
38
+
39
+ AlterHtmlHelper::$options = json_decode(Option::getOption('webp-express-alter-html-options', null), true);
40
+ AlterHtmlHelper::$options['only-for-webps-that-exists'] = false;
41
+
42
+ $requestUrl = AlterHtmlHelper::getWebPUrlInBase(
43
+ $sourceUrl,
44
+ $rootId,
45
+ Paths::getUrlById($rootId),
46
+ Paths::getAbsDirById($rootId)
47
+ );
48
+
49
+
50
+ $result[] = '### Lets check that browsers supporting webp gets a freshly converted WEBP ' .
51
+ 'when a non-existing WEBP is requested, which has a corresponding source';
52
+ $result[] = 'Making a HTTP request for the test image (pretending to be a client that supports webp, by setting the "Accept" header to "image/webp")';
53
+ $requestArgs = [
54
+ 'headers' => [
55
+ 'ACCEPT' => 'image/webp'
56
+ ]
57
+ ];
58
+ list($success, $errors, $headers, $return) = SelfTestHelper::remoteGet($requestUrl, $requestArgs);
59
+
60
+ if (!$success) {
61
+ //$result[count($result) - 1] .= '. FAILED';
62
+ //$result[] = '*' . $requestUrl . '*';
63
+
64
+ $result = array_merge($result, $errors);
65
+ $result[] = 'The test **failed**{: .error}';
66
+
67
+ $result[] = 'Why did it fail? It could either be that the redirection rule did not trigger ' .
68
+ 'or it could be that the PHP script could not locate a source image corresponding to the destination URL. ' .
69
+ 'Currently, this analysis cannot dertermine which was the case and it cannot be helpful ' .
70
+ 'if the latter is the case (sorry!). However, if the redirection rules are the problem, here is some info:';
71
+
72
+ $result[] = '### Diagnosing redirection problems (presuming it is the redirection to the script that is failing)';
73
+ $result = array_merge($result, SelfTestHelper::diagnoseFailedRewrite($this->config));
74
+
75
+
76
+ //$result[count($result) - 1] .= '. FAILED';
77
+ return [false, $result, $createdTestFiles];
78
+ }
79
+ //$result[count($result) - 1] .= '. ok!';
80
+ $result[] = '*' . $requestUrl . '*';
81
+
82
+ $result = array_merge($result, SelfTestHelper::printHeaders($headers));
83
+
84
+ if (!isset($headers['content-type'])) {
85
+ $result[] = 'Bummer. There is no "content-type" response header. The test FAILED';
86
+ return [false, $result, $createdTestFiles];
87
+ }
88
+
89
+ if ($headers['content-type'] == 'image/' . $imageType) {
90
+ $result[] = 'Bummer. As the "content-type" header reveals, we got the ' . $imageType . '.';
91
+ $result[] = 'The test **failed**{: .error}.';
92
+ $result[] = 'Now, what went wrong?';
93
+
94
+ if (isset($headers['x-webp-convert-log'])) {
95
+ //$result[] = 'Inspect the "x-webp-convert-log" headers above, and you ' .
96
+ // 'should have your answer (it is probably because you do not have any conversion methods working).';
97
+ if (SelfTestHelper::hasHeaderContaining($headers, 'x-webp-convert-log', 'Performing fail action: original')) {
98
+ $result[] = 'The answer lies in the "x-convert-log" response headers: ' .
99
+ '**The conversion failed**{: .error}. ';
100
+ }
101
+ } else {
102
+ $result[] = 'Well, there is indication that the redirection isnt working. ' .
103
+ 'The PHP script should set "x-webp-convert-log" response headers, but there are none. ';
104
+ 'While these headers could have been eaten in a Cloudflare-like setup, the problem is ';
105
+ 'probably that the redirection simply failed';
106
+
107
+ $result[] = '### Diagnosing redirection problems';
108
+ $result = array_merge($result, SelfTestHelper::diagnoseFailedRewrite($this->config));
109
+ }
110
+ return [false, $result, $createdTestFiles];
111
+ }
112
+
113
+ if ($headers['content-type'] != 'image/webp') {
114
+ $result[] = 'However. As the "content-type" header reveals, we did not get a webp' .
115
+ 'Surprisingly we got: "' . $headers['content-type'] . '"';
116
+ $result[] = 'The test FAILED.';
117
+ if (strpos($headers['content-type'], 'text/html') !== false) {
118
+ $result[] = 'Body:';
119
+ $result[] = print_r($return['body'], true);
120
+ }
121
+ return [false, $result, $createdTestFiles];
122
+ }
123
+
124
+ $result[] = '**Alrighty**{: .ok}. We got a webp.';
125
+ if (isset($headers['x-webp-convert-log'])) {
126
+ $result[] = 'The "x-webp-convert-log" headers reveals we got the webp from the PHP script. **Great!**{: .ok}';
127
+ } else {
128
+ $result[] = 'Interestingly, there are no "x-webp-convert-log" headers even though ' .
129
+ 'the PHP script always produces such. Could it be you have some weird setup that eats these headers?';
130
+ }
131
+
132
+ if (SelfTestHelper::hasVaryAcceptHeader($headers)) {
133
+ $result[] = 'All is however not super-duper:';
134
+
135
+ $result[] = '**Notice: We received a Vary:Accept header. ' .
136
+ 'That header need not to be set. Actually, it is a little bit bad for performance ' .
137
+ 'as proxies are currently doing a bad job maintaining several caches (in many cases they simply do not)**{: .warn}';
138
+ $noWarningsYet = false;
139
+ }
140
+ if (!SelfTestHelper::hasCacheControlOrExpiresHeader($headers)) {
141
+ $result[] = '**Notice: No cache-control or expires header has been set. ' .
142
+ 'It is recommended to do so. Set it nice and big once you are sure the webps have a good quality/compression compromise.**{: .warn}';
143
+ }
144
+ $result[] = '';
145
+
146
+ return [$noWarningsYet, $result, $createdTestFiles];
147
+ }
148
+
149
+ /*
150
+ private static function doRunTest($this->config)
151
+ {
152
+ $result = [];
153
+ $result[] = '# Testing redirection to converter';
154
+
155
+ $createdTestFiles = false;
156
+ if (!file_exists(Paths::getConfigFileName())) {
157
+ $result[] = 'Hold on. You need to save options before you can run this test. There is no config file yet.';
158
+ return [true, $result, $createdTestFiles];
159
+ }
160
+
161
+
162
+ if ($this->config['image-types'] == 0) {
163
+ $result[] = 'No image types have been activated, nothing to test';
164
+ return [true, $result, $createdTestFiles];
165
+ }
166
+
167
+ if ($this->config['image-types'] & 1) {
168
+ list($success, $subResult, $createdTestFiles) = self::runTestForImageType($this->config, 'jpeg');
169
+ $result = array_merge($result, $subResult);
170
+
171
+ if ($success) {
172
+ if ($this->config['image-types'] & 2) {
173
+ $result[] = '### Performing same tests for PNG';
174
+ list($success, $subResult, $createdTestFiles2) = self::runTestForImageType($this->config, 'png');
175
+ $createdTestFiles = $createdTestFiles || $createdTestFiles2;
176
+ if ($success) {
177
+ //$result[count($result) - 1] .= '. **ok**{: .ok}';
178
+ $result[] .= 'All tests passed for PNG as well.';
179
+ $result[] = '(I shall spare you for the report, which is almost identical to the one above)';
180
+ } else {
181
+ $result = array_merge($result, $subResult);
182
+ }
183
+ }
184
+ }
185
+ } else {
186
+ list($success, $subResult, $createdTestFiles) = self::runTestForImageType($this->config, 'png');
187
+ $result = array_merge($result, $subResult);
188
+ }
189
+
190
+ if ($success) {
191
+ $result[] = '### Conclusion';
192
+ $result[] = 'Everything **seems to work**{: .ok} as it should. ' .
193
+ 'However, notice that this test only tested an image which was placed in the *uploads* folder. ' .
194
+ 'The rest of the image roots (such as theme images) have not been tested (it is on the TODO). ' .
195
+ 'Also on the TODO: If one image type is disabled, check that it does not redirect to the conversion script. ' .
196
+ 'These things probably work, though.';
197
+ }
198
+
199
+
200
+ return [true, $result, $createdTestFiles];
201
+ }*/
202
+
203
+ protected function getSuccessMessage()
204
+ {
205
+ return 'Everything **seems to work**{: .ok} as it should. ' .
206
+ 'However, a check is on the TODO: ' .
207
+ 'TODO: Check that disabled image types does not get converted. ';
208
+ }
209
+
210
+ public function startupTests()
211
+ {
212
+ $result[] = '# Testing "WebP Realizer" functionality';
213
+ if (!$this->config['enable-redirection-to-webp-realizer']) {
214
+ $result[] = 'Turned off, nothing to test (if you just turned it on without saving, remember: this is a live test so you need to save settings)';
215
+ return [false, $result];
216
+ }
217
+ return [true, $result];
218
+ }
219
+
220
+ public static function runTest()
221
+ {
222
+ $config = Config::loadConfigAndFix(false);
223
+ $me = new SelfTestRedirectToWebPRealizer($config);
224
+ return $me->startTest();
225
+ }
226
+
227
+
228
+ }
lib/classes/WebPOnDemand.php ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ This class is used by wod/webp-on-demand.php, which does not do a Wordpress bootstrap, but does register an autoloader for
4
+ the WebPExpress classes.
5
+
6
+ Calling Wordpress functions will FAIL. Make sure not to do that in either this class or the helpers.
7
+ */
8
+ //error_reporting(E_ALL);
9
+ //ini_set('display_errors', 1);
10
+
11
+ namespace WebPExpress;
12
+
13
+ use \WebPExpress\ConvertHelperIndependent;
14
+ use \WebPExpress\Sanitize;
15
+ use \WebPExpress\SanityCheck;
16
+ use \WebPExpress\SanityException;
17
+ use \WebPExpress\ValidateException;
18
+ use \WebPExpress\Validate;
19
+ use \WebPExpress\WodConfigLoader;
20
+
21
+ class WebPOnDemand extends WodConfigLoader
22
+ {
23
+ private static function getSourceDocRoot() {
24
+
25
+
26
+ //echo 't:' . $_GET['test'];exit;
27
+ // Check if it is in an environment variable
28
+ $source = self::getEnvPassedInRewriteRule('REQFN');
29
+ if ($source !== false) {
30
+ self::$checking = 'source (passed through env)';
31
+ return SanityCheck::absPathExistsAndIsFile($source);
32
+ }
33
+
34
+ // Check if it is in header (but only if .htaccess was configured to send in header)
35
+ if (isset($wodOptions['base-htaccess-on-these-capability-tests'])) {
36
+ $capTests = $wodOptions['base-htaccess-on-these-capability-tests'];
37
+ $passThroughHeaderDefinitelyUnavailable = ($capTests['passThroughHeaderWorking'] === false);
38
+ $passThrougEnvVarDefinitelyAvailable =($capTests['passThroughEnvWorking'] === true);
39
+ // This determines if .htaccess was configured to send in querystring
40
+ $headerMagicAddedInHtaccess = ((!$passThrougEnvVarDefinitelyAvailable) && (!$passThroughHeaderDefinitelyUnavailable));
41
+ } else {
42
+ $headerMagicAddedInHtaccess = true; // pretend its true
43
+ }
44
+ if ($headerMagicAddedInHtaccess && (isset($_SERVER['HTTP_REQFN']))) {
45
+ self::$checking = 'source (passed through request header)';
46
+ return SanityCheck::absPathExistsAndIsFile($_SERVER['HTTP_REQFN']);
47
+ }
48
+
49
+ if (!isset(self::$docRoot)) {
50
+ //$source = self::getEnvPassedInRewriteRule('REQFN');
51
+ if (isset($_GET['root-id']) && isset($_GET['xsource-rel-to-root-id'])) {
52
+ $xsrcRelToRootId = SanityCheck::noControlChars($_GET['xsource-rel-to-root-id']);
53
+ $srcRelToRootId = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRelToRootId, 1));
54
+ //echo $srcRelToRootId; exit;
55
+
56
+ $rootId = SanityCheck::noControlChars($_GET['root-id']);
57
+ SanityCheck::pregMatch('#^[a-z]+$#', $rootId, 'Not a valid root-id');
58
+
59
+ $source = self::getRootPathById($rootId) . '/' . $srcRelToRootId;
60
+ return SanityCheck::absPathExistsAndIsFile($source);
61
+ }
62
+ }
63
+
64
+ // Check querystring (relative path to docRoot) - when docRoot is available
65
+ if (isset(self::$docRoot) && isset($_GET['xsource-rel'])) {
66
+ self::$checking = 'source (passed as relative path, through querystring)';
67
+ $xsrcRel = SanityCheck::noControlChars($_GET['xsource-rel']);
68
+ $srcRel = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRel, 1));
69
+ return SanityCheck::absPathExistsAndIsFile(self::$docRoot . '/' . $srcRel);
70
+ }
71
+
72
+ // Check querystring (relative path to plugin) - when docRoot is unavailable
73
+ /*TODO
74
+ if (!isset(self::$docRoot) && isset($_GET['xsource-rel-to-plugin-dir'])) {
75
+ self::$checking = 'source (passed as relative path to plugin dir, through querystring)';
76
+ $xsrcRelPlugin = SanityCheck::noControlChars($_GET['xsource-rel-to-plugin-dir']);
77
+ $srcRelPlugin = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRelPlugin, 1));
78
+ return SanityCheck::absPathExistsAndIsFile(self::$docRoot . '/' . $srcRel);
79
+ }*/
80
+
81
+
82
+ // Check querystring (full path)
83
+ // - But only on Nginx (our Apache .htaccess rules never passes absolute url)
84
+ if (
85
+ (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) &&
86
+ (isset($_GET['source']) || isset($_GET['xsource']))
87
+ ) {
88
+ self::$checking = 'source (passed as absolute path on nginx)';
89
+ if (isset($_GET['source'])) {
90
+ $source = SanityCheck::absPathExistsAndIsFile($_GET['source']);
91
+ } else {
92
+ $xsrc = SanityCheck::noControlChars($_GET['xsource']);
93
+ return SanityCheck::absPathExistsAndIsFile(substr($xsrc, 1));
94
+ }
95
+ }
96
+
97
+ // Last resort is to use $_SERVER['REQUEST_URI'], well knowing that it does not give the
98
+ // correct result in all setups (ie "folder method 1")
99
+ if (isset(self::$docRoot)) {
100
+ self::$checking = 'source (retrieved by the request_uri server var)';
101
+ $srcRel = SanityCheck::pathWithoutDirectoryTraversal(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
102
+ return SanityCheck::absPathExistsAndIsFile(self::$docRoot . $srcRel);
103
+ }
104
+ }
105
+
106
+ private static function getSourceNoDocRoot()
107
+ {
108
+ $dirIdOfHtaccess = self::getEnvPassedInRewriteRule('WE_HTACCESS_ID');
109
+ if ($dirIdOfHtaccess === false) {
110
+ $dirIdOfHtaccess = SanityCheck::noControlChars($_GET['htaccess-id']);
111
+ }
112
+
113
+ if (!in_array($dirIdOfHtaccess, ['uploads', 'themes', 'wp-content', 'plugins', 'index'])) {
114
+ throw new \Exception('invalid htaccess directory id argument.');
115
+ }
116
+
117
+ // First try ENV
118
+ $sourceRelHtaccess = self::getEnvPassedInRewriteRule('WE_SOURCE_REL_HTACCESS');
119
+
120
+ // Otherwise use query-string
121
+ if ($sourceRelHtaccess === false) {
122
+ if (isset($_GET['xsource-rel-htaccess'])) {
123
+ $x = SanityCheck::noControlChars($_GET['xsource-rel-htaccess']);
124
+ $sourceRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal(substr($x, 1));
125
+ } else {
126
+ throw new \Exception('Argument for source path is missing');
127
+ }
128
+ }
129
+
130
+ $sourceRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal($sourceRelHtaccess);
131
+
132
+
133
+ $imageRoots = self::getImageRootsDef();
134
+
135
+ $source = $imageRoots->byId($dirIdOfHtaccess)->getAbsPath() . '/' . $sourceRelHtaccess;
136
+ return $source;
137
+ }
138
+
139
+ private static function getSource() {
140
+ if (self::$usingDocRoot) {
141
+ $source = self::getSourceDocRoot();
142
+ } else {
143
+ $source = self::getSourceNoDocRoot();
144
+ }
145
+ return $source;
146
+ }
147
+
148
+ private static function processRequestNoTryCatch() {
149
+
150
+ self::loadConfig();
151
+
152
+ $options = self::$options;
153
+ $wodOptions = self::$wodOptions;
154
+ $serveOptions = $options['webp-convert'];
155
+ $convertOptions = &$serveOptions['convert'];
156
+ //echo '<pre>' . print_r($wodOptions, true) . '</pre>'; exit;
157
+
158
+
159
+ // Validate that WebPExpress was configured to redirect to this conversion script
160
+ // (but do not require that for Nginx)
161
+ // ------------------------------------------------------------------------------
162
+ self::$checking = 'settings';
163
+ if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
164
+ if (!isset($wodOptions['enable-redirection-to-converter']) || ($wodOptions['enable-redirection-to-converter'] === false)) {
165
+ throw new ValidateException('Redirection to conversion script is not enabled');
166
+ }
167
+ }
168
+
169
+ // Check source (the image to be converted)
170
+ // --------------------------------------------
171
+ self::$checking = 'source';
172
+ $source = self::getSource();
173
+
174
+ //self::exitWithError($source);
175
+
176
+ $imageRoots = self::getImageRootsDef();
177
+
178
+ // Get upload dir
179
+ $uploadDirAbs = $imageRoots->byId('uploads')->getAbsPath();
180
+
181
+ // Check destination path
182
+ // --------------------------------------------
183
+ self::$checking = 'destination path';
184
+ $destination = ConvertHelperIndependent::getDestination(
185
+ $source,
186
+ $wodOptions['destination-folder'],
187
+ $wodOptions['destination-extension'],
188
+ self::$webExpressContentDirAbs,
189
+ $uploadDirAbs,
190
+ self::$usingDocRoot,
191
+ self::getImageRootsDef()
192
+ );
193
+
194
+ //$destination = SanityCheck::absPathIsInDocRoot($destination);
195
+ $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Does not end with .webp');
196
+
197
+ //self::exitWithError($destination);
198
+
199
+ // Done with sanitizing, lets get to work!
200
+ // ---------------------------------------
201
+ if (isset($wodOptions['success-response']) && ($wodOptions['success-response'] == 'original')) {
202
+ $serveOptions['serve-original'] = true;
203
+ $serveOptions['serve-image']['headers']['vary-accept'] = false;
204
+ } else {
205
+ $serveOptions['serve-image']['headers']['vary-accept'] = true;
206
+ }
207
+ //echo $source . '<br>' . $destination; exit;
208
+
209
+ ConvertHelperIndependent::serveConverted(
210
+ $source,
211
+ $destination,
212
+ $serveOptions,
213
+ self::$webExpressContentDirAbs . '/log',
214
+ 'Conversion triggered with the conversion script (wod/webp-on-demand.php)'
215
+ );
216
+ }
217
+
218
+ public static function processRequest() {
219
+ try {
220
+ self::processRequestNoTryCatch();
221
+ } catch (SanityException $e) {
222
+ self::exitWithError('Sanity check failed for ' . self::$checking . ': '. $e->getMessage());
223
+ } catch (ValidateException $e) {
224
+ self::exitWithError('Validation failed for ' . self::$checking . ': '. $e->getMessage());
225
+ } catch (\Exception $e) {
226
+ self::exitWithError('Error occured while calculating ' . self::$checking . ': '. $e->getMessage());
227
+ }
228
+ }
229
+ }
lib/classes/WebPRealizer.php ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ This class is used by wod/webp-realizer.php, which does not do a Wordpress bootstrap, but does register an autoloader for
4
+ the WebPExpress classes.
5
+
6
+ Calling Wordpress functions will FAIL. Make sure not to do that in either this class or the helpers.
7
+ */
8
+ //error_reporting(E_ALL);
9
+ //ini_set('display_errors', 1);
10
+
11
+ namespace WebPExpress;
12
+
13
+ use \WebPExpress\ConvertHelperIndependent;
14
+ use \WebPExpress\Sanitize;
15
+ use \WebPExpress\SanityCheck;
16
+ use \WebPExpress\SanityException;
17
+ use \WebPExpress\ValidateException;
18
+ use \WebPExpress\Validate;
19
+ use \WebPExpress\WodConfigLoader;
20
+
21
+ class WebPRealizer extends WodConfigLoader
22
+ {
23
+ private static function getDestinationDocRoot() {
24
+ $docRoot = self::$docRoot;
25
+
26
+ // Check if it is in an environment variable
27
+ $destRel = self::getEnvPassedInRewriteRule('DESTINATIONREL');
28
+ if ($destRel !== false) {
29
+ return SanityCheck::absPath($docRoot . '/' . $destRel);
30
+ }
31
+
32
+ // Check querystring (relative path)
33
+ if (isset($_GET['xdestination-rel'])) {
34
+ $xdestRel = SanityCheck::noControlChars($_GET['xdestination-rel']);
35
+ $destRel = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestRel, 1));
36
+ $destination = SanityCheck::absPath($docRoot . '/' . $destRel);
37
+ return SanityCheck::absPathIsInDocRoot($destination);
38
+ }
39
+
40
+ // Check querystring (full path)
41
+ // - But only on Nginx (our Apache .htaccess rules never passes absolute url)
42
+ if (
43
+ (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) &&
44
+ (isset($_GET['destination']) || isset($_GET['xdestination']))
45
+ ) {
46
+ if (isset($_GET['destination'])) {
47
+ $destination = SanityCheck::absPathIsInDocRoot($_GET['destination']);
48
+ } else {
49
+ $xdest = SanityCheck::noControlChars($_GET['xdestination']);
50
+ $destination = SanityCheck::absPathIsInDocRoot(substr($xdest, 1));
51
+ }
52
+ }
53
+
54
+ // Last resort is to use $_SERVER['REQUEST_URI'], well knowing that it does not give the
55
+ // correct result in all setups (ie "folder method 1")
56
+ $destRel = SanityCheck::pathWithoutDirectoryTraversal(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
57
+ $destination = SanityCheck::absPath($docRoot . $destRel);
58
+ return SanityCheck::absPathIsInDocRoot($destination);
59
+ }
60
+
61
+ private static function getDestinationNoDocRoot() {
62
+
63
+ $dirIdOfHtaccess = self::getEnvPassedInRewriteRule('WE_HTACCESS_ID');
64
+ if ($dirIdOfHtaccess === false) {
65
+ $dirIdOfHtaccess = SanityCheck::noControlChars($_GET['htaccess-id']);
66
+ }
67
+
68
+ if (!in_array($dirIdOfHtaccess, ['uploads', 'cache'])) {
69
+ throw new \Exception('invalid htaccess directory id argument. It must be either "uploads" or "cache".');
70
+ }
71
+
72
+
73
+ // First try ENV
74
+ $destinationRelHtaccess = self::getEnvPassedInRewriteRule('WE_DESTINATION_REL_HTACCESS');
75
+
76
+ // Otherwise use query-string
77
+ if ($destinationRelHtaccess === false) {
78
+ if (isset($_GET['xdestination-rel-htaccess'])) {
79
+ $x = SanityCheck::noControlChars($_GET['xdestination-rel-htaccess']);
80
+ $destinationRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal(substr($x, 1));
81
+ } else {
82
+ throw new \Exception('Argument for destination path is missing');
83
+ }
84
+ }
85
+
86
+ $destinationRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal($destinationRelHtaccess);
87
+
88
+ $imageRoots = self::getImageRootsDef();
89
+ if ($dirIdOfHtaccess == 'uploads') {
90
+ return $imageRoots->byId('uploads')->getAbsPath() . '/' . $destinationRelHtaccess;
91
+ } elseif ($dirIdOfHtaccess == 'cache') {
92
+ return $imageRoots->byId('wp-content')->getAbsPath() . '/webp-express/webp-images/' . $destinationRelHtaccess;
93
+ }
94
+ /*
95
+ $pathTokens = explode('/', $destinationRelCacheRoot);
96
+ $imageRootId = array_shift($pathTokens);
97
+ $destinationRelSpecificCacheRoot = implode('/', $pathTokens);
98
+
99
+ $imageRootId = SanityCheck::pregMatch(
100
+ '#^[a-z\-]+$#',
101
+ $imageRootId,
102
+ 'The image root ID is not a valid root id'
103
+ );
104
+
105
+ // TODO: Validate that the root id is in scope
106
+
107
+ if (count($pathTokens) == 0) {
108
+ throw new \Exception('invalid destination argument. It must contain dashes.');
109
+ }
110
+
111
+ return $imageRoots->byId($imageRootId)->getAbsPath() . '/' . $destinationRelSpecificCacheRoot;
112
+
113
+ /*
114
+ if ($imageRootId !== false) {
115
+
116
+ //$imageRootId = self::getEnvPassedInRewriteRule('WE_IMAGE_ROOT_ID');
117
+ if ($imageRootId !== false) {
118
+ $imageRootId = SanityCheck::pregMatch('#^[a-z\-]+$#', $imageRootId, 'The image root ID passed in ENV is not a valid root-id');
119
+
120
+ $destinationRelImageRoot = self::getEnvPassedInRewriteRule('WE_DESTINATION_REL_IMAGE_ROOT');
121
+ if ($destinationRelImageRoot !== false) {
122
+ $destinationRelImageRoot = SanityCheck::pathWithoutDirectoryTraversal($destinationRelImageRoot);
123
+ }
124
+ $imageRoots = self::getImageRootsDef();
125
+ return $imageRoots->byId($imageRootId)->getAbsPath() . '/' . $destinationRelImageRoot;
126
+ }
127
+
128
+ if (isset($_GET['xdestination-rel-image-root'])) {
129
+ $xdestinationRelImageRoot = SanityCheck::noControlChars($_GET['xdestination-rel-image-root']);
130
+ $destinationRelImageRoot = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestinationRelImageRoot, 1));
131
+
132
+ $imageRootId = SanityCheck::noControlChars($_GET['image-root-id']);
133
+ SanityCheck::pregMatch('#^[a-z\-]+$#', $imageRootId, 'Not a valid root-id');
134
+
135
+ $imageRoots = self::getImageRootsDef();
136
+ return $imageRoots->byId($imageRootId)->getAbsPath() . '/' . $destinationRelImageRoot;
137
+ }
138
+
139
+ throw new \Exception('Argument for destination file missing');
140
+ //WE_DESTINATION_REL_IMG_ROOT*/
141
+
142
+ /*
143
+ $destAbs = SanityCheck::noControlChars(self::getEnvPassedInRewriteRule('WEDESTINATIONABS'));
144
+ if ($destAbs !== false) {
145
+ return SanityCheck::pathWithoutDirectoryTraversal($destAbs);
146
+ }
147
+
148
+ // Check querystring (relative path)
149
+ if (isset($_GET['xdest-rel-to-root-id'])) {
150
+ $xdestRelToRootId = SanityCheck::noControlChars($_GET['xdest-rel-to-root-id']);
151
+ $destRelToRootId = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestRelToRootId, 1));
152
+
153
+ $rootId = SanityCheck::noControlChars($_GET['root-id']);
154
+ SanityCheck::pregMatch('#^[a-z]+$#', $rootId, 'Not a valid root-id');
155
+ return self::getRootPathById($rootId) . '/' . $destRelToRootId;
156
+ }
157
+ */
158
+
159
+ }
160
+
161
+ private static function getDestination() {
162
+ self::$checking = 'destination path';
163
+ if (self::$usingDocRoot) {
164
+ $destination = self::getDestinationDocRoot();
165
+ } else {
166
+ $destination = self::getDestinationNoDocRoot();
167
+ }
168
+ SanityCheck::pregMatch('#\.webp$#', $destination, 'Does not end with .webp');
169
+
170
+ return $destination;
171
+ }
172
+
173
+ private static function processRequestNoTryCatch() {
174
+
175
+ self::loadConfig();
176
+
177
+ $options = self::$options;
178
+ $wodOptions = self::$wodOptions;
179
+ $serveOptions = $options['webp-convert'];
180
+ $convertOptions = &$serveOptions['convert'];
181
+ //echo '<pre>' . print_r($wodOptions, true) . '</pre>'; exit;
182
+
183
+
184
+ // Validate that WebPExpress was configured to redirect to this conversion script
185
+ // (but do not require that for Nginx)
186
+ // ------------------------------------------------------------------------------
187
+ self::$checking = 'settings';
188
+ if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
189
+ if (!isset($wodOptions['enable-redirection-to-webp-realizer']) || ($wodOptions['enable-redirection-to-webp-realizer'] === false)) {
190
+ throw new ValidateException('Redirection to webp realizer is not enabled');
191
+ }
192
+ }
193
+
194
+ // Get destination
195
+ // --------------------------------------------
196
+ self::$checking = 'destination';
197
+ $destination = self::getDestination();
198
+
199
+ //self::exitWithError($destination);
200
+
201
+ // Validate source path
202
+ // --------------------------------------------
203
+ $checking = 'source path';
204
+ $source = ConvertHelperIndependent::findSource(
205
+ $destination,
206
+ $wodOptions['destination-folder'],
207
+ $wodOptions['destination-extension'],
208
+ self::$usingDocRoot ? 'doc-root' : 'image-roots',
209
+ self::$webExpressContentDirAbs,
210
+ self::getImageRootsDef()
211
+ );
212
+ //self::exitWithError('source:' . $source);
213
+ //echo '<h3>destination:</h3> ' . $destination . '<h3>source:</h3>' . $source; exit;
214
+
215
+ if ($source === false) {
216
+ header('X-WebP-Express-Error: webp-realizer.php could not find an existing jpg/png that corresponds to the webp requested', true);
217
+
218
+ $protocol = isset($_SERVER["SERVER_PROTOCOL"]) ? $_SERVER["SERVER_PROTOCOL"] : 'HTTP/1.0';
219
+ header($protocol . " 404 Not Found");
220
+ die();
221
+ //echo 'destination requested:<br><i>' . $destination . '</i>';
222
+ }
223
+ //$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
224
+
225
+ // Done with sanitizing, lets get to work!
226
+ // ---------------------------------------
227
+ $serveOptions['add-vary-header'] = false;
228
+ $serveOptions['fail'] = '404';
229
+ $serveOptions['fail-when-fail-fails'] = '404';
230
+ $serveOptions['serve-image']['headers']['vary-accept'] = false;
231
+
232
+ ConvertHelperIndependent::serveConverted(
233
+ $source,
234
+ $destination,
235
+ $serveOptions,
236
+ self::$webExpressContentDirAbs . '/log',
237
+ 'Conversion triggered with the conversion script (wod/webp-realizer.php)'
238
+ );
239
+
240
+ }
241
+
242
+ public static function processRequest() {
243
+ try {
244
+ self::processRequestNoTryCatch();
245
+ } catch (SanityException $e) {
246
+ self::exitWithError('Sanity check failed for ' . self::$checking . ': '. $e->getMessage());
247
+ } catch (ValidateException $e) {
248
+ self::exitWithError('Validation failed for ' . self::$checking . ': '. $e->getMessage());
249
+ } catch (\Exception $e) {
250
+ self::exitWithError('Error occured while calculating ' . self::$checking . ': '. $e->getMessage());
251
+ }
252
+ }
253
+ }
lib/classes/WodConfigLoader.php ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ This class is used by wod/webp-on-demand.php, which does not do a Wordpress bootstrap, but does register an autoloader for
4
+ the WebPExpress classes.
5
+
6
+ Calling Wordpress functions will FAIL. Make sure not to do that in either this class or the helpers.
7
+ */
8
+ //error_reporting(E_ALL);
9
+ //ini_set('display_errors', 1);
10
+
11
+ namespace WebPExpress;
12
+
13
+ use \WebPExpress\ImageRoots;
14
+ use \WebPExpress\Sanitize;
15
+ use \WebPExpress\SanityCheck;
16
+ use \WebPExpress\SanityException;
17
+ use \WebPExpress\ValidateException;
18
+ use \WebPExpress\Validate;
19
+
20
+ class WodConfigLoader
21
+ {
22
+
23
+ protected static $docRoot;
24
+ protected static $checking;
25
+ protected static $wodOptions;
26
+ protected static $options;
27
+ protected static $usingDocRoot;
28
+ protected static $webExpressContentDirAbs;
29
+
30
+ public static function exitWithError($msg) {
31
+ header('X-WebP-Express-Error: ' . $msg, true);
32
+ echo $msg;
33
+ exit;
34
+ }
35
+
36
+ /**
37
+ * Get environment variable set with mod_rewrite module
38
+ * Return false if the environment variable isn't found
39
+ */
40
+ protected static function getEnvPassedInRewriteRule($envName) {
41
+ // Envirenment variables passed through the REWRITE module have "REWRITE_" as a prefix (in Apache, not Litespeed, if I recall correctly)
42
+ // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
43
+ // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
44
+ // We simply look for an environment variable that ends with what we are looking for.
45
+ // (so make sure to make it unique)
46
+ $len = strlen($envName);
47
+ foreach ($_SERVER as $key => $item) {
48
+ if (substr($key, -$len) == $envName) {
49
+ return $item;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ protected static function getWebPExpressContentDirWithDocRoot()
56
+ {
57
+ // Get relative path to wp-content
58
+ // --------------------------------
59
+ self::$checking = 'Relative path to wp-content dir';
60
+
61
+ // Passed in env variable?
62
+ $wpContentDirRel = self::getEnvPassedInRewriteRule('WPCONTENT');
63
+ if ($wpContentDirRel === false) {
64
+ // Passed in QS?
65
+ if (isset($_GET['wp-content'])) {
66
+ $wpContentDirRel = SanityCheck::pathWithoutDirectoryTraversal($_GET['wp-content']);
67
+ } else {
68
+ // In case above fails, fall back to standard location
69
+ $wpContentDirRel = 'wp-content';
70
+ }
71
+ }
72
+
73
+ // Check WebP Express content dir
74
+ // ---------------------------------
75
+ self::$checking = 'WebP Express content dir';
76
+
77
+ $webExpressContentDirAbs = SanityCheck::absPathExistsAndIsDir(self::$docRoot . '/' . $wpContentDirRel . '/webp-express');
78
+ return $webExpressContentDirAbs;
79
+ }
80
+
81
+ protected static function getWebPExpressContentDirNoDocRoot() {
82
+ // Check wp-content
83
+ // ----------------------
84
+ self::$checking = 'path to wp-content dir';
85
+
86
+ // Passed in env variable?
87
+ $wpContentDirRelToPluginDir = self::getEnvPassedInRewriteRule('WE_WP_CONTENT_REL_TO_PLUGIN_DIR');
88
+ if ($wpContentDirRelToPluginDir === false) {
89
+ // Passed in QS?
90
+ if (isset($_GET['xwp-content-rel-to-plugin-dir'])) {
91
+
92
+ $xwpContentDirRelToPluginDir = SanityCheck::noControlChars($_GET['xwp-content-rel-to-plugin-dir']);
93
+ $wpContentDirRelToPluginDir = SanityCheck::pathDirectoryTraversalAllowed(substr($xwpContentDirRelToPluginDir, 1));
94
+
95
+ } else {
96
+ throw new \Exception('Path to wp-content was not received');
97
+ }
98
+ }
99
+
100
+ // Check WebP Express content dir
101
+ // ---------------------------------
102
+ self::$checking = 'WebP Express content dir';
103
+ // echo 'dir:' . $wpContentDirRelToPluginDir . '<br>'; exit;
104
+
105
+ $pathToPluginDir = dirname(dirname(dirname(__DIR__)));
106
+ //echo $pathToPluginDir; exit;
107
+
108
+ $webExpressContentDirAbs = SanityCheck::pathDirectoryTraversalAllowed($pathToPluginDir . '/' . $wpContentDirRelToPluginDir . '/webp-express');
109
+ //echo $webExpressContentDirAbs; exit;
110
+ if (@!file_exists($webExpressContentDirAbs)) {
111
+ throw new \Exception('Dir not found');
112
+ }
113
+ $webExpressContentDirAbs = @realpath($webExpressContentDirAbs);
114
+ if ($webExpressContentDirAbs === false) {
115
+ throw new \Exception('WebP Express content dir is outside restricted open_basedir!');
116
+ }
117
+ return $webExpressContentDirAbs;
118
+ }
119
+
120
+ protected static function getImageRootsDef()
121
+ {
122
+ if (!isset(self::$wodOptions['image-roots'])) {
123
+ throw new \Exception('No image roots defined in config.');
124
+ }
125
+ return new ImageRoots(self::$wodOptions['image-roots']);
126
+ }
127
+
128
+ protected static function loadConfig() {
129
+
130
+ $usingDocRoot = !(
131
+ isset($_GET['xwp-content-rel-to-plugin-dir']) ||
132
+ self::getEnvPassedInRewriteRule('WE_WP_CONTENT_REL_TO_PLUGIN_DIR')
133
+ );
134
+ self::$usingDocRoot = $usingDocRoot;
135
+
136
+ if ($usingDocRoot) {
137
+ // Check DOCUMENT_ROOT
138
+ // ----------------------
139
+ self::$checking = 'DOCUMENT_ROOT';
140
+ $docRootAvailable = PathHelper::isDocRootAvailableAndResolvable();
141
+ if (!$docRootAvailable) {
142
+ throw new \Exception(
143
+ 'Document root is no longer available. It was available when the .htaccess rules was created and ' .
144
+ 'the rules are based on that. You need to regenerate the rules (or fix your document root configuration)'
145
+ );
146
+ }
147
+
148
+ $docRoot = SanityCheck::absPath($_SERVER["DOCUMENT_ROOT"]);
149
+ $docRoot = rtrim($docRoot, '/');
150
+ self::$docRoot = $docRoot;
151
+ }
152
+
153
+ if ($usingDocRoot) {
154
+ self::$webExpressContentDirAbs = self::getWebPExpressContentDirWithDocRoot();
155
+ } else {
156
+ self::$webExpressContentDirAbs = self::getWebPExpressContentDirNoDocRoot();
157
+ }
158
+
159
+ // Check config file name
160
+ // ---------------------------------
161
+ self::$checking = 'config file';
162
+
163
+ $configFilename = self::$webExpressContentDirAbs . '/config/wod-options.json';
164
+ if (!file_exists($configFilename)) {
165
+ throw new \Exception('Configuration file was not found (wod-options.json)');
166
+ }
167
+
168
+ // Check config file
169
+ // --------------------
170
+ $configLoadResult = file_get_contents($configFilename);
171
+ if ($configLoadResult === false) {
172
+ throw new \Exception('Cannot open config file');
173
+ }
174
+ $json = SanityCheck::isJSONObject($configLoadResult);
175
+
176
+ self::$options = json_decode($json, true);
177
+ self::$wodOptions = self::$options['wod'];
178
+ }
179
+
180
+ }
lib/dismissable-messages/0.15.0/new-scope-setting-content.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WebPExpress;
3
+
4
+ // introduced in 0.14.0 (migrate 9)
5
+ DismissableMessages::printDismissableMessage(
6
+ 'info',
7
+ 'WebP Express 0.15 introduced a new "scope" setting which determines which folders that WebP Express ' .
8
+ 'operates in. It has been set to "All content" in order not to change behaviour. ' .
9
+ 'However, I would usually recommend limitting scope to "Uploads and Themes".',
10
+ '0.15.0/new-scope-setting-content',
11
+ 'Got it!'
12
+ );
lib/dismissable-messages/0.15.0/new-scope-setting-index.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ // introduced in 0.14.0 (migrate 9)
6
+ DismissableMessages::printDismissableMessage(
7
+ 'info',
8
+ 'WebP Express 0.15 introduced a new "scope" setting which determines which folders that WebP Express ' .
9
+ 'operates in. It has been set to work on "index" (any images in the whole install, including wp-adimn) ' .
10
+ 'in order not to change the behaviour. However, I would usually recommend a more limitted scope, ie. "Uploads and Themes".',
11
+ '0.15.0/new-scope-setting-index',
12
+ 'Got it!'
13
+ );
lib/dismissable-messages/0.15.0/new-scope-setting-no-uploads.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ DismissableMessages::printDismissableMessage(
6
+ 'info',
7
+ 'WebP Express 0.15 introduced a new "scope" setting which determines which folders that WebP Express ' .
8
+ 'operates in. The migration script did not set it to include "uploads" because it seemed that it ' .
9
+ 'would not be possible to write the .htaccess rules there.',
10
+ '0.15.0/new-scope-setting-no-uploads',
11
+ 'Got it!'
12
+ );
lib/migrate/migrate11.php ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPExpress;
4
+
5
+ use \WebPExpress\Config;
6
+ use \WebPExpress\Messenger;
7
+
8
+ function webpexpress_migrate11() {
9
+
10
+ $config = Config::loadConfigAndFix(false); // false, because we do not need to test if quality detection is working
11
+
12
+ // There is a new option: "scope"
13
+ // We want to set it to match current setup as closely as possible.
14
+
15
+ $rootsWithRulesIn = HTAccess::getRootsWithWebPExpressRulesIn();
16
+
17
+ if (in_array('index', $rootsWithRulesIn)) {
18
+ $scope = ['index', 'plugins', 'themes', 'uploads', 'wp-content'];
19
+ } elseif (in_array('wp-content', $rootsWithRulesIn)) {
20
+ $scope = ['plugins', 'themes', 'uploads', 'wp-content'];
21
+ } else {
22
+ $scope = ['themes', 'uploads'];
23
+ }
24
+
25
+ // However, if some of the roots cannot be used, we remove these.
26
+
27
+ $scope2 = [];
28
+ foreach ($scope as $rootId) {
29
+ if (Paths::canWriteHTAccessRulesInDir($rootId)) {
30
+ $scope2[] = $rootId;
31
+ }
32
+ }
33
+ if (count($scope2) == 0) {
34
+ Messenger::addMessage(
35
+ 'warning',
36
+ 'WebP Express cannot update the .htaccess rules that it needs to. ' .
37
+ 'Please go to WebP Express settings and click "Save settings and force new .htaccess rules".'
38
+ );
39
+ $scope2 = ['themes', 'uploads'];
40
+ }
41
+
42
+ $config['scope'] = $scope2;
43
+
44
+ if (in_array('index', $config['scope'])) {
45
+ DismissableMessages::addDismissableMessage('0.15.0/new-scope-setting-index');
46
+ } elseif (in_array('wp-content', $config['scope'])) {
47
+ DismissableMessages::addDismissableMessage('0.15.0/new-scope-setting-content');
48
+ } elseif (!in_array('uploads', $config['scope'])) {
49
+ DismissableMessages::addDismissableMessage('0.15.0/new-scope-setting-no-uploads');
50
+ }
51
+
52
+ /*
53
+ error_log('roots with rules:' . implode(',', $rootsWithRulesIn));
54
+ error_log('scope:' . implode(',', $config['scope']));
55
+ error_log('scope2:' . implode(',', $scope2));*/
56
+
57
+
58
+ $forceHtaccessRegeneration = true;
59
+ $result = Config::saveConfigurationAndHTAccess($config, $forceHtaccessRegeneration);
60
+
61
+ if ($result['saved-both-config']) {
62
+ Messenger::addMessage(
63
+ 'info',
64
+ 'Successfully migrated <i>WebP Express</i> options for 0.15.0.'
65
+ );
66
+ Option::updateOption('webp-express-migration-version', '11');
67
+
68
+ } else {
69
+ Messenger::addMessage(
70
+ 'error',
71
+ 'Failed migrating webp express options to 0.15.0. Probably you need to grant write permissions in your wp-content folder.'
72
+ );
73
+ }
74
+
75
+ }
76
+
77
+ webpexpress_migrate11();
lib/migrate/migrate7.php CHANGED
@@ -36,7 +36,7 @@ function webpexpress_migrate7() {
36
  if (Config::saveConfigurationFileAndWodOptions($config)) {
37
 
38
  $msg = 'Successfully migrated <i>WebP Express</i> options for 0.12. ';
39
- // The webp realizer rules where errornous, so recreate rules, if neccessary. (see issue #195)
40
 
41
  if (($config['enable-redirection-to-webp-realizer']) && ($config['destination-folder'] != 'mingled')) {
42
  HTAccess::saveRules($config);
36
  if (Config::saveConfigurationFileAndWodOptions($config)) {
37
 
38
  $msg = 'Successfully migrated <i>WebP Express</i> options for 0.12. ';
39
+ // The webp realizer rules where errornous, so recreate rules, if necessary. (see issue #195)
40
 
41
  if (($config['enable-redirection-to-webp-realizer']) && ($config['destination-folder'] != 'mingled')) {
42
  HTAccess::saveRules($config);
lib/options/css/webp-express-options-page.css CHANGED
@@ -9,6 +9,42 @@
9
  word-wrap: break-word;
10
  }
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
 
14
 
@@ -178,9 +214,27 @@
178
  .help .popup.wider {
179
  width: 450px;
180
  }
181
- .help .popup.widest {
182
  width: 600px;
183
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  @media (max-width: 500px) {
185
  .help .popup {
186
  max-width: 380px;
@@ -586,5 +640,5 @@ ul.with-bullets {
586
  bottom: 72px;
587
  position: absolute;
588
  right: 0;
589
- left: 3%;
590
  }
9
  word-wrap: break-word;
10
  }
11
 
12
+ /* Classes used in our pseudo markdown. */
13
+ .webpexpress.md h1 {
14
+ margin: 1em 0 1em;
15
+ font-size: 2em;
16
+ font-weight: normal;
17
+ }
18
+ .webpexpress.md h2 {
19
+ margin: 1em 0 0.5em;
20
+ font-size: 1.6em;
21
+ font-weight: normal;
22
+ text-transform: uppercase;
23
+ text-align: center;
24
+ padding: 10px 0;
25
+ background-color: #ccc;
26
+ }
27
+ .webpexpress.md h3 {
28
+ margin: 1em 0 0.5em;
29
+ font-size: 1.4em;
30
+ font-weight: normal;
31
+ }
32
+ .webpexpress.md h4 {
33
+ font-size: 1.0em;
34
+ margin: 1em 0 0.5em;
35
+ /*font-weight: 500;*/
36
+ font-weight: normal;
37
+ font-style: italic;
38
+ }
39
+ .webpexpress.md .warn {
40
+ background-color: yellow;
41
+ }
42
+ .webpexpress.md .ok {
43
+ color: green;
44
+ }
45
+ .webpexpress.md .error {
46
+ color: red;
47
+ }
48
 
49
 
50
 
214
  .help .popup.wider {
215
  width: 450px;
216
  }
217
+ .help .popup.even-wider {
218
  width: 600px;
219
  }
220
+ .help .popup.widest {
221
+ width: 750px;
222
+ }
223
+ @media (max-width: 1150px) {
224
+ .help .popup {
225
+ max-width: 620px;
226
+ }
227
+ }
228
+ @media (max-width: 900px) {
229
+ .help .popup {
230
+ max-width: 560px;
231
+ }
232
+ }
233
+ @media (max-width: 830px) {
234
+ .help .popup {
235
+ max-width: 530px;
236
+ }
237
+ }
238
  @media (max-width: 500px) {
239
  .help .popup {
240
  max-width: 380px;
640
  bottom: 72px;
641
  position: absolute;
642
  right: 0;
643
+ left: 3%;
644
  }
lib/options/enqueue_scripts.php CHANGED
@@ -2,14 +2,11 @@
2
 
3
  if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4
 
5
- include_once __DIR__ . '/../classes/Paths.php';
6
  use \WebPExpress\Paths;
7
-
8
- include_once __DIR__ . '/../classes/Config.php';
9
  use \WebPExpress\Config;
10
 
11
- $ver = '3'; // note: Minimum 1
12
- $jsDir = 'js/0.14.9'; // We change dir when it is critical that no-one gets the cached version (there is a plugin that strips version strings out there...)
13
 
14
  if (!function_exists('webp_express_add_inline_script')) {
15
  function webp_express_add_inline_script($id, $script, $position) {
@@ -33,6 +30,9 @@ wp_enqueue_script('escapehtml');
33
 
34
  $config = Config::getConfigForOptionsPage();
35
 
 
 
 
36
 
37
  // Add converter, bulk convert and whitelist script, EXCEPT for "no conversion" mode
38
  if (!(isset($config['operation-mode']) && ($config['operation-mode'] == 'no-conversion'))) {
@@ -51,10 +51,18 @@ if (!(isset($config['operation-mode']) && ($config['operation-mode'] == 'no-conv
51
  wp_register_script('converters', plugins_url($jsDir . '/converters.js', __FILE__), ['sortable', 'daspopup', 'escapehtml'], $ver);
52
 
53
  // PS: no escaping/sanitizing needed as json_encode always produces something safe
54
- webp_express_add_inline_script('converters', 'window.webpExpressPaths = ' . json_encode(Paths::getUrlsAndPathsForTheJavascript()) . ';', 'before');
 
 
 
 
55
 
56
  // PS: no escaping/sanitizing needed as json_encode always produces something safe
57
- webp_express_add_inline_script('converters', 'window.converters = ' . json_encode($config['converters']) . ';', 'before');
 
 
 
 
58
  wp_enqueue_script('converters');
59
 
60
  // Whitelist
@@ -76,6 +84,7 @@ if (!(isset($config['operation-mode']) && ($config['operation-mode'] == 'no-conv
76
  /*
77
  AlterHTMLHelper::getWebPUrlInBase(
78
  Paths::getPluginUrl() . '/webp-express', // source url
 
79
  Paths::getPluginUrl(), // base url
80
  Paths::getPluginDirAbs() // base dir
81
  );
@@ -99,12 +108,21 @@ if (!(isset($config['operation-mode']) && ($config['operation-mode'] == 'no-conv
99
  //wp_enqueue_script('api_keys');
100
 
101
  wp_register_script( 'page', plugins_url($jsDir . '/page.js', __FILE__), [], $ver);
 
 
 
 
 
 
 
 
 
 
 
 
102
  webp_express_add_inline_script(
103
  'page',
104
- 'window.webpExpressAjaxConvertNonce = "' . wp_create_nonce('webpexpress-ajax-convert-nonce') . '";' .
105
- 'window.webpExpressAjaxListUnconvertedFilesNonce = "' . wp_create_nonce('webpexpress-ajax-list-unconverted-files-nonce') . '";' .
106
- 'window.webpExpressAjaxPurgeCacheNonce = "' . wp_create_nonce('webpexpress-ajax-purge-cache-nonce') . '";' .
107
- 'window.webpExpressAjaxViewLogNonce = "' . wp_create_nonce('webpexpress-ajax-view-log-nonce') . '";',
108
  'before'
109
  );
110
  wp_enqueue_script('page');
2
 
3
  if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4
 
 
5
  use \WebPExpress\Paths;
 
 
6
  use \WebPExpress\Config;
7
 
8
+ $ver = '1'; // note: Minimum 1
9
+ $jsDir = 'js/0.15.0'; // We change dir when it is critical that no-one gets the cached version (there is a plugin that strips version strings out there...)
10
 
11
  if (!function_exists('webp_express_add_inline_script')) {
12
  function webp_express_add_inline_script($id, $script, $position) {
30
 
31
  $config = Config::getConfigForOptionsPage();
32
 
33
+ // selftest
34
+ wp_register_script('webpexpress_selftest', plugins_url($jsDir . '/self-test.js', __FILE__), ['escapehtml'], $ver);
35
+ wp_enqueue_script('webpexpress_selftest');
36
 
37
  // Add converter, bulk convert and whitelist script, EXCEPT for "no conversion" mode
38
  if (!(isset($config['operation-mode']) && ($config['operation-mode'] == 'no-conversion'))) {
51
  wp_register_script('converters', plugins_url($jsDir . '/converters.js', __FILE__), ['sortable', 'daspopup', 'escapehtml'], $ver);
52
 
53
  // PS: no escaping/sanitizing needed as json_encode always produces something safe
54
+ webp_express_add_inline_script(
55
+ 'converters',
56
+ 'window.webpExpressPaths = ' . json_encode(Paths::getUrlsAndPathsForTheJavascript()) . ';',
57
+ 'before'
58
+ );
59
 
60
  // PS: no escaping/sanitizing needed as json_encode always produces something safe
61
+ webp_express_add_inline_script(
62
+ 'converters',
63
+ 'window.converters = ' . json_encode($config['converters']) . ';',
64
+ 'before'
65
+ );
66
  wp_enqueue_script('converters');
67
 
68
  // Whitelist
84
  /*
85
  AlterHTMLHelper::getWebPUrlInBase(
86
  Paths::getPluginUrl() . '/webp-express', // source url
87
+ $baseId,
88
  Paths::getPluginUrl(), // base url
89
  Paths::getPluginDirAbs() // base dir
90
  );
108
  //wp_enqueue_script('api_keys');
109
 
110
  wp_register_script( 'page', plugins_url($jsDir . '/page.js', __FILE__), [], $ver);
111
+
112
+ // TODO: Add all vars needed to this array (whitelist, converters, etc)
113
+ $javascriptVars = [
114
+ 'ajax-nonces' => [
115
+ 'convert' => wp_create_nonce('webpexpress-ajax-convert-nonce'),
116
+ 'list-unconverted-files' => wp_create_nonce('webpexpress-ajax-list-unconverted-files-nonce'),
117
+ 'purge-cache' => wp_create_nonce('webpexpress-ajax-purge-cache-nonce'),
118
+ 'view-log' => wp_create_nonce('webpexpress-ajax-view-log-nonce'),
119
+ 'self-test' => wp_create_nonce('webpexpress-ajax-self-test-nonce'),
120
+ ],
121
+ 'can-use-doc-root-for-structuring' => Paths::canUseDocRootForRelPaths()
122
+ ];
123
  webp_express_add_inline_script(
124
  'page',
125
+ 'window.webpExpress = ' . json_encode($javascriptVars, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK),
 
 
 
126
  'before'
127
  );
128
  wp_enqueue_script('page');
lib/options/js/0.14.9/escapeHTML.js DELETED
@@ -1,16 +0,0 @@
1
- /*
2
- function htmlEscape(str) {
3
- return str
4
- .replace(/&/g, '&amp;')
5
- .replace(/"/g, '&quot;')
6
- .replace(/'/g, '&#39;')
7
- .replace(/</g, '&lt;')
8
- .replace(/>/g, '&gt;');
9
- }
10
- */
11
- function webpexpress_escapeHTML(s)
12
- {
13
- return s.replace(/./gm, function(s) {
14
- return "&#" + s.charCodeAt(0) + ";";
15
- });
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/options/js/{0.14.9 → 0.15.0}/authorized_sites_bak.js RENAMED
File without changes
lib/options/js/{0.14.9 → 0.15.0}/bulk-convert.js RENAMED
@@ -5,7 +5,7 @@ function openBulkConvertPopup() {
5
 
6
  var data = {
7
  'action': 'list_unconverted_files',
8
- 'nonce' : window.webpExpressAjaxListUnconvertedFilesNonce,
9
  };
10
  jQuery.post(ajaxurl, data, function(response) {
11
  if ((typeof response == 'object') && (response['success'] == false)) {
@@ -39,11 +39,11 @@ function openBulkConvertPopup() {
39
  } else {
40
  html += '<div>'
41
  html += '<p>There are ' + numFiles + ' unconverted files.</p>';
42
- html += '<p><i>Note that in a typical setup, you will have redirect rules which triggers conversion when needed, ' +
43
  'and thus you have no need for bulk conversion. In fact, in that case, you should probably not bulk convert ' +
44
  'because bulk conversion will also convert images and thumbnails which are not in use, and thus take up ' +
45
- 'more disk space than neccessary. The bulk conversion feature was only added in order to make the plugin usable even when ' +
46
- 'there is problems with redirects (ie on Nginx in case you do not have access to the config or on Microsoft IIS). ' +
47
  '</i></p><br>';
48
  html += '<button onclick="startBulkConversion()" class="button button-primary" type="button">Start conversion</button>';
49
  html += '</div>';
@@ -71,10 +71,10 @@ function pauseOrResumeBulkConversion() {
71
  function startBulkConversion() {
72
  var html = '<br>';
73
  html += '<style>' +
74
- '.has-tip {cursor:pointer; position:relative;}\n' +
75
  '.has-tip .tip {display: none}\n' +
76
  '.has-tip:hover .tip {display: block}\n' +
77
- '.tip{padding: 5px 10px; background-color:#ff9;position:absolute; right: 0; min-width:110px; font-size:10px; color: black; border:1px solid black; max-width:90%;z-index:10}\n' +
78
  '.reduction {float:right;}\n' +
79
  '</style>';
80
  html += '<button id="bulkPauseResumeBtn" onclick="pauseOrResumeBulkConversion()" class="button button-primary" type="button">Pause</button>';
@@ -151,7 +151,7 @@ function webpexpress_viewLog(groupPointer, filePointer) {
151
  url: ajaxurl,
152
  data: {
153
  'action': 'webpexpress_view_log',
154
- 'nonce' : window.webpExpressAjaxViewLogNonce,
155
  'source': source
156
  },
157
  success: (response) => {
@@ -218,7 +218,7 @@ function convertNextInBulkQueue() {
218
 
219
  var data = {
220
  'action': 'convert_file',
221
- 'nonce' : window.webpExpressAjaxConvertNonce,
222
  'filename': group.root + '/' + filename
223
 
224
  //'whatever': ajax_object.we_value // We pass php values differently!
@@ -262,14 +262,19 @@ function convertNextInBulkQueue() {
262
 
263
  bulkInfo['orgTotalFilesize'] += orgSize;
264
  bulkInfo['webpTotalFilesize'] += webpSize;
265
-
 
266
  html += ' <span style="color:green">ok</span></span>' +
267
  htmlViewLog +
268
-
 
 
 
 
 
 
 
269
  getReductionHtml(orgSize, webpSize, 'Size of original', 'Size of webp')
270
-
271
- //html += ' <span style="color:green" class="has-tip">ok<span class="tip">' + result['log'] + '</span></span>' +
272
- // getReductionHtml(orgSize, webpSize, 'Size of original', 'Size of webp')
273
  } else {
274
  html += ' <span style="color:red">failed</span>' + htmlViewLog;
275
  /*
5
 
6
  var data = {
7
  'action': 'list_unconverted_files',
8
+ 'nonce' : window.webpExpress['ajax-nonces']['list-unconverted-files'],
9
  };
10
  jQuery.post(ajaxurl, data, function(response) {
11
  if ((typeof response == 'object') && (response['success'] == false)) {
39
  } else {
40
  html += '<div>'
41
  html += '<p>There are ' + numFiles + ' unconverted files.</p>';
42
+ html += '<p><i>Note that in a typical setup, you will have redirect rules which trigger conversion when needed, ' +
43
  'and thus you have no need for bulk conversion. In fact, in that case, you should probably not bulk convert ' +
44
  'because bulk conversion will also convert images and thumbnails which are not in use, and thus take up ' +
45
+ 'more disk space than necessary. The bulk conversion feature was only added in order to make the plugin usable even when ' +
46
+ 'there are problems with redirects (ie on Nginx in case you do not have access to the config or on Microsoft IIS). ' +
47
  '</i></p><br>';
48
  html += '<button onclick="startBulkConversion()" class="button button-primary" type="button">Start conversion</button>';
49
  html += '</div>';
71
  function startBulkConversion() {
72
  var html = '<br>';
73
  html += '<style>' +
74
+ '.has-tip {cursor:pointer; position:static;}\n' +
75
  '.has-tip .tip {display: none}\n' +
76
  '.has-tip:hover .tip {display: block}\n' +
77
+ '.tip{padding: 5px 10px; background-color:#ff9;min-width:310px; font-size:10px; color: black; border:1px solid black; max-width:90%;z-index:10}\n' +
78
  '.reduction {float:right;}\n' +
79
  '</style>';
80
  html += '<button id="bulkPauseResumeBtn" onclick="pauseOrResumeBulkConversion()" class="button button-primary" type="button">Pause</button>';
151
  url: ajaxurl,
152
  data: {
153
  'action': 'webpexpress_view_log',
154
+ 'nonce' : window.webpExpress['ajax-nonces']['view-log'],
155
  'source': source
156
  },
157
  success: (response) => {
218
 
219
  var data = {
220
  'action': 'convert_file',
221
+ 'nonce' : window.webpExpress['ajax-nonces']['convert'],
222
  'filename': group.root + '/' + filename
223
 
224
  //'whatever': ajax_object.we_value // We pass php values differently!
262
 
263
  bulkInfo['orgTotalFilesize'] += orgSize;
264
  bulkInfo['webpTotalFilesize'] += webpSize;
265
+ //'- Saved at: ' + result['destination-path'] +
266
+ /*
267
  html += ' <span style="color:green">ok</span></span>' +
268
  htmlViewLog +
269
+ getReductionHtml(orgSize, webpSize, 'Size of original', 'Size of webp')*/
270
+
271
+ html += ' <span style="color:green" class="has-tip">ok' +
272
+ '<span class="tip">' +
273
+ '<b>Destination:</b><br>' + result['destination-path'] + '<br><br>' +
274
+ '<b>Url:</b><br><a href="' + result['destination-url'] + '">' + result['destination-url'] + '<br>' +
275
+ '</span>' +
276
+ '</span>' +
277
  getReductionHtml(orgSize, webpSize, 'Size of original', 'Size of webp')
 
 
 
278
  } else {
279
  html += ' <span style="color:red">failed</span>' + htmlViewLog;
280
  /*
lib/options/js/{0.14.9 → 0.15.0}/converters.js RENAMED
@@ -417,53 +417,8 @@ function encodePathforQS(path) {
417
  }
418
 
419
  function testConverter(id) {
420
- //alert('h' + id);
421
  openTestConvertPopup(id);
422
  return;
423
- var converter = window.convertersMap[id];
424
-
425
- // https://stackoverflow.com/questions/4321068/to-invoke-thickbox-using-javascript
426
-
427
- var urls = window.webpExpressPaths['urls'];
428
- var paths = window.webpExpressPaths['filePaths'];
429
-
430
- var url = '/' + urls['webpExpressRoot'] + '/test/test-run.php';
431
- //alert(url);
432
-
433
-
434
- // test images here: http://nottinghamtec.co.uk/~aer/TestPatterns/1080/
435
- filename = 'test.jpg';
436
- /* filename = 'stones.jpg';
437
- filename = 'architecture2.jpg';
438
- filename = 'test1.png';
439
- filename = 'focus.jpg';*/
440
-
441
- url += '?source=' + encodePathforQS(paths['webpExpressRoot'] + '/test/' + filename);
442
- url += '&configDirRel=' + encodePathforQS(paths['configRelToDocRoot']);
443
- url += '&destination=' + encodePathforQS(paths['destinationRoot'] + '/test-conversions/' + filename + '.webp');
444
- url += '&converter=' + converter['converter'];
445
- if (document.getElementById('max_quality')) {
446
- url += '&max-quality=' + document.getElementById('max_quality').value;
447
- }
448
- if (document.getElementById('quality_specific')) {
449
- url += '&quality=' + document.getElementById('quality_specific').value;
450
- }
451
- //url += '&method=' + document.getElementsByName('webp_express_method')[0].value;
452
-
453
- if (converter.options) {
454
- for (var option in converter.options) {
455
- if (converter.options.hasOwnProperty(option)) {
456
- //alert(option);
457
- url += '&' + option + '=' + converter.options[option];
458
- }
459
- }
460
- }
461
- var w = Math.min(1000, Math.max(200, document.documentElement.clientWidth - 100));
462
- var h = Math.max(250, document.documentElement.clientHeight - 80);
463
-
464
- url += '&TB_iframe=true&width=' + w + '&height=' + h;
465
- //alert(url);
466
- tb_show("Test running converter: " + converter['id'], url);
467
  }
468
 
469
  /*
417
  }
418
 
419
  function testConverter(id) {
 
420
  openTestConvertPopup(id);
421
  return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  }
423
 
424
  /*
lib/options/js/{0.14.9 → 0.15.0}/das-popup.js RENAMED
File without changes
lib/options/js/0.15.0/escapeHTML.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ function htmlEscape(str) {
3
+ return str
4
+ .replace(/&/g, '&amp;')
5
+ .replace(/"/g, '&quot;')
6
+ .replace(/'/g, '&#39;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;');
9
+ }
10
+ */
11
+ function webpexpress_escapeHTML(s)
12
+ {
13
+ return s.replace(/./gm, function(s) {
14
+ var safe = /[0-9a-zA-Z\!]/;
15
+ if (safe.test(s.charAt(0))) {
16
+ return s.charAt(0);
17
+ }
18
+
19
+ switch (s.charAt(0)) {
20
+ case '*':
21
+ case '#':
22
+ case ' ':
23
+ case '{':
24
+ case '}':
25
+ case ':':
26
+ case '.':
27
+ case '`':
28
+ return s.charAt(0);
29
+ default:
30
+ return "&#" + s.charCodeAt(0) + ";";
31
+ }
32
+
33
+ });
34
+ }
lib/options/js/{0.14.9 → 0.15.0}/image-comparison-slider.js RENAMED
File without changes
lib/options/js/{0.14.9 → 0.15.0}/page.js RENAMED
@@ -147,12 +147,16 @@ document.addEventListener('DOMContentLoaded', function() {
147
 
148
  // PNG encoding changing
149
  if (el('image_types') && el('png_row')) {
150
- function updatePngRow() {
151
- toggleVisibility('png_row', el('image_types').value != '1');
 
 
 
 
152
  }
153
- updatePngRow();
154
  el('image_types').addEventListener('change', function() {
155
- updatePngRow();
156
  });
157
  }
158
 
@@ -180,13 +184,22 @@ document.addEventListener('DOMContentLoaded', function() {
180
  });
181
  }
182
 
 
 
 
 
183
 
 
 
 
 
 
184
 
185
  // Toggle File Extension (only show when "mingled" is selected)
186
- if (el('destination_folder') && el('destination_extension_row') && el('destination_extension')) {
187
- el('destination_extension_row').classList.add('effect-opacity');
188
  function updateDestinationExtensionVisibility() {
189
- toggleVisibility('destination_extension_row', el('destination_folder').value == 'mingled');
190
 
191
  if (el('destination_folder').value == 'separate') {
192
  el('destination_extension').value = 'append';
147
 
148
  // PNG encoding changing
149
  if (el('image_types') && el('png_row')) {
150
+ function updatePngAndJpgRowVisibilities() {
151
+ var imageTypes = parseInt(el('image_types').value, 10);
152
+ var pngEnabled = (imageTypes & 2);
153
+ var jpegEnabled = (imageTypes & 1);
154
+ toggleVisibility('png_row', pngEnabled);
155
+ toggleVisibility('jpeg_row', jpegEnabled);
156
  }
157
+ updatePngAndJpgRowVisibilities();
158
  el('image_types').addEventListener('change', function() {
159
+ updatePngAndJpgRowVisibilities();
160
  });
161
  }
162
 
184
  });
185
  }
186
 
187
+ // If "doc-root" cannot be used for structuring, disable the option and set to "image-roots"
188
+ if (!window.webpExpress['can-use-doc-root-for-structuring']) {
189
+ el('destination_structure').classList.add('effect-opacity');
190
+ toggleVisibility('destination_structure', false);
191
 
192
+ if (el('destination_structure').value == 'doc-root') {
193
+ el('destination_structure').value = 'image-roots';
194
+ }
195
+
196
+ }
197
 
198
  // Toggle File Extension (only show when "mingled" is selected)
199
+ if (el('destination_folder') && el('destination_extension')) {
200
+ el('destination_extension').classList.add('effect-opacity');
201
  function updateDestinationExtensionVisibility() {
202
+ toggleVisibility('destination_extension', el('destination_folder').value == 'mingled');
203
 
204
  if (el('destination_folder').value == 'separate') {
205
  el('destination_extension').value = 'append';
lib/options/js/{0.14.9 → 0.15.0}/purge-cache.js RENAMED
@@ -16,7 +16,7 @@ function openDeleteConvertedFilesPopup() {
16
  function purgeCache(onlyPng) {
17
  var data = {
18
  'action': 'webpexpress_purge_cache',
19
- 'nonce' : window.webpExpressAjaxPurgeCacheNonce,
20
  'only-png': onlyPng
21
  };
22
  jQuery.post(ajaxurl, data, function(response) {
16
  function purgeCache(onlyPng) {
17
  var data = {
18
  'action': 'webpexpress_purge_cache',
19
+ 'nonce' : window.webpExpress['ajax-nonces']['purge-cache'],
20
  'only-png': onlyPng
21
  };
22
  jQuery.post(ajaxurl, data, function(response) {
lib/options/js/0.15.0/self-test.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ if (typeof WebPExpress == 'undefined') {
2
+ WebPExpress = {};
3
+ }
4
+
5
+ WebPExpress.SelfTest = {
6
+ 'clear': function() {
7
+ document.getElementById('webpexpress_test_redirection_content').innerHTML = '';
8
+ },
9
+ 'write': function(html) {
10
+ //var el = document.getElementById('webpexpress_test_redirection_content');
11
+ //el.innerHTML += html;
12
+ var el = document.createElement('div');
13
+ el.innerHTML = html;
14
+ document.getElementById('webpexpress_test_redirection_content').appendChild(el);
15
+ },
16
+ 'simpleMdToHtml': function(line) {
17
+ //return s.replace(/./gm, function(s) {
18
+
19
+
20
+ if (line.substr(0, 3) == '```') {
21
+ return '<pre>' + line.replace(/\`/gm, '') + '</pre>';
22
+ }
23
+
24
+ // Bold with inline attributtes (ie: "hi **bold**{: .red}")
25
+ line = line.replace(/\*\*([^\*]+)\*\*\{:\s([^}]+)\}/gm, function(s, g1, g2) {
26
+ // g2 is the inline attributes.
27
+ // right now we only support classes, and only ONE class.
28
+ // so it is easy.
29
+ var className = g2.substr(1);
30
+
31
+ return '<b class="' + className + '">' + g1 + '</b>';
32
+ //return '<b>' + s.substr(2, s.length - 4) + '</b>';
33
+ });
34
+
35
+ // Bold
36
+ line = line.replace(/(\*\*[^\*]+\*\*)/gm, function(s) {
37
+ return '<b>' + s.substr(2, s.length - 4) + '</b>';
38
+ });
39
+
40
+ // Italic
41
+ line = line.replace(/(\*[^\*]+\*)/gm, function(s) {
42
+ return '<i>' + s.substr(1, s.length - 2) + '</i>';
43
+ });
44
+
45
+ // Headline
46
+ if (line.substr(0, 2) == '# ') {
47
+ line = '<h1>' + line.substr(2) + '</h1>';
48
+ }
49
+ if (line.substr(0, 3) == '## ') {
50
+ line = '<h2>' + line.substr(3) + '</h2>';
51
+ }
52
+ if (line.substr(0, 4) == '### ') {
53
+ line = '<h3>' + line.substr(4) + '</h3>';
54
+ }
55
+ if (line.substr(0, 5) == '#### ') {
56
+ line = '<h4>' + line.substr(5) + '</h4>';
57
+ }
58
+
59
+ // Empty line
60
+ if (line == '') {
61
+ line = '<br>';
62
+ }
63
+
64
+
65
+ // "ok!" green
66
+ line = line.replace(/ok\!/gmi, function(s) {
67
+ return '<span style="color:green; font-weight: bold;">ok</span>';
68
+ });
69
+
70
+ // "great" green
71
+ /*
72
+ line = line.replace(/great/gmi, function(s) {
73
+ return '<span style="color:green; font-weight: bold;">' + s + '</span>';
74
+ });*/
75
+
76
+ // "failed" red
77
+ line = line.replace(/failed/gmi, function(s) {
78
+ return '<span style="color:red; font-weight:bold">FAILED</span>';
79
+ });
80
+
81
+ return '<div class="webpexpress md">' + line + '</div>';
82
+
83
+ },
84
+ 'responseHandler': function(response) {
85
+ if ((typeof response == 'object') && (response['success'] == false)) {
86
+ html = '<h1>Error</h1>';
87
+ if (response['data'] && ((typeof response['data']) == 'string')) {
88
+ html += webpexpress_escapeHTML(response['data']);
89
+ }
90
+ WebPExpress.SelfTest.write(html);
91
+ document.getElementById('bulkconvertcontent').innerHTML = html;
92
+ return;
93
+ }
94
+
95
+ var responseObj = JSON.parse(response);
96
+ var result = responseObj['result'];
97
+ if (typeof result == 'string') {
98
+ result = [result];
99
+ }
100
+
101
+ for (var i=0; i<result.length; i++) {
102
+ var line = result[i];
103
+ if (typeof line != 'string') {
104
+ continue;
105
+ }
106
+ line = webpexpress_escapeHTML(line);
107
+ line = WebPExpress.SelfTest.simpleMdToHtml(line);
108
+
109
+ WebPExpress.SelfTest.write(line);
110
+ }
111
+ //result = result.join('<br>');
112
+
113
+
114
+
115
+ var next = responseObj['next'];
116
+ if (next == 'done') {
117
+ //WebPExpress.SelfTest.write('<br>done');
118
+ } else if (next == 'break') {
119
+ WebPExpress.SelfTest.write('breaking');
120
+ } else {
121
+ WebPExpress.SelfTest.runTest(next);
122
+ }
123
+
124
+ },
125
+ 'runTest': function(testId) {
126
+ var data = {
127
+ 'action': 'webpexpress_self_test',
128
+ 'testId': testId,
129
+ 'nonce' : window.webpExpress['ajax-nonces']['self-test'],
130
+ };
131
+
132
+ jQuery.ajax({
133
+ method: 'POST',
134
+ url: ajaxurl,
135
+ data: data,
136
+ success: WebPExpress.SelfTest.responseHandler,
137
+ error: function() {
138
+ WebPExpress.SelfTest.write('PHP error. Check your debug.log for more info (make sure debugging is enabled)');
139
+ },
140
+ });
141
+ },
142
+ 'openPopup': function(testId) {
143
+ WebPExpress.SelfTest.clear();
144
+ var w = Math.min(1000, Math.max(200, document.documentElement.clientWidth - 100));
145
+ var h = Math.max(250, document.documentElement.clientHeight - 80);
146
+
147
+ var title = 'Self testing';
148
+ if (testId == 'redirectToExisting') {
149
+ title = 'Testing redirection to existing webp';
150
+ }
151
+ tb_show(title, '#TB_inline?inlineId=webpexpress_test_redirection_popup&width=' + w + '&height=' + h);
152
+
153
+ WebPExpress.SelfTest.runTest(testId);
154
+
155
+ }
156
+ }
lib/options/js/{0.14.9 → 0.15.0}/sortable.min.js RENAMED
File without changes
lib/options/js/{0.14.9 → 0.15.0}/test-convert.js RENAMED
@@ -1,4 +1,3 @@
1
-
2
  function openTestConvertPopup(converterId) {
3
  var html = '<div id="tc_conversion_options">options</div><div><div id="tc_conversion_result"><h2>Result</h2>wait...</div></div>'
4
  document.getElementById('tc_content').innerHTML = html;
@@ -130,7 +129,7 @@ function runTestConversion() {
130
 
131
  var data = {
132
  'action': 'convert_file',
133
- 'nonce': window.webpExpressAjaxConvertNonce,
134
  'filename': window.webpExpressPaths['filePaths']['webpExpressRoot'] + '/test/' + elTxt('image'),
135
  "converter": elTxt("converter"),
136
  'config-overrides': JSON.stringify(configOverrides)
@@ -239,14 +238,21 @@ function convertResponseCallback(response){
239
  var srcUrl = '/' + window.webpExpressPaths['urls']['webpExpressRoot'] + '/test/' + filename;
240
  //html += '<img src="/' + srcUrl + '" style="width:100%">';
241
 
 
 
 
 
242
  var webpUrl = '/' + window.webpExpressPaths['urls']['content'] +
243
  '/webp-express/webp-images/doc-root/' +
244
  window.webpExpressPaths['filePaths']['pluginRelToDocRoot'] + '/' +
245
  'webp-express/' +
246
  'test/' +
247
  filename + '.webp';
 
248
  //html += '<img src="' + webpUrl + '" style="width:100%">';
249
 
 
 
250
  html += '<div class="cd-image-container">';
251
  html += ' <div class="cd-image-label webp">WebP: ' + webpSizeStr + '</div>';
252
  html += ' <div class="cd-image-label original">' + (filename.toLowerCase().indexOf('png') > 0 ? 'PNG' : 'JPEG') + ': ' + orgSizeStr + '</div>';
 
1
  function openTestConvertPopup(converterId) {
2
  var html = '<div id="tc_conversion_options">options</div><div><div id="tc_conversion_result"><h2>Result</h2>wait...</div></div>'
3
  document.getElementById('tc_content').innerHTML = html;
129
 
130
  var data = {
131
  'action': 'convert_file',
132
+ 'nonce': window.webpExpress['ajax-nonces']['convert'],
133
  'filename': window.webpExpressPaths['filePaths']['webpExpressRoot'] + '/test/' + elTxt('image'),
134
  "converter": elTxt("converter"),
135
  'config-overrides': JSON.stringify(configOverrides)
238
  var srcUrl = '/' + window.webpExpressPaths['urls']['webpExpressRoot'] + '/test/' + filename;
239
  //html += '<img src="/' + srcUrl + '" style="width:100%">';
240
 
241
+
242
+
243
+ // TODO: THIS DOES NOT WORK. NEEDS ATTENTION!
244
+ /*
245
  var webpUrl = '/' + window.webpExpressPaths['urls']['content'] +
246
  '/webp-express/webp-images/doc-root/' +
247
  window.webpExpressPaths['filePaths']['pluginRelToDocRoot'] + '/' +
248
  'webp-express/' +
249
  'test/' +
250
  filename + '.webp';
251
+ */
252
  //html += '<img src="' + webpUrl + '" style="width:100%">';
253
 
254
+ var webpUrl = result['destination-url'];
255
+
256
  html += '<div class="cd-image-container">';
257
  html += ' <div class="cd-image-label webp">WebP: ' + webpSizeStr + '</div>';
258
  html += ' <div class="cd-image-label original">' + (filename.toLowerCase().indexOf('png') > 0 ? 'PNG' : 'JPEG') + ': ' + orgSizeStr + '</div>';
lib/options/js/{0.14.9 → 0.15.0}/whitelist.js RENAMED
File without changes
lib/options/options/conversion-options/jpeg.inc CHANGED
@@ -19,7 +19,7 @@ png
19
  //$canDetectQuality = false;
20
  ?>
21
 
22
- <tr>
23
  <th scope="row" colspan=1>
24
  Jpeg options
25
  <?php echo helpIcon(
19
  //$canDetectQuality = false;
20
  ?>
21
 
22
+ <tr id="jpeg_row" class="toggler effect-opacity">
23
  <th scope="row" colspan=1>
24
  Jpeg options
25
  <?php echo helpIcon(
lib/options/options/general/destination-extension.inc CHANGED
@@ -30,7 +30,8 @@
30
  'or <a target="_blank" href="https://wordpress.org/plugins/shortpixel-image-optimiser/">Shortpixel</a>, set this option to Set"</p>' .
31
  (($config['operation-mode'] == 'cdn-friendly') ? '<p>In this mode, the webp files will be stored in the same folder as the originals, except for images that are not inside the uploads folder (these are stored in wp-content/webp-express/webp-images/doc-root).</p>' : '') .
32
  '<p>Changing this option will cause existing webp images to be renamed (only those in the upload folder, and only those that has a ' .
33
- 'corresponding source image)</p>'
 
34
  );
35
  }
36
  ?>
30
  'or <a target="_blank" href="https://wordpress.org/plugins/shortpixel-image-optimiser/">Shortpixel</a>, set this option to Set"</p>' .
31
  (($config['operation-mode'] == 'cdn-friendly') ? '<p>In this mode, the webp files will be stored in the same folder as the originals, except for images that are not inside the uploads folder (these are stored in wp-content/webp-express/webp-images/doc-root).</p>' : '') .
32
  '<p>Changing this option will cause existing webp images to be renamed (only those in the upload folder, and only those that has a ' .
33
+ 'corresponding source image)</p>' .
34
+ '<p>Note that this option only applies to the webp images stored in the uploads folder (mingled).</p>'
35
  );
36
  }
37
  ?>
lib/options/options/general/destination-structure.inc ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <tr id="destination_structure_row">
3
+ <th scope="row">Destination structure<?php
4
+ echo helpIcon(
5
+ '<p>This setting determines how the converted files are structured within the folder that WebP Express ' .
6
+ 'uses for storing webp images</p>' .
7
+ '<p><span style="text-decoration:underline">"document root"</span><br>' .
8
+ 'When "document root" is selected, the webp images will be stored in:<br>' .
9
+ '<span style="white-space:pre-wrap;font-family:monospace">[cache root]/doc-root/[relative path of source image, from document root].</span><br>' .
10
+ '</p>' .
11
+ '<p><span style="text-decoration:underline">"image roots"</span><br>' .
12
+ 'A Wordpress site has images stored in different locations. Uploaded files are for example stored in the <i>uploads</i> folder, which is ' .
13
+ 'usually - but not always - located in the "wp-content" folder. I call the uploads folder an "image root". Other roots are: ' .
14
+ '"themes", "plugins", "wp-content" and "index". When the "image roots" setting is selected, the webp files ' .
15
+ 'are stored in a structure that mirrors the relative path of the source image within its image root. ' .
16
+ 'For "uploads", that location is:<br>' .
17
+ '<span style="white-space:pre-wrap;font-family:monospace">[cache root]/uploads/[relative path of source image, from uploads root].</span><br>' .
18
+ 'More generally we have:<br>' .
19
+ '<span style="white-space:pre-wrap;font-family:monospace"">[cache root]/[image root]/[relative path of source image, from its image root].</span><br>' .
20
+ '</p>' .
21
+ '<p><span style="text-decoration:underline">Which option is best?</span><br>' .
22
+ 'Well, in most cases it does not matter. However, as the "image roots" option is new, I would recommend staying with the ' .
23
+ '"document root" setting. On Nginx, I recommend "document root", as it requires fewer rewrite rules.</p>' .
24
+ '<p>The "image roots" option was added to make WebP Express work on systems where the document root is set up incorrectly or ' .
25
+ 'is outside opendir restriction or an image root is located outside document root. WebP Express automatically detects this and will not allow you to select "document root" in those cases.</p>'
26
+ );
27
+ ?></th>
28
+ <td>
29
+ <select name="destination-structure" id="destination_structure">
30
+ <?php
31
+ webpexpress_selectBoxOptions($config['destination-structure'], [
32
+ 'doc-root' => 'Document root',
33
+ 'image-roots' => 'Image roots',
34
+ ]);
35
+ ?>
36
+ </select>
37
+ </td>
38
+ </tr>
lib/options/options/general/general.inc CHANGED
@@ -1,9 +1,16 @@
1
  <fieldset class="block">
 
 
 
2
  <h3>General</h3>
 
3
  <!--<div><i>The options here affects the rules created in the .htaccess. <?php echo helpIcon('And so does some other options. If "Redirect directly to converted image" is set, the "Destination folder" and "File Extension" and "Caching" options will be used'); ?></i></div>-->
4
  <table class="form-table">
5
  <tbody>
6
  <?php
 
 
 
7
  include_once 'image-types.inc';
8
 
9
  if (($config['operation-mode'] != 'no-conversion')) {
@@ -11,6 +18,10 @@
11
  }
12
  include_once 'destination-extension.inc';
13
 
 
 
 
 
14
  // TODO:
15
  // Perhaps also show cache control in cdn-friendly mode?
16
  if (($config['operation-mode'] == 'tweaked') || ($config['operation-mode'] == 'varied-image-responses')) {
1
  <fieldset class="block">
2
+ <button onclick="WebPExpress.SelfTest.openPopup('allInfo')" class="button button-secondary" type="button" style="position: absolute; right: 15px; top:15px">
3
+ System info
4
+ </button>
5
  <h3>General</h3>
6
+
7
  <!--<div><i>The options here affects the rules created in the .htaccess. <?php echo helpIcon('And so does some other options. If "Redirect directly to converted image" is set, the "Destination folder" and "File Extension" and "Caching" options will be used'); ?></i></div>-->
8
  <table class="form-table">
9
  <tbody>
10
  <?php
11
+ if (($config['operation-mode'] != 'no-conversion')) {
12
+ include_once 'scope.inc';
13
+ }
14
  include_once 'image-types.inc';
15
 
16
  if (($config['operation-mode'] != 'no-conversion')) {
18
  }
19
  include_once 'destination-extension.inc';
20
 
21
+ if (($config['operation-mode'] != 'no-conversion')) {
22
+ include_once 'destination-structure.inc';
23
+ }
24
+
25
  // TODO:
26
  // Perhaps also show cache control in cdn-friendly mode?
27
  if (($config['operation-mode'] == 'tweaked') || ($config['operation-mode'] == 'varied-image-responses')) {
lib/options/options/general/image-types.inc CHANGED
@@ -33,6 +33,7 @@ $imageTypes = $config['image-types'];
33
  echo '<select name="image-types" id="image_types">';
34
  echo '<option value="0"' . ($imageTypes == 0 ? ' selected' : '') . '>None! (disable)</option>';
35
  echo '<option value="1"' . ($imageTypes == 1 ? ' selected' : '') . '>Only jpegs</option>';
 
36
  echo '<option value="3"' . ($imageTypes == 3 ? ' selected' : '') . '>Both jpegs and pngs</option>';
37
  echo '</select>';
38
 
33
  echo '<select name="image-types" id="image_types">';
34
  echo '<option value="0"' . ($imageTypes == 0 ? ' selected' : '') . '>None! (disable)</option>';
35
  echo '<option value="1"' . ($imageTypes == 1 ? ' selected' : '') . '>Only jpegs</option>';
36
+ echo '<option value="2"' . ($imageTypes == 2 ? ' selected' : '') . '>Only pngs</option>';
37
  echo '<option value="3"' . ($imageTypes == 3 ? ' selected' : '') . '>Both jpegs and pngs</option>';
38
  echo '</select>';
39
 
lib/options/options/general/scope.inc ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <tr>
2
+ <th scope="row">Scope<?php
3
+ echo helpIcon(
4
+ '<p>This setting determines which folders WebP Express is operational in. ' .
5
+ 'If for example "Uploads only" is selected, WebP Express will only convert the upload images and only put ' .
6
+ 'an <i>.htaccess</i> file in the <i>uploads</i> folder (if needed). Also, <i>Alter HTML</i> will limit itself to that area.' .
7
+ '</p>' .
8
+ '<p>The "All content" setting will work on <i>uploads</i>, <i>themes</i>, <i>plugins</i> - anything in the "wp-content" ' .
9
+ '(or whatever it has been renamed to). It will work on uploads, even if the uploads folder has been ' .
10
+ 'configured to reside outside of wp-content - and on <i>plugins<i>, even if plugins has been moved.</p>'
11
+ );
12
+ ?></th>
13
+ <td>
14
+ <select name="scope" id="scope">
15
+ <?php
16
+ /*webpexpress_selectBoxOptions($config['destination-structure'], [
17
+ 'doc-root' => 'Document root',
18
+ 'image-roots' => 'Image roots',
19
+ ]);
20
+ */
21
+ $imageTypes = $config['scope'];
22
+ sort($imageTypes, SORT_STRING);
23
+ $imageTypesString = implode(',', $imageTypes);
24
+
25
+ $options = [
26
+ ['uploads', 'Uploads only'],
27
+ ['themes', 'Themes only'],
28
+ ['themes,uploads', 'Uploads and themes'],
29
+ ['plugins,themes,uploads,wp-content', 'All content'],
30
+ ['index,plugins,themes,uploads,wp-content', 'Everything (including wp-admin)'],
31
+ ];
32
+
33
+ $otherValidOptions = ['index', 'wp-content'];
34
+
35
+ if (in_array($imageTypesString, $otherValidOptions)) {
36
+ $options[] = [$imageTypesString, $imageTypesString];
37
+ }
38
+
39
+ $hasSelected = false;
40
+ foreach ($options as list($optionId, $optionName)) {
41
+ $selected = ($imageTypesString == $optionId);
42
+ if ($selected) {
43
+ $hasSelected = true;
44
+ }
45
+ echo '<option value="' . $optionId . '"' . ($selected ? ' selected' : '') . '>' . $optionName . '</option>';
46
+ }
47
+
48
+ if (!$hasSelected) {
49
+ // Non-default option is "selected".
50
+ // This can be done in migrate11.php or user could have edited config.php manually.
51
+ // PS: We can rest assure that scope is sanitized - any invalid rootId is filtered out in Config::fix.
52
+ echo '<option value="' . $imageTypesString . '" selected>' .
53
+ 'Custom: ' . implode(', ', explode(',', $imageTypesString)) .
54
+ '</option>';
55
+ }
56
+ ?>
57
+ </select>
58
+ </td>
59
+ </tr>
lib/options/options/redirection-rules/enable-redirection-to-converter.inc CHANGED
@@ -18,5 +18,8 @@
18
  value="true"
19
  type="checkbox"
20
  >
 
 
 
21
  </td>
22
  </tr>
18
  value="true"
19
  type="checkbox"
20
  >
21
+ <button style="margin-left: 30px;" onclick="WebPExpress.SelfTest.openPopup('redirectToConverter')" class="button button-secondary" type="button">
22
+ Live test
23
+ </button>
24
  </td>
25
  </tr>
lib/options/options/redirection-rules/enable-redirection-to-webp-realizer.inc CHANGED
@@ -1,6 +1,6 @@
1
  <tr>
2
  <th scope="row">
3
- Create webp files upon request?</span>
4
  <?php echo helpIcon(
5
  '<p>Enabling this option will add lines in the .htaccess which redirects requests for non-existing webp-files to ' .
6
  'the converter script (webp-realizer.php). ' .
@@ -25,5 +25,8 @@
25
  value="true"
26
  type="checkbox"
27
  >
 
 
 
28
  </td>
29
  </tr>
1
  <tr>
2
  <th scope="row">
3
+ Create webp files upon request?
4
  <?php echo helpIcon(
5
  '<p>Enabling this option will add lines in the .htaccess which redirects requests for non-existing webp-files to ' .
6
  'the converter script (webp-realizer.php). ' .
25
  value="true"
26
  type="checkbox"
27
  >
28
+ <button style="margin-left: 30px;" onclick="WebPExpress.SelfTest.openPopup('redirectToWebPRealizer')" class="button button-secondary" type="button">
29
+ Live test
30
+ </button>
31
  </td>
32
  </tr>
lib/options/options/redirection-rules/redirect-to-existing.inc CHANGED
@@ -24,6 +24,10 @@
24
  echo ($config['redirect-to-existing-in-htaccess'] ? 'checked="checked"' : '')
25
  ?> >
26
 
 
 
 
 
27
  <?php
28
  /*if ($config['operation-mode'] == 'no-conversion') {
29
  // Cache control header
24
  echo ($config['redirect-to-existing-in-htaccess'] ? 'checked="checked"' : '')
25
  ?> >
26
 
27
+ <button style="margin-left: 30px;" onclick="WebPExpress.SelfTest.openPopup('redirectToExisting')" class="button button-secondary" type="button">
28
+ Live test
29
+ </button>
30
+
31
  <?php
32
  /*if ($config['operation-mode'] == 'no-conversion') {
33
  // Cache control header
lib/options/options/redirection-rules/redirection-rules.inc CHANGED
@@ -1,14 +1,14 @@
1
  <fieldset class="block">
2
- <?php if ($config['operation-mode'] == 'no-conversion') : ?>
3
  <h2>Redirecting jpeg/png to existing webp (varied image response)</h2>
4
  <p>
5
  Enabling this adds rules to the <i>.htaccess</i> which internally redirects jpg/pngs to webp
6
  and sets the <i>Vary:Accept</i> response header.
7
  <i>Beware that special attention is needed if you are using a CDN (see FAQ).</i>
8
  </p>
9
- <?php elseif ($config['operation-mode'] == 'cdn-friendly') : ?>
10
  <h2><i>.htaccess</i> rules for webp generation</h2>
11
- <?php else : ?>
12
  <h3>Redirection rules</h3>
13
  <div><i>The options here affects the rules created in the .htaccess. <?php
14
  echo helpIcon('Note: The general options also affects the rules.');
@@ -44,8 +44,14 @@
44
  include_once 'enable-redirection-to-converter.inc';
45
  include_once 'enable-redirection-to-webp-realizer.inc';
46
  break;
 
 
 
47
  }
48
  ?>
49
  </tbody>
50
  </table>
 
 
 
51
  </fieldset>
1
  <fieldset class="block">
2
+ <?php if ($config['operation-mode'] == 'no-conversion') : ?>
3
  <h2>Redirecting jpeg/png to existing webp (varied image response)</h2>
4
  <p>
5
  Enabling this adds rules to the <i>.htaccess</i> which internally redirects jpg/pngs to webp
6
  and sets the <i>Vary:Accept</i> response header.
7
  <i>Beware that special attention is needed if you are using a CDN (see FAQ).</i>
8
  </p>
9
+ <?php elseif ($config['operation-mode'] == 'cdn-friendly') : ?>
10
  <h2><i>.htaccess</i> rules for webp generation</h2>
11
+ <?php else : ?>
12
  <h3>Redirection rules</h3>
13
  <div><i>The options here affects the rules created in the .htaccess. <?php
14
  echo helpIcon('Note: The general options also affects the rules.');
44
  include_once 'enable-redirection-to-converter.inc';
45
  include_once 'enable-redirection-to-webp-realizer.inc';
46
  break;
47
+ default:
48
+ echo 'Error: unknown operation mode! Try saving, it should fix it';
49
+ break;
50
  }
51
  ?>
52
  </tbody>
53
  </table>
54
+ <div id="webpexpress_test_redirection_popup" style="display:none;">
55
+ <div id="webpexpress_test_redirection_content"></div>
56
+ </div>
57
  </fieldset>
lib/options/page-messages.php CHANGED
@@ -98,7 +98,7 @@ if (($config['operation-mode'] == 'cdn-friendly') && !$config['alter-html']['ena
98
  if ($webpEnabled) {
99
  Messenger::printMessage(
100
  'info',
101
- 'You should consider enabling Alter HTML. This is not neccessary, as you have <i>Cache Enabler</i> enabled, which alters HTML. ' .
102
  'However, it is a good idea because currently <i>Cache Enabler</i> does not replace as many URLs as WebP Express (ie ' .
103
  'background images in inline styles)'
104
  );
@@ -169,7 +169,7 @@ if (Config::isConfigFileThereAndOk() ) { // && PlatformInfo::definitelyGotModEnv
169
  "It needs to be set to <i>All</i>, or at least <i>FileInfo</i> to allow rewrite rules in <i>.htaccess</i> files.)<br>" .
170
  "Disabled <i>.htaccess</i> files is actually a good thing, both performance-wise and security-wise. <br> " .
171
  "But it means you will have to insert the following rules into your apache configuration manually:" .
172
- "<pre>" . htmlentities(print_r(Config::generateHTAccessRulesFromConfigFile(), true)) . "</pre>"
173
  );
174
  }
175
  }*/
98
  if ($webpEnabled) {
99
  Messenger::printMessage(
100
  'info',
101
+ 'You should consider enabling Alter HTML. This is not necessary, as you have <i>Cache Enabler</i> enabled, which alters HTML. ' .
102
  'However, it is a good idea because currently <i>Cache Enabler</i> does not replace as many URLs as WebP Express (ie ' .
103
  'background images in inline styles)'
104
  );
169
  "It needs to be set to <i>All</i>, or at least <i>FileInfo</i> to allow rewrite rules in <i>.htaccess</i> files.)<br>" .
170
  "Disabled <i>.htaccess</i> files is actually a good thing, both performance-wise and security-wise. <br> " .
171
  "But it means you will have to insert the following rules into your apache configuration manually:" .
172
+ "<pre>" . htmlentities(print_r(Config::hmmm(), true)) . "</pre>"
173
  );
174
  }
175
  }*/
lib/options/page.php CHANGED
@@ -121,7 +121,11 @@ function helpIcon($text, $customClass = '') {
121
  if (strlen($text) > 150) {
122
  if (strlen($text) > 300) {
123
  if (strlen($text) > 500) {
124
- $className = 'widest';
 
 
 
 
125
  } else {
126
  $className = 'wider';
127
  }
121
  if (strlen($text) > 150) {
122
  if (strlen($text) > 300) {
123
  if (strlen($text) > 500) {
124
+ if (strlen($text) > 1000) {
125
+ $className = 'widest';
126
+ } else {
127
+ $className = 'even-wider';
128
+ }
129
  } else {
130
  $className = 'wider';
131
  }
lib/options/submit.php CHANGED
@@ -9,13 +9,18 @@ use \WebPExpress\ConvertersHelper;
9
  use \WebPExpress\DismissableMessages;
10
  use \WebPExpress\HTAccess;
11
  use \WebPExpress\Messenger;
 
12
  use \WebPExpress\Paths;
13
 
 
14
 
15
  check_admin_referer('webpexpress-save-settings-nonce');
16
 
17
  DismissableMessages::dismissMessage('0.14.0/say-hello-to-vips');
18
 
 
 
 
19
 
20
  /*
21
  --------------------------------
@@ -116,6 +121,21 @@ function webpexpress_getSanitizedQuality($keyInPOST, $fallback = 75) {
116
  return max(0, min($q, 100));
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  /**
121
  * Get sanitized whitelist
@@ -164,7 +184,7 @@ function webpexpress_getSanitizedWhitelist() {
164
  */
165
  function webpexpress_getSanitizedConverters() {
166
  $convertersPosted = (isset($_POST['converters']) ? $_POST['converters'] : '[]');
167
- $convertersPosted = json_decode(wp_unslash($convertersPosted), true); // holy moly! - https://stackoverflow.com/questions/2496455/why-are-post-variables-getting-escaped-in-php
168
 
169
  $convertersSanitized = [];
170
 
@@ -305,8 +325,10 @@ $sanitized = [
305
  'image-types' => intval(webpexpress_getSanitizedChooseFromSet('image-types', '3', [
306
  '0',
307
  '1',
 
308
  '3'
309
  ])),
 
310
  'destination-folder' => webpexpress_getSanitizedChooseFromSet('destination-folder', 'separate', [
311
  'separate',
312
  'mingled',
@@ -315,6 +337,10 @@ $sanitized = [
315
  'append',
316
  'set',
317
  ]),
 
 
 
 
318
  'cache-control' => webpexpress_getSanitizedChooseFromSet('cache-control', 'no-header', [
319
  'no-header',
320
  'set',
@@ -418,6 +444,9 @@ $sanitized = [
418
 
419
  ];
420
 
 
 
 
421
 
422
  /*
423
  ------------------------------------------------------
@@ -435,7 +464,7 @@ $oldConfig = $config;
435
  $config = array_merge($config, [
436
  'operation-mode' => $sanitized['operation-mode'],
437
 
438
- // redirection rules
439
  'image-types' => $sanitized['image-types'],
440
  'forward-query-string' => true,
441
  ]);
@@ -580,6 +609,7 @@ if ($sanitized['operation-mode'] != 'no-conversion') {
580
  }
581
  }
582
 
 
583
 
584
  switch ($sanitized['operation-mode']) {
585
  case 'varied-image-responses':
@@ -637,6 +667,18 @@ if ($sanitized['force'] || HTAccess::doesRewriteRulesNeedUpdate($config)) {
637
  }
638
 
639
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  // SAVE!
641
  // -----
642
  $result = Config::saveConfigurationAndHTAccess($config, $sanitized['force']);
@@ -660,18 +702,26 @@ if (!$result['saved-both-config']) {
660
 
661
  }
662
  } else {
663
- if (($config['destination-folder'] != $oldConfig['destination-folder']) || ($config['destination-extension'] != $oldConfig['destination-extension'])) {
664
- $whatShouldIt = '';
665
- if ($config['destination-folder'] == $oldConfig['destination-folder']) {
666
- $whatShouldIt = 'renamed';
667
- $whatShouldIt2 = 'rename';
 
 
 
 
 
 
 
 
668
  } else {
669
- if ($config['destination-extension'] == $oldConfig['destination-extension']) {
670
- $whatShouldIt = 'relocated';
671
- $whatShouldIt2 = 'relocate';
672
  } else {
673
- $whatShouldIt = 'relocated and renamed';
674
- $whatShouldIt2 = 'relocate and rename';
675
  }
676
  }
677
 
@@ -680,32 +730,31 @@ if (!$result['saved-both-config']) {
680
  if ($numFilesMoved == 0) {
681
  Messenger::addMessage(
682
  'notice',
683
- 'No cached webp files needed to be ' . $whatShouldIt
684
  );
685
 
686
  } else {
687
  Messenger::addMessage(
688
  'success',
689
- 'The webp files was ' . $whatShouldIt . ' (' . $whatShouldIt . ' ' . $numFilesMoved . ' images)'
690
  );
691
  }
692
  } else {
693
  if ($numFilesMoved == 0) {
694
  Messenger::addMessage(
695
  'warning',
696
- 'No webp files could not be ' . $whatShouldIt . ' (failed to ' . $whatShouldIt2 . ' ' . $numFilesFailedMoving . ' images)'
697
  );
698
  } else {
699
  Messenger::addMessage(
700
  'warning',
701
- 'Some webp files could not be ' . $whatShouldIt . ' (failed to ' . $whatShouldIt2 . ' ' . $numFilesFailedMoving . ' images, but successfully ' . $whatShouldIt . ' ' . $numFilesMoved . ' images)'
702
  );
703
 
704
  }
705
  }
706
  }
707
 
708
-
709
  if (!$result['rules-needed-update']) {
710
  Messenger::addMessage(
711
  'success',
@@ -713,6 +762,49 @@ if (!$result['saved-both-config']) {
713
  );
714
  } else {
715
  $rulesResult = $result['htaccess-result'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  /*
717
  'mainResult' // 'index', 'wp-content' or 'failed'
718
  'minRequired' // 'index' or 'wp-content'
@@ -725,6 +817,7 @@ if (!$result['saved-both-config']) {
725
  'uploadFailed' // true if failed to write to plugin folder (it only tries that, if pluginToo == 'yes')
726
  'uploadFailedBadly' // true if plugin failed AND it seems we have rewrite rules there
727
  */
 
728
  $mainResult = $rulesResult['mainResult'];
729
  $rules = $rulesResult['rules'];
730
 
@@ -813,7 +906,7 @@ if (!$result['saved-both-config']) {
813
  'Please grant write permmissions to you uploads folder. Otherwise uploaded mages will not be converted to webp'
814
  );
815
  }
816
- }
817
  }
818
  }
819
 
9
  use \WebPExpress\DismissableMessages;
10
  use \WebPExpress\HTAccess;
11
  use \WebPExpress\Messenger;
12
+ use \WebPExpress\PathHelper;
13
  use \WebPExpress\Paths;
14
 
15
+ // TODO: Move this code to a class
16
 
17
  check_admin_referer('webpexpress-save-settings-nonce');
18
 
19
  DismissableMessages::dismissMessage('0.14.0/say-hello-to-vips');
20
 
21
+ DismissableMessages::dismissMessage('0.15.0/new-scope-setting-no-uploads');
22
+ DismissableMessages::dismissMessage('0.15.0/new-scope-setting-index');
23
+ DismissableMessages::dismissMessage('0.15.0/new-scope-setting-content');
24
 
25
  /*
26
  --------------------------------
121
  return max(0, min($q, 100));
122
  }
123
 
124
+ function webpexpress_getSanitizedScope() {
125
+ $scopeText = webpexpress_getSanitizedText('scope');
126
+ if ($scopeText == '') {
127
+ $scopeText = 'uploads';
128
+ }
129
+ $scope = explode(',', $scopeText);
130
+ $allowed = Paths::getImageRootIds();
131
+ $result = [];
132
+ foreach ($scope as $imageRootId) {
133
+ if (in_array($imageRootId, $allowed)) {
134
+ $result[] = $imageRootId;
135
+ }
136
+ }
137
+ return $result;
138
+ }
139
 
140
  /**
141
  * Get sanitized whitelist
184
  */
185
  function webpexpress_getSanitizedConverters() {
186
  $convertersPosted = (isset($_POST['converters']) ? $_POST['converters'] : '[]');
187
+ $convertersPosted = json_decode(wp_unslash($convertersPosted), true); // holy moly! Wordpress automatically adds slashes to the global POST vars- https://stackoverflow.com/questions/2496455/why-are-post-variables-getting-escaped-in-php
188
 
189
  $convertersSanitized = [];
190
 
325
  'image-types' => intval(webpexpress_getSanitizedChooseFromSet('image-types', '3', [
326
  '0',
327
  '1',
328
+ '2',
329
  '3'
330
  ])),
331
+ 'scope' => webpexpress_getSanitizedScope(),
332
  'destination-folder' => webpexpress_getSanitizedChooseFromSet('destination-folder', 'separate', [
333
  'separate',
334
  'mingled',
337
  'append',
338
  'set',
339
  ]),
340
+ 'destination-structure' => webpexpress_getSanitizedChooseFromSet('destination-structure', 'doc-root', [
341
+ 'doc-root',
342
+ 'image-roots',
343
+ ]),
344
  'cache-control' => webpexpress_getSanitizedChooseFromSet('cache-control', 'no-header', [
345
  'no-header',
346
  'set',
444
 
445
  ];
446
 
447
+ if (!Paths::canUseDocRootForRelPaths()) {
448
+ $sanitized['destination-structure'] = 'image-roots';
449
+ }
450
 
451
  /*
452
  ------------------------------------------------------
464
  $config = array_merge($config, [
465
  'operation-mode' => $sanitized['operation-mode'],
466
 
467
+ 'scope' => $sanitized['scope'],
468
  'image-types' => $sanitized['image-types'],
469
  'forward-query-string' => true,
470
  ]);
609
  }
610
  }
611
 
612
+ $config['destination-structure'] = $sanitized['destination-structure'];
613
 
614
  switch ($sanitized['operation-mode']) {
615
  case 'varied-image-responses':
667
  }
668
 
669
 
670
+ $config['environment-when-config-was-saved'] = [
671
+ 'doc-root-available' => PathHelper::isDocRootAvailable(),
672
+ 'doc-root-resolvable' => PathHelper::isDocRootAvailableAndResolvable(),
673
+ 'doc-root-usable-for-structuring' => Paths::canUseDocRootForRelPaths(),
674
+ 'image-roots' => Paths::getImageRootsDef(),
675
+ 'document-root' => null,
676
+ ];
677
+
678
+ if (PathHelper::isDocRootAvailable()) {
679
+ $config['document-root'] = $_SERVER['DOCUMENT_ROOT'];
680
+ }
681
+
682
  // SAVE!
683
  // -----
684
  $result = Config::saveConfigurationAndHTAccess($config, $sanitized['force']);
702
 
703
  }
704
  } else {
705
+ $changeFolder = ($config['destination-folder'] != $oldConfig['destination-folder']);
706
+ $changeExtension = ($config['destination-extension'] != $oldConfig['destination-extension']);
707
+ $changeStructure = ($config['destination-structure'] != $oldConfig['destination-structure']);
708
+
709
+ if ($changeFolder || $changeExtension || $changeStructure) {
710
+
711
+ $relocate = $changeFolder || $changeStructure;
712
+ $rename = $changeExtension;
713
+
714
+ $actionPastTense = '';
715
+ if ($rename && $relocate) {
716
+ $actionPastTense = 'relocated and renamed';
717
+ $actionPresentTense = 'relocate and rename';
718
  } else {
719
+ if ($rename) {
720
+ $actionPastTense = 'renamed';
721
+ $actionPresentTense = 'rename';
722
  } else {
723
+ $actionPastTense = 'relocated';
724
+ $actionPresentTense = 'relocate';
725
  }
726
  }
727
 
730
  if ($numFilesMoved == 0) {
731
  Messenger::addMessage(
732
  'notice',
733
+ 'No cached webp files needed to be ' . $actionPastTense
734
  );
735
 
736
  } else {
737
  Messenger::addMessage(
738
  'success',
739
+ 'The webp files was ' . $actionPastTense . ' (' . $actionPastTense . ' ' . $numFilesMoved . ' images)'
740
  );
741
  }
742
  } else {
743
  if ($numFilesMoved == 0) {
744
  Messenger::addMessage(
745
  'warning',
746
+ 'No webp files could not be ' . $actionPastTense . ' (failed to ' . $actionPresentTense . ' ' . $numFilesFailedMoving . ' images)'
747
  );
748
  } else {
749
  Messenger::addMessage(
750
  'warning',
751
+ 'Some webp files could not be ' . $actionPastTense . ' (failed to ' . $actionPresentTense . ' ' . $numFilesFailedMoving . ' images, but successfully ' . $actionPastTense . ' ' . $numFilesMoved . ' images)'
752
  );
753
 
754
  }
755
  }
756
  }
757
 
 
758
  if (!$result['rules-needed-update']) {
759
  Messenger::addMessage(
760
  'success',
762
  );
763
  } else {
764
  $rulesResult = $result['htaccess-result'];
765
+ list($success, $successfullWrites, $successfulDeactivations, $failedWrites, $failedDeactivations) = $rulesResult;
766
+
767
+ $msg = '<p>Configuration was saved</p>';
768
+
769
+ if (count($successfullWrites) > 0) {
770
+ $msg .= '<p>Rewrite rules were saved to the following files:</p>';
771
+ foreach ($successfullWrites as $rootId) {
772
+ $msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootId . ')<br>';
773
+ }
774
+ }
775
+
776
+ if (count($successfulDeactivations) > 0) {
777
+ $msg .= '<p>Rewrite rules were removed from the following files:</p>';
778
+ foreach ($successfulDeactivations as $rootId) {
779
+ $msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootId . ')<br>';
780
+ }
781
+ }
782
+
783
+ Messenger::addMessage(
784
+ ($success ? 'success' : 'info'),
785
+ $msg
786
+ );
787
+
788
+
789
+ if (count($failedWrites) > 0) {
790
+ $msg = '<p>Failed writing rewrite rules to the following files:</p>';
791
+ foreach ($failedWrites as $rootId) {
792
+ $msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootId . ')<br>';
793
+ }
794
+ $msg .= 'You need to change the file permissions to allow WebP Express to save the rules.';
795
+ Messenger::addMessage('error', $msg);
796
+ } else {
797
+ if (count($failedDeactivations) > 0) {
798
+ $msg = '<p>Failed deleting unused rewrite rules in the following files:</p>';
799
+ foreach ($failedDeactivations as $rootId) {
800
+ $msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootId . ')<br>';
801
+ }
802
+ $msg .= 'You need to change the file permissions to allow WebP Express to remove the rules or ' .
803
+ 'remove them manually';
804
+ Messenger::addMessage('error', $msg);
805
+ }
806
+ }
807
+
808
  /*
809
  'mainResult' // 'index', 'wp-content' or 'failed'
810
  'minRequired' // 'index' or 'wp-content'
817
  'uploadFailed' // true if failed to write to plugin folder (it only tries that, if pluginToo == 'yes')
818
  'uploadFailedBadly' // true if plugin failed AND it seems we have rewrite rules there
819
  */
820
+ /*
821
  $mainResult = $rulesResult['mainResult'];
822
  $rules = $rulesResult['rules'];
823
 
906
  'Please grant write permmissions to you uploads folder. Otherwise uploaded mages will not be converted to webp'
907
  );
908
  }
909
+ }*/
910
  }
911
  }
912
 
vendor/composer/autoload_classmap.php CHANGED
@@ -6,87 +6,4 @@ $vendorDir = dirname(dirname(__FILE__));
6
  $baseDir = dirname($vendorDir);
7
 
8
  return array(
9
- 'DOMUtilForWebP\\ImageUrlReplacer' => $vendorDir . '/rosell-dk/dom-util-for-webp/src/ImageUrlReplacer.php',
10
- 'DOMUtilForWebP\\PictureTags' => $vendorDir . '/rosell-dk/dom-util-for-webp/src/PictureTags.php',
11
- 'ImageMimeTypeGuesser\\Detectors\\AbstractDetector' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/AbstractDetector.php',
12
- 'ImageMimeTypeGuesser\\Detectors\\ExifImageType' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/ExifImageType.php',
13
- 'ImageMimeTypeGuesser\\Detectors\\FInfo' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/FInfo.php',
14
- 'ImageMimeTypeGuesser\\Detectors\\GetImageSize' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/GetImageSize.php',
15
- 'ImageMimeTypeGuesser\\Detectors\\MimeContentType' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/MimeContentType.php',
16
- 'ImageMimeTypeGuesser\\Detectors\\SniffFirstFourBytes' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/SniffFirstFourBytes.php',
17
- 'ImageMimeTypeGuesser\\Detectors\\Stack' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/Detectors/Stack.php',
18
- 'ImageMimeTypeGuesser\\GuessFromExtension' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/GuessFromExtension.php',
19
- 'ImageMimeTypeGuesser\\ImageMimeTypeGuesser' => $vendorDir . '/rosell-dk/image-mime-type-guesser/src/ImageMimeTypeGuesser.php',
20
- 'WebPConvertCloudService\\AccessCheck' => $vendorDir . '/rosell-dk/webp-convert-cloud-service/src/AccessCheck.php',
21
- 'WebPConvertCloudService\\Serve' => $vendorDir . '/rosell-dk/webp-convert-cloud-service/src/Serve.php',
22
- 'WebPConvertCloudService\\WebPConvertCloudService' => $vendorDir . '/rosell-dk/webp-convert-cloud-service/src/WebPConvertCloudService.php',
23
- 'WebPConvert\\Convert\\ConverterFactory' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/ConverterFactory.php',
24
- 'WebPConvert\\Convert\\Converters\\AbstractConverter' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/AbstractConverter.php',
25
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\AutoQualityTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/AutoQualityTrait.php',
26
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\DestinationPreparationTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/DestinationPreparationTrait.php',
27
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\LoggerTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/LoggerTrait.php',
28
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\OptionsTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/OptionsTrait.php',
29
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\WarningLoggerTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/WarningLoggerTrait.php',
30
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\CloudConverterTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/CloudConverterTrait.php',
31
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\CurlTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/CurlTrait.php',
32
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\EncodingAutoTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/EncodingAutoTrait.php',
33
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\ExecTrait' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/ExecTrait.php',
34
- 'WebPConvert\\Convert\\Converters\\Cwebp' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Cwebp.php',
35
- 'WebPConvert\\Convert\\Converters\\Ewww' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Ewww.php',
36
- 'WebPConvert\\Convert\\Converters\\Gd' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Gd.php',
37
- 'WebPConvert\\Convert\\Converters\\Gmagick' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Gmagick.php',
38
- 'WebPConvert\\Convert\\Converters\\GmagickBinary' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/GmagickBinary.php',
39
- 'WebPConvert\\Convert\\Converters\\GraphicsMagick' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/GraphicsMagick.php',
40
- 'WebPConvert\\Convert\\Converters\\ImageMagick' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/ImageMagick.php',
41
- 'WebPConvert\\Convert\\Converters\\Imagick' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Imagick.php',
42
- 'WebPConvert\\Convert\\Converters\\ImagickBinary' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/ImagickBinary.php',
43
- 'WebPConvert\\Convert\\Converters\\Stack' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Stack.php',
44
- 'WebPConvert\\Convert\\Converters\\Vips' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Vips.php',
45
- 'WebPConvert\\Convert\\Converters\\Wpc' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Converters/Wpc.php',
46
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailedException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailedException.php',
47
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConversionSkippedException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConversionSkippedException.php',
48
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConverterNotOperationalException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConverterNotOperationalException.php',
49
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConverterNotOperational\\InvalidApiKeyException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConverterNotOperational/InvalidApiKeyException.php',
50
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConverterNotOperational\\SystemRequirementsNotMetException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConverterNotOperational/SystemRequirementsNotMetException.php',
51
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\FileSystemProblemsException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/FileSystemProblemsException.php',
52
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\FileSystemProblems\\CreateDestinationFileException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/FileSystemProblems/CreateDestinationFileException.php',
53
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\FileSystemProblems\\CreateDestinationFolderException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/FileSystemProblems/CreateDestinationFolderException.php',
54
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInputException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInputException.php',
55
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInput\\ConverterNotFoundException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInput/ConverterNotFoundException.php',
56
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInput\\InvalidImageTypeException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInput/InvalidImageTypeException.php',
57
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInput\\TargetNotFoundException' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInput/TargetNotFoundException.php',
58
- 'WebPConvert\\Convert\\Helpers\\JpegQualityDetector' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Helpers/JpegQualityDetector.php',
59
- 'WebPConvert\\Convert\\Helpers\\PhpIniSizes' => $vendorDir . '/rosell-dk/webp-convert/src/Convert/Helpers/PhpIniSizes.php',
60
- 'WebPConvert\\Exceptions\\InvalidInputException' => $vendorDir . '/rosell-dk/webp-convert/src/Exceptions/InvalidInputException.php',
61
- 'WebPConvert\\Exceptions\\InvalidInput\\InvalidImageTypeException' => $vendorDir . '/rosell-dk/webp-convert/src/Exceptions/InvalidInput/InvalidImageTypeException.php',
62
- 'WebPConvert\\Exceptions\\InvalidInput\\TargetNotFoundException' => $vendorDir . '/rosell-dk/webp-convert/src/Exceptions/InvalidInput/TargetNotFoundException.php',
63
- 'WebPConvert\\Exceptions\\WebPConvertException' => $vendorDir . '/rosell-dk/webp-convert/src/Exceptions/WebPConvertException.php',
64
- 'WebPConvert\\Helpers\\InputValidator' => $vendorDir . '/rosell-dk/webp-convert/src/Helpers/InputValidator.php',
65
- 'WebPConvert\\Helpers\\MimeType' => $vendorDir . '/rosell-dk/webp-convert/src/Helpers/MimeType.php',
66
- 'WebPConvert\\Helpers\\PathChecker' => $vendorDir . '/rosell-dk/webp-convert/src/Helpers/PathChecker.php',
67
- 'WebPConvert\\Loggers\\BaseLogger' => $vendorDir . '/rosell-dk/webp-convert/src/Loggers/BaseLogger.php',
68
- 'WebPConvert\\Loggers\\BufferLogger' => $vendorDir . '/rosell-dk/webp-convert/src/Loggers/BufferLogger.php',
69
- 'WebPConvert\\Loggers\\EchoLogger' => $vendorDir . '/rosell-dk/webp-convert/src/Loggers/EchoLogger.php',
70
- 'WebPConvert\\Options\\ArrayOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/ArrayOption.php',
71
- 'WebPConvert\\Options\\BooleanOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/BooleanOption.php',
72
- 'WebPConvert\\Options\\Exceptions\\InvalidOptionTypeException' => $vendorDir . '/rosell-dk/webp-convert/src/Options/Exceptions/InvalidOptionTypeException.php',
73
- 'WebPConvert\\Options\\Exceptions\\InvalidOptionValueException' => $vendorDir . '/rosell-dk/webp-convert/src/Options/Exceptions/InvalidOptionValueException.php',
74
- 'WebPConvert\\Options\\Exceptions\\OptionNotFoundException' => $vendorDir . '/rosell-dk/webp-convert/src/Options/Exceptions/OptionNotFoundException.php',
75
- 'WebPConvert\\Options\\GhostOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/GhostOption.php',
76
- 'WebPConvert\\Options\\IntegerOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/IntegerOption.php',
77
- 'WebPConvert\\Options\\IntegerOrNullOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/IntegerOrNullOption.php',
78
- 'WebPConvert\\Options\\MetadataOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/MetadataOption.php',
79
- 'WebPConvert\\Options\\Option' => $vendorDir . '/rosell-dk/webp-convert/src/Options/Option.php',
80
- 'WebPConvert\\Options\\Options' => $vendorDir . '/rosell-dk/webp-convert/src/Options/Options.php',
81
- 'WebPConvert\\Options\\QualityOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/QualityOption.php',
82
- 'WebPConvert\\Options\\SensitiveArrayOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/SensitiveArrayOption.php',
83
- 'WebPConvert\\Options\\SensitiveStringOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/SensitiveStringOption.php',
84
- 'WebPConvert\\Options\\StringOption' => $vendorDir . '/rosell-dk/webp-convert/src/Options/StringOption.php',
85
- 'WebPConvert\\Serve\\Exceptions\\ServeFailedException' => $vendorDir . '/rosell-dk/webp-convert/src/Serve/Exceptions/ServeFailedException.php',
86
- 'WebPConvert\\Serve\\Header' => $vendorDir . '/rosell-dk/webp-convert/src/Serve/Header.php',
87
- 'WebPConvert\\Serve\\Report' => $vendorDir . '/rosell-dk/webp-convert/src/Serve/Report.php',
88
- 'WebPConvert\\Serve\\ServeConvertedWebP' => $vendorDir . '/rosell-dk/webp-convert/src/Serve/ServeConvertedWebP.php',
89
- 'WebPConvert\\Serve\\ServeConvertedWebPWithErrorHandling' => $vendorDir . '/rosell-dk/webp-convert/src/Serve/ServeConvertedWebPWithErrorHandling.php',
90
- 'WebPConvert\\Serve\\ServeFile' => $vendorDir . '/rosell-dk/webp-convert/src/Serve/ServeFile.php',
91
- 'WebPConvert\\WebPConvert' => $vendorDir . '/rosell-dk/webp-convert/src/WebPConvert.php',
92
  );
6
  $baseDir = dirname($vendorDir);
7
 
8
  return array(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  );
vendor/composer/autoload_static.php CHANGED
@@ -41,98 +41,11 @@ class ComposerStaticInit16597e36dd1bfcd787ed5a8e6d908243
41
  ),
42
  );
43
 
44
- public static $classMap = array (
45
- 'DOMUtilForWebP\\ImageUrlReplacer' => __DIR__ . '/..' . '/rosell-dk/dom-util-for-webp/src/ImageUrlReplacer.php',
46
- 'DOMUtilForWebP\\PictureTags' => __DIR__ . '/..' . '/rosell-dk/dom-util-for-webp/src/PictureTags.php',
47
- 'ImageMimeTypeGuesser\\Detectors\\AbstractDetector' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/AbstractDetector.php',
48
- 'ImageMimeTypeGuesser\\Detectors\\ExifImageType' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/ExifImageType.php',
49
- 'ImageMimeTypeGuesser\\Detectors\\FInfo' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/FInfo.php',
50
- 'ImageMimeTypeGuesser\\Detectors\\GetImageSize' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/GetImageSize.php',
51
- 'ImageMimeTypeGuesser\\Detectors\\MimeContentType' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/MimeContentType.php',
52
- 'ImageMimeTypeGuesser\\Detectors\\SniffFirstFourBytes' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/SniffFirstFourBytes.php',
53
- 'ImageMimeTypeGuesser\\Detectors\\Stack' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/Detectors/Stack.php',
54
- 'ImageMimeTypeGuesser\\GuessFromExtension' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/GuessFromExtension.php',
55
- 'ImageMimeTypeGuesser\\ImageMimeTypeGuesser' => __DIR__ . '/..' . '/rosell-dk/image-mime-type-guesser/src/ImageMimeTypeGuesser.php',
56
- 'WebPConvertCloudService\\AccessCheck' => __DIR__ . '/..' . '/rosell-dk/webp-convert-cloud-service/src/AccessCheck.php',
57
- 'WebPConvertCloudService\\Serve' => __DIR__ . '/..' . '/rosell-dk/webp-convert-cloud-service/src/Serve.php',
58
- 'WebPConvertCloudService\\WebPConvertCloudService' => __DIR__ . '/..' . '/rosell-dk/webp-convert-cloud-service/src/WebPConvertCloudService.php',
59
- 'WebPConvert\\Convert\\ConverterFactory' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/ConverterFactory.php',
60
- 'WebPConvert\\Convert\\Converters\\AbstractConverter' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/AbstractConverter.php',
61
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\AutoQualityTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/AutoQualityTrait.php',
62
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\DestinationPreparationTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/DestinationPreparationTrait.php',
63
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\LoggerTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/LoggerTrait.php',
64
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\OptionsTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/OptionsTrait.php',
65
- 'WebPConvert\\Convert\\Converters\\BaseTraits\\WarningLoggerTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/BaseTraits/WarningLoggerTrait.php',
66
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\CloudConverterTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/CloudConverterTrait.php',
67
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\CurlTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/CurlTrait.php',
68
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\EncodingAutoTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/EncodingAutoTrait.php',
69
- 'WebPConvert\\Convert\\Converters\\ConverterTraits\\ExecTrait' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/ConverterTraits/ExecTrait.php',
70
- 'WebPConvert\\Convert\\Converters\\Cwebp' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Cwebp.php',
71
- 'WebPConvert\\Convert\\Converters\\Ewww' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Ewww.php',
72
- 'WebPConvert\\Convert\\Converters\\Gd' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Gd.php',
73
- 'WebPConvert\\Convert\\Converters\\Gmagick' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Gmagick.php',
74
- 'WebPConvert\\Convert\\Converters\\GmagickBinary' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/GmagickBinary.php',
75
- 'WebPConvert\\Convert\\Converters\\GraphicsMagick' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/GraphicsMagick.php',
76
- 'WebPConvert\\Convert\\Converters\\ImageMagick' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/ImageMagick.php',
77
- 'WebPConvert\\Convert\\Converters\\Imagick' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Imagick.php',
78
- 'WebPConvert\\Convert\\Converters\\ImagickBinary' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/ImagickBinary.php',
79
- 'WebPConvert\\Convert\\Converters\\Stack' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Stack.php',
80
- 'WebPConvert\\Convert\\Converters\\Vips' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Vips.php',
81
- 'WebPConvert\\Convert\\Converters\\Wpc' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Converters/Wpc.php',
82
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailedException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailedException.php',
83
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConversionSkippedException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConversionSkippedException.php',
84
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConverterNotOperationalException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConverterNotOperationalException.php',
85
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConverterNotOperational\\InvalidApiKeyException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConverterNotOperational/InvalidApiKeyException.php',
86
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\ConverterNotOperational\\SystemRequirementsNotMetException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/ConverterNotOperational/SystemRequirementsNotMetException.php',
87
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\FileSystemProblemsException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/FileSystemProblemsException.php',
88
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\FileSystemProblems\\CreateDestinationFileException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/FileSystemProblems/CreateDestinationFileException.php',
89
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\FileSystemProblems\\CreateDestinationFolderException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/FileSystemProblems/CreateDestinationFolderException.php',
90
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInputException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInputException.php',
91
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInput\\ConverterNotFoundException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInput/ConverterNotFoundException.php',
92
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInput\\InvalidImageTypeException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInput/InvalidImageTypeException.php',
93
- 'WebPConvert\\Convert\\Exceptions\\ConversionFailed\\InvalidInput\\TargetNotFoundException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Exceptions/ConversionFailed/InvalidInput/TargetNotFoundException.php',
94
- 'WebPConvert\\Convert\\Helpers\\JpegQualityDetector' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Helpers/JpegQualityDetector.php',
95
- 'WebPConvert\\Convert\\Helpers\\PhpIniSizes' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Convert/Helpers/PhpIniSizes.php',
96
- 'WebPConvert\\Exceptions\\InvalidInputException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Exceptions/InvalidInputException.php',
97
- 'WebPConvert\\Exceptions\\InvalidInput\\InvalidImageTypeException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Exceptions/InvalidInput/InvalidImageTypeException.php',
98
- 'WebPConvert\\Exceptions\\InvalidInput\\TargetNotFoundException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Exceptions/InvalidInput/TargetNotFoundException.php',
99
- 'WebPConvert\\Exceptions\\WebPConvertException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Exceptions/WebPConvertException.php',
100
- 'WebPConvert\\Helpers\\InputValidator' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Helpers/InputValidator.php',
101
- 'WebPConvert\\Helpers\\MimeType' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Helpers/MimeType.php',
102
- 'WebPConvert\\Helpers\\PathChecker' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Helpers/PathChecker.php',
103
- 'WebPConvert\\Loggers\\BaseLogger' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Loggers/BaseLogger.php',
104
- 'WebPConvert\\Loggers\\BufferLogger' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Loggers/BufferLogger.php',
105
- 'WebPConvert\\Loggers\\EchoLogger' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Loggers/EchoLogger.php',
106
- 'WebPConvert\\Options\\ArrayOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/ArrayOption.php',
107
- 'WebPConvert\\Options\\BooleanOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/BooleanOption.php',
108
- 'WebPConvert\\Options\\Exceptions\\InvalidOptionTypeException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/Exceptions/InvalidOptionTypeException.php',
109
- 'WebPConvert\\Options\\Exceptions\\InvalidOptionValueException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/Exceptions/InvalidOptionValueException.php',
110
- 'WebPConvert\\Options\\Exceptions\\OptionNotFoundException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/Exceptions/OptionNotFoundException.php',
111
- 'WebPConvert\\Options\\GhostOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/GhostOption.php',
112
- 'WebPConvert\\Options\\IntegerOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/IntegerOption.php',
113
- 'WebPConvert\\Options\\IntegerOrNullOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/IntegerOrNullOption.php',
114
- 'WebPConvert\\Options\\MetadataOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/MetadataOption.php',
115
- 'WebPConvert\\Options\\Option' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/Option.php',
116
- 'WebPConvert\\Options\\Options' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/Options.php',
117
- 'WebPConvert\\Options\\QualityOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/QualityOption.php',
118
- 'WebPConvert\\Options\\SensitiveArrayOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/SensitiveArrayOption.php',
119
- 'WebPConvert\\Options\\SensitiveStringOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/SensitiveStringOption.php',
120
- 'WebPConvert\\Options\\StringOption' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Options/StringOption.php',
121
- 'WebPConvert\\Serve\\Exceptions\\ServeFailedException' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Serve/Exceptions/ServeFailedException.php',
122
- 'WebPConvert\\Serve\\Header' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Serve/Header.php',
123
- 'WebPConvert\\Serve\\Report' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Serve/Report.php',
124
- 'WebPConvert\\Serve\\ServeConvertedWebP' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Serve/ServeConvertedWebP.php',
125
- 'WebPConvert\\Serve\\ServeConvertedWebPWithErrorHandling' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Serve/ServeConvertedWebPWithErrorHandling.php',
126
- 'WebPConvert\\Serve\\ServeFile' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/Serve/ServeFile.php',
127
- 'WebPConvert\\WebPConvert' => __DIR__ . '/..' . '/rosell-dk/webp-convert/src/WebPConvert.php',
128
- );
129
-
130
  public static function getInitializer(ClassLoader $loader)
131
  {
132
  return \Closure::bind(function () use ($loader) {
133
  $loader->prefixLengthsPsr4 = ComposerStaticInit16597e36dd1bfcd787ed5a8e6d908243::$prefixLengthsPsr4;
134
  $loader->prefixDirsPsr4 = ComposerStaticInit16597e36dd1bfcd787ed5a8e6d908243::$prefixDirsPsr4;
135
- $loader->classMap = ComposerStaticInit16597e36dd1bfcd787ed5a8e6d908243::$classMap;
136
 
137
  }, null, ClassLoader::class);
138
  }
41
  ),
42
  );
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  public static function getInitializer(ClassLoader $loader)
45
  {
46
  return \Closure::bind(function () use ($loader) {
47
  $loader->prefixLengthsPsr4 = ComposerStaticInit16597e36dd1bfcd787ed5a8e6d908243::$prefixLengthsPsr4;
48
  $loader->prefixDirsPsr4 = ComposerStaticInit16597e36dd1bfcd787ed5a8e6d908243::$prefixDirsPsr4;
 
49
 
50
  }, null, ClassLoader::class);
51
  }
vendor/composer/installed.json CHANGED
@@ -115,17 +115,17 @@
115
  },
116
  {
117
  "name": "rosell-dk/webp-convert",
118
- "version": "2.1.5",
119
- "version_normalized": "2.1.5.0",
120
  "source": {
121
  "type": "git",
122
  "url": "https://github.com/rosell-dk/webp-convert.git",
123
- "reference": "b28ce7fe71ce4f930bd352f12b4f1591ac21b9f0"
124
  },
125
  "dist": {
126
  "type": "zip",
127
- "url": "https://api.github.com/repos/rosell-dk/webp-convert/zipball/b28ce7fe71ce4f930bd352f12b4f1591ac21b9f0",
128
- "reference": "b28ce7fe71ce4f930bd352f12b4f1591ac21b9f0",
129
  "shasum": ""
130
  },
131
  "require": {
@@ -143,7 +143,7 @@
143
  "ext-vips": "to use Vips extension for converting.",
144
  "php-stan/php-stan": "Suggested for dev, in order to analyse code before committing"
145
  },
146
- "time": "2019-08-02T14:13:24+00:00",
147
  "type": "library",
148
  "extra": {
149
  "scripts-descriptions": {
115
  },
116
  {
117
  "name": "rosell-dk/webp-convert",
118
+ "version": "2.1.6",
119
+ "version_normalized": "2.1.6.0",
120
  "source": {
121
  "type": "git",
122
  "url": "https://github.com/rosell-dk/webp-convert.git",
123
+ "reference": "5cb744d2786468fb51883d1f3a90562166df3320"
124
  },
125
  "dist": {
126
  "type": "zip",
127
+ "url": "https://api.github.com/repos/rosell-dk/webp-convert/zipball/5cb744d2786468fb51883d1f3a90562166df3320",
128
+ "reference": "5cb744d2786468fb51883d1f3a90562166df3320",
129
  "shasum": ""
130
  },
131
  "require": {
143
  "ext-vips": "to use Vips extension for converting.",
144
  "php-stan/php-stan": "Suggested for dev, in order to analyse code before committing"
145
  },
146
+ "time": "2019-08-10T22:04:08+00:00",
147
  "type": "library",
148
  "extra": {
149
  "scripts-descriptions": {
vendor/rosell-dk/webp-convert/.php_cs.dist DELETED
@@ -1,19 +0,0 @@
1
- <?php
2
-
3
- $finder = PhpCsFixer\Finder::create()
4
- ->exclude('tests')
5
- ->in(__DIR__)
6
- ;
7
-
8
- $config = PhpCsFixer\Config::create();
9
- $config
10
- ->setRules([
11
- '@PSR2' => true,
12
- 'array_syntax' => [
13
- 'syntax' => 'short',
14
- ],
15
- ])
16
- ->setFinder($finder)
17
- ;
18
-
19
- return $config;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vendor/rosell-dk/webp-convert/BACKERS.md CHANGED
@@ -13,6 +13,7 @@ To become a backer yourself, visit [my page at patreon](https://www.patreon.com/
13
  | Name | Date | Message (max 70 chars, plain text only) |
14
  | --------------------- | ---------- | ----------------------------------------------------------------------- |
15
  | Tammy Valgardson | 2018-12-27 | |
 
16
 
17
  <sub>I reserve the right to disallow inappropriate messages. No Trump hooting or bashing here, please. And don't be aggressive, obscene or anything unpleasant. But I don't have to point that out, do I?</sub>
18
 
13
  | Name | Date | Message (max 70 chars, plain text only) |
14
  | --------------------- | ---------- | ----------------------------------------------------------------------- |
15
  | Tammy Valgardson | 2018-12-27 | |
16
+ | Max Kreminsky | 2019-08-02 | |
17
 
18
  <sub>I reserve the right to disallow inappropriate messages. No Trump hooting or bashing here, please. And don't be aggressive, obscene or anything unpleasant. But I don't have to point that out, do I?</sub>
19
 
vendor/rosell-dk/webp-convert/README.md CHANGED
@@ -94,17 +94,22 @@ The old introduction (for 1.3.9) is available here: [docs/v1.3/serving/convert-a
94
  The library can be used to create a *WebP On Demand* solution, which automatically serves WebP images instead of jpeg/pngs for browsers that supports WebP. To set this up, follow what's described [in this tutorial (not updated for 2.0 yet)](https://github.com/rosell-dk/webp-convert/blob/master/docs/v1.3/webp-on-demand/webp-on-demand.md).
95
 
96
 
97
- ## WebP Convert in the wild
98
- *WebP Convert* is used in the following projects:
99
 
100
- #### [webp-express](https://github.com/rosell-dk/webp-express)
101
- Wordpress integration
102
 
103
- #### [webp-convert-cloud-service](https://github.com/rosell-dk/webp-convert-cloud-service)
 
 
 
 
 
 
104
  A cloud service based on WebPConvert
105
 
106
- #### [kirby-webp](https://github.com/S1SYPHOS/kirby-webp)
107
- Kirby CMS integration
108
 
109
  ## Supporting WebP Convert
110
  Bread on the table don't come for free, even though this library does, and always will. I enjoy developing this, and supporting you guys, but I kind of need the bread too. Please make it possible for me to have both:
94
  The library can be used to create a *WebP On Demand* solution, which automatically serves WebP images instead of jpeg/pngs for browsers that supports WebP. To set this up, follow what's described [in this tutorial (not updated for 2.0 yet)](https://github.com/rosell-dk/webp-convert/blob/master/docs/v1.3/webp-on-demand/webp-on-demand.md).
95
 
96
 
97
+ ## Projects using WebP Convert
 
98
 
99
+ ### CMS plugins using WebP Convert
100
+ This library is used as the engine to provide webp conversions to a handful of platforms. Hopefully this list will be growing over time. Currently there are plugins / extensions / modules / whatever the term is for the following CMS'es (ordered by [market share](https://w3techs.com/technologies/overview/content_management/all)):
101
 
102
+ - [Wordpress](https://github.com/rosell-dk/webp-express)
103
+ - [Contao](https://github.com/postyou/contao-webp-bundle)
104
+ - [Kirby](https://github.com/S1SYPHOS/kirby-webp)
105
+
106
+ ### Other projects using WebP Convert
107
+
108
+ - [webp-convert-cloud-service](https://github.com/rosell-dk/webp-convert-cloud-service)
109
  A cloud service based on WebPConvert
110
 
111
+ - [webp-convert-concat](https://github.com/rosell-dk/webp-convert-concat)
112
+ The webp-convert library and its dependents as a single PHP file (or two)
113
 
114
  ## Supporting WebP Convert
115
  Bread on the table don't come for free, even though this library does, and always will. I enjoy developing this, and supporting you guys, but I kind of need the bread too. Please make it possible for me to have both:
vendor/rosell-dk/webp-convert/composer.json CHANGED
@@ -12,24 +12,7 @@
12
  "@composer validate --no-check-all --strict",
13
  "@phpstan-global"
14
  ],
15
- "test": [
16
- "@test-wod-build",
17
- "@test-webp-convert-build",
18
- "@test-src"
19
- ],
20
- "test-src-build": [
21
- "@test-wod-build",
22
- "@test-webp-convert-build"
23
- ],
24
- "test-wod-build": [
25
- "phpunit --no-coverage build-tests-wod",
26
- "phpunit --no-coverage --bootstrap tests/bootstrap-wod-test.php tests"
27
- ],
28
- "test-webp-convert-build": [
29
- "phpunit --no-coverage build-tests-webp-convert",
30
- "phpunit --no-coverage --bootstrap tests/bootstrap-webp-convert-test.php tests"
31
- ],
32
- "test-src": "phpunit --coverage-text",
33
  "phpunit": "phpunit --coverage-text",
34
  "test-no-cov": "phpunit --no-coverage",
35
  "cs-fix-all": [
12
  "@composer validate --no-check-all --strict",
13
  "@phpstan-global"
14
  ],
15
+ "test": "phpunit --coverage-text",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  "phpunit": "phpunit --coverage-text",
17
  "test-no-cov": "phpunit --no-coverage",
18
  "cs-fix-all": [
vendor/rosell-dk/webp-convert/phpdox.xml DELETED
@@ -1,9 +0,0 @@
1
- <?xml version="1.0" encoding="utf-8" ?>
2
- <phpdox xmlns="http://xml.phpdox.net/config">
3
- <project name="WebP Convert" source="${basedir}/src" workdir="${basedir}/build/api/xml">
4
- <collector backend="parser" />
5
- <generator output="${basedir}/build/api">
6
- <build engine="html" output="html"/>
7
- </generator>
8
- </project>
9
- </phpdox>
 
 
 
 
 
 
 
 
 
vendor/rosell-dk/webp-convert/phpstan.neon DELETED
@@ -1,28 +0,0 @@
1
- parameters:
2
- reportUnmatchedIgnoredErrors: false
3
- ignoreErrors:
4
- - '#Instantiated class Imagick not found.#'
5
- - '#Instantiated class Gmagick not found.#'
6
- - '#Caught class ImagickException not found.#'
7
- - '# on an unknown class Imagick.#'
8
- - '# on an unknown class Gmagick.#'
9
- -
10
- message: '#Strict comparison using === between int and false will always evaluate to false.#'
11
- path: %currentWorkingDirectory%/src/Convert/Converters/Gd.php
12
- -
13
- message: '#Strict comparison using === between resource and false will always evaluate to false.#'
14
- path: %currentWorkingDirectory%/src/Convert/Converters/Ewww.php
15
- -
16
- message: '#Strict comparison using === between resource and false will always evaluate to false.#'
17
- path: %currentWorkingDirectory%/src/Convert/Converters/ConverterTraits/CurlTrait.php
18
- -
19
- message: '#Strict comparison using === between resource and false will always evaluate to false.#'
20
- path: %currentWorkingDirectory%/src/Convert/BaseConverters/AbstractCloudCurlConverter.php
21
- - '#Function vips_call invoked with 4 parameters, 2 required.#'
22
- - '#Function vips_.* not found.#'
23
- -
24
- message: '#SystemRequirementsNotMet#'
25
- path: %currentWorkingDirectory%/src/Convert/Converters/ConverterTraits/ExecTrait.php
26
- -
27
- message: '#Call to function is_callable\(\) with array.* always evaluate to false#'
28
- path: %currentWorkingDirectory%/src/Convert/ConverterFactory.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vendor/rosell-dk/webp-convert/phpunit.xml.dist DELETED
@@ -1,41 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
-
3
- <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
- xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
5
- backupGlobals="false"
6
- backupStaticAttributes="false"
7
- colors="true"
8
- convertErrorsToExceptions="true"
9
- convertNoticesToExceptions="true"
10
- convertWarningsToExceptions="false"
11
- processIsolation="false"
12
- stopOnFailure="false"
13
- bootstrap="vendor/autoload.php"
14
-
15
- >
16
- <testsuites>
17
- <testsuite name="WebPConvert Test Suite">
18
- <directory>./tests/</directory>
19
- </testsuite>
20
- </testsuites>
21
-
22
- <filter>
23
- <whitelist>
24
- <directory suffix=".php">src/</directory>
25
- <exclude>
26
- <directory>./vendor</directory>
27
- <directory>./tests</directory>
28
- <directory>./build-tests-wod</directory>
29
- <directory>./build-tests-complete</directory>
30
- </exclude>
31
- </whitelist>
32
- </filter>
33
-
34
- <logging>
35
- <log type="junit" target="build/report.junit.xml"/>
36
- <log type="coverage-clover" target="coverage.clover"/>
37
- <log type="coverage-text" target="build/coverage.txt"/>
38
- <log type="coverage-html" target="build/coverage"/>
39
- </logging>
40
-
41
- </phpunit>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vendor/rosell-dk/webp-convert/src/Convert/Converters/Cwebp.php CHANGED
@@ -269,10 +269,16 @@ class Cwebp extends AbstractConverter
269
 
270
  // command-line-options
271
  if ($options['command-line-options']) {
 
 
272
  array_push(
273
  $cmdOptions,
274
  ...self::escapeShellArgOnCommandLineOptions($options['command-line-options'])
275
  );
 
 
 
 
276
  }
277
 
278
  // Source file
269
 
270
  // command-line-options
271
  if ($options['command-line-options']) {
272
+ /*
273
+ In some years, we can use the splat instead (requires PHP 5.6)
274
  array_push(
275
  $cmdOptions,
276
  ...self::escapeShellArgOnCommandLineOptions($options['command-line-options'])
277
  );
278
+ */
279
+ foreach (self::escapeShellArgOnCommandLineOptions($options['command-line-options']) as $cmdLineOption) {
280
+ array_push($cmdOptions, $cmdLineOption);
281
+ }
282
  }
283
 
284
  // Source file
vendor/rosell-dk/webp-convert/src/Helpers/Sanitize.php ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WebPConvert\Helpers;
4
+
5
+ class Sanitize
6
+ {
7
+
8
+ /**
9
+ * The NUL character is a demon, because it can be used to bypass other tests
10
+ * See https://st-g.de/2011/04/doing-filename-checks-securely-in-PHP.
11
+ *
12
+ * @param string $string string remove NUL characters in
13
+ */
14
+ public static function removeNUL($string)
15
+ {
16
+ return str_replace(chr(0), '', $string);
17
+ }
18
+
19
+ public static function removeStreamWrappers($string)
20
+ {
21
+ return preg_replace('#^\\w+://#', '', $string);
22
+ }
23
+
24
+ public static function path($string)
25
+ {
26
+ $string = self::removeNUL($string);
27
+ $string = self::removeStreamWrappers($string);
28
+ return $string;
29
+ }
30
+ }
vendor/rosell-dk/webp-convert/src/Options/Options.php CHANGED
@@ -34,15 +34,24 @@ class Options
34
  *
35
  * Conveniently add several options in one call.
36
  *
37
- * @param Option[] ...$options Array of options objects to add
38
  * @return void
39
  */
40
- public function addOptions(...$options)
41
  {
 
42
  foreach ($options as $option) {
43
  $this->addOption($option);
44
  }
45
  }
 
 
 
 
 
 
 
 
 
46
 
47
  /**
48
  * Set the value of an option.
34
  *
35
  * Conveniently add several options in one call.
36
  *
 
37
  * @return void
38
  */
39
+ public function addOptions()
40
  {
41
+ $options = func_get_args();
42
  foreach ($options as $option) {
43
  $this->addOption($option);
44
  }
45
  }
46
+ /*
47
+ In some years, we can use the splat instead (requires PHP 5.6):
48
+ @param Option[] ...$options Array of options objects to add
49
+ public function addOptions(...$options)
50
+ {
51
+ foreach ($options as $option) {
52
+ $this->addOption($option);
53
+ }
54
+ }*/
55
 
56
  /**
57
  * Set the value of an option.
webp-express.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: WebP Express
4
  * Plugin URI: https://github.com/rosell-dk/webp-express
5
  * Description: Serve autogenerated WebP images instead of jpeg/png to browsers that supports WebP. Works on anything (media library images, galleries, theme images etc).
6
- * Version: 0.14.22
7
  * Author: Bjørn Rosell
8
  * Author URI: https://www.bitwise-it.dk
9
  * License: GPL2
3
  * Plugin Name: WebP Express
4
  * Plugin URI: https://github.com/rosell-dk/webp-express
5
  * Description: Serve autogenerated WebP images instead of jpeg/png to browsers that supports WebP. Works on anything (media library images, galleries, theme images etc).
6
+ * Version: 0.15.0
7
  * Author: Bjørn Rosell
8
  * Author URI: https://www.bitwise-it.dk
9
  * License: GPL2
wod/webp-on-demand.php CHANGED
@@ -1,223 +1,6 @@
1
  <?php
2
 
3
- namespace WebPExpress;
4
-
5
- error_reporting(E_ALL);
6
- ini_set('display_errors', 1);
7
-
8
- use \WebPConvert\WebPConvert;
9
- use \WebPConvert\Serve\ServeConvertedWebP;
10
- use \WebPExpress\ConvertHelperIndependent;
11
- use \WebPExpress\Sanitize;
12
- use \WebPExpress\SanityCheck;
13
- use \WebPExpress\SanityException;
14
- use \WebPExpress\ValidateException;
15
- use \WebPExpress\Validate;
16
-
17
- class WebPOnDemand
18
- {
19
-
20
- private static $docRoot;
21
-
22
- public static function exitWithError($msg) {
23
- header('X-WebP-Express-Error: ' . $msg, true);
24
- echo $msg;
25
- exit;
26
- }
27
-
28
- /**
29
- * Get environment variable set with mod_rewrite module
30
- * Return false if the environment variable isn't found
31
- */
32
- private static function getEnvPassedInRewriteRule($envName) {
33
- // Envirenment variables passed through the REWRITE module have "REWRITE_" as a prefix (in Apache, not Litespeed, if I recall correctly)
34
- // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
35
- // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
36
- // We simply look for an environment variable that ends with what we are looking for.
37
- // (so make sure to make it unique)
38
- $len = strlen($envName);
39
- foreach ($_SERVER as $key => $item) {
40
- if (substr($key, -$len) == $envName) {
41
- return $item;
42
- }
43
- }
44
- return false;
45
- }
46
-
47
- public static function process() {
48
-
49
- include_once __DIR__ . "/../lib/classes/ConvertHelperIndependent.php";
50
- include_once __DIR__ . '/../lib/classes/Sanitize.php';
51
- include_once __DIR__ . '/../lib/classes/SanityCheck.php';
52
- include_once __DIR__ . '/../lib/classes/SanityException.php';
53
- include_once __DIR__ . '/../lib/classes/Validate.php';
54
- include_once __DIR__ . '/../lib/classes/ValidateException.php';
55
-
56
- // Check input
57
- // --------------
58
- try {
59
-
60
- // Check DOCUMENT_ROOT
61
- // ----------------------
62
- $checking = 'DOCUMENT_ROOT';
63
- $docRoot = SanityCheck::absPath($_SERVER["DOCUMENT_ROOT"]);
64
- $docRoot = rtrim($docRoot, '/');
65
- $docRoot = SanityCheck::absPathExistsAndIsDir($docRoot);
66
-
67
- // Check wp-content
68
- // ----------------------
69
-
70
- // Passed in env variable?
71
- $wpContentDirRel = self::getEnvPassedInRewriteRule('WPCONTENT');
72
- if ($wpContentDirRel === false) {
73
-
74
- // Passed in QS?
75
- if (isset($_GET['wp-content'])) {
76
- $wpContentDirRel = SanityCheck::pathWithoutDirectoryTraversal($_GET['wp-content']);
77
- } else {
78
- // In case above fails, fall back to standard location
79
- $wpContentDirRel = 'wp-content';
80
- }
81
- }
82
-
83
- // Check WebP Express content dir
84
- // ---------------------------------
85
- $checking = 'WebP Express content dir';
86
- $webExpressContentDirAbs = SanityCheck::absPathExistsAndIsDir($docRoot . '/' . $wpContentDirRel . '/webp-express');
87
-
88
-
89
- // Check config file name
90
- // ---------------------------------
91
- $checking = 'config file';
92
- $configFilename = SanityCheck::absPathExistsAndIsFile($webExpressContentDirAbs . '/config/wod-options.json');
93
-
94
-
95
- // Check config file
96
- // --------------------
97
- $configLoadResult = file_get_contents($configFilename);
98
- if ($configLoadResult === false) {
99
- throw new ValidateException('Cannot open config file');
100
- }
101
- $json = SanityCheck::isJSONObject($configLoadResult);
102
-
103
- $options = json_decode($json, true);
104
- $wodOptions = $options['wod'];
105
- $serveOptions = $options['webp-convert'];
106
- $convertOptions = &$serveOptions['convert'];
107
- //echo '<pre>' . print_r($wodOptions, true) . '</pre>'; exit;
108
-
109
-
110
- // Validate that WebPExpress was configured to redirect to this conversion script
111
- // (but do not require that for Nginx)
112
- // ------------------------------------------------------------------------------
113
- $checking = 'settings';
114
- if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
115
- if (!isset($wodOptions['enable-redirection-to-converter']) || ($wodOptions['enable-redirection-to-converter'] === false)) {
116
- throw new ValidateException('Redirection to conversion script is not enabled');
117
- }
118
-
119
- }
120
-
121
-
122
- // Check source (the image to be converted)
123
- // --------------------------------------------
124
- $checking = 'source';
125
-
126
- // Check if it is in an environment variable
127
- $source = self::getEnvPassedInRewriteRule('REQFN');
128
- if ($source !== false) {
129
- $checking = 'source (passed through env)';
130
- $source = SanityCheck::absPathExistsAndIsFile($source);
131
- } else {
132
- // Check if it is in header (but only if .htaccess was configured to send in header)
133
- if (isset($wodOptions['base-htaccess-on-these-capability-tests'])) {
134
- $capTests = $wodOptions['base-htaccess-on-these-capability-tests'];
135
- $passThroughHeaderDefinitelyUnavailable = ($capTests['passThroughHeaderWorking'] === false);
136
- $passThrougEnvVarDefinitelyAvailable =($capTests['passThroughEnvWorking'] === true);
137
- // This determines if .htaccess was configured to send in querystring
138
- $headerMagicAddedInHtaccess = ((!$passThrougEnvVarDefinitelyAvailable) && (!$passThroughHeaderDefinitelyUnavailable));
139
- } else {
140
- $headerMagicAddedInHtaccess = true; // pretend its true
141
- }
142
-
143
- if ($headerMagicAddedInHtaccess && (isset($_SERVER['HTTP_REQFN']))) {
144
- $checking = 'source (passed through request header)';
145
- $source = SanityCheck::absPathExistsAndIsFile($_SERVER['HTTP_REQFN']);
146
- } else {
147
- // Check querystring (relative path)
148
- $srcRel = '';
149
- if (isset($_GET['xsource-rel'])) {
150
- $checking = 'source (passed as relative path, through querystring)';
151
- $xsrcRel = SanityCheck::noControlChars($_GET['xsource-rel']);
152
- $srcRel = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRel, 1));
153
- $source = SanityCheck::absPathExistsAndIsFile($docRoot . '/' . $srcRel);
154
- } else {
155
- // Then querystring (full path)
156
- // - But only on Nginx (our Apache .htaccess rules never passes absolute url)
157
- if (
158
- (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) &&
159
- (isset($_GET['source']) || isset($_GET['xsource']))
160
- ) {
161
- $checking = 'source (passed as absolute path on nginx)';
162
- if (isset($_GET['source'])) {
163
- $source = SanityCheck::absPathExistsAndIsFile($_GET['source']);
164
- } else {
165
- $xsrc = SanityCheck::noControlChars($_GET['xsource']);
166
- $source = SanityCheck::absPathExistsAndIsFile(substr($xsrc, 1));
167
- }
168
- } else {
169
- // Last resort is to use $_SERVER['REQUEST_URI'], well knowing that it does not give the
170
- // correct result in all setups (ie "folder method 1")
171
- $checking = 'source (retrieved by the request_uri server var)';
172
- $srcRel = SanityCheck::pathWithoutDirectoryTraversal(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
173
- $source = SanityCheck::absPathExistsAndIsFile($docRoot . $srcRel);
174
- }
175
- }
176
- }
177
- }
178
-
179
- // Make sure it is in doc root
180
- $source = SanityCheck::absPathIsInDocRoot($source);
181
-
182
- // Check destination path
183
- // --------------------------------------------
184
- $checking = 'destination path';
185
- $destination = ConvertHelperIndependent::getDestination(
186
- $source,
187
- $wodOptions['destination-folder'],
188
- $wodOptions['destination-extension'],
189
- $webExpressContentDirAbs,
190
- $docRoot . '/' . $wodOptions['paths']['uploadDirRel']
191
- );
192
- //echo 'dest:' . $destination; exit;
193
- $destination = SanityCheck::absPathIsInDocRoot($destination);
194
- $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Does not end with .webp');
195
-
196
- } catch (SanityException $e) {
197
- self::exitWithError('Sanity check failed for ' . $checking . ': '. $e->getMessage());
198
- } catch (ValidateException $e) {
199
- self::exitWithError('Validation failed for ' . $checking . ': '. $e->getMessage());
200
- }
201
-
202
- // Done with sanitizing, lets get to work!
203
- // ---------------------------------------
204
- if (isset($wodOptions['success-response']) && ($wodOptions['success-response'] == 'original')) {
205
- $serveOptions['serve-original'] = true;
206
- $serveOptions['serve-image']['headers']['vary-accept'] = false;
207
- } else {
208
- $serveOptions['serve-image']['headers']['vary-accept'] = true;
209
- }
210
- //echo $source . '<br>' . $destination; exit;
211
-
212
- ConvertHelperIndependent::serveConverted(
213
- $source,
214
- $destination,
215
- $serveOptions,
216
- $webExpressContentDirAbs . '/log',
217
- 'Conversion triggered with the conversion script (wod/webp-on-demand.php)'
218
- );
219
- }
220
- }
221
 
222
  // Protect against directly accessing webp-on-demand.php
223
  // Only protect on Apache. We know for sure that the method is not reliable on nginx. We have not tested on litespeed yet, so we dare not.
@@ -230,4 +13,13 @@ if (stripos($_SERVER["SERVER_SOFTWARE"], 'apache') !== false && stripos($_SERVER
230
  }
231
  }
232
 
233
- WebPOnDemand::process();
 
 
 
 
 
 
 
 
 
1
  <?php
2
 
3
+ use \WebPExpress\WebPOnDemand;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  // Protect against directly accessing webp-on-demand.php
6
  // Only protect on Apache. We know for sure that the method is not reliable on nginx. We have not tested on litespeed yet, so we dare not.
13
  }
14
  }
15
 
16
+ define('WOD_DIR', __DIR__);
17
+
18
+ function webpexpress_autoloader($class) {
19
+ if (strpos($class, 'WebPExpress\\') === 0) {
20
+ require_once WOD_DIR . '/../lib/classes/' . substr($class, 12) . '.php';
21
+ }
22
+ }
23
+ spl_autoload_register('webpexpress_autoloader');
24
+
25
+ WebPOnDemand::processRequest();
wod/webp-realizer.php CHANGED
@@ -1,220 +1,25 @@
1
  <?php
2
- namespace WebPExpress;
3
 
4
- use \WebPConvert\WebPConvert;
5
- use \WebPConvert\Serve\ServeConvertedWebP;
6
- use \WebPExpress\ConvertHelperIndependent;
7
- use \WebPExpress\Sanitize;
8
- use \WebPExpress\SanityCheck;
9
- use \WebPExpress\SanityException;
10
- use \WebPExpress\ValidateException;
11
- use \WebPExpress\Validate;
12
-
13
- class WebPRealizer
14
- {
15
-
16
- private static $docRoot;
17
-
18
- public static function exitWithError($msg) {
19
- header('X-WebP-Express-Error: ' . $msg, true);
20
- echo $msg;
21
- exit;
22
- }
23
-
24
- /**
25
- * Get environment variable set with mod_rewrite module
26
- * Return false if the environment variable isn't found
27
- */
28
- private static function getEnvPassedInRewriteRule($envName) {
29
- // Envirenment variables passed through the REWRITE module have "REWRITE_" as a prefix (in Apache, not Litespeed, if I recall correctly)
30
- // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
31
- // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
32
- // We simply look for an environment variable that ends with what we are looking for.
33
- // (so make sure to make it unique)
34
- $len = strlen($envName);
35
- foreach ($_SERVER as $key => $item) {
36
- if (substr($key, -$len) == $envName) {
37
- return $item;
38
- }
39
- }
40
- return false;
41
- }
42
-
43
- public static function process() {
44
-
45
- include_once __DIR__ . "/../lib/classes/ConvertHelperIndependent.php";
46
- include_once __DIR__ . '/../lib/classes/Sanitize.php';
47
- include_once __DIR__ . '/../lib/classes/SanityCheck.php';
48
- include_once __DIR__ . '/../lib/classes/SanityException.php';
49
- include_once __DIR__ . '/../lib/classes/Validate.php';
50
- include_once __DIR__ . '/../lib/classes/ValidateException.php';
51
-
52
- // Check input
53
- // --------------
54
-
55
- try {
56
-
57
- // Check DOCUMENT_ROOT
58
- // ----------------------
59
- $checking = 'DOCUMENT_ROOT';
60
- $docRoot = SanityCheck::absPath($_SERVER["DOCUMENT_ROOT"]);
61
-
62
- // Use realpath to expand symbolic links and check if it exists
63
- $docRoot = realpath($docRoot);
64
- if ($docRoot === false) {
65
- throw new SanityException('Cannot find document root');
66
- }
67
- $docRoot = rtrim($docRoot, '/');
68
- $docRoot = SanityCheck::absPathExistsAndIsDir($docRoot);
69
-
70
- // Check wp-content
71
- // ----------------------
72
-
73
- // Passed in env variable?
74
- $wpContentDirRel = self::getEnvPassedInRewriteRule('WPCONTENT');
75
- if ($wpContentDirRel === false) {
76
-
77
- // Passed in QS?
78
- if (isset($_GET['wp-content'])) {
79
- $wpContentDirRel = SanityCheck::pathWithoutDirectoryTraversal($_GET['wp-content']);
80
- } else {
81
- // In case above fails, fall back to standard location
82
- $wpContentDirRel = 'wp-content';
83
- }
84
- }
85
-
86
- // Check WebP Express content dir
87
- // ---------------------------------
88
- $checking = 'WebP Express content dir';
89
- $webExpressContentDirAbs = SanityCheck::absPathExistsAndIsDir($docRoot . '/' . $wpContentDirRel . '/webp-express');
90
-
91
-
92
- // Check config file name
93
- // ---------------------------------
94
- $checking = 'config file';
95
- $configFilename = SanityCheck::absPathExistsAndIsFileInDocRoot($webExpressContentDirAbs . '/config/wod-options.json');
96
-
97
-
98
- // Check config file
99
- // --------------------
100
- $configLoadResult = file_get_contents($configFilename);
101
- if ($configLoadResult === false) {
102
- throw new ValidateException('Cannot open config file');
103
- }
104
- $json = SanityCheck::isJSONObject($configLoadResult);
105
-
106
- $options = json_decode($json, true);
107
- $wodOptions = $options['wod'];
108
- $serveOptions = $options['webp-convert'];
109
- $convertOptions = &$serveOptions['convert'];
110
- //echo '<pre>' . print_r($wodOptions, true) . '</pre>'; exit;
111
-
112
-
113
- // Validate that WebPExpress was configured to redirect to this conversion script
114
- // (but do not require that for Nginx)
115
- // ------------------------------------------------------------------------------
116
- $checking = 'settings';
117
- if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
118
- if (!isset($wodOptions['enable-redirection-to-webp-realizer']) || ($wodOptions['enable-redirection-to-webp-realizer'] === false)) {
119
- throw new ValidateException('Redirection to webp realizer is not enabled');
120
- }
121
- }
122
-
123
-
124
- // Check destination (the image that was requested, but has not been converted yet)
125
- // ------------------------------------------------------------------------------------
126
- $checking = 'destination path';
127
-
128
- // Check if it is in an environment variable
129
- $destRel = self::getEnvPassedInRewriteRule('DESTINATIONREL');
130
- if ($destRel !== false) {
131
- $destination = SanityCheck::absPath($docRoot . '/' . $destRel);
132
- } else {
133
- // Check querystring (relative path)
134
- if (isset($_GET['xdestination-rel'])) {
135
- $xdestRel = SanityCheck::noControlChars($_GET['xdestination-rel']);
136
- $destRel = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestRel, 1));
137
- $destination = SanityCheck::absPath($docRoot . '/' . $destRel);
138
- } else {
139
-
140
- // Then querystring (full path)
141
- // - But only on Nginx (our Apache .htaccess rules never passes absolute url)
142
- if (
143
- (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) &&
144
- (isset($_GET['destination']) || isset($_GET['xdestination']))
145
- ) {
146
- if (isset($_GET['destination'])) {
147
- $destination = SanityCheck::absPathIsInDocRoot($_GET['destination']);
148
- } else {
149
- $xdest = SanityCheck::noControlChars($_GET['xdestination']);
150
- $destination = SanityCheck::absPathIsInDocRoot(substr($xdest, 1));
151
- }
152
- } else {
153
- // Last resort is to use $_SERVER['REQUEST_URI'], well knowing that it does not give the
154
- // correct result in all setups (ie "folder method 1")
155
- $destRel = SanityCheck::pathWithoutDirectoryTraversal(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
156
- $destination = SanityCheck::absPath($docRoot . $destRel);
157
- }
158
- }
159
- }
160
-
161
- $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Does not end with .webp');
162
- $destination = SanityCheck::absPathIsInDocRoot($destination);
163
-
164
-
165
- // Validate source path
166
- // --------------------------------------------
167
- $checking = 'source path';
168
- $source = ConvertHelperIndependent::findSource(
169
- $destination,
170
- $wodOptions['destination-folder'],
171
- $wodOptions['destination-extension'],
172
- $webExpressContentDirAbs
173
- );
174
-
175
- if ($source === false) {
176
- header('X-WebP-Express-Error: webp-realizer.php could not find an existing jpg/png that corresponds to the webp requested', true);
177
-
178
- $protocol = isset($_SERVER["SERVER_PROTOCOL"]) ? $_SERVER["SERVER_PROTOCOL"] : 'HTTP/1.0';
179
- header($protocol . " 404 Not Found");
180
- die();
181
- //echo 'destination requested:<br><i>' . $destination . '</i>';
182
- }
183
- $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
184
-
185
- } catch (SanityException $e) {
186
- self::exitWithError('Sanity check failed for ' . $checking . ': '. $e->getMessage());
187
- } catch (ValidateException $e) {
188
- self::exitWithError('Validation failed for ' . $checking . ': '. $e->getMessage());
189
- }
190
-
191
-
192
- // Done with sanitizing, lets get to work!
193
- // ---------------------------------------
194
- $serveOptions['add-vary-header'] = false;
195
- $serveOptions['fail'] = '404';
196
- $serveOptions['fail-when-fail-fails'] = '404';
197
- $serveOptions['serve-image']['headers']['vary-accept'] = false;
198
-
199
- ConvertHelperIndependent::serveConverted(
200
- $source,
201
- $destination,
202
- $serveOptions,
203
- $webExpressContentDirAbs . '/log',
204
- 'Conversion triggered with the conversion script (wod/webp-realizer.php)'
205
- );
206
- }
207
- }
208
 
209
  // Protect against directly accessing webp-on-demand.php
210
  // Only protect on Apache. We know for sure that the method is not reliable on nginx. We have not tested on litespeed yet, so we dare not.
211
  if (stripos($_SERVER["SERVER_SOFTWARE"], 'apache') !== false && stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
212
  if (strpos($_SERVER['REQUEST_URI'], 'webp-realizer.php') !== false) {
213
- WebPRealizer::exitWithError(
214
  'It seems you are visiting this file (plugins/webp-express/wod/webp-realizer.php) directly. We do not allow this.'
215
  );
216
  exit;
217
  }
218
  }
219
 
220
- WebPRealizer::process();
 
 
 
 
 
 
 
 
 
1
  <?php
 
2
 
3
+ use \WebPExpress\WebPRealizer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  // Protect against directly accessing webp-on-demand.php
6
  // Only protect on Apache. We know for sure that the method is not reliable on nginx. We have not tested on litespeed yet, so we dare not.
7
  if (stripos($_SERVER["SERVER_SOFTWARE"], 'apache') !== false && stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
8
  if (strpos($_SERVER['REQUEST_URI'], 'webp-realizer.php') !== false) {
9
+ WebPOnDemand::exitWithError(
10
  'It seems you are visiting this file (plugins/webp-express/wod/webp-realizer.php) directly. We do not allow this.'
11
  );
12
  exit;
13
  }
14
  }
15
 
16
+ define('WOD_DIR', __DIR__);
17
+
18
+ function webpexpress_autoloader($class) {
19
+ if (strpos($class, 'WebPExpress\\') === 0) {
20
+ require_once WOD_DIR . '/../lib/classes/' . substr($class, 12) . '.php';
21
+ }
22
+ }
23
+ spl_autoload_register('webpexpress_autoloader');
24
+
25
+ WebPRealizer::processRequest();