rosell-dk / webp-express

Wordpress plugin for serving autogenerated WebP images instead of jpeg/png to browsers that supports WebP

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Test on multisite

rosell-dk opened this issue · comments

when will this point be back at roadmap?
your plugin sounds great but we have a multisite system. is there an option i can activate/deactivate to activate the plugin at multisite for selftest?

So it was noticed, that I took it off the roadmap :)
Perhaps I should put it back on... It might actually not be too hard to make it work multisite.

Currently the plugin will immediately deactivate, if it detects it runs in multi site configuration. The only reason I do this, is because I have not tested multi site AT ALL.

To remove that behaviour, you must edit one of the plugin files manually.

In plugins/webp-express/lib/activate-first-time.php, remove the following code:

if ( is_multisite() ) {
    Messenger::addMessage('error', 'You are on multisite. It is not supported yet. BUT IT IS ON THE ROADMAP! Stay tuned! The plugin has been <i>deactivated</i> again!');
    Actions::procastinate('deactivate');
    return;
}

I look forward to hear how it goes...

commented

It works on the main site, doesn't seem to work on "child" sites. Might me just me though, I'm using nginx and maybe the routing is just off.

Works fine for me so far, if the plugin is not network activated, but activated on the child website independently.

@feeltheice77, @ktmn and @portalzine: I have multisite functionality in beta.
Will you help testing it?

To test it, you must:

  1. Go to https://wordpress.org/plugins/webp-express/advanced/
  2. Scroll down to the “Previous versions” section (in the bottom of the page)
  3. Select “Development version” and click “Download”
  4. Install manually

Please report test results (good or bad).

Related threads / topics / issues:
#173
https://wordpress.org/support/topic/multisite-509/#post-11147135

commented

I updated to development version. Network activated it and now the settings page is in network admin and not on any other dashboards. All good there.

The main site serves webps, but the child sites (subdirectory) don't.

For example:
Visiting example.com the file example.com/wp-content/uploads/2018/01/img.jpg is served as webp.
Visiting example.com/childsite the file example.com/childsite/wp-content/uploads/sites/50/2018/03/another.jpg is not served as webp.

"Direct link/non-wp image" such as example.com/otherassets/logo.png is correctly served as webp whether it's displayed on example.com or example.com/childsite.

My nginx conf has this:

if ($http_accept ~* "webp") {
	rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?source=$document_root$request_uri&wp-content=wp-content&%1 break;
}

Does the source need to be changed?

Also here are the settings:
image

The source parameter in Query String is currently only used when the "Do not pass source in Query String" is disabled, which currently only can be done in Tweaked mode. I'm considering changing that behaviour because it seems that option to pass source in QS is needed in more cases than I thought.

To try if it works with source, you can switch to Tweaked mode and disable that option.
Or you can modify "wod/webp-on-demand.php"

Look for this code:

    if ($allowInQS) {
        if (isset($_GET['xsource'])) {
            return substr($_GET['xsource'], 1);         // No url decoding needed as $_GET is already decoded
        } elseif (isset($_GET['source'])) {
            return $_GET['source'];
        }
    }

change it to:

        if (isset($_GET['xsource'])) {
            return substr($_GET['xsource'], 1);         // No url decoding needed as $_GET is already decoded
        } elseif (isset($_GET['source'])) {
            return $_GET['source'];
        }

I haven't tested multisite with Nginx. Forgot about that. What are the nginx configuration rules that you use for Multisite?

commented

What are the nginx configuration rules that you use for Multisite?

Using the Nginx Helper plugin, the multisite gist of it is this:

map $uri $blogname{
	~^(?<blogpath>/[^/]+/)files/(.*)	$blogpath ;
}

map $blogname $blogid{
	default -999;
        include /var/www/example.com/htdocs/wp-content/plugins/nginx-helper/map.conf ;
}

server{	## inside server block 

	location ~ ^(/[^/]+/)?files/(?<rt_file>.+) {
		try_files /wp-content/blogs.dir/$blogid/files/$rt_file /wp-includes/ms-files.php?file=$rt_file ;
		access_log off;	log_not_found off; expires max;
	}
}
commented

