CoolCMS API

ResponseReceiverTrait uses trait:short

Receiver check of a response for the conditional `receiver` role, used by the Bazaar and Comcat `ResponsePresenter`.

Whoever may mark a response as read is the one it was sent to. When the receiver role (conditional = 1, next to owner) grants a key, the gate (ApiPermissionGuardTrait::requireApiPermission() internally, RbacCheckTrait::checkApiPermission() externally) calls self::isReceiver() / self::verifyReceiver() of this domain.

The receiver is the one the response was sent to, not its sender (hence not isOwner(): that also admits the sender, which is right for save UPDATE and delete but wrong for reading the detail that carries readKey). The caller is the receiver when at least one holds:

  1. a privileged role (currently admin) - AdvertScopeGuardTrait::advertScopeAllows();
  2. the advert owner (advert.insertUserId from the database equals jwt.userId) - same check;
  3. the advert password capability (anonymous editor) - same check;
  4. a valid read_key of this response, passed explicitly as an internal call parameter ($data['readKey']) - self::receiverReadKeyMatches().

Branches 1-3 are decided by advertScopeAllows() over the advertId loaded from the database for this response. Branch 4 compares the stored key: an empty stored key, an empty given key or a deleted response never passes. The key is never stored in the session or the JWT.

What receiver opens:

  • get (detail): the grant; externally only branches 1-3 (the key never arrives there);
  • read: self::checkPermissionForRead() (#[ConditionallyUpdatable] on Response::$read) delegates to the same check; updateColumns itself stays admin-only, a grant there would bypass the restriction to the read column;
  • markReadByKey: the grant plus the key in $data['readKey']; an internal action without action*();
  • save UPDATE and delete stay with owner (sender or advert owner), the key does not authorize them; getAll and getBy are admin-only.

Table of Contents

Methods

internalActionMarkReadByKey()  : array<string, mixed>
Marks a response as read from an e-mail link, without login, using only the key stored with the response.
advertScopeAllows()  : bool
Returns true when the caller may see the data of advert `$advertId`.
advertScopeCapability()  : string
Capability scope of this module (`bazaar.advert.edit` or `comcat.advert.edit`).
advertScopePrivilegeKey()  : string
Privilege key to ask the privileged role about: always `getAll` of this presenter, i.e. "may the caller read this collection without restriction?".
checkPermissionForRead()  : bool
Check for `#[ConditionallyUpdatable(check: 'checkPermissionForRead')]` on `Response::$read`: only the one the response was sent to may mark it as read.
getOwnedRequestId()  : int|null
isReceiver()  : bool
Internal gate: the `receiver` role granted the key, so this domain function decides.
isResponseReceiver()  : bool
Returns whether the caller is the receiver of this response. The single implementation, used by the internal gate ({@see self::isReceiver()}), the external gate ({@see self::verifyReceiver()}) and the column check {@see self::checkPermissionForRead()}.
loadScopedAdvert()  : object|null
Loads the advert from the DB by id; returns `null` when it does not exist.
markReceivedAsRead()  : Result
Writes `read = 1` via the manager (`ResponseMarkReadTrait::markRead()`); the presenter only decided.
receiverReadKeyMatches()  : bool
Compares the given key with the stored one; delegates to the manager (`ResponseMarkReadTrait::readKeyMatches()`), the single comparison implementation.
verifyReceiver()  : bool
External gate: reads only the request identifier (`id`), decides the receiver against the database; the identity comes from the JWT (including the capability). The key is never passed from outside, so branch 4 never applies. The receiver grant exists only for `get`; any other action returns false.

Methods

internalActionMarkReadByKey()

Marks a response as read from an e-mail link, without login, using only the key stored with the response.

public internalActionMarkReadByKey([int $id = 0 ][, string $key = '' ]) : array<string, mixed>

Deliberately without action*() (internal bridge from the Front presenter only), like myList: the RBAC key :Api:{Bazaar,Comcat}:Response:markReadByKey has only the receiver grant (and admin), so the gate decides via self::isReceiver() with the key in $ownedData['readKey'] (an explicit parameter of this call, never session or JWT). Externally the action does not exist (guarded by FrontApiSurfacePolicyTest::testInternalOnlyKeysHaveNoActionCounterpart()).

A wrong key, a nonexistent response, a deleted response and a response without a stored key return the same neutral result (INVALID_KEY), so the action is no oracle for response existence; a gate denial is translated to it as well. The key is not deleted after use. The reply is a bare Result envelope without response data.

Parameters
$id : int = 0
$key : string = ''
Return values
array<string, mixed>

advertScopeAllows()

Returns true when the caller may see the data of advert `$advertId`.

protected advertScopeAllows(int|null $advertId) : bool

Fail-closed on every axis: a missing or non-positive id, a nonexistent advert, a foreign owner and an invalid capability all end in false.

Parameters
$advertId : int|null
Return values
bool

advertScopeCapability()

Capability scope of this module (`bazaar.advert.edit` or `comcat.advert.edit`).

protected abstract advertScopeCapability() : string

Without the distinction a capability for Bazaar advert 5 would unlock company 5.

Return values
string

advertScopePrivilegeKey()

Privilege key to ask the privileged role about: always `getAll` of this presenter, i.e. "may the caller read this collection without restriction?".

protected abstract advertScopePrivilegeKey() : string
Return values
string

checkPermissionForRead()

Check for `#[ConditionallyUpdatable(check: 'checkPermissionForRead')]` on `Response::$read`: only the one the response was sent to may mark it as read.

protected checkPermissionForRead(int $id, mixed $value, int|null $userId) : bool

Delegates to self::isResponseReceiver() without a key (neither the external nor the column updateColumns can pass one). The sender of the response does not pass here.

IMPORTANT: decides against the database, not the payload; $id only names the response. Fail-closed: an unknown response and a response without an advert return false.

Parameters
$id : int

id of one response (bulk updateColumns calls it for each separately)

$value : mixed

value to be written; not decisive for the "read" flag

$userId : int|null

identity from the JWT, null means anonymous

Return values
bool

getOwnedRequestId()

protected abstract getOwnedRequestId() : int|null
Return values
int|null

isReceiver()

Internal gate: the `receiver` role granted the key, so this domain function decides.

protected isReceiver(object|null $entity, int|null $userId[, array<string|int, mixed> $data = [] ]) : bool

$entity comes from the $owned closure, loaded from the database; the key comes only from $data['readKey'] (a call parameter).

Parameters
$entity : object|null
$userId : int|null
$data : array<string|int, mixed> = []
Return values
bool

isResponseReceiver()

Returns whether the caller is the receiver of this response. The single implementation, used by the internal gate ({@see self::isReceiver()}), the external gate ({@see self::verifyReceiver()}) and the column check {@see self::checkPermissionForRead()}.

protected isResponseReceiver(object|null $response[, string|null $readKey = null ]) : bool
Parameters
$response : object|null

response loaded from the database (never built from the request); null means none

$readKey : string|null = null

key from the URL link (internal calls only); null or empty disables branch 4

Return values
bool

loadScopedAdvert()

Loads the advert from the DB by id; returns `null` when it does not exist.

protected abstract loadScopedAdvert(int $advertId) : object|null

IMPORTANT: never build the entity from request data; the ownership decision relies on DB state.

The return type is ?object on purpose: both shared classes are generic, so a union would need seven type parameters. instanceof in self::advertScopeAllows() narrows it and is fail-closed: anything other than an advert or company means false.

Parameters
$advertId : int
Return values
object|null

markReceivedAsRead()

Writes `read = 1` via the manager (`ResponseMarkReadTrait::markRead()`); the presenter only decided.

protected abstract markReceivedAsRead(int $id) : Result
Parameters
$id : int
Return values
Result

receiverReadKeyMatches()

Compares the given key with the stored one; delegates to the manager (`ResponseMarkReadTrait::readKeyMatches()`), the single comparison implementation.

protected abstract receiverReadKeyMatches(string|null $stored, string $given) : bool
Parameters
$stored : string|null
$given : string
Return values
bool

verifyReceiver()

External gate: reads only the request identifier (`id`), decides the receiver against the database; the identity comes from the JWT (including the capability). The key is never passed from outside, so branch 4 never applies. The receiver grant exists only for `get`; any other action returns false.

protected verifyReceiver(int|null $userId) : bool
Parameters
$userId : int|null
Return values
bool
On this page

Search results