When you take a copy of your WordPress site to a staging environment, all the customers, orders, and other data is copied to the staging copy. Also, the API keys and other credentials which might be stored in your WordPress database are copied to the staging copy.
But there’s one thing which usually goes unnoticed when copying the site to staging, that is the overdue scheduled tasks. When the very first page of the site is loaded on the copy, the overdue scheduled tasks are executed. If your site is sending emails to subscribers or making calls to a live payment gateway or updating the CRM or posting to webhooks, these things will go to the real data.
What is a staging site?
This is kind of a private copy of the site for you to test things before publishing them to the live site. Usually, you will use it to test updates, new plugins, redesigns, etc.
It’s an exact copy of the site with all the files and an exact copy of the database with all the data.
Some of the options from the database which have data which can impact the users and site data:
wp_options-> It contains all the plugin settings, the live API keys, webhook URLs, etc which are being used by the site.cron-> It contains all the scheduled tasks and their next scheduled run time.
So essentially the staging copy will keep all of this data as well.
How does WP-Cron send the email?
On our site, there is no system wide cron by default, instead WP-Cron is used to execute the cron functionality on our site. WP-Cron simply triggers the cron functions when someone visits our site. From the init hook, the function wp_cron() is called which then checks for any due cron tasks and triggers them.
Since 6.9, due cron tasks are now spawned on the shutdown hook instead of init. What it means is, now from the shutdown hook a loopback wp_remote_post() is performed to your-site-url/wp-cron.php, which would then process the cron jobs registered under the cron option.
If we copy the database from production to staging, the cron also comes over to the staging site, and they are already overdue in the copy as well, so on the very first visit to the staging site, all the overdue tasks would be triggered and executed.
DISABLE_WP_CRON can be defined to stop the visit-triggered runs of the cron only, but keep in mind that if the cron is being called by a server cron (out of site) the tasks would still be executed.
What else sends email from a copy?
The question comes, what if someone is testing on the copy, and they make a change to a real user’s password -> wp_update_user() would email the user.
Or what if they change the email address of a user -> They’ll get a notice on their old email address.
So basically all of the orders, form entries, resent notifications, the ones you send to subscribers in newsletter or any other plugin that needs to send mails to users goes through the wp_mail() function, and that’s nearly all of it.
Some plugins might use PHP mail() function or their own SMTP client function for sending mails, but most of the commonly used features are handled using the wp_mail() function.
How does a copy call live APIs?
WordPress provides us with a built-in HTTP API which we can use to perform various HTTP requests on the site.
wp_remote_get()wp_remote_post()wp_remote_request()
This is used by the core updates, payment gateways, CRM plugins, etc. to communicate with external APIs. It is also used by webhook plugins to send requests to other sites, send notifications to Slack, check if a plugin is premium or not via license keys etc.
There can be some security issues with this, if we are copying the keys from production to staging and using them, the staging site would be talking to the live accounts.
How does WordPress know it is staging?
wp_get_environment_type(): Since 5.5.0, returns the current WordPress environment type. The values can be local, development, staging, production. This value is set through the WP_ENVIRONMENT_TYPE server env variable or constant. Note: if a constant is set it would override the env variable. If neither of these is set or if it is unknown the value production would be returned. This can be set per server, either through wp-config.php or server configuration files.
For e.g. if we create a copy of our WordPress installation without changing the value of the WP_ENVIRONMENT_TYPE constant, every plugin or feature which asks for this information would see it as production for that particular copy.
How do we stop the email?
wp_mail() is a pluggable function, which means that it can be modified or replaced with other functionality. We can take advantage of this to disable it.
There are two ways to do this.
Way 1:
We can just directly define our own wp_mail(), the first wp_mail definition we do in our code would be the one which gets executed.
This way the plugin Disable Emails works, it has 30,000+ active installs, and has a must-use option, which even survives when you do a database refresh from a live site.
Must-use plugins get loaded before any other plugins, they are not in the database, they are part of the WordPress environment.
Way 2:
We can use the filter pre_wp_mail to prevent wp_mail() from running, since 5.7.0 this is available.
The filter pre_wp_mail gives us the value before the wp_mail function is executed, and we can just return a non-null value from it and that would prevent the wp_mail() to even run, and our defined value would be returned by the wp_mail() and it would just send nothing.
The wp_mail filter runs first, so that we can get or modify the recipients, subject, headers etc. before it is sent out.
Both of these ways miss direct PHP mail() and any other own SMTP clients implementation.
How do we stop the API calls?
pre_http_request filter, available since 2.9.0, runs right before anything leaves the server to be sent to a remote URL, allowing you to change/intercept the request.
We can return WP_Error from this and the request would fail just like a network error.
Adding define( 'WP_HTTP_BLOCK_EXTERNAL', true ); to your wp-config.php would block all external requests by default, meaning any code trying to hit an external URL would fail.
This would also not let you access api.wordpress.org, so if you try to fetch data from there, it will break for example, plugin and other data updates.
If we want to allow some requests, we can do that by adding the host(s) to the WP_ACCESSIBLE_HOSTS constant. We can add wildcards also, for example, *.wordpress.org. This would allow any subdomain of wordpress.org.
Setting up Axle Staging Guard
Staging Guard
Stops staging copies from emailing real people or getting indexed.
Use it when you copy the live site to staging or to your laptop.
Learn more about Staging GuardTry it live: Staging GuardThe free plugin has the following checks to determine if the current site is a live site.
WP_ENVIRONMENT_TYPE-> If the constantWP_ENVIRONMENT_TYPEis set the plugin would initialise on the basis of the value of that.AXLE_STAGING_GUARD_PRODUCTION_HOSTS-> If the constantAXLE_STAGING_GUARD_PRODUCTION_HOSTSis set and contains some value, this can be used to add a list of domains which are marked as the live site.- Production lock -> If none of the above is set, the plugin will check for a production lock. Under the menu Tools -> Staging Guard it will show “This is the live site”, if this statement is clicked the site is marked as a production site.
This lock stores the sha256 hash of the live domain, not the domain itself, to prevent search-replace breaking the lock. As search and replace will rewrite the domains and hashes would remain the same.
The hashes are created such that www.example.com and example.com would be considered the same and the hash for both of them would be the same.
If none of these conditions are set, the plugin will be considered as an off state and the plugin will be disabled.
Email on staging copies
By default, on the copies of the site, they are set to block all email.
There’s another option to redirect all the email sent from the site to a single test email address.
The subject of the email sent to test email gets modified to a default value that contains the word [STAGING → original recipients] -> so that we can keep track of which email is the test email.
Cc and Bcc recipients are also stopped.
We can add one or more addresses to always deliver the email to, these addresses can also be addresses of whole team or even a whole @domain
The blocked email is reported as sent, but the form behaves as if nothing happened.
There’s a log of all the recipients, the subjects and the time of sending the email. But the email bodies are only logged if we switch them on in the settings. The entries in the email log are deleted after 7 days.
When another plugin is found replacing the wp_mail() function, a warning is shown, stating about the conflict.
API calls on staging copies
axle-staging-guard provides us 3 modes,
- allow all
- log only
- block except specified hosts
On a fresh plugin installation, the mode would be set to log only but for existing installations the mode would be allow all
The list would contain the method, host, path, the calling plugin and the number of times it’s being called.
The log does not contain any query strings, request bodies, or headers.
The following hosts will be always allowed regardless of the settings,
wordpress.org*.wordpress.orgw.org- Your own site
localhost
Blocked call -> WP_Error with the code axle_staging_guard_http_blocked
We can use the WP-CLI command to test a URL by running the command wp axle-staging-guard test-url <url> this would provide us the verdict of the URL, nothing would be sent out with this.
What can it not catch?
The plugins which are replacing the wp_mail() entirely will only display a warning message saying that they are replacing the function and will continue as usual.
In PHP SDK they are using their own cURL or socket connection to send the request, instead of using the default wp_remote_request() function WordPress provides.
- When the script is running on the browser side for example – analytics scripts, embeds etc.
Another thing to keep in mind, if we are copying the staging database back to live, the lock for that would match and the live site would be unguarded.
A setup that holds
We can also install it on live. On the live site we can go to Tools > Staging Guard and click the “This is the live site” button. -> or we can set the WP_ENVIRONMENT_TYPE to production on the live site, and staging on the staging site.
Staging server
On the per staging server settings we can go to the wp-config.php and set the constant AXLE_STAGING_GUARD_MAIL_MODE to either block or redirect, these would determine how the email is being handled if a staging server is detected.
The value of this constant is read-only on the screen, it cannot be changed by a database copy.
Outgoing calls
For the outgoing calls, the first day we would be putting it to “Log only”, and then we can read the list of hosts being requested to and allow the ones we trust, and then at the end we would be blocking all the requests except the ones we allow.
Database refresh
After every database refresh we can run the command wp axle-staging-guard status to check the status of the plugin.
Pushing a staging over a live site
We should freeze the live site first with Axle Read-Only Mode.
Read-Only Mode
Freeze the site during a migration or deploy. Visitors keep reading; nothing written gets lost.
Use it when you move the site to a new host.
Learn more about Read-Only ModeTry it live: Read-Only ModeTry it first: try.axlewp.com/axle-staging-guard