Switching to Tweaked mode and commenting out the if ($allowInQS) condition didn't have any effect afaik.

Yeah, it seems that these rules are for serving static files directly. The rules probably takes the request before our rules gets to process.

Solving this probably requires some trickery.
We should read up on this: https://easyengine.io/wordpress-nginx/tutorials/multisite/static-files-handling
And you should check the updated Nginx rules in the FAQ.

I have other stuff in my hands, but shall return to this

Btw: Does the Alter HTML functionality work ?

commented

Does the Alter HTML functionality work ?

With a custom theme (Sage based) mostly not - only image in widget area was converted (created with Monster Widget), but not image block in post content. But with 2019 seemed like it worked everywhere, but started getting 404's for WC product page images, so not compatible with that I don't think.

commented

As for the nginx config, this is more similar to the conf I'm using.

It's got this part for multisite:

if (!-e $request_filename) {
	rewrite /wp-admin$ $scheme://$host$uri/ permanent;
	rewrite ^(/[^/]+)?(/wp-.*) $2 last;
	rewrite ^/[^/]+(/.*.php)$ $1 last;
}

and webp code I added after it:

if ($http_accept ~* "webp") {
	rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?source=$document_root$request_uri&wp-content=wp-content&%1 break;
}

The rewrite ^(/[^/]+)?(/wp-.*) $2 last; line rewrote child site urls and it never reached webp-on-demand.php.

So I moved the webp part above it and now the issue was with the source variable. The nginx $document_root$request_uri includes the child site name in the uri, while in the filesystem it's path doesn't include it.

Bad: /var/www/example.com/public_html/childsite/wp-content/uploads/sites/50/2018/03/another.jpg
Good: /var/www/example.com/public_html/wp-content/uploads/sites/50/2018/03/another.jpg

Once I added some PHP to strip out the childsite from the path it works great for me. I wonder if there's a proper way to do it with nginx or not, but other than that, a filter for $source before this:

if (!file_exists($source)) {
    header('X-WebP-Express-Error: Source file not found!', true);
    echo 'Source file not found!';
    exit;
}

would do the trick too.

I cannot add filter because webp-on-demand. php does not run Wordpress init.

I'm thinking that I for NGINX multisite could allow webp-on-demand.php to do some heuristics

webp-on-demand.php knows the path to "wp-content", as the path is provided in the querystring (as a relative path between document root and "wp-content"). So it knows that images are to be found in that path.
Ie:
/var/www/example.com/public_html/wp-content

So when it receives "BAD" (works for GOOD as well):
var/www/example.com/public_html/childsite/wp-content/uploads/sites/50/2018/03/another.jpg

It could first subtract document root:
/childsite/wp-content/uploads/sites/50/2018/03/another.jpg

and then it could skip folders until it reaches "wp-content":
/wp-content/uploads/sites/50/2018/03/another.jpg

and then it could append it to document root:
var/www/example.com/public_html/wp-content/uploads/sites/50/2018/03/another.jpg

Perhaps only for NGINX and multisite

I can easily have webp-on-demand.php know that we are running in multisite, by storing that fact in the config (wod-options.json) - or by requiring it to be passed in the query string.

The method could perhaps also be used for Apache sites. Perhaps optionally

Another approach could be to try to come up with a rewrite rule that inserts the path from the matched path instead of the server variables. The path will be relative, so webp-on-demand.php must then accept relative paths too. Something like this:

if ($http_accept ~* "webp") {
	rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?source-rel=$1.$2&wp-content=wp-content&%1 break;
}

I'm not sure if that will match the subsite as well. In case it does, one could do something like this:

