Not storing WordPress filesystem credentials 02

Where I fall back on my idea of storing filesystem access credentials.

A not fully scoped and researched idea

I admit that the idea of storing filesystem access credentials in a cookie is not a good one.
I’ve made some research on the implementation details and came across the sentence “you should not do it” so many times to drop the idea.
I’ve gone for a temporary and middle-ground solution that I will further research later and will leverage the plugin structure to do so.

A credentials store abstraction

Whatever the implementation details will be I will need to create, read, update and delete credentials on a user base.
Such a storing and retrieval might be based on PHP sessions, cookies, database options or any other system: in this phase I do not care and will hence work on an interface:

// file src/Credentials/CredentialsInterface.php

<?php

namespace tad\FrontToBack\Credentials;


interface CredentialsInterface {

    public function get_for_user( $user_id );

    public function set_for_user( $user_id, $data );

    public function delete_for_user( $user_id );
}

This interface in place I will be able to rewrite the Filesystem class tests to use it.
I’m using wp-browser to carry out the tests and function-mocker to mock and assert object instances and functions.
See full test class code here.

<?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 try to get stored credentials before requiring them
     */
    public function it_should_try_to_get_stored_credentials_before_requiring_them() {
        $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', true );
        Test::replace( 'WP_Filesystem', true );
        $credentials = Test::replace( '\tad\FrontToBack\Credentials\CredentialsInterface' )
                           ->method( 'get_for_user' )
                           ->get();

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

        $credentials->wasCalledWithOnce( [ get_current_user_id() ], 'get_for_user' );
    }

    /**
     * @test
     * it should delete invalid stored credentials
     */
    public function it_should_delete_invalid_stored_credentials() {
        $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', true );
        Test::replace( 'WP_Filesystem', false );
        $credentials = Test::replace( '\tad\FrontToBack\Credentials\CredentialsInterface' )
                           ->method( 'get_for_user' )
                           ->method( 'delete_for_user' )
                           ->get();

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

        $credentials->wasCalledWithOnce( [ get_current_user_id() ], 'delete_for_user' );
    }

    /**
     * @test
     * it should store valid credentials
     */
    public function it_should_store_valid_credentials() {
        $creds                          = 'valid_credentials';
        $request_filesystem_credentials = Test::replace( 'request_filesystem_credentials', $creds );
        Test::replace( 'WP_Filesystem', true );
        $credentials = Test::replace( '\tad\FrontToBack\Credentials\CredentialsInterface' )
                           ->method( 'get_for_user', null )
                           ->method( 'set_for_user' )
                           ->get();

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

        $credentials->wasCalledWithOnce( [ get_current_user_id(), $creds ], 'set_for_user' );
    }

}

And after some TDD game of red and green lights I will land myself a working filesystem abstraction class.
See full class code here.

<?php

namespace tad\FrontToBack\Templates;


use tad\FrontToBack\Credentials\Credentials;
use tad\FrontToBack\Credentials\CredentialsInterface;

class Filesystem {

    /**
     * @var string The file extension of the template files.
     */
    protected $templates_extension;

    /**
     * @var string The absolute path to the templates folder.
     */
    private $templates_root_folder;

    /**
     * @var string The absolute path to the master template file.
     */
    protected $master_template_path;

    /**
     * @var \WP_Filesystem_Base
     */
    protected $wpfs;
    /**
     * @var CredentialsInterface
     */
    private $credentials;


    public function initialize_wp_filesystem( $templates_root_folder = null, $url = null ) {
        if ( empty( $this->wpfs ) ) {
            $user_id     = get_current_user_id();
            $credentials = $this->credentials->get_for_user( $user_id );

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

            if ( ! WP_Filesystem( $credentials ) ) {
                $this->credentials->delete_for_user( $user_id );
                request_filesystem_credentials( $url, '', true, $templates_root_folder, null );

                return false;
            }

            global $wp_filesystem;
            $this->wpfs = $wp_filesystem;
            $this->credentials->set_for_user( $user_id, $credentials );
        }

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

    public function __construct( $templates_root_folder = null, \WP_Filesystem_Base $wpfs = null, CredentialsInterface $credentials = 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->credentials           = $credentials ? $credentials : new Credentials();

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

    /** 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();
    }
}

To allow for the class to work without an injected CredentialsInterface object I’m defaulting the Filesystem::credentials property to a new instance of the NonStoringCredentials class.

Leveraging dependency resolution

Since the whole plugin is bootstrapped in the init.php file that’s the place where which class should be used to store and retrieve credentials should be set.
While that’s currently not the case an option might one day allow a user to decide upon different credentials storing methods and the solution below allows for an easy system wide modification

<?php
use tad\FrontToBack\Credentials\NonStoringCredentials;
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( 'credentials-store', function () {
    return new NonStoringCredentials();
} );

$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, null, ftb()->get( 'credentials-store' ) );
} );

$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 code below in particular

$plugin->set( 'credentials-store', function () {
    return new NonStoringCredentials();
} );

$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, null, ftb()->get( 'credentials-store' ) );
} );

could one day be modified to this

$plugin->set( 'credentials-store', function () {
    $credential_store_method = get_option('ftb_credential_store_method', 'not_storing');

    switch $credential_store_method:
        case 'not_storing': return new NonStoringCredentials();
        case 'cookie': return new CookieCredentials();
        case 'session': return new SessionCredentials();
        case 'ssh': return new SSHCredentials();
        default: return new NonStoringCredentials();

} );

$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, null, ftb()->get( 'credentials-store' ) );
} );

And allow me to come over on this code later when I will have researched the credential storing issue with more care.

Next

I will move into the parsing of the template files to dig into the core ot the front-to-back plugin.

Probably related

I appreciate your input