cf8151999c2116eafe9343ce4ad024f480b07620
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Backend / ToolbarItems / ShortcutToolbarItem.php
1 <?php
2 namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Backend\Module\ModuleLoader;
20 use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Core\Database\Connection;
23 use TYPO3\CMS\Core\Database\ConnectionPool;
24 use TYPO3\CMS\Core\Database\Query\QueryHelper;
25 use TYPO3\CMS\Core\Imaging\Icon;
26 use TYPO3\CMS\Core\Imaging\IconFactory;
27 use TYPO3\CMS\Core\Page\PageRenderer;
28 use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
29 use TYPO3\CMS\Core\Resource\ResourceFactory;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32
33 /**
34 * Class to render the shortcut menu
35 */
36 class ShortcutToolbarItem implements ToolbarItemInterface
37 {
38 /**
39 * @const integer Number of super global group
40 */
41 const SUPERGLOBAL_GROUP = -100;
42
43 /**
44 * @var string
45 */
46 public $perms_clause;
47
48 /**
49 * @var array
50 */
51 public $fieldArray;
52
53 /**
54 * All available shortcuts
55 *
56 * @var array
57 */
58 protected $shortcuts;
59
60 /**
61 * @var array
62 */
63 protected $shortcutGroups;
64
65 /**
66 * Labels of all groups.
67 * If value is 1, the system will try to find a label in the locallang array.
68 *
69 * @var array
70 */
71 protected $groupLabels;
72
73 /**
74 * @var IconFactory
75 */
76 protected $iconFactory;
77
78 /**
79 * @var ModuleLoader
80 */
81 protected $moduleLoader;
82
83 /**
84 * Constructor
85 */
86 public function __construct()
87 {
88 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
89 $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_misc.xlf');
90 // Needed to get the correct icons when reloading the menu after saving it
91 $this->moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
92 $this->moduleLoader->load($GLOBALS['TBE_MODULES']);
93
94 // By default, 5 groups are set
95 $this->shortcutGroups = [
96 1 => '1',
97 2 => '1',
98 3 => '1',
99 4 => '1',
100 5 => '1'
101 ];
102 $this->shortcutGroups = $this->initShortcutGroups();
103 $this->shortcuts = $this->initShortcuts();
104
105 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Toolbar/ShortcutMenu');
106 $languageService = $this->getLanguageService();
107 $this->getPageRenderer()->addInlineLanguageLabelArray([
108 'bookmark.delete' => $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarksDelete'),
109 'bookmark.confirmDelete' => $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.confirmBookmarksDelete'),
110 'bookmark.create' => $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.createBookmark'),
111 'bookmark.savedTitle' => $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarkSavedTitle'),
112 'bookmark.savedMessage' => $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarkSavedMessage'),
113 ]);
114 }
115
116 /**
117 * Checks whether the user has access to this toolbar item
118 *
119 * @return bool TRUE if user has access, FALSE if not
120 */
121 public function checkAccess()
122 {
123 return (bool)$this->getBackendUser()->getTSConfigVal('options.enableBookmarks');
124 }
125
126 /**
127 * Render shortcut icon
128 *
129 * @return string HTML
130 */
131 public function getItem()
132 {
133 $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarks'));
134 $icon = $this->iconFactory->getIcon('apps-toolbar-menu-shortcut', Icon::SIZE_SMALL)->render('inline');
135 return '
136 <span class="toolbar-item-icon" title="' . $title . '">' . $icon . '</span>
137 <span class="toolbar-item-title">' . $title . '</span>
138 ';
139 }
140
141 /**
142 * Render drop down content
143 *
144 * @return string HTML
145 */
146 public function getDropDown()
147 {
148 $languageService = $this->getLanguageService();
149 $shortcutEdit = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarksEdit'));
150 $shortcutDelete = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarksDelete'));
151 $editIcon = '<a href="#" class="dropdown-table-actions-btn dropdown-table-actions-btn-edit t3js-shortcut-edit" title="' . $shortcutEdit . '">'
152 . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render('inline') . '</a>';
153 $deleteIcon = '<a href="#" class="dropdown-table-actions-btn dropdown-table-actions-btn-delete t3js-shortcut-delete" title="' . $shortcutDelete . '">'
154 . $this->iconFactory->getIcon('actions-delete', Icon::SIZE_SMALL)->render('inline') . '</a>';
155
156 $shortcutMenu = [];
157 $shortcutMenu[] = '<h3 class="dropdown-headline">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarks')) . '</h3>';
158 $shortcutMenu[] = '<hr>';
159
160 // Render groups and the contained shortcuts
161 $groups = $this->getGroupsFromShortcuts();
162 $groupCount = 0;
163 arsort($groups, SORT_NUMERIC);
164 foreach ($groups as $groupId => $groupLabel) {
165 $shortcuts = $this->getShortcutsByGroup($groupId);
166 if (count($shortcuts) > 0) {
167 if ($groupCount !== 0) {
168 $shortcutMenu[] = '<hr>';
169 }
170 $groupCount++;
171 if ($groupLabel) {
172 $shortcutMenu[] = '<h3 class="dropdown-headline" id="shortcut-group-' . (int)$groupId . '">' . $groupLabel . '</h3>';
173 }
174 $shortcutMenu[] = '<div class="dropdown-table" data-shortcutgroup="' . (int)$groupId . '">';
175 foreach ($shortcuts as $shortcut) {
176 $shortcutItem = '<div class="dropdown-table-row t3js-topbar-shortcut" data-shortcutid="' . (int)$shortcut['raw']['uid'] . '" data-shortcutgroup="' . (int)$groupId . '">';
177 $shortcutItem .= '<div class="dropdown-table-column dropdown-table-icon">';
178 $shortcutItem .= $shortcut['icon'];
179 $shortcutItem .= '</div>';
180 $shortcutItem .= '<div class="dropdown-table-column dropdown-table-title">';
181 $shortcutItem .= '<a class="dropdown-table-title-ellipsis" href="#" onclick="' . htmlspecialchars($shortcut['action']) . ' return false;">';
182 $shortcutItem .= htmlspecialchars($shortcut['label']);
183 $shortcutItem .= '</a>';
184 $shortcutItem .= '</div>';
185 $shortcutItem .= '<div class="dropdown-table-column dropdown-table-actions">' . $editIcon . $deleteIcon . '</div>';
186 $shortcutItem .= '</div>';
187 $shortcutMenu[] = $shortcutItem;
188 }
189 $shortcutMenu[] = '</div>';
190 }
191 }
192
193 $compiledShortcutMenu = implode(LF, $shortcutMenu);
194 if (count($shortcutMenu) === 2) {
195 // No shortcuts added yet, show a small help message how to add shortcuts
196 $title = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarks'));
197 $icon = '<span title="' . $title . '">' . $this->iconFactory->getIcon('actions-system-shortcut-new', Icon::SIZE_SMALL)->render('inline') . '</span>';
198 $label = str_replace('%icon%', $icon, htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:bookmarkDescription')));
199 $compiledShortcutMenu .= '<p>' . $label . '</p>';
200 }
201
202 return $compiledShortcutMenu;
203 }
204
205 /**
206 * Renders the menu so that it can be returned as response to an AJAX call
207 *
208 * @param ServerRequestInterface $request
209 * @param ResponseInterface $response
210 * @return ResponseInterface
211 */
212 public function menuAction(ServerRequestInterface $request, ResponseInterface $response)
213 {
214 $menuContent = $this->getDropDown();
215
216 $response->getBody()->write($menuContent);
217 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
218 return $response;
219 }
220
221 /**
222 * This toolbar item needs no additional attributes
223 *
224 * @return array
225 */
226 public function getAdditionalAttributes()
227 {
228 return [];
229 }
230
231 /**
232 * This item has a drop down
233 *
234 * @return bool
235 */
236 public function hasDropDown()
237 {
238 return true;
239 }
240
241 /**
242 * Retrieves the shortcuts for the current user
243 *
244 * @return array Array of shortcuts
245 */
246 protected function initShortcuts()
247 {
248 $backendUser = $this->getBackendUser();
249 // Traverse shortcuts
250 $lastGroup = 0;
251 $shortcuts = [];
252 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
253 ->getQueryBuilderForTable('sys_be_shortcuts');
254 $result = $queryBuilder->select('*')
255 ->from('sys_be_shortcuts')
256 ->where(
257 $queryBuilder->expr()->andX(
258 $queryBuilder->expr()->eq(
259 'userid',
260 $queryBuilder->createNamedParameter($backendUser->user['uid'], \PDO::PARAM_INT)
261 ),
262 $queryBuilder->expr()->gte(
263 'sc_group',
264 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
265 )
266 )
267 )
268 ->orWhere(
269 $queryBuilder->expr()->in(
270 'sc_group',
271 $queryBuilder->createNamedParameter(
272 array_keys($this->getGlobalShortcutGroups()),
273 Connection::PARAM_INT_ARRAY
274 )
275 )
276 )
277 ->orderBy('sc_group')
278 ->addOrderBy('sorting')
279 ->execute();
280
281 while ($row = $result->fetch()) {
282 $shortcut = ['raw' => $row];
283
284 list($row['module_name'], $row['M_module_name']) = explode('|', $row['module_name']);
285
286 $queryParts = parse_url($row['url']);
287 $queryParameters = GeneralUtility::explodeUrl2Array($queryParts['query'], 1);
288 if ($row['module_name'] === 'xMOD_alt_doc.php' && is_array($queryParameters['edit'])) {
289 $shortcut['table'] = key($queryParameters['edit']);
290 $shortcut['recordid'] = key($queryParameters['edit'][$shortcut['table']]);
291 if ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] === 'edit') {
292 $shortcut['type'] = 'edit';
293 } elseif ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] === 'new') {
294 $shortcut['type'] = 'new';
295 }
296 if (substr($shortcut['recordid'], -1) === ',') {
297 $shortcut['recordid'] = substr($shortcut['recordid'], 0, -1);
298 }
299 } else {
300 $shortcut['type'] = 'other';
301 }
302 // Check for module access
303 $moduleName = $row['M_module_name'] ?: $row['module_name'];
304
305 // Check if the user has access to this module
306 if (!is_array($this->moduleLoader->checkMod($moduleName))) {
307 continue;
308 }
309 $pageId = $this->getLinkedPageId($row['url']);
310
311 if (!$backendUser->isAdmin()) {
312 if (MathUtility::canBeInterpretedAsInteger($pageId)) {
313 // Check for webmount access
314 if ($backendUser->isInWebMount($pageId) === null) {
315 continue;
316 }
317 // Check for record access
318 $pageRow = BackendUtility::getRecord('pages', $pageId);
319 if ($pageRow === null) {
320 continue;
321 }
322 if (!$backendUser->doesUserHaveAccess($pageRow, ($perms = 1))) {
323 continue;
324 }
325 }
326 }
327 $moduleParts = explode('_', $moduleName);
328 $shortcutGroup = (int)$row['sc_group'];
329 if ($shortcutGroup && $lastGroup !== $shortcutGroup && $shortcutGroup !== self::SUPERGLOBAL_GROUP) {
330 $shortcut['groupLabel'] = $this->getShortcutGroupLabel($shortcutGroup);
331 }
332 $lastGroup = $shortcutGroup;
333
334 if ($row['description']) {
335 $shortcut['label'] = $row['description'];
336 } else {
337 $shortcut['label'] = GeneralUtility::fixed_lgd_cs(rawurldecode($queryParts['query']), 150);
338 }
339 $shortcut['group'] = $shortcutGroup;
340 $shortcut['icon'] = $this->getShortcutIcon($row, $shortcut);
341 $shortcut['iconTitle'] = $this->getShortcutIconTitle($shortcut['label'], $row['module_name'], $row['M_module_name']);
342 $shortcut['action'] = 'jump(' . GeneralUtility::quoteJSvalue($this->getTokenUrl($row['url'])) . ',' . GeneralUtility::quoteJSvalue($moduleName) . ',' . GeneralUtility::quoteJSvalue($moduleParts[0]) . ', ' . (int)$pageId . ');';
343
344 $shortcuts[] = $shortcut;
345 }
346 return $shortcuts;
347 }
348
349 /**
350 * Adds the correct token, if the url is an index.php script
351 *
352 * @param string $url
353 * @return string
354 */
355 protected function getTokenUrl($url)
356 {
357 $parsedUrl = parse_url($url);
358 parse_str($parsedUrl['query'], $parameters);
359
360 // parse the returnUrl and replace the module token of it
361 if (isset($parameters['returnUrl'])) {
362 $parsedReturnUrl = parse_url($parameters['returnUrl']);
363 parse_str($parsedReturnUrl['query'], $returnUrlParameters);
364 if (strpos($parsedReturnUrl['path'], 'index.php') !== false && isset($returnUrlParameters['M'])) {
365 $module = $returnUrlParameters['M'];
366 $returnUrl = BackendUtility::getModuleUrl($module, $returnUrlParameters);
367 $parameters['returnUrl'] = $returnUrl;
368 $url = $parsedUrl['path'] . '?' . http_build_query($parameters);
369 }
370 }
371
372 if (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['M'])) {
373 $module = $parameters['M'];
374 $url = BackendUtility::getModuleUrl($module, $parameters);
375 } elseif (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['route'])) {
376 $routePath = $parameters['route'];
377 /** @var \TYPO3\CMS\Backend\Routing\Router $router */
378 $router = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Router::class);
379 try {
380 $route = $router->match($routePath);
381 if ($route) {
382 $routeIdentifier = $route->getOption('_identifier');
383 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
384 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
385 unset($parameters['route']);
386 $url = (string)$uriBuilder->buildUriFromRoute($routeIdentifier, $parameters);
387 }
388 } catch (\TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException $e) {
389 $url = '';
390 }
391 }
392 return $url;
393 }
394
395 /**
396 * Gets shortcuts for a specific group
397 *
398 * @param int $groupId Group Id
399 * @return array Array of shortcuts that matched the group
400 */
401 protected function getShortcutsByGroup($groupId)
402 {
403 $shortcuts = [];
404 foreach ($this->shortcuts as $shortcut) {
405 if ($shortcut['group'] == $groupId) {
406 $shortcuts[] = $shortcut;
407 }
408 }
409 return $shortcuts;
410 }
411
412 /**
413 * Gets a shortcut by its uid
414 *
415 * @param int $shortcutId Shortcut id to get the complete shortcut for
416 * @return mixed An array containing the shortcut's data on success or FALSE on failure
417 */
418 protected function getShortcutById($shortcutId)
419 {
420 $returnShortcut = false;
421 foreach ($this->shortcuts as $shortcut) {
422 if ($shortcut['raw']['uid'] == (int)$shortcutId) {
423 $returnShortcut = $shortcut;
424 continue;
425 }
426 }
427 return $returnShortcut;
428 }
429
430 /**
431 * Gets the available shortcut groups from default groups, user TSConfig, and global groups
432 *
433 * @return array
434 */
435 protected function initShortcutGroups()
436 {
437 $languageService = $this->getLanguageService();
438 $backendUser = $this->getBackendUser();
439 // Groups from TSConfig
440 $bookmarkGroups = $backendUser->getTSConfigProp('options.bookmarkGroups');
441 if (is_array($bookmarkGroups) && !empty($bookmarkGroups)) {
442 foreach ($bookmarkGroups as $groupId => $label) {
443 if (!empty($label)) {
444 $this->shortcutGroups[$groupId] = (string)$label;
445 } elseif ($backendUser->isAdmin()) {
446 unset($this->shortcutGroups[$groupId]);
447 }
448 }
449 }
450 // Generate global groups, all global groups have negative IDs.
451 if (!empty($this->shortcutGroups)) {
452 $groups = $this->shortcutGroups;
453 foreach ($groups as $groupId => $groupLabel) {
454 $this->shortcutGroups[$groupId * -1] = $groupLabel;
455 }
456 }
457 // Group -100 is kind of superglobal and can't be changed.
458 $this->shortcutGroups[self::SUPERGLOBAL_GROUP] = 1;
459 // Add labels
460 foreach ($this->shortcutGroups as $groupId => $groupLabel) {
461 $groupId = (int)$groupId;
462 $label = $groupLabel;
463 if ($groupLabel == '1') {
464 $label = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:bookmark_group_' . abs($groupId)));
465 if (empty($label)) {
466 // Fallback label
467 $label = htmlspecialchars($languageService->getLL('bookmark_group')) . ' ' . abs($groupId);
468 }
469 }
470 if ($groupId < 0) {
471 // Global group
472 $label = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:bookmark_global')) . ': ' . (!empty($label) ? $label : abs($groupId));
473 if ($groupId === self::SUPERGLOBAL_GROUP) {
474 $label = htmlspecialchars($languageService->getLL('bookmark_global')) . ': ' . htmlspecialchars($languageService->getLL('bookmark_all'));
475 }
476 }
477 $this->shortcutGroups[$groupId] = $label;
478 }
479 return $this->shortcutGroups;
480 }
481
482 /**
483 * Fetches the available shortcut groups, renders a form so it can be saved later on, usually called via AJAX
484 *
485 * @param ServerRequestInterface $request
486 * @param ResponseInterface $response
487 * @return ResponseInterface the full HTML for the form
488 */
489 public function editFormAction(ServerRequestInterface $request, ResponseInterface $response)
490 {
491 $languageService = $this->getLanguageService();
492 $parsedBody = $request->getParsedBody();
493 $queryParams = $request->getQueryParams();
494
495 $selectedShortcutId = (int)(isset($parsedBody['shortcutId']) ? $parsedBody['shortcutId'] : $queryParams['shortcutId']);
496 $selectedShortcutGroupId = (int)(isset($parsedBody['shortcutGroup']) ? $parsedBody['shortcutGroup'] : $queryParams['shortcutGroup']);
497 $selectedShortcut = $this->getShortcutById($selectedShortcutId);
498
499 $shortcutGroups = $this->shortcutGroups;
500 if (!$this->getBackendUser()->isAdmin()) {
501 foreach ($shortcutGroups as $groupId => $groupName) {
502 if ((int)$groupId < 0) {
503 unset($shortcutGroups[$groupId]);
504 }
505 }
506 }
507
508 // build the form
509 $content = '
510 <form class="shortcut-form" role="form" data-shortcutid="' . (int)$selectedShortcutId . '">
511 <h3 class="dropdown-headline">
512 ' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.bookmarksEdit')) . '
513 </h3>
514 <hr>
515 <div class="form-group">
516 <input type="text" class="form-control" name="shortcut-title" value="' . htmlspecialchars($selectedShortcut['label']) . '">
517 </div>';
518 $content .= '
519 <div class="form-group">
520 <select class="form-control" name="shortcut-group">';
521 foreach ($shortcutGroups as $shortcutGroupId => $shortcutGroupTitle) {
522 $content .= '<option value="' . (int)$shortcutGroupId . '"' . ($selectedShortcutGroupId == $shortcutGroupId ? ' selected="selected"' : '') . '>' . htmlspecialchars($shortcutGroupTitle) . '</option>';
523 }
524 $content .= '
525 </select>
526 </div>
527 <hr>
528 <input type="button" class="btn btn-default shortcut-form-cancel" value="Cancel">
529 <input type="button" class="btn btn-success shortcut-form-save" value="Save">
530 </form>';
531
532 $response->getBody()->write($content);
533 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
534 return $response;
535 }
536
537 /**
538 * Deletes a shortcut through an AJAX call
539 *
540 * @param ServerRequestInterface $request
541 * @param ResponseInterface $response
542 * @return ResponseInterface
543 */
544 public function removeShortcutAction(ServerRequestInterface $request, ResponseInterface $response)
545 {
546 $parsedBody = $request->getParsedBody();
547 $queryParams = $request->getQueryParams();
548
549 $shortcutId = (int)(isset($parsedBody['shortcutId']) ? $parsedBody['shortcutId'] : $queryParams['shortcutId']);
550 $fullShortcut = $this->getShortcutById($shortcutId);
551 $success = false;
552 if ($fullShortcut['raw']['userid'] == $this->getBackendUser()->user['uid']) {
553 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
554 ->getQueryBuilderForTable('sys_be_shortcuts');
555 $affectedRows = $queryBuilder->delete('sys_be_shortcuts')
556 ->where(
557 $queryBuilder->expr()->eq(
558 'uid',
559 $queryBuilder->createNamedParameter($shortcutId, \PDO::PARAM_INT)
560 )
561 )
562 ->execute();
563 if ($affectedRows === 1) {
564 $success = true;
565 }
566 }
567 $response->getBody()->write(json_encode(['success' => $success]));
568 return $response;
569 }
570
571 /**
572 * Creates a shortcut through an AJAX call
573 *
574 * @param ServerRequestInterface $request
575 * @param ResponseInterface $response
576 * @return ResponseInterface
577 */
578 public function createShortcutAction(ServerRequestInterface $request, ResponseInterface $response)
579 {
580 $languageService = $this->getLanguageService();
581 $parsedBody = $request->getParsedBody();
582 $queryParams = $request->getQueryParams();
583
584 // Default name
585 $shortcutName = 'Shortcut';
586 $shortcutNamePrepend = '';
587 $url = isset($parsedBody['url']) ? $parsedBody['url'] : $queryParams['url'];
588
589 // Use given display name
590 if (!empty($parsedBody['displayName'])) {
591 $shortcutName = $parsedBody['displayName'];
592 }
593
594 // Determine shortcut type
595 $url = rawurldecode($url);
596 $queryParts = parse_url($url);
597 $queryParameters = GeneralUtility::explodeUrl2Array($queryParts['query'], true);
598
599 // Proceed only if no scheme is defined, as URL is expected to be relative
600 if (empty($queryParts['scheme'])) {
601 if (is_array($queryParameters['edit'])) {
602 $shortcut['table'] = key($queryParameters['edit']);
603 $shortcut['recordid'] = key($queryParameters['edit'][$shortcut['table']]);
604 $shortcut['pid'] = BackendUtility::getRecord($shortcut['table'], $shortcut['recordid'])['pid'];
605 if ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] == 'edit') {
606 $shortcut['type'] = 'edit';
607 $shortcutNamePrepend = htmlspecialchars($languageService->getLL('shortcut_edit'));
608 } elseif ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] == 'new') {
609 $shortcut['type'] = 'new';
610 $shortcutNamePrepend = htmlspecialchars($languageService->getLL('shortcut_create'));
611 }
612 } else {
613 $shortcut['type'] = 'other';
614 $shortcut['table'] = '';
615 $shortcut['recordid'] = 0;
616 }
617
618 // Check if given id is a combined identifier
619 if (!empty($queryParameters['id']) && preg_match('/^[0-9]+:/', $queryParameters['id'])) {
620 try {
621 $resourceFactory = ResourceFactory::getInstance();
622 $resource = $resourceFactory->getObjectFromCombinedIdentifier($queryParameters['id']);
623 $shortcutName = trim($shortcutNamePrepend . ' ' . $resource->getName());
624 } catch (ResourceDoesNotExistException $e) {
625 }
626 } else {
627 // Lookup the title of this page and use it as default description
628 $pageId = (int)($shortcut['pid'] ?: ($shortcut['recordid'] ?: $this->getLinkedPageId($url)));
629 $page = false;
630 if ($pageId) {
631 $page = BackendUtility::getRecord('pages', $pageId);
632 }
633 if (!empty($page)) {
634 // Set the name to the title of the page
635 if ($shortcut['type'] === 'other') {
636 if (empty($shortcutName)) {
637 $shortcutName = $page['title'];
638 } else {
639 $shortcutName .= ' (' . $page['title'] . ')';
640 }
641 } else {
642 $shortcutName = $shortcutNamePrepend . ' ' .
643 $languageService->sL($GLOBALS['TCA'][$shortcut['table']]['ctrl']['title']) .
644 ' (' . $page['title'] . ')';
645 }
646 } elseif ($shortcut['table'] !== '' && $shortcut['type'] !== 'other') {
647 $shortcutName = $shortcutNamePrepend . ' ' .
648 $languageService->sL($GLOBALS['TCA'][$shortcut['table']]['ctrl']['title']);
649 }
650 }
651
652 return $this->tryAddingTheShortcut($response, $url, $shortcutName);
653 }
654 }
655
656 /**
657 * Try to adding a shortcut
658 *
659 * @param ResponseInterface $response
660 * @param string $url
661 * @param string $shortcutName
662 * @return ResponseInterface
663 */
664 protected function tryAddingTheShortcut(ResponseInterface $response, $url, $shortcutName)
665 {
666 $module = GeneralUtility::_POST('module');
667 $shortcutCreated = 'failed';
668
669 if (!empty($module) && !empty($url)) {
670 $shortcutCreated = 'alreadyExists';
671
672 if (!BackendUtility::shortcutExists($url) && !is_array($module)) {
673 $shortcutCreated = $this->addShortcut($url, $shortcutName, $module);
674 }
675 }
676
677 $response->getBody()->write($shortcutCreated);
678 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
679 return $response;
680 }
681
682 /**
683 * Add a shortcut now with some user stuffs
684 *
685 * @param string $url
686 * @param string $shortcutName
687 * @param string $module
688 *
689 * @return string
690 */
691 protected function addShortcut($url, $shortcutName, $module)
692 {
693 $moduleLabels = $this->moduleLoader->getLabelsForModule($module);
694 if ($shortcutName === 'Shortcut' && !empty($moduleLabels['shortdescription'])) {
695 $shortcutName = $this->getLanguageService()->sL($moduleLabels['shortdescription']);
696 }
697
698 $motherModule = GeneralUtility::_POST('motherModName');
699 $fieldValues = [
700 'userid' => $this->getBackendUser()->user['uid'],
701 'module_name' => $module . '|' . $motherModule,
702 'url' => $url,
703 'description' => $shortcutName,
704 'sorting' => $GLOBALS['EXEC_TIME']
705 ];
706
707 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
708 ->getQueryBuilderForTable('sys_be_shortcuts');
709 $affectedRows = $queryBuilder
710 ->insert('sys_be_shortcuts')
711 ->values($fieldValues)
712 ->execute();
713
714 if ($affectedRows === 1) {
715 return 'success';
716 } else {
717 return 'failed';
718 }
719 }
720
721 /**
722 * Exists already a shortcut entry for this TYPO3 url?
723 *
724 * @param string $url
725 *
726 * @return bool
727 */
728 protected function shortcutExists($url)
729 {
730 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
731 ->getQueryBuilderForTable('sys_be_shortcuts');
732 $uid = $queryBuilder->select('uid')
733 ->from('sys_be_shortcuts')
734 ->where(
735 $queryBuilder->expr()->eq(
736 'userid',
737 $queryBuilder->createNamedParameter($this->getBackendUser()->user['uid'], \PDO::PARAM_INT)
738 ),
739 $queryBuilder->expr()->eq('url', $queryBuilder->createNamedParameter($url, \PDO::PARAM_STR))
740 )
741 ->execute()
742 ->fetchColumn();
743
744 return (bool)$uid;
745 }
746
747 /**
748 * Gets called when a shortcut is changed, checks whether the user has
749 * permissions to do so and saves the changes if everything is ok
750 *
751 * @param ServerRequestInterface $request
752 * @param ResponseInterface $response
753 * @return ResponseInterface
754 */
755 public function saveFormAction(ServerRequestInterface $request, ResponseInterface $response)
756 {
757 $parsedBody = $request->getParsedBody();
758 $queryParams = $request->getQueryParams();
759
760 $backendUser = $this->getBackendUser();
761 $shortcutId = (int)(isset($parsedBody['shortcutId']) ? $parsedBody['shortcutId'] : $queryParams['shortcutId']);
762 $shortcutName = strip_tags(isset($parsedBody['shortcutTitle']) ? $parsedBody['shortcutTitle'] : $queryParams['shortcutTitle']);
763 $shortcutGroupId = (int)(isset($parsedBody['shortcutGroup']) ? $parsedBody['shortcutGroup'] : $queryParams['shortcutGroup']);
764
765 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
766 ->getQueryBuilderForTable('sys_be_shortcuts');
767 $queryBuilder->update('sys_be_shortcuts')
768 ->where(
769 $queryBuilder->expr()->eq(
770 'uid',
771 $queryBuilder->createNamedParameter($shortcutId, \PDO::PARAM_INT)
772 )
773 )
774 ->set('description', $shortcutName)
775 ->set('sc_group', $shortcutGroupId);
776
777 if (!$backendUser->isAdmin()) {
778 // Users can only modify their own shortcuts
779 $queryBuilder->andWhere(
780 $queryBuilder->expr()->eq(
781 'userid',
782 $queryBuilder->createNamedParameter($backendUser->user['uid'], \PDO::PARAM_INT)
783 )
784 );
785
786 if ($shortcutGroupId < 0) {
787 $queryBuilder->set('sc_group', 0);
788 }
789 }
790
791 if ($queryBuilder->execute() === 1) {
792 $response->getBody()->write($shortcutName);
793 } else {
794 $response->getBody()->write('failed');
795 }
796 return $response->withHeader('Content-Type', 'text/html; charset=utf-8');
797 }
798
799 /**
800 * Gets the label for a shortcut group
801 *
802 * @param int $groupId A shortcut group id
803 * @return string The shortcut group label, can be an empty string if no group was found for the id
804 */
805 protected function getShortcutGroupLabel($groupId)
806 {
807 return isset($this->shortcutGroups[$groupId]) ? $this->shortcutGroups[$groupId] : '';
808 }
809
810 /**
811 * Gets a list of global groups, shortcuts in these groups are available to all users
812 *
813 * @return array Array of global groups
814 */
815 protected function getGlobalShortcutGroups()
816 {
817 $globalGroups = [];
818 foreach ($this->shortcutGroups as $groupId => $groupLabel) {
819 if ($groupId < 0) {
820 $globalGroups[$groupId] = $groupLabel;
821 }
822 }
823 return $globalGroups;
824 }
825
826 /**
827 * runs through the available shortcuts an collects their groups
828 *
829 * @return array Array of groups which have shortcuts
830 */
831 protected function getGroupsFromShortcuts()
832 {
833 $groups = [];
834 foreach ($this->shortcuts as $shortcut) {
835 $groups[$shortcut['group']] = $this->shortcutGroups[$shortcut['group']];
836 }
837 return array_unique($groups);
838 }
839
840 /**
841 * Gets the icon for the shortcut
842 *
843 * @param array $row
844 * @param array $shortcut
845 * @return string Shortcut icon as img tag
846 */
847 protected function getShortcutIcon($row, $shortcut)
848 {
849 $languageService = $this->getLanguageService();
850 $titleAttribute = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:toolbarItems.shortcut'));
851 switch ($row['module_name']) {
852 case 'xMOD_alt_doc.php':
853 $table = $shortcut['table'];
854 $recordid = $shortcut['recordid'];
855 $icon = '';
856 if ($shortcut['type'] == 'edit') {
857 // Creating the list of fields to include in the SQL query:
858 $selectFields = $this->fieldArray;
859 $selectFields[] = 'uid';
860 $selectFields[] = 'pid';
861 if ($table == 'pages') {
862 $selectFields[] = 'module';
863 $selectFields[] = 'extendToSubpages';
864 $selectFields[] = 'doktype';
865 }
866 if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
867 $selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']);
868 }
869 if ($GLOBALS['TCA'][$table]['ctrl']['type']) {
870 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['type'];
871 }
872 if ($GLOBALS['TCA'][$table]['ctrl']['typeicon_column']) {
873 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
874 }
875 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
876 $selectFields[] = 't3ver_state';
877 }
878
879 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
880 ->getQueryBuilderForTable($table);
881 $queryBuilder->select(...array_unique(array_values($selectFields)))
882 ->from($table)
883 ->where(
884 $queryBuilder->expr()->in(
885 'uid',
886 $queryBuilder->createNamedParameter($recordid, \PDO::PARAM_INT)
887 )
888 );
889
890 if ($table === 'pages' && $this->perms_clause) {
891 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($this->perms_clause));
892 }
893
894 $row = $queryBuilder->execute()->fetch();
895
896 $icon = '<span title="' . $titleAttribute . '">' . $this->iconFactory->getIconForRecord($table, (array)$row, Icon::SIZE_SMALL)->render() . '</span>';
897 } elseif ($shortcut['type'] == 'new') {
898 $icon = '<span title="' . $titleAttribute . '">' . $this->iconFactory->getIconForRecord($table, [], Icon::SIZE_SMALL)->render() . '</span>';
899 }
900 break;
901 case 'file_edit':
902 $icon = '<span title="' . $titleAttribute . '">' . $this->iconFactory->getIcon('mimetypes-text-html', Icon::SIZE_SMALL)->render() . '</span>';
903 break;
904 case 'wizard_rte':
905 $icon = '<span title="' . $titleAttribute . '">' . $this->iconFactory->getIcon('mimetypes-word', Icon::SIZE_SMALL)->render() . '</span>';
906 break;
907 default:
908 $iconIdentifier = '';
909 $moduleName = $row['module_name'];
910 if (strpos($moduleName, '_') !== false) {
911 list($mainModule, $subModule) = explode('_', $moduleName, 2);
912 $iconIdentifier = $this->moduleLoader->modules[$mainModule]['sub'][$subModule]['iconIdentifier'];
913 } elseif (!empty($moduleName)) {
914 $iconIdentifier = $this->moduleLoader->modules[$moduleName]['iconIdentifier'];
915 }
916 if (!$iconIdentifier) {
917 $iconIdentifier = 'empty-empty';
918 }
919 $icon = '<span title="' . $titleAttribute . '">' . $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)->render() . '</span>';
920 }
921 return $icon;
922 }
923
924 /**
925 * Returns title for the shortcut icon
926 *
927 * @param string $shortcutLabel Shortcut label
928 * @param string $moduleName Backend module name (key)
929 * @param string $parentModuleName Parent module label
930 * @return string Title for the shortcut icon
931 */
932 protected function getShortcutIconTitle($shortcutLabel, $moduleName, $parentModuleName = '')
933 {
934 $languageService = $this->getLanguageService();
935 if (substr($moduleName, 0, 5) == 'xMOD_') {
936 $title = substr($moduleName, 5);
937 } else {
938 list($mainModule, $subModule) = explode('_', $moduleName);
939 $mainModuleLabels = $this->moduleLoader->getLabelsForModule($mainModule);
940 $title = $languageService->sL($mainModuleLabels['title']);
941 if (!empty($subModule)) {
942 $subModuleLabels = $this->moduleLoader->getLabelsForModule($moduleName);
943 $title .= '>' . $languageService->sL($subModuleLabels['title']);
944 }
945 }
946 if ($parentModuleName) {
947 $title .= ' (' . $parentModuleName . ')';
948 }
949 $title .= ': ' . $shortcutLabel;
950 return $title;
951 }
952
953 /**
954 * Return the ID of the page in the URL if found.
955 *
956 * @param string $url The URL of the current shortcut link
957 * @return string If a page ID was found, it is returned. Otherwise: 0
958 */
959 protected function getLinkedPageId($url)
960 {
961 return preg_replace('/.*[\\?&]id=([^&]+).*/', '$1', $url);
962 }
963
964 /**
965 * Position relative to others, live search should be very right
966 *
967 * @return int
968 */
969 public function getIndex()
970 {
971 return 20;
972 }
973
974 /**
975 * Returns the current BE user.
976 *
977 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
978 */
979 protected function getBackendUser()
980 {
981 return $GLOBALS['BE_USER'];
982 }
983
984 /**
985 * Returns current PageRenderer
986 *
987 * @return PageRenderer
988 */
989 protected function getPageRenderer()
990 {
991 return GeneralUtility::makeInstance(PageRenderer::class);
992 }
993
994 /**
995 * Returns LanguageService
996 *
997 * @return \TYPO3\CMS\Lang\LanguageService
998 */
999 protected function getLanguageService()
1000 {
1001 return $GLOBALS['LANG'];
1002 }
1003 }