Part of laptop screen showing Wordpress Admin Area

How to Fix WordPress File Permissions Issues

For anyone working on WordPress sites, the dreaded message that WordPress requires FTP credentials to add a plugin or remove one will come up. Or that theme files can’t be edited.

File ownership mismatch often causes errors in WordPress

Most of these stem from mismatches in file and folder ownership on your Linux server.

Typical examples would be images or files uploaded directly to the server rather than via WordPress, such as when restoring a backup while logged in as the ROOT user. WordPress tends to run as the Apache user, which means WordPress then cannot edit the files or delete them.

What is needed is to change ownership from the current owner, to the Apache user (or whichever user & group Apache runs as):

# sudo chown apache:apache /var/www/html -R

This instructs Linux to change ownership with the chown command, to the user apache, in the group apache at the specific file / folder location (/var/www/html, the normal web directory for RHEL/Centos). The flag ‘-R’ means recursive, so the command will be applied to all files in the specified folder, and all subfolders and files.

However, doing this manually will be forgotten at some point, so a better solution is to automate it with a cron job. Cron jobs are tasks that run on a specified schedule in Linux.

Cron jobs are usually located at /etc/crontab but some Linux distributions put it elsewhere. So we’ll make a cron job running every hour by placing only one of the below instructions into the file at the bottom:

0 \* \* \* \* chown apache:apache /var/www/html -R >/dev/null 2>&1
@hourly chown apache:apache /var/www/html -R >/dev/null 2>&1

This will now run your command every hour once the file is saved. The >/dev/null 2>&1 end of the command means any output will be silenced, so you’re not getting alerted each time the cron job runs. It will also mean errors won’t be communicated.

For Shared Hosting

You may not have access to the crontab or a cron job manager if you’re on a shared hosting plan, but there are other fixes which can be applied to WordPress.

You should be able to edit your wp-config.php file (in the www home directory).

Insert the following code at the end of file:

/** Sets up 'direct' method for WordPress to enable update/auto update with out FTP */
define('FS_METHOD','direct');

Once that’s saved, WordPress should now be able to update itself and plugins without errors, and also install and delete plugins.

Non-Wordpress Sites

The chown fix will work on any website with the same type of file permissions issue, as it’s likely the Apache user isn’t the file owner.

More on Cron Jobs

Linode has a good article about how cron jobs work. And Crontab.guru has a huge list of example cron job commands.

PHP code from Wordpress

Custom wp-config for wordpress behind reverse proxy & using a staging server

WordPress, especially in version 5+, is an amazing piece of software. However, in certain situations the default code falls short. Recently, I needed to set up WordPress behind a reverse proxy, parallel-host a staging server, and use both a visual page builder and a javascript-based translation plugin.

And all of it needed to be easy-to-use for non-technical client marketing staff.

In short: A complicated setup.

The client should be able to edit and post content in a simple way, which isn’t possible in a reverse proxy setup when using a page builder. This is because the WP_HOME setting (i.e. the homepage URL) will direct any navigation from the hosting server to the public URL, the one we’re marketing to customers as well as doing SEO for. And neither our page builder or the translation plugin will work from the public domain!

I also needed the client’s web team to be able to backup and restore the site in a somewhat user-friendly way.

The code isn’t complicated to set up once it was tested in a few variations to see what worked best. Please note this doesn’t cover any Apache or nginx settings to set up the reverse proxy, as configuring and troubleshooting is best left to a hosting specialist. No special settings were needed on a typical LAMP stack for this custom code to work.

Full code at the end.

Why Modify wp-config.php?

The config file for WordPress is the most reliable place to put custom configs as it doesn’t get overwritten by WP updates, and any code here is executed before any HTML output. This is important as cookies and headers must be set before any HTML is sent to the browser.

It also executes very quickly, as WP loads files in this order:

  1. index.php
  2. wp-blog-header.php
  3. wp-load.php and template-loader.php
  4. wp-config.php loaded by wp-load.php

URL Settings

The setup only requires 3 values to be set. First we need to know the public homepage URL ($public_url), which is the front-end of our reverse proxy. This is a fully qualified URL.

Then we need the actual hosting domains for $production_server (production.example.com) and $staging_server (stage.example.com) servers. These use subdomains but could easily be domain1.com and domain2.com with no changes.

