1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Beuser\Controller;
17
18use TYPO3\CMS\Backend\Authentication\Event\SwitchUserEvent;
19use TYPO3\CMS\Backend\Authentication\PasswordReset;
20use TYPO3\CMS\Backend\Utility\BackendUtility;
21use TYPO3\CMS\Beuser\Domain\Model\BackendUser;
22use TYPO3\CMS\Beuser\Domain\Model\Demand;
23use TYPO3\CMS\Beuser\Domain\Repository\BackendUserGroupRepository;
24use TYPO3\CMS\Beuser\Domain\Repository\BackendUserRepository;
25use TYPO3\CMS\Beuser\Domain\Repository\BackendUserSessionRepository;
26use TYPO3\CMS\Beuser\Service\ModuleDataStorageService;
27use TYPO3\CMS\Beuser\Service\UserInformationService;
28use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
29use TYPO3\CMS\Core\Context\Context;
30use TYPO3\CMS\Core\Messaging\FlashMessage;
31use TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface;
32use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
33use TYPO3\CMS\Core\Session\SessionManager;
34use TYPO3\CMS\Core\Utility\GeneralUtility;
35use TYPO3\CMS\Core\Utility\HttpUtility;
36use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
37use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
38use TYPO3\CMS\Extbase\Mvc\RequestInterface;
39use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
40use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
41use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
42
43/**
44 * Backend module user administration controller
45 * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
46 */
47class BackendUserController extends ActionController
48{
49    /**
50     * @var int
51     */
52    const RECENT_USERS_LIMIT = 3;
53
54    /**
55     * @var \TYPO3\CMS\Beuser\Domain\Model\ModuleData
56     */
57    protected $moduleData;
58
59    /**
60     * @var ModuleDataStorageService
61     */
62    protected $moduleDataStorageService;
63
64    /**
65     * @var BackendUserRepository
66     */
67    protected $backendUserRepository;
68
69    /**
70     * @var BackendUserGroupRepository
71     */
72    protected $backendUserGroupRepository;
73
74    /**
75     * @var BackendUserSessionRepository
76     */
77    protected $backendUserSessionRepository;
78
79    /**
80     * @var UserInformationService
81     */
82    protected $userInformationService;
83
84    public function __construct(
85        ModuleDataStorageService $moduleDataStorageService,
86        BackendUserRepository $backendUserRepository,
87        BackendUserGroupRepository $backendUserGroupRepository,
88        BackendUserSessionRepository $backendUserSessionRepository,
89        UserInformationService $userInformationService
90    ) {
91        $this->moduleDataStorageService = $moduleDataStorageService;
92        $this->backendUserRepository = $backendUserRepository;
93        $this->backendUserGroupRepository = $backendUserGroupRepository;
94        $this->backendUserSessionRepository = $backendUserSessionRepository;
95        $this->userInformationService = $userInformationService;
96    }
97
98    /**
99     * Load and persist module data
100     *
101     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
102     * @param \TYPO3\CMS\Extbase\Mvc\ResponseInterface $response
103     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
104     */
105    public function processRequest(RequestInterface $request, ResponseInterface $response)
106    {
107        $this->moduleData = $this->moduleDataStorageService->loadModuleData();
108        // We "finally" persist the module data.
109        try {
110            parent::processRequest($request, $response);
111            $this->moduleDataStorageService->persistModuleData($this->moduleData);
112        } catch (StopActionException $e) {
113            $this->moduleDataStorageService->persistModuleData($this->moduleData);
114            throw $e;
115        }
116    }
117
118    /**
119     * Assign default variables to view
120     * @param ViewInterface $view
121     */
122    protected function initializeView(ViewInterface $view)
123    {
124        $view->assignMultiple([
125            'shortcutLabel' => 'backendUsers',
126            'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'],
127            'timeFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
128        ]);
129    }
130
131    /**
132     * Displays all BackendUsers
133     * - Switch session to different user
134     *
135     * @param \TYPO3\CMS\Beuser\Domain\Model\Demand $demand
136     */
137    public function indexAction(Demand $demand = null)
138    {
139        if ($demand === null) {
140            $demand = $this->moduleData->getDemand();
141        } else {
142            $this->moduleData->setDemand($demand);
143        }
144        // Switch user until logout
145        $switchUser = (int)GeneralUtility::_GP('SwitchUser');
146        if ($switchUser > 0) {
147            $this->switchUser($switchUser);
148        }
149        $compareUserList = $this->moduleData->getCompareUserList();
150
151        $this->view->assignMultiple([
152            'onlineBackendUsers' => $this->getOnlineBackendUsers(),
153            'demand' => $demand,
154            'backendUsers' => $this->backendUserRepository->findDemanded($demand),
155            'backendUserGroups' => array_merge([''], $this->backendUserGroupRepository->findAll()->toArray()),
156            'compareUserUidList' => array_combine($compareUserList, $compareUserList),
157            'currentUserUid' => $this->getBackendUserAuthentication()->user['uid'],
158            'compareUserList' => !empty($compareUserList) ? $this->backendUserRepository->findByUidList($compareUserList) : '',
159        ]);
160    }
161
162    /**
163     * Views all currently logged in BackendUsers and their sessions
164     */
165    public function onlineAction()
166    {
167        $onlineUsersAndSessions = [];
168        $onlineUsers = $this->backendUserRepository->findOnline();
169        foreach ($onlineUsers as $onlineUser) {
170            $onlineUsersAndSessions[] = [
171                'backendUser' => $onlineUser,
172                'sessions' => $this->backendUserSessionRepository->findByBackendUser($onlineUser)
173            ];
174        }
175
176        $currentSessionId = $this->getBackendUserAuthentication()->getSessionId();
177        $sessionBackend = $this->getSessionBackend();
178        if ($sessionBackend instanceof HashableSessionBackendInterface) {
179            $currentSessionId = $sessionBackend->hash($currentSessionId);
180        }
181        $this->view->assignMultiple([
182            'shortcutLabel' => 'onlineUsers',
183            'onlineUsersAndSessions' => $onlineUsersAndSessions,
184            'currentSessionId' => $currentSessionId,
185        ]);
186    }
187
188    /**
189     * @param int $uid
190     */
191    public function showAction(int $uid = 0): void
192    {
193        $data = $this->userInformationService->getUserInformation($uid);
194        $this->view->assignMultiple([
195            'shortcutLabel' => 'showUser',
196            'data' => $data
197        ]);
198    }
199
200    /**
201     * Compare backend users from demand
202     */
203    public function compareAction()
204    {
205        $compareUserList = $this->moduleData->getCompareUserList();
206        if (empty($compareUserList)) {
207            $this->redirect('index');
208        }
209
210        $compareData = [];
211        foreach ($compareUserList as $uid) {
212            if ($compareInformation = $this->userInformationService->getUserInformation($uid)) {
213                $compareData[] = $compareInformation;
214            }
215        }
216
217        $this->view->assignMultiple([
218            'shortcutLabel' => 'compareUsers',
219            'compareUserList' => $compareData,
220            'onlineBackendUsers' => $this->getOnlineBackendUsers()
221        ]);
222    }
223
224    /**
225     * Starts the password reset process for a selected user.
226     *
227     * @param int $user
228     */
229    public function initiatePasswordResetAction(int $user): void
230    {
231        $context = GeneralUtility::makeInstance(Context::class);
232        /** @var BackendUser $user */
233        $user = $this->backendUserRepository->findByUid($user);
234        if (!$user || !$user->isPasswordResetEnabled() || !$context->getAspect('backend.user')->isAdmin()) {
235            // Add an error message
236            $this->addFlashMessage(
237                LocalizationUtility::translate('LLL:EXT:beuser/Resources/Private/Language/locallang.xlf:flashMessage.resetPassword.error.text', 'beuser') ?? '',
238                LocalizationUtility::translate('LLL:EXT:beuser/Resources/Private/Language/locallang.xlf:flashMessage.resetPassword.error.title', 'beuser') ?? '',
239                FlashMessage::ERROR
240            );
241        } else {
242            GeneralUtility::makeInstance(PasswordReset::class)->initiateReset(
243                $GLOBALS['TYPO3_REQUEST'],
244                $context,
245                $user->getEmail()
246            );
247            $this->addFlashMessage(
248                LocalizationUtility::translate('LLL:EXT:beuser/Resources/Private/Language/locallang.xlf:flashMessage.resetPassword.success.text', 'beuser', [$user->getEmail()]) ?? '',
249                LocalizationUtility::translate('LLL:EXT:beuser/Resources/Private/Language/locallang.xlf:flashMessage.resetPassword.success.title', 'beuser') ?? '',
250                FlashMessage::OK
251            );
252        }
253        $this->forward('index');
254    }
255
256    /**
257     * Attaches one backend user to the compare list
258     *
259     * @param int $uid
260     */
261    public function addToCompareListAction($uid)
262    {
263        $this->moduleData->attachUidCompareUser($uid);
264        $this->moduleDataStorageService->persistModuleData($this->moduleData);
265        $this->forward('index');
266    }
267
268    /**
269     * Removes given backend user to the compare list
270     *
271     * @param int $uid
272     * @param int $redirectToCompare
273     */
274    public function removeFromCompareListAction($uid, int $redirectToCompare = 0)
275    {
276        $this->moduleData->detachUidCompareUser($uid);
277        $this->moduleDataStorageService->persistModuleData($this->moduleData);
278        if ($redirectToCompare) {
279            $this->redirect('compare');
280        } else {
281            $this->redirect('index');
282        }
283    }
284
285    /**
286     * Removes all backend users from the compare list
287     */
288    public function removeAllFromCompareListAction(): void
289    {
290        foreach ($this->moduleData->getCompareUserList() as $user) {
291            $this->moduleData->detachUidCompareUser($user);
292        }
293        $this->moduleDataStorageService->persistModuleData($this->moduleData);
294        $this->redirect('index');
295    }
296
297    /**
298     * Terminate BackendUser session and logout corresponding client
299     * Redirects to onlineAction with message
300     *
301     * @param \TYPO3\CMS\Beuser\Domain\Model\BackendUser $backendUser
302     * @param string $sessionId
303     */
304    protected function terminateBackendUserSessionAction(BackendUser $backendUser, $sessionId)
305    {
306        // terminating value of persisted session ID (probably hashed value)
307        $sessionBackend = $this->getSessionBackend();
308        $success = $sessionBackend->remove($sessionId);
309
310        if ($success) {
311            $this->addFlashMessage(LocalizationUtility::translate('LLL:EXT:beuser/Resources/Private/Language/locallang.xlf:terminateSessionSuccess', 'beuser') ?? '');
312        }
313        $this->forward('online');
314    }
315
316    /**
317     * Switches to a given user (SU-mode) and then redirects to the start page of the backend to refresh the navigation etc.
318     *
319     * @param int $switchUser BE-user record that will be switched to
320     */
321    protected function switchUser($switchUser)
322    {
323        $targetUser = BackendUtility::getRecord('be_users', $switchUser);
324        if (is_array($targetUser) && $this->getBackendUserAuthentication()->isAdmin()) {
325            // Set backend user listing module as starting module for switchback
326            $this->getBackendUserAuthentication()->uc['startModuleOnFirstLogin'] = 'system_BeuserTxBeuser';
327            $this->getBackendUserAuthentication()->uc['recentSwitchedToUsers'] = $this->generateListOfMostRecentSwitchedUsers($targetUser['uid']);
328            $this->getBackendUserAuthentication()->writeUC();
329
330            // User switch   written to log
331            $this->getBackendUserAuthentication()->writelog(
332                255,
333                2,
334                0,
335                1,
336                'User %s switched to user %s (be_users:%s)',
337                [
338                    $this->getBackendUserAuthentication()->user['username'],
339                    $targetUser['username'],
340                    $targetUser['uid'],
341                ]
342            );
343
344            $this->getSessionBackend()->update(
345                $this->getBackendUserAuthentication()->getSessionId(),
346                [
347                    'ses_userid' => (int)$targetUser['uid'],
348                    'ses_backuserid' => (int)$this->getBackendUserAuthentication()->user['uid']
349                ]
350            );
351
352            $event = new SwitchUserEvent(
353                $this->getBackendUserAuthentication()->getSessionId(),
354                $targetUser,
355                (array)$this->getBackendUserAuthentication()->user
356            );
357            $this->eventDispatcher->dispatch($event);
358
359            $redirectUrl = 'index.php' . ($GLOBALS['TYPO3_CONF_VARS']['BE']['interfaces'] ? '' : '?commandLI=1');
360            HttpUtility::redirect($redirectUrl);
361        }
362    }
363
364    /**
365     * Generates a list of users to whom where switched in the past. This is limited by RECENT_USERS_LIMIT.
366     *
367     * @param int $targetUserUid
368     * @return int[]
369     */
370    protected function generateListOfMostRecentSwitchedUsers(int $targetUserUid): array
371    {
372        $latestUserUids = [];
373        $backendUser = $this->getBackendUserAuthentication();
374
375        if (isset($backendUser->uc['recentSwitchedToUsers']) && is_array($backendUser->uc['recentSwitchedToUsers'])) {
376            $latestUserUids = $backendUser->uc['recentSwitchedToUsers'];
377        }
378
379        // Remove potentially existing user in that list
380        $index = array_search($targetUserUid, $latestUserUids, true);
381        if ($index !== false) {
382            unset($latestUserUids[$index]);
383        }
384
385        array_unshift($latestUserUids, $targetUserUid);
386        $latestUserUids = array_slice($latestUserUids, 0, static::RECENT_USERS_LIMIT);
387
388        return $latestUserUids;
389    }
390
391    /**
392     * @return BackendUserAuthentication
393     */
394    protected function getBackendUserAuthentication(): BackendUserAuthentication
395    {
396        return $GLOBALS['BE_USER'];
397    }
398
399    /**
400     * @return SessionBackendInterface
401     */
402    protected function getSessionBackend()
403    {
404        $loginType = $this->getBackendUserAuthentication()->getLoginType();
405        return GeneralUtility::makeInstance(SessionManager::class)->getSessionBackend($loginType);
406    }
407
408    /**
409     * Create an array with the uids of online users as the keys
410     * [
411     *   1 => true,
412     *   5 => true
413     * ]
414     * @return array
415     */
416    protected function getOnlineBackendUsers(): array
417    {
418        $onlineUsers = $this->backendUserSessionRepository->findAllActive();
419        $onlineBackendUsers = [];
420        if (is_array($onlineUsers)) {
421            foreach ($onlineUsers as $onlineUser) {
422                $onlineBackendUsers[$onlineUser['ses_userid']] = true;
423            }
424        }
425        return $onlineBackendUsers;
426    }
427}
428