if ($http_accept ~* "webp") {
	rewrite ^/(childsite|childsite2)(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?source-rel=$2.$3&wp-content=wp-content&%1 break;
}
commented

I'm not good enough with nginx or regex to make it work in server config.

if ($http_accept ~* "webp") {
	rewrite ^/(childsite|childsite2)(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php?source-rel=$2.$3&wp-content=wp-content&%1 break;
}

Experiemented with this a little, couldn't make it work.
The $1, $2 and $3 will be different things for main site and child sites, and the (childsite|childsite2) part matches childsite for /childsite2 and $2 ends up with 2/wp-content/....
In my earlier comment there should be a way to get the actual child site name, but it's meant for older versions of WPMU I think and doesn't work for me.


I don't know what the best way would be for different setups but for me, this could work:

On a child site
the $document_root is /var/www/example.com/public_html
and $request_uri is /childsite/wp-content/uploads/sites/50/2018/03/another.jpg

What about passing webp-on-demand.php?source-root=$document_root&source-uri=$request_uri&wp-content=wp-content&%1

Then webp-on-demand.php would need to check that the first URI part of $_GET['source-uri'] matches $_GET['wp-content'] and if it doesn't, then remove the parts that come before it, and then put the root and uri together for the source.

Something like this:

if(isset($_GET['source-root']) && isset($_GET['source-uri'])) {
    $wp_content = isset($_GET['wp-content']) ? $_GET['wp-content'] : 'wp-content';
    $parts = explode('/', $_GET['source-uri']);
    foreach($parts as $index => $part) {
        if($part !== $wp_content) {
            unset($parts[$index]);
        } else {
            break;
        }
    }
    $source = $_GET['source-root'] . '/' . implode('/', $parts);
}

I cannot add filter because webp-on-demand. php does not run Wordpress init.

When push comes to shove you could do a non-wordpress filter, like look for a specifically named file in $_SERVER['DOCUMENT_ROOT'] and include it if it exists, and then apply a specifically named function from there.

Both ideas sounds good...

It seems there is no reason to pass source-root, as document root should be the same in nginx conf and in the script.

Even before this, I had been thinking about introducing "source-rel" (relative path from document root to source file). Passing the complete path increases the risk of problems of firewall blocking. I think I'll go with "source-rel" rather than "source-uri".

I think implementing the filter in the script, and activating it by name through the QS will do. It is simpler and doesn't require any security measures. Ie:
?source-rel=$request_uri&source-rel-filter=discard-first-folder

commented

Sounds good, as long as discard-first-folder doesn't discard the first folder when it's already wp-content (parent site).

I see. discard-parts-before-wp-content would be a better name, then.

This stuff doesn't work when upload folder has been moved or wp-content folder has been moved (not just renamed), but I guess it solves the needs of the majority of the minority that has these kinds of needs.

I have added the option to discard parts before wp-content. To test, please update to master. Note that the vendor library isn't distributed here on github, so make sure to copy the vendor folder over.

There is now a new option "Method for passing filename" in the "Redirect rules" section. You must select "Pass through query string (relative)"

I just discovered that you were using ?source=$document_root$request_uri rather than ?source=$request_filename, which I currently recommend in the FAQ.

Perhaps changing that solves it so we don't need the filter after all?

commented

source-rel=$request_uri&source-rel-filter=discard-parts-before-wp-content with Pass through query string (relative path) enabled works with master.

Except it broke static/non-wp images, by looking for wp-content where there was none. Slight alteration to the code fixed it:

if (isset($_GET['source-rel-filter'])) {
    $parts = explode('/', $srcRel);
    $wp_content = isset($_GET['wp-content']) ? $_GET['wp-content'] : 'wp-content';
    if ($_GET['source-rel-filter'] == 'discard-parts-before-wp-content' && in_array($wp_content, $parts)) {
        foreach($parts as $index => $part) {
            if($part !== $wp_content) {
                unset($parts[$index]);
            } else {
                break;
            }
        }
        $srcRel = implode('/', $parts);
    }
}

(Moved $parts and $wp_content declarations up and added in_array($wp_content, $parts) check).


I just discovered that you were using ?source=$document_root$request_uri rather than ?source=$request_filename, which I currently recommend in the FAQ.

Perhaps changing that solves it so we don't need the filter after all?

source=$request_filename gave the same value as source=$document_root$request_uri.

The value seems to be dynamic based on rewrites and whatnot. Logging it at the "end":

location ~\.php$ {
	...
	add_header X-filename "$request_filename" always;
}

the value ends up as /var/www/html/example.com/public_html/wp-content/plugins/webp-express/wod/webp-on-demand.php by the

if ($http_accept ~* "webp") {
	rewrite ^/(.*).(jpe?g|png)$ /wp-content/plugins/webp-express/wod/webp-on-demand.php... break;
}

block, while in there it's still $document_root$request_uri, so to get the "correct" $request_filename I think I would need to move the webp block back below this block multisite:

if (!-e $request_filename) {
	rewrite /wp-admin$ $scheme://$host$uri/ permanent;
	rewrite ^(/[^/]+)?(/wp-.*) $2 last;
	rewrite ^(/[^/]+)?(/.*\.php) $2 last;
}

but I'm not quite sure how it works and the last parameter makes it never reach the webp block on child sites (if that makes any sense). It's probably possible but not with my setup/knowledge.


webp-on-demand.php security:

I'm just wondering, how safe is it to use the $_GET variables in webp-on-demand.php "as is"? Could someone navigate to example.com/wp-content/plugins/webp-express/wod/wod-on-demand.php with some fishy values for the query strings and get something out of it?
Probably not, but for peace of mind, would it be a valid opt-in security measure for webp-on-demand.php's first line to check for some sort of $_GET['password'] and the server config to include it as a parameter:

if ($http_accept ~* "webp") {
	rewrite ^/(.*).(jpe?g|png)$ .../webp-on-demand.php?password=asd123... break;
}

or would that leak somewhere in the serving process? Delivering the correct password to check against to webp-on-demand.php is probably also a challenge, but something to think about I suppose.

Altered your change a bit, so the code for 'discard-parts-before-wp-content' filter is contained inside the
if ($_GET['source-rel-filter'] == 'discard-parts-before-wp-content'):

        if (isset($_GET['source-rel-filter'])) {
            if ($_GET['source-rel-filter'] == 'discard-parts-before-wp-content') {
                $parts = explode('/', $srcRel);
                $wp_content = isset($_GET['wp-content']) ? $_GET['wp-content'] : 'wp-content';

                if (in_array($wp_content, $parts)) {
                    foreach($parts as $index => $part) {
                        if($part !== $wp_content) {
                            unset($parts[$index]);
                        } else {
                            break;
                        }
                    }
                    $srcRel = implode('/', $parts);
                }
            }
        }

Yes security. I have also wondered if the script could be misused somehow...
Actually, denying direct calls to webp-on-demand.php seems to be easily achieved in Nginx with the "internal" directive: https://nginx.org/en/docs/http/ngx_http_core_module.html#internal

commented

Nice, thanks!

I tried inserting this:

    location ~ webp-on-demand\.php$ {
         internal;
    }

It results in a 404 when webp-on-demand is requested directly.
However, it then serves the php source code instead of invoking it when an image is requested

Perhaps a password is the simplest solution. It could be stored in the configuration file as well as be handed over in the query string. The query string cannot be detected because the redirect is internal (both on Nginx and in Apache).

The password could be autogenerated. Nginx users, who will need to know it, would be able to view it by opening wp-content/webp-express/config/config.php. The wp-content/webp-express/config folder is protected from the outside by the .htaccess file in it. A similar protection must be done in Nginx

Opening a new issue for that

It seems we can just insert these lines in the beginning of webp-on-demand.php:

if (preg_match('#webp-on-demand.php#', $_SERVER['REQUEST_URI'])) {
    echo 'Direct access is not allowed';
    exit;
}

I'm hoping to release 0.12.0 tomorrow...

Tomorrow from now ;)

@ktmn @rosell-dk I tried to run webp express on multisite (subdir mode) with configs from FAQ, but it doesn't work. What I should add to nginx config?

Any news on this one? I did read through this issue, but I did not find the proper nginx rules for multisite install (in my case subdomain, but that should make any difference I guess)

commented

All I got is this one rule (and it's quite high up in the config file) but I'm using an older version to make it work.

What is your conf, what errors are you getting?

Hi guys, I'm on WP-Engine (which is on NGINX I guess), is there any solution to make the plugin work on child sites? I use sub-directories.

commented

I second that– would be great to be able to install this plugin WITHOUT having to network activate it– I.E. just use it on a child site