Automating WordPress backends with Chrome and PHP

If you’ve ever used ManageWP then you know their onboarding is quite magical. With only three pieces of information, the WordPress site url, username and password, ManageWP will automatically install and configure their worker plugin. That allows your site to be fully managed by ManageWP.

ManageWP isn’t open source so I have no idea what wizardry they are doing to pull this off. However today I’ll show you how to accomplish a similar thing using Chrome, PHP and Javascript.

Controlling Google Chrome with just PHP code.

This sounds too good to be true but it’s real. With Chrome PHP you can write PHP code to control a real Chrome instance. Their documentation is straightforward. The Chrome PHP package can be added with Composer to either existing codebase or start from strach. You can do pretty anything a real Chrome browser can do.

For demonstration purposes let’s create a new empty directory with a single file chrome.php. If you have VS Code installed then run the following.

cd ~/Documents
mkdir chrome-php
cd chrome-php/
composer require chrome-php/chrome
code chrome.php

Start chrome.php with the following

<?php
require 'vendor/autoload.php';
use HeadlessChromium\BrowserFactory;
$browserFactory = new BrowserFactory();
$browser        = $browserFactory->createBrowser([
    'windowSize'      => [1440, 960],
    'connectionDelay' => 0.3,
]);

Depending on which version of Chrome you have you might need to tweak starting line new BrowserFactory() to look for a different name. My laptop running PopOS the following is what worked:

$browserFactory = new BrowserFactory( "google-chrome" );

Automating WordPress logins.

Now we are ready to do something. The following will sign into a WordPress /wp-admin/ and then save a picture of the backend to a screenshot.

try {
    $page = $browser->createPage();
    $page->navigate('https://example.com/wp-login.php')->waitForNavigation();

    $evaluation = $page->evaluate(
    '(() => {
        document.querySelector("#user_login").value = "my-user-name";
        document.querySelector("#user_pass").value = "my-password";
        document.querySelector("#loginform").submit();
    })()'
    );

    $evaluation->waitForPageReload();

    $page->screenshot()->saveToFile('screenshot.png');
} finally {
    $browser->close();
}

We can run this via the command line php chrome.php which will produce the following screenshot.

Pretty cool, right? How about something more advanced? Let’s navigate to the plugins page and install a new plugin. Assuming we are logged in, using the above code, we could then navigate around and click on things.

// Search for Jetpack plugin
$page->navigate('https://example.com/wp-admin/plugin-install.php?tab=search&type=term&s=Jetpack')->waitForNavigation();

// Install Jetpack plugin
$page->mouse()->find("[data-slug='jetpack']")->click();

File uploads with cleaver PHP & JS hacks.

One thing I couldn’t find was an easy solution for uploading files. There doesn’t appear to be a way to interact with file input fields. With some trial an error I did find a workaround. It’s possible to have Javascript create the file object and then attach that to the file input field. PHP can take in the raw file contents with base64 which is passed to Javascript. If the file is huge then you might run into issues however this works great for small files. Here is an example uploading a paid WordPress plugin:

$plugin_zip    = "/home/austin/Downloads/elementor-pro-3.3.6.zip";
$plugin_base64 = base64_encode( file_get_contents( $plugin_zip ) );

try {
    $page = $browser->createPage();
    $page->navigate('https://example.com/wp-login.php')->waitForNavigation();

    $evaluation = $page->evaluate(
    '(() => {
        document.querySelector("#user_login").value = "username";
        document.querySelector("#user_pass").value = "password";
        document.querySelector("#loginform").submit();
    })()'
    );

    $evaluation->waitForPageReload();

    $page->navigate('https://example.com/wp-admin/plugin-install.php')->waitForNavigation();
    $page->mouse()->find('a.upload-view-toggle.page-title-action')->click();
    $evaluation = $page->evaluate(
        '(() => {
                plugin_base64 = "' . $plugin_base64 . '"
                parts = Uint8Array.from(atob( plugin_base64 ), c => c.charCodeAt(0))
                blob = new Blob( [parts], {type: "application/zip" } )
                file = new File( [blob], "elementor-pro.zip", {
                    type: "application/zip"
                })
                list = new DataTransfer()
                list.items.add( file )
                document.querySelector("#pluginzip").files = list.files
                document.querySelector(".wp-upload-form").submit()
            })()'
        );
    $evaluation->waitForPageReload();
} finally {
    $browser->close();
}

Possibilities are endless, however probably best to limit using real WordPress credentials.

Automating connection to WordPress backend as a real user could come in handy for a wide range of use cases. One time connections, like connecting to ManageWP, makes sense. However repetitive use should be avoided. There are better ways to programmatically talk to a WordPress site. Anything like WordPress REST API, WPGraphQL or even WP-CLI over SSH would be a better approach. Since WordPress 5.6 Application Passwords are now the recommend approach securely talking with WordPress.