CoolCMS API

MessageThreadRolesTrait

Resolves the `owner` and `receiver` roles over a private thread for the `MessageThread`, `Message` and `MessageParticipant` Api presenters.

A thread or message has no single owner, so the roles cannot be derived from the entity (OwnerAwareInterface / ReceiverAwareInterface are not implemented here). They are derived from the authors of the individual messages:

  • owner: the caller is a thread participant and wrote at least one undeleted message.
  • receiver: the caller is a participant and the thread has an undeleted message they did not write (a system message counts).

A fresh thread is therefore only owner for its creator and only receiver for the other side; after a reply both have both roles. The permission matrix may grant each role separately. A non-member (including a guest without an account and an anonymous caller) never passes and must not learn whether the thread exists.

How it is enforced: actions over a single thread have registrated = 0, owner = 1, receiver = 1. The gate (ApiPermissionGuardTrait::requireApiPermission()) calls the presenter's isOwner() / isReceiver(), which delegate to self::isThreadOwner() / self::isThreadReceiver() and query the DB via self::resolveThreadRoles(). The thread id arrives only as ownedData['threadId'] (a call parameter) and the identity comes from the JWT; ownership is never read from the request. These actions have no action*(), so the external path (RbacCheckTrait) cannot reach them.

Lists without a single record (myThreads, myUnreadCount, MessageParticipant:myList) stay registrated = 1 with a server-side scope because a conditional role has no record to judge.

Table of Contents

Properties

$threadRolesCache  : array<string, array{member: bool, owner: bool, receiver: bool}>

Methods

isThreadOwner()  : bool
Tells whether the user owns the thread, i.e. authored at least one undeleted message.
isThreadReceiver()  : bool
Tells whether the user is a receiver, i.e. the thread has an undeleted message they did not write; false without an identity.
notMemberWriteShape()  : array<string, mixed>
Returns the response for a non-member on write actions, the same as the manager returns (`USER_UNAUTHORIZED`).
resolveThreadRoles()  : array{member: bool, owner: bool, receiver: bool}
Returns the caller's roles over the thread by querying participants and messages in the DB.
threadDenial()  : array<string, mixed>
Translates a gate denial into the response of an action over a thread.
threadIdFrom()  : int
threadRolesOf()  : array{member: bool, owner: bool, receiver: bool}

Properties

$threadRolesCache

private array<string, array{member: bool, owner: bool, receiver: bool}> $threadRolesCache = []

Methods

isThreadOwner()

Tells whether the user owns the thread, i.e. authored at least one undeleted message.

protected isThreadOwner(int $userId[, array<string|int, mixed> $data = [] ]) : bool

The thread id comes from the call parameter (ownedData) and the entity is not read, because neither a message nor a thread yields its author without a query.

The presenter wires it into the gate through its own isOwner(): the custom classification in OwnershipFallbackCoverageTest and in the permission matrix recognizes a custom check by isOwner()/isReceiver() being declared on the presenter, not on the trait.

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

isThreadReceiver()

Tells whether the user is a receiver, i.e. the thread has an undeleted message they did not write; false without an identity.

protected isThreadReceiver(int|null $userId[, array<string|int, mixed> $data = [] ]) : bool
Parameters
$userId : int|null
$data : array<string|int, mixed> = []
Return values
bool

notMemberWriteShape()

Returns the response for a non-member on write actions, the same as the manager returns (`USER_UNAUTHORIZED`).

protected static notMemberWriteShape([bool $withCode = true ]) : array<string, mixed>
Parameters
$withCode : bool = true
Return values
array<string, mixed>

resolveThreadRoles()

Returns the caller's roles over the thread by querying participants and messages in the DB.

protected abstract resolveThreadRoles(int $threadId, int $userId) : array{member: bool, owner: bool, receiver: bool}

Implemented by the presenter through its Api manager.

Parameters
$threadId : int
$userId : int
Return values
array{member: bool, owner: bool, receiver: bool}

threadDenial()

Translates a gate denial into the response of an action over a thread.

protected threadDenial(array<string, mixed> $denied, int $threadId, array<string, mixed> $notMemberShape) : array<string, mixed>

Call it only with a non-null result of requireApiPermission(), which the action returns as its first statement:

if (($denied = $this->requireApiPermission('myThreadReply', ownedData: ['threadId' => $threadId])) !== null) {
    return $this->threadDenial($denied, $threadId, self::notMemberWriteShape());
}

A denial of a non-member becomes $notMemberShape, exactly the response the manager's membership check gives (empty list / success = false), so the response shape never reveals which thread exists. A member who lacks the role grant gets the regular gate denial, and so does an anonymous caller.

Parameters
$denied : array<string, mixed>
$threadId : int
$notMemberShape : array<string, mixed>
Return values
array<string, mixed>

threadIdFrom()

private static threadIdFrom(array<string|int, mixed> $data) : int
Parameters
$data : array<string|int, mixed>
Return values
int

threadRolesOf()

private threadRolesOf(int $threadId, int $userId) : array{member: bool, owner: bool, receiver: bool}
Parameters
$threadId : int
$userId : int
Return values
array{member: bool, owner: bool, receiver: bool}
On this page

Search results