[BUGFIX] Disable restricted users to edit page properties
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / FrontendEditing / FrontendEditingController.php
1 <?php
2 namespace TYPO3\CMS\Core\FrontendEditing;
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 TYPO3\CMS\Core\Type\Bitmask\Permission;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Controller class for frontend editing.
22 */
23 class FrontendEditingController
24 {
25 /**
26 * GET/POST parameters for the FE editing.
27 * Accessed as $GLOBALS['BE_USER']->frontendEdit->TSFE_EDIT, thus public
28 *
29 * @var array
30 */
31 public $TSFE_EDIT;
32
33 /**
34 * @var \TYPO3\CMS\Core\DataHandling\DataHandler
35 */
36 protected $tce;
37
38 /**
39 * Initializes configuration options.
40 *
41 * @return void
42 */
43 public function initConfigOptions()
44 {
45 $this->TSFE_EDIT = GeneralUtility::_GP('TSFE_EDIT');
46 // Include classes for editing IF editing module in Admin Panel is open
47 if ($GLOBALS['BE_USER']->isFrontendEditingActive()) {
48 if ($this->isEditAction()) {
49 $this->editAction();
50 }
51 }
52 }
53
54 /**
55 * 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.
56 * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element
57 * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel"
58 *
59 * @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.
60 * @param array $conf TypoScript configuration properties for the editPanel
61 * @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
62 * @param array $dataArray Alternative data array to use. Default is $this->data
63 * @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.
64 */
65 public function displayEditPanel($content, array $conf, $currentRecord, array $dataArray)
66 {
67 if ($conf['newRecordFromTable']) {
68 $currentRecord = $conf['newRecordFromTable'] . ':NEW';
69 $conf['allow'] = 'new';
70 $checkEditAccessInternals = false;
71 } else {
72 $checkEditAccessInternals = true;
73 }
74 list($table, $uid) = explode(':', $currentRecord);
75 // Page ID for new records, 0 if not specified
76 $newRecordPid = (int)$conf['newRecordInPid'];
77 if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $GLOBALS['TSFE']->id) {
78 if ($table == 'pages') {
79 $newUid = $uid;
80 } else {
81 if ($conf['newRecordFromTable']) {
82 $newUid = $GLOBALS['TSFE']->id;
83 if ($newRecordPid) {
84 $newUid = $newRecordPid;
85 }
86 } else {
87 $newUid = -1 * $uid;
88 }
89 }
90 }
91 if ($GLOBALS['TSFE']->displayEditIcons && $table && $this->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->allowedToEditLanguage($table, $dataArray)) {
92 $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
93 if ($editClass) {
94 $edit = GeneralUtility::getUserObj($editClass);
95 if (is_object($edit)) {
96 $allowedActions = $this->getAllowedEditActions($table, $conf, $dataArray['pid']);
97 $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, $this->getHiddenFields($dataArray));
98 }
99 }
100 }
101 return $content;
102 }
103
104 /**
105 * Adds an edit icon to the content string. The edit icon links to FormEngine with proper parameters for editing the table/fields of the context.
106 * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well).
107 *
108 * @param string $content The content to which the edit icons should be appended
109 * @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 FormEngine
110 * @param array $conf TypoScript properties for configuring the edit icons.
111 * @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
112 * @param array $dataArray Alternative data array to use. Default is $this->data
113 * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine
114 * @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.
115 */
116 public function displayEditIcons($content, $params, array $conf = array(), $currentRecord = '', array $dataArray = array(), $addUrlParamStr = '')
117 {
118 // Check incoming params:
119 list($currentRecordTable, $currentRecordUID) = explode(':', $currentRecord);
120 list($fieldList, $table) = array_reverse(GeneralUtility::trimExplode(':', $params, true));
121 // Reverse the array because table is optional
122 if (!$table) {
123 $table = $currentRecordTable;
124 } elseif ($table != $currentRecordTable) {
125 // If the table is set as the first parameter, and does not match the table of the current record, then just return.
126 return $content;
127 }
128 $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID;
129 // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
130 if (!array_key_exists('allow', $conf)) {
131 $conf['allow'] = 'edit';
132 }
133 if ($GLOBALS['TSFE']->displayFieldEditIcons && $table && $this->allowedToEdit($table, $dataArray, $conf) && $fieldList && $this->allowedToEditLanguage($table, $dataArray)) {
134 $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
135 if ($editClass) {
136 $edit = GeneralUtility::getUserObj($editClass);
137 if (is_object($edit)) {
138 $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
139 }
140 }
141 }
142 return $content;
143 }
144
145 /*****************************************************
146 *
147 * Frontend Editing
148 *
149 ****************************************************/
150 /**
151 * Returns TRUE if an edit-action is sent from the Admin Panel
152 *
153 * @return bool
154 * @see index_ts.php
155 */
156 public function isEditAction()
157 {
158 if (is_array($this->TSFE_EDIT)) {
159 if ($this->TSFE_EDIT['cancel']) {
160 unset($this->TSFE_EDIT['cmd']);
161 } else {
162 $cmd = (string)$this->TSFE_EDIT['cmd'];
163 if (($cmd != 'edit' || is_array($this->TSFE_EDIT['data']) && ($this->TSFE_EDIT['doSave'] || $this->TSFE_EDIT['update'] || $this->TSFE_EDIT['update_close'])) && $cmd != 'new') {
164 // $cmd can be a command like "hide" or "move". If $cmd is "edit" or "new" it's an indication to show the formfields. But if data is sent with update-flag then $cmd = edit is accepted because edit may be sent because of .keepGoing flag.
165 return true;
166 }
167 }
168 }
169 return false;
170 }
171
172 /**
173 * Returns TRUE if an edit form is shown on the page.
174 * Used from index_ts.php where a TRUE return-value will result in classes etc. being included.
175 *
176 * @return bool
177 * @see index_ts.php
178 */
179 public function isEditFormShown()
180 {
181 if (is_array($this->TSFE_EDIT)) {
182 $cmd = (string)$this->TSFE_EDIT['cmd'];
183 if ($cmd == 'edit' || $cmd == 'new') {
184 return true;
185 }
186 }
187 }
188
189 /**
190 * Management of the on-page frontend editing forms and edit panels.
191 * Basically taking in the data and commands and passes them on to the proper classes as they should be.
192 *
193 * @return void
194 * @throws UnexpectedValueException if TSFE_EDIT[cmd] is not a valid command
195 * @see index_ts.php
196 */
197 public function editAction()
198 {
199 // Commands
200 list($table, $uid) = explode(':', $this->TSFE_EDIT['record']);
201 $uid = (int)$uid;
202 $cmd = $this->TSFE_EDIT['cmd'];
203 // Look for some TSFE_EDIT data that indicates we should save.
204 if (($this->TSFE_EDIT['doSave'] || $this->TSFE_EDIT['update'] || $this->TSFE_EDIT['update_close']) && is_array($this->TSFE_EDIT['data'])) {
205 $cmd = 'save';
206 }
207 if ($cmd == 'save' || $cmd && $table && $uid && isset($GLOBALS['TCA'][$table])) {
208 // Hook for defining custom editing actions. Naming is incorrect, but preserves compatibility.
209 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['extEditAction'])) {
210 $_params = array();
211 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['extEditAction'] as $_funcRef) {
212 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
213 }
214 }
215 // Perform the requested editing command.
216 $cmdAction = 'do' . ucwords($cmd);
217 if (is_callable(array($this, $cmdAction))) {
218 $this->{$cmdAction}($table, $uid);
219 } else {
220 throw new \UnexpectedValueException('The specified frontend edit command (' . $cmd . ') is not valid.', 1225818120);
221 }
222 }
223 }
224
225 /**
226 * Hides a specific record.
227 *
228 * @param string $table The table name for the record to hide.
229 * @param int $uid The UID for the record to hide.
230 * @return void
231 */
232 public function doHide($table, $uid)
233 {
234 $hideField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
235 if ($hideField) {
236 $recData = array();
237 $recData[$table][$uid][$hideField] = 1;
238 $this->initializeTceMain();
239 $this->tce->start($recData, array());
240 $this->tce->process_datamap();
241 }
242 }
243
244 /**
245 * Unhides (shows) a specific record.
246 *
247 * @param string $table The table name for the record to unhide.
248 * @param int $uid The UID for the record to unhide.
249 * @return void
250 */
251 public function doUnhide($table, $uid)
252 {
253 $hideField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
254 if ($hideField) {
255 $recData = array();
256 $recData[$table][$uid][$hideField] = 0;
257 $this->initializeTceMain();
258 $this->tce->start($recData, array());
259 $this->tce->process_datamap();
260 }
261 }
262
263 /**
264 * Moves a record up.
265 *
266 * @param string $table The table name for the record to move.
267 * @param int $uid The UID for the record to hide.
268 * @return void
269 */
270 public function doUp($table, $uid)
271 {
272 $this->move($table, $uid, 'up');
273 }
274
275 /**
276 * Moves a record down.
277 *
278 * @param string $table The table name for the record to move.
279 * @param int $uid The UID for the record to move.
280 * @return void
281 */
282 public function doDown($table, $uid)
283 {
284 $this->move($table, $uid, 'down');
285 }
286
287 /**
288 * Moves a record after a given element. Used for drag.
289 *
290 * @param string $table The table name for the record to move.
291 * @param int $uid The UID for the record to move.
292 * @return void
293 */
294 public function doMoveAfter($table, $uid)
295 {
296 $afterUID = $GLOBALS['BE_USER']->frontendEdit->TSFE_EDIT['moveAfter'];
297 $this->move($table, $uid, '', $afterUID);
298 }
299
300 /**
301 * Moves a record
302 *
303 * @param string $table The table name for the record to move.
304 * @param int $uid The UID for the record to move.
305 * @param string $direction The direction to move, either 'up' or 'down'.
306 * @param int $afterUID The UID of record to move after. This is specified for dragging only.
307 * @return void
308 */
309 protected function move($table, $uid, $direction = '', $afterUID = 0)
310 {
311 $cmdData = array();
312 $sortField = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
313 if ($sortField) {
314 // Get self
315 $fields = array_unique(GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'] . ',uid,pid,' . $sortField, true));
316 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(implode(',', $fields), $table, 'uid=' . $uid);
317 if ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
318 // Record before or after
319 if ($GLOBALS['BE_USER']->adminPanel instanceof \TYPO3\CMS\Frontend\View\AdminPanelView && $GLOBALS['BE_USER']->adminPanel->extGetFeAdminValue('preview')) {
320 $ignore = array('starttime' => 1, 'endtime' => 1, 'disabled' => 1, 'fe_group' => 1);
321 }
322 $copyAfterFieldsQuery = '';
323 if ($GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
324 $cAFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
325 foreach ($cAFields as $fieldName) {
326 $copyAfterFieldsQuery .= ' AND ' . $fieldName . '="' . $row[$fieldName] . '"';
327 }
328 }
329 if (!empty($direction)) {
330 if ($direction == 'up') {
331 $operator = '<';
332 $order = 'DESC';
333 } else {
334 $operator = '>';
335 $order = 'ASC';
336 }
337 $sortCheck = ' AND ' . $sortField . $operator . (int)$row[$sortField];
338 }
339 $GLOBALS['TYPO3_DB']->sql_free_result($res);
340 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid,pid', $table, 'pid=' . (int)$row['pid'] . $sortCheck . $copyAfterFieldsQuery . $GLOBALS['TSFE']->sys_page->enableFields($table, '', $ignore), '', $sortField . ' ' . $order, '2');
341 if ($row2 = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
342 if ($afterUID) {
343 $cmdData[$table][$uid]['move'] = -$afterUID;
344 } elseif ($direction == 'down') {
345 $cmdData[$table][$uid]['move'] = -$row2['uid'];
346 } elseif ($row3 = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
347 // Must take the second record above...
348 $cmdData[$table][$uid]['move'] = -$row3['uid'];
349 } else {
350 // ... and if that does not exist, use pid
351 $cmdData[$table][$uid]['move'] = $row['pid'];
352 }
353 } elseif ($direction == 'up') {
354 $cmdData[$table][$uid]['move'] = $row['pid'];
355 }
356 $GLOBALS['TYPO3_DB']->sql_free_result($res);
357 }
358 if (!empty($cmdData)) {
359 $this->initializeTceMain();
360 $this->tce->start(array(), $cmdData);
361 $this->tce->process_cmdmap();
362 }
363 }
364 }
365
366 /**
367 * Deletes a specific record.
368 *
369 * @param string $table The table name for the record to delete.
370 * @param int $uid The UID for the record to delete.
371 * @return void
372 */
373 public function doDelete($table, $uid)
374 {
375 $cmdData[$table][$uid]['delete'] = 1;
376 if (!empty($cmdData)) {
377 $this->initializeTceMain();
378 $this->tce->start(array(), $cmdData);
379 $this->tce->process_cmdmap();
380 }
381 }
382
383 /**
384 * Saves a record based on its data array.
385 *
386 * @param string $table The table name for the record to save.
387 * @param int $uid The UID for the record to save.
388 * @return void
389 */
390 public function doSave($table, $uid)
391 {
392 $data = $this->TSFE_EDIT['data'];
393 if (!empty($data)) {
394 $this->initializeTceMain();
395 $this->tce->start($data, array());
396 $this->tce->process_uploads($_FILES);
397 $this->tce->process_datamap();
398 // Save the new UID back into TSFE_EDIT
399 $newUID = $this->tce->substNEWwithIDs['NEW'];
400 if ($newUID) {
401 $GLOBALS['BE_USER']->frontendEdit->TSFE_EDIT['newUID'] = $newUID;
402 }
403 }
404 }
405
406 /**
407 * Saves a record based on its data array and closes it.
408 *
409 * @param string $table The table name for the record to save.
410 * @param int $uid The UID for the record to save.
411 * @return void
412 * @note This method is only a wrapper for doSave() but is needed so
413 */
414 public function doSaveAndClose($table, $uid)
415 {
416 $this->doSave($table, $uid);
417 }
418
419 /**
420 * Stub for closing a record. No real functionality needed since content
421 * element rendering will take care of everything.
422 *
423 * @param string $table The table name for the record to close.
424 * @param int $uid The UID for the record to close.
425 * @return void
426 */
427 public function doClose($table, $uid)
428 {
429 }
430
431 /**
432 * Checks whether the user has access to edit the language for the
433 * requested record.
434 *
435 * @param string $table The name of the table.
436 * @param array $currentRecord The record.
437 * @return bool
438 */
439 protected function allowedToEditLanguage($table, array $currentRecord)
440 {
441 // If no access right to record languages, return immediately
442 if ($table === 'pages') {
443 $lang = $GLOBALS['TSFE']->sys_language_uid;
444 } elseif ($table === 'tt_content') {
445 $lang = $GLOBALS['TSFE']->sys_language_content;
446 } elseif ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
447 $lang = $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
448 } else {
449 $lang = -1;
450 }
451 if ($GLOBALS['BE_USER']->checkLanguageAccess($lang)) {
452 $languageAccess = true;
453 } else {
454 $languageAccess = false;
455 }
456 return $languageAccess;
457 }
458
459 /**
460 * Checks whether the user is allowed to edit the requested table.
461 *
462 * @param string $table The name of the table.
463 * @param array $dataArray The data array.
464 * @param array $conf The configuration array for the edit panel.
465 * @param bool $checkEditAccessInternals Boolean indicating whether recordEditAccessInternals should not be checked. Defaults
466 * @return bool
467 */
468 protected function allowedToEdit($table, array $dataArray, array $conf, $checkEditAccessInternals = true)
469 {
470 // Unless permissions specifically allow it, editing is not allowed.
471 $mayEdit = false;
472 if ($checkEditAccessInternals) {
473 $editAccessInternals = $GLOBALS['BE_USER']->recordEditAccessInternals($table, $dataArray, false, false);
474 } else {
475 $editAccessInternals = true;
476 }
477 if ($editAccessInternals) {
478 if ($table == 'pages') {
479 // 2 = permission to edit the page
480 if ($GLOBALS['BE_USER']->isAdmin() || $GLOBALS['BE_USER']->doesUserHaveAccess($dataArray, 2)) {
481 $mayEdit = true;
482 }
483 } else {
484 // 16 = permission to edit content on the page
485 if ($GLOBALS['BE_USER']->isAdmin() || $GLOBALS['BE_USER']->doesUserHaveAccess(\TYPO3\CMS\Backend\Utility\BackendUtility::getRecord('pages', $dataArray['pid']), 16)) {
486 $mayEdit = true;
487 }
488 }
489 if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $GLOBALS['TSFE']->id) {
490 // Permissions:
491 $types = GeneralUtility::trimExplode(',', GeneralUtility::strtolower($conf['allow']), true);
492 $allow = array_flip($types);
493 $perms = $GLOBALS['BE_USER']->calcPerms($GLOBALS['TSFE']->page);
494 if ($table == 'pages') {
495 $allow = $this->getAllowedEditActions($table, $conf, $dataArray['pid'], $allow);
496 // Can only display editbox if there are options in the menu
497 if (!empty($allow)) {
498 $mayEdit = true;
499 }
500 } else {
501 $mayEdit = !empty($allow) && $perms & Permission::CONTENT_EDIT;
502 }
503 }
504 }
505 return $mayEdit;
506 }
507
508 /**
509 * Takes an array of generally allowed actions and filters that list based on page and content permissions.
510 *
511 * @param string $table The name of the table.
512 * @param array $conf The configuration array.
513 * @param int $pid The PID where editing will occur.
514 * @param string $allow Comma-separated list of actions that are allowed in general.
515 * @return array
516 */
517 protected function getAllowedEditActions($table, array $conf, $pid, $allow = '')
518 {
519 if (!$allow) {
520 $types = GeneralUtility::trimExplode(',', GeneralUtility::strtolower($conf['allow']), true);
521 $allow = array_flip($types);
522 }
523 if (!$conf['onlyCurrentPid'] || $pid == $GLOBALS['TSFE']->id) {
524 // Permissions
525 $types = GeneralUtility::trimExplode(',', GeneralUtility::strtolower($conf['allow']), true);
526 $allow = array_flip($types);
527 $perms = $GLOBALS['BE_USER']->calcPerms($GLOBALS['TSFE']->page);
528 if ($table == 'pages') {
529 // Rootpage
530 if (count($GLOBALS['TSFE']->config['rootLine']) === 1) {
531 unset($allow['move']);
532 unset($allow['hide']);
533 unset($allow['delete']);
534 }
535 if (!($perms & Permission::PAGE_EDIT) || !$GLOBALS['BE_USER']->checkLanguageAccess(0)) {
536 unset($allow['edit']);
537 unset($allow['move']);
538 unset($allow['hide']);
539 }
540 if (!($perms & Permission::PAGE_DELETE)) {
541 unset($allow['delete']);
542 }
543 if (!($perms & Permission::PAGE_NEW)) {
544 unset($allow['new']);
545 }
546 }
547 }
548 return $allow;
549 }
550
551 /**
552 * Adds any extra Javascript includes needed for Front-end editing
553 *
554 * @return string
555 */
556 public function getJavascriptIncludes()
557 {
558 // No extra JS includes needed
559 return '';
560 }
561
562 /**
563 * Gets the hidden fields (array key=field name, value=field value) to be used in the edit panel for a particular content element.
564 * In the normal case, no hidden fields are needed but special controllers such as TemplaVoila need to track flexform pointers, etc.
565 *
566 * @param array $dataArray The data array for a specific content element.
567 * @return array
568 */
569 public function getHiddenFields(array $dataArray)
570 {
571 // No special hidden fields needed.
572 return array();
573 }
574
575 /**
576 * Initializes \TYPO3\CMS\Core\DataHandling\DataHandler since it is used on modification actions.
577 *
578 * @return void
579 */
580 protected function initializeTceMain()
581 {
582 if (!isset($this->tce)) {
583 $this->tce = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
584 $this->tce->stripslashes_values = 0;
585 }
586 }
587 }