Front to Back 04

Wrapping and testing up filesystem access.

Exploring filesystem credentials

In my previous post I’ve outlined some use cases the Front to Back plugin will cover that involve access to the filesystem.
Accessing the filesystem in a WordPress environment means getting access credentials each time the filesystem class needs to be instantiated; looking inside the code of the crucial require_filesystem_credentials function it’s clear that the best possible scenario, the one the plugin is currently handling, is the one where WordPress has direct access to the underlying filesystem.
Any other case will require credentials and those credentials will be required each time the filesystem class needs to write or read from the filesystem.
In the specific case of this plugin it’s unlikely anything but direct access will be used but I want to make sure options are available.
When it comes to other access methods WordPress will try to fetch the credentials from constants usually defined in the wp-config.php file, see the code chunk below from the require_filesystem_credentials function:

// If defined, set it to that, Else, If POST'd, set it to that, If not, Set it to whatever it previously was(saved details in option)
$credentials['hostname'] = defined('FTP_HOST') ? FTP_HOST : (!empty($_POST['hostname']) ? wp_unslash( $_POST['hostname'] ) : $credentials['hostname']);
$credentials['username'] = defined('FTP_USER') ? FTP_USER : (!empty($_POST['username']) ? wp_unslash( $_POST['username'] ) : $credentials['username']);
$credentials['password'] = defined('FTP_PASS') ? FTP_PASS : (!empty($_POST['password']) ? wp_unslash( $_POST['password'] ) : '');

// Check to see if we are setting the public/private keys for ssh
$credentials['public_key'] = defined('FTP_PUBKEY') ? FTP_PUBKEY : (!empty($_POST['public_key']) ? wp_unslash( $_POST['public_key'] ) : '');
$credentials['private_key'] = defined('FTP_PRIKEY') ? FTP_PRIKEY : (!empty($_POST['private_key']) ? wp_unslash( $_POST['private_key'] ) : '');

a further shot is given to look into the $_POST object. That’s the way the double call to the function works in almost all the examples of WordPress Filesystem API usage.

When to require credentials?

When will the plugin need to access the filesystem?
I’ve outlined the scenarios in the previous post and all of them will happen when the user is on a page edit screen.
What’s currently happening is that two classes will need to access the filesystem:

  • tad\FrontToBack\Templates\MasterChecker to make sure the master template is in place in the templates folder
  • tad\FrontToBack\Templates\Creator to be able to create or move templates when saving the post

The plugin init.php file is handling the dependency resolution

$plugin->set( 'templates-filesystem', function () {
    $templates_folder = ftb_get_option( 'templates_folder' );
    $templates_folder = $templates_folder ?: ftb()->get( 'templates/default-folder' );

    return new Filesystem( $templates_folder );
} );

$plugin->set( 'master-template-checker', function () {
    return new MasterChecker( ftb()->get( 'templates-filesystem' ) );
} );

$plugin->set( 'templates-creator', function () {
    return new Creator( ftb()->get( 'templates-filesystem' ) );
} );

and making sure credentials will be required at most once as one instance only of the tad\FrontToBack\Templates\Filesystem class will be handled around by the makeshift Dependency Injection container.
It’s now time to test the method responsible for credentials and access of the tad\FrontToBack\Templates\Filesystem class: I’ve added more test methods to the FilesystemTest class and took care of defining a tests-config.php file to make sure the filesystem method will be set to direct during tests (see Codeception configuration file and the test configuration file):

/**
 * @test
 * it should not have access if credentials are needed
 */
public function it_should_not_have_access_if_credentials_are_needed() {
    Test::replace( 'request_filesystem_credentials', false );

    $sut = new Filesystem();

    Test::assertFalse( $sut->has_access() );
}

/**
 * @test
 * it should not have access if WP_Filesystem init fails
 */
public function it_should_not_have_access_if_wp_filesystem_init_fails() {
    Test::replace( 'WP_Filesystem', false );

    $sut = new Filesystem();

    Test::assertFalse( $sut->has_access() );
}

/**
 * @test
 * it should have access if credentials are good
 */
public function it_should_have_access_if_credentials_are_good() {
    Test::replace( 'request_filesystem_credentials', true );

    $sut = new Filesystem();

    Test::assertTrue( $sut->has_access() );
}

/**
 * @test
 * it should not call WP filesystem initialization if already set
 */
public function it_should_not_call_wp_filesystem_initialization_if_already_set() {
    $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', true );

    $wpfs = Test::replace( '\WP_Filesystem_Base' )->get();
    $sut  = new Filesystem( __DIR__, $wpfs );

    $request_filesystem_credentials->wasNotCalled();
}

/**
 * @test
 * it should request credentials for current url
 */
