Tims-Chat/files/lib/data/room/RoomAction.class.php

493 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*
* Copyright (c) 2010-2022 Tim Düsterhus.
*
* Use of this software is governed by the Business Source License
* included in the LICENSE file.
*
* Change Date: 2026-09-17
*
* On the date above, in accordance with the Business Source
* License, use of this software will be governed by version 2
* or later of the General Public License.
*/
namespace chat\data\room;
use chat\data\command\CommandCache;
use chat\data\message\MessageAction;
use chat\data\user\User as ChatUser;
use chat\data\user\UserAction as ChatUserAction;
use chat\system\box\RoomListBoxController;
use wcf\data\AbstractDatabaseObjectAction;
use wcf\data\box\Box;
use wcf\data\ISortableAction;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\package\PackageCache;
use wcf\data\user\UserProfile;
use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\event\EventHandler;
use wcf\system\exception\PermissionDeniedException;
use wcf\system\exception\UserInputException;
use wcf\system\push\PushHandler;
use wcf\system\user\activity\point\UserActivityPointHandler;
use wcf\system\WCF;
/**
* Executes chat room-related actions.
*/
class RoomAction extends AbstractDatabaseObjectAction implements ISortableAction
{
/**
* @inheritDoc
*/
protected $permissionsDelete = [
'admin.chat.canManageRoom',
];
/**
* @inheritDoc
*/
protected $permissionsUpdate = [
'admin.chat.canManageRoom',
];
/**
* Validates parameters and permissions.
*/
public function validateJoin()
{
unset($this->parameters['user']);
$this->readString('sessionID');
$this->parameters['sessionID'] = \pack(
'H*',
\str_replace('-', '', $this->parameters['sessionID'])
);
$this->readInteger('roomID');
$room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
if ($room === null) {
throw new UserInputException('roomID');
}
if (!$room->canSee($user = null, $reason)) {
throw $reason;
}
if (!$room->canJoin($user = null, $reason)) {
throw $reason;
}
}
/**
* Makes the given user join the current chat room.
*/
public function join()
{
$objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName(
'be.bastelstu.chat.messageType',
'be.bastelstu.chat.messageType.join'
);
if (!$objectTypeID) {
throw new \LogicException('Missing object type');
}
// User cannot be set during an AJAX request, but may be set by Tims Chat itself.
if (!isset($this->parameters['user'])) {
$this->parameters['user'] = WCF::getUser();
}
$user = new ChatUser($this->parameters['user']);
// Check parameters
$room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
if ($room === null) {
throw new UserInputException('roomID');
}
$sessionID = $this->parameters['sessionID'];
if (\strlen($sessionID) !== 16) {
throw new UserInputException('sessionID');
}
try {
// Create room_to_user mapping.
$sql = "INSERT INTO chat1_room_to_user
(active, roomID, userID)
VALUES (?, ?, ?)";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([ 0, $room->roomID, $user->userID ]);
} catch (\wcf\system\database\exception\DatabaseException $e) {
// Ignore if there already is a mapping.
if ((string)$e->getCode() !== '23000') {
throw $e;
}
}
try {
$sql = "INSERT INTO chat1_session
(roomID, userID, sessionID, lastRequest)
VALUES (?, ?, ?, ?)";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([
$room->roomID,
$user->userID,
$sessionID,
TIME_NOW,
]);
} catch (\wcf\system\database\exception\DatabaseException $e) {
if ((string)$e->getCode() !== '23000') {
throw $e;
}
throw new UserInputException('sessionID');
}
$markAsBack = static function () use ($user, $room) {
$userProfile = new UserProfile($user->getDecoratedObject());
$package = PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat');
$command = CommandCache::getInstance()->getCommandByPackageAndIdentifier($package, 'back');
$processor = $command->getProcessor();
$processor->execute([ ], $room, $userProfile);
};
if ($user->chatAway !== null) {
$markAsBack();
}
// Attempt to mark the user as active in the room.
$sql = "UPDATE chat1_room_to_user
SET active = ?
WHERE roomID = ?
AND userID = ?";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([ 1, $room->roomID, $user->userID ]);
if ($statement->getAffectedRows() === 0) {
// The User already is inside the room: Nothing to do here.
return;
}
// Update lastPull. This must not be merged into the above query, because of the 'getAffectedRows' check.
$sql = "UPDATE chat1_room_to_user
SET lastPull = ?
WHERE roomID = ?
AND userID = ?";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([ TIME_NOW, $room->roomID, $user->userID ]);
(new MessageAction(
[ ],
'create',
[
'data' => [
'roomID' => $room->roomID,
'userID' => $user->userID,
'username' => $user->username,
'time' => TIME_NOW,
'objectTypeID' => $objectTypeID,
'payload' => \serialize([ ]),
],
]
))->executeAction();
UserActivityPointHandler::getInstance()->fireEvent(
'be.bastelstu.chat.activityPointEvent.join',
0,
$user->userID
);
$pushHandler = PushHandler::getInstance();
$pushHandler->sendMessage([
'message' => 'be.bastelstu.chat.join',
'target' => 'registered',
]);
}
/**
* Validates parameters and permissions.
*/
public function validateLeave()
{
unset($this->parameters['user']);
$this->readString('sessionID');
$this->parameters['sessionID'] = \pack(
'H*',
\str_replace('-', '', $this->parameters['sessionID'])
);
$this->readInteger('roomID');
$room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
if ($room === null) {
throw new UserInputException('roomID');
}
// Do not check permissions: If the user is not inside the room nothing happens, if he is it
// may lead to a faster eviction of the user.
}
/**
* Makes the given user leave the current chat room.
*/
public function leave()
{
$objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName(
'be.bastelstu.chat.messageType',
'be.bastelstu.chat.messageType.leave'
);
if ($objectTypeID) {
// User cannot be set during an AJAX request, but may be set by Tims Chat itself.
if (!isset($this->parameters['user'])) {
$this->parameters['user'] = WCF::getUser();
}
$user = new ChatUser($this->parameters['user']);
$room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
if ($room === null) {
throw new UserInputException('roomID');
}
$sessionID = null;
if (isset($this->parameters['sessionID'])) {
$sessionID = $this->parameters['sessionID'];
if (\strlen($sessionID) !== 16) {
throw new UserInputException('sessionID');
}
}
// Delete session.
$condition = new PreparedStatementConditionBuilder();
$condition->add('roomID = ?', [ $room->roomID ]);
$condition->add('userID = ?', [ $user->userID ]);
if ($sessionID !== null) {
$condition->add('sessionID = ?', [ $sessionID ]);
}
$sql = "DELETE FROM chat1_session
{$condition}";
$statement = WCF::getDB()->prepare($sql);
$statement->execute($condition->getParameters());
if ($statement->getAffectedRows() === 0) {
throw new UserInputException('sessionID');
}
try {
$commited = false;
WCF::getDB()->beginTransaction();
// Check whether we deleted the last session.
$sql = "SELECT COUNT(*)
FROM chat1_session
WHERE roomID = ?
AND userID = ?";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([ $room->roomID, $user->userID ]);
// We did not: Nothing to do here.
if ($statement->fetchColumn()) {
return;
}
// Mark the user as inactive.
$sql = "UPDATE chat1_room_to_user
SET active = ?
WHERE roomID = ?
AND userID = ?";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([ 0, $room->roomID, $user->userID ]);
\assert($statement->getAffectedRows() > 0);
WCF::getDB()->commitTransaction();
$commited = true;
} finally {
if (!$commited) {
WCF::getDB()->rollBackTransaction();
}
}
(new MessageAction(
[ ],
'create',
[
'data' => [
'roomID' => $room->roomID,
'userID' => $user->userID,
'username' => $user->username,
'time' => TIME_NOW,
'objectTypeID' => $objectTypeID,
'payload' => \serialize([ ]),
],
]
))->executeAction();
$pushHandler = PushHandler::getInstance();
$pushHandler->sendMessage([
'message' => 'be.bastelstu.chat.leave',
'target' => 'registered',
]);
} else {
throw new \LogicException('Missing object type');
}
}
/**
* Validates parameters and permissions.
*/
public function validateGetUsers()
{
if (empty($this->getObjects())) {
$this->readObjects();
}
if (\count($this->getObjects()) !== 1) {
throw new UserInputException('objectIDs');
}
$room = $this->getObjects()[0];
$user = new ChatUser(WCF::getUser());
if (!$user->isInRoom($room->getDecoratedObject())) {
throw new PermissionDeniedException();
}
}
/**
* Returns the userIDs of the users in this room.
*/
public function getUsers()
{
if (empty($this->getObjects())) {
$this->readObjects();
}
if (\count($this->getObjects()) !== 1) {
throw new UserInputException('objectIDs');
}
$room = $this->getObjects()[0];
$users = (new ChatUserAction(
[ ],
'getUsersByID',
[
'userIDs' => \array_keys($room->getUsers()),
]
))->executeAction()['returnValues'];
$users = \array_map(static function (array $user) use ($room) {
$userProfile = UserProfileRuntimeCache::getInstance()->getObject($user['userID']);
if (!isset($user['permissions'])) {
$user['permissions'] = [];
}
$user['permissions']['canWritePublicly'] = $room->canWritePublicly($userProfile);
return $user;
}, $users);
EventHandler::getInstance()->fireAction($this, 'getUsers', $users);
return $users;
}
/**
* @inheritDoc
*/
public function validateUpdatePosition()
{
// validate permissions
if (\is_array($this->permissionsUpdate) && !empty($this->permissionsUpdate)) {
WCF::getSession()->checkPermissions($this->permissionsUpdate);
} else {
throw new PermissionDeniedException();
}
$this->readIntegerArray('structure', false, 'data');
$roomList = new RoomList();
$roomList->readObjects();
foreach ($this->parameters['data']['structure'][0] as $roomID) {
$room = $roomList->search($roomID);
if ($room === null) {
throw new UserInputException('structure');
}
}
}
/**
* @inheritDoc
*/
public function updatePosition()
{
$roomList = new RoomList();
$roomList->readObjects();
$i = 0;
WCF::getDB()->beginTransaction();
foreach ($this->parameters['data']['structure'][0] as $roomID) {
$room = $roomList->search($roomID);
if ($room === null) {
continue;
}
$editor = new RoomEditor($room);
$editor->update([
'position' => $i++,
]);
}
WCF::getDB()->commitTransaction();
}
/**
* Validates permissions.
*/
public function validateGetBoxRoomList()
{
if (!Room::canSeeAny()) {
throw new PermissionDeniedException();
}
$this->readBoolean('isSidebar', true);
$this->readBoolean('skipEmptyRooms', true);
$this->readInteger('activeRoomID', true);
unset($this->parameters['boxController']);
$this->readInteger('boxID', true);
if ($this->parameters['boxID']) {
$box = new Box($this->parameters['boxID']);
if ($box->boxID) {
$this->parameters['boxController'] = $box->getController();
if ($this->parameters['boxController'] instanceof RoomListBoxController) {
// all checks passed, end validation; otherwise throw the exception below
return;
}
}
throw new UserInputException('boxID');
}
}
/**
* Returns dashboard roomlist.
*/
public function getBoxRoomList()
{
if (isset($this->parameters['boxController'])) {
$this->parameters['boxController']->setActiveRoomID($this->parameters['activeRoomID']);
return [
'template' => $this->parameters['boxController']->getContent(),
];
}
// Fetch all rooms, the templates have filtering in place
$rooms = RoomCache::getInstance()->getRooms();
$template = 'boxRoomList' . ($this->parameters['isSidebar'] ? 'Sidebar' : '');
WCF::getTPL()->assign([
'boxRoomList' => $rooms,
'skipEmptyRooms' => $this->parameters['skipEmptyRooms'],
'activeRoomID' => $this->parameters['activeRoomID'],
]);
return [
'template' => WCF::getTPL()->fetch($template, 'chat'),
];
}
}