PHPCS warnings
Issues reported as VIP warnings should not be pushed to a production server unless a specific use case requires it. Some common issues reported as VIP warnings are listed below.
PHP_CodeSniffer (PHPCS) scans that are run against WordPress application code by the VIP Code Analysis Bot—or scans that are run manually after following the instructions to install PHPCS for WordPress VIP—will run with identical standards that include the WordPress-VIP-Go
standard.
The PHPCS scan will generate a report that itemizes identified errors and warnings categorized by severity.
Warnings are issues that should not be pushed to a production environment unless a very specific use case requires it. Some common issues reported as warnings are described below.
Some common issues reported as warnings are described below.
*_meta
as a hit counters
Do not use meta (post_meta
, comment_meta
, etc.) to track counts (e.g. votes, page views, etc.). It is not performant and will not work as expected due to caching and race conditions on high-volume sites.
In general, avoid counting or tracking user events within WordPress. Instead, consider using a JavaScript-based solution paired with a dedicated analytics service (such as Google Analytics).
Check for is_array()
, !empty()
or is_wp_error()
Before using a function that depends on an array, always check to make sure the arguments you are passing are arrays. If not, PHP will throw a warning.
Recommended example:
$tags_array = get_the_terms( get_the_ID(), 'post_tag');
//get_the_terms function returns array of term objects on success, false if there are no terms or the post does not exist, WP_Error on failure. Thus is_array is what we have to check against
if ( is_array( $tags_array ) ) {
$tags = wp_list_pluck( $tags_array , 'name');
}
Not recommended:
$tags = wp_list_pluck( get_the_terms( get_the_ID(), 'post_tag') , 'name');
Common functions/language constructs that are used without checking the parameters beforehand: foreach()
, array_merge()
, array_filter()
, array_map()
, array_unique()
, wp_list_pluck()
Always check the values passed as parameters or cast the value as an array before using them.
Commented out code, debug code or output
Application code should not contain debug code and should not output debugging information. That includes the use of functions that provide backtrace information, such as wp_debug_backtrace_summary()
or debug_backtrace()
.
The use of commented-out code should be avoided. Having code not ready for production on production could easily lead to mistakes while reviewing, such as accidentally publishing commented-out code.
Cron events scheduled less than 15 minutes apart or expensive events
Overly frequent cron events (anything less than 15 minutes) can significantly impact the site’s performance, as can cron-expensive events.
Custom API endpoints without permissions callback
Register a valid permissions callback for custom API routes that write or read private data to avoid data exposure or manipulation.
Custom roles
Custom user roles and capabilities must be managed via our helper functions for the best compatibility between environments and for added security.
Custom wp_mail
headers
To create custom headers using user-supplied data (e.g. “FROM” header), always use WordPress’s filters, such as wp_mail_from()
and wp_mail_from_name()
Direct database queries
Thanks to the WordPress API, it is rare to need to query database tables directly. Using WordPress APIs rather than rolling custom functions ensures compatibility with past and future versions of WordPress and PHP. It also makes code reviews go more smoothly because we know we can trust the APIs. Visit our page covering database queries for more information.
Additionally, direct database queries bypass internal caching. If necessary, you should evaluate the potential performance of these queries and add caching if needed. Any queries modifying database contents may cause sync problems with the object cache.
Encoding values used when creating a URL or passed to add_query_arg()
Values passed to add_query_arg()
expected to be encoded with urlencode()
or rawurlencode()
to prevent parameter hijacking.
For example, passing the following code:
$m_yurl = 'admin.php?action=delete&post_id=321';
$my_url = add_query_arg( 'my_arg', 'somevalue&post_id=123', $my_url );
You would expect the url to be:admin.php?action=delete&post_id=321&somevalue%26post_id%3D123
But in fact it becomes:admin.php?action=delete&post_id=321&somevalue&post_id=123
eval()
and create_function()
Both these functions can execute arbitrary code constructed at run time, which can be created through difficult-to-follow execution flows. These methods can make your site fragile because unforeseen conditions can cause syntax errors in the executed code, which becomes dynamic. A much better alternative is an Anonymous Function, which is hardcoded into the file and can never change during execution.
If there are no other options than to use this construct, pay special attention not to pass any user-provided data into it without properly validating it beforehand.
Expensive 404 pages
The 404 page must be one of the fastest on the site, as it is only cached for 10 seconds. A traffic spike from a broken link can cause performance and availability problems if the 404 page is doing expensive database lookups.
Flash (.swf) files
Flash (.swf) files are not advisable on the WordPress VIP Platform, as they often present security vulnerabilities that are difficult to detect and secure (due to poor development practices or bugs in the Flash Player).
Front-end database writes
Avoid functions that write to the database on the front end. Front-end database writes typically will not work as expected due to page caching and can cause a significant load on the database.
Functions that use JOINS, taxonomy relation queries, -cat, -tax queries, subselects or API calls
Evaluate the following queries closely to avoid performance issues. They are often expensive, causing known problems with large datasets:
category__and
,tag__and
,tax_query
withAND
category__not_in
,tag__not_in
, andtax_query
withNOT IN
tax_query
with multiple taxonomiesmeta_query
with a large result set (e.g. looking for only posts with athumbnail_id
meta on a large site, looking for posts with a specific meta value on a key)
Generating email
To prevent issues with spam, abuse, or other unwanted communications, application code should not generate (or allow users to generate) email messages to site users or user-supplied email addresses. To send small amounts of email to admins or a specific email address, use the built-in WordPress wp_mail()
function. To send large amounts of email (e.g. mailing lists, invitations to view or share content, notifications of site activity, or other messages generated in bulk) a third-party SMTP or ESP (Email Service Provider) services should be used instead.
Including files with untrusted paths or filenames
locate_template()
, get_template_part()
, and sometimes include()
or require()
are typically used to include templates. If your template name, file name, or path contains non-static data or can be filtered, you must validate it against directory traversal using validate_file()
or by detecting the string “..”.
Minified JavaScript files
Minified JavaScript files should be committed with changes to their unminified counterparts. Minified files cannot be read for review and are much harder to work with when debugging issues.
Mobile Detection
When targeting mobile visitors, use jetpack_is_mobile()
instead of wp_is_mobile()
. It is more robust and works better with full-page caching.
function jetpack_is_mobile( $kind = 'any', $return_matched_agent = false )
Where kind
can be:
- smart
- dumb
- any
You can also use:
Jetpack_User_Agent_Info::is_ipad()
Jetpack_User_Agent_Info::is_tablet()
Views from mobile devices are cached, and to make sure things work as expected, please let us know before including any additional server-side logic (PHP) in your code.
These are loaded automatically.
No LIMIT
queries
Using posts_per_page
(or numberposts
) with the value set to -1
or an unreasonably high number, or setting nopaging
to true
opens up the potential for scaling issues if the query ends up querying thousands of posts.
You should always fetch the lowest possible number that still gives acceptable results. Imagine that your site grows over time to include 10,000 posts. If you specify -1
for posts_per_page
, you’ll query with no limit and fetch all 10,000 posts every time the query runs, destroying your site’s performance. If you know you’ll never have more than 15 posts, then set posts_per_page
to 15. If you think you might have more than 15 but doubt it’d hit more than 100, set the limit to 100. If it gets much higher than that, you might need to reconsider the page architecture.
Not using the settings API
Use the WordPress Settings API instead of handling the output of settings pages and storage yourself. It handles much of the heavy lifting for you, including added security.
Make sure to validate and sanitize submitted values from users, using the sanitize
callback in the register_setting
call.
Prefixing functions, constants, classes, and slugs
The best practice in WordPress is to prefix everything, including less obvious things like post_type
and taxonomy slugs, and cron event names, and typical things like names of functions, constants, and classes.
Plugin registration hooks
register_activation_hook()
and register_deactivation_hook()
are not supported because of the way plugins are loaded on WordPress VIP using wpcom_vip_load_plugin()
.
reCaptcha for share by email
Jetpack’s share by email feature (aka Sharedaddy) must be implemented with reCaptcha to protect against the WordPress.com network being seen as a source of email spam, which would adversely affect VIP sites.
Relative file includes
Including files with relative paths may lead to unintended results. It’s recommended that all files are included with an absolute path.
Several functions are available to compose the file path, such as __DIR__
, dirname( __FILE__ )
, plugin_dir_path( __FILE__ )
, or get_template_directory()
.
// Don't do this:
require_once 'file.php';
// Do this instead:
require_once __DIR__ . '/file.php';
Remote calls
Remote calls, such as fetching information from external APIs or resources, should rely on the WordPress HTTP API (no cURL) and be cached. Examples of remote calls that should be cached: wp_remote_get()
, wp_safe_remote_get()
and wp_oembed_get()
.
Removing the admin bar
Removing the WordPress admin toolbar is strongly discouraged, and it must not be removed for administrators or vip_support
roles.
Return values not checked
When defining a variable through a function call, always check the function’s return value before calling additional functions or methods using that variable.
function wpcom_vip_meta_desc() {
$text = wpcom_vip_get_meta_desc();
if ( ! empty( $text ) ) {
echo "<meta name='description' content='" . esc_attr( $text ) . "' />";
}
}
Safelisting values for input/output validation
When working with user-submitted data, try to accept data only from a finite list of known and trusted values where possible. For example:
$possible_values = array( 'a', 1, 'good' );
if ( ! in_array( $untrusted, $possible_values, true ) )
die( "Don't do that!" );
Serializing data
VIP recommends JSON for serializing data. Unserialize has known vulnerability problems with Object Injection.
Skipping full-page caching
Page caching by the edge cache servers improves performance by serving end-users a page directly from the nearest datacenter. GET parameters are always cached individually per GET parameter. The page cache will only respect the Vary
headers for X-Country-Code
and Accept
, but not forCookie
.
switch_to_blog()
For multisite instances, switch_to_blog()
only switches the database context, not the code that would run for that site (e.g. different filters).
Taxonomy queries that do not specify 'include_children' => false
Almost all taxonomy queries include 'include_children' => true
by default. This can have a very significant performance impact on code, and in some cases, queries will time out. We recommend adding 'include_children' => false
to all taxonomy queries when possible.
In many instances where all posts in either a parent or child term are wanted, query for the parent term and use a save_post()
hook to determine whether a child term is added and if so, enforce that its parent term is also added. A one-time WP-CLI command might be needed to ensure previous data integrity.
Uncached functions
Not all WordPress functions that interact with the database are cacheable. To ensure high performance and stability, please avoid using any of the functions on our Uncached Functions list.
Use wp_json_encode()
over json_encode()
wp_json_encode()
will ensure the string is valid UTF-8, while the regular function will return false
if it encounters invalid UTF-8. It also supports backward compatibility for versions of PHP that do not accept all the parameters.
Use wp_parse_url()
instead of parse_url()
In PHP versions lower than 5.4.7, schemeless and relative URLs would not be parsed correctly by parse_url()
. We recommend using wp_parse_url()
for backward compatibility.
Use wp_safe_redirect()
instead of wp_redirect()
Use wp_safe_redirect()
along with the allowed_redirect_hosts
filter to avoid malicious redirects within code, and call exit()
after a redirect to prevent executing unwanted code.
Using __FILE__
for page registration
When adding menus or registering your plugins, use a unique handle or slug other than __FILE__
to avoid revealing system paths.
Using ==
instead of ===
PHP handles type juggling. Meaning that this:
$var = 0;
if ( $var == 'safe_string' ){
return true;
}
Will return true
. VIP recommends using ===
over ==
.
Other interesting things that are equal are:
(bool) true == 'string'
null == 0
0 == '0SQLinjection'
1 == '1XSS'
0123 == 83
(here0123
is parsed as an octal representation)0xF == 15
(here0xF
is parsed as a hexadecimal representation of a number)01 == '1string'
0 == 'test'
0 == ''
Using $_REQUEST
$_REQUEST
does not identify the data location (such as POST
, or GET
, or a cookie), potentially introducing sneaky and hard-to-find bugs and causing difficulty reviewing code. A much better practice is to be explicit and use either $_POST
or $_GET
instead.
Using bloginfo()
without escaping
Use get_bloginfo()
instead of bloginfo()
so that data can be properly late escaped on output. get_bloginfo()
can also return multiple data types, be used in multiple places, and may need to be escaped with many different functions depending on the context. For example:
echo '<a href="' . esc_url( get_bloginfo( 'url' ) ) . '">' . esc_html( get_bloginfo( 'name' ) ) . '</a>';
echo '<meta property="og:description" content="' . esc_attr( get_bloginfo( 'description' ) ) . '">';
Using closing PHP tags
As part of the WordPress Coding Standards, all PHP files should omit the closing PHP tag to prevent an accidental output of whitespace and other characters, which can cause issues such as Headers already sent
errors.
Using extract()
Never use extract()
; it is too opaque and difficult to predict its behavior under various inputs. It is too easy to unknowingly introduce new variables into a function’s scope, leading to unintended and difficult debugging conflicts.
Using page templates instead of rewrites
A common “hack” in the WordPress community when requiring a custom feature to live at a vanity URL (e.g.,/lifestream/) is to use a Page + Page Template. This isn’t ideal for numerous reasons:
- It requires multiple queries to handle the lookup for the page and any additional manual loops.
- The page must be manually created in each environment and new developer machine, which impedes development workflow.
Last updated: July 11, 2024