public function it_should_d_request_credentials_for_current_url() {
    $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', true );
    Test::replace( 'site_url', 'http://example.com' );
    $_SERVER['REQUEST_URI'] = 'foo?some=var';
    $url                    = 'http://example.com/foo?some=var';

    $sut = new Filesystem( __DIR__ );

    $request_filesystem_credentials->wasCalledOnce();
    $request_filesystem_credentials->wasCalledWithOnce( [ $url, '', false, __DIR__ . '/', null ] );
}

/**
 * @test
 * it should require credentials again for same url if WP_Filesystem fails on credentials
 */
public function it_should_require_credentials_again_for_same_url_if_wp_filesystem_fails_on_credentials() {
    $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', true );
    Test::replace( 'site_url', 'http://example.com' );
    Test::replace( 'WP_Filesystem', false );
    $_SERVER['REQUEST_URI'] = 'foo?some=var';
    $url                    = 'http://example.com/foo?some=var';

    $sut = new Filesystem( __DIR__ );

    $request_filesystem_credentials->wasCalledTimes( 2 );
    $request_filesystem_credentials->wasCalledWithOnce( [ $url, '', false, __DIR__ . '/', null ] );
    $request_filesystem_credentials->wasCalledWithOnce( [ $url, '', true, __DIR__ . '/', null ] );
} 

The Test is an alias for the function-mocker library.
Some tests later the Filesystem class looks now in better shape.

<?php

namespace tad\FrontToBack\Templates;


class Filesystem {

    /**
     * @var \WP_Filesystem_Base
     */
    protected $wpfs;

    /**
     * @var string 
     */
    private   $templates_root_folder;

    public function initialize_wp_filesystem( $templates_root_folder = null, $url = null ) {
        if ( empty( $this->wpfs ) ) {
            $templates_root_folder = $templates_root_folder ? $templates_root_folder : $this->templates_root_folder;
            $url                   = $url ?: trailingslashit( site_url() ) . $_SERVER['REQUEST_URI'];
            if ( false === ( $creds = request_filesystem_credentials( $url, '', false, $templates_root_folder, null ) ) ) {
                return false;
            }

            if ( ! WP_Filesystem( $creds ) ) {
                request_filesystem_credentials( $url, '', true, $templates_root_folder, null );

                return false;
            }
            global $wp_filesystem;
            $this->wpfs = $wp_filesystem;

            return true;
        }

        return ! empty( $this->wpfs );
    }

    public function __construct( $templates_root_folder = null, \WP_Filesystem_Base $wpfs = null ) {
        require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php';
        require_once ABSPATH . '/wp-admin/includes/file.php';
        $this->templates_root_folder = $templates_root_folder ? trailingslashit( $templates_root_folder ) : ftb_get_option( 'templates_folder' );
        if ( empty( $wpfs ) ) {
            $this->initialize_wp_filesystem();
        } else {
            $this->wpfs = $wpfs;
        }
    }

    /** Forwards calls to \WP_Filesystem_Base
     *
     * @param $name
     * @param $arguments
     *
     * @return mixed
     */
    public function __call( $name, $arguments ) {
        return call_user_func_array( array(
            $this->wpfs,
            $name
        ), $arguments );
    }

    public function get_wpfs() {
        return $this->wpfs;
    }

    public function get_templates_root_folder() {
        return $this->templates_root_folder;
    }

    public function has_access() {
        return $this->initialize_wp_filesystem();
    }
}

Keeping up the flow

Now that credential requirement is taken care of is time to take the possible flow into account and I will do it with a slightly modified version of a scenario used in the previous post:

1a. Page creation

* Given the user can create pages and is the admin area
* And the filesystem access method requires the user to supply credentials
* When the user creates the "About" page
* Then the plugin should display the credentials form
* And submit to the same page the credentials

1b. Page creation after credentials

* Given the user can create pages and is the admin area
* And the user created the "About" page
* And the filesystem access method requires the user to supply credentials
* And the user supplied valid credentials
* And submitted the credentials form
* When the page reloads after credentials form submission
* Then the plugin should create the `about.php` template

This scenario can be taken care of simply looking up the templates folder on each page edit screen load: is the page template in the templates folder? If “no” then create it.
But what about changing a page post_name and hence renaming the template too? More scenarios.

2a. Page post_name change

* Given the user can create pages and is the admin area
* And the filesystem access method requires the user to supply credentials
* When the user changes the "About" page `post_name` from `about` to `about-us`
* Then the plugin should display the credentials form
* And submit to the same page the credentials

2b. Page post_name change after credentials

* Given the user can create pages and is the admin area
* When the user changes the "About" page `post_name` from `about` to `about-us`
* And the filesystem access method requires the user to supply credentials
* And the user supplied valid credentials
* And submitted the credentials form
* When the page reloads after credentials form submission
* Then the plugin should move the `about.php` template to `about-us.php`

This second case cannot be treated as a stateless transition as the previous one and I will deal with it next.