Storing WordPress filesystem credentials 01

Testing the waters to ease the user interaction with the filesystem.

A sorely needed function

The “Front to Back” plugin will rely on its access to the filesystem: that’s how it’s meant to work and while I do not see myself using its template creation and reading powers anywhere but on local I can think of situations where having that possibility not denied on a live server could be worth the effort.
How’s the filesystem abstraction class currently accessed then?

Init procedures

Each time a page is served the main plugin file will include the init.php initialization file:

<?php
use tad\FrontToBack\OptionsPage;
use tad\FrontToBack\Templates\Creator;
use tad\FrontToBack\Templates\Filesystem;
use tad\FrontToBack\Templates\MasterChecker;

require_once __DIR__ . '/src/functions/commons.php';

$plugin = ftb();

/**
 *  Variables
 */
$plugin->set( 'path', __DIR__ );
$plugin->set( 'url', plugins_url( '', __FILE__ ) );

$templates_extension = 'php';
$plugin->set( 'templates/extension', $templates_extension );
$plugin->set( 'templates/master-template-name', "master.{$templates_extension}" );

/**
 * Initializers
 */
$plugin->set( 'templates/default-folder', function () {
    return WP_CONTENT_DIR . '/ftb-templates';
} );

$plugin->set( 'options-page', function () {
    return new OptionsPage();
} );

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

/**
 * Kickstart
 */
/** @var OptionsPage $optionsPage */
$optionsPage = $plugin->get( 'options-page' );
$optionsPage->hooks();

/** @var MasterChecker $masterTemplateChecker */
$masterTemplateChecker = $plugin->get( 'master-template-checker' );
$masterTemplateChecker->hooks();

/** @var Creator $templatesCreator */
$templatesCreator = $plugin->get( 'templates-creator' );
$templatesCreator->hooks();

The lines referring the filesystem initializations tell me that:

  1. it will be initialized as soon as a class needing filesystem access gets it from the dependency injection container
  2. it will be initialized on the currently specified templates folder
  3. the dependency injection container will pass the same instance around as a singleton

Looking inside the code of the function that will take care of initializing the WordPress filesystem class I can very well see that credentials will be required on each page load.

public function initialize_wp_filesystem( $templates_root_folder = null, $url = null, $data = array() ) {
    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 );
}

This in turn means that a WordPress installation that has not direct access to the filesystem will require the credentials on each page load. Talk about bad User Experience.

WordPress endorses a solution around the credentials being required each time the filesystem access is required that consists in setting those in the config file.
If the filesystem access method is, say, ftp then the following constants in the wp-config.php file would avoid the request each time

define('FTP_HOST','ftp.example.com');
define('FTP_USER', 'waldo');
define('FTP_PASS', 'secret');

While this means that installation-wide settings could skip the credentials request completely it should not lead to the easy solution that sound like this: if it’s legit to store them in the config file then it’s safe to store them in the database using an option or a transient.
The reasoning does not apply as the site administrator could decide to store credentials in the config file but that decision should not happen on a plugin level overriding the probably explicit will of the administrator not to store the credentials anywhere.
So is there a way to ease the filesystem access?
I will use cookies to store each WordPress user filesystem access credentials for a month once those have been submitted at least once.
Should a user right to access a folder or file be removed than the credentials stored in the cookie will be invalid and new credentials will be required.

Testing it

Testing the idea is made with some additional test methods in the FilesystemTest class.
The class is using function-mocker as mocking engine and the wp-browser add-on module for Codeception.
Before the show begins I will add an adapter class to wrap and be able to mock and spy calls to the underlying cookie related methods

<?php

namespace tad\FrontToBack\Cookies;


class Operations {

    public function setcookie( $name, $value = null, $expire = null, $path = null, $domain = null, $secure = null, $httponly = null ) {
        return setcookie( $name, $value, $expire, $path, $domain, $secure, $httponly );
    }

    public function get_cookie( $key ) {
        return empty( $_COOKIE[ $key ] ) ? null : $_COOKIE[ $key ];
    }
}

This done I’ve modified the Filesystem class constructor to allow for the injection of this new class

// class `Filesystem`

public function __construct( $templates_root_folder = null, \WP_Filesystem_Base $wpfs = null, Operations $cookie_operations = 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' );
    $this->cookie_operations    = $cookie_operations ? $cookie_operations : new Operations();
    if ( empty( $wpfs ) ) {
        $this->initialize_wp_filesystem();
    } else {
        $this->wpfs = $wpfs;
    }
    $this->templates_extension  = ftb()->get( 'templates/extension' );
    $this->master_template_path = $this->templates_root_folder . ftb()->get( 'templates/master-template-name' );
}

and will finally write a first test to set TDD in motion

<?php
namespace tad\FrontToBack\Templates;

use tad\FunctionMocker\FunctionMocker as Test;

class FilesystemTest extends \WP_UnitTestCase {

    protected $backupGlobals = false;

    protected $masterTemplateName;

    protected $templatesExtension;

    public function setUp() {
        // before
        parent::setUp();

        // your set up methods here
        Test::setUp();
        $this->masterTemplateName = ftb()->get( 'templates/master-template-name' );
        $this->templatesExtension = ftb()->get( 'templates/extension' );
    }

    public function tearDown() {
        // your tear down methods here
        Test::tearDown();
        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should be instantiatable
     */
    public function it_should_be_instantiatable() {
        Test::assertInstanceOf( 'tad\FrontToBack\Templates\Filesystem', new Filesystem() );
    }

    // more tests... 

    /**
     * @test
     * it should store valid credentials in cookie
     */
    public function it_should_store_valid_credentials_in_cookie() {
        $creds                          = array( 'foo' => 'bar' );
        $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', $creds );
        Test::replace( 'WP_Filesystem', true );
        $cookie_operations = Test::replace( 'tad\FrontToBack\Cookies\Operations' )
                                 ->method( 'get_cookie', null )
                                 ->method( 'setcookie', true )
                                 ->get();

        $sut = new Filesystem( __DIR__, null, $cookie_operations );

        $name   = $sut->get_credentials_cookie_name();
        $expire = $sut->get_credentials_cookie_expire();
        $cookie_operations->wasCalledWithOnce( [
            $name,
            $creds,
            $expire,
            '/',
            site_url(),
            false,
            false
        ], 'setcookie' );
    }

    /**
     * @test
     * it should retrieve credentials from cookies first if available
     */
    public function it_should_retrieve_credentials_from_cookies_first_if_available() {
        $creds                          = array( 'foo' => 'bar' );
        $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials' );
        $WP_Filesystem                  = Test::replace( 'WP_Filesystem', true );

        $cookie_operations = Test::replace( 'tad\FrontToBack\Cookies\Operations' )
                                 ->method( 'get_cookie', $creds )
                                 ->get();

        $sut = new Filesystem( __DIR__, null, $cookie_operations );

        $cookie_operations->wasCalledWithTimes( [ $sut->get_credentials_cookie_name() ], 2, 'get_cookie' );
        $request_filesystem_credentials->wasNotCalled();
        $WP_Filesystem->wasCalledWithOnce( [ $creds ] );
    }    
}

Next

I will finish testing the cookie credentials storing idea and move to beefier parts of the plugin.