patricknelson/silverstripe-migrations

Use closure to neatly encapsulate state.

patricknelson opened this issue · 2 comments

When creating a temporary admin account, we need to execute code in a neatly encapsulated manner since we have to maintain global state. I think we should completely remove (or at least deprecate and set a new major version) the use of loginAsAdmin() method with a superior whileAdmin($closure) method.

This is because a temp admin is created sometimes (if one isn't available) but never deleted and it's sloppy to do this OUTSIDE of this scope!

e.g.

static::whileAdmin(function() {
    // ... do fancy admin-only stuff here, like...
    $page->doPublish();
});

Proof of concept:

/**
 * The intent with this method is to allow it to maintain it's own state while allowing you to execute your own
 * arbitrary code within that state (i.e. while logged in as an administrator).
 *
 * @param   callable    $closure    The closure (or class/method array) that you'd like to execute while logged in
 *                                  as an admin.
 * @throws  Exception|MigrationException
 */
protected static function whileAdmin(callable $closure) {
    // Keeps track of the fact that a temporary admin was just created so we can delete it later.
    $tempAdmin = false;
    $admin = null;

    if (!Member::currentUserID()) {
        // See if a default admin is setup yet.
        if (!Security::has_default_admin()) {
            // Generate a randomized user/pass and use that as the default administrator just for this session.
            $tempAdmin = true;
            $user = substr(str_shuffle(sha1("u" . microtime())), 0, 20);
            $pass = substr(str_shuffle(sha1("p" . microtime())), 0, 20);
            Security::setDefaultAdmin($user, $pass);
        }

        $admin = Member::default_admin();
        if (!$admin) throw new MigrationException("Cannot login: No default administrator found.");

        Session::start();
        Session::set("loggedInAs", $admin->ID);
    }

    // Call passed closure.
    try {
        call_user_func($closure);
    } catch(Exception $e) {}

    // Clean up.
    Session::set("loggedInAs", null);
    if ($tempAdmin && $admin) $admin->delete();

    // Throw the exception if one occurred (in lieu of a "finally" block in older PHP versions).
    if (isset($e)) throw $e;
}

PS: May need to also account for the possibility that a user may already be logged in somehow and do not want to upset that in the line above:

    Session::set("loggedInAs", null);