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