[BUGFIX] Repair overrideVals for FormEngine
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / EditDocumentController.php
1 <?php
2 namespace TYPO3\CMS\Backend\Controller;
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 Psr\Http\Message\ServerRequestInterface;
18 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
19 use TYPO3\CMS\Backend\Form\FormResultCompiler;
20 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Backend\Utility\IconUtility;
23 use TYPO3\CMS\Core\DataHandling\DataHandler;
24 use TYPO3\CMS\Core\Http\Response;
25 use TYPO3\CMS\Core\Imaging\Icon;
26 use TYPO3\CMS\Core\Imaging\IconFactory;
27 use TYPO3\CMS\Core\Messaging\FlashMessage;
28 use TYPO3\CMS\Core\Messaging\FlashMessageService;
29 use TYPO3\CMS\Core\Page\PageRenderer;
30 use TYPO3\CMS\Core\Type\Bitmask\Permission;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32 use TYPO3\CMS\Core\Utility\HttpUtility;
33 use TYPO3\CMS\Core\Utility\MathUtility;
34 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
35 use TYPO3\CMS\Frontend\Page\PageRepository;
36 use TYPO3\CMS\Backend\Form\FormDataCompiler;
37 use TYPO3\CMS\Backend\Form\NodeFactory;
38 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
39
40 /**
41 * Script Class: Drawing the editing form for editing records in TYPO3.
42 * Notice: It does NOT use tce_db.php to submit data to, rather it handles submissions itself
43 */
44 class EditDocumentController implements \TYPO3\CMS\Core\Http\ControllerInterface {
45
46 /**
47 * GPvar "edit": Is an array looking approx like [tablename][list-of-ids]=command, eg.
48 * "&edit[pages][123]=edit". See \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick(). Value can be seen modified
49 * internally (converting NEW keyword to id, workspace/versioning etc).
50 *
51 * @var array
52 */
53 public $editconf;
54
55 /**
56 * Commalist of fieldnames to edit. The point is IF you specify this list, only those
57 * fields will be rendered in the form. Otherwise all (available) fields in the record
58 * is shown according to the types configuration in $GLOBALS['TCA']
59 *
60 * @var bool
61 */
62 public $columnsOnly;
63
64 /**
65 * Default values for fields (array with tablenames, fields etc. as keys).
66 * Can be seen modified internally.
67 *
68 * @var array
69 */
70 public $defVals;
71
72 /**
73 * Array of values to force being set (as hidden fields). Will be set as $this->defVals
74 * IF defVals does not exist.
75 *
76 * @var array
77 */
78 public $overrideVals;
79
80 /**
81 * If set, this value will be set in $this->retUrl (which is used quite many places
82 * as the return URL). If not set, "dummy.php" will be set in $this->retUrl
83 *
84 * @var string
85 */
86 public $returnUrl;
87
88 /**
89 * Close-document command. Not really sure of all options...
90 *
91 * @var int
92 */
93 public $closeDoc;
94
95 /**
96 * Quite simply, if this variable is set, then the processing of incoming data will be performed
97 * as if a save-button is pressed. Used in the forms as a hidden field which can be set through
98 * JavaScript if the form is somehow submitted by JavaScript).
99 *
100 * @var bool
101 */
102 public $doSave;
103
104 /**
105 * The data array from which the data comes...
106 *
107 * @var array
108 */
109 public $data;
110
111 /**
112 * @var string
113 */
114 public $cmd;
115
116 /**
117 * @var array
118 */
119 public $mirror;
120
121 /**
122 * Clear-cache cmd.
123 *
124 * @var string
125 */
126 public $cacheCmd;
127
128 /**
129 * Redirect (not used???)
130 *
131 * @var string
132 */
133 public $redirect;
134
135 /**
136 * Boolean: If set, then the GET var "&id=" will be added to the
137 * retUrl string so that the NEW id of something is returned to the script calling the form.
138 *
139 * @var bool
140 */
141 public $returnNewPageId;
142
143 /**
144 * @var string
145 */
146 public $vC;
147
148 /**
149 * update BE_USER->uc
150 *
151 * @var array
152 */
153 public $uc;
154
155 /**
156 * ID for displaying the page in the frontend (used for SAVE/VIEW operations)
157 *
158 * @var int
159 */
160 public $popViewId;
161
162 /**
163 * Additional GET vars for the link, eg. "&L=xxx"
164 *
165 * @var string
166 */
167 public $popViewId_addParams;
168
169 /**
170 * Alternative URL for viewing the frontend pages.
171 *
172 * @var string
173 */
174 public $viewUrl;
175
176 /**
177 * If this is pointing to a page id it will automatically load all content elements
178 * (NORMAL column/default language) from that page into the form!
179 *
180 * @var int
181 * @deprecated since TYPO3 CMS 7, will be removed with TYPO3 CMS 8
182 */
183 public $editRegularContentFromId;
184
185 /**
186 * Alternative title for the document handler.
187 *
188 * @var string
189 */
190 public $recTitle;
191
192 /**
193 * If set, then no SAVE/VIEW button is printed
194 *
195 * @var bool
196 */
197 public $noView;
198
199 /**
200 * @var string
201 */
202 public $perms_clause;
203
204 /**
205 * If set, the $this->editconf array is returned to the calling script
206 * (used by wizard_add.php for instance)
207 *
208 * @var bool
209 */
210 public $returnEditConf;
211
212 /**
213 * Workspace used for the editing action.
214 *
215 * @var NULL|integer
216 */
217 protected $workspace;
218
219 /**
220 * document template object
221 *
222 * @var \TYPO3\CMS\Backend\Template\DocumentTemplate
223 */
224 public $doc;
225
226 /**
227 * a static HTML template, usually in templates/alt_doc.html
228 *
229 * @var string
230 */
231 public $template;
232
233 /**
234 * Content accumulation
235 *
236 * @var string
237 */
238 public $content;
239
240 /**
241 * Return URL script, processed. This contains the script (if any) that we should
242 * RETURN TO from the FormEngine script IF we press the close button. Thus this
243 * variable is normally passed along from the calling script so we can properly return if needed.
244 *
245 * @var string
246 */
247 public $retUrl;
248
249 /**
250 * Contains the parts of the REQUEST_URI (current url). By parts we mean the result of resolving
251 * REQUEST_URI (current url) by the parse_url() function. The result is an array where eg. "path"
252 * is the script path and "query" is the parameters...
253 *
254 * @var array
255 */
256 public $R_URL_parts;
257
258 /**
259 * Contains the current GET vars array; More specifically this array is the foundation for creating
260 * the R_URI internal var (which becomes the "url of this script" to which we submit the forms etc.)
261 *
262 * @var array
263 */
264 public $R_URL_getvars;
265
266 /**
267 * Set to the URL of this script including variables which is needed to re-display the form. See main()
268 *
269 * @var string
270 */
271 public $R_URI;
272
273 /**
274 * @var array
275 */
276 public $MCONF;
277
278 /**
279 * @var array
280 */
281 public $pageinfo;
282
283 /**
284 * Is loaded with the "title" of the currently "open document" - this is used in the
285 * Document Selector box. (see makeDocSel())
286 *
287 * @var string
288 */
289 public $storeTitle = '';
290
291 /**
292 * Contains an array with key/value pairs of GET parameters needed to reach the
293 * current document displayed - used in the Document Selector box. (see compileStoreDat())
294 *
295 * @var array
296 */
297 public $storeArray;
298
299 /**
300 * Contains storeArray, but imploded into a GET parameter string (see compileStoreDat())
301 *
302 * @var string
303 */
304 public $storeUrl;
305
306 /**
307 * Hashed value of storeURL (see compileStoreDat())
308 *
309 * @var string
310 */
311 public $storeUrlMd5;
312
313 /**
314 * Module session data
315 *
316 * @var array
317 */
318 public $docDat;
319
320 /**
321 * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
322 * the various documents on the GET parameter list needed to open it. The values are
323 * arrays with 0,1,2 keys with information about the document (see compileStoreDat()).
324 * The docHandler variable is stored in the $docDat session data, key "0".
325 *
326 * @var array
327 */
328 public $docHandler;
329
330 /**
331 * Array of the elements to create edit forms for.
332 *
333 * @var array
334 */
335 public $elementsData;
336
337 /**
338 * Pointer to the first element in $elementsData
339 *
340 * @var array
341 */
342 public $firstEl;
343
344 /**
345 * Counter, used to count the number of errors (when users do not have edit permissions)
346 *
347 * @var int
348 */
349 public $errorC;
350
351 /**
352 * Counter, used to count the number of new record forms displayed
353 *
354 * @var int
355 */
356 public $newC;
357
358 /**
359 * Is set to the pid value of the last shown record - thus indicating which page to
360 * show when clicking the SAVE/VIEW button
361 *
362 * @var int
363 */
364 public $viewId;
365
366 /**
367 * Is set to additional parameters (like "&L=xxx") if the record supports it.
368 *
369 * @var string
370 */
371 public $viewId_addParams;
372
373 /**
374 * Module TSconfig, loaded from main() based on the page id value of viewId
375 *
376 * @var array
377 */
378 public $modTSconfig;
379
380 /**
381 * @var FormResultCompiler
382 */
383 protected $formResultCompiler;
384
385 /**
386 * Used internally to disable the storage of the document reference (eg. new records)
387 *
388 * @var bool
389 */
390 public $dontStoreDocumentRef = 0;
391
392 /**
393 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
394 */
395 protected $signalSlotDispatcher;
396
397 /**
398 * Stores information needed to preview the currently saved record
399 *
400 * @var array
401 */
402 protected $previewData = [];
403
404 /**
405 * @var IconFactory
406 */
407 protected $iconFactory;
408
409 /**
410 * Constructor
411 */
412 public function __construct() {
413 $GLOBALS['SOBE'] = $this;
414 $this->getLanguageService()->includeLLFile('EXT:lang/locallang_alt_doc.xlf');
415 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
416 }
417
418 /**
419 * Get the SignalSlot dispatcher
420 *
421 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
422 */
423 protected function getSignalSlotDispatcher() {
424 if (!isset($this->signalSlotDispatcher)) {
425 $this->signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
426 }
427 return $this->signalSlotDispatcher;
428 }
429
430 /**
431 * Emits a signal after a function was executed
432 *
433 * @param string $signalName
434 */
435 protected function emitFunctionAfterSignal($signalName) {
436 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, $signalName . 'After', array($this));
437 }
438
439 /**
440 * First initialization.
441 *
442 * @return void
443 */
444 public function preInit() {
445 if (GeneralUtility::_GP('justLocalized')) {
446 $this->localizationRedirect(GeneralUtility::_GP('justLocalized'));
447 }
448 // Setting GPvars:
449 $this->editconf = GeneralUtility::_GP('edit');
450 $this->defVals = GeneralUtility::_GP('defVals');
451 $this->overrideVals = GeneralUtility::_GP('overrideVals');
452 $this->columnsOnly = GeneralUtility::_GP('columnsOnly');
453 $this->returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
454 $this->closeDoc = GeneralUtility::_GP('closeDoc');
455 $this->doSave = GeneralUtility::_GP('doSave');
456 $this->returnEditConf = GeneralUtility::_GP('returnEditConf');
457 $this->localizationMode = GeneralUtility::_GP('localizationMode');
458 $this->workspace = GeneralUtility::_GP('workspace');
459 $this->uc = GeneralUtility::_GP('uc');
460 // Setting override values as default if defVals does not exist.
461 if (!is_array($this->defVals) && is_array($this->overrideVals)) {
462 $this->defVals = $this->overrideVals;
463 }
464 // Setting return URL
465 $this->retUrl = $this->returnUrl ?: BackendUtility::getModuleUrl('dummy');
466 // Fix $this->editconf if versioning applies to any of the records
467 $this->fixWSversioningInEditConf();
468 // Make R_URL (request url) based on input GETvars:
469 $this->R_URL_parts = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
470 $this->R_URL_getvars = GeneralUtility::_GET();
471 $this->R_URL_getvars['edit'] = $this->editconf;
472 // MAKE url for storing
473 $this->compileStoreDat();
474 // Get session data for the module:
475 $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
476 $this->docHandler = $this->docDat[0];
477 // If a request for closing the document has been sent, act accordingly:
478 if ($this->closeDoc > 0) {
479 $this->closeDocument($this->closeDoc);
480 }
481 // If NO vars are sent to the script, try to read first document:
482 // Added !is_array($this->editconf) because editConf must not be set either.
483 // Anyways I can't figure out when this situation here will apply...
484 if (is_array($this->R_URL_getvars) && count($this->R_URL_getvars) < 2 && !is_array($this->editconf)) {
485 $this->setDocument($this->docDat[1]);
486 }
487
488 // Sets a temporary workspace, this request is based on
489 if ($this->workspace !== NULL) {
490 $this->getBackendUser()->setTemporaryWorkspace($this->workspace);
491 }
492
493 $this->emitFunctionAfterSignal(__FUNCTION__);
494 }
495
496 /**
497 * Detects, if a save command has been triggered.
498 *
499 * @return bool TRUE, then save the document (data submitted)
500 */
501 public function doProcessData() {
502 $out = $this->doSave || isset($_POST['_savedok']) || isset($_POST['_saveandclosedok']) || isset($_POST['_savedokview']) || isset($_POST['_savedoknew']) || isset($_POST['_translation_savedok_x']) || isset($_POST['_translation_savedokclear_x']);
503 return $out;
504 }
505
506 /**
507 * Do processing of data, submitting it to TCEmain.
508 *
509 * @return void
510 */
511 public function processData() {
512 $beUser = $this->getBackendUser();
513 // GPvars specifically for processing:
514 $control = GeneralUtility::_GP('control');
515 $this->data = GeneralUtility::_GP('data');
516 $this->cmd = GeneralUtility::_GP('cmd');
517 $this->mirror = GeneralUtility::_GP('mirror');
518 $this->cacheCmd = GeneralUtility::_GP('cacheCmd');
519 $this->redirect = GeneralUtility::_GP('redirect');
520 $this->returnNewPageId = GeneralUtility::_GP('returnNewPageId');
521 $this->vC = GeneralUtility::_GP('vC');
522 // See tce_db.php for relevate options here:
523 // Only options related to $this->data submission are included here.
524 /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
525 $tce = GeneralUtility::makeInstance(DataHandler::class);
526 $tce->stripslashes_values = FALSE;
527
528 if (!empty($control)) {
529 $tce->setControl($control);
530 }
531 if (isset($_POST['_translation_savedok_x'])) {
532 $tce->updateModeL10NdiffData = 'FORCE_FFUPD';
533 }
534 if (isset($_POST['_translation_savedokclear_x'])) {
535 $tce->updateModeL10NdiffData = 'FORCE_FFUPD';
536 $tce->updateModeL10NdiffDataClear = TRUE;
537 }
538 // Setting default values specific for the user:
539 $TCAdefaultOverride = $beUser->getTSConfigProp('TCAdefaults');
540 if (is_array($TCAdefaultOverride)) {
541 $tce->setDefaultsFromUserTS($TCAdefaultOverride);
542 }
543 // Setting internal vars:
544 if ($beUser->uc['neverHideAtCopy']) {
545 $tce->neverHideAtCopy = 1;
546 }
547 // Loading TCEmain with data:
548 $tce->start($this->data, $this->cmd);
549 if (is_array($this->mirror)) {
550 $tce->setMirror($this->mirror);
551 }
552 // Checking referer / executing
553 $refInfo = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
554 $httpHost = GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY');
555 if ($httpHost != $refInfo['host'] && $this->vC != $beUser->veriCode() && !$GLOBALS['TYPO3_CONF_VARS']['SYS']['doNotCheckReferer']) {
556 $tce->log('', 0, 0, 0, 1, 'Referer host \'%s\' and server host \'%s\' did not match and veriCode was not valid either!', 1, array($refInfo['host'], $httpHost));
557 debug('Error: Referer host did not match with server host.');
558 } else {
559 // Perform the saving operation with TCEmain:
560 $tce->process_uploads($_FILES);
561 $tce->process_datamap();
562 $tce->process_cmdmap();
563 // If pages are being edited, we set an instruction about updating the page tree after this operation.
564 if ($tce->pagetreeNeedsRefresh && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))) {
565 BackendUtility::setUpdateSignal('updatePageTree');
566 }
567 // If there was saved any new items, load them:
568 if (!empty($tce->substNEWwithIDs_table)) {
569 // save the expanded/collapsed states for new inline records, if any
570 FormEngineUtility::updateInlineView($this->uc, $tce);
571 $newEditConf = array();
572 foreach ($this->editconf as $tableName => $tableCmds) {
573 $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
574 if (!empty($keys)) {
575 foreach ($keys as $key) {
576 $editId = $tce->substNEWwithIDs[$key];
577 // Check if the $editId isn't a child record of an IRRE action
578 if (!(is_array($tce->newRelatedIDs[$tableName]) && in_array($editId, $tce->newRelatedIDs[$tableName]))) {
579 // Translate new id to the workspace version:
580 if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord($beUser->workspace, $tableName, $editId, 'uid')) {
581 $editId = $versionRec['uid'];
582 }
583 $newEditConf[$tableName][$editId] = 'edit';
584 }
585 // Traverse all new records and forge the content of ->editconf so we can continue to EDIT these records!
586 if ($tableName == 'pages' && $this->retUrl != BackendUtility::getModuleUrl('dummy') && $this->returnNewPageId) {
587 $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
588 }
589 }
590 } else {
591 $newEditConf[$tableName] = $tableCmds;
592 }
593 }
594 // Resetting editconf if newEditConf has values:
595 if (!empty($newEditConf)) {
596 $this->editconf = $newEditConf;
597 }
598 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
599 $this->R_URL_getvars['edit'] = $this->editconf;
600 // Unsetting default values since we don't need them anymore.
601 unset($this->R_URL_getvars['defVals']);
602 // Re-compile the store* values since editconf changed...
603 $this->compileStoreDat();
604 }
605 // See if any records was auto-created as new versions?
606 if (!empty($tce->autoVersionIdMap)) {
607 $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
608 }
609 // If a document is saved and a new one is created right after.
610 if (isset($_POST['_savedoknew']) && is_array($this->editconf)) {
611 // Finding the current table:
612 reset($this->editconf);
613 $nTable = key($this->editconf);
614 // Finding the first id, getting the records pid+uid
615 reset($this->editconf[$nTable]);
616 $nUid = key($this->editconf[$nTable]);
617 $nRec = BackendUtility::getRecord($nTable, $nUid, 'pid,uid');
618 // Setting a blank editconf array for a new record:
619 $this->editconf = array();
620 if ($this->getNewIconMode($nTable) == 'top') {
621 $this->editconf[$nTable][$nRec['pid']] = 'new';
622 } else {
623 $this->editconf[$nTable][-$nRec['uid']] = 'new';
624 }
625 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
626 $this->R_URL_getvars['edit'] = $this->editconf;
627 // Re-compile the store* values since editconf changed...
628 $this->compileStoreDat();
629 }
630 // If a preview is requested
631 if (isset($_POST['_savedokview'])) {
632 // Get the first table and id of the data array from DataHandler
633 $table = reset(array_keys($this->data));
634 $id = reset(array_keys($this->data[$table]));
635 if (!MathUtility::canBeInterpretedAsInteger($id)) {
636 $id = $tce->substNEWwithIDs[$id];
637 }
638 // Store this information for later use
639 $this->previewData['table'] = $table;
640 $this->previewData['id'] = $id;
641 }
642 $tce->printLogErrorMessages(isset($_POST['_saveandclosedok']) || isset($_POST['_translation_savedok_x']) ? $this->retUrl : $this->R_URL_parts['path'] . '?' . GeneralUtility::implodeArrayForUrl('', $this->R_URL_getvars));
643 }
644 // || count($tce->substNEWwithIDs)... If any new items has been save, the document is CLOSED
645 // because if not, we just get that element re-listed as new. And we don't want that!
646 if (isset($_POST['_saveandclosedok']) || isset($_POST['_translation_savedok_x']) || $this->closeDoc < 0) {
647 $this->closeDocument(abs($this->closeDoc));
648 }
649 }
650
651 /**
652 * Initialize the normal module operation
653 *
654 * @return void
655 */
656 public function init() {
657 $beUser = $this->getBackendUser();
658 // Setting more GPvars:
659 $this->popViewId = GeneralUtility::_GP('popViewId');
660 $this->popViewId_addParams = GeneralUtility::_GP('popViewId_addParams');
661 $this->viewUrl = GeneralUtility::_GP('viewUrl');
662 $this->editRegularContentFromId = GeneralUtility::_GP('editRegularContentFromId');
663 $this->recTitle = GeneralUtility::_GP('recTitle');
664 $this->noView = GeneralUtility::_GP('noView');
665 $this->perms_clause = $beUser->getPagePermsClause(1);
666 // Set other internal variables:
667 $this->R_URL_getvars['returnUrl'] = $this->retUrl;
668 $this->R_URI = $this->R_URL_parts['path'] . '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $this->R_URL_getvars), '&');
669 // Setting virtual document name
670 $this->MCONF['name'] = 'xMOD_alt_doc.php';
671
672 // Create an instance of the document template object
673 $this->doc = $GLOBALS['TBE_TEMPLATE'];
674 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
675 $pageRenderer->addInlineLanguageLabelFile('EXT:lang/locallang_alt_doc.xlf');
676 $this->doc->setModuleTemplate('EXT:backend/Resources/Private/Templates/alt_doc.html');
677 $this->doc->form = '<form action="' . htmlspecialchars($this->R_URI) . '" method="post" enctype="multipart/form-data" name="editform" onsubmit="document.editform._scrollPosition.value=(document.documentElement.scrollTop || document.body.scrollTop); TBE_EDITOR.checkAndDoSubmit(1); return false;">';
678 // override the default jumpToUrl
679 $this->doc->JScodeArray['jumpToUrl'] = '
680 function jumpToUrl(URL,formEl) {
681 if (!TBE_EDITOR.isFormChanged()) {
682 window.location.href = URL;
683 } else if (formEl && formEl.type=="checkbox") {
684 formEl.checked = formEl.checked ? 0 : 1;
685 }
686 }
687 ';
688 // define the window size of the element browser
689 $popupWindowWidth = 700;
690 $popupWindowHeight = 750;
691 $popupWindowSize = trim($beUser->getTSConfigVal('options.popupWindowSize'));
692 if (!empty($popupWindowSize)) {
693 list($popupWindowWidth, $popupWindowHeight) = GeneralUtility::intExplode('x', $popupWindowSize);
694 }
695 $t3Configuration = array(
696 'PopupWindow' => array(
697 'width' => $popupWindowWidth,
698 'height' => $popupWindowHeight
699 ),
700 );
701 $javascript = '
702 TYPO3.configuration = ' . json_encode($t3Configuration) . ';
703 // Object: TS:
704 // passwordDummy and decimalSign are used by tbe_editor.js and have to be declared here as
705 // TS object overwrites the object declared in tbe_editor.js
706 function typoSetup() { //
707 this.uniqueID = "";
708 this.passwordDummy = "********";
709 this.PATH_typo3 = "";
710 this.decimalSign = ".";
711 }
712 var TS = new typoSetup();
713
714 // Info view:
715 function launchView(table,uid,bP) { //
716 var backPath= bP ? bP : "";
717 var thePreviewWindow = window.open(
718 backPath+' . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('show_item') . '&table=') . ' + encodeURIComponent(table) + "&uid=" + encodeURIComponent(uid),
719 "ShowItem" + TS.uniqueID,
720 "height=300,width=410,status=0,menubar=0,resizable=0,location=0,directories=0,scrollbars=1,toolbar=0"
721 );
722 if (thePreviewWindow && thePreviewWindow.focus) {
723 thePreviewWindow.focus();
724 }
725 }
726 function deleteRecord(table,id,url) { //
727 window.location.href = ' . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url)+"&vC=' . $beUser->veriCode() . '&prErr=1&uPT=1";
728 }
729 ';
730
731 $previewCode = isset($_POST['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '';
732
733 $this->doc->JScode = $this->doc->wrapScriptTags($javascript . $previewCode);
734 // Setting up the context sensitive menu:
735 $this->doc->getContextMenuCode();
736 $this->doc->bodyTagAdditions = 'onload="window.scrollTo(0,' . MathUtility::forceIntegerInRange(GeneralUtility::_GP('_scrollPosition'), 0, 10000) . ');"';
737
738 $this->emitFunctionAfterSignal(__FUNCTION__);
739 }
740
741 /**
742 * @return string
743 */
744 protected function generatePreviewCode() {
745 $currentPageId = MathUtility::convertToPositiveInteger($this->popViewId);
746 $table = $this->previewData['table'];
747 $recordId = $this->previewData['id'];
748
749 $pageTsConfig = BackendUtility::getPagesTSconfig($currentPageId);
750 $previewConfiguration = isset($pageTsConfig['TCEMAIN.']['preview.'][$table . '.'])
751 ? $pageTsConfig['TCEMAIN.']['preview.'][$table . '.']
752 : array();
753
754 $recordArray = BackendUtility::getRecord($table, $recordId);
755
756 // find the right preview page id
757 $previewPageId = 0;
758 if (isset($previewConfiguration['previewPageId'])) {
759 $previewPageId = $previewConfiguration['previewPageId'];
760 }
761 // if no preview page was configured
762 if (!$previewPageId) {
763 $rootPageData = NULL;
764 $rootLine = BackendUtility::BEgetRootLine($currentPageId);
765 $currentPage = reset($rootLine);
766 if ((int)$currentPage['doktype'] === PageRepository::DOKTYPE_DEFAULT) {
767 // try the current page
768 $previewPageId = $currentPageId;
769 } else {
770 // or search for the root page
771 foreach ($rootLine as $page) {
772 if ($page['is_siteroot']) {
773 $rootPageData = $page;
774 break;
775 }
776 }
777 $previewPageId = isset($rootPageData)
778 ? (int)$rootPageData['uid']
779 : $currentPageId;
780 }
781 }
782
783 $linkParameters = [
784 'no_cache' => 1,
785 ];
786
787 // language handling
788 $languageField = isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])
789 ? $GLOBALS['TCA'][$table]['ctrl']['languageField']
790 : '';
791 if ($languageField && !empty($recordArray[$languageField])) {
792 $l18nPointer = isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])
793 ? $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
794 : '';
795 if (
796 $l18nPointer && !empty($recordArray[$l18nPointer])
797 && isset($previewConfiguration['useDefaultLanguageRecord'])
798 && !$previewConfiguration['useDefaultLanguageRecord']
799 ) {
800 // use parent record
801 $recordId = $recordArray[$l18nPointer];
802 }
803 $linkParameters['L'] = $recordArray[$languageField];
804 }
805
806 // map record data to GET parameters
807 if (isset($previewConfiguration['fieldToParameterMap.'])) {
808 foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
809 $value = $recordArray[$field];
810 if ($field === 'uid') {
811 $value = $recordId;
812 }
813 $linkParameters[$parameterName] = $value;
814 }
815 }
816
817 // add/override parameters by configuration
818 if (isset($previewConfiguration['additionalGetParameters.'])) {
819 $linkParameters = array_replace($linkParameters, $previewConfiguration['additionalGetParameters.']);
820 }
821
822 $this->popViewId = $previewPageId;
823 $this->popViewId_addParams = GeneralUtility::implodeArrayForUrl('', $linkParameters, '', FALSE, TRUE);
824
825 $previewPageRootline = BackendUtility::BEgetRootLine($this->popViewId);
826 return '
827 if (window.opener) {
828 '
829 . BackendUtility::viewOnClick($this->popViewId, '', $previewPageRootline, '', $this->viewUrl, $this->popViewId_addParams, FALSE)
830 . '
831 } else {
832 '
833 . BackendUtility::viewOnClick($this->popViewId, '', $previewPageRootline, '', $this->viewUrl, $this->popViewId_addParams)
834 . '
835 }';
836 }
837
838 /**
839 * Main module operation
840 *
841 * @return void
842 */
843 public function main() {
844 $body = '';
845 // Begin edit:
846 if (is_array($this->editconf)) {
847 /** @var FormResultCompiler formResultCompiler */
848 $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
849
850 if ($this->editRegularContentFromId) {
851 $this->editRegularContentFromId();
852 }
853 // Creating the editing form, wrap it with buttons, document selector etc.
854 $editForm = $this->makeEditForm();
855 if ($editForm) {
856 $this->firstEl = reset($this->elementsData);
857 // Checking if the currently open document is stored in the list of "open documents" - if not, then add it:
858 if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5])) && !$this->dontStoreDocumentRef) {
859 $this->docHandler[$this->storeUrlMd5] = array($this->storeTitle, $this->storeArray, $this->storeUrl, $this->firstEl);
860 $this->getBackendUser()->pushModuleData('FormEngine', array($this->docHandler, $this->storeUrlMd5));
861 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
862 }
863 // Module configuration
864 $this->modTSconfig = $this->viewId ? BackendUtility::getModTSconfig($this->viewId, 'mod.xMOD_alt_doc') : array();
865 $body = $this->formResultCompiler->JStop();
866 $body .= $this->compileForm($editForm);
867 $body .= $this->formResultCompiler->printNeededJSFunctions();
868 }
869 }
870 // Access check...
871 // The page will show only if there is a valid page and if this page may be viewed by the user
872 $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
873 // Setting up the buttons and markers for docheader
874 $docHeaderButtons = $this->getButtons();
875 $markers = array(
876 'LANGSELECTOR' => $this->langSelector(),
877 'CSH' => $docHeaderButtons['csh'],
878 'CONTENT' => $body
879 );
880 // Build the <body> for the module
881 $this->content = $this->doc->startPage('TYPO3 Edit Document');
882 $this->content .= $this->doc->moduleBody($this->pageinfo, $docHeaderButtons, $markers);
883 $this->content .= $this->doc->endPage();
884 $this->content = $this->doc->insertStylesAndJS($this->content);
885 }
886
887 /**
888 * Outputting the accumulated content to screen
889 *
890 * @return void
891 * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
892 */
893 public function printContent() {
894 GeneralUtility::logDeprecatedFunction();
895 echo $this->content;
896 }
897
898 /***************************
899 *
900 * Sub-content functions, rendering specific parts of the module content.
901 *
902 ***************************/
903 /**
904 * Creates the editing form with FormEnigne, based on the input from GPvars.
905 *
906 * @return string HTML form elements wrapped in tables
907 */
908 public function makeEditForm() {
909 // Initialize variables:
910 $this->elementsData = array();
911 $this->errorC = 0;
912 $this->newC = 0;
913 $editForm = '';
914 $trData = NULL;
915 $beUser = $this->getBackendUser();
916 // Traverse the GPvar edit array
917 // Tables:
918 foreach ($this->editconf as $table => $conf) {
919 if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
920 // Traverse the keys/comments of each table (keys can be a commalist of uids)
921 foreach ($conf as $cKey => $command) {
922 if ($command == 'edit' || $command == 'new') {
923 // Get the ids:
924 $ids = GeneralUtility::trimExplode(',', $cKey, TRUE);
925 // Traverse the ids:
926 foreach ($ids as $theUid) {
927
928 // Don't save this document title in the document selector if the document is new.
929 if ($command === 'new') {
930 $this->dontStoreDocumentRef = 1;
931 }
932
933 /** @var TcaDatabaseRecord $formDataGroup */
934 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
935 /** @var FormDataCompiler $formDataCompiler */
936 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
937 /** @var NodeFactory $nodeFactory */
938 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
939
940 try {
941 // Reset viewId - it should hold data of last entry only
942 $this->viewId = 0;
943 $this->viewId_addParams = '';
944
945 $formDataCompilerInput = [
946 'tableName' => $table,
947 'vanillaUid' => (int)$theUid,
948 'command' => $command,
949 'returnUrl' => $this->R_URI,
950 ];
951 $formData = $formDataCompiler->compile($formDataCompilerInput);
952
953 // Set this->viewId if possible
954 if ($command === 'new' && $table !== 'pages' && !empty($formData['parentPageRow']['uid'])) {
955 $this->viewId = $formData['parentPageRow']['uid'];
956 } else {
957 if ($table == 'pages') {
958 $this->viewId = $formData['databaseRow']['uid'];
959 } elseif (!empty($formData['parentPageRow']['uid'])) {
960 $this->viewId = $formData['parentPageRow']['uid'];
961 // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
962 if (!empty($formData['vanillaTableTca']['ctrl']['languageField'])
963 && is_array($formData['databaseRow'][$formData['vanillaTableTca']['ctrl']['languageField']])
964 && $formData['databaseRow'][$formData['vanillaTableTca']['ctrl']['languageField']][0] > 0
965 ) {
966 $this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['vanillaTableTca']['ctrl']['languageField']][0];
967 }
968 }
969 }
970
971 // Determine if delete button can be shown
972 $deleteAccess = FALSE;
973 if ($command === 'edit') {
974 $permission = $formData['userPermissionOnPage'];
975 if ($formData['tableName'] === 'pages') {
976 $deleteAccess = $permission & Permission::PAGE_DELETE ? TRUE : FALSE;
977 } else {
978 $deleteAccess = $permission & Permission::CONTENT_EDIT ? TRUE : FALSE;
979 }
980 }
981
982 // Display "is-locked" message:
983 if ($command === 'edit') {
984 $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
985 if ($lockInfo) {
986 /** @var $flashMessage \TYPO3\CMS\Core\Messaging\FlashMessage */
987 $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, htmlspecialchars($lockInfo['msg']), '', FlashMessage::WARNING);
988 /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
989 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
990 /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
991 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
992 $defaultFlashMessageQueue->enqueue($flashMessage);
993 }
994 }
995
996 // Record title
997 if (!$this->storeTitle) {
998 $this->storeTitle = $this->recTitle
999 ? htmlspecialchars($this->recTitle)
1000 : BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), TRUE);
1001 }
1002
1003 $this->elementsData[] = array(
1004 'table' => $table,
1005 'uid' => $formData['databaseRow']['uid'],
1006 'pid' => $formData['databaseRow']['pid'],
1007 'cmd' => $command,
1008 'deleteAccess' => $deleteAccess
1009 );
1010
1011 // Set additional FormData
1012 // @todo: This is a hack and should be done differently
1013 if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1014 $formData['overrideValues'] = $this->overrideVals[$table];
1015 }
1016
1017 if ($command !== 'new') {
1018 BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1019 }
1020
1021 // Set list if only specific fields should be rendered. This will trigger
1022 // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1023 if ($this->columnsOnly) {
1024 if (is_array($this->columnsOnly)) {
1025 $formData['fieldListToRender'] = $this->columnsOnly[$table];
1026 } else {
1027 $formData['fieldListToRender'] = $this->columnsOnly;
1028 }
1029 }
1030
1031 $formData['renderType'] = 'outerWrapContainer';
1032 $formResult = $nodeFactory->create($formData)->render();
1033
1034 $html = $formResult['html'];
1035
1036 $formResult['html'] = '';
1037 $formResult['doSaveFieldName'] = 'doSave';
1038
1039 // @todo: Put all the stuff into FormEngine as final "compiler" class
1040 // @todo: This is done here for now to not rewrite JStop()
1041 // @todo: and printNeededJSFunctions() now
1042 $this->formResultCompiler->mergeResult($formResult);
1043
1044 // Seems the pid is set as hidden field (again) at end?!
1045 if ($command == 'new') {
1046 // @todo: looks ugly
1047 $html .= LF
1048 . '<input type="hidden"'
1049 . ' name="data[' . $table . '][' . $formData['databaseRow']['uid'] . '][pid]"'
1050 . ' value="' . $formData['databaseRow']['pid'] . '" />';
1051 $this->newC++;
1052 }
1053
1054 $editForm .= $html;
1055
1056 } catch (AccessDeniedException $e) {
1057 $this->errorC++;
1058 // Try to fetch error message from "recordInternals" be user object
1059 // @todo: This construct should be logged and localized and de-uglified
1060 $message = $beUser->errorMsg;
1061 if (empty($message)) {
1062 // Create message from exception.
1063 $message = $e->getMessage() . ' ' . $e->getCode();
1064 }
1065 $editForm .= $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noEditPermission', TRUE)
1066 . '<br /><br />' . htmlspecialchars($message) . '<br /><br />';
1067 }
1068 } // End of for each uid
1069 }
1070 }
1071 }
1072 }
1073 return $editForm;
1074 }
1075
1076 /**
1077 * Create the panel of buttons for submitting the form or otherwise perform operations.
1078 *
1079 * @return array All available buttons as an assoc. array
1080 */
1081 protected function getButtons() {
1082 $lang = $this->getLanguageService();
1083 $buttons = array(
1084 'save' => '',
1085 'save_view' => '',
1086 'save_new' => '',
1087 'save_close' => '',
1088 'close' => '',
1089 'delete' => '',
1090 'undo' => '',
1091 'history' => '',
1092 'columns_only' => '',
1093 'csh' => '',
1094 'translation_save' => '',
1095 'translation_saveclear' => ''
1096 );
1097 /** @var IconFactory $iconFactory */
1098 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1099 // Render SAVE type buttons:
1100 // The action of each button is decided by its name attribute. (See doProcessData())
1101 if (!$this->errorC && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']) {
1102 // SAVE button:
1103 $buttons['save'] = '<button name="_savedok" class="c-inputButton" value="1" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.saveDoc', TRUE) . '">'
1104 . $iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL)
1105 . '</button>';
1106 // SAVE / VIEW button:
1107 if ($this->viewId && !$this->noView && $this->getNewIconMode($this->firstEl['table'], 'saveDocView')) {
1108 $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1109 if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1110 $excludeDokTypes = GeneralUtility::intExplode(',', $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'], TRUE);
1111 } else {
1112 // exclude sysfolders, spacers and recycler by default
1113 $excludeDokTypes = array(PageRepository::DOKTYPE_RECYCLER, PageRepository::DOKTYPE_SYSFOLDER, PageRepository::DOKTYPE_SPACER);
1114 }
1115 if (!in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, TRUE) || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'].'.']['previewPageId'])) {
1116 $buttons['save_view'] = '<button name="_savedokview" class="c-inputButton t3js-editform-submitButton" value="1" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.saveDocShow', TRUE) . '" onclick="window.open(\'\', \'newTYPO3frontendWindow\');">'
1117 . $iconFactory->getIcon('actions-document-save-view', Icon::SIZE_SMALL)
1118 . '</button>';
1119 }
1120 }
1121 // SAVE / NEW button:
1122 if (count($this->elementsData) === 1 && $this->getNewIconMode($this->firstEl['table'])) {
1123 $buttons['save_new'] = '<button name="_savedoknew" class="c-inputButton t3js-editform-submitButton" value="1" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.saveNewDoc', TRUE) . '">'
1124 . $iconFactory->getIcon('actions-document-save-new', Icon::SIZE_SMALL)
1125 . '</button>';
1126 }
1127 // SAVE / CLOSE
1128 $buttons['save_close'] = '<button name="_saveandclosedok" class="c-inputButton t3js-editform-submitButton" value="1" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.saveCloseDoc', TRUE) . '">'
1129 . $iconFactory->getIcon('actions-document-save-close', Icon::SIZE_SMALL)
1130 . '</button>';
1131 // FINISH TRANSLATION / SAVE / CLOSE
1132 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['explicitConfirmationOfTranslation']) {
1133 $buttons['translation_save'] = '<button name="_translation_savedok" class="c-inputButton" value="1" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.translationSaveDoc', TRUE) . '">'
1134 . $iconFactory->getIcon('actions-document-save-translation', Icon::SIZE_SMALL)
1135 . '</button>';
1136 $buttons['translation_saveclear'] = '<button name="_translation_savedokclear" class="c-inputButton" value="1" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.translationSaveDocClear', TRUE) . '">'
1137 . $iconFactory->getIcon('actions-document-save-cleartranslationcache', Icon::SIZE_SMALL)
1138 . '</button>';
1139 }
1140 }
1141 // CLOSE button:
1142 $buttons['close'] = '<a href="#" class="t3js-editform-close" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:rm.closeDoc', TRUE) . '">' . $this->iconFactory->getIcon('actions-document-close', Icon::SIZE_SMALL) . '</a>';
1143 // DELETE + UNDO buttons:
1144 if (!$this->errorC && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly'] && count($this->elementsData) === 1) {
1145 if ($this->firstEl['cmd'] != 'new' && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])) {
1146 // Delete:
1147 if ($this->firstEl['deleteAccess'] && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly'] && !$this->getNewIconMode($this->firstEl['table'], 'disableDelete')) {
1148 $buttons['delete'] = '<a href="#" class="t3js-editform-delete-record" data-return-url="' . htmlspecialchars($this->retUrl) . '" data-uid="' . htmlspecialchars($this->firstEl['uid']) . '" data-table="' . htmlspecialchars($this->firstEl['table']) . '" title="' . $lang->getLL('deleteItem', TRUE) . '">' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL) . '</a>';
1149 }
1150 // Undo:
1151 $undoRes = $this->getDatabaseConnection()->exec_SELECTquery('tstamp', 'sys_history', 'tablename=' . $this->getDatabaseConnection()->fullQuoteStr($this->firstEl['table'], 'sys_history') . ' AND recuid=' . (int)$this->firstEl['uid'], '', 'tstamp DESC', '1');
1152 if ($undoButtonR = $this->getDatabaseConnection()->sql_fetch_assoc($undoRes)) {
1153 $aOnClick = 'window.location.href=' .
1154 GeneralUtility::quoteJSvalue(
1155 BackendUtility::getModuleUrl(
1156 'record_history',
1157 array(
1158 'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1159 'revert' => 'ALL_FIELDS',
1160 'sumUp' => -1,
1161 'returnUrl' => $this->R_URI,
1162 )
1163 )
1164 ) . '; return false;';
1165 $buttons['undo'] = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '"' . ' title="' . htmlspecialchars(sprintf($lang->getLL('undoLastChange'), BackendUtility::calcAge(($GLOBALS['EXEC_TIME'] - $undoButtonR['tstamp']), $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')))) . '">' . $this->iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL) . '</a>';
1166 }
1167 if ($this->getNewIconMode($this->firstEl['table'], 'showHistory')) {
1168 $aOnClick = 'window.location.href=' .
1169 GeneralUtility::quoteJSvalue(
1170 BackendUtility::getModuleUrl(
1171 'record_history',
1172 array(
1173 'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1174 'returnUrl' => $this->R_URI,
1175 )
1176 )
1177 ) . '; return false;';
1178 $buttons['history'] = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL) . '</a>';
1179 }
1180 // If only SOME fields are shown in the form, this will link the user to the FULL form:
1181 if ($this->columnsOnly) {
1182 $buttons['columns_only'] = '<a href="' . htmlspecialchars(($this->R_URI . '&columnsOnly=')) . '" title="' . $lang->getLL('editWholeRecord', TRUE) . '">' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL) . '</a>';
1183 }
1184 }
1185 }
1186 // add the CSH icon
1187 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'TCEforms');
1188 $buttons['shortcut'] = $this->shortCutLink();
1189 $buttons['open_in_new_window'] = $this->openInNewWindowLink();
1190
1191 return $buttons;
1192 }
1193
1194 /**
1195 * Returns the language switch/selector for editing,
1196 * show only when a single record is edited
1197 * - multiple records are too confusing
1198 *
1199 * @return string The HTML
1200 */
1201 public function langSelector() {
1202 $langSelector = '';
1203 if (count($this->elementsData) === 1) {
1204 $langSelector = $this->languageSwitch($this->firstEl['table'], $this->firstEl['uid'], $this->firstEl['pid']);
1205 }
1206 return $langSelector;
1207 }
1208
1209 /**
1210 * Put together the various elements (buttons, selectors, form) into a table
1211 *
1212 * @param string $editForm HTML form.
1213 * @return string Composite HTML
1214 */
1215 public function compileForm($editForm) {
1216 $formContent = '
1217 <!-- EDITING FORM -->
1218 ' . $editForm . '
1219
1220 <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
1221 <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />';
1222 if ($this->returnNewPageId) {
1223 $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1224 }
1225 $formContent .= '<input type="hidden" name="popViewId" value="' . htmlspecialchars($this->viewId) . '" />';
1226 if ($this->viewId_addParams) {
1227 $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
1228 }
1229 $formContent .= '
1230 <input type="hidden" name="closeDoc" value="0" />
1231 <input type="hidden" name="doSave" value="0" />
1232 <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
1233 <input type="hidden" name="_scrollPosition" value="" />';
1234 return $formContent;
1235 }
1236
1237 /**
1238 * Create shortcut icon
1239 *
1240 * @return string
1241 */
1242 public function shortCutLink() {
1243 if ($this->returnUrl === 'sysext/backend/Resources/Private/Templates/Close.html' || !$this->getBackendUser()->mayMakeShortcut()) {
1244 return '';
1245 }
1246 return $this->doc->makeShortcutIcon('returnUrl,edit,defVals,overrideVals,columnsOnly,returnNewPageId,editRegularContentFromId,noView', '', $this->MCONF['name'], 1);
1247 }
1248
1249 /**
1250 * Creates open-in-window link
1251 *
1252 * @return string
1253 */
1254 public function openInNewWindowLink() {
1255 if ($this->returnUrl === 'sysext/backend/Resources/Private/Templates/Close.html') {
1256 return '';
1257 }
1258 $aOnClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue(GeneralUtility::linkThisScript(array('returnUrl' => 'sysext/backend/Resources/Private/Templates/Close.html'))) . ',' . GeneralUtility::quoteJSvalue(md5($this->R_URI)) . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1259 return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '" title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.openInNewWindow', TRUE) . '">' . $this->iconFactory->getIcon('actions-window-open', Icon::SIZE_SMALL)->render() . '</a>';
1260 }
1261
1262 /***************************
1263 *
1264 * Localization stuff
1265 *
1266 ***************************/
1267 /**
1268 * Make selector box for creating new translation for a record or switching to edit the record in an existing language.
1269 * Displays only languages which are available for the current page.
1270 *
1271 * @param string $table Table name
1272 * @param int $uid Uid for which to create a new language
1273 * @param int $pid Pid of the record
1274 * @return string <select> HTML element (if there were items for the box anyways...)
1275 */
1276 public function languageSwitch($table, $uid, $pid = NULL) {
1277 $content = '';
1278 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1279 $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1280 // Table editable and activated for languages?
1281 if ($this->getBackendUser()->check('tables_modify', $table) && $languageField && $transOrigPointerField && !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']) {
1282 if (is_null($pid)) {
1283 $row = BackendUtility::getRecord($table, $uid, 'pid');
1284 $pid = $row['pid'];
1285 }
1286 // Get all avalibale languages for the page
1287 $langRows = $this->getLanguages($pid);
1288 // Page available in other languages than default language?
1289 if (is_array($langRows) && count($langRows) > 1) {
1290 $rowsByLang = array();
1291 $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
1292 // Get record in current language
1293 $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
1294 if (!is_array($rowCurrent)) {
1295 $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
1296 }
1297 $currentLanguage = $rowCurrent[$languageField];
1298 // Disabled for records with [all] language!
1299 if ($currentLanguage > -1) {
1300 // Get record in default language if needed
1301 if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
1302 $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord($table, $rowCurrent[$transOrigPointerField], $fetchFields);
1303 if (!is_array($rowsByLang[0])) {
1304 $rowsByLang[0] = BackendUtility::getRecord($table, $rowCurrent[$transOrigPointerField], $fetchFields);
1305 }
1306 } else {
1307 $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
1308 }
1309 if ($rowCurrent[$transOrigPointerField] || $currentLanguage === '0') {
1310 // Get record in other languages to see what's already available
1311 $translations = $this->getDatabaseConnection()->exec_SELECTgetRows($fetchFields, $table, 'pid=' . (int)$pid . ' AND ' . $languageField . '>0' . ' AND ' . $transOrigPointerField . '=' . (int)$rowsByLang[0]['uid'] . BackendUtility::deleteClause($table) . BackendUtility::versioningPlaceholderClause($table));
1312 foreach ($translations as $row) {
1313 $rowsByLang[$row[$languageField]] = $row;
1314 }
1315 }
1316 $langSelItems = array();
1317 foreach ($langRows as $lang) {
1318 if ($this->getBackendUser()->checkLanguageAccess($lang['uid'])) {
1319 $newTranslation = isset($rowsByLang[$lang['uid']]) ? '' : ' [' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.new', TRUE) . ']';
1320 // Create url for creating a localized record
1321 if ($newTranslation) {
1322 $redirectUrl = BackendUtility::getModuleUrl('record_edit', array(
1323 'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $lang['uid'],
1324 'returnUrl' => $this->retUrl
1325 ));
1326 $href = $this->doc->issueCommand('&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $lang['uid'], $redirectUrl);
1327 } else {
1328 $href = BackendUtility::getModuleUrl('record_edit', array(
1329 'edit[' . $table . '][' . $rowsByLang[$lang['uid']]['uid'] . ']' => 'edit',
1330 'returnUrl' => $this->retUrl
1331 ));
1332 }
1333 $langSelItems[$lang['uid']] = '
1334 <option value="' . htmlspecialchars($href) . '"' . ($currentLanguage == $lang['uid'] ? ' selected="selected"' : '') . '>' . htmlspecialchars(($lang['title'] . $newTranslation)) . '</option>';
1335 }
1336 }
1337 // If any languages are left, make selector:
1338 if (count($langSelItems) > 1) {
1339 $onChange = 'if(this.options[this.selectedIndex].value){window.location.href=(this.options[this.selectedIndex].value);}';
1340 $content = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_general.xlf:LGL.language', TRUE) . ' <select name="_langSelector" onchange="' . htmlspecialchars($onChange) . '">
1341 ' . implode('', $langSelItems) . '
1342 </select>';
1343 }
1344 }
1345 }
1346 }
1347 return $content;
1348 }
1349
1350 /**
1351 * Redirects to FormEngine with new parameters to edit a just created localized record
1352 *
1353 * @param string $justLocalized String passed by GET &justLocalized=
1354 * @return void
1355 */
1356 public function localizationRedirect($justLocalized) {
1357 list($table, $orig_uid, $language) = explode(':', $justLocalized);
1358 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
1359 $localizedRecord = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('uid', $table, $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '=' . (int)$language . ' AND ' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] . '=' . (int)$orig_uid . BackendUtility::deleteClause($table) . BackendUtility::versioningPlaceholderClause($table));
1360 if (is_array($localizedRecord)) {
1361 // Create parameters and finally run the classic page module for creating a new page translation
1362 $location = BackendUtility::getModuleUrl('record_edit', array(
1363 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
1364 'returnUrl' => GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'))
1365 ));
1366 HttpUtility::redirect($location);
1367 }
1368 }
1369 }
1370
1371 /**
1372 * Returns sys_language records available for record translations on given page.
1373 *
1374 * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT hidden. If set to another value, the query will select all sys_language records that has a pages_language_overlay record on that page (and is not hidden, unless you are admin user)
1375 * @return array Language records including faked record for default language
1376 */
1377 public function getLanguages($id) {
1378 $modSharedTSconfig = BackendUtility::getModTSconfig($id, 'mod.SHARED');
1379 // Fallback non sprite-configuration
1380 if (preg_match('/\\.gif$/', $modSharedTSconfig['properties']['defaultLanguageFlag'])) {
1381 $modSharedTSconfig['properties']['defaultLanguageFlag'] = str_replace('.gif', '', $modSharedTSconfig['properties']['defaultLanguageFlag']);
1382 }
1383 $languages = array(
1384 0 => array(
1385 'uid' => 0,
1386 'pid' => 0,
1387 'hidden' => 0,
1388 'title' => $modSharedTSconfig['properties']['defaultLanguageLabel'] !== ''
1389 ? $modSharedTSconfig['properties']['defaultLanguageLabel'] . ' (' . $this->getLanguageService()->sl('LLL:EXT:lang/locallang_mod_web_list.xlf:defaultLanguage') . ')'
1390 : $this->getLanguageService()->sl('LLL:EXT:lang/locallang_mod_web_list.xlf:defaultLanguage'),
1391 'flag' => $modSharedTSconfig['properties']['defaultLanguageFlag']
1392 )
1393 );
1394 $exQ = $this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0';
1395 if ($id) {
1396 $rows = $this->getDatabaseConnection()->exec_SELECTgetRows('sys_language.*', 'pages_language_overlay,sys_language', 'pages_language_overlay.sys_language_uid=sys_language.uid AND pages_language_overlay.pid=' . (int)$id . BackendUtility::deleteClause('pages_language_overlay') . $exQ, 'pages_language_overlay.sys_language_uid,sys_language.uid,sys_language.pid,sys_language.tstamp,sys_language.hidden,sys_language.title,sys_language.language_isocode,sys_language.static_lang_isocode,sys_language.flag', 'sys_language.title');
1397 } else {
1398 $rows = $this->getDatabaseConnection()->exec_SELECTgetRows('sys_language.*', 'sys_language', 'sys_language.hidden=0', '', 'sys_language.title');
1399 }
1400 if ($rows) {
1401 foreach ($rows as $row) {
1402 $languages[$row['uid']] = $row;
1403 }
1404 }
1405 return $languages;
1406 }
1407
1408 /***************************
1409 *
1410 * Other functions
1411 *
1412 ***************************/
1413 /**
1414 * Fix $this->editconf if versioning applies to any of the records
1415 *
1416 * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
1417 * @return void
1418 */
1419 public function fixWSversioningInEditConf($mapArray = FALSE) {
1420 // Traverse the editConf array
1421 if (is_array($this->editconf)) {
1422 // Tables:
1423 foreach ($this->editconf as $table => $conf) {
1424 if (is_array($conf) && $GLOBALS['TCA'][$table]) {
1425 // Traverse the keys/comments of each table (keys can be a commalist of uids)
1426 $newConf = array();
1427 foreach ($conf as $cKey => $cmd) {
1428 if ($cmd == 'edit') {
1429 // Traverse the ids:
1430 $ids = GeneralUtility::trimExplode(',', $cKey, TRUE);
1431 foreach ($ids as $idKey => $theUid) {
1432 if (is_array($mapArray)) {
1433 if ($mapArray[$table][$theUid]) {
1434 $ids[$idKey] = $mapArray[$table][$theUid];
1435 }
1436 } else {
1437 // Default, look for versions in workspace for record:
1438 $calcPRec = $this->getRecordForEdit($table, $theUid);
1439 if (is_array($calcPRec)) {
1440 // Setting UID again if it had changed, eg. due to workspace versioning.
1441 $ids[$idKey] = $calcPRec['uid'];
1442 }
1443 }
1444 }
1445 // Add the possibly manipulated IDs to the new-build newConf array:
1446 $newConf[implode(',', $ids)] = $cmd;
1447 } else {
1448 $newConf[$cKey] = $cmd;
1449 }
1450 }
1451 // Store the new conf array:
1452 $this->editconf[$table] = $newConf;
1453 }
1454 }
1455 }
1456 }
1457
1458 /**
1459 * Get record for editing.
1460 *
1461 * @param string $table Table name
1462 * @param int $theUid Record UID
1463 * @return array Returns record to edit, FALSE if none
1464 */
1465 public function getRecordForEdit($table, $theUid) {
1466 // Fetch requested record:
1467 $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid');
1468 if (is_array($reqRecord)) {
1469 // If workspace is OFFLINE:
1470 if ($this->getBackendUser()->workspace != 0) {
1471 // Check for versioning support of the table:
1472 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1473 // If the record is already a version of "something" pass it by.
1474 if ($reqRecord['pid'] == -1) {
1475 // (If it turns out not to be a version of the current workspace there will be trouble, but that is handled inside TCEmain then and in the interface it would clearly be an error of links if the user accesses such a scenario)
1476 return $reqRecord;
1477 } else {
1478 // The input record was online and an offline version must be found or made:
1479 // Look for version of this workspace:
1480 $versionRec = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $reqRecord['uid'], 'uid,pid,t3ver_oid');
1481 return is_array($versionRec) ? $versionRec : $reqRecord;
1482 }
1483 } else {
1484 // This means that editing cannot occur on this record because it was not supporting versioning which is required inside an offline workspace.
1485 return FALSE;
1486 }
1487 } else {
1488 // In ONLINE workspace, just return the originally requested record:
1489 return $reqRecord;
1490 }
1491 } else {
1492 // Return FALSE because the table/uid was not found anyway.
1493 return FALSE;
1494 }
1495 }
1496
1497 /**
1498 * Function, which populates the internal editconf array with editing commands for all tt_content elements from the normal column in normal language from the page pointed to by $this->editRegularContentFromId
1499 *
1500 * @return void
1501 * @deprecated since TYPO3 CMS 7, will be removed with TYPO3 CMS 8
1502 */
1503 public function editRegularContentFromId() {
1504 GeneralUtility::logDeprecatedFunction();
1505 $dbConnection = $this->getDatabaseConnection();
1506 $res = $dbConnection->exec_SELECTquery('uid', 'tt_content', 'pid=' . (int)$this->editRegularContentFromId . BackendUtility::deleteClause('tt_content') . BackendUtility::versioningPlaceholderClause('tt_content') . ' AND colPos=0 AND sys_language_uid=0', '', 'sorting');
1507 if ($dbConnection->sql_num_rows($res)) {
1508 $ecUids = array();
1509 while ($ecRec = $dbConnection->sql_fetch_assoc($res)) {
1510 $ecUids[] = $ecRec['uid'];
1511 }
1512 $this->editconf['tt_content'][implode(',', $ecUids)] = 'edit';
1513 }
1514 $dbConnection->sql_free_result($res);
1515 }
1516
1517 /**
1518 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
1519 *
1520 * @return void
1521 * @see makeDocSel()
1522 */
1523 public function compileStoreDat() {
1524 $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray('edit,defVals,overrideVals,columnsOnly,noView,editRegularContentFromId,workspace', $this->R_URL_getvars);
1525 $this->storeUrl = GeneralUtility::implodeArrayForUrl('', $this->storeArray);
1526 $this->storeUrlMd5 = md5($this->storeUrl);
1527 }
1528
1529 /**
1530 * Function used to look for configuration of buttons in the form: Fx. disabling buttons or showing them at various positions.
1531 *
1532 * @param string $table The table for which the configuration may be specific
1533 * @param string $key The option for look for. Default is checking if the saveDocNew button should be displayed.
1534 * @return string Return value fetched from USER TSconfig
1535 */
1536 public function getNewIconMode($table, $key = 'saveDocNew') {
1537 $TSconfig = $this->getBackendUser()->getTSConfig('options.' . $key);
1538 $output = trim(isset($TSconfig['properties'][$table]) ? $TSconfig['properties'][$table] : $TSconfig['value']);
1539 return $output;
1540 }
1541
1542 /**
1543 * Handling the closing of a document
1544 *
1545 * @param int $code Close code: 0/1 will redirect to $this->retUrl, 3 will clear the docHandler (thus closing all documents) and other values will call setDocument with ->retUrl
1546 * @return void
1547 */
1548 public function closeDocument($code = 0) {
1549 // If current document is found in docHandler,
1550 // then unset it, possibly unset it ALL and finally, write it to the session data
1551 if (isset($this->docHandler[$this->storeUrlMd5])) {
1552 // add the closing document to the recent documents
1553 $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
1554 if (!is_array($recentDocs)) {
1555 $recentDocs = array();
1556 }
1557 $closedDoc = $this->docHandler[$this->storeUrlMd5];
1558 $recentDocs = array_merge(array($this->storeUrlMd5 => $closedDoc), $recentDocs);
1559 if (count($recentDocs) > 8) {
1560 $recentDocs = array_slice($recentDocs, 0, 8);
1561 }
1562 // remove it from the list of the open documents
1563 unset($this->docHandler[$this->storeUrlMd5]);
1564 if ($code == '3') {
1565 $recentDocs = array_merge($this->docHandler, $recentDocs);
1566 $this->docHandler = array();
1567 }
1568 $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
1569 $this->getBackendUser()->pushModuleData('FormEngine', array($this->docHandler, $this->docDat[1]));
1570 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
1571 }
1572 // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: (used by other scripts, like wizard_add, to know which records was created or so...)
1573 if ($this->returnEditConf && $this->retUrl != BackendUtility::getModuleUrl('dummy')) {
1574 $this->retUrl .= '&returnEditConf=' . rawurlencode(json_encode($this->editconf));
1575 }
1576 // If code is NOT set OR set to 1, then make a header location redirect to $this->retUrl
1577 if (!$code || $code == 1) {
1578 HttpUtility::redirect($this->retUrl);
1579 } else {
1580 $this->setDocument('', $this->retUrl);
1581 }
1582 }
1583
1584 /**
1585 * Redirects to the document pointed to by $currentDocFromHandlerMD5 OR $retUrl (depending on some internal calculations).
1586 * Most likely you will get a header-location redirect from this function.
1587 *
1588 * @param string $currentDocFromHandlerMD5 Pointer to the document in the docHandler array
1589 * @param string $retUrl Alternative/Default retUrl
1590 * @return void
1591 */
1592 public function setDocument($currentDocFromHandlerMD5 = '', $retUrl = '') {
1593 if ($retUrl === '') {
1594 return;
1595 }
1596 if (!$this->modTSconfig['properties']['disableDocSelector'] && is_array($this->docHandler) && !empty($this->docHandler)) {
1597 if (isset($this->docHandler[$currentDocFromHandlerMD5])) {
1598 $setupArr = $this->docHandler[$currentDocFromHandlerMD5];
1599 } else {
1600 $setupArr = reset($this->docHandler);
1601 }
1602 if ($setupArr[2]) {
1603 $sParts = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
1604 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
1605 }
1606 }
1607 HttpUtility::redirect($retUrl);
1608 }
1609
1610 /**
1611 * Injects the request object for the current request or subrequest
1612 *
1613 * @param ServerRequestInterface $request
1614 * @return \Psr\Http\Message\ResponseInterface $response
1615 */
1616 public function processRequest(ServerRequestInterface $request) {
1617
1618 BackendUtility::lockRecords();
1619
1620 // Preprocessing, storing data if submitted to
1621 $this->preInit();
1622
1623 // Checks, if a save button has been clicked (or the doSave variable is sent)
1624 if ($this->doProcessData()) {
1625 $this->processData();
1626 }
1627
1628 $this->init();
1629 $this->main();
1630
1631 /** @var Response $response */
1632 $response = GeneralUtility::makeInstance(Response::class);
1633 $response->getBody()->write($this->content);
1634 return $response;
1635 }
1636
1637 /**
1638 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1639 */
1640 protected function getBackendUser() {
1641 return $GLOBALS['BE_USER'];
1642 }
1643
1644 /**
1645 * Returns LanguageService
1646 *
1647 * @return \TYPO3\CMS\Lang\LanguageService
1648 */
1649 protected function getLanguageService() {
1650 return $GLOBALS['LANG'];
1651 }
1652
1653 /**
1654 * Returns the database connection
1655 *
1656 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
1657 */
1658 protected function getDatabaseConnection() {
1659 return $GLOBALS['TYPO3_DB'];
1660 }
1661
1662 }