4

Craft 3.2 updates how the allowAnonymous behavior works. I have things working in our plugins but I don't understand the different syntaxes needed across our different allowAnonymous use cases.

For example, Sprout Forms fails to submit unless we update the action to use the new self::ALLOW_ANONYMOUS_LIVE setting. However, Sprout SEO XML Sitemaps work fine without any updates. When I look through Craft I see some methods using the conventions (install controller, preview controller) and others not using any new conventions (generate thumb, live preview controller)

In one case, the Sprout Address Field controller, the prefix action- seems to matter now and removing it alone, gets things updated and working again:

// Failure while logged out
protected $allowAnonymous = [
    'action-update-address-form-html'
];

// Success while logged out
protected $allowAnonymous = [
    'update-address-form-html'
];

// Success while logged out
protected $allowAnonymous = [
    'update-address-form-html' => self::ALLOW_ANONYMOUS_LIVE
];

The general question is: what has been updated with the allowAnonymous behavior in Craft 3.2 and what scenarios does it require that we use the additional configuration settings such as ALLOW_ANONYMOUS_LIVE and ALLOW_ANONYMOUS_OFFLINE?

1 Answer 1

8

Here are the new constants that Craft 3.2 introduced:

const ALLOW_ANONYMOUS_NEVER = 0;
const ALLOW_ANONYMOUS_LIVE = 1;
const ALLOW_ANONYMOUS_OFFLINE = 2;

By default every controller will have the following property:

protected $allowAnonymous = self::ALLOW_ANONYMOUS_NEVER;

And you can override it in your controller in various ways:

// Allow anonymous access to all actions only when the site is live
public $allowAnonymous = true;

// Allow anonymous access to the `preview` action only when the site is live
public $allowAnonymous = ['preview'];

// Allow anonymous access to the `preview` action only when the site is live
public $allowAnonymous = ['preview' => self::ALLOW_ANONYMOUS_LIVE];

// Allow anonymous access to the `preview` action only when the site is offline
public $allowAnonymous = ['preview' => self::ALLOW_ANONYMOUS_OFFLINE];

// Allow anonymous access to the `preview` action when the site is live or offline
public $allowAnonymous = ['preview' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE];

The single pipeline | is a bitwise OR operator. It compares each bit (a binary digit of 0 or 1) in the integers and returns a new integer with a 1 wherever either numbers had a 1, and a 0 anywhere else (similar to how the || operator campares booleans). So the expression self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE is the same as 1 | 2 and will evaluate to 3. Here are some examples of how the | bitwise operator works:

( 0 = 0000) | ( 0 = 0000) => ( 0 = 0000)
( 0 = 0000) | ( 1 = 0001) => ( 1 = 0001)
( 0 = 0000) | ( 2 = 0010) => ( 2 = 0010)
( 1 = 0001) | ( 1 = 0001) => ( 1 = 0001)
( 1 = 0001) | ( 2 = 0010) => ( 3 = 0011)
( 2 = 0010) | ( 2 = 0010) => ( 2 = 0010)

The init() method of the Craft base controller now normalizes the value as follows:

// Normalize $allowAnonymous
if (is_bool($this->allowAnonymous)) {
    $this->allowAnonymous = (int)$this->allowAnonymous;
} else if (is_array($this->allowAnonymous)) {
    $normalized = [];
    foreach ($this->allowAnonymous as $k => $v) {
        if (
            (is_int($k) && !is_string($v)) ||
            (is_string($k) && !is_int($v))
        ) {
            throw new InvalidArgumentException("Invalid \$allowAnonymous value for key \"{$k}\"");
        }
        if (is_int($k)) {
            $normalized[$v] = self::ALLOW_ANONYMOUS_LIVE;
        } else {
            $normalized[$k] = $v;
        }
    }
    $this->allowAnonymous = $normalized;
} else if (!is_int($this->allowAnonymous)) {
    throw new InvalidConfigException('Invalid $allowAnonymous value');
}

Source: https://github.com/craftcms/cms/blob/3.2.2/src/web/Controller.php#L75-L96

So if the value is a boolean then false will be normalized to 0, whereas true will be normalized to 1.

If the value is an array then each key-value pair in the array will be inspected. If the key is an integer then the value is set to 1, for example:

public $allowAnonymous = ['preview'];

equates to

public $allowAnonymous = [0 => 'preview'];

and is interpreted as

public $allowAnonymous = ['preview' => self::ALLOW_ANONYMOUS_LIVE];

If the key is a not an integer then the key-value pair is left as is.

Here is how $allowAnonymous is enforced:

// Enforce $allowAnonymous
$isLive = Craft::$app->getIsLive();
$test = $isLive ? self::ALLOW_ANONYMOUS_LIVE : self::ALLOW_ANONYMOUS_OFFLINE;

if (is_int($this->allowAnonymous)) {
    $allowAnonymous = $this->allowAnonymous;
} else {
    $allowAnonymous = $this->allowAnonymous[$action->id] ?? self::ALLOW_ANONYMOUS_NEVER;
}

if (!($test & $allowAnonymous)) {
    // Prevent access unless the user is logged in and has the required permissions to view the site
}

Source: https://github.com/craftcms/cms/blob/3.2.2/src/web/Controller.php#L147-L176

The single ampersand & is a bitwise AND operator. It compares each bit in the integers and returns a new integer with a 1 wherever both numbers have a 1, and a 0 anywhere else (similar to how the && operator campares booleans). Here are some examples of how the & bitwise operator works:

( 0 = 0000) & ( 0 = 0000) => ( 0 = 0000) => false
( 0 = 0000) & ( 1 = 0001) => ( 0 = 0000) => false
( 0 = 0000) & ( 2 = 0010) => ( 0 = 0000) => false
( 1 = 0001) & ( 1 = 0001) => ( 1 = 0001) => true
( 1 = 0001) & ( 2 = 0010) => ( 0 = 0000) => false
( 2 = 0010) & ( 2 = 0010) => ( 2 = 0010) => true
( 3 = 0011) & ( 1 = 0001) => ( 1 = 0001) => true
( 3 = 0011) & ( 2 = 0010) => ( 2 = 0010) => true
( 3 = 0011) & ( 3 = 0011) => ( 3 = 0011) => true

By enforcing the condition $test & $allowAnonymous, we can check that $test (whether the site is live or offline) and $allowAnonymous (the permissions granted to the specific controller action) overlap.

More on bitwise operators here.


I'm not sure what the specific issue with your plugin is , but as far as I can tell the only thing that changed in Craft 3.2 was that the ALLOW_ANONYMOUS_OFFLINE constant was added, allowing for (but not requiring) more specific permissions on controller actions.

3
  • It appears the issue we had in Sprout Forms was because we were not calling parent::init(); in our init method. Adding that allows us to remove the self::ALLOW_ANONYMOUS_LIVE addition and if it was in place to begin with, no change would have been necessary. Commented Jul 15, 2019 at 15:04
  • Also, thanks for the break down of how the strings get translated. Nice detail. Commented Jul 15, 2019 at 15:06
  • Yeah that must have been it. In Craft and Yii, if you ever override the init() method then you should always call the parent method with parent::init() :)
    – Ben Croker
    Commented Jul 15, 2019 at 15:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.