TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:27 +02:00
commit 44c503b2d1
233 changed files with 31556 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
/**
* Utility class for menus based on category collections of pages.
*
* Returns all the relevant pages for rendering with a menu content object.
* @internal this is only used for internal purposes and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class CategoryMenuUtility
{
/**
* @var string Name of the field used for sorting the pages
*/
protected static $sortingField;
/**
* Collects all pages for the selected categories, sorted according to configuration.
*
* @param string $selectedCategories Comma-separated list of system categories primary keys
* @param array|null $configuration TypoScript configuration for the "special." keyword
* @param AbstractMenuContentObject $parentObject Back-reference to the calling object
* @return array List of selected pages
*/
public function collectPages($selectedCategories, $configuration, $parentObject)
{
$selectedPages = [];
$categoriesPerPage = [];
// Determine the name of the relation field
$relationField = (string)$parentObject->getParentContentObject()->stdWrapValue('relation', $configuration ?? []);
// Get the pages for each selected category
$selectedCategories = GeneralUtility::intExplode(',', $selectedCategories, true);
foreach ($selectedCategories as $aCategory) {
$collection = CategoryCollection::load(
$aCategory,
true,
'pages',
$relationField
);
$categoryUid = $collection->getUid();
// Loop on the results, overlay each page record found
foreach ($collection as $pageItem) {
$parentObject->getSysPage()->versionOL('pages', $pageItem, true);
if (is_array($pageItem)) {
$selectedPages[$pageItem['uid']] = $parentObject->getSysPage()->getLanguageOverlay('pages', $pageItem);
// Keep a list of the categories each page belongs to
if (!isset($categoriesPerPage[$pageItem['uid']])) {
$categoriesPerPage[$pageItem['uid']] = [];
}
$categoriesPerPage[$pageItem['uid']][] = $categoryUid;
}
}
}
// Loop on the selected pages to add the categories they belong to, as comma-separated list of category uid's)
// (this makes them available for rendering, if needed)
foreach ($selectedPages as $uid => $pageRecord) {
$selectedPages[$uid]['_categories'] = implode(',', $categoriesPerPage[$uid]);
}
// Sort the pages according to the sorting property
self::$sortingField = (string)$parentObject->getParentContentObject()->stdWrapValue('sorting', $configuration ?? []);
$order = (string)$parentObject->getParentContentObject()->stdWrapValue('order', $configuration ?? []);
$selectedPages = $this->sortPages($selectedPages, $order);
return $selectedPages;
}
/**
* Sorts the selected pages
*
* If the sorting field is not defined or does not corresponding to an existing field
* of the "pages" tables, the list of pages will remain unchanged.
*
* @param array $pages List of selected pages
* @param string $order Order for sorting (should "asc" or "desc")
* @return array Sorted list of pages
*/
protected function sortPages($pages, $order)
{
// Perform the sorting only if a criterion was actually defined
if (!empty(self::$sortingField)) {
// Check that the sorting field exists (checking the first record is enough)
$firstPage = current($pages);
if (isset($firstPage[self::$sortingField])) {
// Make sure the order property is either "asc" or "desc" (default is "asc")
if (!empty($order)) {
$order = strtolower($order);
if ($order !== 'desc') {
$order = 'asc';
}
}
$sortMultiplier = $order === 'asc' ? 1 : -1;
uasort($pages, static function (array $pageA, array $pageB) use ($sortMultiplier): int {
return strnatcasecmp($pageA[self::$sortingField], $pageB[self::$sortingField]) * $sortMultiplier;
});
}
}
return $pages;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu\Exception;
use TYPO3\CMS\Frontend\Exception;
/**
* No such menu type exception
*/
class NoSuchMenuTypeException extends Exception {}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
/**
* Factory for menu content objects. Allows overriding the default
* types like 'TMENU' with an own implementation (only one possible)
* and new types can be registered.
* @internal this is only used for internal purposes and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class MenuContentObjectFactory implements SingletonInterface
{
/**
* Register of TypoScript keys to according render class
*/
protected array $menuTypeToClassMapping = [
'TMENU' => TextMenuContentObject::class,
];
/**
* Gets a typo script string like 'TMENU' and returns an object of this type
*
* @throws Exception\NoSuchMenuTypeException
*/
public function getMenuObjectByType(string $type = ''): AbstractMenuContentObject
{
$upperCasedClassName = strtoupper($type);
if (array_key_exists($upperCasedClassName, $this->menuTypeToClassMapping)) {
/** @var AbstractMenuContentObject $object */
$object = GeneralUtility::makeInstance($this->menuTypeToClassMapping[$upperCasedClassName]);
return $object;
}
throw new NoSuchMenuTypeException(
'Menu type ' . (string)$type . ' has no implementing class.',
1363278130
);
}
/**
* Register new menu type or override existing type
*
* @param string $type Menu type to be used in TypoScript
* @param string $className Class rendering the menu
*/
public function registerMenuType(string $type, string $className)
{
$this->menuTypeToClassMapping[strtoupper($type)] = $className;
}
}
@@ -0,0 +1,166 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Extension class creating text based menus
*/
class TextMenuContentObject extends AbstractMenuContentObject
{
/**
* Traverses the ->result array of menu items configuration (made by ->generate()) and renders each item.
* An instance of ContentObjectRenderer is also made and for each menu item rendered it is loaded with
* the record for that page so that any stdWrap properties that applies will have the current menu items record available.
*
* @return string The HTML for the menu including submenus
*/
public function writeMenu()
{
if (empty($this->result)) {
return '';
}
$register = $this->request->getAttribute('frontend.register.stack')->current();
$cObjectForCurrentMenu = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$menuContent = [];
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$subMenuObjSuffixes = $typoScriptService->explodeConfigurationForOptionSplit(['sOSuffix' => $this->mconf['submenuObjSuffixes'] ?? null], count($this->result));
$explicitSpacerRenderingEnabled = ($this->mconf['SPC'] ?? false);
foreach ($this->result as $key => $val) {
$register->set('count_HMENU_MENUOBJ', (int)$register->get('count_HMENU_MENUOBJ', 0) + 1);
$register->set('count_MENUOBJ', (int)$register->get('count_MENUOBJ', 0) + 1);
// Initialize the cObj with the page record of the menu item
$cObjectForCurrentMenu->setRequest($this->request);
$cObjectForCurrentMenu->start($this->menuArr[$key], 'pages');
$this->I = [];
$this->I['key'] = $key;
$this->I['val'] = $val;
$this->I['title'] = $this->getPageTitle($this->menuArr[$key]['title'] ?? '', $this->menuArr[$key]['nav_title'] ?? '');
$this->I['title.'] = $this->I['val']['stdWrap.'] ?? [];
$this->I['title'] = $cObjectForCurrentMenu->stdWrapValue('title', $this->I);
$this->I['uid'] = $this->menuArr[$key]['uid'] ?? 0;
$this->I['mount_pid'] = $this->menuArr[$key]['mount_pid'] ?? 0;
$this->I['pid'] = $this->menuArr[$key]['pid'] ?? 0;
$this->I['spacer'] = $this->menuArr[$key]['isSpacer'] ?? false;
// Make link tag
$this->I['val']['additionalParams'] = $cObjectForCurrentMenu->stdWrapValue('additionalParams', $this->I['val']);
$linkResult = $this->link((int)$key, (string)($this->I['val']['altTarget'] ?? ''), ($this->mconf['forceTypeValue'] ?? ''));
if ($linkResult === null) {
$this->I['val']['doNotLinkIt'] = 1;
}
// Title attribute of links
$titleAttrValue = $cObjectForCurrentMenu->stdWrapValue('ATagTitle', $this->I['val']);
if ($linkResult && $titleAttrValue !== '') {
$linkResult = $linkResult->withAttribute('title', $titleAttrValue);
}
$this->I['linkHREF'] = $linkResult;
$this->I['val']['doNotLinkIt'] = (bool)$cObjectForCurrentMenu->stdWrapValue('doNotLinkIt', $this->I['val']);
// Compile link tag
if (!$this->I['spacer'] && !$this->I['val']['doNotLinkIt']) {
$this->setATagParts($linkResult);
} else {
$this->I['A1'] = '';
$this->I['A2'] = '';
}
// ATagBeforeWrap processing:
if ($this->I['val']['ATagBeforeWrap'] ?? false) {
$wrapPartsBefore = explode('|', $this->I['val']['linkWrap'] ?? '');
$wrapPartsAfter = ['', ''];
} else {
$wrapPartsBefore = ['', ''];
$wrapPartsAfter = explode('|', $this->I['val']['linkWrap'] ?? '');
}
if (($this->I['val']['stdWrap2'] ?? false) || isset($this->I['val']['stdWrap2.'])) {
$stdWrap2 = (string)(isset($this->I['val']['stdWrap2.']) ? $cObjectForCurrentMenu->stdWrap('|', $this->I['val']['stdWrap2.']) : '|');
$stdWrap2Value = (string)($this->I['val']['stdWrap2'] ?? '|');
$stdWrap2Value = $stdWrap2Value !== '' ? $stdWrap2Value : '|';
$wrapPartsStdWrap = explode($stdWrap2Value, $stdWrap2);
} else {
$wrapPartsStdWrap = ['', ''];
}
// Make before, middle and after parts
$this->I['parts'] = [];
$this->I['parts']['before'] = $this->getBeforeAfter('before', $cObjectForCurrentMenu);
$this->I['parts']['stdWrap2_begin'] = $wrapPartsStdWrap[0];
// stdWrap for doNotShowLink
$this->I['val']['doNotShowLink'] = $cObjectForCurrentMenu->stdWrapValue('doNotShowLink', $this->I['val']);
if (!$this->I['val']['doNotShowLink']) {
$this->I['parts']['notATagBeforeWrap_begin'] = $wrapPartsAfter[0];
$this->I['parts']['ATag_begin'] = $this->I['A1'];
$this->I['parts']['ATagBeforeWrap_begin'] = $wrapPartsBefore[0];
$this->I['parts']['title'] = $this->I['title'];
$this->I['parts']['ATagBeforeWrap_end'] = $wrapPartsBefore[1] ?? '';
$this->I['parts']['ATag_end'] = $this->I['A2'];
$this->I['parts']['notATagBeforeWrap_end'] = $wrapPartsAfter[1] ?? '';
}
$this->I['parts']['stdWrap2_end'] = $wrapPartsStdWrap[1] ?? '';
$this->I['parts']['after'] = $this->getBeforeAfter('after', $cObjectForCurrentMenu);
// Passing I to a user function
if ($this->mconf['IProcFunc'] ?? false) {
$this->I = $this->userProcess('IProcFunc', $this->I);
}
// Merge parts + beforeAllWrap
$this->I['theItem'] = implode('', $this->I['parts']);
$allWrap = $cObjectForCurrentMenu->stdWrapValue('allWrap', $this->I['val']);
$this->I['theItem'] = $cObjectForCurrentMenu->wrap($this->I['theItem'], $allWrap);
if ($this->I['val']['subst_elementUid'] ?? false) {
$this->I['theItem'] = str_replace('{elementUid}', (string)$this->I['uid'], $this->I['theItem']);
}
if (is_array($this->I['val']['allStdWrap.'] ?? null)) {
$this->I['theItem'] = $cObjectForCurrentMenu->stdWrap($this->I['theItem'], $this->I['val']['allStdWrap.']);
}
$isSpacerPage = $this->I['spacer'] ?? false;
// If rendering of SPACERs is enabled, also allow rendering submenus with Spacers
if (!$isSpacerPage || $explicitSpacerRenderingEnabled) {
// Add part to the accumulated result + fetch submenus
$this->I['theItem'] .= $this->subMenu($this->I['uid'], $subMenuObjSuffixes[$key]['sOSuffix'] ?? '', $key);
}
$part = $cObjectForCurrentMenu->stdWrapValue('wrapItemAndSub', $this->I['val']);
$menuContent[] = $part ? $cObjectForCurrentMenu->wrap($this->I['theItem'], $part) : $this->I['theItem'];
}
$menuContent = implode('', $menuContent);
if (is_array($this->mconf['stdWrap.'] ?? null)) {
$menuContent = (string)$cObjectForCurrentMenu->stdWrap($menuContent, $this->mconf['stdWrap.']);
}
return $cObjectForCurrentMenu->wrap($menuContent, $this->mconf['wrap'] ?? '');
}
/**
* Generates the before* and after* stdWrap for TMENUs
* Evaluates:
* - before.stdWrap*
* - beforeWrap
* - after.stdWrap*
* - afterWrap
*
* @param string $pref Can be "before" or "after" and determines which kind of stdWrap to process (basically this is the prefix of the TypoScript properties that are read from the ->I['val'] array
* @return string The resulting HTML
*/
protected function getBeforeAfter(string $pref, ContentObjectRenderer $cObjectForCurrentMenu): string
{
$processedPref = $cObjectForCurrentMenu->stdWrapValue($pref, $this->I['val']);
if (isset($this->I['val'][$pref . 'Wrap'])) {
return $cObjectForCurrentMenu->wrap($processedPref, $this->I['val'][$pref . 'Wrap']);
}
return $processedPref;
}
}