ID)) { continue; } $role = 'Member'; $pivot = isset($user->pivot) ? $user->pivot : null; if ($pivot && isset($pivot->settings)) { $settings = maybe_unserialize($pivot->settings); if (is_array($settings)) { if (!empty($settings['is_admin'])) { $role = 'Admin'; } elseif (!empty($settings['is_viewer_only'])) { $role = 'Viewer'; } } } $displayName = isset($user->display_name) ? (string)$user->display_name : ''; $email = isset($user->user_email) ? (string)$user->user_email : ''; $record = [ 'ID' => (int)$user->ID, 'display_name' => $displayName, 'photo' => fluent_boards_user_avatar($email, $displayName), 'role' => $role ]; // Explicit allow-list: anything not in PUBLIC_USER_FIELDS can never reach the response. $sanitizedUsers[] = array_intersect_key($record, array_flip(self::PUBLIC_USER_FIELDS)); } return $sanitizedUsers; } /** * Swap a user relation on a model for its public-safe list before the model is serialized. * * Loaded relations override attributes of the same name during toArray()/JSON encoding, * so assigning the sanitized list as an attribute alone is not enough: the relation must be * unloaded first. Only the sanitized plain array is left on the model. */ public static function replaceUserRelation($model, $relation) { if ($model->relationLoaded($relation)) { $users = $model->getRelation($relation); } else { $users = $model->$relation()->get(); } $model->unsetRelation($relation); $model->setAttribute($relation, self::sanitizeUsers($users)); return $model; } /** * Defense in depth for public responses: drop any still-loaded relation that would * serialize a WordPress user model, whatever name it was loaded under. */ public static function stripUserRelations($model) { foreach ($model->getRelations() as $name => $value) { if (self::containsUserModel($value)) { $model->unsetRelation($name); } } return $model; } private static function containsUserModel($value) { if ($value instanceof User) { return true; } foreach (self::toIterable($value) as $item) { if ($item instanceof User) { return true; } } return false; } private static function toIterable($value) { if (is_array($value)) { return $value; } if (is_object($value) && method_exists($value, 'all')) { return (array)$value->all(); } if ($value instanceof \Traversable) { return iterator_to_array($value, false); } return []; } private static function signature($boardId) { $secret = self::getSecretForBoard($boardId); return hash_hmac('sha256', 'fluent_boards_public_board_' . absint($boardId), $secret); } private static function getSecretForBoard($boardId) { $board = Board::find($boardId); $perBoardSalt = $board ? $board->getMetaByKey('public_token_salt') : ''; return wp_salt('auth') . $perBoardSalt; } public static function revokeAccessToken($boardId) { $board = Board::find($boardId); if ($board) { $board->updateMeta('public_token_salt', wp_generate_password(32, true, true)); } } }