1<?php 2declare(strict_types = 1); 3namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders; 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18use TYPO3\CMS\Backend\Routing\UriBuilder; 19use TYPO3\CMS\Backend\Utility\BackendUtility; 20use TYPO3\CMS\Core\Type\Bitmask\Permission; 21use TYPO3\CMS\Core\Utility\GeneralUtility; 22 23/** 24 * Context menu item provider for pages table 25 */ 26class PageProvider extends RecordProvider 27{ 28 /** 29 * @var string 30 */ 31 protected $table = 'pages'; 32 33 /** 34 * @var array 35 */ 36 protected $itemsConfiguration = [ 37 'view' => [ 38 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view', 39 'iconIdentifier' => 'actions-view-page', 40 'callbackAction' => 'viewRecord' 41 ], 42 'edit' => [ 43 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit', 44 'iconIdentifier' => 'actions-page-open', 45 'callbackAction' => 'editRecord' 46 ], 47 'new' => [ 48 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new', 49 'iconIdentifier' => 'actions-page-new', 50 'callbackAction' => 'newRecord' 51 ], 52 'info' => [ 53 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info', 54 'iconIdentifier' => 'actions-document-info', 55 'callbackAction' => 'openInfoPopUp' 56 ], 57 'divider1' => [ 58 'type' => 'divider' 59 ], 60 'copy' => [ 61 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', 62 'iconIdentifier' => 'actions-edit-copy', 63 'callbackAction' => 'copy' 64 ], 65 'copyRelease' => [ 66 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy', 67 'iconIdentifier' => 'actions-edit-copy-release', 68 'callbackAction' => 'clipboardRelease' 69 ], 70 'cut' => [ 71 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut', 72 'iconIdentifier' => 'actions-edit-cut', 73 'callbackAction' => 'cut' 74 ], 75 'cutRelease' => [ 76 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease', 77 'iconIdentifier' => 'actions-edit-cut-release', 78 'callbackAction' => 'clipboardRelease' 79 ], 80 'pasteAfter' => [ 81 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter', 82 'iconIdentifier' => 'actions-document-paste-after', 83 'callbackAction' => 'pasteAfter' 84 ], 85 'pasteInto' => [ 86 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto', 87 'iconIdentifier' => 'actions-document-paste-into', 88 'callbackAction' => 'pasteInto' 89 ], 90 'divider2' => [ 91 'type' => 'divider' 92 ], 93 'more' => [ 94 'type' => 'submenu', 95 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more', 96 'iconIdentifier' => '', 97 'callbackAction' => 'openSubmenu', 98 'childItems' => [ 99 'newWizard' => [ 100 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard', 101 'iconIdentifier' => 'actions-page-new', 102 'callbackAction' => 'newPageWizard', 103 ], 104 'pagesSort' => [ 105 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title', 106 'iconIdentifier' => 'actions-page-move', 107 'callbackAction' => 'pagesSort', 108 ], 109 'pagesNewMultiple' => [ 110 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title', 111 'iconIdentifier' => 'apps-pagetree-drag-move-between', 112 'callbackAction' => 'pagesNewMultiple', 113 ], 114 'openListModule' => [ 115 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_db_list', 116 'iconIdentifier' => 'actions-system-list-open', 117 'callbackAction' => 'openListModule', 118 ], 119 'mountAsTreeRoot' => [ 120 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint', 121 'iconIdentifier' => 'actions-pagetree-mountroot', 122 'callbackAction' => 'mountAsTreeRoot', 123 ], 124 ], 125 ], 126 'divider3' => [ 127 'type' => 'divider' 128 ], 129 'enable' => [ 130 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable', 131 'iconIdentifier' => 'actions-edit-unhide', 132 'callbackAction' => 'enableRecord', 133 ], 134 'disable' => [ 135 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable', 136 'iconIdentifier' => 'actions-edit-hide', 137 'callbackAction' => 'disableRecord', 138 ], 139 'delete' => [ 140 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete', 141 'iconIdentifier' => 'actions-edit-delete', 142 'callbackAction' => 'deleteRecord', 143 ], 144 'history' => [ 145 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history', 146 'iconIdentifier' => 'actions-document-history-open', 147 'callbackAction' => 'openHistoryPopUp', 148 ], 149 'clearCache' => [ 150 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache', 151 'iconIdentifier' => 'actions-system-cache-clear', 152 'callbackAction' => 'clearCache', 153 ], 154 ]; 155 156 /** 157 * @var bool 158 */ 159 protected $languageAccess = false; 160 161 /** 162 * Checks if the provider can add items to the menu 163 * 164 * @return bool 165 */ 166 public function canHandle(): bool 167 { 168 return $this->table === 'pages'; 169 } 170 171 /** 172 * @return int 173 */ 174 public function getPriority(): int 175 { 176 return 100; 177 } 178 179 /** 180 * @param string $itemName 181 * @param string $type 182 * @return bool 183 */ 184 protected function canRender(string $itemName, string $type): bool 185 { 186 if (in_array($type, ['divider', 'submenu'], true)) { 187 return true; 188 } 189 if (in_array($itemName, $this->disabledItems, true)) { 190 return false; 191 } 192 $canRender = false; 193 switch ($itemName) { 194 case 'view': 195 $canRender = $this->canBeViewed(); 196 break; 197 case 'edit': 198 $canRender = $this->canBeEdited(); 199 break; 200 case 'new': 201 case 'newWizard': 202 case 'pagesNewMultiple': 203 $canRender = $this->canBeCreated(); 204 break; 205 case 'info': 206 $canRender = $this->canShowInfo(); 207 break; 208 case 'enable': 209 $canRender = $this->canBeEnabled(); 210 break; 211 case 'disable': 212 $canRender = $this->canBeDisabled(); 213 break; 214 case 'delete': 215 $canRender = $this->canBeDeleted(); 216 break; 217 case 'history': 218 $canRender = $this->canShowHistory(); 219 break; 220 case 'openListModule': 221 $canRender = $this->canOpenListModule(); 222 break; 223 case 'pagesSort': 224 $canRender = $this->canBeSorted(); 225 break; 226 case 'mountAsTreeRoot': 227 $canRender = !$this->isRoot(); 228 break; 229 case 'copy': 230 $canRender = $this->canBeCopied(); 231 break; 232 case 'copyRelease': 233 $canRender = $this->isRecordInClipboard('copy'); 234 break; 235 case 'cut': 236 $canRender = $this->canBeCut() && !$this->isRecordInClipboard('cut'); 237 break; 238 case 'cutRelease': 239 $canRender = $this->isRecordInClipboard('cut'); 240 break; 241 case 'pasteAfter': 242 $canRender = $this->canBePastedAfter(); 243 break; 244 case 'pasteInto': 245 $canRender = $this->canBePastedInto(); 246 break; 247 case 'clearCache': 248 $canRender = $this->canClearCache(); 249 break; 250 } 251 return $canRender; 252 } 253 254 /** 255 * Saves calculated permissions for a page to speed things up 256 */ 257 protected function initPermissions() 258 { 259 $this->pagePermissions = $this->backendUser->calcPerms($this->record); 260 $this->languageAccess = $this->hasLanguageAccess(); 261 } 262 263 /** 264 * Checks if the user may create pages below the given page 265 * 266 * @return bool 267 */ 268 protected function canBeCreated(): bool 269 { 270 if (!$this->backendUser->checkLanguageAccess(0)) { 271 return false; 272 } 273 return $this->hasPagePermission(Permission::PAGE_NEW); 274 } 275 276 /** 277 * Checks if the user has editing rights 278 * 279 * @return bool 280 */ 281 protected function canBeEdited(): bool 282 { 283 if (!$this->languageAccess) { 284 return false; 285 } 286 if ($this->isRoot()) { 287 return false; 288 } 289 if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) { 290 return false; 291 } 292 if ($this->backendUser->isAdmin()) { 293 return true; 294 } 295 if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) { 296 return false; 297 } 298 return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT); 299 } 300 301 /** 302 * Check if a page is locked 303 * 304 * @return bool 305 */ 306 protected function isRecordLocked(): bool 307 { 308 return (bool)$this->record['editlock']; 309 } 310 311 /** 312 * Checks if the page is allowed to can be cut 313 * 314 * @return bool 315 */ 316 protected function canBeCut(): bool 317 { 318 if (!$this->languageAccess) { 319 return false; 320 } 321 if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField']) 322 && !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1]) 323 ) { 324 return false; 325 } 326 return !$this->isWebMount() 327 && $this->canBeEdited() 328 && !$this->isDeletePlaceholder(); 329 } 330 331 /** 332 * Checks if the page is allowed to be copied 333 * 334 * @return bool 335 */ 336 protected function canBeCopied(): bool 337 { 338 if (!$this->languageAccess) { 339 return false; 340 } 341 if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField']) 342 && !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1]) 343 ) { 344 return false; 345 } 346 return !$this->isRoot() 347 && !$this->isWebMount() 348 && !$this->isRecordInClipboard('copy') 349 && $this->hasPagePermission(Permission::PAGE_SHOW) 350 && !$this->isDeletePlaceholder(); 351 } 352 353 /** 354 * Checks if something can be pasted into the node 355 * 356 * @return bool 357 */ 358 protected function canBePastedInto(): bool 359 { 360 if (!$this->languageAccess) { 361 return false; 362 } 363 $clipboardElementCount = count($this->clipboard->elFromTable($this->table)); 364 365 return $clipboardElementCount 366 && $this->canBeCreated() 367 && !$this->isDeletePlaceholder(); 368 } 369 370 /** 371 * Checks if something can be pasted after the node 372 * 373 * @return bool 374 */ 375 protected function canBePastedAfter(): bool 376 { 377 if (!$this->languageAccess) { 378 return false; 379 } 380 $clipboardElementCount = count($this->clipboard->elFromTable($this->table)); 381 return $clipboardElementCount 382 && $this->canBeCreated() 383 && !$this->isDeletePlaceholder(); 384 } 385 386 /** 387 * Check if sub pages of given page can be sorted 388 * 389 * @return bool 390 */ 391 protected function canBeSorted(): bool 392 { 393 if (!$this->languageAccess) { 394 return false; 395 } 396 return $this->backendUser->check('tables_modify', $this->table) 397 && $this->hasPagePermission(Permission::CONTENT_EDIT) 398 && !$this->isDeletePlaceholder() 399 && $this->backendUser->workspace === 0; 400 } 401 402 /** 403 * Checks if the page is allowed to be removed 404 * 405 * @return bool 406 */ 407 protected function canBeDeleted(): bool 408 { 409 if (!$this->languageAccess) { 410 return false; 411 } 412 return !$this->isDeletePlaceholder() 413 && !$this->isRecordLocked() 414 && !$this->isDeletionDisabledInTS() 415 && $this->hasPagePermission(Permission::PAGE_DELETE); 416 } 417 418 /** 419 * Checks if the page is allowed to be viewed in frontend 420 * 421 * @return bool 422 */ 423 protected function canBeViewed(): bool 424 { 425 return !$this->isRoot() && !$this->isDeleted(); 426 } 427 428 /** 429 * Checks if the page is allowed to show info 430 * 431 * @return bool 432 */ 433 protected function canShowInfo(): bool 434 { 435 return !$this->isRoot(); 436 } 437 438 /** 439 * Checks if the user has clear cache rights 440 * 441 * @return bool 442 */ 443 protected function canClearCache(): bool 444 { 445 return !$this->isRoot() 446 && ($this->backendUser->isAdmin() || $this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false); 447 } 448 449 /** 450 * Determines whether this node is deleted. 451 * 452 * @return bool 453 */ 454 protected function isDeleted(): bool 455 { 456 return !empty($this->record['deleted']) || $this->isDeletePlaceholder(); 457 } 458 459 /** 460 * Returns true if current record is a root page 461 * 462 * @return bool 463 */ 464 protected function isRoot() 465 { 466 return (int)$this->identifier === 0; 467 } 468 469 /** 470 * Returns true if current record is a web mount 471 * 472 * @return bool 473 */ 474 protected function isWebMount() 475 { 476 return in_array($this->identifier, $this->backendUser->returnWebmounts()); 477 } 478 479 /** 480 * @param string $itemName 481 * @return array 482 */ 483 protected function getAdditionalAttributes(string $itemName): array 484 { 485 $attributes = []; 486 if ($itemName === 'view') { 487 $attributes += $this->getViewAdditionalAttributes(); 488 } 489 if ($itemName === 'enable' || $itemName === 'disable') { 490 $attributes += $this->getEnableDisableAdditionalAttributes(); 491 } 492 if ($itemName === 'delete') { 493 $attributes += $this->getDeleteAdditionalAttributes(); 494 } 495 if ($itemName === 'pasteInto') { 496 $attributes += $this->getPasteAdditionalAttributes('into'); 497 } 498 if ($itemName === 'pasteAfter') { 499 $attributes += $this->getPasteAdditionalAttributes('after'); 500 } 501 if ($itemName === 'pagesSort') { 502 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 503 $attributes += [ 504 'data-pages-sort-url' => (string)$uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid']]), 505 ]; 506 } 507 if ($itemName === 'pagesNewMultiple') { 508 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 509 $attributes += [ 510 'data-pages-new-multiple-url' => (string)$uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid']]), 511 ]; 512 } 513 if ($itemName === 'edit') { 514 $attributes = [ 515 'data-pages-language-uid' => $this->record['sys_language_uid'] 516 ]; 517 } 518 return $attributes; 519 } 520 521 /** 522 * @return int 523 */ 524 protected function getPreviewPid(): int 525 { 526 return (int)$this->record['sys_language_uid'] === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent']; 527 } 528 529 /** 530 * Returns the view link 531 * 532 * @return string 533 */ 534 protected function getViewLink(): string 535 { 536 $language = (int)$this->record['sys_language_uid']; 537 $additionalParams = ($language > 0) ? '&L=' . $language : ''; 538 return BackendUtility::getPreviewUrl( 539 $this->getPreviewPid(), 540 '', 541 null, 542 '', 543 '', 544 $additionalParams 545 ); 546 } 547 548 /** 549 * Returns true if a current user has access to the language of the record 550 * 551 * @see BackendUserAuthentication::checkLanguageAccess() 552 * @return bool 553 */ 554 protected function hasLanguageAccess(): bool 555 { 556 if ($this->backendUser->isAdmin()) { 557 return true; 558 } 559 $languageField = $GLOBALS['TCA'][$this->table]['ctrl']['languageField'] ?? ''; 560 if ($languageField !== '' && isset($this->record[$languageField])) { 561 return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]); 562 } 563 return true; 564 } 565} 566