We’re not qualifying these URLs as the PHP server variable we’re going to check isn’t itself fully qualified, and there’s no reason to check against https vs http URLs, which our CDN (Cloudflare in this case) handles for us.

/* Staging and reverse proxy settings */

$public_url 		= 'https://public-server.com';
$production_server 	= 'production.example.com';
$staging_server 	= 'stage.example.com';

Setting and Removing Our Cookie

Mmmmm, cookies :-)

To set the cookie controlling our define( ‘WP_HOME’, “https://[www.example.com]” ); WordPress setting, we need to pick an arbitrary URL path to check for.

In this case, the parametric /?cookiesetter.

If the PHP $_SERVER variable matches, we execute the cookie setting code, then redirect with a 302 to the /wp-admin/ directory on our production server. A 302 redirect is used as the browser will remember 301 URLs in many cases, and may skip loading the requested URL and so the cookie isn’t set.

WordPress will automatically redirect to /wp-login/ if our visitor isn’t already logged in.

Note that we have ‘https://’ hardcoded in this redirect target, as we don’t want to risk a non-secure login page showing for a WordPress user.

We’re setting a cookie valid for 8 hours to match a working day. 3600 is one hour in seconds.

if($_SERVER['REQUEST_URI'] == "/?cookiesetter"){

	setcookie("admincookie", 'exists', time()+3600*8, '/', $production_server, true, true);  /* expire in 8 hours */
	header("Location: https://$production_server/wp-admin/", TRUE, 302);
	exit;

}

Removing the cookie is usually not needed, as we can go to the $public_url to see any changes made to the website, but it’s added as an option.

Again, we use a specific URL path – /?cookieremover – to trigger the code execution, give our cookie lifespan a negative number (this removes a cookie) and redirect to the homepage with ‘/’. We’ve not added the protocol (https://) as it doesn’t matter in this case.

if($_SERVER['REQUEST_URI'] == "/?cookieremover"){

	setcookie("admincookie", '', time()-3600, '/', $production_server, true, true);   /* expired, removes cookie */
	header("Location: /", TRUE, 302);
	exit;

}

Optionally, we could have redirected to the $public_url.

Staging Server Settings

Our staging server needs to set both WP_SITEURL and WP_HOME to itself so resources and internal links are self-referencing. In other words, how WordPress would work by default. This section ensures that should we take a backup from either stage or production and apply this to the other server, we’re executing the correct code.

Additionally, we need to keep the staging server out of the search engines’ indexes, so we set a new header with PHP to noindex all pages and resources on the staging server.

if($_SERVER['HTTP_HOST'] == $staging_server ){
	
	define( 'WP_SITEURL', "https://$staging_server" );
	define( 'WP_HOME', "https://$staging_server" );

	header("X-Robots-Tag: noindex, nofollow", true);

}

Production Server Settings

This is where the dynamic handling of our two WP settings have a direct effect on the front end as experienced by the WordPress user. Effectively, we’re going to ensure anyone editing the site stays on the actual hosting server rather than directed to the $public_url and unable to do edits.

First, check we’re not on the staging server.

Note that we can’t* check for the production server, as the server will always see itself in the HTTP_HOST URL. The server does not see the $public_url without modifying headers such as x-forwarded-for. Similarly, the SERVER_NAME value also remains the same, and relies on server setup, rather than hosting location.

* OK, can’t is a strong word here. We could, but it would involve significantly more setup and testing from the client’s web team, and I want to minimize this.

Then, we check for our set cookie, and if available, set both WP_SITEURL and WP_HOME to the $production_server value. If not available, we set the WP_HOME variable to our $public_url.

Now, our user will see all internal links on the site point to $production_server/[path]. A user without this cookie would follow internal links to $public_url/[path], switching to the ‘real’ domain name we’re promoting.

if($_SERVER['HTTP_HOST'] != $staging_server ){
	
	if(isset($_COOKIE['admincookie'])){
			
		define( 'WP_SITEURL', "https://$production_server" );
		define( 'WP_HOME', "https://$production_server" );
		
	} else {

		define( 'WP_SITEURL', "https://$production_server" );
		define( 'WP_HOME', $public_url );
	}

}

Optionally, we could set the WP_SITEURL to our $public_url as well. Normally, the hosting server should serve resources faster than the reverse proxy server as an intermediate step for each request is removed. If using a CDN, test which performs better in your case.

Now we have a setup with wordpress behind a reverse proxy, a staging server which behaves correctly, and the client is able to edit, publish, backup and restore the site with a good level of user-friendliness.

Why not Check is_user_logged_in()?

The problem I encountered is that the user must be able to log in to WordPress before is_user_logged_in() is TRUE.

Unfortunately, the /wp-login/ page tends to redirect to the WP_HOME URL when submitting the login form, and so our user isn’t logged in.

Another solution we initially tested was using a list of IP addresses to control the WP_HOME and WP_SITEURL settings. This was an effective but not efficient solution that required programming skills to maintain and update each time a user connects from a new IP address.

Complete Code

Copy the following into your wp-config.php at or near the top to implement this solution. It should work fine once updated with your site’s settings, let me know if it doesn’t and I’ll try to help.

/* Staging and reverse proxy settings */

$public_url 		= 'https://public-server.com';
$production_server 	= 'production.example.com';
$staging_server 	= 'stage.example.com';

/* Staging and reverse proxy code */

if($_SERVER['REQUEST_URI'] == "/?cookiesetter"){

	setcookie("admincookie", 'exists', time()+3600*6, '/', $production_server, true, true);  /* expire in 6 hours */
	header("Location: https://$production_server/wp-admin/", TRUE, 302);
	exit;

}

if($_SERVER['REQUEST_URI'] == "/?cookieremover"){

	setcookie("admincookie", '', time()-3600, '/', $production_server, true, true);   /* expired, removes cookie */
	header("Location: /", TRUE, 302);
	exit;

}

if($_SERVER['HTTP_HOST'] == $staging_server ){
	
	define( 'WP_SITEURL', "https://$staging_server" );
	define( 'WP_HOME', "https://$staging_server" );

	header("X-Robots-Tag: noindex, nofollow", true);

}

if($_SERVER['HTTP_HOST'] != $staging_server ){
	
	if(isset($_COOKIE['admincookie'])){
			
		define( 'WP_SITEURL', "https://$production_server" );
		define( 'WP_HOME', "https://$production_server" );
		
	} else {

		define( 'WP_SITEURL', "https://$production_server" );
		define( 'WP_HOME', $public_url );
	}

}

Photo by Lavi Perchik on Unsplash

Stop People Copying Your Content with only HTML & CSS

Today I came across a website which had disabled copy and highlighting of the text, even when I turned off JavaScript in the browser. Curious, I researched how this stop content copying was accomplished.

I found that there’s both multiple CSS extensions and a CSS directive which disables a user selecting text on an element. Further, you can disable the right-click menu with an attribute.

Here’s how to disable copy and paste on websites.

First, adding the attribute & value oncontextmenu=”return false” to a page element will disable the right-click menu.

<body oncontextmenu="return false">

The CSS directive user-select: none; will disable highlighting and copying. The directive is well supported in modern browsers, apart from Safari on iOS and desktop.

This example CSS class includes all prefixes (only the two initial prefixes are needed currently) that can be applied:

.nocopy {
 -webkit-touch-callout: none;
 -webkit-user-select: none;
 -khtml-user-select: none;
 -moz-user-select: none;
 -ms-user-select: none;
 user-select: none;
}

Example usage:

<body class="nocopy">

Prefixes listed against browser usage:

iOS Safari-webkit-touch-callout: none;
Chrome/Safari/Opera-webkit-user-select: none;
Konqueror-khtml-user-select: none;
Firefox-moz-user-select: none;
Internet Explorer/Edge-ms-user-select: none;

I would definitely avoid this on a production website, as it’s incredibly user un-friendly and looks a bit dodgy. After all, your visitors arrive to read / learn from your content. Often that involves taking notes / clippings for use elsewhere.

It’s an interesting option however.

Fix MyISAM MySQL Tables with myisamchk

At times MyISAM tables in MySQL can crash and repair with PHPMyAdmin or from within the MySQL Server console won’t work. In these cases, this is how to repair (recover) the MyISAM table with myisamchk using these steps:

Go to the database directory with:

cd /var/lib/mysql/[database name]

Run myisamchk with:

myisamchk -r [table name]

Should that fail, use:

myisamchk -r -v -f [table name]

The flags mean:

  • -r = recover
  • -v = verbose
  • -f = force

https://dev.mysql.com/doc/refman/8.0/en/myisamchk-repair-options.html