e79ed21209e8379cafde83c7b21bd2b20e9dcb50
[Packages/TYPO3.CMS.git] / typo3 / sysext / feedit / Classes / FrontendEditPanel.php
1 <?php
2 namespace TYPO3\CMS\Feedit;
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 use TYPO3\CMS\Adminpanel\Service\EditToolbarService;
17 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
18 use TYPO3\CMS\Core\Imaging\Icon;
19 use TYPO3\CMS\Core\Imaging\IconFactory;
20 use TYPO3\CMS\Core\Localization\LanguageService;
21 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\MathUtility;
24 use TYPO3\CMS\Core\Utility\PathUtility;
25 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
26
27 /**
28 * View class for the edit panels in frontend editing.
29 *
30 * @internal this is a concrete TYPO3 implementation and solely used for EXT:feedit and not part of TYPO3's Core API.
31 */
32 class FrontendEditPanel
33 {
34 /**
35 * The Content Object Renderer
36 *
37 * @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
38 */
39 protected $cObj;
40
41 /**
42 * Property for accessing TypoScriptFrontendController centrally
43 *
44 * @var TypoScriptFrontendController
45 */
46 protected $frontendController;
47
48 /**
49 * @var FrontendBackendUserAuthentication
50 */
51 protected $backendUser;
52
53 /**
54 * @var \TYPO3\CMS\Core\Imaging\IconFactory
55 */
56 protected $iconFactory;
57
58 /**
59 * Constructor for the edit panel
60 *
61 * @param mixed $_ Previous the database connection
62 * @param TypoScriptFrontendController $frontendController
63 * @param FrontendBackendUserAuthentication $backendUser
64 */
65 public function __construct($_ = null, TypoScriptFrontendController $frontendController = null, FrontendBackendUserAuthentication $backendUser = null)
66 {
67 $this->frontendController = $frontendController ?: $GLOBALS['TSFE'];
68 $this->backendUser = $backendUser ?: $GLOBALS['BE_USER'];
69 $this->cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
70 $this->cObj->start([]);
71 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
72 }
73
74 /**
75 * Generates the "edit panels" which can be shown for a page or records on a page when the Admin Panel is enabled for a backend users surfing the frontend.
76 * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
77 * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
78 *
79 * @param string $content A content string containing the content related to the edit panel. For cObject "EDITPANEL" this is empty but not so for the stdWrap property. The edit panel is appended to this string and returned.
80 * @param array $conf TypoScript configuration properties for the editPanel
81 * @param string $currentRecord The "table:uid" of the record being shown. If empty string then $this->currentRecord is used. For new records (set by $conf['newRecordFromTable']) it's auto-generated to "[tablename]:NEW
82 * @param array $dataArr Alternative data array to use. Default is $this->data
83 * @param string $table
84 * @param array $allow
85 * @param int $newUID
86 * @param array $hiddenFields
87 * @return string The input content string with the editPanel appended. This function returns only an edit panel appended to the content string if a backend user is logged in (and has the correct permissions). Otherwise the content string is directly returned.
88 */
89 public function editPanel($content, array $conf, $currentRecord = '', array $dataArr = [], $table = '', array $allow = [], $newUID = 0, array $hiddenFields = [])
90 {
91 $hiddenFieldString = '';
92
93 // Special content is about to be shown, so the cache must be disabled.
94 $this->frontendController->set_no_cache('Frontend edit panel is shown', true);
95
96 $formName = 'TSFE_EDIT_FORM_' . substr($this->frontendController->uniqueHash(), 0, 4);
97 $formTag = '<form name="' . $formName . '" id ="' . $formName . '" action="' . htmlspecialchars(GeneralUtility::getIndpEnv('REQUEST_URI')) . '" method="post" enctype="multipart/form-data" onsubmit="return TBE_EDITOR.checkSubmit(1);">';
98 $sortField = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
99 $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
100 $hideField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
101
102 $panel = '';
103 if (isset($allow['toolbar'])) {
104 $editToolbarService = GeneralUtility::makeInstance(EditToolbarService::class);
105 $panel .= $editToolbarService->createToolbar();
106 }
107 if (isset($allow['edit'])) {
108 $icon = '<span title="' . $this->getLabel('p_editRecord') . '">' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render('inline') . '</span>';
109 $panel .= $this->editPanelLinkWrap($icon, $formName, 'edit', $dataArr['_LOCALIZED_UID'] ? $table . ':' . $dataArr['_LOCALIZED_UID'] : $currentRecord);
110 }
111 // Hiding in workspaces because implementation is incomplete
112 if (isset($allow['move']) && $sortField && $this->backendUser->workspace === 0) {
113 $icon = '<span title="' . $this->getLabel('p_moveUp') . '">' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render('inline') . '</span>';
114 $panel .= $this->editPanelLinkWrap($icon, $formName, 'up');
115 $icon = '<span title="' . $this->getLabel('p_moveDown') . '">' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render('inline') . '</span>';
116 $panel .= $this->editPanelLinkWrap($icon, $formName, 'down');
117 }
118 // Hiding in workspaces because implementation is incomplete
119 // Hiding for localizations because it is unknown what should be the function in that case
120 if (isset($allow['hide']) && $hideField && $this->backendUser->workspace === 0 && !$dataArr['_LOCALIZED_UID']) {
121 if ($dataArr[$hideField]) {
122 $icon = $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render('inline');
123 $panel .= $this->editPanelLinkWrap($icon, $formName, 'unhide');
124 } else {
125 $icon = $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render('inline');
126 $panel .= $this->editPanelLinkWrap($icon, $formName, 'hide', '', $this->getLabel('p_hideConfirm'));
127 }
128 }
129 if (isset($allow['new'])) {
130 if ($table === 'pages') {
131 $icon = '<span title="' . $this->getLabel('p_newSubpage') . '">'
132 . $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL)->render('inline')
133 . '</span>';
134 $panel .= $this->editPanelLinkWrap($icon, $formName, 'new', $currentRecord, '');
135 } else {
136 $icon = '<span title="' . $this->getLabel('p_newRecordAfter') . '">'
137 . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render('inline')
138 . '</span>';
139 $panel .= $this->editPanelLinkWrap($icon, $formName, 'new', $currentRecord, '', $newUID);
140 }
141 }
142 // Hiding in workspaces because implementation is incomplete
143 // Hiding for localizations because it is unknown what should be the function in that case
144 if (isset($allow['delete']) && $this->backendUser->workspace === 0 && !$dataArr['_LOCALIZED_UID']) {
145 $icon = '<span title="' . $this->getLabel('p_delete') . '">'
146 . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render('inline')
147 . '</span>';
148 $panel .= $this->editPanelLinkWrap($icon, $formName, 'delete', '', $this->getLabel('p_deleteConfirm'));
149 }
150 // Final
151 $labelTxt = $this->cObj->stdWrap($conf['label'], $conf['label.']);
152 foreach ((array)$hiddenFields as $name => $value) {
153 $hiddenFieldString .= '<input type="hidden" name="TSFE_EDIT[' . htmlspecialchars($name) . ']" value="' . htmlspecialchars($value) . '"/>' . LF;
154 }
155
156 $panel = '<!-- BE_USER Edit Panel: -->
157 ' . $formTag . $hiddenFieldString . '
158 <input type="hidden" name="TSFE_EDIT[cmd]" value="" />
159 <input type="hidden" name="TSFE_EDIT[record]" value="' . $currentRecord . '" />
160 <div class="typo3-editPanel" style="display: none;">'
161 . '<div class="typo3-editPanel-btn-group">'
162 . $panel
163 . '</div>' .
164 ($labelTxt ? '<div class="typo3-editPanel-label">' . sprintf($labelTxt, htmlspecialchars(GeneralUtility::fixed_lgd_cs($dataArr[$labelField], 50))) . '</div>' : '') . '
165 </div>
166 </form>';
167
168 // Wrap the panel
169 if ($conf['innerWrap']) {
170 $panel = $this->cObj->wrap($panel, $conf['innerWrap']);
171 }
172 if ($conf['innerWrap.']) {
173 $panel = $this->cObj->stdWrap($panel, $conf['innerWrap.']);
174 }
175
176 // Wrap the complete panel
177 if ($conf['outerWrap']) {
178 $panel = $this->cObj->wrap($panel, $conf['outerWrap']);
179 }
180 if ($conf['outerWrap.']) {
181 $panel = $this->cObj->stdWrap($panel, $conf['outerWrap.']);
182 }
183 if ($conf['printBeforeContent']) {
184 $finalOut = $panel . $content;
185 } else {
186 $finalOut = $content . $panel;
187 }
188
189 $hidden = $this->isDisabled($table, $dataArr) ? ' typo3-feedit-element-hidden' : '';
190 $outerWrapConfig = $conf['stdWrap.'] ?? ['wrap' => '<div class="typo3-feedit-element' . $hidden . '">|</div>'];
191 $finalOut = $this->cObj->stdWrap($finalOut, $outerWrapConfig);
192
193 return $finalOut;
194 }
195
196 /**
197 * Adds an edit icon to the content string. The edit icon links to EditDocumentController with proper parameters for editing the table/fields of the context.
198 * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
199 *
200 * @param string $content The content to which the edit icons should be appended
201 * @param string $params The parameters defining which table and fields to edit. Syntax is [tablename]:[fieldname],[fieldname],[fieldname],... OR [fieldname],[fieldname],[fieldname],... (basically "[tablename]:" is optional, default table is the one of the "current record" used in the function). The fieldlist is sent as "&columnsOnly=" parameter to EditDocumentController
202 * @param array $conf TypoScript properties for configuring the edit icons.
203 * @param string $currentRecord The "table:uid" of the record being shown. If empty string then $this->currentRecord is used. For new records (set by $conf['newRecordFromTable']) it's auto-generated to "[tablename]:NEW
204 * @param array $dataArr Alternative data array to use. Default is $this->data
205 * @param string $addUrlParamStr Additional URL parameters for the link pointing to EditDocumentController
206 * @param string $table
207 * @param int $editUid
208 * @param string $fieldList
209 * @return string The input content string, possibly with edit icons added (not necessarily in the end but just after the last string of normal content.
210 */
211 public function editIcons($content, $params, array $conf = [], $currentRecord = '', array $dataArr = [], $addUrlParamStr = '', $table, $editUid, $fieldList)
212 {
213 // Special content is about to be shown, so the cache must be disabled.
214 $this->frontendController->set_no_cache('Display frontend edit icons', true);
215 $iconTitle = $this->cObj->stdWrap($conf['iconTitle'], $conf['iconTitle.']);
216 $iconImg = '<span title="' . htmlspecialchars($iconTitle, ENT_COMPAT, 'UTF-8', false) . '" style="' . ($conf['styleAttribute'] ? htmlspecialchars($conf['styleAttribute']) : '') . '">'
217 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render('inline')
218 . '</span>';
219 $noView = GeneralUtility::_GP('ADMCMD_view') ? 1 : 0;
220
221 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
222 $url = (string)$uriBuilder->buildUriFromRoute(
223 'record_edit',
224 [
225 'edit[' . $table . '][' . $editUid . ']' => 'edit',
226 'columnsOnly' => $fieldList,
227 'noView' => $noView,
228 'feEdit' => 1
229 ]
230 ) . $addUrlParamStr;
231 $icon = $this->editPanelLinkWrap_doWrap($iconImg, $url, 'content-link');
232 if ($conf['beforeLastTag'] < 0) {
233 $content = $icon . $content;
234 } elseif ($conf['beforeLastTag'] > 0) {
235 $cBuf = rtrim($content);
236 $secureCount = 30;
237 while ($secureCount && substr($cBuf, -1) === '>' && substr($cBuf, -4) !== '</a>') {
238 $cBuf = rtrim(preg_replace('/<[^<]*>$/', '', $cBuf));
239 $secureCount--;
240 }
241 $content = strlen($cBuf) && $secureCount ? substr($content, 0, strlen($cBuf)) . $icon . substr($content, strlen($cBuf)) : ($content = $icon . $content);
242 } else {
243 $content .= $icon;
244 }
245 return $content;
246 }
247
248 /**
249 * Helper function for editPanel() which wraps icons in the panel in a link with the action of the panel.
250 * The links are for some of them not simple hyperlinks but onclick-actions which submits a little form which the panel is wrapped in.
251 *
252 * @param string $string The string to wrap in a link, typ. and image used as button in the edit panel.
253 * @param string $formName The name of the form wrapping the edit panel.
254 * @param string $cmd The command of the link. There is a predefined list available: edit, new, up, down etc.
255 * @param string $currentRecord The "table:uid" of the record being processed by the panel.
256 * @param string $confirm Text string with confirmation message; If set a confirm box will be displayed before carrying out the action (if Yes is pressed)
257 * @param int|string $nPid "New pid" - for new records
258 * @return string A <a> tag wrapped string.
259 */
260 protected function editPanelLinkWrap($string, $formName, $cmd, $currentRecord = '', $confirm = '', $nPid = '')
261 {
262 $noView = GeneralUtility::_GP('ADMCMD_view') ? 1 : 0;
263 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
264 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
265 if ($cmd === 'edit') {
266 $rParts = explode(':', $currentRecord);
267 $out = $this->editPanelLinkWrap_doWrap($string, (string)$uriBuilder->buildUriFromRoute('record_edit', ['edit[' . $rParts[0] . '][' . $rParts[1] . ']' => 'edit', 'noView' => $noView, 'feEdit' => 1]), $currentRecord);
268 } elseif ($cmd === 'new') {
269 $rParts = explode(':', $currentRecord);
270 if ($rParts[0] === 'pages') {
271 $out = $this->editPanelLinkWrap_doWrap($string, (string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $rParts[1], 'pagesOnly' => 1]), $currentRecord);
272 } else {
273 if (!(int)$nPid) {
274 $nPid = MathUtility::canBeInterpretedAsInteger($rParts[1]) ? -$rParts[1] : $this->frontendController->id;
275 }
276 $out = $this->editPanelLinkWrap_doWrap($string, (string)$uriBuilder->buildUriFromRoute('record_edit', ['edit[' . $rParts[0] . '][' . $nPid . ']' => 'new', 'noView' => $noView]), $currentRecord);
277 }
278 } else {
279 if ($confirm && $this->backendUser->jsConfirmation(JsConfirmation::FE_EDIT)) {
280 // Gets htmlspecialchared later
281 $cf1 = 'if (confirm(' . GeneralUtility::quoteJSvalue($confirm) . ')) {';
282 $cf2 = '}';
283 } else {
284 $cf1 = ($cf2 = '');
285 }
286 $out = '<a href="#" class="typo3-editPanel-btn typo3-editPanel-btn-default" onclick="' . htmlspecialchars($cf1 . 'document.' . $formName . '[\'TSFE_EDIT[cmd]\'].value=\'' . $cmd . '\'; document.' . $formName . '.submit();' . $cf2 . ' return false;') . '">' . $string . '</a>';
287 }
288 return $out;
289 }
290
291 /**
292 * Creates a link to a script (eg. EditDocumentController or NewRecordController) which either opens in the current frame OR in a pop-up window.
293 *
294 * @param string $string The string to wrap in a link, typ. and image used as button in the edit panel.
295 * @param string $url The URL of the link. Should be absolute if supposed to work with <base> path set.
296 * @param string $additionalClasses Additional CSS classes
297 * @return string A <a> tag wrapped string.
298 * @see editPanelLinkWrap()
299 */
300 protected function editPanelLinkWrap_doWrap($string, $url, $additionalClasses = '')
301 {
302 $width = MathUtility::forceIntegerInRange($this->backendUser->getTSConfig()['options.']['feedit.']['popupWidth'] ?? 690, 690, 5000, 690);
303 $height = MathUtility::forceIntegerInRange($this->backendUser->getTSConfig()['options.']['feedit.']['popupHeight'] ?? 500, 500, 5000, 500);
304 $onclick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue($url . '&returnUrl=' . rawurlencode(PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html')))) . ',\'FEquickEditWindow\',\'width=' . $width . ',height=' . $height . ',status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
305 return '<a href="#" class="typo3-editPanel-btn typo3-editPanel-btn-default frontEndEditIconLinks ' . htmlspecialchars($additionalClasses) . '" onclick="' . htmlspecialchars($onclick) . '" style="display: none;">' . $string . '</a>';
306 }
307
308 /**
309 * Returns TRUE if the input table/row would be hidden in the frontend, according to the current time and simulate user group
310 *
311 * @param string $table The table name
312 * @param array $row The data record
313 * @return bool
314 */
315 protected function isDisabled($table, array $row)
316 {
317 $status = false;
318 if (
319 $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] &&
320 $row[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']] ||
321 $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'] &&
322 $this->frontendController->simUserGroup &&
323 $row[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group']] == $this->frontendController->simUserGroup ||
324 $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'] &&
325 $row[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime']] > $GLOBALS['EXEC_TIME'] ||
326 $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'] &&
327 $row[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime']] &&
328 $row[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime']] < $GLOBALS['EXEC_TIME']
329 ) {
330 $status = true;
331 }
332
333 return $status;
334 }
335
336 /**
337 * Returns the label for key. If a translation for the language set in $this->uc['lang']
338 * is found that is returned, otherwise the default value.
339 * If the global variable $LOCAL_LANG is NOT an array (yet) then this function loads
340 * the global $LOCAL_LANG array with the content of "EXT:core/Resources/Private/Language/locallang_tsfe.xlf"
341 * such that the values therein can be used for labels in the Admin Panel
342 *
343 * @param string $key Key for a label in the $GLOBALS['LOCAL_LANG'] array of "EXT:core/Resources/Private/Language/locallang_tsfe.xlf
344 * @return string The value for the $key
345 */
346 protected function getLabel(string $key): string
347 {
348 if (!is_array($GLOBALS['LOCAL_LANG'])) {
349 $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_tsfe.xlf');
350 if (!is_array($GLOBALS['LOCAL_LANG'])) {
351 $GLOBALS['LOCAL_LANG'] = [];
352 }
353 }
354 return htmlspecialchars($this->getLanguageService()->getLL($key));
355 }
356
357 /**
358 * @return LanguageService
359 */
360 protected function getLanguageService(): LanguageService
361 {
362 return $GLOBALS['LANG'];
363 }
364 }