VIP Go local development – Windows

For non-Windows-specific development, please see our local development documentation.

Everything in this task list assumes you have administrative access to the host machine. For Windows 10 hosts, any command prompt, PowerShell, VS Code instance which is run must be Run as Administrator.

  1. On Windows 10, install Git for Windows and the OpenSSH client.
  2. Enable the OpenSSH Authentication Agent service for automatic starting, which is disabled by default. Then add the SSH key associated with your Github account to the agent via PowerShell or command prompt. Create a key first if neccessary, then run:
    ssh-add {path_to_public_key}
  3. Configure Git to use OpenSSH, and increase the size of Git packages and buffers to allow large file and pack transfers. Add the following settings to your user profiles .gitconfig, the sections are listed, do not create duplicate sections, append if needed.
        autocrlf = true
        sshCommand = C:\\\\Windows\\\\System32\\\\OpenSSH\\\\ssh.exe
        packedGitLimit = 128m
        packedGitWindowSize = 128m
        deltaCacheSize = 128m
        packSizeLimit = 128m
        windowMemory = 128m
        postBuffer = 1024m
  4. Run the VVV installation. If you run into issues installing, try a previous, known good VirtualBox version. Follow through running vagrant up for the first time, additional sites can be added later.Later instructions will refer to the base installation folder for VVV, where you checked it out, as {vvv_path}.
  5. Create a new VVV site definition inside config/config.yml for the site with the custom site template, for instance this example creates a site at https://vipgo.test:
        - vipgo.test
        wp_type: subdirectory
          WP_ALLOW_MULTISITE: true
          MULTISITE: true
          SUBDOMAIN_INSTALL: false
          DOMAIN_CURRENT_SITE: "vipgo.test"
          PATH_CURRENT_SITE: "/"
          WP_DEBUG: true
          WP_DEBUG_LOG: true
          WP_DEBUG_DISPLAY: true
          SCRIPT_DEBUG: true
          VIP_GO_APP_ENVIRONMENT: true
          DISALLOW_FILE_EDIT: true
          DISALLOW_FILE_MODS: true

    Enable helpful utilities by modifying the config files utilities section:

        - memcached-admin
        - opcache-status
        - phpmyadmin
        - webgrind
        - trusted-hosts
        - tls-ca

    Finally, increase the resources, and optionally add a static IP to your instance. In my case, my VirtualBox network uses, and I am assigning the instance the IP Remove the private_network_ip and network_ip lines if not needed.

      memory: 4096
      cores: 2
  6. Copy the database dump SQL file you have to database/sql/backups directory, and change the name to match the site definition name, in this example vipgo. If you do this before you reprovision, it will load this dump file when provisioning as the sites database. You can do a find/replace on the file before loading (may be slow with a large file), or use WP CLI inside the instance to change the domain names.
  7. Reprovision the instance to create the new site:
    vagrant reload --provision
  8. Add an .htaccess file to support the multisite instance in {vvv_path}/www/vipgo/public_html/:
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ - [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
    RewriteRule . index.php [L]
  9. The VIP repository is hosted in the sites wp-content directory, you need to empty it first. In this example, find it at {vvv_path}/www/vipgo/public_html/wp-content/.
  10. Clone the sites VIP go instance repository, URL of {instance_url} the folder:
    git clone {instance_url} --recursive {vvv_path}/www/vipgo/public_html/wp-content
  11. Pull all of the VIP Go MU plugins into your VVV instance. Note: this only works if you have a SSH key associated with a Github account. If there are any failures, please check your Git configuration accepts the larger pack and buffer sizes specified above.
    git clone --recursive {vvv_path}/www/vipgo/public_html/wp-content/mu-plugins/
  12. Add the VIP Go configuration from your site repository into the VVV site configuration located at {vvv_path}/www/vipgo/public_html/wp-config.php. Locate the comment \/* That’s all, stop editing! Happy blogging. *\/ and place this block above it:
    if ( file_exists( __DIR__ . '/wp-content/vip-config/vip-config.php' ) ) {
        require_once( __DIR__ . '/wp-content/vip-config/vip-config.php' );
  13. If you run into any issues with missing plugins, either mu-plugins or from your site repository, you likely need to pull the Git submodule. To do this, clear the plugin directory then reinitialize the plugin. For instance, assume the plugin name is plugins/polldaddy, and from the command prompt run:
    git submodule update --init plugins/polldaddy
  14. Perform a WP CLI search-replace to update domain names and links. In this example, assume the initial domain name is prod-vip.go, which will be replaced with vipgo.test. From a command prompt, in a directory inside the {vvv_path}, SSH into the Vagrant instance:
    vagrant ssh

    Then switch to the site path, in this example, /srv/www/vipgo/public_html/, and run the following search-replace:

    wp search-replace 'prod-vip.go' 'vipgo.test' --recurse-objects --all-tables --skip-tables=wp_*users
  15. If you don’t already have a functional user for the site, you can add one using WP CLI, again via Vagrant SSH in the example directory of /srv/www/vipgo/public_html/:
    wp user create bobross happy@little.trees --role=administrator
    wp super-admin add bobross

    Make sure you record the password printed to the console so you can successfully login.

  16. In order to use VVV certificates and avoid security warnings, you need to add the certificate authority as a Trusted Root CA. You can find instructions to update your host system here. On Windows 10, this is done from a command prompt on the host using the following command:
    certutil -enterprise -f -v -AddStore "Root" "certificates/ca/ca.crt"
  17. For Windows 10 users hosting on VirtualBox and needing to cross-browser test in Microsoft Edge, there is currently an issue with connecting to the VirtualBox Host Adapter. You can disable this by modifying the *NdisDeviceType registry key of the adapter, and setting the value to 0 from 1. You need to identify the correct adapter, referenced in the following path by {adpater_id}:

    Reboot after changing this setting, make sure you vagrant up, then test. Do this at your own risk.


Thanks to Ryan Leeson of Trellist for the work in putting this guide together.

Managing User Access

We encourage all VIP clients to manage user access to their websites and GitHub repositories. It’s best to designate primary administrators who will be in charge of adding and removing users. We recommend at least one primary administrator for each client, two would be even better in case one of them is unavailable. Primary administrators should feel empowered to add additional administrators as they see fit while following best practices.

Managing GitHub Access

A collaborator with admin privileges in your GitHub repository will be able to add or remove additional collaborators. Access to GitHub also authenticates access to the VIP Dashboard. Find instructions in GitHub’s Documentation. There are a few points to keep in mind when deciding on permissions for collaborators:

  • Please only add users with read or write permissions as required. Users who would need write access include those who will be committing code to the repository.
  • Admin collaborators can force push (which is blocked in VIP Go environments) after removing restrictions. If they do this, they should restore restrictions afterwards.
  • Please review the full description of every permission level in GitHub’s documentation.

Add a User to Your VIP Go Site

As an Administrator, you are able to add users to your website through wp-admin (also known as the WordPress Dashboard). Add users to your site using the default WordPress user roles, or customized roles unique to your business needs:

  1. Log into your VIP Go site to access wp-admin.
  2. From the sidebar, click on Users.
  3. Next click the Add New button to add a new user to your site.
  4. Fill out the form and select the role that your user should be assigned.
    • Check the Send User Notification option if you want the user to receive an email with a password-set link. If you do not select this option, the user will need to access the login URL ( and use the password reset feature to generate a password.
  5. That’s it! You did it!

Add a User to Your VIP Go Multisite

In a multisite, user management is done at the network (top) level. Only a Super Admin can add users to the network. Once users are added at the network level, Administrators for individual subsites can then add any of these existing users as required.

If you’d like to add a user to your VIP Go multisite as a Super Admin, follow the steps outlined here:

  1. Log into your VIP Go multisite to access wp-admin.
  2. In the admin bar, at the top, hover over My Sites > Network Admin > Users and then click on Users. Adding a user to a multisite through network admin
  3. Next, click the Add New button to add a new user to your site.
  4. Fill out the form. Note this will only prompt you for a username and email address. A password reset link will be sent to the user via email by default.
  5. Now that you’ve added the user, you will be able to edit the user profile. Click Edit user at the top of the screen. (You can also go back to the Users list in Network Admin and hover over the user.)
  6. Check the box that says Grant this user super admin privileges for the Network.
  7. Scroll down to the end of this page and click Update User. Now the user has Super Admin privileges for the entire multisite.
  8. Note, if you ever need to delete this user, you must first remove the Super Admin privileges. To do that, go into the user’s profile, unselect the super admin privileges option, and then update.

Testing your site

When you test your site, we recommend you run all tests on your production environment. Walk through your site using all the functionality of your dashboard, including plugins that you and your team will use on a regular basis. This could include creating test posts and widgets.

In addition to testing backend functionality, you’ll want to look at the frontend to ensure it functions as expected. If anything appears broken or is not working as expected, you might want to take a deeper look at your output and see which errors or warnings need to be addressed.

At a minimum, we recommend the following  tests:

  • Create a post as a user with the “editor” role
  • Create a post as a user with the “author” role
  • Upload an image to the media library
  • Edit a post
  • Delete a post
  • Create a new user
  • Delete a user
  • Change a user’s role
  • Add a widget
  • Modify a widget
  • Verify settings are correct for external services like Google Analytics, Twitter, Facebook, etc.
  • Any features of your editorial workflow that rely on plugins or theme functionality
  • 301 redirects, if any, still work

JavaScript security best practices

The primary vulnerability we need to be careful of in JavaScript is Cross Site Scripting (XSS). In WordPress with PHP, we use escaping functions to avoid that — esc_html(), esc_attr(), esc_url(), etc. Given that, it only seems natural that we would also need to escape HTML in JavaScript.

As it turns out out, however, that’s the wrong way to approach JavaScript security. To avoid XSS, we want to avoid inserting HTML directly into the document and instead, programmatically create DOM nodes and append them to the DOM. This means avoiding .html(), .innerHTML, and other related functions, and instead using .append(), .prepend(), .before(), .after(), and so on.

Here is an example:

    url: ''
}).done( function( data ) {
    var link = '<a href="' + data.url + '">' + data.title + '</a>';

    jQuery( '#my-div' ).html( link );

This approach is dangerous, because we’re trusting that the response from includes only safe data – something we can not guarantee, even if the site is our own. Who is to say that data.title doesn’t contain alert( "haxxored");;?

Instead, the correct approach is to create a new DOM node programmatically, then attach it to the DOM:

    url: ''
}).done( function( data ) {
    var a = jQuery( '<a />' );
    a.attr( 'href', data.url );
    a.text( data.title );

    jQuery( '#my-div' ).append( a );

Note: It’s technically faster to insert HTML, because the browser is optimized to parse HTML. The best solution is to minimize insertions of DOM nodes by building larger objects in memory, then insert it into the DOM all at once, when possible.

By passing the data through either jQuery or the browser’s DOM API’s, we ensure the values are properly sanitized and remove the need to inject insecure HTML snippets into the page.

To ensure the security of your application, use the DOM APIs provided by the browser (or jQuery) for all DOM manipulation.

Escaping Dynamic JavaScript Values

When it comes to sending dynamic data from PHP to JavaScript, care must be taken to ensure values are properly escaped. The core function esc_js() helps escape JavaScript for us in DOM attributes, while all other values should be encoded with json_encode().

From the WP Codex on esc_js():

It is intended to be used for inline JS (in a tag attribute, for example onclick=”…”).

If you’re not working with inline JS in HTML event handler attributes, a more suitable function to use is json_encode, which is built-in to PHP.

This approach is incorrect:

var title = '<?php echo esc_js( $title ); ?>';
var url = '<?php echo esc_js( $url ); ?>';

Depending on the context, it’s better to use rawurlencode() (note that it adds the quotes automatically) or wp_json_encode() in combination with esc_url():

var title = decodeURIComponent( '<?php echo rawurlencode( (string) $title ); ?>' )
var url   = <?php echo wp_json_encode( esc_url( $url ) ) ?>;

For more examples, please see our code sample repository.

Stripping Tags

It may be tempting to use .html() followed by .text() to strip tags – but this approach is still vulnerable to attack:

// Incorrect
var text = jQuery('
<div />').html( some_html_string ).text();
jQuery( '.some-div' ).html( text );

Setting the HTML of an element will always trigger things like src attributes to be executed, which can lead to attacks like this:

// XSS attack waiting to happen
var some_html_string = '<img src="a" onerror="alert('haxxored');" />';

As soon as that string is set as a DOM element’s HTML (even if it’s not currently attached to the DOM!), src will be loaded, will error out, and the code in the onerror handler will be executed…all before .text() is ever called.

The need to strip tags is often indicative of bad practices – remember, always use the appropriate API for DOM manipulation.

// Correct
jQuery( '.some-div' ).text( some_html_string );

Here are some things to watch out for:

// jQuery passes the values in these methods through to eval():
$('#my-div').after('<script>alert("1. XSS Attack with jQuery after()");</script>');
$('#my-div').append('<script>alert("2. XSS Attack with jQuery append()");</script>');
$('#my-div').before('<script>alert("3. XSS Attack with jQuery before()");</script>');
$('#my-div').html('<script>alert("4. XSS Attack with jQuery html()");</script>');
$('#my-div').prepend('<script>alert("5. XSS Attack with jQuery prepend()");</script>');
$('#my-div').replaceWith('<div id="my-div"><script>alert("6. XSS Attack with jQuery replaceWith()");</script></div>');

// And these:
$('<script>alert("7. XSS Attack with jQuery appendTo()");</script>').appendTo('#my-div');
$('<script>alert("8. XSS Attack with jQuery insertAfter()");</script>').insertAfter('#my-div');
$('<script>alert("9. XSS Attack with jQuery insertBefore()");</script>').insertBefore('#my-div');
$('<script>alert("10. XSS Attack with jQuery prependTo()");</script>').prependTo('#my-div');
$('<div id="my-div"><script>alert("11. XSS Attack with jQuery replaceAll()");</script></div>').replaceAll('#my-div');

// Plain JS will also evaluate these:
document.write('<script>alert("12. XSS Attack with document.write()");</script>');
document.writeln('<script>alert("13. XSS Attack with document.writeln()");</script>');

// Script elements inserted using innerHTML do not execute when they are inserted:
var div = document.createElement('div');

div.innerHTML = 'Foo<script>alert("No attack with script innerHTML");</script>';
div.innerHTML = 'Foo<span></span><script defer>alert("No attack with deferred script innerHTML");</script>';
div.innerHTML = '<scr' + 'ipt>alert("No attack with concat script tag innerHTML");</script>';

// JS prepend() and JS append() are safe when using strings:
var div2 = document.createElement('div');

div2.prepend('<script>alert("No attack with JS prepend()");</script>');
div2.append('<script>alert("No attack with JS append()");</script>');

// ...But not safe when inserting a Node:
var div3 = document.createElement('div');

var newScript = document.createElement("script");
var inlineScript = document.createTextNode("alert('14. XSS Attack with JS append()');");

// Other ways to use innerHTML can cause XSS though:
var div4 = document.createElement('div');

var myImageSrc = 'x" onerror="alert(\'15. XSS Attack with img innerHTML\')';
div.innerHTML = '<img src="' + myImageSrc + '">';

You can run this JSBin to see it in action.

Using encodeURIComponent()

When using values as part of a URL, for example when adding parameters to a URL or building a mailto: link the JavaScript variables need to be encoded to be correctly interpreted by the browser. Using encodeURIComponent() will ensure that the characters you use will be properly interpreted by the browser. This also helps prevent some trickery such as adding & which may cause the browser to incorrectly interpret the values you were expecting. You can find more information on this on the OWASP website.

Using DOMPurify

As mentioned above, using jQuery’s .html() function or React’s dangerouslySetInnerHTML() function can open your site to XSS vulnerabilities by treating arbitrary strings as HTML. These functions should be avoided whenever possible. While it’s easy to think content from your own site is “safe,” it can become an attack vector if a user’s account is compromised or if another part of the application is not doing enough validation.

While we recommend that first you try to use structured data and build the HTML inside the JavaScript, that’s not always feasible. If you do need to include HTML strings inside your JavaScript, we recommend using the DOMPurify package to sanitize strings to remove executable elements that could contain attack vectors. This is very similar to how WP_KSES works.

To use DOMPurify you need to include it as such:

 * For Browsers
import DOMPurify from 'dompurify';
// or
const DOMPurify = require('dompurify');

 * For Node.js we need JSDOM's window
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom'); 
const window = (new JSDOM('')).window;
const DOMPurify = createDOMPurify(window);

You can then call it as such:

const clean = DOMPurify.sanitize(dirty);

Here are a few examples taken from the DOMPurify Readme:

DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); // becomes <img src="x">
abc<iframe/\/src=jAva&amp;Tab;script:alert(3)>def'); // becomes 



<math><mi//xlink:href="data:x,<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3Ealert(4)%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&amp;lt;script&amp;gt;" title="&amp;lt;script&amp;gt;" />">'); // becomes 






</TABL>'); // becomes 

// =>








<li><A HREF=//>click</UL>

'); // becomes 


<li><a href="//">click</a></li>



Other Common XSS Vectors

  • Using eval(). Never do this.
  • Un-safelisted / un-sanitized data from URLs, URL fragments, query strings, cookies.
  • Including untrusted / unreviewed third-party JavaScript libraries.
  • Using outdated / unpatched third-party JavaScript libraries.

Helpful Resources

Database queries

Direct database queries should be avoided wherever possible. Instead, it’s best to rely on WordPress API functions for fetching and manipulating data.

Of course this is not always possible, so if any direct queries need to be run here are some best practices to follow:

  • Use filters to adjust queries to your needs. Filters such as posts_where can help adjust the default queries done by WP_Query. This helps keep your code compatible with other plugins. There are numerous filters available to hook into inside /wp-includes/query.php.
  • Make sure that all your queries are protected against SQL injection by making use of $wpdb->prepare and other escaping functions like esc_sql and like_escape.
  • Try to avoid cross-table queries, especially queries which could contain huge datasets such as negating taxonomy queries like the -cat option to exclude posts of a certain category. These queries can cause a huge load on the database servers.
  • Remember that the database is not a tool box. Although you might be able to perform a lot of work on the database side, your code will scale much better by keeping database queries simple and performing necessary calculations and logic in PHP.
  • Avoid using DISTINCT, GROUP, or other query statements that cause the generation of temporary tables to deliver the results.
  • Be aware of the amount of data you are requesting. Make sure to include defensive limits.
  • When creating your own queries in your development environment, be sure to examine the query for performance issues using the EXPLAIN statement. Confirm indexes are being used.
  • Don’t JOIN the users table.
  • Cache the results of queries where it makes sense.

ACF 5 and VIP Go

Advanced Custom Fields (ACF) is a popular plugin that many VIP clients choose to use on their sites. However, ACF is not available for clients on the VIP platform, and has not undergone a full, line-by-line review for use on VIP Go. Clients wishing to use ACF on VIP Go accept the security and performance risks of using it. This page outlines some additional steps needed to make ACF more secure and performant, but should not be interpreted as the equivalent of a line-by-line review, or VIP’s approval of the plugin.

Please note that while ACF 5 can be used on most VIP Go sites, ACF 4 is unavailable. If in doubt about whether ACF is allowed for use on your VIP site, please contact us.

When using ACF 5 and ACF 5 Pro, several additional steps are needed in order to make ACF secure, and avoid performance issues:

  • Hide the Admin UI
  • Define fields in PHP
  • Use taxonomies for searchable fields
  • Avoid the_field and escape
  • Secure fields that allow arbitrary output

Hide the ACF Admin UI

The fields UI can be used to add arbitrary fields, including unsafe fields. For example it can allow an admin to display the passwords of users. You can disable the UI using this filter:

add_filter('acf/settings/show_admin', '__return_false');

Define Fields in PHP

In order to make sure that all ACF usage is secure, define the fields in PHP or local json, rather than at runtime. This way they remained versioned and safe. This can be done via the import export menu of a local developer environment to setup the fields available and export them to PHP.

Documentation on how to do this can be found here on the ACF website.

Alternatively, fields can be defined via the local JSON feature as described here, but keep in mind that saving local JSON will not work in production as the filesystem is read only, nor is it desirable as it would bypass the security benefits.

Being Mindful of Taxonomy Term Storage

If an ACF field is going to be queried, filtered, or searched for in a post query, use the taxonomy data checkbox so that the field is stored as a term, not a post meta value. This ensures performance is not impacted by expensive meta queries on the frontend

the_field and Escaping

the_field has no context as to when or where it is called. So how is it to know if it should be using esc_url, esc_attr or wp_kses_post? It doesn’t, which makes it dangerous from a security point of view. Instead, use get_field in combination with an escaping function, e.g.

$url = get_field( 'custom_link' );
echo esc_url( $url );

Flexible content is the exception to this, and should be clearly marked on usage via comments

Fields That Use Arbitrary Output

If the field types that allow arbitrary output are to be used, they must be accounted for in the acf/format_value and equivalent filters such as acf/format_value/type=textarea.

For example:

function vip_make_acf_text_areas_safe( $value, $post_id, $field ) {
	return wp_kses_post( $value );

add_filter('acf/format_value/type=textarea', 'vip_make_acf_text_areas_safe', 10, 3);

This way different escaping can be applied via different `$field` values. Alternatively, if all fields of that type use the same escaping, this can be done instead:

add_filter('acf/format_value/type=textarea', 'wp_kses_post', 10, 1);

For more information, see:

The File System on VIP Go

The VIP Platform has been designed with for high-scale performance and security. One of the ways we do that is to run all web servers in read-only mode. This helps protect applications on the platform against many common form of attack (e.g. vulnerabilities that allow installation of backdoor shells and other malicious files); allows for automated, high-capacity horizontal scaling; and dynamic image resizing and manipulation.

Media Uploads

The VIP Platform stores all uploaded media in a globally distributed object store called the VIP Files Service. This is seamlessly integrated with your WordPress application and all common operation including uploads, cropping, editing, and deletes work as expected.

All images uploaded to the VIP Files Service can make use of the Photon API, which allows for dynamic resizing and other manipulation (cropping, letterboxing, filters, etc.). Because image sizes all are created on-the-fly, uploads are significantly faster and changes to image sizes is much easier (no need to run scripts to regenerate files).

Files uploaded to the Files Service are treated as case-insensitive.

Programmatic Access to Media Uploads

For programmatic access to media stored in the VIP Files Service, such as uploading and modifying files, you can use several different APIs.

For simple file uploads, the media_handle_sideload, media_sideload_image, or wp_upload_bits functions are very easy to work with.

For more complex interactions, we’ve integrated a custom PHP Stream Wrapper, which allows most filesystem functions (e.g. fopen, fwrite, file_put_contents, etc.) to work with media uploads. This means that most plugins that interact with media uploads will work on the VIP Platform without any modifications. There are some caveats

A few caveats to be aware of:

  • This only works for the uploads directory (i.e. paths that begin with /wp-content/uploads/; other paths will not work).
  • We default to a year/month directory structure for media uploads. This can be overridden by adding the following filter to your theme’s functions.php file:
    add_filter( 'pre_option_uploads_use_yearmonth_folders', function() {return '0';}, 9999 );
  • You must use the WordPress function wp_get_upload_dir() or wp_upload_dir() to generate the correct, writeable path (hard-coding a path like /wp-content/uploads/... will not work).
  • Currently, not all filesystem functions and operations are supported (e.g. directory traversal).
  • Not all use cases are ideal or supported (e.g. files with high volumes of updates, such as logging).
  • Because communication with the VIP Files Service happens over HTTP, high number of function calls (e.g. lots of file_exists checks) can result in a performance hit and should be used with caution.
  • file_exists will always return true for extension-free values (e.g. directory names).


Here’s a simple example of how to upload a file using PHP functions:

$csv_content = '1,hello,admin';

$upload_dir = wp_get_upload_dir()['basedir'];

$file_path = $upload_dir . '/csv/updated.csv';

file_put_contents( $file_path, $csv_content );

// The file will now be accessible at <a href=""></a>

Here’s an example that parses an uploaded CSV and stores the contents in a variable:

$csv_attachment = get_attached_file( 1234 );

$csv_file = file( $csv_attachment );

$csv_content = array_map( 'str_getcsv', $csv_file );

Local File Operations

There may be cases where you need to perform local manipulations to temporary files (e.g. download a zip, extract its contents, and then upload them to WordPress). For these cases, you can use the system temp directory, which is the only local, writeable path on web servers on the VIP Platform.

A few things to keep in mind:

  • Use get_temp_dir() to find the right path. Or if you need to generate a temporary file, use wp_tempnam().
  • You should clean up and remove all files after they’re no longer needed. Use unlink() to remove those files.
  • Files and directories can only be relied on for the duration of the current request.
    • Each request may be served by a different container and the sequence of requests from a given user are not guaranteed to be served by the same container.
    • Containers are transient and may be created and destroyed due to autoscaling.

If you have any questions about the VIP Files Service or working with the filesystem on the VIP Platform, please don’t hesitate to get in touch with the VIP team so we can work with you on the implementation.

VIP Go and the WordPress REST API– recommendations and requirements

This document describes how your usage of the WordPress REST API can be structured to ensure your application and your WordPress installation are as performant and stable as possible.

“Front-end API requests”: WordPress REST API requests used to generate the front end of the site in some application, e.g. requests made by a NodeJS app which is generating the front end, or from a mobile application, etc.

“WordPress REST API responses”: the response of the WordPress REST API to requests.

General guidelines

Our guidelines and requirements around front-end API requests are as follows:

  • The front end application should handle unexpected responses to front-end API requests robustly and gracefully; when the front end application receives an unexpected result (e.g. because network conditions prevent the request reaching the API or because of a bug in the API code) it should accommodate this eventuality, e.g. serve up stale content from an object cache within the front end application, show an appropriate error page, back off in request frequency to the API endpoint, etc.
  • Only authenticated front-end users should generate authenticated front-end API requests; authenticated requests bypass the VIP Go Varnish cache, authenticating any proportion of front-end API requests can easily cause issues with site stability and uptime.

Our guidelines and requirements around WordPress REST API responses are similar to our requirements for WordPress themes, please bear in mind the following:

  • WordPress REST API responses should be fast and performant; your API stability will be strongly correlated to how swiftly your API endpoints can respond to requests (especially for any authenticated requests which will not benefit from Varnish caching). Fast API responses can be achieved using traditional WordPress performance optimisation techniques, such as using object caching to reduce repetitive expensive operations, avoiding external HTTP requests, etc.
  • WordPress REST API responses to front-end API requests should be cached by VIP Go; VIP Go runs a Varnish caching layer, API requests from your front end application should aim to hit this cache to serve the responses efficiently and from a location nearer your users.
  • WordPress REST API responses to front-end API requests should never cause writes; as traffic increases, requests from client site which cause database writes will easily cause issues with site stability and uptime.

For a significant utilization of the WordPress REST API, e.g. replacing the front end of your site with a Node application or a high usage mobile application, we will ask you to talk over the following points with us:

  • What is the caching strategy for your application: what are you caching, how are you caching, how long are the caches held for, and how will the caches be cleared?
  • Typical profile of requests: For each type of view, what requests will your application make when the caches are cold? What requests will be made when the caches are warm?
  • How fast are your API endpoints in responding to common requests used to generate popular views in your application?
  • What is your test plan for your use of the REST API?
  • What is your rollout plan for your use of the REST API (including a rollback plan)?

We are happy to discuss how best to structure your use of the WordPress REST API, please open a ticket and we’ll be happy to help.


To avoid compatibility issues and reduce complexity, we strongly recommend you do not change the rest_url_prefix from wp-json; i.e. do not move the WordPress REST API endpoints from; for example, this can cause issues with various VIP services which utilise the REST API on each VIP Go site.


By default VIP Go will cache most REST API endpoints for 1 minute. We strongly recommend that your site does not change this to use PURGEs, but we are happy to talk through any specific requirements that you have and advise on the best approach.

If you want to adjust the TTL for REST responses, you can use the `wpcom_vip_rest_read_response_ttl` filter:

add_filter( 'wpcom_vip_rest_read_response_ttl', function( $ttl, $response, $rest_server, $request ) {
    // Cache REST API GET requests for 5 minutes.
    return MINUTE_IN_SECONDS * 5;
}, 10, 4 );

Querying on meta_value

There’s a quick rule of thumb to know if querying on meta value will be a problem. Ask yourself:

“Will I be querying for this meta_value using WP_Query?”

If the answer is no, then you’ve got a perfect use case for postmeta values.

If the answer is yes, then the query is likely to have issues with performance and scalability. This is because the WordPress postmeta table has an index on meta_key, but not meta_value.

However, many use cases can be modified to avoid performance problems:

  • Taxonomy Terms – Some meta_value queries can be transformed into taxonomy queries. For example, instead of using a meta_value to filter if a post should be shown to visitors with membership level “Premium”, use a custom taxonomy and a term for each of the membership levels in order to leverage the indexes.
  • Binary Situations – When meta_value is set as a binary value (ex. “hide_on_homepage” = “true”), MySQL will look at every single row that has the meta_key “hide_on_homepage” in order to check for the meta_value “true”. The solution is to change this so that the mere presence of the meta_key means that the post should be hidden on the homepage. If a post shouldn’t be hidden on the homepage, simply delete the “hide_on_homepage” meta_key. This will leverage the meta_key index and can result in large performance gains.
  • Non-binary Situations – Instead of setting meta_key equal to “primary_category” and meta_value equal to “sports”, you can set meta_key to “primary_category_sports”. This enables you to query by primary_category. However, instead of doing get_post_meta( $id, ‘primary_category’), you would need to iterate over possible values of primary_category with get_post_meta( $id, ‘primary_category_sports’). If you need to do both, you could use a ‘primary_category’ and a ‘primary_category_sports’ meta_key that both update when the primary category changes. Another, better, solution for this particular use case would be to use a hidden taxonomy named primary_category and have the categories be the terms.
  • Elasticsearch – If it’s not possible to avoid performing a meta_value query, consider using Elasticsearch instead of MySQL.

One caveat to note is that if you are using Elasticsearch on your site (regardless of if it’s for a particular query or just in general) having multiple distinct meta_keys such as the example of Non-binary situations could potentially cause severe performance problems for Elasticsearch. This is based on the way Elasticsearch will store the data and not how it queries the data and therefore it doesn’t matter if you are using elasticsearch for that particular query or not.

Code review: errors, warnings, and notices

If you have a site that with VIP Application Support, you have the ability to request for code review from the VIP Team.

The goal of our reviews is to make sure that your site will be:

  • Secure, because pushing a site live with insecure code presents a liability to you and your whole userbase;
  • Performant, because going live and finding out that your code can’t handle the traffic levels that your site expects puts most of your launch efforts to waste.

We also review for development best practices to make sure that your site will continue to live on without significant maintenance costs or major issues when WordPress is upgraded.

Before submitting any code for review, please be sure to look through our All About Code Review documentation. The following is a checklist of items VIP looks for during reviewing. Please note that this is a living list and we are adding and modifying it as we continue to refine our processes and platform.

On VIP Go, we typically bucket feedback into three categories:

  • VIP Error – This won’t work or will expose your site to severe performance and security concerns. This category was formerly called a VIP Blocker.
  • VIP Warning – We strongly recommend your team take care of these issues as soon as possible.
  • VIP Notices – Needs to be considered carefully when including them in your VIP theme or plugin.

VIP Errors

Errors are items that if not fixed will likely either not work because of platform incompatibility issues or open your site to serious performance and security issues. We strongly recommend they be fixed before being committed to VIP Go. Here’s a partial list of what can be an Error:

Filesystem Operations

On the VIP Platform, web servers run in read-only mode. File operations are only allowed in the /tmp/ directory and media uploads via the VIP Files Service. For more information please consult

Cache constraints

On VIP Go there are many layers of caching. This means that certain operations may not work as expected. You can learn about the Varnish powered Full Page Cache as well as the object and database caching here: We also have documentation on controlling the VIP Go page cache and on caching for WordPress REST API requests.

Validation, Sanitization, and Escaping

Your code works, but is it safe? When writing code for the VIP Go environment, you’ll need to be extra cautious of how you handle data coming into WordPress and how it’s presented to the end user. Please review our documentation on validating, sanitizing, and escaping.

$_GET, $_POST, $_REQUEST, $_SERVER and other data from untrusted sources (including values from the database such as post meta and options) need to be validated and sanitized as early as possible (for example when assigning a $_POST value to a local variable) and escaped as late as possible on output.

Nonces should be used to validate all form submissions.

Capability checks need to validate that users can take the requested actions.

The save / update handler for new admin pages:

  • Must do a nonce check.
  • Must use a nonce added into the new page output.
  • Must check for user capability.

The save / update handler for new sections on existing core admin pages:

  • Must do a nonce check.
  • Must use the existing _wpnonce or use one added into the new section output.
  • Must check for user capability.

It’s best to do the output escaping as late as possible, ideally as it’s being outputted, as opposed to further up in your script. This way you can always be sure that your data is properly escaped and you don’t need to remember if the variable has been previously validated.

Here are two examples. In order to keep this straight forward, we’ve kept them simple. Imagine a scenario with much more code between the place where $title is defined and where it’s used. The first example is more clear that $title is escaped.

$title = $instance['title'];

// Logic that sets up the widget

echo $before_title . esc_html( $title ) . $after_title;


$title = esc_html( $instance['title'] );

// Logic that sets up the widget

echo $before_title . $title . $after_title;

Inserting HTML directly into DOM with JavaScript

To avoid XSS, inserting HTML directly into the document should be avoided.  Instead, DOM nodes should be programmatically created and appended to the DOM.  This means avoiding .html(), .innerHTML(), and other related functions, and instead using .append(), .prepend(),.before(), .after(), and so on.  More information.

Using uncached functions

WordPress provides a variety of functions that interact with the database, not all of which are cacheable. To ensure high performance and stability, please avoid using any of the functions listed on our Uncached Functions list.

VIP Warnings

We strongly recommend your team take care of these issues as soon as possible. In most circumstances code that falls under this category should not be pushed to a production server unless a specific use makes it acceptable.

Safelisting values for input/output validation

When working with user-submitted data, try where possible to accept data only from a finite list of known and trusted values. For example:

$possible_values = array( 'a', 1, 'good' );
if ( ! in_array( $untrusted, $possible_values, true ) )
die( "Don't do that!" );

Direct Database Queries

Thanks to WordPress’ extensive API, you should almost never need to query database tables directly. Using WordPress APIs rather than rolling your own functions saves you time and assures 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. More information.

Additionally, direct database queries bypass internal caching. If absolutely necessary, you should evaluate the potential performance of these queries and add caching if needed.  Any queries that would modify database contents may also put the object cache out of sync with the data, causing problems.

Arbitrary JavaScript and CSS stored in options or meta

To limit attack vectors via malicious users or compromised accounts, arbitrary JavaScript cannot be stored in options or meta and then output as-is.

CSS in options or meta should also generally be avoided, but if absolutely necessary, it’s a good idea to properly sanitize it. See art-direction-redux for an example.

Encoding values used when creating a url or passed to add_query_arg()

Add_query_arg() is a really useful function, but it might not work as intended.
The values passed to it are not encoded meaning that passing

$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:

But in fact it becomes:

Using rawurlencode() on the values passed to it prevents this.

Using rawurlencode() on any variable used as part of the query string, either by using add_query_arg() or directly by string concatenation will also prevent parameter hijacking.

Prefixing functions, constants, classes, and slugs

Per the well-known WordPress adage: prefix all the things.

This applies to things obvious things such as names of function, constants, and classes, and also less obvious ones like post_type and taxonomy slugs, cron event names, etc.

Not checking return values

When defining a variable through a function call, you should 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 ) . "' />";

Order By Rand

MySQL queries that use ORDER BY RAND() can be pretty challenging and slow on large datasets. An alternate option can be to retrieve 100 posts and pick one at random.

Manipulating the timezone server-side

Using date_default_timezone_set() and similar isn’t allowed because it conflicts with stats and other systems. Developers instead should use WordPress’s internal timezone support. More information.

Skipping Full Page Caching

On VIP Go, Varnish is used to cache pages at the edges, which improves performance by serving end-users a page that comes directly from the nearest data center. The functionality is different in comparison to VIP, where VIP Go always caches the GET parameters and the caching is done individually per GET parameter (rather than being stripped out with the same page used for all requests as it is on VIP).  This means that code relying on vary_cache_on_function() will not work as intended, as Varnish will only respect the Vary headers for X-Country-Code and Accept but not forCookie.

Ajax calls on every pageload

Making POST requests to admin-ajax.php on every pageload, or on any pageload without user input, will cause performance issues and need to be rethought. If you have questions, we would be happy to help work through an alternate implementation.  GET requests to admin-ajax.php on VIP Go are cached just like any other GET request.

Front-end db writes

Functions used on the front-end that write to the database are not allowed. This is due to scaling concerns and can easily bring down a site.

*_meta as a hit counters

Please don’t use meta (post_meta, comment_meta, etc.) to track counts of things (e.g. votes, pageviews, etc.). First of all, it won’t work properly because of caching and due to race conditions on high volume sites. It’s also just a recipe for disaster and an easy way to break your site. In general, you should not try to count/track user events within WordPress; consider using a JavaScript-based solution paired with a dedicated analytics service (such as Google Analytics) instead.

eval() and create_function()

Both these functions can execute arbitrary code that’s 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.

We strongly recommend using Anonymous Functions, which are much cleaner and more secure.

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 number possible that still gives you the number of results you find acceptable. 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, which is going to destroy 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 that you’d want to display but doubt it’d hit more than 100 ever, set the limit to 100. If it gets much higher than that, you might need to rethink the page architecture a bit.

Cron schedules less than 15 minutes or expensive events

Overly frequent cron events (anything less than 15 minutes) can significantly impact the performance of the site, as can cron events that are expensive.

Flash (.swf) files

Flash (.swf) files are not advisable on VIP Go, as they often present a security threat (largely due to poor development practices or due to bugs in the Flash Player) and vulnerabilities are hard to find/detect/secure. Plus, who needs Flash? 🙂

Incorrect licenses

All plugin and theme code on VIP Go must be compatible with the GNU Public License v2 (GPL2) license, the same license used for WordPress. Custom code written in-house is fine as long as it complies with the license.

The reasoning for this is that the GPL is the foundation of the WordPress open source project; we want to respect all of the developers who choose to honor this license, and who contribute to the community by building fully GPL compatible themes/plugins.

Ignore development only files

If it’s feasible within your development workflow, we ask that you .gitignore any files that are used exclusively in the local development of your theme, these include but are not limited to .svnignore, config.rb, sass-cache, grunt files, PHPUnit tests, etc.

VIP Requirements

Every theme must include a VIP attribution linkwp_head(), and wp_footer() calls.

Expensive 404 Pages

The 404 page needs to be one of the fastest on the site, as it is only cached for 10 seconds. That means that a traffic spike from a broken link can cause performance and even availability problems if the 404 page is doing expensive db lookups.

Commented out code, Debug code or output

VIP themes 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(). If you’re encountering an issue that can’t be debugged in your development environment, we’ll be glad to help troubleshoot it with you. The use of commented out code should be avoided. Having code that is not ready for production on production is bad practice and could easily lead to mistakes while reviewing (since the commented out code might not have been reviewed and the removing on a comment might slip in accidentally).

Generating email

To prevent issues with spam, abuse or other unwanted communications, your code should not generate, or allow users to generate, email messages to site users or user-supplied email addresses. That includes mailing list functionality, invitations to view or share content, notifications of site activity, or other messages generated in bulk. Where needed, you can integrate third-party SMTP or ESP (Email Service Provider) services that allow sharing of content by email, as long as they don’t depend on the VIP Go infrastructure for message delivery. If you only need to send out a few emails to admins or to a specific email address not in bulk amounts, you can use the built-in wp_mail() functionality.

Custom wp_mail headers

The PHP Mailer is properly escaping headers for you only in case you’re using appropriate filters inside WordPress. Every time you want to create custom headers using user supplied data (eg.: “FROM” header), make sure you’re using filters provided by WordPress for you. See wp_mail_from() and wp_mail_from_name()

Serializing data

Unserialize has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data.

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 any non-static data or can be filtered, you must validate it against directory traversal using validate_file() or by detecting the string “..”

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.

You can use one of the many functions available to compose the file path, such as __DIR__ or dirname( __FILE__ ) or plugin_dir_path( __FILE__ ) or get_template_directory()

// Don't do this:
require_once 'file.php';

// Do this instead:
require_once __DIR__ . '/file.php';

Settings alteration

Using ini_set() for alternating PHP settings, as well as other functions with the ability to change the configuration at runtime of your scripts, such as error_reporting(), is prohibited on the VIP Go platform. Allowed error reporting in production can lead to Full Path Disclosure.

Minified JavaScript files

JavaScript files that are minified should also 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.

reCaptcha for Share by Email

To protect against abuse of Jetpack’s share by e-mail feature (aka Sharedaddy) it must be implemented along with reCaptcha. This helps protect against the risk of the network being seen as a source of e-mail spam, which would adversely affect VIP sites. This blog post explains how to implement reCaptcha.

Removing the admin bar

The admin bar is an integral part of the WordPress experience and we highly discourage removing it. It must not be removed for administrator and vip_support roles.

Remote calls

Remote calls such as fetching information from external APIs or resources should rely on the WordPress HTTP API (no cURL) and should be cached. Example of remote calls that should be cached are wp_remote_get(), wp_safe_remote_get() and wp_oembed_get(). More information.

Using __FILE__ for page registration

When adding menus or registering your plugins, make sure that you use a unique handle or slug other than __FILE__ to ensure that you are not revealing system paths.

Functions that use JOINS, taxonomy relation queries, -cat, -tax queries, subselects or API calls

Close evaluation of the queries is recommended as these can be expensive and lead to performance issues. Queries with known problems when working with large datasets:

  • category__and, tag__and, tax_query with AND
  • category__not_in, tag__not_in, and tax_query with NOT IN
  • tax_query with multiple taxonomies
  • meta_query with a large result set (e.g. looking for only posts with a thumbnail_id meta on a large site, looking for posts with a specific meta value on a key)

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 'include_children' => false to be added to all taxonomy queries when possible.

In many instances where all posts in either a parent or child term are wanted, this can be replaced by only querying for the parent term and using a save_post() hook to determine a child term is added, and if so enforce that it’s parent term is also added. A one-time WP-CLI command might be needed to ensure previous data integrity.

Custom roles

For best compatibility between environments and for added security, custom user roles and capabilities need to be managed via our helper functions.

Using extract()

extract() should never be used because it is too opaque and difficult to understand how it will behave under a variety of inputs. It makes it too easy to unknowingly introduce new variables into a function’s scope, potentially leading to unintended and difficult to debug conflicts.


$_REQUEST should never be used because it is hard to track where the data is coming from (was it POST, or GET, or a cookie?), which makes reviewing the code more difficult. Additionally, it makes it easy to introduce sneaky and hard to find bugs, as any of the aforementioned locations can supply the data, which is hard to predict.  Much better to be explicit and use either $_POST or $_GET instead.

Not Using the Settings API

Instead of handling the output of settings pages and storage yourself, use the WordPress Settings API as it handles a lot of the heavy lifting for you including added security.

Make sure to also validate and sanitize submitted values from users using the sanitize callback in the register_setting call.

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:

  • Requires WordPress to do multiple queries to handle the lookup for the Page and any additional loops you manually run through.
  • Impedes development workflow as it requires the Page to be manually created in each environment and new developer machines as well.

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 therefore recommend that you use wp_parse_url() for backward compatibility.

Use wp_safe_redirect() instead of wp_redirect()

Using wp_safe_redirect(), along with the allowed_redirect_hosts filter, can help avoid any chances of malicious redirects within code.  It’s also important to remember to call exit() after a redirect so that no other unwanted code is executed.

Mobile Detection

When targeting mobile visitors, jetpack_is_mobile() should be used 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 for you automatically.

Custom API Endpoints without Permissions Callback

For custom API routes that perform writes or read private data, we strongly recommend registering a valid permissions callback for it to ensure that no data is exposed or manipulated.  More information.

Using bloginfo() without escaping

Keeping with the theme of Escaping All the Things, code that uses bloginfo() should use get_bloginfo() instead, so that the data can be properly late escaped on output.  Since get_bloginfo() can return multiple types of data, and it can be used in multiple places, it may need to be escaped with many different functions depending on the context:

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' ) ) . '">';

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().

VIP Notices

These are items that should be addressed but that code that goes live to production with these will not cause a performance or security problem. They might be best practices that help code maintenance or help keep the error logs clean, etc.

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.

For example instead of

$tags = wp_list_pluck( get_the_terms( get_the_ID(), 'post_tag') , 'name');


$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');

Here are some 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.

Using in_array() without strict parameter

PHP handles type juggling. This also applies to in_array() meaning that this:

in_array( 0, ['safe_value', 'another string']);

Will return true. See Using == instead of ===.

Inline resources

Inlining images, scripts or styles has been a common workaround for performance problems related to HTTP 1.x As more and more of the web is now served via newer protocols (SPDY, HTTP 2.0) these techniques are now detrimental as they cannot be cached and require to be sent every time with the parent resource. Read more about this here.

Using == instead of ===

PHP handles type juggling. Meaning that this:

$var = 0;
if ( $var == 'safe_string' ){
    return true;

Will return true. Unless this is the behavior you want you should always use === over ==.

Other interesting things that are equal are:

  • (bool) true == 'string'
  • null == 0
  • 0 == '0SQLinjection'
  • 1 == '1XSS'
  • 0123 == 83 (here 0123 is parsed as an octal representation)
  • 0xF == 15 (here 0xF is parsed as a hexadecimal representation of a number)
  • 01 == '1string'
  • 0 == 'test'
  • 0 == ''

Using output buffering

Output buffering should be used only when truly necessary and should never be used in a context where it is called conditionally or across multiple functions/classes. If used it should always be in the same scope and not with conditionals.

Not defining post_status Or post_type

By default the post_status of a query is set to publish for anonymous users on the front end. It is not set in any WP_ADMIN context including Ajax queries. Queries on the front end for logged in users will also contain an OR statement for private posts created by the logged in user, even if that user is not part of the site. This will reduce the effectiveness of MySQL indexes, specifically the type_status_date index.
The same is true for post_type, if you know that only a certain post_type will match the rest of the query (for example for a taxonomy, meta or just general query) adding the post_type as well as the post_status will help MySQL better utilize the indexes as it’s disposal.

Using closing PHP tags

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. This is part of the WordPress Coding Standards.

Use wp_json_encode() over json_encode()

wp_json_encode() will take care of making sure 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.

Caching large values in options

The options cache on VIP Go works the same as on core WordPress (different from how VIP works). This means that all the options are not autoloaded in a single cache key and that therefore the size of options is not as important as on VIP. That being said, memcache still has a 1MB cache key limit.


For VIP Go Multisite instances, switch_to_blog() only switches the database context. Not the code that would run for that site (for example different filters). It should only be used with extreme caution.

Performance Considerations

We want to make sure that your site runs smoothly and can handle any traffic load. As such, we often make recommendations related to performance, such as: are remote requests fast and cached? Does the site request more data than needed?

Uncached Pageload

Uncached pageloads should be optimized as much as possible. We will load different pages and templates on your theme uncached, looking for slow queries, slow or timed out remote requests, queries that are overly repeated, or function routines that are slow.

Term queries should consider include_children =>false

Previously 'include_children' => false was added to almost every taxonomy query on When a term query is processed, WordPress first looks to see if the term is shared. When global terms were enabled on (before the 4.4 merge) this would always return true.

If the term is shared it will not include the children term. If the term is not shared, it will include the children.

As of 4.4 Terms have all been split on which means that this now adds 'include_children' => true to almost all taxonomy queries. This can have a very significant performance impact on code that was performant previously. In some cases this change means that queries that used to return almost instantly will time out. We therefore now recommend 'include_children' => false to be added to all taxonomy queries when possible.

In many instances where the wanted behaviour is to grab all posts that are either the parent or a child term this can be replaced by only querying for the parent term and enforcing inside a save_post() hook that if a child term is added that it’s parent term is also added. A one time WP CLI command might be needed to ensure previous data integrity.

User security best practices

We encourage all users on VIP sites to follow best practices when it comes to securing their devices, accounts and access to VIP tools. Two-factor authentication is required for all users with the ability to publish to a VIP site and we also recommend following at least these basic steps:

  1. Set a login password for all user accounts on your computer.
  2. Set a complex (more than 4 character) passcode to unlock your mobile devices. Do not use fingerprints or patterns.
  3. Enable a screen saver that activates after a short period of time and requires a password to turn off.
  4. Use only strong passwords. Never use the same password in more than one place.
  5. Use a password manager such as 1Password or LastPass.
  6. Never put passwords in text documents, Google Docs, intranet pages, post-it notes or other unencrypted forms of storage.
  7. Use two-factor authentication for any services that support it, including accounts, Google apps such as Gmail, Dropbox, Twitter, Facebook, Github, iCloud, LinkedIn, PayPal and others. Do not store 2FA backup codes anywhere online. We strongly recommend using an authenticator app, such as Authy or Google Authenticator, over SMS-based two-factor authentication.
  8. Turn on device locating services such as “Find My Mac” for Apple laptops or “Find My iPhone” for iPhones.
  9. Encrypt your computer’s hard drive, and make sure any backups are encrypted too.
  10. Install and run anti-virus software with the latest virus definitions.
  11. Enable your computer’s firewall.
  12. Ensure that your home and office network routers are running the latest firmware and aren’t using default passwords.
  13. Be suspicious of any unusual requests to share sensitive information, such as usernames, passwords or other personal data. Report any such requests and “phishing” attempts.
  14. If working in public, use a privacy screen to prevent your activity being seen.

If you have any security-related questions about your account, your VIP site or any related service, please contact us via support ticket.

VIP Go local development

Because developing sites for VIP Go is different than developing for hosted VIP sites, you need a different development environment.

Your VIP Go site runs three codebases: WordPress core (tracking the most current version), the VIP Go mu-plugins, and the codebase from your specific site repo. Because of this, a variety of WordPress local development environments can be suitably configured for VIP Go development purposes.

Here we describe using a Varying Vagrant Vagrants (VVV) based local development environment. Other options may include: Chassis, Docker-WP, Laravel Valet, etc.

For local development on Windows specifically, please see this documentation.

VVV for VIP Go Development

Note: These instructions assume familiarity with command line tools a macOS, Linux, or similar Operating System.

Prerequisite: all git operations referenced in this guide assume you have an ssh keypair registered with GitHub and are using ssh (vs. https) protocols. Using https protocols may lead to unexpected errors and is not supported.

Step 1: Setting up VVV

The basic instructions for installing VVV are in their documentation. Complete setup per their instructions before continuing below.

Step 2: Adding your site code

Follow the instructions to add a new site, and reprovision vagrant. Then, find and remove the entire wp-content folder at {VVV FOLDER}/www/{site name}/public_html/wp-content. Replace {VVV FOLDER} with the path to your VVV folder (i.e. the folder you installed VVV into), and {site name} with the name of your site.

Git clone your VIP Go site repo in place of it, using the following command. Replace {CLONE URL} with the GitHub clone URL for your VIP Go GitHub repository.

git clone {CLONE URL} {VVV FOLDER}/www/{site name}/public_html/wp-content

Note: VIP Go sites must use the wp-content folder structure from If you do not yet have a VIP Go site repo hosted with us, please use this vip-skeleton repo for the “CLONE URL” above and place your codebase (theme & plugins) within it for testing. Once your VIP Go site repo has been provisioned, you’ll most likely use that repo instead.

Step 3: Adding the VIP Go MU plugins

Vip Go uses a series of platform-specific plugins which are found in the  vip-go-mu-plugins repository on GitHub.  To replicate the VIP Go environment git clone this repo into wp-content/mu-plugins/. You will want to make sure the contents of the repo are in the root of mu-plugins and not a folder such as vip-go-mu-plugins which is the default. You may use the following command replacing {VVV FOLDER} with the path to your VVV folder (i.e., the folder you installed VVV into).

git clone --recursive {VVV FOLDER}/www/{site name}/public_html/wp-content/mu-plugins/

Note: the vip-go-mu-plugins repository is using SSH protocol for submodules, and as GitHub does not allow anonymous SSH connections, you’ll have to set up an SSH key for your GitHub account and use it for interaction with the repository (cloning and submodule updates).

Periodically pull changes down from this repository to ensure you have the latest code… we suggest checking for and pulling changes before the start of development on any given day:

$ cd {VVV FOLDER}/www/{site name}/public_html/wp-content/mu-plugins/
$ git pull origin master
$ git submodule update --init --recursive

Alternatively, you can use the build-generated mu-plugins repository for local development – this repository does not contain submodule.

Note: Do not commit the mu-plugins/ directory to your VIP Go site’s repository.

The object-cache.php code that’s used in production can be found in drop-ins/object-cache/object-cache.php. Be sure to copy or symlink that to the root of wp-content in order for it to be activated in your local environment. If you have multiple sites using the same repo, be sure to set WP_CACHE_KEY_SALT for each site to avoid cache key collisions.

Step 4: Updating your wp-config file

Add the following code to your wp-config.php just above this line: /* That's all, stop editing! Happy blogging. */:

Include the VIP config file:

if ( file_exists( __DIR__ . '/wp-content/vip-config/vip-config.php' ) ) {
    require_once( __DIR__ . '/wp-content/vip-config/vip-config.php' );

Account for file permissions and auto-updates:

The local environment will replicate the file permissions behaviour on VIP Go found in mu-plugins/a8c-files.php only allowing files to be uploaded to tmp and uploads. The following constants should also be added to replicate the file setup on VIP Go. This prevents plugins and themes from being uploaded or updated from WP-Admin matching the Go environment.

define( 'DISALLOW_FILE_EDIT', true );
define( 'DISALLOW_FILE_MODS', true );

Step 5: Creating an admin user via WP-CLI

SSH into the VVV virtual machine and use WP-CLI to create a new user account with the administrator role. Use the new admin user account to access WP Admin and delete the default admin user account.

wp user create exampleusername --role=administrator

This step is a security precaution to avoid default administrator user credentials from a local development environment being present in a production environment. The vip-go-mu-plugins will block login attempts using the admin username and display the notice: Logins are restricted for that user. Please try a different user account.

Step 6: Finishing up

Your development environment is ready to “Go”.  Navigate to site-name.test/wp-admin in your browser and log in and you should see a “VIP” menu in wp-admin.

Using the site’s content in local development

VIP includes VaultPress access with each VIP site, to give clients the ability to self-service the download of hourly SQL database backups. VaultPress is accessible from each site’s WordPress dashboard, under the Jetpack menu. For multisite environments, each site is stored separately in VaultPress. Note that only wp_-prefixed tables are included in these backups.

This backup can then be imported into a local development environment using WP CLI. The jetpack_optionsrow needs to be removed before importing into a local or staging environment, to avoid creating Jetpack conflicts with the production site.

Although VaultPress on VIP Go offers SQL backups only, the media library for each site is redundantly backed up and secure. Please open a ticket to request a copy of the media library.

Performance improvements by removing usage of post__not_in

Using post__not_in means that the query cache hitrate will often be a lot lower. It can also make the query slower if the exclusion list is large. In almost all cases you can gain great speed improvements by requesting more posts and skipping the posts in PHP.

example, instead of:

$other_posts_in_tag = get_posts( array(
	'tag_id' =>  $tag_id,
	'posts_per_page' => $limit,
	'post__not_in'	=> $array_of_post_ids_to_skip,
	'suppress_filters' => false,

foreach ( $other_posts_in_tag as $post ){
	//logic goes here;

do this:

$other_posts_in_tag = get_posts( array(
	'tag_id' =>  $tag_id,
	'posts_per_page' => $limit + count( $array_of_post_ids_to_skip ),
	'suppress_filters' => false,

foreach ( $other_posts_in_tag as $post ){
	if ( in_array( $post, $array_of_post_ids_to_skip ) ){
	//logic goes here;

Using WP_Rewrite instead of _GET parameters to leverage full page caching

Often in code we’re used to doing something like this But this doesn’t play nice with full page caching provided by batcache. To leverage full page cache we need to use the WP_Rewrite API, you can use rewrite endpoints Or use the add_rewrite_rule() and add_rewrite_tag() functions (guide here)
With the help of these functions, you can rewrite your url so that it will now be:

But wait — there’s more!

You can even speed up ajax requests with this technique.

You can create to make ajax requests that are cached for 5 minutes for all users with batcache. This will make request time go down often 100 folds! Note that the ajax rewrites are to index.php and not to admin-ajax.php. You are creating a new endpoint that will call your PHP function, not rewriting the query to pass to admin-ajax.php

Creating good changesets

Changesets are the heart of any version control system, and making good changesets is vitally important to the maintainability of your code. As all code on VIP is reviewed by a real person, it’s even more important all changesets are well crafted.

Remember always code (and commit) for the maintainer.

A Good Changeset:

Represents one logical change

What comprises a ‘logical change’ is up for interpretation, but only directly related changes are included. Generally, the smaller the changeset, the better.

Good Example: Adding the CSS, JS, HTML, and PHP code for a new UI button.

Bad Example: Adding the new UI button, fixing whitespacing, and tweaking copy in the footer.

Bundles related changes together

It’s much easier to trace refactorings and other changes if related changes are grouped together. Rather than splitting a logical change into many separate commits, related changes should be combined.

Good Example: Refactoring code into a new plugin by moving it to a new file and including that file.

Bad Example: Refactoring code into a new plugin by putting the code removal, addition, and include into separate commits.

Is Atomic

An atomic commit means that the system is always left in a consistent state after the changeset is committed. No one commit would cause the codebase to be in an invalid state. The commit is easily rolled back to a previous valid state, including all related changes, without the need to analyze the potential interdependencies of neighboring commits.

Good Example: Adding a new feature to the homepage by committing the HTML / PHP changes alongside the required CSS / JS changes, so there is never an incomplete state (HTML elements without styling) in the codebase.

Bad Example: Committing the HTML changes and requisite CSS / JS separately. The first commit represents an inconsistent state, as the feature can exist in the DOM without being properly styled.

Is Properly Described

Accurately describing the changes is very important for others (and future you) looking at your code. A good commit message describes the what and why of a change. Please see Writing Good Commit Messages for more information.

Writing good commit messages

Commit messages are one of the most common ways developers communicate with other developers, including our VIP team, so it’s important that your commit message clearly communicate changes with everybody else.

Who are we writing commit messages for?

The audience of a commit message is:

0. People reading the commit timeline.

1. People debugging code.

What is a good commit message?

Having these assumptions in mind:

1. Good commit messages should have a subject line. One sentence briefly describing what the change is, and (if it makes sense) why it was necessary.

A good subject line gives the reader the power to know the gist of the commit without bothering to read the whole commit message.


Fix stats link on

This does not need a high-level why part, because it’s obvious – the links weren’t working.


Stats Report: clear caches on each post to save memory

Here we need a why part, because if the message was only “clear caches on each post”, the obvious follow-up question is, “Why would you clear cache for each post in a loop?!”.

Whenever the commit is a part of a clearly-defined and named project, prefixing the commit with the project name is also very helpful. It’s not mandatory, because often the project space is vague and the list of committed files reveals similar information.

2. There should be an empty line between the subject line and the rest of the commit message (if any). Whitespace is like bacon for our brains.

3. A good commit message tells why a change was made.

Reasoning why is helpful to both of our audiences. Those following the timeline, can learn a new approach and how to make their code better. Those tracing bugs gain insight for the context of the problem you were trying to solve, and it helps them decide whether the root cause is in the implementation or higher up the chain.

Explaining why is tricky, because it’s often obvious. “I’m fixing it because it’s broken”. “I’m improving this, because it can be better.”

If it’s obvious, go one level deeper. The 5 Whys technique is great. Not only for looking for root causes of problems, but for making sure you are doing what you are doing for the right reasons.


JSON API: Split class into hierarchy for easier inclusion in ExamplePlugin

Including the old code required a bunch of hacks and compatibility layers.
With the new hierarchy, we can get rid of almost all the hacks and drop the files into ExamplePlugin as is.

Here the commit message very conveniently explains what the downsides were of the old approach and why the new approach is better.


Remove filtering by ticket

It's not very useful, while it's slow to generate.

The workflow is to usually go to the ticket page and see associated
comments there.

Here the commit message shares a UX decision we made, which is the primary reason of the commit.

5. Most commits fix a problem. In this case a good commit message explains what caused the problem and what its consequences were.

Everybody needs to know what caused a problem in order to avoid causing a similar problem again. Knowing the consequences can explain already noticed erroneous behaviour and can help somebody debugging a problem compare the consequences of this, already fixed problem with the one being debugged.

If possible, avoid the word fix. Almost always there is a more specific verb for your action.

If the problem is caused by a single changeset, a good commit message will mention it.

6. A good commit message explains how it achieves its goal. But only if isn’t obvious.

Most of the time it’s obvious. Only sometimes some high-level algorithm is encoded in the change and it would benefit the reader to know it.


Add a first pass client stat for bandwidth

Bandwidth is extrapolated from a month sample. From
there we get the average number of bytes per pageview
for each blog. This data is cached in means.json.

All the code for generating the data in means.json is
in the static methods of the class.

Here we explain the algorithm for guessing bandwidth data. It would have been possible to extract this information from the commit, but it would’ve taken a lot of time and energy. Also, by including it in the commit message we imply that it’s important for you to know that.

7. If the subject line of a commit message contains the word and or in other way lists more than one item, the commit is probably too large. Split it.

Make your commits as small as possible. If you notice a coding style problem while fixing a bug, make a note and fix it after you fix the bug. If you are fixing a bug and you notice another bug, make a note and fix the second bug in another commit.

The same is especially true for white space changes to existing code. White spaces changes should be a separate commit.

8. A good commit message should not depend on the code to explain what it does or why it does it.

Two notes here:

This doesn’t mean we should tell what each line of code does. It means that we should convey all the non-trivial information in the code to the commit message.

This doesn’t mean we whouldn’t include any of this information in the code. Knowing why a function exists, what it does, or what algorithm does it use can often be a useful comment.

9. It’s perfectly OK to spend more time crafting your commit message than writing the code for your commit.

10. It’s perfectly OK for your commit message to be longer than your commit.

11. A good commit message gives props and references relevant tickets.

12. Common sense always overrules what a good commit message thinks it should be.

Other perspectives

Here’s another excellent post that explains how to approach a good commit message:

The Code: guidelines for VIP developers


At VIP, we feel very privileged to work with some of the best developers on some of the world’s biggest sites. It’s a small community of smart people who get to build some amazing technology.

As a developer working on VIP, I will:

  • Never stop learning.
  • Not be afraid to ask questions.
  • Be open to feedback, constructive criticism, and collaborative discussion.
  • Be proactive in finding solutions, and not wait for someone else to resolve it for me.
  • Test and review my code before submitting for peer review.
  • Escape, sanitize, and validate all the things.
  • Be kind, courteous, and helpful to my fellow developers.

Two helpful links to get you started:

Writing custom WP-CLI commands

Occasionally, you may find that you need to access or transform large amounts of data on your site. If it’s for more than a dozen posts, it’s usually more efficient to write a custom WP-CLI command (sometimes called a “bin script”), where you can do things such as easily change strings, assign categories, or add post meta across hundreds or thousands of posts. However, with great power comes great responsibility — any small mistake you make with your logic could have negative repercussions across your entire dataset!

Some general tips to keep in mind when writing your script:

  • Default your command to do a test run without affecting live data. Add an argument to allow a “live” run — this way, you can compare what the actual impact is versus the expected impact:
    $dry_mode = ! empty ( $assoc_args['dry-run'] );
    if ( ! $dry_mode ) {
    	WP_CLI::line( " * Removing {$user_login} ( {$user_id} )... " );
    	$remove_result = remove_user_from_blog( $user_id, $blog_id );
    	if ( is_wp_error( $remove_result ) ) {
    		$failed_to_remove[] = $user;
    } else {
    	WP_CLI::line( " * Will remove {$user_login} ( {$user_id} )... " );
    • Check your CLI methods have the necessary arguments. WP CLI passes 2 arguments ($args and $assoc_args) to each command, you’ll need these to implement dry run options. You can take advantage of wp_parse_args() for setting default values for optional parameters:
      $args_assoc = wp_parse_args( $args_assoc, array(
          'dry-run' => true,
      	// etc...
          'post-meta' => 'some_default_post_meta'
      ) );
    • Use WP-CLI::Error only if you want to interrupt the command.  If you just want to know about the error and have it logged for further investigation or just for knowing what did not went as expected, you should be using WP_CLI::Line or WP_CLI::Warning with custom debugging information as this won’t make the command to exit and stop further execution.  Some “errors” are also not errors, but are expected (i.e. you don’t want to update post which does not meet certain conditions, etc.).
    • Comment well and provide clear usage instructions. It’s important to be very clear about what each part is doing and the reasoning behind the logic. Comments are especially helpful when something maybe doesn’t work as intended and there needs to be debugging.
    • Be as verbose as possible. It’s important when running the command to know that something is happening, what’s happening and when the script will finish. Have an opening line in the script and a line for every action the command is performing:
      public function __invoke( $args, $assoc_args ) {
      	// ...process args
      	// Let user know if command is running dry or live
      	if ( true === $dry_mode ) {
      		WP_CLI::line( "===Dry Run===" );
      	} else {
      		WP_CLI::line( "Doing it live!" );
      	// ...define $query_args for WP_Query object
      	// Set variables for holding stats printed on the end of the run
      	$updated = $missed = 0;
      	do {
      		// Let user know how many posts are about to be processed
      		WP_CLI::line( sprintf( "Processing %d posts at offset of %d of %d total found posts", count( $query->posts ), $offset, $query->found_posts ) );
      		// stuff
      		// Let user know what is happening
      		WP_CLI::line( sprintf( "Updating %s meta for post_id: " ), 'some_meta_key', $post_id );
      		// Save result of update/delete functions
      		$updated = update_post_meta( $post_id, 'some_meta_key', sanitize_text_field( $some_meta_value ) ); if ( $updated ) {
      			// Let user if update was successful
      			WP_CLI::line( "Success: Updated post_meta '%s' for post_id %d with value %s", 'some_meta_key', $post_id, serialize( $some_meta_value ) );
      			// Count successful updates
      		} else {
      			// If not successful, provide some helpful debug info
      			WP_CLI::line( "Error: Failed to update post_meta '%s' for post_id %d with value %s", 'some_meta_key', $post_id, serialize( $some_meta_value ) ); // There are some values (eg.: WP_Error object) that should be serialized in order to print something meaningful
      			// Count any errors/skips
      			// Free up memory
      			$query = new WP_Query( $query_args );
      	} while( $query->have_posts() );
      	// Let user know result of the script
      	WP_CLI::line( "Finished the script. Updated: %d. Missed: %d", $updated, $missed );
    • Always use $wpdb->prepare method in direct DB queries as a safeguard against SQL injection attacks and when dealing with “LIKE” statements, use the $wpdb->esc_like method:
global $wpdb;
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title = %s AND ID = %d", $post_title, $min_post_id ) );

$like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
$query = $wpdb->prepare( "SELECT * FROM {$wpdb->posts} as p AND ((p.post_title LIKE %s) OR (p.post_name LIKE %s))", $like, $like );

Best Practices on VIP for Scale

  • Always extend the WPCOM_VIP_CLI_Command class (instead of WP_CLI_Command) provided in the development helpers to utilize its helper functions like stop_the_insanity()
  • Make sure you require the file that contains your new command (e.g. in your functions.php file) and only include it if WP_CLI is defined and true:
// CLI scripts
if ( defined( 'WP_CLI' ) && WP_CLI ) {
	require_once MY_THEME_DIR . '/inc/class-mycommand1-cli.php';
	require_once MY_THEME_DIR . '/inc/class-mycommand2-cli.php';
  • If your command is importing posts or calling wp_update_post(), make sure to define( 'WP_IMPORTING', true ); at the top of the related code to ensure only the minimum of extra actions are fired.
  • Use the progress bar class to have a better idea of the completion time. While operating the command, the time to finish running scripts in production often takes much longer than it takes in staging environment (the same applies to live runs versus initial dry runs).
    public function __invoke( $args, $assoc_args ) {
    	// ...process args
    	$posts_per_page = 100; // posts per page will be used for ticks
    	// ...define $query_args and create new WP_Query object
    	// New progress bar -- provide number of all posts we'll be dealing with as well as a size of a batch processed before the first/next tick will happen
    	$progress = new cliprogressBar( sprintf( 'Starting the command. Found %d posts', $query->found_posts ), $query->found_posts, $posts_per_page );
    	do {
    		WP_CLI::line( sprintf( "Processing %d posts at offset of %d of %d total found posts", count( $query->posts ), $offset, $query->found_posts ) );
    		// stuff
    		$progress->tick( $posts_per_page ); //tick
    		// Free up memory
    		$query = new WP_Query( $query_args );
    	} while ( $query->have_posts() );
    	WP_CLI::line( "Finished the script. Updated: %d. Missed: %d", $updated, $missed );
  • If you’re modifying lots of data on a live site, make sure to prepare your command for long runs. The command should be prepared for processing without exhausting memory and overloading the database:
    • Use sleep() in key places to help with loads associated with cache invalidation and replication.
    • Use the following WPCOM_VIP_CLI_Command helper methods:
      • stop_the_insanity() to clear memory after having processed 100 posts or less to avoid interruptions, especially when using get_posts() or WP_Query
      • When processing a large number of posts, use the start_bulk_operation() and end_bulk_operation() class methods to disable functionality that is often problematic with large write operations
  • Prepare the command for restart. Even if the sleep and stop_the_insanity functions are in place, command might die in the middle of its run. Commands dealing with a lot of posts or other long-running commands should be prepared for restart. You might either design them to be idempotent (meaning they can safely be run multiple times) or provide an option to start from certain point, perhaps using an offset argument or other suitable mean.
  • Direct Database Queries will probably break in unexpected ways. Use core functions as much as possible, as WP-CLI loads WordPress core with your theme and plugins, which are available to you in the command. Using direct SQL queries (specifically those that do UPDATEs or DELETEs) will cause the caches to be invalid. If a direct SQL query is required, only do SELECTs, but perform write operations using the core WordPress functionality. You may also want to remove certain hooks from wp_update_post or other actions to get the desired behaviour. In some rare contexts, a direct SQL query could be a better choice for certain reasons, such as preventing certain hooks from being triggered and/or WP_Query being too expensive for what you need. When building your custom direct SQL queries, remember to properly sanitize the input (as you’ll miss the advantage of core’s sanitization checks) and follow it with clean_post_cache() to flush associated cache so updates will be visible on your site before the cache expires.
    $wpdb->update( $wpdb->posts,
    //table array
    ( 'post_content' => sanitize_text_field( $post_content ) // Data should not be SQL escaped, but sanitized ),
    //data array( 'ID' => intval( $post_id ) ), // WHERE
    array( '%s' ), // data format
    array( '%d' ) // where format
    clean_post_cache( $post_id ); // Clean the cache to reflect changes
  • Using a no-LIMIT query can lead to timeout and failure, especially if it takes longer than 30 seconds. Instead, we recommend using smaller queries and paging through the results:
class Test_CLI_Command extends WPCOM_VIP_CLI_Command {
	 * CLI command that takes a metakey (required) and post category (optional)
	 * and publishes all pending posts once they have have had their metakeys updated.
	 * @subcommand update-metakey
	 * @synopsis --meta-key= [--category=] [--dry-run]
	public function update_metakey( $args, $assoc_args ) {
		// Disable term counting, Elasticsearch indexing, and PushPress. 
		$posts_per_page = 100;
		$paged = 1;
		$count = 0;
		// Meta key is required, otherwise an error will be returned.
		if ( isset( $assoc_args['meta-key'] ) ) {
			$meta_key = $assoc_args['meta-key'];
		} else {
			// Caution: calling WP_CLI::error stops the execution of the command. Use it only in case you want to stop the execution. Otherwise, use WP_CLI::warning or WP_CLI::line for non-blocking errors.
			WP_CLI::error( 'Must have --meta-key attached.' );
		// Category value is optional.
		if ( isset( $assoc_args['category'] ) ) {
			$cat = $assoc_args['category'];
		} else {
			$cat = '';
		// If --dry-run is not set, then it will default to true. Must set --dry-run explicitly to false to run this command. 
		if ( isset( $assoc_args['dry-run'] ) ) {
			// Passing `--dry-run=false` to the command leads to the `false` value being set to string `'false'`, but casting `'false'` to bool produces `true`. Thus the special handling.
			if ( 'false' === $assoc_args['dry-run'] ) {
				$dry_run = false;
			}else {
				$dry_run = (bool) $assoc_args['dry-run'];
		} else {
			$dry_run = true;
		if ( $dry_run ) {
			WP_CLI::line( 'Running in dry-run mode.' );
		} else {
			WP_CLI::line( 'We\'re doing it live!' );
		do {
			$posts = get_posts( array( 'posts_per_page' => $posts_per_page, 'paged' => $paged, 'category' => $cat, 'post_status' => 'pending', 'suppress_filters' => 'false', ));
			foreach ( $posts as $post ) {
				if ( ! $dry_run ) {
					update_post_meta( $post->ID, $meta_key, 'true' );
					wp_update_post( array( 'post_status' => 'publish' ) );
				} $count++;

			// Pause.
			WP_CLI::line( 'Pausing for a breath...' );
			sleep( 3 );
			// Free up memory.
			/* At this point, we have to decide whether to increase the value of $paged. In case a value which is being used for querying the posts (like post_status in our example) is being changed via the command, we should keep the WP_Query starting from the beginning in every iteration.
			 * If the any value used for querying the posts is not being changed, then we need to update the value in order to walk through all the posts. */
			// $paged++;
		} while ( count( $posts ) );
		if ( false === $dry_run ) {
			WP_CLI::success( sprintf( '%d posts have successfully been published and had their metakeys updated.', $count ) );
		} else {
			WP_CLI::success( sprintf( '%d posts will be published and have their metakeys updated.', $count ) );
		$this->end_bulk_operation(); // Trigger a term count as well as trigger bulk indexing of Elasticsearch site. }
	 * CLI command that takes a taxonomy (required) and updates terms in that
	 * taxonomy by removing the "test-" prefix.
	 * @subcommand update-terms
	 * @synopsis --taxonomy= [--dry_run]
	public function update_terms( $args, $assoc_args ) {
		$count = 0;

		// Disable term counting, Elasticsearch indexing, and PushPress.
		// Taxonomy value is required, otherwise an error will be returned.
		if ( isset( $assoc_args['taxonomy'] ) ) {
			$taxonomy = $assoc_args['taxonomy'];
		} else {
			WP_CLI::error( 'Must have a --taxonomy attached.' );
		if ( isset( $assoc_args['dry-run'] ) ) {
			if ( 'false' === $assoc_args['dry-run'] ) {
				$dry_run = false;
			} else {
				$dry_run = (bool) $assoc_args['dry-run'];
		} else {
			$dry_run = true;
		if ( $dry_run ) {
			WP_CLI::line( 'Running in dry-run mode.' );
		} else {
			WP_CLI::line( 'We\'re doing it live!' );
		$terms = get_terms( array( 'taxonomy' => $taxonomy ) );
		foreach ( $terms as $term ) {
			if ( ! $dry_run ) {
				wp_update_term( $term->term_id, $term->taxonomy, array( 'name' => str_replace( 'test ', '', $term->name ), 'slug' => str_replace( 'test-', '', $term->slug ), ) );
		// Trigger a term count as well as trigger bulk indexing of Elasticsearch site.
		if ( false === $dry_run ) {
			WP_CLI::success( sprintf( '%d terms were updated.', $count ) );
		} else {
			WP_CLI::success( sprintf( '%d terms will be updated.', $count ) );
} WP_CLI::add_command( 'test-command', 'Test_CLI_Command' );


How do I debug CLI with New Relic?

By default WP-CLI commands and Cron events are not monitored by New Relic, but if you would like us to make New Relic available for these please send us a support request.

Ready to get started?

Drop us a note.

No matter where you are in the planning process, we’re happy to help, and we’re actual humans here on the other side of the form. 👋 We’re here to discuss your challenges and plans, evaluate your existing resources or a potential partner, or even make some initial recommendations. And, of course, we’re here to help any time you’re in the market for some robust WordPress awesomeness.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.