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:
- a privileged role (currently
admin) - AdvertScopeGuardTrait::advertScopeAllows(); - the advert owner (
advert.insertUserIdfrom the database equalsjwt.userId) - same check; - the advert password capability (anonymous editor) - same check;
- a valid
read_keyof 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]onResponse::$read) delegates to the same check;updateColumnsitself stays admin-only, a grant there would bypass the restriction to thereadcolumn;markReadByKey: the grant plus the key in$data['readKey']; an internal action withoutaction*();saveUPDATE anddeletestay withowner(sender or advert owner), the key does not authorize them;getAllandgetByare 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
booladvertScopeCapability()
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
stringadvertScopePrivilegeKey()
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
stringcheckPermissionForRead()
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
updateColumnscalls it for each separately) - $value : mixed
-
value to be written; not decisive for the "read" flag
- $userId : int|null
-
identity from the JWT,
nullmeans anonymous
Return values
boolgetOwnedRequestId()
protected
abstract getOwnedRequestId() : int|null
Return values
int|nullisReceiver()
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
boolisResponseReceiver()
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);
nullmeans none - $readKey : string|null = null
-
key from the URL link (internal calls only);
nullor empty disables branch 4
Return values
boolloadScopedAdvert()
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|nullmarkReceivedAsRead()
Writes `read = 1` via the manager (`ResponseMarkReadTrait::markRead()`); the presenter only decided.
protected
abstract markReceivedAsRead(int $id) : Result
Parameters
- $id : int
Return values
ResultreceiverReadKeyMatches()
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
boolverifyReceiver()
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