Completed
Push — master ( 64542d...27c7de )
by
unknown
12:47
created

DataHandler   F

Complexity

Total Complexity 1531

Size/Duplication

Total Lines 9025
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 3806
dl 0
loc 9025
rs 0.8
c 4
b 0
f 0
wmc 1531

190 Methods

Rating   Name   Duplication   Size   Complexity  
F checkValue_input_Eval() 0 145 45
A checkValue_input_ValidateEmail() 0 18 2
B checkValue_group_select_processDBdata() 0 43 8
A setControl() 0 3 1
B copyPages() 0 29 7
A getAllowedTablesToCopyWhenCopyingAPage() 0 20 6
A normalizeTimeFormat() 0 13 2
A resolveSortingAndPidForNewRecord() 0 27 4
A applyDefaultsForFieldArray() 0 15 4
B start() 0 34 7
A hook_processDatamap_afterDatabaseOperations() 0 14 4
A getCheckModifyAccessListHookObjects() 0 13 4
B setMirror() 0 19 7
F process_datamap() 0 352 73
B setDefaultsFromUserTS() 0 16 7
A __construct() 0 12 2
A checkModifyAccessList() 0 9 5
B moveRecord_procBasedOnFieldType() 0 25 10
A postProcessSqlServerInsert() 0 11 4
B deleteL10nOverlayRecords() 0 33 7
A castReferenceValue() 0 15 5
A getRuntimeCache() 0 3 1
A getInlineFieldType() 0 15 5
A addDefaultPermittedLanguageIfNotSet() 0 20 5
D checkValueForCheck() 0 58 13
A checkValue_inline() 0 4 1
B insertNewCopyVersion() 0 35 7
A deleteClause() 0 7 3
D process_cmdmap() 0 137 37
A getRecordProperties() 0 7 4
A createRelationHandlerInstance() 0 9 1
A resolveVersionedRecords() 0 27 3
B fixCopyAfterDuplFields() 0 15 9
F remapListedDBRecords_procDBRefs() 0 56 16
F checkValueForInput() 0 78 29
A isInWebMount() 0 6 2
A newlog() 0 3 1
A controlActiveElements() 0 6 2
C checkStoredRecord() 0 58 17
A resolveFieldConfigurationAndRespectColumnsOverrides() 0 9 2
B checkValueForText() 0 31 8
A checkValue_inline_processDBdata() 0 26 3
A checkValue_checkMax() 0 9 2
A addNewValuesToRemapStackChildIds() 0 5 3
A addRemapAction() 0 9 1
A checkValue_flexArray2Xml() 0 5 1
A getRecordPropertiesFromRow() 0 14 4
B copyL10nOverlayRecords() 0 65 10
C canDeletePage() 0 48 12
F checkValue() 0 85 22
A resolvePid() 0 16 2
C copyRecord_processInline() 0 76 15
B destNotInsideSelf() 0 28 6
A resetElementsToBeDeleted() 0 3 1
A undeleteRecord() 0 4 2
A overlayAutoVersionId() 0 7 2
C versionizeRecord() 0 69 12
B doesPageHaveUnallowedTables() 0 36 6
A getCorrelationId() 0 3 1
A registerReferenceIndexRowsForDrop() 0 3 1
B checkForRecordsFromDisallowedTables() 0 28 8
A doesRecordExist() 0 3 1
A setHistory() 0 8 2
B checkValueForInline() 0 28 7
A checkValue_group_select_explodeSelectGroupValue() 0 9 2
A registerRecordIdForPageCacheClearing() 0 11 4
F copyRecord() 0 117 33
A tableAdminOnly() 0 4 1
A newFieldArray() 0 13 5
A isReferenceField() 0 3 4
A isRecordCopied() 0 10 4
B copy_remapTranslationSourceField() 0 32 9
A getDefaultLanguagePageId() 0 9 2
A checkValueForInternalReferences() 0 31 5
A isRecordInWebMount() 0 7 2
A tableReadOnly() 0 4 1
A deleteAction() 0 15 5
A getUniqueCountStatement() 0 47 5
B moveL10nOverlayRecords() 0 45 8
A recordInfo() 0 15 4
B fixUniqueInPid() 0 22 8
A version_remapMMForVersionSwap_flexFormCallBack() 0 12 5
A remapListedDBRecords_flexFormCallBack() 0 13 4
A copyRecord_flexFormCallBack() 0 11 4
A setCorrelationId() 0 3 1
A checkValueForSlug() 0 31 6
C remapListedDBRecords() 0 53 14
A getLocalTCE() 0 13 1
A resetNestedElementCalls() 0 3 1
B discardSubPagesAndRecordsOnPage() 0 52 10
A disableDeleteClause() 0 3 1
C moveRecord() 0 65 13
B processClearCacheQueue() 0 37 7
A clearPrefixFromValue() 0 4 1
A addDeleteRestriction() 0 4 2
C checkRecordInsertAccess() 0 37 13
A hardDeleteSingleRecord() 0 5 1
A moveRecord_procFields() 0 7 4
A triggerRemapAction() 0 7 4
A getCommandMapElements() 0 13 6
A discardLocalizationOverlayRecords() 0 29 4
A getHistoryRecords() 0 3 1
F fillInFieldArray() 0 129 44
D prepareCacheFlush() 0 127 16
C recordInfoWithPermissionCheck() 0 50 15
B _ACTION_FLEX_FORMdata() 0 26 10
A registerNestedElementCall() 0 5 1
A getCacheManager() 0 3 1
A isRecordUndeletable() 0 17 3
A getFieldEvalCacheIdentifier() 0 3 1
F localize() 0 146 35
A getOriginalParentOfRecord() 0 7 2
A getTableEntries() 0 6 3
A convNumEntityToByteValue() 0 14 4
A prependLabel() 0 3 1
A postProcessDatabaseInsert() 0 15 4
A hasDeletedRecord() 0 5 2
B placeholderShadowing() 0 40 9
A getOuterMostInstance() 0 12 5
D getSortNumber() 0 143 21
D processRemapStack() 0 138 30
A getResourceFactory() 0 3 1
B checkValueForRadio() 0 32 8
B int_pageTreeInfo() 0 55 10
A log() 0 17 5
A copyRecord_procBasedOnFieldType() 0 30 6
B fixUniqueInSite() 0 24 7
B getUnique() 0 35 9
A getAutoVersionId() 0 7 2
A deleteRecord_procFields() 0 9 3
A deletePages() 0 29 6
B isTableAllowedForThisPage() 0 25 9
A isNestedElementCallRegistered() 0 4 1
C deleteRecord_procBasedOnFieldType() 0 36 13
A setNullValues() 0 13 4
A eventPid() 0 3 2
A overrideFieldArray() 0 6 2
B getExcludeListArray() 0 20 8
C checkValue_SW() 0 47 17
A getRecordsWithSameValue() 0 38 3
A isElementToBeDeleted() 0 4 1
A isOuterMostInstance() 0 3 1
C remapListedDBRecords_procInline() 0 55 16
A deleteEl() 0 7 2
A registerElementsToBeDeleted() 0 4 1
A getPID() 0 12 2
C discardRecordRelations() 0 32 13
A getPlaceholderTitleForTableLabel() 0 14 3
A deleteVersionsForRecord() 0 10 6
A fixUniqueInSiteForSubpages() 0 8 3
A getLanguageService() 0 3 1
B getVersionizedIncomingFieldArray() 0 20 9
F moveRecord_raw() 0 154 30
A unsetElementsToBeDeleted() 0 9 3
A increaseSortingOfFollowingRecords() 0 26 5
A insertUpdateDB_preprocessBasedOnFieldType() 0 11 5
A isSubmittedValueEqualToStoredValue() 0 20 6
F deleteRecord() 0 109 28
F discard() 0 109 25
F inlineLocalizeSynchronize() 0 137 38
B checkValue_text_Eval() 0 29 9
C compareFieldArrayWithCurrentAndUnset() 0 48 12
A postProcessPostgresqlInsert() 0 30 2
B deleteSpecificPage() 0 73 10
A printLogErrorMessages() 0 31 3
B updateFlexFormData() 0 35 9
B applyFiltersToValues() 0 18 7
B doesRecordExist_pageLookUp() 0 39 9
F insertDB() 0 86 20
D clear_cacheCmd() 0 61 16
A getRecordHistoryStore() 0 9 1
F copySpecificPage() 0 118 30
B getPreviousLocalizedRecordUid() 0 55 7
B doesBranchExist() 0 29 6
A pageInfo() 0 16 3
A cannotDeleteRecord() 0 13 6
C updateDB() 0 56 14
B checkValue_flex_procInData() 0 21 7
B copyRecord_raw() 0 60 11
A compileAdminTables() 0 3 1
A dbAnalysisStoreExec() 0 6 4
B version_remapMMForVersionSwap_execSwap() 0 15 7
D checkValue_flex_procInData_travDS() 0 92 30
C checkRecordUpdateAccess() 0 37 12
B checkValueForFlex() 0 72 9
F copyRecord_processManyToMany() 0 35 17
A getCopyHeader() 0 26 5
A updateRefIndex() 0 6 2
F checkValueForGroupSelect() 0 69 21
B version_remapMMForVersionSwap() 0 55 11

How to fix   Complexity   

Complex Class

Complex classes like DataHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\DataHandling;
17
18
use Doctrine\DBAL\DBALException;
19
use Doctrine\DBAL\Driver\Statement;
20
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
21
use Doctrine\DBAL\Platforms\SQLServerPlatform;
22
use Doctrine\DBAL\Types\IntegerType;
23
use Psr\Log\LoggerAwareInterface;
24
use Psr\Log\LoggerAwareTrait;
25
use TYPO3\CMS\Backend\Utility\BackendUtility;
26
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27
use TYPO3\CMS\Core\Cache\CacheManager;
28
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
29
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
30
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowException;
31
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowLoopException;
32
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowRootException;
33
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueException;
34
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
35
use TYPO3\CMS\Core\Configuration\Richtext;
36
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
37
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
38
use TYPO3\CMS\Core\Database\Connection;
39
use TYPO3\CMS\Core\Database\ConnectionPool;
40
use TYPO3\CMS\Core\Database\Query\QueryHelper;
41
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
42
use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
43
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
44
use TYPO3\CMS\Core\Database\RelationHandler;
45
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
46
use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor;
47
use TYPO3\CMS\Core\DataHandling\Model\CorrelationId;
48
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
49
use TYPO3\CMS\Core\Html\RteHtmlParser;
50
use TYPO3\CMS\Core\Localization\LanguageService;
51
use TYPO3\CMS\Core\Messaging\FlashMessage;
52
use TYPO3\CMS\Core\Messaging\FlashMessageService;
53
use TYPO3\CMS\Core\Resource\ResourceFactory;
54
use TYPO3\CMS\Core\Service\OpcodeCacheService;
55
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
56
use TYPO3\CMS\Core\SysLog\Action\Cache as SystemLogCacheAction;
57
use TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
58
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
59
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
60
use TYPO3\CMS\Core\Type\Bitmask\Permission;
61
use TYPO3\CMS\Core\Utility\ArrayUtility;
62
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
63
use TYPO3\CMS\Core\Utility\GeneralUtility;
64
use TYPO3\CMS\Core\Utility\HttpUtility;
65
use TYPO3\CMS\Core\Utility\MathUtility;
66
use TYPO3\CMS\Core\Utility\StringUtility;
67
use TYPO3\CMS\Core\Versioning\VersionState;
68
69
/**
70
 * The main data handler class which takes care of correctly updating and inserting records.
71
 * This class was formerly known as TCEmain.
72
 *
73
 * This is the TYPO3 Core Engine class for manipulation of the database
74
 * This class is used by eg. the tce_db BE route (SimpleDataHandlerController) which provides an interface for POST forms to this class.
75
 *
76
 * Dependencies:
77
 * - $GLOBALS['TCA'] must exist
78
 * - $GLOBALS['LANG'] must exist
79
 *
80
 * Also see document 'TYPO3 Core API' for details.
81
 */
82
class DataHandler implements LoggerAwareInterface
83
{
84
    use LoggerAwareTrait;
85
86
    // *********************
87
    // Public variables you can configure before using the class:
88
    // *********************
89
    /**
90
     * If TRUE, the default log-messages will be stored. This should not be necessary if the locallang-file for the
91
     * log-display is properly configured. So disabling this will just save some database-space as the default messages are not saved.
92
     *
93
     * @var bool
94
     */
95
    public $storeLogMessages = true;
96
97
    /**
98
     * If TRUE, actions are logged to sys_log.
99
     *
100
     * @var bool
101
     */
102
    public $enableLogging = true;
103
104
    /**
105
     * If TRUE, the datamap array is reversed in the order, which is a nice thing if you're creating a whole new
106
     * bunch of records.
107
     *
108
     * @var bool
109
     */
110
    public $reverseOrder = false;
111
112
    /**
113
     * If TRUE, only fields which are different from the database values are saved! In fact, if a whole input array
114
     * is similar, it's not saved then.
115
     *
116
     * @var bool
117
     * @internal should only be used from within TYPO3 Core
118
     */
119
    public $checkSimilar = true;
120
121
    /**
122
     * This will read the record after having updated or inserted it. If anything is not properly submitted an error
123
     * is written to the log. This feature consumes extra time by selecting records
124
     *
125
     * @var bool
126
     */
127
    public $checkStoredRecords = true;
128
129
    /**
130
     * If set, values '' and 0 will equal each other when the stored records are checked.
131
     *
132
     * @var bool
133
     */
134
    public $checkStoredRecords_loose = true;
135
136
    /**
137
     * If this is set, then a page is deleted by deleting the whole branch under it (user must have
138
     * delete permissions to it all). If not set, then the page is deleted ONLY if it has no branch.
139
     *
140
     * @var bool
141
     */
142
    public $deleteTree = false;
143
144
    /**
145
     * If set, then the 'hideAtCopy' flag for tables will be ignored.
146
     *
147
     * @var bool
148
     */
149
    public $neverHideAtCopy = false;
150
151
    /**
152
     * If set, then the TCE class has been instantiated during an import action of a T3D
153
     *
154
     * @var bool
155
     */
156
    public $isImporting = false;
157
158
    /**
159
     * If set, then transformations are NOT performed on the input.
160
     *
161
     * @var bool
162
     */
163
    public $dontProcessTransformations = false;
164
165
    /**
166
     * Will distinguish between translations (with parent) and localizations (without parent) while still using the same methods to copy the records
167
     * TRUE: translation of a record connected to the default language
168
     * FALSE: localization of a record without connection to the default language
169
     *
170
     * @var bool
171
     */
172
    protected $useTransOrigPointerField = true;
173
174
    /**
175
     * If TRUE, workspace restrictions are bypassed on edit and create actions (process_datamap()).
176
     * YOU MUST KNOW what you do if you use this feature!
177
     *
178
     * @var bool
179
     * @internal should only be used from within TYPO3 Core
180
     */
181
    public $bypassWorkspaceRestrictions = false;
182
183
    /**
184
     * If TRUE, access check, check for deleted etc. for records is bypassed.
185
     * YOU MUST KNOW what you are doing if you use this feature!
186
     *
187
     * @var bool
188
     */
189
    public $bypassAccessCheckForRecords = false;
190
191
    /**
192
     * Comma-separated list. This list of tables decides which tables will be copied. If empty then none will.
193
     * If '*' then all will (that the user has permission to of course)
194
     *
195
     * @var string
196
     * @internal should only be used from within TYPO3 Core
197
     */
198
    public $copyWhichTables = '*';
199
200
    /**
201
     * If 0 then branch is NOT copied.
202
     * If 1 then pages on the 1st level is copied.
203
     * If 2 then pages on the second level is copied ... and so on
204
     *
205
     * @var int
206
     */
207
    public $copyTree = 0;
208
209
    /**
210
     * [table][fields]=value: New records are created with default values and you can set this array on the
211
     * form $defaultValues[$table][$field] = $value to override the default values fetched from TCA.
212
     * If ->setDefaultsFromUserTS is called UserTSconfig default values will overrule existing values in this array
213
     * (thus UserTSconfig overrules externally set defaults which overrules TCA defaults)
214
     *
215
     * @var array
216
     * @internal should only be used from within TYPO3 Core
217
     */
218
    public $defaultValues = [];
219
220
    /**
221
     * [table][fields]=value: You can set this array on the form $overrideValues[$table][$field] = $value to
222
     * override the incoming data. You must set this externally. You must make sure the fields in this array are also
223
     * found in the table, because it's not checked. All columns can be set by this array!
224
     *
225
     * @var array
226
     * @internal should only be used from within TYPO3 Core
227
     */
228
    public $overrideValues = [];
229
230
    /**
231
     * If entries are set in this array corresponding to fields for update, they are ignored and thus NOT updated.
232
     * You could set this array from a series of checkboxes with value=0 and hidden fields before the checkbox with 1.
233
     * Then an empty checkbox will disable the field.
234
     *
235
     * @var array
236
     * @internal should only be used from within TYPO3 Core
237
     */
238
    public $data_disableFields = [];
239
240
    /**
241
     * Use this array to validate suggested uids for tables by setting [table]:[uid]. This is a dangerous option
242
     * since it will force the inserted record to have a certain UID. The value just have to be TRUE, but if you set
243
     * it to "DELETE" it will make sure any record with that UID will be deleted first (raw delete).
244
     * The option is used for import of T3D files when synchronizing between two mirrored servers.
245
     * As a security measure this feature is available only for Admin Users (for now)
246
     *
247
     * @var array
248
     */
249
    public $suggestedInsertUids = [];
250
251
    /**
252
     * Object. Call back object for FlexForm traversal. Useful when external classes wants to use the
253
     * iteration functions inside DataHandler for traversing a FlexForm structure.
254
     *
255
     * @var object
256
     * @internal should only be used from within TYPO3 Core
257
     */
258
    public $callBackObj;
259
260
    /**
261
     * A string which can be used as correlationId for RecordHistory entries.
262
     * The string can later be used to rollback multiple changes at once.
263
     *
264
     * @var CorrelationId|null
265
     */
266
    protected $correlationId;
267
268
    // *********************
269
    // Internal variables (mapping arrays) which can be used (read-only) from outside
270
    // *********************
271
    /**
272
     * Contains mapping of auto-versionized records.
273
     *
274
     * @var array
275
     * @internal should only be used from within TYPO3 Core
276
     */
277
    public $autoVersionIdMap = [];
278
279
    /**
280
     * When new elements are created, this array contains a map between their "NEW..." string IDs and the eventual UID they got when stored in database
281
     *
282
     * @var array
283
     */
284
    public $substNEWwithIDs = [];
285
286
    /**
287
     * Like $substNEWwithIDs, but where each old "NEW..." id is mapped to the table it was from.
288
     *
289
     * @var array
290
     * @internal should only be used from within TYPO3 Core
291
     */
292
    public $substNEWwithIDs_table = [];
293
294
    /**
295
     * Holds the tables and there the ids of newly created child records from IRRE
296
     *
297
     * @var array
298
     * @internal should only be used from within TYPO3 Core
299
     */
300
    public $newRelatedIDs = [];
301
302
    /**
303
     * This array is the sum of all copying operations in this class.
304
     *
305
     * @var array
306
     * @internal should only be used from within TYPO3 Core
307
     */
308
    public $copyMappingArray_merged = [];
309
310
    /**
311
     * Per-table array with UIDs that have been deleted.
312
     *
313
     * @var array
314
     */
315
    protected $deletedRecords = [];
316
317
    /**
318
     * Errors are collected in this variable.
319
     *
320
     * @var array
321
     * @internal should only be used from within TYPO3 Core
322
     */
323
    public $errorLog = [];
324
325
    /**
326
     * Fields from the pages-table for which changes will trigger a pagetree refresh
327
     *
328
     * @var array
329
     */
330
    public $pagetreeRefreshFieldsFromPages = ['pid', 'sorting', 'deleted', 'hidden', 'title', 'doktype', 'is_siteroot', 'fe_group', 'nav_hide', 'nav_title', 'module', 'starttime', 'endtime', 'content_from_pid', 'extendToSubpages'];
331
332
    /**
333
     * Indicates whether the pagetree needs a refresh because of important changes
334
     *
335
     * @var bool
336
     * @internal should only be used from within TYPO3 Core
337
     */
338
    public $pagetreeNeedsRefresh = false;
339
340
    // *********************
341
    // Internal Variables, do not touch.
342
    // *********************
343
344
    // Variables set in init() function:
345
346
    /**
347
     * The user-object the script uses. If not set from outside, this is set to the current global $BE_USER.
348
     *
349
     * @var BackendUserAuthentication
350
     */
351
    public $BE_USER;
352
353
    /**
354
     * Will be set to uid of be_user executing this script
355
     *
356
     * @var int
357
     * @internal should only be used from within TYPO3 Core
358
     */
359
    public $userid;
360
361
    /**
362
     * Will be set to username of be_user executing this script
363
     *
364
     * @var string
365
     * @internal should only be used from within TYPO3 Core
366
     */
367
    public $username;
368
369
    /**
370
     * Will be set if user is admin
371
     *
372
     * @var bool
373
     * @internal should only be used from within TYPO3 Core
374
     */
375
    public $admin;
376
377
    /**
378
     * @var PagePermissionAssembler
379
     */
380
    protected $pagePermissionAssembler;
381
382
    /**
383
     * The list of <table>-<fields> that cannot be edited by user. This is compiled from TCA/exclude-flag combined with non_exclude_fields for the user.
384
     *
385
     * @var array
386
     */
387
    protected $excludedTablesAndFields = [];
388
389
    /**
390
     * Data submitted from the form view, used to control behaviours,
391
     * e.g. this is used to activate/deactivate fields and thus store NULL values
392
     *
393
     * @var array
394
     */
395
    protected $control = [];
396
397
    /**
398
     * Set with incoming data array
399
     *
400
     * @var array
401
     */
402
    public $datamap = [];
403
404
    /**
405
     * Set with incoming cmd array
406
     *
407
     * @var array
408
     */
409
    public $cmdmap = [];
410
411
    /**
412
     * List of changed old record ids to new records ids
413
     *
414
     * @var array
415
     */
416
    protected $mmHistoryRecords = [];
417
418
    /**
419
     * List of changed old record ids to new records ids
420
     *
421
     * @var array
422
     */
423
    protected $historyRecords = [];
424
425
    // Internal static:
426
427
    /**
428
     * The interval between sorting numbers used with tables with a 'sorting' field defined.
429
     *
430
     * Min 1, should be power of 2
431
     *
432
     * @var int
433
     * @internal should only be used from within TYPO3 Core
434
     */
435
    public $sortIntervals = 256;
436
437
    // Internal caching arrays
438
    /**
439
     * User by function checkRecordInsertAccess() to store whether a record can be inserted on a page id
440
     *
441
     * @var array
442
     */
443
    protected $recInsertAccessCache = [];
444
445
    /**
446
     * Caching array for check of whether records are in a webmount
447
     *
448
     * @var array
449
     */
450
    protected $isRecordInWebMount_Cache = [];
451
452
    /**
453
     * Caching array for page ids in webmounts
454
     *
455
     * @var array
456
     */
457
    protected $isInWebMount_Cache = [];
458
459
    /**
460
     * Used for caching page records in pageInfo()
461
     *
462
     * @var array
463
     */
464
    protected $pageCache = [];
465
466
    // Other arrays:
467
    /**
468
     * For accumulation of MM relations that must be written after new records are created.
469
     *
470
     * @var array
471
     * @internal
472
     */
473
    public $dbAnalysisStore = [];
474
475
    /**
476
     * Used for tracking references that might need correction after operations
477
     *
478
     * @var array
479
     * @internal
480
     */
481
    public $registerDBList = [];
482
483
    /**
484
     * Used for tracking references that might need correction in pid field after operations (e.g. IRRE)
485
     *
486
     * @var array
487
     * @internal
488
     */
489
    public $registerDBPids = [];
490
491
    /**
492
     * Used by the copy action to track the ids of new pages so subpages are correctly inserted!
493
     * THIS is internally cleared for each executed copy operation! DO NOT USE THIS FROM OUTSIDE!
494
     * Read from copyMappingArray_merged instead which is accumulating this information.
495
     *
496
     * NOTE: This is used by some outside scripts (e.g. hooks), as the results in $copyMappingArray_merged
497
     * are only available after an action has been completed.
498
     *
499
     * @var array
500
     * @internal
501
     */
502
    public $copyMappingArray = [];
503
504
    /**
505
     * Array used for remapping uids and values at the end of process_datamap
506
     *
507
     * @var array
508
     * @internal
509
     */
510
    public $remapStack = [];
511
512
    /**
513
     * Array used for remapping uids and values at the end of process_datamap
514
     * (e.g. $remapStackRecords[<table>][<uid>] = <index in $remapStack>)
515
     *
516
     * @var array
517
     * @internal
518
     */
519
    public $remapStackRecords = [];
520
521
    /**
522
     * Array used for checking whether new children need to be remapped
523
     *
524
     * @var array
525
     */
526
    protected $remapStackChildIds = [];
527
528
    /**
529
     * Array used for executing addition actions after remapping happened (set processRemapStack())
530
     *
531
     * @var array
532
     */
533
    protected $remapStackActions = [];
534
535
    /**
536
     * Registry object to gather reference index update requests and perform updates after
537
     * main processing has been done. The first call to start() instantiates this object.
538
     * Recursive sub instances receive this instance via __construct().
539
     * The final update() call is done at the end of process_cmdmap() or process_datamap()
540
     * in the outer most instance.
541
     *
542
     * @var ReferenceIndexUpdater
543
     */
544
    protected $referenceIndexUpdater;
545
546
    /**
547
     * Tells, that this DataHandler instance was called from \TYPO3\CMS\Impext\ImportExport.
548
     * This variable is set by \TYPO3\CMS\Impext\ImportExport
549
     *
550
     * @var bool
551
     * @internal only used within TYPO3 Core
552
     */
553
    public $callFromImpExp = false;
554
555
    // Various
556
557
    /**
558
     * Set to "currentRecord" during checking of values.
559
     *
560
     * @var array
561
     * @internal
562
     */
563
    public $checkValue_currentRecord = [];
564
565
    /**
566
     * Disable delete clause
567
     *
568
     * @var bool
569
     */
570
    protected $disableDeleteClause = false;
571
572
    /**
573
     * @var array
574
     */
575
    protected $checkModifyAccessListHookObjects;
576
577
    /**
578
     * @var array
579
     */
580
    protected $version_remapMMForVersionSwap_reg;
581
582
    /**
583
     * The outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler:
584
     * This object instantiates itself on versioning and localization ...
585
     *
586
     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
587
     */
588
    protected $outerMostInstance;
589
590
    /**
591
     * Internal cache for collecting records that should trigger cache clearing
592
     *
593
     * @var array
594
     */
595
    protected static $recordsToClearCacheFor = [];
596
597
    /**
598
     * Internal cache for pids of records which were deleted. It's not possible
599
     * to retrieve the parent folder/page at a later stage
600
     *
601
     * @var array
602
     */
603
    protected static $recordPidsForDeletedRecords = [];
604
605
    /**
606
     * Runtime Cache to store and retrieve data computed for a single request
607
     *
608
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
609
     */
610
    protected $runtimeCache;
611
612
    /**
613
     * Prefix for the cache entries of nested element calls since the runtimeCache has a global scope.
614
     *
615
     * @var string
616
     */
617
    protected $cachePrefixNestedElementCalls = 'core-datahandler-nestedElementCalls-';
618
619
    /**
620
     * Sets up the data handler cache and some additional options, the main logic is done in the start() method.
621
     *
622
     * @param ReferenceIndexUpdater|null $referenceIndexUpdater Hand over from outer most instance to sub instances
623
     */
624
    public function __construct(ReferenceIndexUpdater $referenceIndexUpdater = null)
625
    {
626
        $this->checkStoredRecords = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecords'];
627
        $this->checkStoredRecords_loose = (bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['checkStoredRecordsLoose'];
628
        $this->runtimeCache = $this->getRuntimeCache();
629
        $this->pagePermissionAssembler = GeneralUtility::makeInstance(PagePermissionAssembler::class, $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPermissions']);
630
        if ($referenceIndexUpdater === null) {
631
            // Create ReferenceIndexUpdater object. This should only happen on outer most instance,
632
            // sub instances should receive the reference index updater from a parent.
633
            $referenceIndexUpdater = GeneralUtility::makeInstance(ReferenceIndexUpdater::class);
634
        }
635
        $this->referenceIndexUpdater = $referenceIndexUpdater;
636
    }
637
638
    /**
639
     * @param array $control
640
     * @internal
641
     */
642
    public function setControl(array $control)
643
    {
644
        $this->control = $control;
645
    }
646
647
    /**
648
     * Initializing.
649
     * For details, see 'TYPO3 Core API' document.
650
     * This function does not start the processing of data, but merely initializes the object
651
     *
652
     * @param array $data Data to be modified or inserted in the database
653
     * @param array $cmd Commands to copy, move, delete, localize, versionize records.
654
     * @param BackendUserAuthentication|null $altUserObject An alternative userobject you can set instead of the default, which is $GLOBALS['BE_USER']
655
     */
656
    public function start($data, $cmd, $altUserObject = null)
657
    {
658
        // Initializing BE_USER
659
        $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
660
        $this->userid = $this->BE_USER->user['uid'] ?? 0;
661
        $this->username = $this->BE_USER->user['username'] ?? '';
662
        $this->admin = $this->BE_USER->user['admin'] ?? false;
663
        if ($this->BE_USER->uc['recursiveDelete'] ?? false) {
664
            $this->deleteTree = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $deleteTree was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
665
        }
666
667
        // set correlation id for each new set of data or commands
668
        $this->correlationId = CorrelationId::forScope(
669
            md5(StringUtility::getUniqueId(self::class))
670
        );
671
672
        // Get default values from user TSconfig
673
        $tcaDefaultOverride = $this->BE_USER->getTSConfig()['TCAdefaults.'] ?? null;
674
        if (is_array($tcaDefaultOverride)) {
675
            $this->setDefaultsFromUserTS($tcaDefaultOverride);
676
        }
677
678
        // generates the excludelist, based on TCA/exclude-flag and non_exclude_fields for the user:
679
        if (!$this->admin) {
680
            $this->excludedTablesAndFields = array_flip($this->getExcludeListArray());
681
        }
682
        // Setting the data and cmd arrays
683
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
684
            reset($data);
685
            $this->datamap = $data;
686
        }
687
        if (is_array($cmd)) {
0 ignored issues
show
introduced by
The condition is_array($cmd) is always true.
Loading history...
688
            reset($cmd);
689
            $this->cmdmap = $cmd;
690
        }
691
    }
692
693
    /**
694
     * Function that can mirror input values in datamap-array to other uid numbers.
695
     * Example: $mirror[table][11] = '22,33' will look for content in $this->datamap[table][11] and copy it to $this->datamap[table][22] and $this->datamap[table][33]
696
     *
697
     * @param array $mirror This array has the syntax $mirror[table_name][uid] = [list of uids to copy data-value TO!]
698
     * @internal
699
     */
700
    public function setMirror($mirror)
701
    {
702
        if (!is_array($mirror)) {
0 ignored issues
show
introduced by
The condition is_array($mirror) is always true.
Loading history...
703
            return;
704
        }
705
706
        foreach ($mirror as $table => $uid_array) {
707
            if (!isset($this->datamap[$table])) {
708
                continue;
709
            }
710
711
            foreach ($uid_array as $id => $uidList) {
712
                if (!isset($this->datamap[$table][$id])) {
713
                    continue;
714
                }
715
716
                $theIdsInArray = GeneralUtility::trimExplode(',', $uidList, true);
717
                foreach ($theIdsInArray as $copyToUid) {
718
                    $this->datamap[$table][$copyToUid] = $this->datamap[$table][$id];
719
                }
720
            }
721
        }
722
    }
723
724
    /**
725
     * Initializes default values coming from User TSconfig
726
     *
727
     * @param array $userTS User TSconfig array
728
     * @internal should only be used from within DataHandler
729
     */
730
    public function setDefaultsFromUserTS($userTS)
731
    {
732
        if (!is_array($userTS)) {
0 ignored issues
show
introduced by
The condition is_array($userTS) is always true.
Loading history...
733
            return;
734
        }
735
736
        foreach ($userTS as $k => $v) {
737
            $k = mb_substr($k, 0, -1);
738
            if (!$k || !is_array($v) || !isset($GLOBALS['TCA'][$k])) {
739
                continue;
740
            }
741
742
            if (is_array($this->defaultValues[$k])) {
743
                $this->defaultValues[$k] = array_merge($this->defaultValues[$k], $v);
744
            } else {
745
                $this->defaultValues[$k] = $v;
746
            }
747
        }
748
    }
749
750
    /**
751
     * When a new record is created, all values that haven't been set but are set via PageTSconfig / UserTSconfig
752
     * get applied here.
753
     *
754
     * This is only executed for new records. The most important part is that the pageTS of the actual resolved $pid
755
     * is taken, and a new field array with empty defaults is set again.
756
     *
757
     * @param string $table
758
     * @param int $pageId
759
     * @param array $prepopulatedFieldArray
760
     * @return array
761
     */
762
    protected function applyDefaultsForFieldArray(string $table, int $pageId, array $prepopulatedFieldArray): array
763
    {
764
        // First set TCAdefaults respecting the given PageID
765
        $tcaDefaults = BackendUtility::getPagesTSconfig($pageId)['TCAdefaults.'] ?? null;
766
        // Re-apply $this->defaultValues settings
767
        $this->setDefaultsFromUserTS($tcaDefaults);
768
        $cleanFieldArray = $this->newFieldArray($table);
769
        if (isset($prepopulatedFieldArray['pid'])) {
770
            $cleanFieldArray['pid'] = $prepopulatedFieldArray['pid'];
771
        }
772
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? null;
773
        if ($sortColumn !== null && isset($prepopulatedFieldArray[$sortColumn])) {
774
            $cleanFieldArray[$sortColumn] = $prepopulatedFieldArray[$sortColumn];
775
        }
776
        return $cleanFieldArray;
777
    }
778
779
    /*********************************************
780
     *
781
     * HOOKS
782
     *
783
     *********************************************/
784
    /**
785
     * Hook: processDatamap_afterDatabaseOperations
786
     * (calls $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);)
787
     *
788
     * Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
789
     * but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
790
     *
791
     * @param array $hookObjectsArr (reference) Array with hook objects
792
     * @param string $status (reference) Status of the current operation, 'new' or 'update
793
     * @param string $table (reference) The table currently processing data for
794
     * @param string $id (reference) The record uid currently processing data for, [integer] or [string] (like 'NEW...')
795
     * @param array $fieldArray (reference) The field array of a record
796
     * @internal should only be used from within DataHandler
797
     */
798
    public function hook_processDatamap_afterDatabaseOperations(&$hookObjectsArr, &$status, &$table, &$id, &$fieldArray)
799
    {
800
        // Process hook directly:
801
        if (!isset($this->remapStackRecords[$table][$id])) {
802
            foreach ($hookObjectsArr as $hookObj) {
803
                if (method_exists($hookObj, 'processDatamap_afterDatabaseOperations')) {
804
                    $hookObj->processDatamap_afterDatabaseOperations($status, $table, $id, $fieldArray, $this);
805
                }
806
            }
807
        } else {
808
            $this->remapStackRecords[$table][$id]['processDatamap_afterDatabaseOperations'] = [
809
                'status' => $status,
810
                'fieldArray' => $fieldArray,
811
                'hookObjectsArr' => $hookObjectsArr
812
            ];
813
        }
814
    }
815
816
    /**
817
     * Gets the 'checkModifyAccessList' hook objects.
818
     * The first call initializes the accordant objects.
819
     *
820
     * @return array The 'checkModifyAccessList' hook objects (if any)
821
     * @throws \UnexpectedValueException
822
     */
823
    protected function getCheckModifyAccessListHookObjects()
824
    {
825
        if (!isset($this->checkModifyAccessListHookObjects)) {
826
            $this->checkModifyAccessListHookObjects = [];
827
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'] ?? [] as $className) {
828
                $hookObject = GeneralUtility::makeInstance($className);
829
                if (!$hookObject instanceof DataHandlerCheckModifyAccessListHookInterface) {
830
                    throw new \UnexpectedValueException($className . ' must implement interface ' . DataHandlerCheckModifyAccessListHookInterface::class, 1251892472);
831
                }
832
                $this->checkModifyAccessListHookObjects[] = $hookObject;
833
            }
834
        }
835
        return $this->checkModifyAccessListHookObjects;
836
    }
837
838
    /*********************************************
839
     *
840
     * PROCESSING DATA
841
     *
842
     *********************************************/
843
    /**
844
     * Processing the data-array
845
     * Call this function to process the data-array set by start()
846
     *
847
     * @return bool|void
848
     */
849
    public function process_datamap()
850
    {
851
        $this->controlActiveElements();
852
853
        // Keep versionized(!) relations here locally:
854
        $registerDBList = [];
855
        $this->registerElementsToBeDeleted();
856
        $this->datamap = $this->unsetElementsToBeDeleted($this->datamap);
857
        // Editing frozen:
858
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
859
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
860
            return false;
861
        }
862
        // First prepare user defined objects (if any) for hooks which extend this function:
863
        $hookObjectsArr = [];
864
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] ?? [] as $className) {
865
            $hookObject = GeneralUtility::makeInstance($className);
866
            if (method_exists($hookObject, 'processDatamap_beforeStart')) {
867
                $hookObject->processDatamap_beforeStart($this);
868
            }
869
            $hookObjectsArr[] = $hookObject;
870
        }
871
        // Pre-process data-map and synchronize localization states
872
        $this->datamap = GeneralUtility::makeInstance(SlugEnricher::class)->enrichDataMap($this->datamap);
873
        $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER, $this->referenceIndexUpdater)->process();
874
        // Organize tables so that the pages-table is always processed first. This is required if you want to make sure that content pointing to a new page will be created.
875
        $orderOfTables = [];
876
        // Set pages first.
877
        if (isset($this->datamap['pages'])) {
878
            $orderOfTables[] = 'pages';
879
        }
880
        $orderOfTables = array_unique(array_merge($orderOfTables, array_keys($this->datamap)));
881
        // Process the tables...
882
        foreach ($orderOfTables as $table) {
883
            // Check if
884
            //	   - table is set in $GLOBALS['TCA'],
885
            //	   - table is NOT readOnly
886
            //	   - the table is set with content in the data-array (if not, there's nothing to process...)
887
            //	   - permissions for tableaccess OK
888
            $modifyAccessList = $this->checkModifyAccessList($table);
889
            if (!$modifyAccessList) {
890
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
891
            }
892
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->datamap[$table]) || !$modifyAccessList) {
893
                continue;
894
            }
895
896
            if ($this->reverseOrder) {
897
                $this->datamap[$table] = array_reverse($this->datamap[$table], 1);
898
            }
899
            // For each record from the table, do:
900
            // $id is the record uid, may be a string if new records...
901
            // $incomingFieldArray is the array of fields
902
            foreach ($this->datamap[$table] as $id => $incomingFieldArray) {
903
                if (!is_array($incomingFieldArray)) {
904
                    continue;
905
                }
906
                $theRealPid = null;
907
908
                // Hook: processDatamap_preProcessFieldArray
909
                foreach ($hookObjectsArr as $hookObj) {
910
                    if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
911
                        $hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
912
                    }
913
                }
914
                // ******************************
915
                // Checking access to the record
916
                // ******************************
917
                $createNewVersion = false;
918
                $recordAccess = false;
919
                $old_pid_value = '';
920
                // Is it a new record? (Then Id is a string)
921
                if (!MathUtility::canBeInterpretedAsInteger($id)) {
922
                    // Get a fieldArray with tca default values
923
                    $fieldArray = $this->newFieldArray($table);
924
                    // A pid must be set for new records.
925
                    if (isset($incomingFieldArray['pid'])) {
926
                        $pid_value = $incomingFieldArray['pid'];
927
                        // Checking and finding numerical pid, it may be a string-reference to another value
928
                        $canProceed = true;
929
                        // If a NEW... id
930
                        if (strpos($pid_value, 'NEW') !== false) {
931
                            if ($pid_value[0] === '-') {
932
                                $negFlag = -1;
933
                                $pid_value = substr($pid_value, 1);
934
                            } else {
935
                                $negFlag = 1;
936
                            }
937
                            // Trying to find the correct numerical value as it should be mapped by earlier processing of another new record.
938
                            if (isset($this->substNEWwithIDs[$pid_value])) {
939
                                if ($negFlag === 1) {
940
                                    $old_pid_value = $this->substNEWwithIDs[$pid_value];
941
                                }
942
                                $pid_value = (int)($negFlag * $this->substNEWwithIDs[$pid_value]);
943
                            } else {
944
                                $canProceed = false;
945
                            }
946
                        }
947
                        $pid_value = (int)$pid_value;
948
                        if ($canProceed) {
949
                            $fieldArray = $this->resolveSortingAndPidForNewRecord($table, $pid_value, $fieldArray);
950
                        }
951
                    }
952
                    $theRealPid = $fieldArray['pid'];
953
                    // Checks if records can be inserted on this $pid.
954
                    // If this is a page translation, the check needs to be done for the l10n_parent record
955
                    if ($table === 'pages' && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {
956
                        $recordAccess = $this->checkRecordInsertAccess($table, $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]);
957
                    } else {
958
                        $recordAccess = $this->checkRecordInsertAccess($table, $theRealPid);
959
                    }
960
                    if ($recordAccess) {
961
                        $this->addDefaultPermittedLanguageIfNotSet($table, $incomingFieldArray);
962
                        $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $incomingFieldArray, true);
963
                        if (!$recordAccess) {
964
                            $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
965
                        } elseif (!$this->bypassWorkspaceRestrictions && !$this->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
966
                            // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
967
                            // So, if no live records were allowed in the current workspace, we have to create a new version of this record
968
                            if (BackendUtility::isTableWorkspaceEnabled($table)) {
969
                                $createNewVersion = true;
970
                            } else {
971
                                $recordAccess = false;
972
                                $this->newlog('Record could not be created in this workspace', SystemLogErrorClassification::USER_ERROR);
973
                            }
974
                        }
975
                    }
976
                    // Yes new record, change $record_status to 'insert'
977
                    $status = 'new';
978
                } else {
979
                    // Nope... $id is a number
980
                    $fieldArray = [];
981
                    $recordAccess = $this->checkRecordUpdateAccess($table, $id, $incomingFieldArray, $hookObjectsArr);
982
                    if (!$recordAccess) {
983
                        if ($this->enableLogging) {
984
                            $propArr = $this->getRecordProperties($table, $id);
985
                            $this->log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify record \'%s\' (%s) without permission. Or non-existing page.', 2, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']);
986
                        }
987
                        continue;
988
                    }
989
                    // Next check of the record permissions (internals)
990
                    $recordAccess = $this->BE_USER->recordEditAccessInternals($table, $id);
991
                    if (!$recordAccess) {
992
                        $this->newlog('recordEditAccessInternals() check failed. [' . $this->BE_USER->errorMsg . ']', SystemLogErrorClassification::USER_ERROR);
993
                    } else {
994
                        // Here we fetch the PID of the record that we point to...
995
                        $tempdata = $this->recordInfo($table, $id, 'pid' . (BackendUtility::isTableWorkspaceEnabled($table) ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : ''));
996
                        $theRealPid = $tempdata['pid'] ?? null;
997
                        // Use the new id of the versionized record we're trying to write to:
998
                        // (This record is a child record of a parent and has already been versionized.)
999
                        if (!empty($this->autoVersionIdMap[$table][$id])) {
1000
                            // For the reason that creating a new version of this record, automatically
1001
                            // created related child records (e.g. "IRRE"), update the accordant field:
1002
                            $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1003
                            // Use the new id of the copied/versionized record:
1004
                            $id = $this->autoVersionIdMap[$table][$id];
1005
                            $recordAccess = true;
1006
                        } elseif (!$this->bypassWorkspaceRestrictions && ($errorCode = $this->BE_USER->workspaceCannotEditRecord($table, $tempdata))) {
1007
                            $recordAccess = false;
1008
                            // Versioning is required and it must be offline version!
1009
                            // Check if there already is a workspace version
1010
                            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid,t3ver_oid');
1011
                            if ($workspaceVersion) {
1012
                                $id = $workspaceVersion['uid'];
1013
                                $recordAccess = true;
1014
                            } elseif ($this->BE_USER->workspaceAllowAutoCreation($table, $id, $theRealPid)) {
1015
                                // new version of a record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1016
                                $this->pagetreeNeedsRefresh = true;
1017
1018
                                /** @var DataHandler $tce */
1019
                                $tce = GeneralUtility::makeInstance(__CLASS__, $this->referenceIndexUpdater);
1020
                                $tce->enableLogging = $this->enableLogging;
1021
                                // Setting up command for creating a new version of the record:
1022
                                $cmd = [];
1023
                                $cmd[$table][$id]['version'] = [
1024
                                    'action' => 'new',
1025
                                    // Default is to create a version of the individual records
1026
                                    'label' => 'Auto-created for WS #' . $this->BE_USER->workspace
1027
                                ];
1028
                                $tce->start([], $cmd, $this->BE_USER);
1029
                                $tce->process_cmdmap();
1030
                                $this->errorLog = array_merge($this->errorLog, $tce->errorLog);
1031
                                // If copying was successful, share the new uids (also of related children):
1032
                                if (!empty($tce->copyMappingArray[$table][$id])) {
1033
                                    foreach ($tce->copyMappingArray as $origTable => $origIdArray) {
1034
                                        foreach ($origIdArray as $origId => $newId) {
1035
                                            $this->autoVersionIdMap[$origTable][$origId] = $newId;
1036
                                        }
1037
                                    }
1038
                                    // Update registerDBList, that holds the copied relations to child records:
1039
                                    $registerDBList = array_merge($registerDBList, $tce->registerDBList);
1040
                                    // For the reason that creating a new version of this record, automatically
1041
                                    // created related child records (e.g. "IRRE"), update the accordant field:
1042
                                    $this->getVersionizedIncomingFieldArray($table, $id, $incomingFieldArray, $registerDBList);
1043
                                    // Use the new id of the copied/versionized record:
1044
                                    $id = $this->autoVersionIdMap[$table][$id];
1045
                                    $recordAccess = true;
1046
                                } else {
1047
                                    $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version failed!', SystemLogErrorClassification::USER_ERROR);
1048
                                }
1049
                            } else {
1050
                                $this->newlog('Could not be edited in offline workspace in the branch where found (failure state: \'' . $errorCode . '\'). Auto-creation of version not allowed in workspace!', SystemLogErrorClassification::USER_ERROR);
1051
                            }
1052
                        }
1053
                    }
1054
                    // The default is 'update'
1055
                    $status = 'update';
1056
                }
1057
                // If access was granted above, proceed to create or update record:
1058
                if (!$recordAccess) {
1059
                    continue;
1060
                }
1061
1062
                // Here the "pid" is set IF NOT the old pid was a string pointing to a place in the subst-id array.
1063
                [$tscPID] = BackendUtility::getTSCpid($table, $id, $old_pid_value ?: $fieldArray['pid']);
1064
                if ($status === 'new') {
1065
                    // Apply TCAdefaults from pageTS
1066
                    $fieldArray = $this->applyDefaultsForFieldArray($table, (int)$tscPID, $fieldArray);
1067
                    // Apply page permissions as well
1068
                    if ($table === 'pages') {
1069
                        $fieldArray = $this->pagePermissionAssembler->applyDefaults(
1070
                            $fieldArray,
1071
                            (int)$tscPID,
1072
                            (int)$this->userid,
1073
                            (int)$this->BE_USER->firstMainGroup
1074
                        );
1075
                    }
1076
                }
1077
                // Processing of all fields in incomingFieldArray and setting them in $fieldArray
1078
                $fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
1079
                $newVersion_placeholderFieldArray = [];
1080
                if ($createNewVersion) {
1081
                    // create a placeholder array with already processed field content
1082
                    $newVersion_placeholderFieldArray = $fieldArray;
1083
                }
1084
                // NOTICE! All manipulation beyond this point bypasses both "excludeFields" AND possible "MM" relations to field!
1085
                // Forcing some values unto field array:
1086
                // NOTICE: This overriding is potentially dangerous; permissions per field is not checked!!!
1087
                $fieldArray = $this->overrideFieldArray($table, $fieldArray);
1088
                if ($createNewVersion) {
1089
                    $newVersion_placeholderFieldArray = $this->overrideFieldArray($table, $newVersion_placeholderFieldArray);
1090
                }
1091
                // Setting system fields
1092
                if ($status === 'new') {
1093
                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1094
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1095
                        if ($createNewVersion) {
1096
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1097
                        }
1098
                    }
1099
                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1100
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1101
                        if ($createNewVersion) {
1102
                            $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
1103
                        }
1104
                    }
1105
                } elseif ($this->checkSimilar) {
1106
                    // Removing fields which are equal to the current value:
1107
                    $fieldArray = $this->compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray);
1108
                }
1109
                if ($GLOBALS['TCA'][$table]['ctrl']['tstamp'] && !empty($fieldArray)) {
1110
                    $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1111
                    if ($createNewVersion) {
1112
                        $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1113
                    }
1114
                }
1115
                // Set stage to "Editing" to make sure we restart the workflow
1116
                if (BackendUtility::isTableWorkspaceEnabled($table)) {
1117
                    $fieldArray['t3ver_stage'] = 0;
1118
                }
1119
                // Hook: processDatamap_postProcessFieldArray
1120
                foreach ($hookObjectsArr as $hookObj) {
1121
                    if (method_exists($hookObj, 'processDatamap_postProcessFieldArray')) {
1122
                        $hookObj->processDatamap_postProcessFieldArray($status, $table, $id, $fieldArray, $this);
1123
                    }
1124
                }
1125
                // Performing insert/update. If fieldArray has been unset by some userfunction (see hook above), don't do anything
1126
                // Kasper: Unsetting the fieldArray is dangerous; MM relations might be saved already
1127
                if (is_array($fieldArray)) {
1128
                    if ($status === 'new') {
1129
                        if ($table === 'pages') {
1130
                            // for new pages always a refresh is needed
1131
                            $this->pagetreeNeedsRefresh = true;
1132
                        }
1133
1134
                        // This creates a new version of the record with online placeholder and offline version
1135
                        if ($createNewVersion) {
1136
                            // new record created in a workspace - so always refresh pagetree to indicate there is a change in the workspace
1137
                            $this->pagetreeNeedsRefresh = true;
1138
1139
                            // Setting placeholder state value for temporary record
1140
                            $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER);
1141
                            // Setting workspace - only so display of placeholders can filter out those from other workspaces.
1142
                            $newVersion_placeholderFieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1143
                            // Only set a label if it is an input field
1144
                            $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1145
                            if ($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['type'] === 'input') {
1146
                                $newVersion_placeholderFieldArray[$labelField] = $this->getPlaceholderTitleForTableLabel($table);
1147
                            }
1148
                            // Saving placeholder as 'original'
1149
                            $this->insertDB($table, $id, $newVersion_placeholderFieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1150
                            // For the actual new offline version, set versioning values to point to placeholder
1151
                            $fieldArray['pid'] = $theRealPid;
1152
                            $fieldArray['t3ver_oid'] = $this->substNEWwithIDs[$id];
1153
                            // Setting placeholder state value for version (so it can know it is currently a new version...)
1154
                            $fieldArray['t3ver_state'] = (string)new VersionState(VersionState::NEW_PLACEHOLDER_VERSION);
1155
                            $fieldArray['t3ver_wsid'] = $this->BE_USER->workspace;
1156
                            // When inserted, $this->substNEWwithIDs[$id] will be changed to the uid of THIS version and so the interface will pick it up just nice!
1157
                            $phShadowId = $this->insertDB($table, $id, $fieldArray, true, 0, true);
1158
                            if ($phShadowId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $phShadowId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1159
                                // Processes fields of the placeholder record:
1160
                                $this->triggerRemapAction($table, $id, [$this, 'placeholderShadowing'], [$table, $phShadowId]);
1161
                                // Hold auto-versionized ids of placeholders:
1162
                                $this->autoVersionIdMap[$table][$this->substNEWwithIDs[$id]] = $phShadowId;
1163
                            }
1164
                        } else {
1165
                            $this->insertDB($table, $id, $fieldArray, false, (int)($incomingFieldArray['uid'] ?? 0));
1166
                        }
1167
                    } else {
1168
                        if ($table === 'pages') {
1169
                            // only a certain number of fields needs to be checked for updates
1170
                            // if $this->checkSimilar is TRUE, fields with unchanged values are already removed here
1171
                            $fieldsToCheck = array_intersect($this->pagetreeRefreshFieldsFromPages, array_keys($fieldArray));
1172
                            if (!empty($fieldsToCheck)) {
1173
                                $this->pagetreeNeedsRefresh = true;
1174
                            }
1175
                        }
1176
                        $this->updateDB($table, $id, $fieldArray);
1177
                        $this->placeholderShadowing($table, $id);
1178
                    }
1179
                }
1180
                // Hook: processDatamap_afterDatabaseOperations
1181
                // Note: When using the hook after INSERT operations, you will only get the temporary NEW... id passed to your hook as $id,
1182
                // but you can easily translate it to the real uid of the inserted record using the $this->substNEWwithIDs array.
1183
                $this->hook_processDatamap_afterDatabaseOperations($hookObjectsArr, $status, $table, $id, $fieldArray);
1184
            }
1185
        }
1186
        // Process the stack of relations to remap/correct
1187
        $this->processRemapStack();
1188
        $this->dbAnalysisStoreExec();
1189
        // Hook: processDatamap_afterAllOperations
1190
        // Note: When this hook gets called, all operations on the submitted data have been finished.
1191
        foreach ($hookObjectsArr as $hookObj) {
1192
            if (method_exists($hookObj, 'processDatamap_afterAllOperations')) {
1193
                $hookObj->processDatamap_afterAllOperations($this);
1194
            }
1195
        }
1196
1197
        if ($this->isOuterMostInstance()) {
1198
            $this->referenceIndexUpdater->update();
1199
            $this->processClearCacheQueue();
1200
            $this->resetElementsToBeDeleted();
1201
        }
1202
    }
1203
1204
    /**
1205
     * @param string $table
1206
     * @param string $value
1207
     * @param string $dbType
1208
     * @return string
1209
     */
1210
    protected function normalizeTimeFormat(string $table, string $value, string $dbType): string
1211
    {
1212
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1213
        $platform = $connection->getDatabasePlatform();
1214
        if ($platform instanceof SQLServerPlatform) {
1215
            $defaultLength = QueryHelper::getDateTimeFormats()[$dbType]['empty'];
1216
            $value = substr(
1217
                $value,
1218
                0,
1219
                strlen($defaultLength)
1220
            );
1221
        }
1222
        return $value;
1223
    }
1224
1225
    /**
1226
     * Sets the "sorting" DB field and the "pid" field of an incoming record that should be added (NEW1234)
1227
     * depending on the record that should be added or where it should be added.
1228
     *
1229
     * This method is called from process_datamap()
1230
     *
1231
     * @param string $table the table name of the record to insert
1232
     * @param int $pid the real PID (numeric) where the record should be
1233
     * @param array $fieldArray field+value pairs to add
1234
     * @return array the modified field array
1235
     */
1236
    protected function resolveSortingAndPidForNewRecord(string $table, int $pid, array $fieldArray): array
1237
    {
1238
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
1239
        // Points to a page on which to insert the element, possibly in the top of the page
1240
        if ($pid >= 0) {
1241
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1242
            $pid = $this->getDefaultLanguagePageId($pid);
1243
            // The numerical pid is inserted in the data array
1244
            $fieldArray['pid'] = $pid;
1245
            // If this table is sorted we better find the top sorting number
1246
            if ($sortColumn) {
1247
                $fieldArray[$sortColumn] = $this->getSortNumber($table, 0, $pid);
1248
            }
1249
        } elseif ($sortColumn) {
1250
            // Points to another record before itself
1251
            // If this table is sorted we better find the top sorting number
1252
            // Because $pid is < 0, getSortNumber() returns an array
1253
            $sortingInfo = $this->getSortNumber($table, 0, $pid);
1254
            $fieldArray['pid'] = $sortingInfo['pid'];
1255
            $fieldArray[$sortColumn] = $sortingInfo['sortNumber'];
1256
        } else {
1257
            // Here we fetch the PID of the record that we point to
1258
            $record = $this->recordInfo($table, abs($pid), 'pid');
0 ignored issues
show
Bug introduced by
It seems like abs($pid) can also be of type double; however, parameter $id of TYPO3\CMS\Core\DataHandl...taHandler::recordInfo() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1258
            $record = $this->recordInfo($table, /** @scrutinizer ignore-type */ abs($pid), 'pid');
Loading history...
1259
            // Ensure that the "pid" is not a translated page ID, but the default page ID
1260
            $fieldArray['pid'] = $this->getDefaultLanguagePageId($record['pid']);
1261
        }
1262
        return $fieldArray;
1263
    }
1264
1265
    /**
1266
     * Fix shadowing of data in case we are editing an offline version of a live "New" placeholder record.
1267
     *
1268
     * @param string $table Table name
1269
     * @param int $id Record uid
1270
     * @internal should only be used from within DataHandler
1271
     */
1272
    public function placeholderShadowing($table, $id)
1273
    {
1274
        $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $id, '*');
1275
        if (empty($liveRecord)) {
1276
            return;
1277
        }
1278
1279
        $liveState = VersionState::cast($liveRecord['t3ver_state']);
1280
        $versionRecord = BackendUtility::getRecord($table, $id);
1281
        $versionState = VersionState::cast($versionRecord['t3ver_state']);
1282
1283
        if (!$liveState->indicatesPlaceholder() || $versionState->equals(VersionState::MOVE_POINTER)) {
1284
            return;
1285
        }
1286
1287
        $placeholderRecord = $liveRecord;
1288
        $factory = GeneralUtility::makeInstance(
1289
            PlaceholderShadowColumnsResolver::class,
1290
            $table,
1291
            $GLOBALS['TCA'][$table] ?? []
1292
        );
1293
        $shadowColumns = $factory->forNewPlaceholder();
1294
        if (empty($shadowColumns)) {
1295
            return;
1296
        }
1297
1298
        $placeholderValues = [];
1299
        foreach ($shadowColumns as $fieldName) {
1300
            if ((string)$versionRecord[$fieldName] !== (string)$placeholderRecord[$fieldName]) {
1301
                $placeholderValues[$fieldName] = $versionRecord[$fieldName];
1302
            }
1303
        }
1304
        if (empty($placeholderValues)) {
1305
            return;
1306
        }
1307
1308
        if ($this->enableLogging) {
1309
            $this->log($table, $placeholderRecord['uid'], SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Shadowing done on fields <i>' . implode(',', array_keys($placeholderValues)) . '</i> in placeholder record ' . $table . ':' . $liveRecord['uid'] . ' (offline version UID=' . $id . ')', -1, [], $this->eventPid($table, $liveRecord['uid'], $liveRecord['pid']));
1310
        }
1311
        $this->updateDB($table, $placeholderRecord['uid'], $placeholderValues);
1312
    }
1313
1314
    /**
1315
     * Create a placeholder title for the label field that does match the field requirements
1316
     *
1317
     * @param string $table The table name
1318
     * @param string $placeholderContent Placeholder content to be used
1319
     * @return string placeholder value
1320
     * @internal should only be used from within DataHandler
1321
     */
1322
    public function getPlaceholderTitleForTableLabel($table, $placeholderContent = null)
1323
    {
1324
        if ($placeholderContent === null) {
1325
            $placeholderContent = 'PLACEHOLDER';
1326
        }
1327
1328
        $labelPlaceholder = '[' . $placeholderContent . ', WS#' . $this->BE_USER->workspace . ']';
1329
        $labelField = $GLOBALS['TCA'][$table]['ctrl']['label'];
1330
        if (!isset($GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'])) {
1331
            return $labelPlaceholder;
1332
        }
1333
        $evalCodesArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$labelField]['config']['eval'], true);
1334
        $transformedLabel = $this->checkValue_input_Eval($labelPlaceholder, $evalCodesArray, '', $table);
1335
        return $transformedLabel['value'] ?? $labelPlaceholder;
1336
    }
1337
1338
    /**
1339
     * Filling in the field array
1340
     * $this->excludedTablesAndFields is used to filter fields if needed.
1341
     *
1342
     * @param string $table Table name
1343
     * @param int $id Record ID
1344
     * @param array $fieldArray Default values, Preset $fieldArray with 'pid' maybe (pid and uid will be not be overridden anyway)
1345
     * @param array $incomingFieldArray Is which fields/values you want to set. There are processed and put into $fieldArray if OK
1346
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1347
     * @param string $status Is 'new' or 'update'
1348
     * @param int $tscPID TSconfig PID
1349
     * @return array Field Array
1350
     * @internal should only be used from within DataHandler
1351
     */
1352
    public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID)
1353
    {
1354
        // Initialize:
1355
        $originalLanguageRecord = null;
1356
        $originalLanguage_diffStorage = null;
1357
        $diffStorageFlag = false;
1358
        // Setting 'currentRecord' and 'checkValueRecord':
1359
        if (strpos($id, 'NEW') !== false) {
1360
            // Must have the 'current' array - not the values after processing below...
1361
            $checkValueRecord = $fieldArray;
1362
            // IF $incomingFieldArray is an array, overlay it.
1363
            // The point is that when new records are created as copies with flex type fields there might be a field containing information about which DataStructure to use and without that information the flexforms cannot be correctly processed.... This should be OK since the $checkValueRecord is used by the flexform evaluation only anyways...
1364
            if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
0 ignored issues
show
introduced by
The condition is_array($checkValueRecord) is always true.
Loading history...
1365
                ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);
1366
            }
1367
            $currentRecord = $checkValueRecord;
1368
        } else {
1369
            // We must use the current values as basis for this!
1370
            $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));
1371
            // This is done to make the pid positive for offline versions; Necessary to have diff-view for page translations in workspaces.
1372
            BackendUtility::fixVersioningPid($table, $currentRecord);
1373
        }
1374
1375
        // Get original language record if available:
1376
        if (is_array($currentRecord)
1377
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']
1378
            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
1379
            && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
1380
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
1381
            && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
1382
        ) {
1383
            $originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');
1384
            BackendUtility::workspaceOL($table, $originalLanguageRecord);
1385
            $originalLanguage_diffStorage = json_decode(
1386
                (string)($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] ?? ''),
1387
                true
1388
            );
1389
        }
1390
1391
        $this->checkValue_currentRecord = $checkValueRecord;
1392
        // In the following all incoming value-fields are tested:
1393
        // - Are the user allowed to change the field?
1394
        // - Is the field uid/pid (which are already set)
1395
        // - perms-fields for pages-table, then do special things...
1396
        // - If the field is nothing of the above and the field is configured in TCA, the fieldvalues are evaluated by ->checkValue
1397
        // If everything is OK, the field is entered into $fieldArray[]
1398
        foreach ($incomingFieldArray as $field => $fieldValue) {
1399
            if (isset($this->excludedTablesAndFields[$table . '-' . $field]) || $this->data_disableFields[$table][$id][$field]) {
1400
                continue;
1401
            }
1402
1403
            // The field must be editable.
1404
            // Checking if a value for language can be changed:
1405
            $languageDeny = $GLOBALS['TCA'][$table]['ctrl']['languageField'] && (string)$GLOBALS['TCA'][$table]['ctrl']['languageField'] === (string)$field && !$this->BE_USER->checkLanguageAccess($fieldValue);
1406
            if ($languageDeny) {
1407
                continue;
1408
            }
1409
1410
            switch ($field) {
1411
                case 'uid':
1412
                case 'pid':
1413
                    // Nothing happens, already set
1414
                    break;
1415
                case 'perms_userid':
1416
                case 'perms_groupid':
1417
                case 'perms_user':
1418
                case 'perms_group':
1419
                case 'perms_everybody':
1420
                    // Permissions can be edited by the owner or the administrator
1421
                    if ($table === 'pages' && ($this->admin || $status === 'new' || $this->pageInfo($id, 'perms_userid') == $this->userid)) {
1422
                        $value = (int)$fieldValue;
1423
                        switch ($field) {
1424
                            case 'perms_userid':
1425
                            case 'perms_groupid':
1426
                                $fieldArray[$field] = $value;
1427
                                break;
1428
                            default:
1429
                                if ($value >= 0 && $value < (2 ** 5)) {
1430
                                    $fieldArray[$field] = $value;
1431
                                }
1432
                        }
1433
                    }
1434
                    break;
1435
                case 't3ver_oid':
1436
                case 't3ver_wsid':
1437
                case 't3ver_state':
1438
                case 't3ver_stage':
1439
                    break;
1440
                case 'l10n_state':
1441
                    $fieldArray[$field] = $fieldValue;
1442
                    break;
1443
                default:
1444
                    if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
1445
                        // Evaluating the value
1446
                        $res = $this->checkValue($table, $field, $fieldValue, $id, $status, $realPid, $tscPID, $incomingFieldArray);
1447
                        if (array_key_exists('value', $res)) {
1448
                            $fieldArray[$field] = $res['value'];
1449
                        }
1450
                        // Add the value of the original record to the diff-storage content:
1451
                        if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
1452
                            $originalLanguage_diffStorage[$field] = (string)$originalLanguageRecord[$field];
1453
                            $diffStorageFlag = true;
1454
                        }
1455
                    } elseif ($GLOBALS['TCA'][$table]['ctrl']['origUid'] === $field) {
1456
                        // Allow value for original UID to pass by...
1457
                        $fieldArray[$field] = $fieldValue;
1458
                    }
1459
            }
1460
        }
1461
1462
        // Dealing with a page translation, setting "sorting", "pid", "perms_*" to the same values as the original record
1463
        if ($table === 'pages' && is_array($originalLanguageRecord)) {
1464
            $fieldArray['sorting'] = $originalLanguageRecord['sorting'];
1465
            $fieldArray['perms_userid'] = $originalLanguageRecord['perms_userid'];
1466
            $fieldArray['perms_groupid'] = $originalLanguageRecord['perms_groupid'];
1467
            $fieldArray['perms_user'] = $originalLanguageRecord['perms_user'];
1468
            $fieldArray['perms_group'] = $originalLanguageRecord['perms_group'];
1469
            $fieldArray['perms_everybody'] = $originalLanguageRecord['perms_everybody'];
1470
        }
1471
1472
        // Add diff-storage information:
1473
        if ($diffStorageFlag
1474
            && !array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
1475
        ) {
1476
            // If the field is set it would probably be because of an undo-operation - in which case we should not update the field of course...
1477
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = json_encode($originalLanguage_diffStorage);
1478
        }
1479
        // Return fieldArray
1480
        return $fieldArray;
1481
    }
1482
1483
    /*********************************************
1484
     *
1485
     * Evaluation of input values
1486
     *
1487
     ********************************************/
1488
    /**
1489
     * Evaluates a value according to $table/$field settings.
1490
     * This function is for real database fields - NOT FlexForm "pseudo" fields.
1491
     * NOTICE: Calling this function expects this: 1) That the data is saved!
1492
     *
1493
     * @param string $table Table name
1494
     * @param string $field Field name
1495
     * @param string $value Value to be evaluated. Notice, this is the INPUT value from the form. The original value (from any existing record) must be manually looked up inside the function if needed - or taken from $currentRecord array.
1496
     * @param string $id The record-uid, mainly - but not exclusively - used for logging
1497
     * @param string $status 'update' or 'new' flag
1498
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1499
     * @param int $tscPID TSconfig PID
1500
     * @param array $incomingFieldArray the fields being explicitly set by the outside (unlike $fieldArray)
1501
     * @return array Returns the evaluated $value as key "value" in this array. Can be checked with isset($res['value']) ...
1502
     * @internal should only be used from within DataHandler
1503
     */
1504
    public function checkValue($table, $field, $value, $id, $status, $realPid, $tscPID, $incomingFieldArray = [])
1505
    {
1506
        $curValueRec = null;
1507
        // Result array
1508
        $res = [];
1509
1510
        // Processing special case of field pages.doktype
1511
        if ($table === 'pages' && $field === 'doktype') {
1512
            // If the user may not use this specific doktype, we issue a warning
1513
            if (!($this->admin || GeneralUtility::inList($this->BE_USER->groupData['pagetypes_select'], $value))) {
1514
                if ($this->enableLogging) {
1515
                    $propArr = $this->getRecordProperties($table, $id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...::getRecordProperties(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1515
                    $propArr = $this->getRecordProperties($table, /** @scrutinizer ignore-type */ $id);
Loading history...
1516
                    $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, [$propArr['header']], $propArr['event_pid']);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $recuid of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1516
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot change the \'doktype\' of page \'%s\' to the desired value.', 1, [$propArr['header']], $propArr['event_pid']);
Loading history...
1517
                }
1518
                return $res;
1519
            }
1520
            if ($status === 'update') {
1521
                // This checks 1) if we should check for disallowed tables and 2) if there are records from disallowed tables on the current page
1522
                $onlyAllowedTables = $GLOBALS['PAGES_TYPES'][$value]['onlyAllowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['onlyAllowedTables'];
1523
                if ($onlyAllowedTables) {
1524
                    // use the real page id (default language)
1525
                    $recordId = $this->getDefaultLanguagePageId($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $pageId of TYPO3\CMS\Core\DataHandl...DefaultLanguagePageId(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1525
                    $recordId = $this->getDefaultLanguagePageId(/** @scrutinizer ignore-type */ $id);
Loading history...
1526
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, $value);
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type integer expected by parameter $doktype of TYPO3\CMS\Core\DataHandl...geHaveUnallowedTables(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1526
                    $theWrongTables = $this->doesPageHaveUnallowedTables($recordId, /** @scrutinizer ignore-type */ $value);
Loading history...
1527
                    if ($theWrongTables) {
1528
                        if ($this->enableLogging) {
1529
                            $propArr = $this->getRecordProperties($table, $id);
1530
                            $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, '\'doktype\' of page \'%s\' could not be changed because the page contains records from disallowed tables; %s', 2, [$propArr['header'], $theWrongTables], $propArr['event_pid']);
1531
                        }
1532
                        return $res;
1533
                    }
1534
                }
1535
            }
1536
        }
1537
1538
        $curValue = null;
1539
        if ((int)$id !== 0) {
1540
            // Get current value:
1541
            $curValueRec = $this->recordInfo($table, $id, $field);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...taHandler::recordInfo(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1541
            $curValueRec = $this->recordInfo($table, /** @scrutinizer ignore-type */ $id, $field);
Loading history...
1542
            // isset() won't work here, since values can be NULL
1543
            if ($curValueRec !== null && array_key_exists($field, $curValueRec)) {
1544
                $curValue = $curValueRec[$field];
1545
            }
1546
        }
1547
1548
        if ($table === 'be_users'
1549
            && ($field === 'admin' || $field === 'password')
1550
            && $status === 'update'
1551
        ) {
1552
            // Do not allow a non system maintainer admin to change admin flag and password of system maintainers
1553
            $systemMaintainers = array_map('intval', $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
1554
            // False if current user is not in system maintainer list or if switch to user mode is active
1555
            $isCurrentUserSystemMaintainer = $this->BE_USER->isSystemMaintainer();
1556
            $isTargetUserInSystemMaintainerList = in_array((int)$id, $systemMaintainers, true);
1557
            if ($field === 'admin') {
1558
                $isFieldChanged = (int)$curValueRec[$field] !== (int)$value;
1559
            } else {
1560
                $isFieldChanged = $curValueRec[$field] !== $value;
1561
            }
1562
            if (!$isCurrentUserSystemMaintainer && $isTargetUserInSystemMaintainerList && $isFieldChanged) {
1563
                $value = $curValueRec[$field];
1564
                $message = GeneralUtility::makeInstance(
1565
                    FlashMessage::class,
1566
                    $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.adminCanNotChangeSystemMaintainer'),
1567
                    '',
1568
                    FlashMessage::ERROR,
1569
                    true
1570
                );
1571
                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1572
                $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
1573
            }
1574
        }
1575
1576
        // Getting config for the field
1577
        $tcaFieldConf = $this->resolveFieldConfigurationAndRespectColumnsOverrides($table, $field);
1578
1579
        // Create $recFID only for those types that need it
1580
        if ($tcaFieldConf['type'] === 'flex') {
1581
            $recFID = $table . ':' . $id . ':' . $field;
1582
        } else {
1583
            $recFID = null;
1584
        }
1585
1586
        // Perform processing:
1587
        $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, [], $tscPID, ['incomingFieldArray' => $incomingFieldArray]);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...andler::checkValue_SW(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1587
        $res = $this->checkValue_SW($res, $value, $tcaFieldConf, $table, /** @scrutinizer ignore-type */ $id, $curValue, $status, $realPid, $recFID, $field, [], $tscPID, ['incomingFieldArray' => $incomingFieldArray]);
Loading history...
1588
        return $res;
1589
    }
1590
1591
    /**
1592
     * Use columns overrides for evaluation.
1593
     *
1594
     * Fetch the TCA ["config"] part for a specific field, including the columnsOverrides value.
1595
     * Used for checkValue purposes currently (as it takes the checkValue_currentRecord value).
1596
     *
1597
     * @param string $table
1598
     * @param string $field
1599
     * @return array
1600
     */
1601
    protected function resolveFieldConfigurationAndRespectColumnsOverrides(string $table, string $field): array
1602
    {
1603
        $tcaFieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1604
        $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1605
        $columnsOverridesConfigOfField = $GLOBALS['TCA'][$table]['types'][$recordType]['columnsOverrides'][$field]['config'] ?? null;
1606
        if ($columnsOverridesConfigOfField) {
1607
            ArrayUtility::mergeRecursiveWithOverrule($tcaFieldConf, $columnsOverridesConfigOfField);
1608
        }
1609
        return $tcaFieldConf;
1610
    }
1611
1612
    /**
1613
     * Branches out evaluation of a field value based on its type as configured in $GLOBALS['TCA']
1614
     * Can be called for FlexForm pseudo fields as well, BUT must not have $field set if so.
1615
     *
1616
     * @param array $res The result array. The processed value (if any!) is set in the "value" key.
1617
     * @param string $value The value to set.
1618
     * @param array $tcaFieldConf Field configuration from $GLOBALS['TCA']
1619
     * @param string $table Table name
1620
     * @param int $id UID of record
1621
     * @param mixed $curValue Current value of the field
1622
     * @param string $status 'update' or 'new' flag
1623
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1624
     * @param string $recFID Field identifier [table:uid:field] for flexforms
1625
     * @param string $field Field name. Must NOT be set if the call is for a flexform field (since flexforms are not allowed within flexforms).
1626
     * @param array $uploadedFiles
1627
     * @param int $tscPID TSconfig PID
1628
     * @param array $additionalData Additional data to be forwarded to sub-processors
1629
     * @return array Returns the evaluated $value as key "value" in this array.
1630
     * @internal should only be used from within DataHandler
1631
     */
1632
    public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = null)
1633
    {
1634
        // Convert to NULL value if defined in TCA
1635
        if ($value === null && !empty($tcaFieldConf['eval']) && GeneralUtility::inList($tcaFieldConf['eval'], 'null')) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
1636
            $res = ['value' => null];
1637
            return $res;
1638
        }
1639
1640
        switch ($tcaFieldConf['type']) {
1641
            case 'text':
1642
                $res = $this->checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field);
1643
                break;
1644
            case 'passthrough':
1645
            case 'imageManipulation':
1646
            case 'user':
1647
                $res['value'] = $value;
1648
                break;
1649
            case 'input':
1650
                $res = $this->checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field);
1651
                break;
1652
            case 'slug':
1653
                $res = $this->checkValueForSlug((string)$value, $tcaFieldConf, $table, $id, (int)$realPid, $field, $additionalData['incomingFieldArray'] ?? []);
1654
                break;
1655
            case 'check':
1656
                $res = $this->checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1657
                break;
1658
            case 'radio':
1659
                $res = $this->checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $realPid, $field);
1660
                break;
1661
            case 'group':
1662
            case 'select':
1663
                $res = $this->checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field);
1664
                break;
1665
            case 'inline':
1666
                $res = $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
1667
                break;
1668
            case 'flex':
1669
                // FlexForms are only allowed for real fields.
1670
                if ($field) {
1671
                    $res = $this->checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field);
1672
                }
1673
                break;
1674
            default:
1675
                // Do nothing
1676
        }
1677
        $res = $this->checkValueForInternalReferences($res, $value, $tcaFieldConf, $table, $id, $field);
1678
        return $res;
1679
    }
1680
1681
    /**
1682
     * Checks values that are used for internal references. If the provided $value
1683
     * is a NEW-identifier, the direct processing is stopped. Instead, the value is
1684
     * forwarded to the remap-stack to be post-processed and resolved into a proper
1685
     * UID after all data has been resolved.
1686
     *
1687
     * This method considers TCA types that cannot handle and resolve these internal
1688
     * values directly, like 'passthrough', 'none' or 'user'. Values are only modified
1689
     * here if the $field is used as 'transOrigPointerField' or 'translationSource'.
1690
     *
1691
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1692
     * @param string $value The value to set.
1693
     * @param array $tcaFieldConf Field configuration from TCA
1694
     * @param string $table Table name
1695
     * @param int $id UID of record
1696
     * @param string $field The field name
1697
     * @return array The result array. The processed value (if any!) is set in the "value" key.
1698
     */
1699
    protected function checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
1700
    {
1701
        $relevantFieldNames = [
1702
            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null,
1703
            $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null,
1704
        ];
1705
1706
        if (
1707
            // in case field is empty
1708
            empty($field)
1709
            // in case the field is not relevant
1710
            || !in_array($field, $relevantFieldNames)
1711
            // in case the 'value' index has been unset already
1712
            || !array_key_exists('value', $res)
1713
            // in case it's not a NEW-identifier
1714
            || strpos($value, 'NEW') === false
1715
        ) {
1716
            return $res;
1717
        }
1718
1719
        $valueArray = [$value];
1720
        $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
1721
        $this->addNewValuesToRemapStackChildIds($valueArray);
1722
        $this->remapStack[] = [
1723
            'args' => [$valueArray, $tcaFieldConf, $id, $table, $field],
1724
            'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
1725
            'field' => $field
1726
        ];
1727
        unset($res['value']);
1728
1729
        return $res;
1730
    }
1731
1732
    /**
1733
     * Evaluate "text" type values.
1734
     *
1735
     * @param string $value The value to set.
1736
     * @param array $tcaFieldConf Field configuration from TCA
1737
     * @param string $table Table name
1738
     * @param int $id UID of record
1739
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1740
     * @param string $field Field name
1741
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1742
     */
1743
    protected function checkValueForText($value, $tcaFieldConf, $table, $id, $realPid, $field)
1744
    {
1745
        if (isset($tcaFieldConf['eval']) && $tcaFieldConf['eval'] !== '') {
1746
            $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1747
            $evalCodesArray = $this->runtimeCache->get($cacheId);
1748
            if (!is_array($evalCodesArray)) {
1749
                $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1750
                $this->runtimeCache->set($cacheId, $evalCodesArray);
1751
            }
1752
            $valueArray = $this->checkValue_text_Eval($value, $evalCodesArray, $tcaFieldConf['is_in']);
1753
        } else {
1754
            $valueArray = ['value' => $value];
1755
        }
1756
1757
        // Handle richtext transformations
1758
        if ($this->dontProcessTransformations) {
1759
            return $valueArray;
1760
        }
1761
        // Keep null as value
1762
        if ($value === null) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
1763
            return $valueArray;
1764
        }
1765
        if (isset($tcaFieldConf['enableRichtext']) && (bool)$tcaFieldConf['enableRichtext'] === true) {
1766
            $recordType = BackendUtility::getTCAtypeValue($table, $this->checkValue_currentRecord);
1767
            $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
1768
            $richtextConfiguration = $richtextConfigurationProvider->getConfiguration($table, $field, $realPid, $recordType, $tcaFieldConf);
1769
            $rteParser = GeneralUtility::makeInstance(RteHtmlParser::class);
1770
            $valueArray['value'] = $rteParser->transformTextForPersistence((string)$value, $richtextConfiguration['proc.'] ?? []);
1771
        }
1772
1773
        return $valueArray;
1774
    }
1775
1776
    /**
1777
     * Evaluate "input" type values.
1778
     *
1779
     * @param string $value The value to set.
1780
     * @param array $tcaFieldConf Field configuration from TCA
1781
     * @param string $table Table name
1782
     * @param int $id UID of record
1783
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1784
     * @param string $field Field name
1785
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1786
     */
1787
    protected function checkValueForInput($value, $tcaFieldConf, $table, $id, $realPid, $field)
1788
    {
1789
        // Handle native date/time fields
1790
        $isDateOrDateTimeField = false;
1791
        $format = '';
1792
        $emptyValue = '';
1793
        $dateTimeTypes = QueryHelper::getDateTimeTypes();
1794
        // normal integer "date" fields (timestamps) are handled in checkValue_input_Eval
1795
        if (isset($tcaFieldConf['dbType']) && in_array($tcaFieldConf['dbType'], $dateTimeTypes, true)) {
1796
            if (empty($value)) {
1797
                $value = null;
1798
            } else {
1799
                $isDateOrDateTimeField = true;
1800
                $dateTimeFormats = QueryHelper::getDateTimeFormats();
1801
                $format = $dateTimeFormats[$tcaFieldConf['dbType']]['format'];
1802
1803
                // Convert the date/time into a timestamp for the sake of the checks
1804
                $emptyValue = $dateTimeFormats[$tcaFieldConf['dbType']]['empty'];
1805
                // We store UTC timestamps in the database, which is what getTimestamp() returns.
1806
                $dateTime = new \DateTime($value);
1807
                $value = $value === $emptyValue ? null : $dateTime->getTimestamp();
1808
            }
1809
        }
1810
        // Secures the string-length to be less than max.
1811
        if (isset($tcaFieldConf['max']) && (int)$tcaFieldConf['max'] > 0) {
1812
            $value = mb_substr((string)$value, 0, (int)$tcaFieldConf['max'], 'utf-8');
1813
        }
1814
1815
        if (empty($tcaFieldConf['eval'])) {
1816
            $res = ['value' => $value];
1817
        } else {
1818
            // Process evaluation settings:
1819
            $cacheId = $this->getFieldEvalCacheIdentifier($tcaFieldConf['eval']);
1820
            $evalCodesArray = $this->runtimeCache->get($cacheId);
1821
            if (!is_array($evalCodesArray)) {
1822
                $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1823
                $this->runtimeCache->set($cacheId, $evalCodesArray);
1824
            }
1825
1826
            $res = $this->checkValue_input_Eval($value, $evalCodesArray, $tcaFieldConf['is_in'] ?? '', $table);
1827
            if (isset($tcaFieldConf['dbType']) && isset($res['value']) && !$res['value']) {
1828
                // set the value to null if we have an empty value for a native field
1829
                $res['value'] = null;
1830
            }
1831
1832
            // Process UNIQUE settings:
1833
            // Field is NOT set for flexForms - which also means that uniqueInPid and unique is NOT available for flexForm fields! Also getUnique should not be done for versioning
1834
            if ($field && !empty($res['value'])) {
1835
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
1836
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id, $realPid);
1837
                }
1838
                if ($res['value'] && in_array('unique', $evalCodesArray, true)) {
1839
                    $res['value'] = $this->getUnique($table, $field, $res['value'], $id);
1840
                }
1841
            }
1842
        }
1843
1844
        // Checking range of value:
1845
        // @todo: The "checkbox" option was removed for type=input, this check could be probably relaxed?
1846
        if (
1847
            isset($tcaFieldConf['range']) && $tcaFieldConf['range']
1848
            && (!isset($tcaFieldConf['checkbox']) || $res['value'] != $tcaFieldConf['checkbox'])
1849
            && (!isset($tcaFieldConf['default']) || (int)$res['value'] !== (int)$tcaFieldConf['default'])
1850
        ) {
1851
            if (isset($tcaFieldConf['range']['upper']) && (int)$res['value'] > (int)$tcaFieldConf['range']['upper']) {
1852
                $res['value'] = (int)$tcaFieldConf['range']['upper'];
1853
            }
1854
            if (isset($tcaFieldConf['range']['lower']) && (int)$res['value'] < (int)$tcaFieldConf['range']['lower']) {
1855
                $res['value'] = (int)$tcaFieldConf['range']['lower'];
1856
            }
1857
        }
1858
1859
        // Handle native date/time fields
1860
        if ($isDateOrDateTimeField) {
1861
            // Convert the timestamp back to a date/time
1862
            $res['value'] = $res['value'] ? gmdate($format, $res['value']) : $emptyValue;
1863
        }
1864
        return $res;
1865
    }
1866
1867
    /**
1868
     * Evaluate "slug" type values.
1869
     *
1870
     * @param string $value The value to set.
1871
     * @param array $tcaFieldConf Field configuration from TCA
1872
     * @param string $table Table name
1873
     * @param int $id UID of record
1874
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1875
     * @param string $field Field name
1876
     * @param array $incomingFieldArray the fields being explicitly set by the outside (unlike $fieldArray) for the record
1877
     * @return array $res The result array. The processed value (if any!) is set in the "value" key.
1878
     * @see SlugEnricher
1879
     * @see SlugHelper
1880
     */
1881
    protected function checkValueForSlug(string $value, array $tcaFieldConf, string $table, $id, int $realPid, string $field, array $incomingFieldArray = []): array
1882
    {
1883
        $workspaceId = $this->BE_USER->workspace;
1884
        $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $tcaFieldConf, $workspaceId);
1885
        $fullRecord = array_replace_recursive($this->checkValue_currentRecord, $incomingFieldArray ?? []);
1886
        // Generate a value if there is none, otherwise ensure that all characters are cleaned up
1887
        if ($value === '') {
1888
            $value = $helper->generate($fullRecord, $realPid);
1889
        } else {
1890
            $value = $helper->sanitize($value);
1891
        }
1892
1893
        // Return directly in case no evaluations are defined
1894
        if (empty($tcaFieldConf['eval'])) {
1895
            return ['value' => $value];
1896
        }
1897
1898
        $state = RecordStateFactory::forName($table)
1899
            ->fromArray($fullRecord, $realPid, $id);
1900
        $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1901
        if (in_array('unique', $evalCodesArray, true)) {
1902
            $value = $helper->buildSlugForUniqueInTable($value, $state);
1903
        }
1904
        if (in_array('uniqueInSite', $evalCodesArray, true)) {
1905
            $value = $helper->buildSlugForUniqueInSite($value, $state);
1906
        }
1907
        if (in_array('uniqueInPid', $evalCodesArray, true)) {
1908
            $value = $helper->buildSlugForUniqueInPid($value, $state);
1909
        }
1910
1911
        return ['value' => $value];
1912
    }
1913
1914
    /**
1915
     * Evaluates 'check' type values.
1916
     *
1917
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1918
     * @param string $value The value to set.
1919
     * @param array $tcaFieldConf Field configuration from TCA
1920
     * @param string $table Table name
1921
     * @param int $id UID of record
1922
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
1923
     * @param string $field Field name
1924
     * @return array Modified $res array
1925
     */
1926
    protected function checkValueForCheck($res, $value, $tcaFieldConf, $table, $id, $realPid, $field)
1927
    {
1928
        $items = $tcaFieldConf['items'];
1929
        if (!empty($tcaFieldConf['itemsProcFunc'])) {
1930
            /** @var ItemProcessingService $processingService */
1931
            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
1932
            $items = $processingService->getProcessingItems(
1933
                $table,
1934
                $realPid,
1935
                $field,
1936
                $this->checkValue_currentRecord,
1937
                $tcaFieldConf,
1938
                $tcaFieldConf['items']
1939
            );
1940
        }
1941
1942
        $itemC = 0;
1943
        if ($items !== null) {
1944
            $itemC = count($items);
1945
        }
1946
        if (!$itemC) {
1947
            $itemC = 1;
1948
        }
1949
        $maxV = (2 ** $itemC) - 1;
1950
        if ($value < 0) {
1951
            // @todo: throw LogicException here? Negative values for checkbox items do not make sense and indicate a coding error.
1952
            $value = 0;
1953
        }
1954
        if ($value > $maxV) {
1955
            // @todo: This case is pretty ugly: If there is an itemsProcFunc registered, and if it returns a dynamic,
1956
            // @todo: changing list of items, then it may happen that a value is transformed and vanished checkboxes
1957
            // @todo: are permanently removed from the value.
1958
            // @todo: Suggestion: Throw an exception instead? Maybe a specific, catchable exception that generates a
1959
            // @todo: error message to the user - dynamic item sets via itemProcFunc on check would be a bad idea anyway.
1960
            $value = $value & $maxV;
1961
        }
1962
        if ($field && $value > 0 && !empty($tcaFieldConf['eval'])) {
1963
            $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true);
1964
            $otherRecordsWithSameValue = [];
1965
            $maxCheckedRecords = 0;
1966
            if (in_array('maximumRecordsCheckedInPid', $evalCodesArray, true)) {
1967
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value, $realPid);
1968
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsCheckedInPid'];
1969
            }
1970
            if (in_array('maximumRecordsChecked', $evalCodesArray, true)) {
1971
                $otherRecordsWithSameValue = $this->getRecordsWithSameValue($table, $id, $field, $value);
1972
                $maxCheckedRecords = (int)$tcaFieldConf['validation']['maximumRecordsChecked'];
1973
            }
1974
1975
            // there are more than enough records with value "1" in the DB
1976
            // if so, set this value to "0" again
1977
            if ($maxCheckedRecords && count($otherRecordsWithSameValue) >= $maxCheckedRecords) {
1978
                $value = 0;
1979
                $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Could not activate checkbox for field "%s". A total of %s record(s) can have this checkbox activated. Uncheck other records first in order to activate the checkbox of this record.', -1, [$this->getLanguageService()->sL(BackendUtility::getItemLabel($table, $field)), $maxCheckedRecords]);
1980
            }
1981
        }
1982
        $res['value'] = $value;
1983
        return $res;
1984
    }
1985
1986
    /**
1987
     * Evaluates 'radio' type values.
1988
     *
1989
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
1990
     * @param string $value The value to set.
1991
     * @param array $tcaFieldConf Field configuration from TCA
1992
     * @param string $table The table of the record
1993
     * @param int $id The id of the record
1994
     * @param int $pid The pid of the record
1995
     * @param string $field The field to check
1996
     * @return array Modified $res array
1997
     */
1998
    protected function checkValueForRadio($res, $value, $tcaFieldConf, $table, $id, $pid, $field)
1999
    {
2000
        if (is_array($tcaFieldConf['items'])) {
2001
            foreach ($tcaFieldConf['items'] as $set) {
2002
                if ((string)$set[1] === (string)$value) {
2003
                    $res['value'] = $value;
2004
                    break;
2005
                }
2006
            }
2007
        }
2008
2009
        // if no value was found and an itemsProcFunc is defined, check that for the value
2010
        if ($tcaFieldConf['itemsProcFunc'] && empty($res['value'])) {
2011
            $processingService = GeneralUtility::makeInstance(ItemProcessingService::class);
2012
            $processedItems = $processingService->getProcessingItems(
2013
                $table,
2014
                $pid,
2015
                $field,
2016
                $this->checkValue_currentRecord,
2017
                $tcaFieldConf,
2018
                $tcaFieldConf['items']
2019
            );
2020
2021
            foreach ($processedItems as $set) {
2022
                if ((string)$set[1] === (string)$value) {
2023
                    $res['value'] = $value;
2024
                    break;
2025
                }
2026
            }
2027
        }
2028
2029
        return $res;
2030
    }
2031
2032
    /**
2033
     * Evaluates 'group' or 'select' type values.
2034
     *
2035
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2036
     * @param string|array $value The value to set.
2037
     * @param array $tcaFieldConf Field configuration from TCA
2038
     * @param string $table Table name
2039
     * @param int $id UID of record
2040
     * @param mixed $curValue Current value of the field
2041
     * @param string $status 'update' or 'new' flag
2042
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2043
     * @param array $uploadedFiles
2044
     * @param string $field Field name
2045
     * @return array Modified $res array
2046
     */
2047
    protected function checkValueForGroupSelect($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $recFID, $uploadedFiles, $field)
2048
    {
2049
        // Detecting if value sent is an array and if so, implode it around a comma:
2050
        if (is_array($value)) {
2051
            $value = implode(',', $value);
2052
        }
2053
        // This converts all occurrences of '&#123;' to the byte 123 in the string - this is needed in very rare cases where file names with special characters (e.g. ???, umlaut) gets sent to the server as HTML entities instead of bytes. The error is done only by MSIE, not Mozilla and Opera.
2054
        // Anyway, this should NOT disturb anything else:
2055
        $value = $this->convNumEntityToByteValue($value);
2056
        // When values are sent as group or select they come as comma-separated values which are exploded by this function:
2057
        $valueArray = $this->checkValue_group_select_explodeSelectGroupValue($value);
2058
        // If multiple is not set, remove duplicates:
2059
        if (!$tcaFieldConf['multiple']) {
2060
            $valueArray = array_unique($valueArray);
2061
        }
2062
        // If an exclusive key is found, discard all others:
2063
        if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['exclusiveKeys']) {
2064
            $exclusiveKeys = GeneralUtility::trimExplode(',', $tcaFieldConf['exclusiveKeys']);
2065
            foreach ($valueArray as $index => $key) {
2066
                if (in_array($key, $exclusiveKeys, true)) {
2067
                    $valueArray = [$index => $key];
2068
                    break;
2069
                }
2070
            }
2071
        }
2072
        // This could be a good spot for parsing the array through a validation-function which checks if the values are correct (except that database references are not in their final form - but that is the point, isn't it?)
2073
        // NOTE!!! Must check max-items of files before the later check because that check would just leave out file names if there are too many!!
2074
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
2075
        // Checking for select / authMode, removing elements from $valueArray if any of them is not allowed!
2076
        if ($tcaFieldConf['type'] === 'select' && $tcaFieldConf['authMode']) {
2077
            $preCount = count($valueArray);
2078
            foreach ($valueArray as $index => $key) {
2079
                if (!$this->BE_USER->checkAuthMode($table, $field, $key, $tcaFieldConf['authMode'])) {
2080
                    unset($valueArray[$index]);
2081
                }
2082
            }
2083
            // During the check it turns out that the value / all values were removed - we respond by simply returning an empty array so nothing is written to DB for this field.
2084
            if ($preCount && empty($valueArray)) {
2085
                return [];
2086
            }
2087
        }
2088
        // For select types which has a foreign table attached:
2089
        $unsetResult = false;
2090
        if (
2091
            $tcaFieldConf['type'] === 'group' && $tcaFieldConf['internal_type'] === 'db'
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($tcaFieldConf['type'] =...ecial'] === 'languages', Probably Intended Meaning: $tcaFieldConf['type'] ==...cial'] === 'languages')
Loading history...
2092
            || $tcaFieldConf['type'] === 'select' && ($tcaFieldConf['foreign_table'] || isset($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages')
2093
        ) {
2094
            // check, if there is a NEW... id in the value, that should be substituted later
2095
            if (strpos($value, 'NEW') !== false) {
2096
                $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2097
                $this->addNewValuesToRemapStackChildIds($valueArray);
2098
                $this->remapStack[] = [
2099
                    'func' => 'checkValue_group_select_processDBdata',
2100
                    'args' => [$valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field],
2101
                    'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 5],
2102
                    'field' => $field
2103
                ];
2104
                $unsetResult = true;
2105
            } else {
2106
                $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $tcaFieldConf['type'], $table, $field);
2107
            }
2108
        }
2109
        if (!$unsetResult) {
2110
            $newVal = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
2111
            $res['value'] = $this->castReferenceValue(implode(',', $newVal), $tcaFieldConf);
2112
        } else {
2113
            unset($res['value']);
2114
        }
2115
        return $res;
2116
    }
2117
2118
    /**
2119
     * Applies the filter methods from a column's TCA configuration to a value array.
2120
     *
2121
     * @param array $tcaFieldConfiguration
2122
     * @param array $values
2123
     * @return array|mixed
2124
     * @throws \RuntimeException
2125
     */
2126
    protected function applyFiltersToValues(array $tcaFieldConfiguration, array $values)
2127
    {
2128
        if (empty($tcaFieldConfiguration['filter']) || !is_array($tcaFieldConfiguration['filter'])) {
2129
            return $values;
2130
        }
2131
        foreach ($tcaFieldConfiguration['filter'] as $filter) {
2132
            if (empty($filter['userFunc'])) {
2133
                continue;
2134
            }
2135
            $parameters = $filter['parameters'] ?: [];
2136
            $parameters['values'] = $values;
2137
            $parameters['tcaFieldConfig'] = $tcaFieldConfiguration;
2138
            $values = GeneralUtility::callUserFunction($filter['userFunc'], $parameters, $this);
2139
            if (!is_array($values)) {
2140
                throw new \RuntimeException('Failed calling filter userFunc.', 1336051942);
2141
            }
2142
        }
2143
        return $values;
2144
    }
2145
2146
    /**
2147
     * Evaluates 'flex' type values.
2148
     *
2149
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2150
     * @param string|array $value The value to set.
2151
     * @param array $tcaFieldConf Field configuration from TCA
2152
     * @param string $table Table name
2153
     * @param int $id UID of record
2154
     * @param mixed $curValue Current value of the field
2155
     * @param string $status 'update' or 'new' flag
2156
     * @param int $realPid The real PID value of the record. For updates, this is just the pid of the record. For new records this is the PID of the page where it is inserted.
2157
     * @param string $recFID Field identifier [table:uid:field] for flexforms
2158
     * @param int $tscPID TSconfig PID
2159
     * @param array $uploadedFiles Uploaded files for the field
2160
     * @param string $field Field name
2161
     * @return array Modified $res array
2162
     */
2163
    protected function checkValueForFlex($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $tscPID, $uploadedFiles, $field)
2164
    {
2165
        if (is_array($value)) {
2166
            // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
2167
            // Problem: when copying a page, flexform XML comes along in the array for the new record - but since $this->checkValue_currentRecord
2168
            // does not have a uid or pid for that sake, the FlexFormTools->getDataStructureIdentifier() function returns no good DS. For new
2169
            // records we do know the expected PID so therefore we send that with this special parameter. Only active when larger than zero.
2170
            $row = $this->checkValue_currentRecord;
2171
            if ($status === 'new') {
2172
                $row['pid'] = $realPid;
2173
            }
2174
2175
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
2176
2177
            // Get data structure. The methods may throw various exceptions, with some of them being
2178
            // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
2179
            // and substitute with a dummy DS.
2180
            $dataStructureArray = ['sheets' => ['sDEF' => []]];
2181
            try {
2182
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
2183
                    ['config' => $tcaFieldConf],
2184
                    $table,
2185
                    $field,
2186
                    $row
2187
                );
2188
2189
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
2190
            } catch (InvalidParentRowException|InvalidParentRowLoopException|InvalidParentRowRootException|InvalidPointerFieldValueException|InvalidIdentifierException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2191
            }
2192
2193
            // Get current value array:
2194
            $currentValueArray = (string)$curValue !== '' ? GeneralUtility::xml2array($curValue) : [];
2195
            if (!is_array($currentValueArray)) {
2196
                $currentValueArray = [];
2197
            }
2198
            // Remove all old meta for languages...
2199
            // Evaluation of input values:
2200
            $value['data'] = $this->checkValue_flex_procInData($value['data'] ?? [], $currentValueArray['data'] ?? [], $uploadedFiles['data'] ?? [], $dataStructureArray, [$table, $id, $curValue, $status, $realPid, $recFID, $tscPID]);
2201
            // Create XML from input value:
2202
            $xmlValue = $this->checkValue_flexArray2Xml($value, true);
2203
2204
            // Here we convert the currently submitted values BACK to an array, then merge the two and then BACK to XML again. This is needed to ensure the charsets are the same
2205
            // (provided that the current value was already stored IN the charset that the new value is converted to).
2206
            $arrValue = GeneralUtility::xml2array($xmlValue);
2207
2208
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'] ?? [] as $className) {
2209
                $hookObject = GeneralUtility::makeInstance($className);
2210
                if (method_exists($hookObject, 'checkFlexFormValue_beforeMerge')) {
2211
                    $hookObject->checkFlexFormValue_beforeMerge($this, $currentValueArray, $arrValue);
2212
                }
2213
            }
2214
2215
            ArrayUtility::mergeRecursiveWithOverrule($currentValueArray, $arrValue);
2216
            $xmlValue = $this->checkValue_flexArray2Xml($currentValueArray, true);
2217
2218
            // Action commands (sorting order and removals of elements) for flexform sections,
2219
            // see FormEngine for the use of this GP parameter
2220
            $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
2221
            if (is_array($actionCMDs[$table][$id][$field]['data'] ?? null)) {
2222
                $arrValue = GeneralUtility::xml2array($xmlValue);
2223
                $this->_ACTION_FLEX_FORMdata($arrValue['data'], $actionCMDs[$table][$id][$field]['data']);
2224
                $xmlValue = $this->checkValue_flexArray2Xml($arrValue, true);
2225
            }
2226
            // Create the value XML:
2227
            $res['value'] = '';
2228
            $res['value'] .= $xmlValue;
2229
        } else {
2230
            // Passthrough...:
2231
            $res['value'] = $value;
2232
        }
2233
2234
        return $res;
2235
    }
2236
2237
    /**
2238
     * Converts an array to FlexForm XML
2239
     *
2240
     * @param array $array Array with FlexForm data
2241
     * @param bool $addPrologue If set, the XML prologue is returned as well.
2242
     * @return string Input array converted to XML
2243
     * @internal should only be used from within DataHandler
2244
     */
2245
    public function checkValue_flexArray2Xml($array, $addPrologue = false)
2246
    {
2247
        /** @var FlexFormTools $flexObj */
2248
        $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
2249
        return $flexObj->flexArray2Xml($array, $addPrologue);
2250
    }
2251
2252
    /**
2253
     * Actions for flex form element (move, delete)
2254
     * allows to remove and move flexform sections
2255
     *
2256
     * @param array $valueArray by reference
2257
     * @param array $actionCMDs
2258
     */
2259
    protected function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
2260
    {
2261
        if (!is_array($valueArray) || !is_array($actionCMDs)) {
0 ignored issues
show
introduced by
The condition is_array($valueArray) is always true.
Loading history...
introduced by
The condition is_array($actionCMDs) is always true.
Loading history...
2262
            return;
2263
        }
2264
2265
        foreach ($actionCMDs as $key => $value) {
2266
            if ($key === '_ACTION') {
2267
                // First, check if there are "commands":
2268
                if (empty(array_filter($actionCMDs[$key]))) {
2269
                    continue;
2270
                }
2271
2272
                asort($actionCMDs[$key]);
2273
                $newValueArray = [];
2274
                foreach ($actionCMDs[$key] as $idx => $order) {
2275
                    // Just one reflection here: It is clear that when removing elements from a flexform, then we will get lost
2276
                    // files unless we act on this delete operation by traversing and deleting files that were referred to.
2277
                    if ($order !== 'DELETE') {
2278
                        $newValueArray[$idx] = $valueArray[$idx];
2279
                    }
2280
                    unset($valueArray[$idx]);
2281
                }
2282
                $valueArray += $newValueArray;
2283
            } elseif (is_array($actionCMDs[$key]) && isset($valueArray[$key])) {
2284
                $this->_ACTION_FLEX_FORMdata($valueArray[$key], $actionCMDs[$key]);
2285
            }
2286
        }
2287
    }
2288
2289
    /**
2290
     * Evaluates 'inline' type values.
2291
     * (partly copied from the select_group function on this issue)
2292
     *
2293
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2294
     * @param string $value The value to set.
2295
     * @param array $tcaFieldConf Field configuration from TCA
2296
     * @param array $PP Additional parameters in a numeric array: $table,$id,$curValue,$status,$realPid,$recFID
2297
     * @param string $field Field name
2298
     * @param array $additionalData Additional data to be forwarded to sub-processors
2299
     * @internal should only be used from within DataHandler
2300
     */
2301
    public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = null)
2302
    {
2303
        [$table, $id, , $status] = $PP;
2304
        $this->checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, $additionalData);
2305
    }
2306
2307
    /**
2308
     * Evaluates 'inline' type values.
2309
     * (partly copied from the select_group function on this issue)
2310
     *
2311
     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
2312
     * @param string $value The value to set.
2313
     * @param array $tcaFieldConf Field configuration from TCA
2314
     * @param string $table Table name
2315
     * @param int $id UID of record
2316
     * @param string $status 'update' or 'new' flag
2317
     * @param string $field Field name
2318
     * @param array $additionalData Additional data to be forwarded to sub-processors
2319
     * @return array|bool Modified $res array
2320
     * @internal should only be used from within DataHandler
2321
     */
2322
    public function checkValueForInline($res, $value, $tcaFieldConf, $table, $id, $status, $field, array $additionalData = null)
2323
    {
2324
        if (!$tcaFieldConf['foreign_table']) {
2325
            // Fatal error, inline fields should always have a foreign_table defined
2326
            return false;
2327
        }
2328
        // When values are sent they come as comma-separated values which are exploded by this function:
2329
        $valueArray = GeneralUtility::trimExplode(',', $value);
2330
        // Remove duplicates: (should not be needed)
2331
        $valueArray = array_unique($valueArray);
2332
        // Example for received data:
2333
        // $value = 45,NEW4555fdf59d154,12,123
2334
        // We need to decide whether we use the stack or can save the relation directly.
2335
        if (!empty($value) && (strpos($value, 'NEW') !== false || !MathUtility::canBeInterpretedAsInteger($id))) {
2336
            $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
2337
            $this->addNewValuesToRemapStackChildIds($valueArray);
2338
            $this->remapStack[] = [
2339
                'func' => 'checkValue_inline_processDBdata',
2340
                'args' => [$valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData],
2341
                'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4],
2342
                'additionalData' => $additionalData,
2343
                'field' => $field,
2344
            ];
2345
            unset($res['value']);
2346
        } elseif ($value || MathUtility::canBeInterpretedAsInteger($id)) {
2347
            $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
2348
        }
2349
        return $res;
2350
    }
2351
2352
    /**
2353
     * Checks if a fields has more items than defined via TCA in maxitems.
2354
     * If there are more items than allowed, the item list is truncated to the defined number.
2355
     *
2356
     * @param array $tcaFieldConf Field configuration from TCA
2357
     * @param array $valueArray Current value array of items
2358
     * @return array The truncated value array of items
2359
     * @internal should only be used from within DataHandler
2360
     */
2361
    public function checkValue_checkMax($tcaFieldConf, $valueArray)
2362
    {
2363
        // BTW, checking for min and max items here does NOT make any sense when MM is used because the above function
2364
        // calls will just return an array with a single item (the count) if MM is used... Why didn't I perform the check
2365
        // before? Probably because we could not evaluate the validity of record uids etc... Hmm...
2366
        // NOTE to the comment: It's not really possible to check for too few items, because you must then determine first,
2367
        // if the field is actual used regarding the CType.
2368
        $maxitems = isset($tcaFieldConf['maxitems']) ? (int)$tcaFieldConf['maxitems'] : 99999;
2369
        return array_slice($valueArray, 0, $maxitems);
2370
    }
2371
2372
    /*********************************************
2373
     *
2374
     * Helper functions for evaluation functions.
2375
     *
2376
     ********************************************/
2377
    /**
2378
     * Gets a unique value for $table/$id/$field based on $value
2379
     *
2380
     * @param string $table Table name
2381
     * @param string $field Field name for which $value must be unique
2382
     * @param string $value Value string.
2383
     * @param int $id UID to filter out in the lookup (the record itself...)
2384
     * @param int $newPid If set, the value will be unique for this PID
2385
     * @return string Modified value (if not-unique). Will be the value appended with a number (until 100, then the function just breaks).
2386
     * @todo: consider workspaces, especially when publishing a unique value which has a unique value already in live
2387
     * @internal should only be used from within DataHandler
2388
     */
2389
    public function getUnique($table, $field, $value, $id, $newPid = 0)
2390
    {
2391
        if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2392
            // Field is not configured in TCA
2393
            return $value;
2394
        }
2395
2396
        if ((string)$GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] === 'exclude') {
2397
            $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2398
            $l10nParent = (int)$this->checkValue_currentRecord[$transOrigPointerField];
2399
            if ($l10nParent > 0) {
2400
                // Current record is a translation and l10n_mode "exclude" just copies the value from source language
2401
                return $value;
2402
            }
2403
        }
2404
2405
        $newValue = $originalValue = $value;
2406
        $statement = $this->getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
2407
        // For as long as records with the test-value existing, try again (with incremented numbers appended)
2408
        if ($statement->fetchColumn()) {
2409
            for ($counter = 0; $counter <= 100; $counter++) {
2410
                $newValue = $value . $counter;
2411
                $statement->bindValue(1, $newValue);
2412
                $statement->execute();
2413
                if (!$statement->fetchColumn()) {
2414
                    break;
2415
                }
2416
            }
2417
        }
2418
2419
        if ($originalValue !== $newValue) {
2420
            $this->log($table, $id, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::WARNING, 'The value of the field "%s" has been changed from "%s" to "%s" as it is required to be unique.', 1, [$field, $originalValue, $newValue], $newPid);
2421
        }
2422
2423
        return $newValue;
2424
    }
2425
2426
    /**
2427
     * Gets the count of records for a unique field
2428
     *
2429
     * @param string $value The string value which should be unique
2430
     * @param string $table Table name
2431
     * @param string $field Field name for which $value must be unique
2432
     * @param int $uid UID to filter out in the lookup (the record itself...)
2433
     * @param int $pid If set, the value will be unique for this PID
2434
     * @return \Doctrine\DBAL\Driver\Statement Return the prepared statement to check uniqueness
2435
     */
2436
    protected function getUniqueCountStatement(
2437
        string $value,
2438
        string $table,
2439
        string $field,
2440
        int $uid,
2441
        int $pid
2442
    ): Statement {
2443
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2444
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
2445
        $queryBuilder
2446
            ->count('uid')
2447
            ->from($table)
2448
            ->where(
2449
                $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value, \PDO::PARAM_STR)),
2450
                $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
2451
            );
2452
        // ignore translations of current record if field is configured with l10n_mode = "exclude"
2453
        if (($GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] ?? '') === 'exclude'
2454
            && ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '') !== ''
2455
            && ($GLOBALS['TCA'][$table]['columns'][$field]['languageField'] ?? '') !== '') {
2456
            $queryBuilder
2457
                ->andWhere(
2458
                    $queryBuilder->expr()->orX(
2459
                    // records without l10n_parent must be taken into account (in any language)
2460
                        $queryBuilder->expr()->eq(
2461
                            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2462
                            $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT)
2463
                        ),
2464
                        // translations of other records must be taken into account
2465
                        $queryBuilder->expr()->neq(
2466
                            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2467
                            $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)
2468
                        )
2469
                    )
2470
                );
2471
        }
2472
        if ($pid !== 0) {
2473
            $queryBuilder->andWhere(
2474
                $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, \PDO::PARAM_INT))
2475
            );
2476
        } else {
2477
            // pid>=0 for versioning
2478
            $queryBuilder->andWhere(
2479
                $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT))
2480
            );
2481
        }
2482
        return $queryBuilder->execute();
2483
    }
2484
2485
    /**
2486
     * gets all records that have the same value in a field
2487
     * excluding the given uid
2488
     *
2489
     * @param string $tableName Table name
2490
     * @param int $uid UID to filter out in the lookup (the record itself...)
2491
     * @param string $fieldName Field name for which $value must be unique
2492
     * @param string $value Value string.
2493
     * @param int $pageId If set, the value will be unique for this PID
2494
     * @return array
2495
     * @internal should only be used from within DataHandler
2496
     */
2497
    public function getRecordsWithSameValue($tableName, $uid, $fieldName, $value, $pageId = 0)
2498
    {
2499
        $result = [];
2500
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
2501
            return $result;
2502
        }
2503
2504
        $uid = (int)$uid;
2505
        $pageId = (int)$pageId;
2506
2507
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
2508
        $queryBuilder->getRestrictions()
2509
            ->removeAll()
2510
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2511
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
2512
2513
        $queryBuilder->select('*')
2514
            ->from($tableName)
2515
            ->where(
2516
                $queryBuilder->expr()->eq(
2517
                    $fieldName,
2518
                    $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
2519
                ),
2520
                $queryBuilder->expr()->neq(
2521
                    'uid',
2522
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
2523
                )
2524
            );
2525
2526
        if ($pageId) {
2527
            $queryBuilder->andWhere(
2528
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT))
2529
            );
2530
        }
2531
2532
        $result = $queryBuilder->execute()->fetchAll();
2533
2534
        return $result;
2535
    }
2536
2537
    /**
2538
     * @param string $value The field value to be evaluated
2539
     * @param array $evalArray Array of evaluations to traverse.
2540
     * @param string $is_in The "is_in" value of the field configuration from TCA
2541
     * @return array
2542
     * @internal should only be used from within DataHandler
2543
     */
2544
    public function checkValue_text_Eval($value, $evalArray, $is_in)
2545
    {
2546
        $res = [];
2547
        $set = true;
2548
        foreach ($evalArray as $func) {
2549
            switch ($func) {
2550
                case 'trim':
2551
                    $value = trim($value);
2552
                    break;
2553
                case 'required':
2554
                    if (!$value) {
2555
                        $set = false;
2556
                    }
2557
                    break;
2558
                default:
2559
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2560
                        if (class_exists($func)) {
2561
                            $evalObj = GeneralUtility::makeInstance($func);
2562
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2563
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2564
                            }
2565
                        }
2566
                    }
2567
            }
2568
        }
2569
        if ($set) {
2570
            $res['value'] = $value;
2571
        }
2572
        return $res;
2573
    }
2574
2575
    /**
2576
     * Evaluation of 'input'-type values based on 'eval' list
2577
     *
2578
     * @param string $value Value to evaluate
2579
     * @param array $evalArray Array of evaluations to traverse.
2580
     * @param string $is_in Is-in string for 'is_in' evaluation
2581
     * @param string $table Table name the eval is evaluated on
2582
     * @return array Modified $value in key 'value' or empty array
2583
     * @internal should only be used from within DataHandler
2584
     */
2585
    public function checkValue_input_Eval($value, $evalArray, $is_in, string $table = ''): array
2586
    {
2587
        $res = [];
2588
        $set = true;
2589
        foreach ($evalArray as $func) {
2590
            switch ($func) {
2591
                case 'int':
2592
                case 'year':
2593
                    $value = (int)$value;
2594
                    break;
2595
                case 'time':
2596
                case 'timesec':
2597
                    // If $value is a pure integer we have the number of seconds, we can store that directly
2598
                    if ($value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2599
                        // $value is an ISO 8601 date
2600
                        $value = (new \DateTime($value))->getTimestamp();
2601
                    }
2602
                    break;
2603
                case 'date':
2604
                case 'datetime':
2605
                    // If $value is a pure integer we have the number of seconds, we can store that directly
2606
                    if ($value !== null && $value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
2607
                        // The value we receive from JS is an ISO 8601 date, which is always in UTC. (the JS code works like that, on purpose!)
2608
                        // For instance "1999-11-11T11:11:11Z"
2609
                        // Since the user actually specifies the time in the server's local time, we need to mangle this
2610
                        // to reflect the server TZ. So we make this 1999-11-11T11:11:11+0200 (assuming Europe/Vienna here)
2611
                        // In the database we store the date in UTC (1999-11-11T09:11:11Z), hence we take the timestamp of this converted value.
2612
                        // For achieving this we work with timestamps only (which are UTC) and simply adjust it for the
2613
                        // TZ difference.
2614
                        try {
2615
                            // Make the date from JS a timestamp
2616
                            $value = (new \DateTime($value))->getTimestamp();
2617
                        } catch (\Exception $e) {
2618
                            // set the default timezone value to achieve the value of 0 as a result
2619
                            $value = (int)date('Z', 0);
2620
                        }
2621
2622
                        // @todo this hacky part is problematic when it comes to times around DST switch! Add test to prove that this is broken.
2623
                        $value -= date('Z', $value);
2624
                    }
2625
                    break;
2626
                case 'double2':
2627
                    $value = preg_replace('/[^0-9,\\.-]/', '', $value);
2628
                    $negative = $value[0] === '-';
2629
                    $value = strtr($value, [',' => '.', '-' => '']);
2630
                    if (strpos($value, '.') === false) {
2631
                        $value .= '.0';
2632
                    }
2633
                    $valueArray = explode('.', $value);
2634
                    $dec = array_pop($valueArray);
2635
                    $value = implode('', $valueArray) . '.' . $dec;
2636
                    if ($negative) {
2637
                        $value *= -1;
2638
                    }
2639
                    $value = number_format($value, 2, '.', '');
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type double expected by parameter $number of number_format(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2639
                    $value = number_format(/** @scrutinizer ignore-type */ $value, 2, '.', '');
Loading history...
2640
                    break;
2641
                case 'md5':
2642
                    if (strlen($value) !== 32) {
2643
                        $set = false;
2644
                    }
2645
                    break;
2646
                case 'trim':
2647
                    $value = trim($value);
2648
                    break;
2649
                case 'upper':
2650
                    $value = mb_strtoupper($value, 'utf-8');
2651
                    break;
2652
                case 'lower':
2653
                    $value = mb_strtolower($value, 'utf-8');
2654
                    break;
2655
                case 'required':
2656
                    if (!isset($value) || $value === '') {
2657
                        $set = false;
2658
                    }
2659
                    break;
2660
                case 'is_in':
2661
                    $c = mb_strlen($value);
2662
                    if ($c) {
2663
                        $newVal = '';
2664
                        for ($a = 0; $a < $c; $a++) {
2665
                            $char = mb_substr($value, $a, 1);
2666
                            if (mb_strpos($is_in, $char) !== false) {
2667
                                $newVal .= $char;
2668
                            }
2669
                        }
2670
                        $value = $newVal;
2671
                    }
2672
                    break;
2673
                case 'nospace':
2674
                    $value = str_replace(' ', '', $value);
2675
                    break;
2676
                case 'alpha':
2677
                    $value = preg_replace('/[^a-zA-Z]/', '', $value);
2678
                    break;
2679
                case 'num':
2680
                    $value = preg_replace('/[^0-9]/', '', $value);
2681
                    break;
2682
                case 'alphanum':
2683
                    $value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
2684
                    break;
2685
                case 'alphanum_x':
2686
                    $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
2687
                    break;
2688
                case 'domainname':
2689
                    if (!preg_match('/^[a-z0-9.\\-]*$/i', $value)) {
2690
                        $value = (string)HttpUtility::idn_to_ascii($value);
2691
                    }
2692
                    break;
2693
                case 'email':
2694
                    if ((string)$value !== '') {
2695
                        $this->checkValue_input_ValidateEmail($value, $set);
2696
                    }
2697
                    break;
2698
                case 'saltedPassword':
2699
                    // An incoming value is either the salted password if the user did not change existing password
2700
                    // when submitting the form, or a plaintext new password that needs to be turned into a salted password now.
2701
                    // The strategy is to see if a salt instance can be created from the incoming value. If so,
2702
                    // no new password was submitted and we keep the value. If no salting instance can be created,
2703
                    // incoming value must be a new plain text value that needs to be hashed.
2704
                    $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
2705
                    $mode = $table === 'fe_users' ? 'FE' : 'BE';
2706
                    try {
2707
                        $hashFactory->get($value, $mode);
2708
                    } catch (InvalidPasswordHashException $e) {
2709
                        // We got no salted password instance, incoming value must be a new plaintext password
2710
                        // Get an instance of the current configured salted password strategy and hash the value
2711
                        $newHashInstance = $hashFactory->getDefaultHashInstance($mode);
2712
                        $value = $newHashInstance->getHashedPassword($value);
2713
                    }
2714
                    break;
2715
                default:
2716
                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
2717
                        if (class_exists($func)) {
2718
                            $evalObj = GeneralUtility::makeInstance($func);
2719
                            if (method_exists($evalObj, 'evaluateFieldValue')) {
2720
                                $value = $evalObj->evaluateFieldValue($value, $is_in, $set);
2721
                            }
2722
                        }
2723
                    }
2724
            }
2725
        }
2726
        if ($set) {
2727
            $res['value'] = $value;
2728
        }
2729
        return $res;
2730
    }
2731
2732
    /**
2733
     * If $value is not a valid e-mail address,
2734
     * $set will be set to false and a flash error
2735
     * message will be added
2736
     *
2737
     * @param string $value Value to evaluate
2738
     * @param bool $set TRUE if an update should be done
2739
     * @throws \InvalidArgumentException
2740
     * @throws \TYPO3\CMS\Core\Exception
2741
     */
2742
    protected function checkValue_input_ValidateEmail($value, &$set)
2743
    {
2744
        if (GeneralUtility::validEmail($value)) {
2745
            return;
2746
        }
2747
2748
        $set = false;
2749
        /** @var FlashMessage $message */
2750
        $message = GeneralUtility::makeInstance(
2751
            FlashMessage::class,
2752
            sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.invalidEmail'), $value),
2753
            '', // header is optional
2754
            FlashMessage::ERROR,
2755
            true // whether message should be stored in session
2756
        );
2757
        /** @var FlashMessageService $flashMessageService */
2758
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2759
        $flashMessageService->getMessageQueueByIdentifier()->enqueue($message);
2760
    }
2761
2762
    /**
2763
     * Returns data for group/db and select fields
2764
     *
2765
     * @param array $valueArray Current value array
2766
     * @param array $tcaFieldConf TCA field config
2767
     * @param int $id Record id, used for look-up of MM relations (local_uid)
2768
     * @param string $status Status string ('update' or 'new')
2769
     * @param string $type The type, either 'select', 'group' or 'inline'
2770
     * @param string $currentTable Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2771
     * @param string $currentField field name, needs to be set for writing to sys_history
2772
     * @return array Modified value array
2773
     * @internal should only be used from within DataHandler
2774
     */
2775
    public function checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, $type, $currentTable, $currentField)
2776
    {
2777
        if ($type === 'group') {
2778
            $tables = $tcaFieldConf['allowed'];
2779
        } elseif (!empty($tcaFieldConf['special']) && $tcaFieldConf['special'] === 'languages') {
2780
            $tables = 'sys_language';
2781
        } else {
2782
            $tables = $tcaFieldConf['foreign_table'];
2783
        }
2784
        $prep = $type === 'group' ? $tcaFieldConf['prepend_tname'] : '';
2785
        $newRelations = implode(',', $valueArray);
2786
        /** @var RelationHandler $dbAnalysis */
2787
        $dbAnalysis = $this->createRelationHandlerInstance();
2788
        $dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2789
        $dbAnalysis->start($newRelations, $tables, '', 0, $currentTable, $tcaFieldConf);
2790
        if ($tcaFieldConf['MM']) {
2791
            // convert submitted items to use version ids instead of live ids
2792
            // (only required for MM relations in a workspace context)
2793
            $dbAnalysis->convertItemArray();
2794
            if ($status === 'update') {
2795
                /** @var RelationHandler $oldRelations_dbAnalysis */
2796
                $oldRelations_dbAnalysis = $this->createRelationHandlerInstance();
2797
                $oldRelations_dbAnalysis->registerNonTableValues = !empty($tcaFieldConf['allowNonIdValues']);
2798
                // Db analysis with $id will initialize with the existing relations
2799
                $oldRelations_dbAnalysis->start('', $tables, $tcaFieldConf['MM'], $id, $currentTable, $tcaFieldConf);
2800
                $oldRelations = implode(',', $oldRelations_dbAnalysis->getValueArray());
2801
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, $prep);
0 ignored issues
show
Bug introduced by
It seems like $prep can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\RelationHandler::writeMM() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2801
                $dbAnalysis->writeMM($tcaFieldConf['MM'], $id, /** @scrutinizer ignore-type */ $prep);
Loading history...
2802
                if ($oldRelations != $newRelations) {
2803
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = $oldRelations;
2804
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = $newRelations;
2805
                } else {
2806
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['oldRecord'][$currentField] = '';
2807
                    $this->mmHistoryRecords[$currentTable . ':' . $id]['newRecord'][$currentField] = '';
2808
                }
2809
            } else {
2810
                $this->dbAnalysisStore[] = [$dbAnalysis, $tcaFieldConf['MM'], $id, $prep, $currentTable];
2811
            }
2812
            $valueArray = $dbAnalysis->countItems();
2813
        } else {
2814
            $valueArray = $dbAnalysis->getValueArray($prep);
0 ignored issues
show
Bug introduced by
It seems like $prep can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2814
            $valueArray = $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prep);
Loading history...
2815
        }
2816
        // Here we should see if 1) the records exist anymore, 2) which are new and check if the BE_USER has read-access to the new ones.
2817
        return $valueArray;
2818
    }
2819
2820
    /**
2821
     * Explodes the $value, which is a list of files/uids (group select)
2822
     *
2823
     * @param string $value Input string, comma separated values. For each part it will also be detected if a '|' is found and the first part will then be used if that is the case. Further the value will be rawurldecoded.
2824
     * @return array The value array.
2825
     * @internal should only be used from within DataHandler
2826
     */
2827
    public function checkValue_group_select_explodeSelectGroupValue($value)
2828
    {
2829
        $valueArray = GeneralUtility::trimExplode(',', $value, true);
2830
        foreach ($valueArray as &$newVal) {
2831
            $temp = explode('|', $newVal, 2);
2832
            $newVal = str_replace(['|', ','], '', rawurldecode($temp[0]));
2833
        }
2834
        unset($newVal);
2835
        return $valueArray;
2836
    }
2837
2838
    /**
2839
     * Starts the processing the input data for flexforms. This will traverse all sheets / languages and for each it will traverse the sub-structure.
2840
     * See checkValue_flex_procInData_travDS() for more details.
2841
     * WARNING: Currently, it traverses based on the actual _data_ array and NOT the _structure_. This means that values for non-valid fields, lKey/vKey/sKeys will be accepted! For traversal of data with a call back function you should rather use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
2842
     *
2843
     * @param array $dataPart The 'data' part of the INPUT flexform data
2844
     * @param array $dataPart_current The 'data' part of the CURRENT flexform data
2845
     * @param array $uploadedFiles The uploaded files for the 'data' part of the INPUT flexform data
2846
     * @param array $dataStructure Data structure for the form (might be sheets or not). Only values in the data array which has a configuration in the data structure will be processed.
2847
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions
2848
     * @param string $callBackFunc Optional call back function, see checkValue_flex_procInData_travDS()  DEPRECATED, use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools instead for traversal!
2849
     * @param array $workspaceOptions
2850
     * @return array The modified 'data' part.
2851
     * @see checkValue_flex_procInData_travDS()
2852
     * @internal should only be used from within DataHandler
2853
     */
2854
    public function checkValue_flex_procInData($dataPart, $dataPart_current, $uploadedFiles, $dataStructure, $pParams, $callBackFunc = '', array $workspaceOptions = [])
2855
    {
2856
        if (is_array($dataPart)) {
0 ignored issues
show
introduced by
The condition is_array($dataPart) is always true.
Loading history...
2857
            foreach ($dataPart as $sKey => $sheetDef) {
2858
                if (isset($dataStructure['sheets'][$sKey]) && is_array($dataStructure['sheets'][$sKey]) && is_array($sheetDef)) {
2859
                    foreach ($sheetDef as $lKey => $lData) {
2860
                        $this->checkValue_flex_procInData_travDS(
2861
                            $dataPart[$sKey][$lKey],
2862
                            $dataPart_current[$sKey][$lKey],
2863
                            $uploadedFiles[$sKey][$lKey],
2864
                            $dataStructure['sheets'][$sKey]['ROOT']['el'],
2865
                            $pParams,
2866
                            $callBackFunc,
2867
                            $sKey . '/' . $lKey . '/',
2868
                            $workspaceOptions
2869
                        );
2870
                    }
2871
                }
2872
            }
2873
        }
2874
        return $dataPart;
2875
    }
2876
2877
    /**
2878
     * Processing of the sheet/language data array
2879
     * When it finds a field with a value the processing is done by ->checkValue_SW() by default but if a call back function name is given that method in this class will be called for the processing instead.
2880
     *
2881
     * @param array $dataValues New values (those being processed): Multidimensional Data array for sheet/language, passed by reference!
2882
     * @param array $dataValues_current Current values: Multidimensional Data array. May be empty array() if not needed (for callBackFunctions)
2883
     * @param array $uploadedFiles Uploaded files array for sheet/language. May be empty array() if not needed (for callBackFunctions)
2884
     * @param array $DSelements Data structure which fits the data array
2885
     * @param array $pParams A set of parameters to pass through for the calling of the evaluation functions / call back function
2886
     * @param string $callBackFunc Call back function, default is checkValue_SW(). If $this->callBackObj is set to an object, the callback function in that object is called instead.
2887
     * @param string $structurePath
2888
     * @param array $workspaceOptions
2889
     * @see checkValue_flex_procInData()
2890
     * @internal should only be used from within DataHandler
2891
     */
2892
    public function checkValue_flex_procInData_travDS(&$dataValues, $dataValues_current, $uploadedFiles, $DSelements, $pParams, $callBackFunc, $structurePath, array $workspaceOptions = [])
2893
    {
2894
        if (!is_array($DSelements)) {
0 ignored issues
show
introduced by
The condition is_array($DSelements) is always true.
Loading history...
2895
            return;
2896
        }
2897
2898
        // For each DS element:
2899
        foreach ($DSelements as $key => $dsConf) {
2900
            // Array/Section:
2901
            if ($DSelements[$key]['type'] === 'array') {
2902
                if (!is_array($dataValues[$key]['el'])) {
2903
                    continue;
2904
                }
2905
2906
                if ($DSelements[$key]['section']) {
2907
                    foreach ($dataValues[$key]['el'] as $ik => $el) {
2908
                        if (!is_array($el)) {
2909
                            continue;
2910
                        }
2911
2912
                        if (!is_array($dataValues_current[$key]['el'])) {
2913
                            $dataValues_current[$key]['el'] = [];
2914
                        }
2915
                        $theKey = key($el);
2916
                        if (!is_array($dataValues[$key]['el'][$ik][$theKey]['el'])) {
2917
                            continue;
2918
                        }
2919
2920
                        $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'][$ik][$theKey]['el'], is_array($dataValues_current[$key]['el'][$ik]) ? $dataValues_current[$key]['el'][$ik][$theKey]['el'] : [], $uploadedFiles[$key]['el'][$ik][$theKey]['el'], $DSelements[$key]['el'][$theKey]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/' . $ik . '/' . $theKey . '/el/', $workspaceOptions);
2921
                    }
2922
                } else {
2923
                    if (!isset($dataValues[$key]['el'])) {
2924
                        $dataValues[$key]['el'] = [];
2925
                    }
2926
                    $this->checkValue_flex_procInData_travDS($dataValues[$key]['el'], $dataValues_current[$key]['el'], $uploadedFiles[$key]['el'], $DSelements[$key]['el'], $pParams, $callBackFunc, $structurePath . $key . '/el/', $workspaceOptions);
2927
                }
2928
            } else {
2929
                // When having no specific sheets, it's "TCEforms.config", when having a sheet, it's just "config"
2930
                $fieldConfiguration = $dsConf['TCEforms']['config'] ?? $dsConf['config'] ?? null;
2931
                // init with value from config for passthrough fields
2932
                if (!empty($fieldConfiguration['type']) && $fieldConfiguration['type'] === 'passthrough') {
2933
                    if (!empty($dataValues_current[$key]['vDEF'])) {
2934
                        // If there is existing value, keep it
2935
                        $dataValues[$key]['vDEF'] = $dataValues_current[$key]['vDEF'];
2936
                    } elseif (
2937
                        !empty($fieldConfiguration['default'])
2938
                        && isset($pParams[1])
2939
                        && !MathUtility::canBeInterpretedAsInteger($pParams[1])
2940
                    ) {
2941
                        // If is new record and a default is specified for field, use it.
2942
                        $dataValues[$key]['vDEF'] = $fieldConfiguration['default'];
2943
                    }
2944
                }
2945
                if (!is_array($fieldConfiguration) || !is_array($dataValues[$key])) {
2946
                    continue;
2947
                }
2948
2949
                foreach ($dataValues[$key] as $vKey => $data) {
2950
                    if ($callBackFunc) {
2951
                        if (is_object($this->callBackObj)) {
2952
                            $res = $this->callBackObj->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
2953
                        } else {
2954
                            $res = $this->{$callBackFunc}($pParams, $fieldConfiguration, $dataValues[$key][$vKey], $dataValues_current[$key][$vKey], $uploadedFiles[$key][$vKey], $structurePath . $key . '/' . $vKey . '/', $workspaceOptions);
2955
                        }
2956
                    } else {
2957
                        // Default
2958
                        [$CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID] = $pParams;
2959
2960
                        $additionalData = [
2961
                            'flexFormId' => $CVrecFID,
2962
                            'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
2963
                        ];
2964
2965
                        $res = $this->checkValue_SW([], $dataValues[$key][$vKey], $fieldConfiguration, $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
2966
                    }
2967
                    // Adding the value:
2968
                    if (isset($res['value'])) {
2969
                        $dataValues[$key][$vKey] = $res['value'];
2970
                    }
2971
                    // Finally, check if new and old values are different (or no .vDEFbase value is found) and if so, we record the vDEF value for diff'ing.
2972
                    // We do this after $dataValues has been updated since I expect that $dataValues_current holds evaluated values from database (so this must be the right value to compare with).
2973
                    if (mb_substr($vKey, -9) !== '.vDEFbase') {
2974
                        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && $vKey !== 'vDEF' && ((string)$dataValues[$key][$vKey] !== (string)$dataValues_current[$key][$vKey] || !isset($dataValues_current[$key][$vKey . '.vDEFbase']))) {
2975
                            // Now, check if a vDEF value is submitted in the input data, if so we expect this has been processed prior to this operation (normally the case since those fields are higher in the form) and we can use that:
2976
                            if (isset($dataValues[$key]['vDEF'])) {
2977
                                $diffValue = $dataValues[$key]['vDEF'];
2978
                            } else {
2979
                                // If not found (for translators with no access to the default language) we use the one from the current-value data set:
2980
                                $diffValue = $dataValues_current[$key]['vDEF'];
2981
                            }
2982
                            // Setting the reference value for vDEF for this translation. This will be used for translation tools to make a diff between the vDEF and vDEFbase to see if an update would be fitting.
2983
                            $dataValues[$key][$vKey . '.vDEFbase'] = $diffValue;
2984
                        }
2985
                    }
2986
                }
2987
            }
2988
        }
2989
    }
2990
2991
    /**
2992
     * Returns data for inline fields.
2993
     *
2994
     * @param array $valueArray Current value array
2995
     * @param array $tcaFieldConf TCA field config
2996
     * @param int $id Record id
2997
     * @param string $status Status string ('update' or 'new')
2998
     * @param string $table Table name, needs to be passed to \TYPO3\CMS\Core\Database\RelationHandler
2999
     * @param string $field The current field the values are modified for
3000
     * @param array $additionalData Additional data to be forwarded to sub-processors
3001
     * @return string Modified values
3002
     */
3003
    protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = null)
3004
    {
3005
        $foreignTable = $tcaFieldConf['foreign_table'];
3006
        $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
3007
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3008
        /** @var RelationHandler $dbAnalysis */
3009
        $dbAnalysis = $this->createRelationHandlerInstance();
3010
        $dbAnalysis->start(implode(',', $valueArray), $foreignTable, '', 0, $table, $tcaFieldConf);
3011
        // IRRE with a pointer field (database normalization):
3012
        if ($tcaFieldConf['foreign_field']) {
3013
            // if the record was imported, sorting was also imported, so skip this
3014
            $skipSorting = (bool)$this->callFromImpExp;
3015
            // update record in intermediate table (sorting & pointer uid to parent record)
3016
            $dbAnalysis->writeForeignField($tcaFieldConf, $id, 0, $skipSorting);
3017
            $newValue = $dbAnalysis->countItems(false);
3018
        } elseif ($this->getInlineFieldType($tcaFieldConf) === 'mm') {
3019
            // In order to fully support all the MM stuff, directly call checkValue_group_select_processDBdata instead of repeating the needed code here
3020
            $valueArray = $this->checkValue_group_select_processDBdata($valueArray, $tcaFieldConf, $id, $status, 'select', $table, $field);
3021
            $newValue = $valueArray[0];
3022
        } else {
3023
            $valueArray = $dbAnalysis->getValueArray();
3024
            // Checking that the number of items is correct:
3025
            $valueArray = $this->checkValue_checkMax($tcaFieldConf, $valueArray);
3026
            $newValue = $this->castReferenceValue(implode(',', $valueArray), $tcaFieldConf);
3027
        }
3028
        return $newValue;
3029
    }
3030
3031
    /*********************************************
3032
     *
3033
     * PROCESSING COMMANDS
3034
     *
3035
     ********************************************/
3036
    /**
3037
     * Processing the cmd-array
3038
     * See "TYPO3 Core API" for a description of the options.
3039
     *
3040
     * @return void|bool
3041
     */
3042
    public function process_cmdmap()
3043
    {
3044
        // Editing frozen:
3045
        if ($this->BE_USER->workspace !== 0 && $this->BE_USER->workspaceRec['freeze']) {
3046
            $this->newlog('All editing in this workspace has been frozen!', SystemLogErrorClassification::USER_ERROR);
3047
            return false;
3048
        }
3049
        // Hook initialization:
3050
        $hookObjectsArr = [];
3051
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
3052
            $hookObj = GeneralUtility::makeInstance($className);
3053
            if (method_exists($hookObj, 'processCmdmap_beforeStart')) {
3054
                $hookObj->processCmdmap_beforeStart($this);
3055
            }
3056
            $hookObjectsArr[] = $hookObj;
3057
        }
3058
        $pasteDatamap = [];
3059
        // Traverse command map:
3060
        foreach ($this->cmdmap as $table => $_) {
3061
            // Check if the table may be modified!
3062
            $modifyAccessList = $this->checkModifyAccessList($table);
3063
            if (!$modifyAccessList) {
3064
                $this->log($table, 0, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to modify table \'%s\' without permission', 1, [$table]);
3065
            }
3066
            // Check basic permissions and circumstances:
3067
            if (!isset($GLOBALS['TCA'][$table]) || $this->tableReadOnly($table) || !is_array($this->cmdmap[$table]) || !$modifyAccessList) {
3068
                continue;
3069
            }
3070
3071
            // Traverse the command map:
3072
            foreach ($this->cmdmap[$table] as $id => $incomingCmdArray) {
3073
                if (!is_array($incomingCmdArray)) {
3074
                    continue;
3075
                }
3076
3077
                if ($table === 'pages') {
3078
                    // for commands on pages do a pagetree-refresh
3079
                    $this->pagetreeNeedsRefresh = true;
3080
                }
3081
3082
                foreach ($incomingCmdArray as $command => $value) {
3083
                    $pasteUpdate = false;
3084
                    if (is_array($value) && isset($value['action']) && $value['action'] === 'paste') {
3085
                        // Extended paste command: $command is set to "move" or "copy"
3086
                        // $value['update'] holds field/value pairs which should be updated after copy/move operation
3087
                        // $value['target'] holds original $value (target of move/copy)
3088
                        $pasteUpdate = $value['update'];
3089
                        $value = $value['target'];
3090
                    }
3091
                    foreach ($hookObjectsArr as $hookObj) {
3092
                        if (method_exists($hookObj, 'processCmdmap_preProcess')) {
3093
                            $hookObj->processCmdmap_preProcess($command, $table, $id, $value, $this, $pasteUpdate);
3094
                        }
3095
                    }
3096
                    // Init copyMapping array:
3097
                    // Must clear this array before call from here to those functions:
3098
                    // Contains mapping information between new and old id numbers.
3099
                    $this->copyMappingArray = [];
3100
                    // process the command
3101
                    $commandIsProcessed = false;
3102
                    foreach ($hookObjectsArr as $hookObj) {
3103
                        if (method_exists($hookObj, 'processCmdmap')) {
3104
                            $hookObj->processCmdmap($command, $table, $id, $value, $commandIsProcessed, $this, $pasteUpdate);
3105
                        }
3106
                    }
3107
                    // Only execute default commands if a hook hasn't been processed the command already
3108
                    if (!$commandIsProcessed) {
3109
                        $procId = $id;
3110
                        $backupUseTransOrigPointerField = $this->useTransOrigPointerField;
3111
                        // Branch, based on command
3112
                        switch ($command) {
3113
                            case 'move':
3114
                                $this->moveRecord($table, $id, $value);
3115
                                break;
3116
                            case 'copy':
3117
                                $target = $value['target'] ?? $value;
3118
                                $ignoreLocalization = (bool)($value['ignoreLocalization'] ?? false);
3119
                                if ($table === 'pages') {
3120
                                    $this->copyPages($id, $target);
3121
                                } else {
3122
                                    $this->copyRecord($table, $id, $target, true, [], '', 0, $ignoreLocalization);
3123
                                }
3124
                                $procId = $this->copyMappingArray[$table][$id];
3125
                                break;
3126
                            case 'localize':
3127
                                $this->useTransOrigPointerField = true;
3128
                                $this->localize($table, $id, $value);
3129
                                break;
3130
                            case 'copyToLanguage':
3131
                                $this->useTransOrigPointerField = false;
3132
                                $this->localize($table, $id, $value);
3133
                                break;
3134
                            case 'inlineLocalizeSynchronize':
3135
                                $this->inlineLocalizeSynchronize($table, $id, $value);
3136
                                break;
3137
                            case 'delete':
3138
                                $this->deleteAction($table, $id);
3139
                                break;
3140
                            case 'undelete':
3141
                                $this->undeleteRecord($table, $id);
3142
                                break;
3143
                        }
3144
                        $this->useTransOrigPointerField = $backupUseTransOrigPointerField;
3145
                        if (is_array($pasteUpdate)) {
3146
                            $pasteDatamap[$table][$procId] = $pasteUpdate;
3147
                        }
3148
                    }
3149
                    foreach ($hookObjectsArr as $hookObj) {
3150
                        if (method_exists($hookObj, 'processCmdmap_postProcess')) {
3151
                            $hookObj->processCmdmap_postProcess($command, $table, $id, $value, $this, $pasteUpdate, $pasteDatamap);
3152
                        }
3153
                    }
3154
                    // Merging the copy-array info together for remapping purposes.
3155
                    ArrayUtility::mergeRecursiveWithOverrule($this->copyMappingArray_merged, $this->copyMappingArray);
3156
                }
3157
            }
3158
        }
3159
        /** @var DataHandler $copyTCE */
3160
        $copyTCE = $this->getLocalTCE();
3161
        $copyTCE->start($pasteDatamap, [], $this->BE_USER);
3162
        $copyTCE->process_datamap();
3163
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3164
        unset($copyTCE);
3165
3166
        // Finally, before exit, check if there are ID references to remap.
3167
        // This might be the case if versioning or copying has taken place!
3168
        $this->remapListedDBRecords();
3169
        $this->processRemapStack();
3170
        foreach ($hookObjectsArr as $hookObj) {
3171
            if (method_exists($hookObj, 'processCmdmap_afterFinish')) {
3172
                $hookObj->processCmdmap_afterFinish($this);
3173
            }
3174
        }
3175
        if ($this->isOuterMostInstance()) {
3176
            $this->referenceIndexUpdater->update();
3177
            $this->processClearCacheQueue();
3178
            $this->resetNestedElementCalls();
3179
        }
3180
    }
3181
3182
    /*********************************************
3183
     *
3184
     * Cmd: Copying
3185
     *
3186
     ********************************************/
3187
    /**
3188
     * Copying a single record
3189
     *
3190
     * @param string $table Element table
3191
     * @param int $uid Element UID
3192
     * @param int $destPid >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
3193
     * @param bool $first Is a flag set, if the record copied is NOT a 'slave' to another record copied. That is, if this record was asked to be copied in the cmd-array
3194
     * @param array $overrideValues Associative array with field/value pairs to override directly. Notice; Fields must exist in the table record and NOT be among excluded fields!
3195
     * @param string $excludeFields Commalist of fields to exclude from the copy process (might get default values)
3196
     * @param int $language Language ID (from sys_language table)
3197
     * @param bool $ignoreLocalization If TRUE, any localization routine is skipped
3198
     * @return int|null ID of new record, if any
3199
     * @internal should only be used from within DataHandler
3200
     */
3201
    public function copyRecord($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '', $language = 0, $ignoreLocalization = false)
3202
    {
3203
        $uid = ($origUid = (int)$uid);
3204
        // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
3205
        if (empty($GLOBALS['TCA'][$table]) || $uid === 0) {
3206
            return null;
3207
        }
3208
        if ($this->isRecordCopied($table, $uid)) {
3209
            return null;
3210
        }
3211
3212
        // Fetch record with permission check
3213
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3214
3215
        // This checks if the record can be selected which is all that a copy action requires.
3216
        if ($row === false) {
3217
            $this->log($table, $uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy record "%s:%s" which does not exist or you do not have permission to read', -1, [$table, $uid]);
3218
            return null;
3219
        }
3220
3221
        // NOT using \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid() because we need the real pid - not the ID of a page, if the input is a page...
3222
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
3223
3224
        // Check if table is allowed on destination page
3225
        if (!$this->isTableAllowedForThisPage($tscPID, $table)) {
3226
            $this->log($table, $uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert record "%s:%s" on a page (%s) that can\'t store record type.', -1, [$table, $uid, $tscPID]);
3227
            return null;
3228
        }
3229
3230
        $fullLanguageCheckNeeded = $table !== 'pages';
3231
        // Used to check language and general editing rights
3232
        if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded)) {
3233
            $this->log($table, $uid, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy record "%s:%s" without having permissions to do so. [' . $this->BE_USER->errorMsg . '].', -1, [$table, $uid]);
3234
            return null;
3235
        }
3236
3237
        $data = [];
3238
        $nonFields = array_unique(GeneralUtility::trimExplode(',', 'uid,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,t3ver_oid,t3ver_wsid,t3ver_state,t3ver_stage,' . $excludeFields, true));
3239
        BackendUtility::workspaceOL($table, $row, $this->BE_USER->workspace);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type true; however, parameter $row of TYPO3\CMS\Backend\Utilit...dUtility::workspaceOL() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3239
        BackendUtility::workspaceOL($table, /** @scrutinizer ignore-type */ $row, $this->BE_USER->workspace);
Loading history...
3240
        $row = BackendUtility::purgeComputedPropertiesFromRecord($row);
3241
3242
        // Initializing:
3243
        $theNewID = StringUtility::getUniqueId('NEW');
3244
        $enableField = isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) ? $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] : '';
3245
        $headerField = $GLOBALS['TCA'][$table]['ctrl']['label'];
3246
        // Getting "copy-after" fields if applicable:
3247
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, abs($destPid), 0) : [];
0 ignored issues
show
Bug introduced by
It seems like abs($destPid) can also be of type double; however, parameter $prevUid of TYPO3\CMS\Core\DataHandl...ixCopyAfterDuplFields() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3247
        $copyAfterFields = $destPid < 0 ? $this->fixCopyAfterDuplFields($table, $uid, /** @scrutinizer ignore-type */ abs($destPid), 0) : [];
Loading history...
3248
        // Page TSconfig related:
3249
        $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
3250
        $tE = $this->getTableEntries($table, $TSConfig);
3251
        // Traverse ALL fields of the selected record:
3252
        foreach ($row as $field => $value) {
3253
            if (!in_array($field, $nonFields, true)) {
3254
                // Get TCA configuration for the field:
3255
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3256
                // Preparation/Processing of the value:
3257
                // "pid" is hardcoded of course:
3258
                // isset() won't work here, since values can be NULL in each of the arrays
3259
                // except setDefaultOnCopyArray, since we exploded that from a string
3260
                if ($field === 'pid') {
3261
                    $value = $destPid;
3262
                } elseif (array_key_exists($field, $overrideValues)) {
3263
                    // Override value...
3264
                    $value = $overrideValues[$field];
3265
                } elseif (array_key_exists($field, $copyAfterFields)) {
3266
                    // Copy-after value if available:
3267
                    $value = $copyAfterFields[$field];
3268
                } else {
3269
                    // Hide at copy may override:
3270
                    if ($first && $field == $enableField && $GLOBALS['TCA'][$table]['ctrl']['hideAtCopy'] && !$this->neverHideAtCopy && !$tE['disableHideAtCopy']) {
3271
                        $value = 1;
3272
                    }
3273
                    // Prepend label on copy:
3274
                    if ($first && $field == $headerField && $GLOBALS['TCA'][$table]['ctrl']['prependAtCopy'] && !$tE['disablePrependAtCopy']) {
3275
                        $value = $this->getCopyHeader($table, $this->resolvePid($table, $destPid), $field, $this->clearPrefixFromValue($table, $value), 0);
3276
                    }
3277
                    // Processing based on the TCA config field type (files, references, flexforms...)
3278
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $tscPID, $language);
3279
                }
3280
                // Add value to array.
3281
                $data[$table][$theNewID][$field] = $value;
3282
            }
3283
        }
3284
        // Overriding values:
3285
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
3286
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
3287
        }
3288
        // Setting original UID:
3289
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3290
            $data[$table][$theNewID][$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3291
        }
3292
        // Do the copy by simply submitting the array through DataHandler:
3293
        /** @var DataHandler $copyTCE */
3294
        $copyTCE = $this->getLocalTCE();
3295
        $copyTCE->start($data, [], $this->BE_USER);
3296
        $copyTCE->process_datamap();
3297
        // Getting the new UID:
3298
        $theNewSQLID = $copyTCE->substNEWwithIDs[$theNewID];
3299
        if ($theNewSQLID) {
3300
            $this->copyMappingArray[$table][$origUid] = $theNewSQLID;
3301
            // Keep automatically versionized record information:
3302
            if (isset($copyTCE->autoVersionIdMap[$table][$theNewSQLID])) {
3303
                $this->autoVersionIdMap[$table][$theNewSQLID] = $copyTCE->autoVersionIdMap[$table][$theNewSQLID];
3304
            }
3305
        }
3306
        $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
3307
        unset($copyTCE);
3308
        if (!$ignoreLocalization && $language == 0) {
3309
            //repointing the new translation records to the parent record we just created
3310
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
3311
            if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3312
                $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = 0;
3313
            }
3314
            $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
3315
        }
3316
3317
        return $theNewSQLID;
3318
    }
3319
3320
    /**
3321
     * Copying pages
3322
     * Main function for copying pages.
3323
     *
3324
     * @param int $uid Page UID to copy
3325
     * @param int $destPid Destination PID: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
3326
     * @internal should only be used from within DataHandler
3327
     */
3328
    public function copyPages($uid, $destPid)
3329
    {
3330
        // Initialize:
3331
        $uid = (int)$uid;
3332
        $destPid = (int)$destPid;
3333
3334
        $copyTablesAlongWithPage = $this->getAllowedTablesToCopyWhenCopyingAPage();
3335
        // Begin to copy pages if we're allowed to:
3336
        if ($this->admin || in_array('pages', $copyTablesAlongWithPage, true)) {
3337
            // Copy this page we're on. And set first-flag (this will trigger that the record is hidden if that is configured)
3338
            // This method also copies the localizations of a page
3339
            $theNewRootID = $this->copySpecificPage($uid, $destPid, $copyTablesAlongWithPage, true);
3340
            // If we're going to copy recursively
3341
            if ($theNewRootID && $this->copyTree) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $theNewRootID of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3342
                // Get ALL subpages to copy (read-permissions are respected!):
3343
                $CPtable = $this->int_pageTreeInfo([], $uid, (int)$this->copyTree, $theNewRootID);
3344
                // Now copying the subpages:
3345
                foreach ($CPtable as $thePageUid => $thePagePid) {
3346
                    $newPid = $this->copyMappingArray['pages'][$thePagePid];
3347
                    if (isset($newPid)) {
3348
                        $this->copySpecificPage($thePageUid, $newPid, $copyTablesAlongWithPage);
3349
                    } else {
3350
                        $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Something went wrong during copying branch');
3351
                        break;
3352
                    }
3353
                }
3354
            }
3355
        } else {
3356
            $this->log('pages', $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to copy page without permission to this table');
3357
        }
3358
    }
3359
3360
    /**
3361
     * Compile a list of tables that should be copied along when a page is about to be copied.
3362
     *
3363
     * First, get the list that the user is allowed to modify (all if admin),
3364
     * and then check against a possible limitation within "DataHandler->copyWhichTables" if not set to "*"
3365
     * to limit the list further down
3366
     *
3367
     * @return array
3368
     */
3369
    protected function getAllowedTablesToCopyWhenCopyingAPage(): array
3370
    {
3371
        // Finding list of tables to copy.
3372
        // These are the tables, the user may modify
3373
        $copyTablesArray = $this->admin ? $this->compileAdminTables() : explode(',', $this->BE_USER->groupData['tables_modify']);
3374
        // If not all tables are allowed then make a list of allowed tables.
3375
        // That is the tables that figure in both allowed tables AND the copyTable-list
3376
        if (strpos($this->copyWhichTables, '*') === false) {
3377
            $definedTablesToCopy = GeneralUtility::trimExplode(',', $this->copyWhichTables, true);
3378
            // Pages are always allowed
3379
            $definedTablesToCopy[] = 'pages';
3380
            $definedTablesToCopy = array_flip($definedTablesToCopy);
3381
            foreach ($copyTablesArray as $k => $table) {
3382
                if (!$table || !isset($definedTablesToCopy[$table])) {
3383
                    unset($copyTablesArray[$k]);
3384
                }
3385
            }
3386
        }
3387
        $copyTablesArray = array_unique($copyTablesArray);
3388
        return $copyTablesArray;
3389
    }
3390
    /**
3391
     * Copying a single page ($uid) to $destPid and all tables in the array copyTablesArray.
3392
     *
3393
     * @param int $uid Page uid
3394
     * @param int $destPid Destination PID: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
3395
     * @param array $copyTablesArray Table on pages to copy along with the page.
3396
     * @param bool $first Is a flag set, if the record copied is NOT a 'slave' to another record copied. That is, if this record was asked to be copied in the cmd-array
3397
     * @return int|null The id of the new page, if applicable.
3398
     * @internal should only be used from within DataHandler
3399
     */
3400
    public function copySpecificPage($uid, $destPid, $copyTablesArray, $first = false)
3401
    {
3402
        // Copy the page itself:
3403
        $theNewRootID = $this->copyRecord('pages', $uid, $destPid, $first);
3404
        $currentWorkspaceId = (int)$this->BE_USER->workspace;
3405
        // If a new page was created upon the copy operation we will proceed with all the tables ON that page:
3406
        if ($theNewRootID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $theNewRootID of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3407
            foreach ($copyTablesArray as $table) {
3408
                // All records under the page is copied.
3409
                if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
3410
                    $fields = ['uid'];
3411
                    $languageField = null;
3412
                    $transOrigPointerField = null;
3413
                    $translationSourceField = null;
3414
                    if (BackendUtility::isTableLocalizable($table)) {
3415
                        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
3416
                        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3417
                        $fields[] = $languageField;
3418
                        $fields[] = $transOrigPointerField;
3419
                        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
3420
                            $translationSourceField = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3421
                            $fields[] = $translationSourceField;
3422
                        }
3423
                    }
3424
                    $isTableWorkspaceEnabled = BackendUtility::isTableWorkspaceEnabled($table);
3425
                    if ($isTableWorkspaceEnabled) {
3426
                        $fields[] = 't3ver_oid';
3427
                        $fields[] = 't3ver_state';
3428
                        $fields[] = 't3ver_wsid';
3429
                    }
3430
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3431
                    $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
3432
                    $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $currentWorkspaceId));
3433
                    $queryBuilder
3434
                        ->select(...$fields)
3435
                        ->from($table)
3436
                        ->where(
3437
                            $queryBuilder->expr()->eq(
3438
                                'pid',
3439
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
3440
                            )
3441
                        );
3442
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby'])) {
3443
                        $queryBuilder->orderBy($GLOBALS['TCA'][$table]['ctrl']['sortby'], 'DESC');
3444
                    }
3445
                    $queryBuilder->addOrderBy('uid');
3446
                    try {
3447
                        $result = $queryBuilder->execute();
3448
                        $rows = [];
3449
                        $movedLiveIds = [];
3450
                        $movedLiveRecords = [];
3451
                        while ($row = $result->fetch()) {
3452
                            if ($isTableWorkspaceEnabled && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER) {
3453
                                $movedLiveIds[(int)$row['t3ver_oid']] = (int)$row['uid'];
3454
                            }
3455
                            $rows[(int)$row['uid']] = $row;
3456
                        }
3457
                        // Resolve placeholders of workspace versions
3458
                        if (!empty($rows) && $currentWorkspaceId > 0 && $isTableWorkspaceEnabled) {
3459
                            // If a record was moved within the page, the PlainDataResolver needs the moved record
3460
                            // but not the original live version, otherwise the moved record is not considered at all.
3461
                            // For this reason, we find the live ids, where there was also a moved record in the SQL
3462
                            // query above in $movedLiveIds and now we removed them before handing them over to PlainDataResolver.
3463
                            // see changeContentSortingAndCopyDraftPage test
3464
                            foreach ($movedLiveIds as $liveId => $movePlaceHolderId) {
3465
                                if (isset($rows[$liveId])) {
3466
                                    $movedLiveRecords[$movePlaceHolderId] = $rows[$liveId];
3467
                                    unset($rows[$liveId]);
3468
                                }
3469
                            }
3470
                            $rows = array_reverse(
3471
                                $this->resolveVersionedRecords(
3472
                                    $table,
3473
                                    implode(',', $fields),
3474
                                    $GLOBALS['TCA'][$table]['ctrl']['sortby'],
3475
                                    array_keys($rows)
3476
                                ),
3477
                                true
3478
                            );
3479
                            foreach ($movedLiveRecords as $movePlaceHolderId => $liveRecord) {
3480
                                $rows[$movePlaceHolderId] = $liveRecord;
3481
                            }
3482
                        }
3483
                        if (is_array($rows)) {
3484
                            $languageSourceMap = [];
3485
                            $overrideValues = $translationSourceField ? [$translationSourceField => 0] : [];
3486
                            $doRemap = false;
3487
                            foreach ($rows as $row) {
3488
                                // Skip localized records that will be processed in
3489
                                // copyL10nOverlayRecords() on copying the default language record
3490
                                $transOrigPointer = $row[$transOrigPointerField];
3491
                                if ($row[$languageField] > 0 && $transOrigPointer > 0 && (isset($rows[$transOrigPointer]) || isset($movedLiveIds[$transOrigPointer]))) {
3492
                                    continue;
3493
                                }
3494
                                // Copying each of the underlying records...
3495
                                $newUid = $this->copyRecord($table, $row['uid'], $theNewRootID, false, $overrideValues);
3496
                                if ($translationSourceField) {
3497
                                    $languageSourceMap[$row['uid']] = $newUid;
3498
                                    if ($row[$languageField] > 0) {
3499
                                        $doRemap = true;
3500
                                    }
3501
                                }
3502
                            }
3503
                            if ($doRemap) {
3504
                                //remap is needed for records in non-default language records in the "free mode"
3505
                                $this->copy_remapTranslationSourceField($table, $rows, $languageSourceMap);
3506
                            }
3507
                        }
3508
                    } catch (DBALException $e) {
3509
                        $databaseErrorMessage = $e->getPrevious()->getMessage();
3510
                        $this->log($table, $uid, SystemLogDatabaseAction::CHECK, 0, SystemLogErrorClassification::USER_ERROR, 'An SQL error occurred: ' . $databaseErrorMessage);
3511
                    }
3512
                }
3513
            }
3514
            $this->processRemapStack();
3515
            return $theNewRootID;
3516
        }
3517
        return null;
3518
    }
3519
3520
    /**
3521
     * Copying records, but makes a "raw" copy of a record.
3522
     * Basically the only thing observed is field processing like the copying of files and correction of ids. All other fields are 1-1 copied.
3523
     * Technically the copy is made with THIS instance of the DataHandler class contrary to copyRecord() which creates a new instance and uses the processData() function.
3524
     * The copy is created by insertNewCopyVersion() which bypasses most of the regular input checking associated with processData() - maybe copyRecord() should even do this as well!?
3525
     * This function is used to create new versions of a record.
3526
     * NOTICE: DOES NOT CHECK PERMISSIONS to create! And since page permissions are just passed through and not changed to the user who executes the copy we cannot enforce permissions without getting an incomplete copy - unless we change permissions of course.
3527
     *
3528
     * @param string $table Element table
3529
     * @param int $uid Element UID
3530
     * @param int $pid Element PID (real PID, not checked)
3531
     * @param array $overrideArray Override array - must NOT contain any fields not in the table!
3532
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3533
     * @return int Returns the new ID of the record (if applicable)
3534
     * @internal should only be used from within DataHandler
3535
     */
3536
    public function copyRecord_raw($table, $uid, $pid, $overrideArray = [], array $workspaceOptions = [])
3537
    {
3538
        $uid = (int)$uid;
3539
        // Stop any actions if the record is marked to be deleted:
3540
        // (this can occur if IRRE elements are versionized and child elements are removed)
3541
        if ($this->isElementToBeDeleted($table, $uid)) {
3542
            return null;
3543
        }
3544
        // Only copy if the table is defined in TCA, a uid is given and the record wasn't copied before:
3545
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
3546
            return null;
3547
        }
3548
3549
        // Fetch record with permission check
3550
        $row = $this->recordInfoWithPermissionCheck($table, $uid, Permission::PAGE_SHOW);
3551
3552
        // This checks if the record can be selected which is all that a copy action requires.
3553
        if ($row === false) {
3554
            $this->log(
3555
                $table,
3556
                $uid,
3557
                SystemLogDatabaseAction::DELETE,
3558
                0,
3559
                SystemLogErrorClassification::USER_ERROR,
3560
                'Attempt to rawcopy/versionize record which either does not exist or you don\'t have permission to read'
3561
            );
3562
            return null;
3563
        }
3564
3565
        // Set up fields which should not be processed. They are still written - just passed through no-questions-asked!
3566
        $nonFields = ['uid', 'pid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_stage', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody'];
3567
3568
        // Merge in override array.
3569
        $row = array_merge($row, $overrideArray);
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type true; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3569
        $row = array_merge(/** @scrutinizer ignore-type */ $row, $overrideArray);
Loading history...
3570
        // Traverse ALL fields of the selected record:
3571
        foreach ($row as $field => $value) {
3572
            if (!in_array($field, $nonFields, true)) {
3573
                // Get TCA configuration for the field:
3574
                $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
3575
                if (is_array($conf)) {
3576
                    // Processing based on the TCA config field type (files, references, flexforms...)
3577
                    $value = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $pid, 0, $workspaceOptions);
3578
                }
3579
                // Add value to array.
3580
                $row[$field] = $value;
3581
            }
3582
        }
3583
        $row['pid'] = $pid;
3584
        // Setting original UID:
3585
        if ($GLOBALS['TCA'][$table]['ctrl']['origUid']) {
3586
            $row[$GLOBALS['TCA'][$table]['ctrl']['origUid']] = $uid;
3587
        }
3588
        // Do the copy by internal function
3589
        $theNewSQLID = $this->insertNewCopyVersion($table, $row, $pid);
3590
        if ($theNewSQLID) {
3591
            $this->dbAnalysisStoreExec();
3592
            $this->dbAnalysisStore = [];
3593
            return $this->copyMappingArray[$table][$uid] = $theNewSQLID;
3594
        }
3595
        return null;
3596
    }
3597
3598
    /**
3599
     * Inserts a record in the database, passing TCA configuration values through checkValue() but otherwise does NOTHING and checks nothing regarding permissions.
3600
     * Passes the "version" parameter to insertDB() so the copy will look like a new version in the log - should probably be changed or modified a bit for more broad usage...
3601
     *
3602
     * @param string $table Table name
3603
     * @param array $fieldArray Field array to insert as a record
3604
     * @param int $realPid The value of PID field.
3605
     * @return int Returns the new ID of the record (if applicable)
3606
     * @internal should only be used from within DataHandler
3607
     */
3608
    public function insertNewCopyVersion($table, $fieldArray, $realPid)
3609
    {
3610
        $id = StringUtility::getUniqueId('NEW');
3611
        // $fieldArray is set as current record.
3612
        // The point is that when new records are created as copies with flex type fields there might be a field containing information about which DataStructure to use and without that information the flexforms cannot be correctly processed.... This should be OK since the $checkValueRecord is used by the flexform evaluation only anyways...
3613
        $this->checkValue_currentRecord = $fieldArray;
3614
        // Makes sure that transformations aren't processed on the copy.
3615
        $backupDontProcessTransformations = $this->dontProcessTransformations;
3616
        $this->dontProcessTransformations = true;
3617
        // Traverse record and input-process each value:
3618
        foreach ($fieldArray as $field => $fieldValue) {
3619
            if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
3620
                // Evaluating the value.
3621
                $res = $this->checkValue($table, $field, $fieldValue, $id, 'new', $realPid, 0, $fieldArray);
3622
                if (isset($res['value'])) {
3623
                    $fieldArray[$field] = $res['value'];
3624
                }
3625
            }
3626
        }
3627
        // System fields being set:
3628
        if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3629
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
3630
        }
3631
        if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3632
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $this->userid;
3633
        }
3634
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3635
            $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
3636
        }
3637
        // Finally, insert record:
3638
        $this->insertDB($table, $id, $fieldArray, true);
3639
        // Resets dontProcessTransformations to the previous state.
3640
        $this->dontProcessTransformations = $backupDontProcessTransformations;
3641
        // Return new id:
3642
        return $this->substNEWwithIDs[$id];
3643
    }
3644
3645
    /**
3646
     * Processing/Preparing content for copyRecord() function
3647
     *
3648
     * @param string $table Table name
3649
     * @param int $uid Record uid
3650
     * @param string $field Field name being processed
3651
     * @param string $value Input value to be processed.
3652
     * @param array $row Record array
3653
     * @param array $conf TCA field configuration
3654
     * @param int $realDestPid Real page id (pid) the record is copied to
3655
     * @param int $language Language ID (from sys_language table) used in the duplicated record
3656
     * @param array $workspaceOptions Options to be forwarded if actions happen on a workspace currently
3657
     * @return array|string
3658
     * @internal
3659
     * @see copyRecord()
3660
     */
3661
    public function copyRecord_procBasedOnFieldType($table, $uid, $field, $value, $row, $conf, $realDestPid, $language = 0, array $workspaceOptions = [])
3662
    {
3663
        $inlineSubType = $this->getInlineFieldType($conf);
3664
        // Get the localization mode for the current (parent) record (keep|select):
3665
        // Register if there are references to take care of or MM is used on an inline field (no change to value):
3666
        if ($this->isReferenceField($conf) || $inlineSubType === 'mm') {
3667
            $value = $this->copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language);
3668
        } elseif ($inlineSubType !== false) {
3669
            $value = $this->copyRecord_processInline($table, $uid, $field, $value, $row, $conf, $realDestPid, $language, $workspaceOptions);
3670
        }
3671
        // For "flex" fieldtypes we need to traverse the structure for two reasons: If there are file references they have to be prepended with absolute paths and if there are database reference they MIGHT need to be remapped (still done in remapListedDBRecords())
3672
        if ($conf['type'] === 'flex') {
3673
            // Get current value array:
3674
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
3675
            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
3676
                ['config' => $conf],
3677
                $table,
3678
                $field,
3679
                $row
3680
            );
3681
            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
3682
            $currentValueArray = GeneralUtility::xml2array($value);
3683
            // Traversing the XML structure, processing files:
3684
            if (is_array($currentValueArray)) {
3685
                $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $realDestPid], 'copyRecord_flexFormCallBack', $workspaceOptions);
3686
                // Setting value as an array! -> which means the input will be processed according to the 'flex' type when the new copy is created.
3687
                $value = $currentValueArray;
3688
            }
3689
        }
3690
        return $value;
3691
    }
3692
3693
    /**
3694
     * Processes the children of an MM relation field (select, group, inline) when the parent record is copied.
3695
     *
3696
     * @param string $table
3697
     * @param int $uid
3698
     * @param string $field
3699
     * @param mixed $value
3700
     * @param array $conf
3701
     * @param string $language
3702
     * @return mixed
3703
     */
3704
    protected function copyRecord_processManyToMany($table, $uid, $field, $value, $conf, $language)
3705
    {
3706
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
3707
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
3708
        $mmTable = isset($conf['MM']) && $conf['MM'] ? $conf['MM'] : '';
3709
        $localizeForeignTable = isset($conf['foreign_table']) && BackendUtility::isTableLocalizable($conf['foreign_table']);
3710
        // Localize referenced records of select fields:
3711
        $localizingNonManyToManyFieldReferences = empty($mmTable) && $localizeForeignTable && isset($conf['localizeReferencesAtParentLocalization']) && $conf['localizeReferencesAtParentLocalization'];
3712
        /** @var RelationHandler $dbAnalysis */
3713
        $dbAnalysis = $this->createRelationHandlerInstance();
3714
        $dbAnalysis->start($value, $allowedTables, $mmTable, $uid, $table, $conf);
3715
        $purgeItems = false;
3716
        if ($language > 0 && $localizingNonManyToManyFieldReferences) {
3717
            foreach ($dbAnalysis->itemArray as $index => $item) {
3718
                // Since select fields can reference many records, check whether there's already a localization:
3719
                $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], $language);
0 ignored issues
show
Bug introduced by
$language of type string is incompatible with the type integer expected by parameter $language of TYPO3\CMS\Backend\Utilit...getRecordLocalization(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3719
                $recordLocalization = BackendUtility::getRecordLocalization($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3720
                if ($recordLocalization) {
3721
                    $dbAnalysis->itemArray[$index]['id'] = $recordLocalization[0]['uid'];
3722
                } elseif ($this->isNestedElementCallRegistered($item['table'], $item['id'], 'localize-' . (string)$language) === false) {
3723
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], $language);
0 ignored issues
show
Bug introduced by
$language of type string is incompatible with the type integer expected by parameter $language of TYPO3\CMS\Core\DataHandl...DataHandler::localize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3723
                    $dbAnalysis->itemArray[$index]['id'] = $this->localize($item['table'], $item['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3724
                }
3725
            }
3726
            $purgeItems = true;
3727
        }
3728
3729
        if ($purgeItems || $mmTable) {
3730
            $dbAnalysis->purgeItemArray();
3731
            $value = implode(',', $dbAnalysis->getValueArray($prependName));
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3731
            $value = implode(',', $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName));
Loading history...
3732
        }
3733
        // Setting the value in this array will notify the remapListedDBRecords() function that this field MAY need references to be corrected
3734
        if ($value) {
3735
            $this->registerDBList[$table][$uid][$field] = $value;
3736
        }
3737
3738
        return $value;
3739
    }
3740
3741
    /**
3742
     * Processes child records in an inline (IRRE) element when the parent record is copied.
3743
     *
3744
     * @param string $table
3745
     * @param int $uid
3746
     * @param string $field
3747
     * @param mixed $value
3748
     * @param array $row
3749
     * @param array $conf
3750
     * @param int $realDestPid
3751
     * @param string $language
3752
     * @param array $workspaceOptions
3753
     * @return string
3754
     */
3755
    protected function copyRecord_processInline(
3756
        $table,
3757
        $uid,
3758
        $field,
3759
        $value,
3760
        $row,
3761
        $conf,
3762
        $realDestPid,
3763
        $language,
3764
        array $workspaceOptions
3765
    ) {
3766
        // Fetch the related child records using \TYPO3\CMS\Core\Database\RelationHandler
3767
        /** @var RelationHandler $dbAnalysis */
3768
        $dbAnalysis = $this->createRelationHandlerInstance();
3769
        $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
3770
        // Walk through the items, copy them and remember the new id:
3771
        foreach ($dbAnalysis->itemArray as $k => $v) {
3772
            $newId = null;
3773
            // If language is set and differs from original record, this isn't a copy action but a localization of our parent/ancestor:
3774
            if ($language > 0 && BackendUtility::isTableLocalizable($table) && $language != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
3775
                // Children should be localized when the parent gets localized the first time, just do it:
3776
                $newId = $this->localize($v['table'], $v['id'], $language);
0 ignored issues
show
Bug introduced by
$language of type string is incompatible with the type integer expected by parameter $language of TYPO3\CMS\Core\DataHandl...DataHandler::localize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3776
                $newId = $this->localize($v['table'], $v['id'], /** @scrutinizer ignore-type */ $language);
Loading history...
3777
            } else {
3778
                if (!MathUtility::canBeInterpretedAsInteger($realDestPid)) {
3779
                    $newId = $this->copyRecord($v['table'], $v['id'], -$v['id']);
3780
                // If the destination page id is a NEW string, keep it on the same page
3781
                } elseif ($this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3782
                    // A filled $workspaceOptions indicated that this call
3783
                    // has it's origin in previous versionizeRecord() processing
3784
                    if (!empty($workspaceOptions)) {
3785
                        // Versions use live default id, thus the "new"
3786
                        // id is the original live default child record
3787
                        $newId = $v['id'];
3788
                        $this->versionizeRecord(
3789
                            $v['table'],
3790
                            $v['id'],
3791
                            $workspaceOptions['label'] ?? 'Auto-created for WS #' . $this->BE_USER->workspace,
3792
                            $workspaceOptions['delete'] ?? false
3793
                        );
3794
                    // Otherwise just use plain copyRecord() to create placeholders etc.
3795
                    } else {
3796
                        // If a record has been copied already during this request,
3797
                        // prevent superfluous duplication and use the existing copy
3798
                        if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3799
                            $newId = $this->copyMappingArray[$v['table']][$v['id']];
3800
                        } else {
3801
                            $newId = $this->copyRecord($v['table'], $v['id'], $realDestPid);
3802
                        }
3803
                    }
3804
                } elseif ($this->BE_USER->workspace > 0 && !BackendUtility::isTableWorkspaceEnabled($v['table'])) {
3805
                    // We are in workspace context creating a new parent version and have a child table
3806
                    // that is not workspace aware. We don't do anything with this child.
3807
                    continue;
3808
                } else {
3809
                    // If a record has been copied already during this request,
3810
                    // prevent superfluous duplication and use the existing copy
3811
                    if (isset($this->copyMappingArray[$v['table']][$v['id']])) {
3812
                        $newId = $this->copyMappingArray[$v['table']][$v['id']];
3813
                    } else {
3814
                        $newId = $this->copyRecord_raw($v['table'], $v['id'], $realDestPid, [], $workspaceOptions);
3815
                    }
3816
                }
3817
            }
3818
            // If the current field is set on a page record, update the pid of related child records:
3819
            if ($table === 'pages') {
3820
                $this->registerDBPids[$v['table']][$v['id']] = $uid;
3821
            } elseif (isset($this->registerDBPids[$table][$uid])) {
3822
                $this->registerDBPids[$v['table']][$v['id']] = $this->registerDBPids[$table][$uid];
3823
            }
3824
            $dbAnalysis->itemArray[$k]['id'] = $newId;
3825
        }
3826
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
3827
        $value = implode(',', $dbAnalysis->getValueArray());
3828
        $this->registerDBList[$table][$uid][$field] = $value;
3829
3830
        return $value;
3831
    }
3832
3833
    /**
3834
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
3835
     *
3836
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
3837
     * @param array $dsConf TCA field configuration (from Data Structure XML)
3838
     * @param string $dataValue The value of the flexForm field
3839
     * @param string $_1 Not used.
3840
     * @param string $_2 Not used.
3841
     * @param string $_3 Not used.
3842
     * @param array $workspaceOptions
3843
     * @return array Result array with key "value" containing the value of the processing.
3844
     * @see copyRecord()
3845
     * @see checkValue_flex_procInData_travDS()
3846
     * @internal should only be used from within DataHandler
3847
     */
3848
    public function copyRecord_flexFormCallBack($pParams, $dsConf, $dataValue, $_1, $_2, $_3, $workspaceOptions)
3849
    {
3850
        // Extract parameters:
3851
        [$table, $uid, $field, $realDestPid] = $pParams;
3852
        // If references are set for this field, set flag so they can be corrected later (in ->remapListedDBRecords())
3853
        if (($this->isReferenceField($dsConf) || $this->getInlineFieldType($dsConf) !== false) && (string)$dataValue !== '') {
3854
            $dataValue = $this->copyRecord_procBasedOnFieldType($table, $uid, $field, $dataValue, [], $dsConf, $realDestPid, 0, $workspaceOptions);
3855
            $this->registerDBList[$table][$uid][$field] = 'FlexForm_reference';
3856
        }
3857
        // Return
3858
        return ['value' => $dataValue];
3859
    }
3860
3861
    /**
3862
     * Find l10n-overlay records and perform the requested copy action for these records.
3863
     *
3864
     * @param string $table Record Table
3865
     * @param string $uid UID of the record in the default language
3866
     * @param string $destPid Position to copy to
3867
     * @param bool $first
3868
     * @param array $overrideValues
3869
     * @param string $excludeFields
3870
     * @internal should only be used from within DataHandler
3871
     */
3872
    public function copyL10nOverlayRecords($table, $uid, $destPid, $first = false, $overrideValues = [], $excludeFields = '')
3873
    {
3874
        // There's no need to perform this for tables that are not localizable
3875
        if (!BackendUtility::isTableLocalizable($table)) {
3876
            return;
3877
        }
3878
3879
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3880
        $queryBuilder->getRestrictions()
3881
            ->removeAll()
3882
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3883
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
3884
3885
        $queryBuilder->select('*')
3886
            ->from($table)
3887
            ->where(
3888
                $queryBuilder->expr()->eq(
3889
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3890
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
3891
                )
3892
            );
3893
3894
        // Never copy the actual placeholders around, as the newly copied records are
3895
        // always created as new record / new placeholder pairs
3896
        if (BackendUtility::isTableWorkspaceEnabled($table)) {
3897
            $queryBuilder->andWhere(
3898
                $queryBuilder->expr()->neq(
3899
                    't3ver_state',
3900
                    VersionState::DELETE_PLACEHOLDER
3901
                )
3902
            );
3903
        }
3904
3905
        // If $destPid is < 0, get the pid of the record with uid equal to abs($destPid)
3906
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, $destPid);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...:getTSconfig_pidValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3906
        $tscPID = BackendUtility::getTSconfig_pidValue($table, /** @scrutinizer ignore-type */ $uid, $destPid);
Loading history...
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...:getTSconfig_pidValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3906
        $tscPID = BackendUtility::getTSconfig_pidValue($table, $uid, /** @scrutinizer ignore-type */ $destPid);
Loading history...
3907
        // Get the localized records to be copied
3908
        $l10nRecords = $queryBuilder->execute()->fetchAll();
3909
        if (is_array($l10nRecords)) {
3910
            $localizedDestPids = [];
3911
            // If $destPid < 0, then it is the uid of the original language record we are inserting after
3912
            if ($destPid < 0) {
3913
                // Get the localized records of the record we are inserting after
3914
                $queryBuilder->setParameter('pointer', abs($destPid), \PDO::PARAM_INT);
3915
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
3916
                // Index the localized record uids by language
3917
                if (is_array($destL10nRecords)) {
3918
                    foreach ($destL10nRecords as $record) {
3919
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
3920
                    }
3921
                }
3922
            }
3923
            $languageSourceMap = [
3924
                $uid => $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
3925
            ];
3926
            // Copy the localized records after the corresponding localizations of the destination record
3927
            foreach ($l10nRecords as $record) {
3928
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
3929
                if ($localizedDestPid < 0) {
3930
                    $newUid = $this->copyRecord($table, $record['uid'], $localizedDestPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
3931
                } else {
3932
                    $newUid = $this->copyRecord($table, $record['uid'], $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
0 ignored issues
show
Bug introduced by
It seems like $destPid < 0 ? $tscPID : $destPid can also be of type string; however, parameter $destPid of TYPO3\CMS\Core\DataHandl...taHandler::copyRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3932
                    $newUid = $this->copyRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid < 0 ? $tscPID : $destPid, $first, $overrideValues, $excludeFields, $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
Loading history...
3933
                }
3934
                $languageSourceMap[$record['uid']] = $newUid;
3935
            }
3936
            $this->copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap);
3937
        }
3938
    }
3939
3940
    /**
3941
     * Remap languageSource field to uids of newly created records
3942
     *
3943
     * @param string $table Table name
3944
     * @param array $l10nRecords array of localized records from the page we're copying from (source records)
3945
     * @param array $languageSourceMap array mapping source records uids to newly copied uids
3946
     */
3947
    protected function copy_remapTranslationSourceField($table, $l10nRecords, $languageSourceMap)
3948
    {
3949
        if (empty($GLOBALS['TCA'][$table]['ctrl']['translationSource']) || empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) {
3950
            return;
3951
        }
3952
        $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'];
3953
        $translationParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
3954
3955
        //We can avoid running these update queries by sorting the $l10nRecords by languageSource dependency (in copyL10nOverlayRecords)
3956
        //and first copy records depending on default record (and map the field).
3957
        foreach ($l10nRecords as $record) {
3958
            $oldSourceUid = $record[$translationSourceFieldName];
3959
            if ($oldSourceUid <= 0 && $record[$translationParentFieldName] > 0) {
3960
                //BC fix - in connected mode 'translationSource' field should not be 0
3961
                $oldSourceUid = $record[$translationParentFieldName];
3962
            }
3963
            if ($oldSourceUid > 0) {
3964
                if (empty($languageSourceMap[$oldSourceUid])) {
3965
                    // we don't have mapping information available e.g when copyRecord returned null
3966
                    continue;
3967
                }
3968
                $newFieldValue = $languageSourceMap[$oldSourceUid];
3969
                $updateFields = [
3970
                    $translationSourceFieldName => $newFieldValue
3971
                ];
3972
                GeneralUtility::makeInstance(ConnectionPool::class)
3973
                    ->getConnectionForTable($table)
3974
                    ->update($table, $updateFields, ['uid' => (int)$languageSourceMap[$record['uid']]]);
3975
                if ($this->BE_USER->workspace > 0) {
3976
                    GeneralUtility::makeInstance(ConnectionPool::class)
3977
                        ->getConnectionForTable($table)
3978
                        ->update($table, $updateFields, ['t3ver_oid' => (int)$languageSourceMap[$record['uid']], 't3ver_wsid' => $this->BE_USER->workspace]);
3979
                }
3980
            }
3981
        }
3982
    }
3983
3984
    /*********************************************
3985
     *
3986
     * Cmd: Moving, Localizing
3987
     *
3988
     ********************************************/
3989
    /**
3990
     * Moving single records
3991
     *
3992
     * @param string $table Table name to move
3993
     * @param int $uid Record uid to move
3994
     * @param int $destPid Position to move to: $destPid: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
3995
     * @internal should only be used from within DataHandler
3996
     */
3997
    public function moveRecord($table, $uid, $destPid)
3998
    {
3999
        if (!$GLOBALS['TCA'][$table]) {
4000
            return;
4001
        }
4002
4003
        // In case the record to be moved turns out to be an offline version,
4004
        // we have to find the live version and work on that one.
4005
        if ($lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $uid, 'uid')) {
4006
            $uid = $lookForLiveVersion['uid'];
4007
        }
4008
        // Initialize:
4009
        $destPid = (int)$destPid;
4010
        // Get this before we change the pid (for logging)
4011
        $propArr = $this->getRecordProperties($table, $uid);
4012
        $moveRec = $this->getRecordProperties($table, $uid, true);
4013
        // This is the actual pid of the moving to destination
4014
        $resolvedPid = $this->resolvePid($table, $destPid);
4015
        // Finding out, if the record may be moved from where it is. If the record is a non-page, then it depends on edit-permissions.
4016
        // If the record is a page, then there are two options: If the page is moved within itself,
4017
        // (same pid) it's edit-perms of the pid. If moved to another place then its both delete-perms of the pid and new-page perms on the destination.
4018
        if ($table !== 'pages' || $resolvedPid == $moveRec['pid']) {
4019
            // Edit rights for the record...
4020
            $mayMoveAccess = $this->checkRecordUpdateAccess($table, $uid);
4021
        } else {
4022
            $mayMoveAccess = $this->doesRecordExist($table, $uid, Permission::PAGE_DELETE);
4023
        }
4024
        // Finding out, if the record may be moved TO another place. Here we check insert-rights (non-pages = edit, pages = new),
4025
        // unless the pages are moved on the same pid, then edit-rights are checked
4026
        if ($table !== 'pages' || $resolvedPid != $moveRec['pid']) {
4027
            // Insert rights for the record...
4028
            $mayInsertAccess = $this->checkRecordInsertAccess($table, $resolvedPid, SystemLogDatabaseAction::MOVE);
4029
        } else {
4030
            $mayInsertAccess = $this->checkRecordUpdateAccess($table, $uid);
4031
        }
4032
        // Checking if there is anything else disallowing moving the record by checking if editing is allowed
4033
        $fullLanguageCheckNeeded = $table !== 'pages';
4034
        $mayEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, false, $fullLanguageCheckNeeded);
4035
        // If moving is allowed, begin the processing:
4036
        if (!$mayEditAccess) {
4037
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record "%s" (%s) without having permissions to do so. [' . $this->BE_USER->errorMsg . ']', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4038
            return;
4039
        }
4040
4041
        if (!$mayMoveAccess) {
4042
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) without having permissions to do so.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4043
            return;
4044
        }
4045
4046
        if (!$mayInsertAccess) {
4047
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) without having permissions to insert.', 14, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4048
            return;
4049
        }
4050
4051
        $recordWasMoved = false;
4052
        // Move the record via a hook, used e.g. for versioning
4053
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4054
            $hookObj = GeneralUtility::makeInstance($className);
4055
            if (method_exists($hookObj, 'moveRecord')) {
4056
                $hookObj->moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, $this);
4057
            }
4058
        }
4059
        // Move the record if a hook hasn't moved it yet
4060
        if (!$recordWasMoved) {
0 ignored issues
show
introduced by
The condition $recordWasMoved is always false.
Loading history...
4061
            $this->moveRecord_raw($table, $uid, $destPid);
4062
        }
4063
    }
4064
4065
    /**
4066
     * Moves a record without checking security of any sort.
4067
     * USE ONLY INTERNALLY
4068
     *
4069
     * @param string $table Table name to move
4070
     * @param int $uid Record uid to move
4071
     * @param int $destPid Position to move to: $destPid: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if
4072
     * @see moveRecord()
4073
     * @internal should only be used from within DataHandler
4074
     */
4075
    public function moveRecord_raw($table, $uid, $destPid)
4076
    {
4077
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
4078
        $origDestPid = $destPid;
4079
        // This is the actual pid of the moving to destination
4080
        $resolvedPid = $this->resolvePid($table, $destPid);
4081
        // Checking if the pid is negative, but no sorting row is defined. In that case, find the correct pid.
4082
        // Basically this check make the error message 4-13 meaning less... But you can always remove this check if you
4083
        // prefer the error instead of a no-good action (which is to move the record to its own page...)
4084
        if (($destPid < 0 && !$sortColumn) || $destPid >= 0) {
4085
            $destPid = $resolvedPid;
4086
        }
4087
        // Get this before we change the pid (for logging)
4088
        $propArr = $this->getRecordProperties($table, $uid);
4089
        $moveRec = $this->getRecordProperties($table, $uid, true);
4090
        // Prepare user defined objects (if any) for hooks which extend this function:
4091
        $hookObjectsArr = [];
4092
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass'] ?? [] as $className) {
4093
            $hookObjectsArr[] = GeneralUtility::makeInstance($className);
4094
        }
4095
        // Timestamp field:
4096
        $updateFields = [];
4097
        if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4098
            $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4099
        }
4100
4101
        // Check if this is a translation of a page, if so then it just needs to be kept "sorting" in sync
4102
        // Usually called from moveL10nOverlayRecords()
4103
        if ($table === 'pages') {
4104
            $defaultLanguagePageId = $this->getDefaultLanguagePageId((int)$uid);
4105
            if ($defaultLanguagePageId !== (int)$uid) {
4106
                $originalTranslationRecord = $this->recordInfo($table, $defaultLanguagePageId, 'pid,' . $sortColumn);
4107
                $updateFields[$sortColumn] = $originalTranslationRecord[$sortColumn];
4108
                // Ensure that the PID is always the same as the default language page
4109
                $destPid = $originalTranslationRecord['pid'];
4110
            }
4111
        }
4112
4113
        // Insert as first element on page (where uid = $destPid)
4114
        if ($destPid >= 0) {
4115
            if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4116
                // Clear cache before moving
4117
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4117
                [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4118
                $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4119
                // Setting PID
4120
                $updateFields['pid'] = $destPid;
4121
                // Table is sorted by 'sortby'
4122
                if ($sortColumn && !isset($updateFields[$sortColumn])) {
4123
                    $sortNumber = $this->getSortNumber($table, $uid, $destPid);
4124
                    $updateFields[$sortColumn] = $sortNumber;
4125
                }
4126
                // Check for child records that have also to be moved
4127
                $this->moveRecord_procFields($table, $uid, $destPid);
4128
                // Create query for update:
4129
                GeneralUtility::makeInstance(ConnectionPool::class)
4130
                    ->getConnectionForTable($table)
4131
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4132
                // Check for the localizations of that element
4133
                $this->moveL10nOverlayRecords($table, $uid, $destPid, $destPid);
4134
                // Call post processing hooks:
4135
                foreach ($hookObjectsArr as $hookObj) {
4136
                    if (method_exists($hookObj, 'moveRecord_firstElementPostProcess')) {
4137
                        $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this);
4138
                    }
4139
                }
4140
4141
                $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4142
                if ($this->enableLogging) {
4143
                    // Logging...
4144
                    $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4145
                    if ($destPid != $propArr['pid']) {
4146
                        // Logged to old page
4147
                        $newPropArr = $this->getRecordProperties($table, $uid);
4148
                        $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4149
                        $this->log($table, $uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4150
                        // Logged to new page
4151
                        $this->log($table, $uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4152
                    } else {
4153
                        // Logged to new page
4154
                        $this->log($table, $uid, SystemLogDatabaseAction::MOVE, $destPid, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4155
                    }
4156
                }
4157
                // Clear cache after moving
4158
                $this->registerRecordIdForPageCacheClearing($table, $uid);
4159
                $this->fixUniqueInPid($table, $uid);
4160
                $this->fixUniqueInSite($table, (int)$uid);
4161
                if ($table === 'pages') {
4162
                    $this->fixUniqueInSiteForSubpages((int)$uid);
4163
                }
4164
            } elseif ($this->enableLogging) {
4165
                $destPropArr = $this->getRecordProperties('pages', $destPid);
4166
                $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, [$propArr['header'], $uid, $destPropArr['header'], $destPid], $propArr['pid']);
4167
            }
4168
        } elseif ($sortColumn) {
4169
            // Put after another record
4170
            // Table is being sorted
4171
            // Save the position to which the original record is requested to be moved
4172
            $originalRecordDestinationPid = $destPid;
4173
            $sortInfo = $this->getSortNumber($table, $uid, $destPid);
4174
            // Setting the destPid to the new pid of the record.
4175
            $destPid = $sortInfo['pid'];
4176
            // If not an array, there was an error (which is already logged)
4177
            if (is_array($sortInfo)) {
4178
                if ($table !== 'pages' || $this->destNotInsideSelf($destPid, $uid)) {
4179
                    // clear cache before moving
4180
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4181
                    // We now update the pid and sortnumber (if not set for page translations)
4182
                    $updateFields['pid'] = $destPid;
4183
                    if (!isset($updateFields[$sortColumn])) {
4184
                        $updateFields[$sortColumn] = $sortInfo['sortNumber'];
4185
                    }
4186
                    // Check for child records that have also to be moved
4187
                    $this->moveRecord_procFields($table, $uid, $destPid);
4188
                    // Create query for update:
4189
                    GeneralUtility::makeInstance(ConnectionPool::class)
4190
                        ->getConnectionForTable($table)
4191
                        ->update($table, $updateFields, ['uid' => (int)$uid]);
4192
                    // Check for the localizations of that element
4193
                    $this->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
4194
                    // Call post processing hooks:
4195
                    foreach ($hookObjectsArr as $hookObj) {
4196
                        if (method_exists($hookObj, 'moveRecord_afterAnotherElementPostProcess')) {
4197
                            $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this);
4198
                        }
4199
                    }
4200
                    $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields], $this->correlationId);
4201
                    if ($this->enableLogging) {
4202
                        // Logging...
4203
                        $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4204
                        if ($destPid != $propArr['pid']) {
4205
                            // Logged to old page
4206
                            $newPropArr = $this->getRecordProperties($table, $uid);
4207
                            $newpagePropArr = $this->getRecordProperties('pages', $destPid);
4208
                            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) to page \'%s\' (%s)', 2, [$propArr['header'], $table . ':' . $uid, $newpagePropArr['header'], $newPropArr['pid']], $propArr['pid']);
4209
                            // Logged to old page
4210
                            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) from page \'%s\' (%s)', 3, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4211
                        } else {
4212
                            // Logged to old page
4213
                            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::MESSAGE, 'Moved record \'%s\' (%s) on page \'%s\' (%s)', 4, [$propArr['header'], $table . ':' . $uid, $oldpagePropArr['header'], $propArr['pid']], $destPid);
4214
                        }
4215
                    }
4216
                    // Clear cache after moving
4217
                    $this->registerRecordIdForPageCacheClearing($table, $uid);
4218
                    $this->fixUniqueInPid($table, $uid);
4219
                    $this->fixUniqueInSite($table, (int)$uid);
4220
                    if ($table === 'pages') {
4221
                        $this->fixUniqueInSiteForSubpages((int)$uid);
4222
                    }
4223
                } elseif ($this->enableLogging) {
4224
                    $destPropArr = $this->getRecordProperties('pages', $destPid);
4225
                    $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move page \'%s\' (%s) to inside of its own rootline (at page \'%s\' (%s))', 10, [$propArr['header'], $uid, $destPropArr['header'], $destPid], $propArr['pid']);
4226
                }
4227
            } else {
4228
                $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) to after another record, although the table has no sorting row.', 13, [$propArr['header'], $table . ':' . $uid], $propArr['event_pid']);
4229
            }
4230
        }
4231
    }
4232
4233
    /**
4234
     * Walk through all fields of the moved record and look for children of e.g. the inline type.
4235
     * If child records are found, they are also move to the new $destPid.
4236
     *
4237
     * @param string $table Record Table
4238
     * @param int $uid Record UID
4239
     * @param int $destPid Position to move to
4240
     * @internal should only be used from within DataHandler
4241
     */
4242
    public function moveRecord_procFields($table, $uid, $destPid)
4243
    {
4244
        $row = BackendUtility::getRecordWSOL($table, $uid);
4245
        if (is_array($row) && (int)$destPid !== (int)$row['pid']) {
4246
            $conf = $GLOBALS['TCA'][$table]['columns'];
4247
            foreach ($row as $field => $value) {
4248
                $this->moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf[$field]['config']);
4249
            }
4250
        }
4251
    }
4252
4253
    /**
4254
     * Move child records depending on the field type of the parent record.
4255
     *
4256
     * @param string $table Record Table
4257
     * @param string $uid Record UID
4258
     * @param string $destPid Position to move to
4259
     * @param string $field Record field
4260
     * @param string $value Record field value
4261
     * @param array $conf TCA configuration of current field
4262
     * @internal should only be used from within DataHandler
4263
     */
4264
    public function moveRecord_procBasedOnFieldType($table, $uid, $destPid, $field, $value, $conf)
4265
    {
4266
        $dbAnalysis = null;
4267
        if ($conf['type'] === 'inline') {
4268
            $foreign_table = $conf['foreign_table'];
4269
            $moveChildrenWithParent = !isset($conf['behaviour']['disableMovingChildrenWithParent']) || !$conf['behaviour']['disableMovingChildrenWithParent'];
4270
            if ($foreign_table && $moveChildrenWithParent) {
4271
                $inlineType = $this->getInlineFieldType($conf);
4272
                if ($inlineType === 'list' || $inlineType === 'field') {
4273
                    if ($table === 'pages') {
4274
                        // If the inline elements are related to a page record,
4275
                        // make sure they reside at that page and not at its parent
4276
                        $destPid = $uid;
4277
                    }
4278
                    $dbAnalysis = $this->createRelationHandlerInstance();
4279
                    $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $MMuid of TYPO3\CMS\Core\Database\RelationHandler::start(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4279
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
4280
                }
4281
            }
4282
        }
4283
        // Move the records
4284
        if (isset($dbAnalysis)) {
4285
            // Moving records to a positive destination will insert each
4286
            // record at the beginning, thus the order is reversed here:
4287
            foreach (array_reverse($dbAnalysis->itemArray) as $v) {
4288
                $this->moveRecord($v['table'], $v['id'], $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $destPid of TYPO3\CMS\Core\DataHandl...taHandler::moveRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4288
                $this->moveRecord($v['table'], $v['id'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4289
            }
4290
        }
4291
    }
4292
4293
    /**
4294
     * Find l10n-overlay records and perform the requested move action for these records.
4295
     *
4296
     * @param string $table Record Table
4297
     * @param string $uid Record UID
4298
     * @param string $destPid Position to move to
4299
     * @param string $originalRecordDestinationPid Position to move the original record to
4300
     * @internal should only be used from within DataHandler
4301
     */
4302
    public function moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
4303
    {
4304
        // There's no need to perform this for non-localizable tables
4305
        if (!BackendUtility::isTableLocalizable($table)) {
4306
            return;
4307
        }
4308
4309
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4310
        $queryBuilder->getRestrictions()
4311
            ->removeAll()
4312
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4313
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->BE_USER->workspace));
4314
4315
        $l10nRecords = $queryBuilder->select('*')
4316
            ->from($table)
4317
            ->where(
4318
                $queryBuilder->expr()->eq(
4319
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
4320
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT, ':pointer')
4321
                )
4322
            )
4323
            ->execute()
4324
            ->fetchAll();
4325
4326
        if (is_array($l10nRecords)) {
4327
            $localizedDestPids = [];
4328
            // If $$originalRecordDestinationPid < 0, then it is the uid of the original language record we are inserting after
4329
            if ($originalRecordDestinationPid < 0) {
4330
                // Get the localized records of the record we are inserting after
4331
                $queryBuilder->setParameter('pointer', abs($originalRecordDestinationPid), \PDO::PARAM_INT);
4332
                $destL10nRecords = $queryBuilder->execute()->fetchAll();
4333
                // Index the localized record uids by language
4334
                if (is_array($destL10nRecords)) {
4335
                    foreach ($destL10nRecords as $record) {
4336
                        $localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = -$record['uid'];
4337
                    }
4338
                }
4339
            }
4340
            // Move the localized records after the corresponding localizations of the destination record
4341
            foreach ($l10nRecords as $record) {
4342
                $localizedDestPid = (int)$localizedDestPids[$record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]];
4343
                if ($localizedDestPid < 0) {
4344
                    $this->moveRecord($table, $record['uid'], $localizedDestPid);
4345
                } else {
4346
                    $this->moveRecord($table, $record['uid'], $destPid);
0 ignored issues
show
Bug introduced by
$destPid of type string is incompatible with the type integer expected by parameter $destPid of TYPO3\CMS\Core\DataHandl...taHandler::moveRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4346
                    $this->moveRecord($table, $record['uid'], /** @scrutinizer ignore-type */ $destPid);
Loading history...
4347
                }
4348
            }
4349
        }
4350
    }
4351
4352
    /**
4353
     * Localizes a record to another system language
4354
     *
4355
     * @param string $table Table name
4356
     * @param int $uid Record uid (to be localized)
4357
     * @param int $language Language ID (from sys_language table)
4358
     * @return int|bool The uid (int) of the new translated record or FALSE (bool) if something went wrong
4359
     * @internal should only be used from within DataHandler
4360
     */
4361
    public function localize($table, $uid, $language)
4362
    {
4363
        $newId = false;
4364
        $uid = (int)$uid;
4365
        if (!$GLOBALS['TCA'][$table] || !$uid || $this->isNestedElementCallRegistered($table, $uid, 'localize-' . (string)$language) !== false) {
4366
            return false;
4367
        }
4368
4369
        $this->registerNestedElementCall($table, $uid, 'localize-' . (string)$language);
4370
        if (!$GLOBALS['TCA'][$table]['ctrl']['languageField'] || !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
4371
            $this->newlog('Localization failed; "languageField" and "transOrigPointerField" must be defined for the table ' . $table, SystemLogErrorClassification::USER_ERROR);
4372
            return false;
4373
        }
4374
        $langRec = BackendUtility::getRecord('sys_language', (int)$language, 'uid,title');
4375
        if (!$langRec) {
4376
            $this->newlog('Sys language UID "' . $language . '" not found valid!', SystemLogErrorClassification::USER_ERROR);
4377
            return false;
4378
        }
4379
4380
        if (!$this->doesRecordExist($table, $uid, Permission::PAGE_SHOW)) {
4381
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' without permission.', SystemLogErrorClassification::USER_ERROR);
4382
            return false;
4383
        }
4384
4385
        // Getting workspace overlay if possible - this will localize versions in workspace if any
4386
        $row = BackendUtility::getRecordWSOL($table, $uid);
4387
        if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
4388
            $this->newlog('Attempt to localize record ' . $table . ':' . $uid . ' that did not exist!', SystemLogErrorClassification::USER_ERROR);
4389
            return false;
4390
        }
4391
4392
        // Make sure that records which are translated from another language than the default language have a correct
4393
        // localization source set themselves, before translating them to another language.
4394
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4395
            && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
4396
            $localizationParentRecord = BackendUtility::getRecord(
4397
                $table,
4398
                $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
4399
            );
4400
            if ((int)$localizationParentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== 0) {
4401
                $this->newlog('Localization failed; Source record ' . $table . ':' . $localizationParentRecord['uid'] . ' contained a reference to an original record that is not a default record (which is strange)!', SystemLogErrorClassification::USER_ERROR);
4402
                return false;
4403
            }
4404
        }
4405
4406
        // Default language records must never have a localization parent as they are the origin of any translation.
4407
        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] !== 0
4408
            && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4409
            $this->newlog('Localization failed; Source record ' . $table . ':' . $row['uid'] . ' contained a reference to an original default record but is a default record itself (which is strange)!', SystemLogErrorClassification::USER_ERROR);
4410
            return false;
4411
        }
4412
4413
        $recordLocalizations = BackendUtility::getRecordLocalization($table, $uid, $language, 'AND pid=' . (int)$row['pid']);
4414
4415
        if (!empty($recordLocalizations)) {
4416
            $this->newlog(sprintf(
4417
                'Localization failed: there already are localizations (%s) for language %d of the "%s" record %d!',
4418
                implode(', ', array_column($recordLocalizations, 'uid')),
4419
                $language,
4420
                $table,
4421
                $uid
4422
            ), 1);
4423
            return false;
4424
        }
4425
4426
        // Initialize:
4427
        $overrideValues = [];
4428
        // Set override values:
4429
        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $langRec['uid'];
4430
        // If the translated record is a default language record, set it's uid as localization parent of the new record.
4431
        // If translating from any other language, no override is needed; we just can copy the localization parent of
4432
        // the original record (which is pointing to the correspondent default language record) to the new record.
4433
        // In copy / free mode the TransOrigPointer field is always set to 0, as no connection to the localization parent is wanted in that case.
4434
        // For pages, there is no "copy/free mode".
4435
        if (($this->useTransOrigPointerField || $table === 'pages') && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
4436
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $uid;
4437
        } elseif (!$this->useTransOrigPointerField) {
4438
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = 0;
4439
        }
4440
        if (isset($GLOBALS['TCA'][$table]['ctrl']['translationSource'])) {
4441
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['translationSource']] = $uid;
4442
        }
4443
        // Copy the type (if defined in both tables) from the original record so that translation has same type as original record
4444
        if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
4445
            $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['type']] = $row[$GLOBALS['TCA'][$table]['ctrl']['type']];
4446
        }
4447
        // Set exclude Fields:
4448
        foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fCfg) {
4449
            $translateToMsg = '';
4450
            // Check if we are just prefixing:
4451
            if ($fCfg['l10n_mode'] === 'prefixLangTitle') {
4452
                if (($fCfg['config']['type'] === 'text' || $fCfg['config']['type'] === 'input') && (string)$row[$fN] !== '') {
4453
                    [$tscPID] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4453
                    [$tscPID] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4454
                    $TSConfig = BackendUtility::getPagesTSconfig($tscPID)['TCEMAIN.'] ?? [];
4455
                    $tE = $this->getTableEntries($table, $TSConfig);
4456
                    if (!empty($TSConfig['translateToMessage']) && !$tE['disablePrependAtCopy']) {
4457
                        $translateToMsg = $this->getLanguageService()->sL($TSConfig['translateToMessage']);
4458
                        $translateToMsg = @sprintf($translateToMsg, $langRec['title']);
4459
                    }
4460
4461
                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processTranslateToClass'] ?? [] as $className) {
4462
                        $hookObj = GeneralUtility::makeInstance($className);
4463
                        if (method_exists($hookObj, 'processTranslateTo_copyAction')) {
4464
                            $hookObj->processTranslateTo_copyAction($row[$fN], $langRec, $this, $fN);
4465
                        }
4466
                    }
4467
                    if (!empty($translateToMsg)) {
4468
                        $overrideValues[$fN] = '[' . $translateToMsg . '] ' . $row[$fN];
4469
                    } else {
4470
                        $overrideValues[$fN] = $row[$fN];
4471
                    }
4472
                }
4473
            }
4474
        }
4475
4476
        if ($table !== 'pages') {
4477
            // Get the uid of record after which this localized record should be inserted
4478
            $previousUid = $this->getPreviousLocalizedRecordUid($table, $uid, $row['pid'], $language);
4479
            // Execute the copy:
4480
            $newId = $this->copyRecord($table, $uid, -$previousUid, true, $overrideValues, '', $language);
4481
            $autoVersionNewId = $this->getAutoVersionId($table, $newId);
4482
            if ($autoVersionNewId !== null) {
4483
                $this->triggerRemapAction($table, $newId, [$this, 'placeholderShadowing'], [$table, $autoVersionNewId], true);
4484
            }
4485
        } else {
4486
            // Create new page which needs to contain the same pid as the original page
4487
            $overrideValues['pid'] = $row['pid'];
4488
            // Take over the hidden state of the original language state, this is done due to legacy reasons where-as
4489
            // pages_language_overlay was set to "hidden -> default=0" but pages hidden -> default 1"
4490
            if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
4491
                $hiddenFieldName = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
4492
                $overrideValues[$hiddenFieldName] = $row[$hiddenFieldName] ?? $GLOBALS['TCA'][$table]['columns'][$hiddenFieldName]['config']['default'];
4493
            }
4494
            $temporaryId = StringUtility::getUniqueId('NEW');
4495
            $copyTCE = $this->getLocalTCE();
4496
            $copyTCE->start([$table => [$temporaryId => $overrideValues]], [], $this->BE_USER);
4497
            $copyTCE->process_datamap();
4498
            // Getting the new UID as if it had been copied:
4499
            $theNewSQLID = $copyTCE->substNEWwithIDs[$temporaryId];
4500
            if ($theNewSQLID) {
4501
                $this->copyMappingArray[$table][$uid] = $theNewSQLID;
4502
                $newId = $theNewSQLID;
4503
            }
4504
        }
4505
4506
        return $newId;
4507
    }
4508
4509
    /**
4510
     * Performs localization or synchronization of child records.
4511
     * The $command argument expects an array, but supports a string for backward-compatibility.
4512
     *
4513
     * $command = array(
4514
     *   'field' => 'tx_myfieldname',
4515
     *   'language' => 2,
4516
     *   // either the key 'action' or 'ids' must be set
4517
     *   'action' => 'synchronize', // or 'localize'
4518
     *   'ids' => array(1, 2, 3, 4) // child element ids
4519
     * );
4520
     *
4521
     * @param string $table The table of the localized parent record
4522
     * @param int $id The uid of the localized parent record
4523
     * @param array|string $command Defines the command to be performed (see example above)
4524
     */
4525
    protected function inlineLocalizeSynchronize($table, $id, $command)
4526
    {
4527
        $parentRecord = BackendUtility::getRecordWSOL($table, $id);
4528
4529
        // Backward-compatibility handling
4530
        if (!is_array($command)) {
4531
            // <field>, (localize | synchronize | <uid>):
4532
            $parts = GeneralUtility::trimExplode(',', $command);
4533
            $command = [
4534
                'field' => $parts[0],
4535
                // The previous process expected $id to point to the localized record already
4536
                'language' => (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]
4537
            ];
4538
            if (!MathUtility::canBeInterpretedAsInteger($parts[1])) {
4539
                $command['action'] = $parts[1];
4540
            } else {
4541
                $command['ids'] = [$parts[1]];
4542
            }
4543
        }
4544
4545
        // In case the parent record is the default language record, fetch the localization
4546
        if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
4547
            // Fetch the live record
4548
            // @todo: this needs to be revisited, as getRecordLocalization() does a BackendWorkspaceRestriction
4549
            // based on $GLOBALS[BE_USER], which could differ from the $this->BE_USER->workspace value
4550
            $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND t3ver_oid=0');
4551
            if (empty($parentRecordLocalization)) {
4552
                if ($this->enableLogging) {
4553
                    $this->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', -1, [], $this->eventPid($table, $id, $parentRecord['pid']));
4554
                }
4555
                return;
4556
            }
4557
            $parentRecord = $parentRecordLocalization[0];
4558
            $id = $parentRecord['uid'];
4559
            // Process overlay for current selected workspace
4560
            BackendUtility::workspaceOL($table, $parentRecord);
4561
        }
4562
4563
        $field = $command['field'];
4564
        $language = $command['language'];
4565
        $action = $command['action'];
4566
        $ids = $command['ids'];
4567
4568
        if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
4569
            return;
4570
        }
4571
4572
        $config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
4573
        $foreignTable = $config['foreign_table'];
4574
4575
        $transOrigPointer = (int)$parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
4576
        $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
4577
4578
        if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) {
4579
            return;
4580
        }
4581
4582
        $inlineSubType = $this->getInlineFieldType($config);
4583
        if ($inlineSubType === false) {
4584
            return;
4585
        }
4586
4587
        $transOrigRecord = BackendUtility::getRecordWSOL($table, $transOrigPointer);
4588
4589
        $removeArray = [];
4590
        $mmTable = $inlineSubType === 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : '';
4591
        // Fetch children from original language parent:
4592
        /** @var RelationHandler $dbAnalysisOriginal */
4593
        $dbAnalysisOriginal = $this->createRelationHandlerInstance();
4594
        $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $table, $config);
4595
        $elementsOriginal = [];
4596
        foreach ($dbAnalysisOriginal->itemArray as $item) {
4597
            $elementsOriginal[$item['id']] = $item;
4598
        }
4599
        unset($dbAnalysisOriginal);
4600
        // Fetch children from current localized parent:
4601
        /** @var RelationHandler $dbAnalysisCurrent */
4602
        $dbAnalysisCurrent = $this->createRelationHandlerInstance();
4603
        $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config);
4604
        // Perform synchronization: Possibly removal of already localized records:
4605
        if ($action === 'synchronize') {
4606
            foreach ($dbAnalysisCurrent->itemArray as $index => $item) {
4607
                $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']);
4608
                if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) {
4609
                    $childTransOrigPointer = $childRecord[$childTransOrigPointerField];
4610
                    // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it:
4611
                    if (!isset($elementsOriginal[$childTransOrigPointer])) {
4612
                        unset($dbAnalysisCurrent->itemArray[$index]);
4613
                        $removeArray[$item['table']][$item['id']]['delete'] = 1;
4614
                    }
4615
                }
4616
            }
4617
        }
4618
        // Perform synchronization/localization: Possibly add unlocalized records for original language:
4619
        if ($action === 'localize' || $action === 'synchronize') {
4620
            foreach ($elementsOriginal as $originalId => $item) {
4621
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4622
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4623
                $dbAnalysisCurrent->itemArray[] = $item;
4624
            }
4625
        } elseif (!empty($ids)) {
4626
            foreach ($ids as $childId) {
4627
                if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) {
4628
                    continue;
4629
                }
4630
                $item = $elementsOriginal[$childId];
4631
                $item['id'] = $this->localize($item['table'], $item['id'], $language);
4632
                $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']);
4633
                $dbAnalysisCurrent->itemArray[] = $item;
4634
            }
4635
        }
4636
        // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record):
4637
        $value = implode(',', $dbAnalysisCurrent->getValueArray());
4638
        $this->registerDBList[$table][$id][$field] = $value;
4639
        // Remove child records (if synchronization requested it):
4640
        if (is_array($removeArray) && !empty($removeArray)) {
4641
            /** @var DataHandler $tce */
4642
            $tce = GeneralUtility::makeInstance(__CLASS__, $this->referenceIndexUpdater);
4643
            $tce->enableLogging = $this->enableLogging;
4644
            $tce->start([], $removeArray, $this->BE_USER);
4645
            $tce->process_cmdmap();
4646
            unset($tce);
4647
        }
4648
        $updateFields = [];
4649
        // Handle, reorder and store relations:
4650
        if ($inlineSubType === 'list') {
4651
            $updateFields = [$field => $value];
4652
        } elseif ($inlineSubType === 'field') {
4653
            $dbAnalysisCurrent->writeForeignField($config, $id);
4654
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4655
        } elseif ($inlineSubType === 'mm') {
4656
            $dbAnalysisCurrent->writeMM($config['MM'], $id);
4657
            $updateFields = [$field => $dbAnalysisCurrent->countItems(false)];
4658
        }
4659
        // Update field referencing to child records of localized parent record:
4660
        if (!empty($updateFields)) {
4661
            $this->updateDB($table, $id, $updateFields);
4662
        }
4663
    }
4664
4665
    /*********************************************
4666
     *
4667
     * Cmd: Deleting
4668
     *
4669
     ********************************************/
4670
    /**
4671
     * Delete a single record
4672
     *
4673
     * @param string $table Table name
4674
     * @param int $id Record UID
4675
     * @internal should only be used from within DataHandler
4676
     */
4677
    public function deleteAction($table, $id)
4678
    {
4679
        $recordToDelete = BackendUtility::getRecord($table, $id);
4680
        // Record asked to be deleted was found:
4681
        if (is_array($recordToDelete)) {
4682
            $recordWasDeleted = false;
4683
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
4684
                $hookObj = GeneralUtility::makeInstance($className);
4685
                if (method_exists($hookObj, 'processCmdmap_deleteAction')) {
4686
                    $hookObj->processCmdmap_deleteAction($table, $id, $recordToDelete, $recordWasDeleted, $this);
4687
                }
4688
            }
4689
            // Delete the record if a hook hasn't deleted it yet
4690
            if (!$recordWasDeleted) {
0 ignored issues
show
introduced by
The condition $recordWasDeleted is always false.
Loading history...
4691
                $this->deleteEl($table, $id);
4692
            }
4693
        }
4694
    }
4695
4696
    /**
4697
     * Delete element from any table
4698
     *
4699
     * @param string $table Table name
4700
     * @param int $uid Record UID
4701
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
4702
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4703
     * @param bool $deleteRecordsOnPage If false and if deleting pages, records on the page will not be deleted (edge case while swapping workspaces)
4704
     * @internal should only be used from within DataHandler
4705
     */
4706
    public function deleteEl($table, $uid, $noRecordCheck = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4707
    {
4708
        if ($table === 'pages') {
4709
            $this->deletePages($uid, $noRecordCheck, $forceHardDelete, $deleteRecordsOnPage);
4710
        } else {
4711
            $this->deleteVersionsForRecord($table, $uid, $forceHardDelete);
4712
            $this->deleteRecord($table, $uid, $noRecordCheck, $forceHardDelete);
4713
        }
4714
    }
4715
4716
    /**
4717
     * Delete versions for element from any table
4718
     *
4719
     * @param string $table Table name
4720
     * @param int $uid Record UID
4721
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4722
     * @internal should only be used from within DataHandler
4723
     */
4724
    public function deleteVersionsForRecord($table, $uid, $forceHardDelete)
4725
    {
4726
        $versions = BackendUtility::selectVersionsOfRecord($table, $uid, 'uid,pid,t3ver_wsid,t3ver_state', $this->BE_USER->workspace ?: null);
4727
        if (is_array($versions)) {
4728
            foreach ($versions as $verRec) {
4729
                if (!$verRec['_CURRENT_VERSION']) {
4730
                    if ($table === 'pages') {
4731
                        $this->deletePages($verRec['uid'], true, $forceHardDelete);
4732
                    } else {
4733
                        $this->deleteRecord($table, $verRec['uid'], true, $forceHardDelete);
4734
                    }
4735
                }
4736
            }
4737
        }
4738
    }
4739
4740
    /**
4741
     * Undelete a single record
4742
     *
4743
     * @param string $table Table name
4744
     * @param int $uid Record UID
4745
     * @internal should only be used from within DataHandler
4746
     */
4747
    public function undeleteRecord($table, $uid)
4748
    {
4749
        if ($this->isRecordUndeletable($table, $uid)) {
4750
            $this->deleteRecord($table, $uid, true, false, true);
4751
        }
4752
    }
4753
4754
    /**
4755
     * Deleting/Undeleting a record
4756
     * This function may not be used to delete pages-records unless the underlying records are already deleted
4757
     * Deletes a record regardless of versioning state (live or offline, doesn't matter, the uid decides)
4758
     * If both $noRecordCheck and $forceHardDelete are set it could even delete a "deleted"-flagged record!
4759
     *
4760
     * @param string $table Table name
4761
     * @param int $uid Record UID
4762
     * @param bool $noRecordCheck Flag: If $noRecordCheck is set, then the function does not check permission to delete record
4763
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4764
     * @param bool $undeleteRecord If TRUE, the "deleted" flag is set to 0 again and thus, the item is undeleted.
4765
     * @internal should only be used from within DataHandler
4766
     */
4767
    public function deleteRecord($table, $uid, $noRecordCheck = false, $forceHardDelete = false, $undeleteRecord = false)
4768
    {
4769
        $currentUserWorkspace = (int)$this->BE_USER->workspace;
4770
        $uid = (int)$uid;
4771
        if (!$GLOBALS['TCA'][$table] || !$uid) {
4772
            $this->log($table, $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions. [' . $this->BE_USER->errorMsg . ']');
4773
            return;
4774
        }
4775
        // Skip processing already deleted records
4776
        if (!$forceHardDelete && !$undeleteRecord && $this->hasDeletedRecord($table, $uid)) {
4777
            return;
4778
        }
4779
4780
        // Checking if there is anything else disallowing deleting the record by checking if editing is allowed
4781
        $deletedRecord = $forceHardDelete || $undeleteRecord;
4782
        $fullLanguageAccessCheck = true;
4783
        if ($table === 'pages') {
4784
            // If this is a page translation, the full language access check should not be done
4785
            $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
4786
            if ($defaultLanguagePageId !== $uid) {
4787
                $fullLanguageAccessCheck = false;
4788
            }
4789
        }
4790
        $hasEditAccess = $this->BE_USER->recordEditAccessInternals($table, $uid, false, $deletedRecord, $fullLanguageAccessCheck);
4791
        if (!$hasEditAccess) {
4792
            $this->log($table, $uid, SystemLogDatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to delete record without delete-permissions');
4793
            return;
4794
        }
4795
        if ($table === 'pages') {
4796
            $perms = Permission::PAGE_DELETE;
4797
        } elseif ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
4798
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
4799
            $perms = Permission::PAGE_EDIT;
4800
        } else {
4801
            $perms = Permission::CONTENT_EDIT;
4802
        }
4803
        if (!$noRecordCheck && !$this->doesRecordExist($table, $uid, $perms)) {
4804
            return;
4805
        }
4806
4807
        // Clear cache before deleting the record, else the correct page cannot be identified by clear_cache
4808
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4808
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
4809
        $this->registerRecordIdForPageCacheClearing($table, $uid, $parentUid);
4810
        $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'];
4811
        $databaseErrorMessage = '';
4812
        if ($deleteField && !$forceHardDelete) {
4813
            $updateFields = [
4814
                $deleteField => $undeleteRecord ? 0 : 1
4815
            ];
4816
            if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
4817
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
4818
            }
4819
            // before (un-)deleting this record, check for child records or references
4820
            $this->deleteRecord_procFields($table, $uid, $undeleteRecord);
4821
            try {
4822
                // Delete all l10n records as well, impossible during undelete because it might bring too many records back to life
4823
                if (!$undeleteRecord) {
4824
                    $this->deletedRecords[$table][] = (int)$uid;
4825
                    $this->deleteL10nOverlayRecords($table, $uid);
4826
                }
4827
                GeneralUtility::makeInstance(ConnectionPool::class)
4828
                    ->getConnectionForTable($table)
4829
                    ->update($table, $updateFields, ['uid' => (int)$uid]);
4830
            } catch (DBALException $e) {
4831
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4832
            }
4833
        } else {
4834
            // Delete the hard way...:
4835
            try {
4836
                $this->hardDeleteSingleRecord($table, (int)$uid);
4837
                $this->deletedRecords[$table][] = (int)$uid;
4838
                $this->deleteL10nOverlayRecords($table, $uid);
4839
            } catch (DBALException $e) {
4840
                $databaseErrorMessage = $e->getPrevious()->getMessage();
4841
            }
4842
        }
4843
        if ($this->enableLogging) {
4844
            $state = $undeleteRecord ? SystemLogDatabaseAction::INSERT : SystemLogDatabaseAction::DELETE;
4845
            if ($databaseErrorMessage === '') {
4846
                if ($forceHardDelete) {
4847
                    $message = 'Record \'%s\' (%s) was deleted unrecoverable from page \'%s\' (%s)';
4848
                } else {
4849
                    $message = $state === 1 ? 'Record \'%s\' (%s) was restored on page \'%s\' (%s)' : 'Record \'%s\' (%s) was deleted from page \'%s\' (%s)';
4850
                }
4851
                $propArr = $this->getRecordProperties($table, $uid);
4852
                $pagePropArr = $this->getRecordProperties('pages', $propArr['pid']);
4853
4854
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::MESSAGE, $message, 0, [
4855
                    $propArr['header'],
4856
                    $table . ':' . $uid,
4857
                    $pagePropArr['header'],
4858
                    $propArr['pid']
4859
                ], $propArr['event_pid']);
4860
            } else {
4861
                $this->log($table, $uid, $state, 0, SystemLogErrorClassification::TODAYS_SPECIAL, $databaseErrorMessage);
4862
            }
4863
        }
4864
4865
        // Add history entry
4866
        if ($undeleteRecord) {
4867
            $this->getRecordHistoryStore()->undeleteRecord($table, $uid, $this->correlationId);
4868
        } else {
4869
            $this->getRecordHistoryStore()->deleteRecord($table, $uid, $this->correlationId);
4870
        }
4871
4872
        // Update reference index with table/uid on left side (recuid)
4873
        $this->updateRefIndex($table, $uid);
4874
        // Update reference index with table/uid on right side (ref_uid). Important if children of a relation are deleted / undeleted.
4875
        $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, $uid, $currentUserWorkspace);
4876
    }
4877
4878
    /**
4879
     * Used to delete page because it will check for branch below pages and disallowed tables on the page as well.
4880
     *
4881
     * @param int $uid Page id
4882
     * @param bool $force If TRUE, pages are not checked for permission.
4883
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4884
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4885
     * @internal should only be used from within DataHandler
4886
     */
4887
    public function deletePages($uid, $force = false, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4888
    {
4889
        $uid = (int)$uid;
4890
        if ($uid === 0) {
4891
            if ($this->enableLogging) {
4892
                $this->log('pages', $uid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
4893
            }
4894
            return;
4895
        }
4896
        // Getting list of pages to delete:
4897
        if ($force) {
4898
            // Returns the branch WITHOUT permission checks (0 secures that), so it cannot return -1
4899
            $pageIdsInBranch = $this->doesBranchExist('', $uid, 0, true);
4900
            $res = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
4901
        } else {
4902
            $res = $this->canDeletePage($uid);
4903
        }
4904
        // Perform deletion if not error:
4905
        if (is_array($res)) {
4906
            foreach ($res as $deleteId) {
4907
                $this->deleteSpecificPage($deleteId, $forceHardDelete, $deleteRecordsOnPage);
4908
            }
4909
        } else {
4910
            /** @var FlashMessage $flashMessage */
4911
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $res, '', FlashMessage::ERROR, true);
4912
            /** @var FlashMessageService $flashMessageService */
4913
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
4914
            $flashMessageService->getMessageQueueByIdentifier()->addMessage($flashMessage);
4915
            $this->newlog($res, SystemLogErrorClassification::USER_ERROR);
4916
        }
4917
    }
4918
4919
    /**
4920
     * Delete a page (or set deleted field to 1) and all records on it.
4921
     *
4922
     * @param int $uid Page id
4923
     * @param bool $forceHardDelete If TRUE, the "deleted" flag is ignored if applicable for record and the record is deleted COMPLETELY!
4924
     * @param bool $deleteRecordsOnPage If false, records on the page will not be deleted (edge case while swapping workspaces)
4925
     * @internal
4926
     * @see deletePages()
4927
     */
4928
    public function deleteSpecificPage($uid, $forceHardDelete = false, bool $deleteRecordsOnPage = true)
4929
    {
4930
        $uid = (int)$uid;
4931
        if (!$uid) {
4932
            // Early void return on invalid uid
4933
            return;
4934
        }
4935
        $forceHardDelete = (bool)$forceHardDelete;
4936
4937
        // Delete either a default language page or a translated page
4938
        $pageIdInDefaultLanguage = $this->getDefaultLanguagePageId($uid);
4939
        $isPageTranslation = false;
4940
        $pageLanguageId = 0;
4941
        if ($pageIdInDefaultLanguage !== $uid) {
4942
            // For translated pages, translated records in other tables (eg. tt_content) for the
4943
            // to-delete translated page have their pid field set to the uid of the default language record,
4944
            // NOT the uid of the translated page record.
4945
            // If a translated page is deleted, only translations of records in other tables of this language
4946
            // should be deleted. The code checks if the to-delete page is a translated page and
4947
            // adapts the query for other tables to use the uid of the default language page as pid together
4948
            // with the language id of the translated page.
4949
            $isPageTranslation = true;
4950
            $pageLanguageId = $this->pageInfo($uid, $GLOBALS['TCA']['pages']['ctrl']['languageField']);
4951
        }
4952
4953
        if ($deleteRecordsOnPage) {
4954
            $tableNames = $this->compileAdminTables();
4955
            foreach ($tableNames as $table) {
4956
                if ($table === 'pages' || ($isPageTranslation && !BackendUtility::isTableLocalizable($table))) {
4957
                    // Skip pages table. And skip table if not translatable, but a translated page is deleted
4958
                    continue;
4959
                }
4960
4961
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
4962
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
4963
                $queryBuilder
4964
                    ->select('uid')
4965
                    ->from($table);
4966
4967
                if ($isPageTranslation) {
4968
                    // Only delete records in the specified language
4969
                    $queryBuilder->where(
4970
                        $queryBuilder->expr()->eq(
4971
                            'pid',
4972
                            $queryBuilder->createNamedParameter($pageIdInDefaultLanguage, \PDO::PARAM_INT)
4973
                        ),
4974
                        $queryBuilder->expr()->eq(
4975
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
4976
                            $queryBuilder->createNamedParameter($pageLanguageId, \PDO::PARAM_INT)
4977
                        )
4978
                    );
4979
                } else {
4980
                    // Delete all records on this page
4981
                    $queryBuilder->where(
4982
                        $queryBuilder->expr()->eq(
4983
                            'pid',
4984
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
4985
                        )
4986
                    );
4987
                }
4988
                $statement = $queryBuilder->execute();
4989
4990
                while ($row = $statement->fetch()) {
4991
                    // Delete any further workspace overlays of the record in question, then delete the record.
4992
                    $this->deleteVersionsForRecord($table, $row['uid'], $forceHardDelete);
4993
                    $this->deleteRecord($table, $row['uid'], true, $forceHardDelete);
4994
                }
4995
            }
4996
        }
4997
4998
        // Delete any further workspace overlays of the record in question, then delete the record.
4999
        $this->deleteVersionsForRecord('pages', $uid, $forceHardDelete);
5000
        $this->deleteRecord('pages', $uid, true, $forceHardDelete);
5001
    }
5002
5003
    /**
5004
     * Used to evaluate if a page can be deleted
5005
     *
5006
     * @param int $uid Page id
5007
     * @return int[]|string If array: List of page uids to traverse and delete (means OK), if string: error message.
5008
     * @internal should only be used from within DataHandler
5009
     */
5010
    public function canDeletePage($uid)
5011
    {
5012
        $uid = (int)$uid;
5013
        $isTranslatedPage = null;
5014
5015
        // If we may at all delete this page
5016
        // If this is a page translation, do the check against the perms_* of the default page
5017
        // Because it is currently only deleting the translation
5018
        $defaultLanguagePageId = $this->getDefaultLanguagePageId($uid);
5019
        if ($defaultLanguagePageId !== $uid) {
5020
            if ($this->doesRecordExist('pages', (int)$defaultLanguagePageId, Permission::PAGE_DELETE)) {
5021
                $isTranslatedPage = true;
5022
            } else {
5023
                return 'Attempt to delete page without permissions';
5024
            }
5025
        } elseif (!$this->doesRecordExist('pages', $uid, Permission::PAGE_DELETE)) {
5026
            return 'Attempt to delete page without permissions';
5027
        }
5028
5029
        $pageIdsInBranch = $this->doesBranchExist('', $uid, Permission::PAGE_DELETE, true);
5030
5031
        if ($this->deleteTree) {
5032
            if ($pageIdsInBranch === -1) {
5033
                return 'Attempt to delete pages in branch without permissions';
5034
            }
5035
5036
            $pagesInBranch = GeneralUtility::intExplode(',', $pageIdsInBranch . $uid, true);
5037
        } else {
5038
            if ($pageIdsInBranch === -1) {
5039
                return 'Attempt to delete page without permissions';
5040
            }
5041
            if ($pageIdsInBranch !== '') {
5042
                return 'Attempt to delete page which has subpages';
5043
            }
5044
5045
            $pagesInBranch = [$uid];
5046
        }
5047
5048
        if ($disallowedTables = $this->checkForRecordsFromDisallowedTables($pagesInBranch)) {
5049
            return 'Attempt to delete records from disallowed tables (' . implode(', ', $disallowedTables) . ')';
5050
        }
5051
5052
        foreach ($pagesInBranch as $pageInBranch) {
5053
            if (!$this->BE_USER->recordEditAccessInternals('pages', $pageInBranch, false, false, $isTranslatedPage ? false : true)) {
5054
                return 'Attempt to delete page which has prohibited localizations.';
5055
            }
5056
        }
5057
        return $pagesInBranch;
5058
    }
5059
5060
    /**
5061
     * Returns TRUE if record CANNOT be deleted, otherwise FALSE. Used to check before the versioning API allows a record to be marked for deletion.
5062
     *
5063
     * @param string $table Record Table
5064
     * @param int $id Record UID
5065
     * @return string Returns a string IF there is an error (error string explaining). FALSE means record can be deleted
5066
     * @internal should only be used from within DataHandler
5067
     */
5068
    public function cannotDeleteRecord($table, $id)
5069
    {
5070
        if ($table === 'pages') {
5071
            $res = $this->canDeletePage($id);
5072
            return is_array($res) ? false : $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_array($res) ? false : $res could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
5073
        }
5074
        if ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap)) {
5075
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
5076
            $perms = Permission::PAGE_EDIT;
5077
        } else {
5078
            $perms = Permission::CONTENT_EDIT;
5079
        }
5080
        return $this->doesRecordExist($table, $id, $perms) ? false : 'No permission to delete record';
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->doesRecord...ssion to delete record' could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
5081
    }
5082
5083
    /**
5084
     * Determines whether a record can be undeleted.
5085
     *
5086
     * @param string $table Table name of the record
5087
     * @param int $uid uid of the record
5088
     * @return bool Whether the record can be undeleted
5089
     * @internal should only be used from within DataHandler
5090
     */
5091
    public function isRecordUndeletable($table, $uid)
5092
    {
5093
        $result = false;
5094
        $record = BackendUtility::getRecord($table, $uid, 'pid', '', false);
5095
        if ($record['pid']) {
5096
            $page = BackendUtility::getRecord('pages', $record['pid'], 'deleted, title, uid', '', false);
5097
            // The page containing the record is not deleted, thus the record can be undeleted:
5098
            if (!$page['deleted']) {
5099
                $result = true;
5100
            } else {
5101
                $this->log($table, $uid, SystemLogDatabaseAction::DELETE, '', SystemLogErrorClassification::USER_ERROR, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $recpid of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5101
                $this->log($table, $uid, SystemLogDatabaseAction::DELETE, /** @scrutinizer ignore-type */ '', SystemLogErrorClassification::USER_ERROR, 'Record cannot be undeleted since the page containing it is deleted! Undelete page "' . $page['title'] . ' (UID: ' . $page['uid'] . ')" first');
Loading history...
5102
            }
5103
        } else {
5104
            // The page containing the record is on rootlevel, so there is no parent record to check, and the record can be undeleted:
5105
            $result = true;
5106
        }
5107
        return $result;
5108
    }
5109
5110
    /**
5111
     * Before a record is deleted, check if it has references such as inline type or MM references.
5112
     * If so, set these child records also to be deleted.
5113
     *
5114
     * @param string $table Record Table
5115
     * @param string $uid Record UID
5116
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5117
     * @see deleteRecord()
5118
     * @internal should only be used from within DataHandler
5119
     */
5120
    public function deleteRecord_procFields($table, $uid, $undeleteRecord = false)
5121
    {
5122
        $conf = $GLOBALS['TCA'][$table]['columns'];
5123
        $row = BackendUtility::getRecord($table, $uid, '*', '', false);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getRecord(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5123
        $row = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ $uid, '*', '', false);
Loading history...
5124
        if (empty($row)) {
5125
            return;
5126
        }
5127
        foreach ($row as $field => $value) {
5128
            $this->deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf[$field]['config'], $undeleteRecord);
5129
        }
5130
    }
5131
5132
    /**
5133
     * Process fields of a record to be deleted and search for special handling, like
5134
     * inline type, MM records, etc.
5135
     *
5136
     * @param string $table Record Table
5137
     * @param string $uid Record UID
5138
     * @param string $field Record field
5139
     * @param string $value Record field value
5140
     * @param array $conf TCA configuration of current field
5141
     * @param bool $undeleteRecord If a record should be undeleted (e.g. from history/undo)
5142
     * @see deleteRecord()
5143
     * @internal should only be used from within DataHandler
5144
     */
5145
    public function deleteRecord_procBasedOnFieldType($table, $uid, $field, $value, $conf, $undeleteRecord = false)
5146
    {
5147
        if ($conf['type'] === 'inline') {
5148
            $foreign_table = $conf['foreign_table'];
5149
            if ($foreign_table) {
5150
                $inlineType = $this->getInlineFieldType($conf);
5151
                if ($inlineType === 'list' || $inlineType === 'field') {
5152
                    /** @var RelationHandler $dbAnalysis */
5153
                    $dbAnalysis = $this->createRelationHandlerInstance();
5154
                    $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
0 ignored issues
show
Bug introduced by
$uid of type string is incompatible with the type integer expected by parameter $MMuid of TYPO3\CMS\Core\Database\RelationHandler::start(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5154
                    $dbAnalysis->start($value, $conf['foreign_table'], '', /** @scrutinizer ignore-type */ $uid, $table, $conf);
Loading history...
5155
                    $dbAnalysis->undeleteRecord = true;
5156
5157
                    $enableCascadingDelete = true;
5158
                    // non type save comparison is intended!
5159
                    if (isset($conf['behaviour']['enableCascadingDelete']) && $conf['behaviour']['enableCascadingDelete'] == false) {
5160
                        $enableCascadingDelete = false;
5161
                    }
5162
5163
                    // Walk through the items and remove them
5164
                    foreach ($dbAnalysis->itemArray as $v) {
5165
                        if (!$undeleteRecord) {
5166
                            if ($enableCascadingDelete) {
5167
                                $this->deleteAction($v['table'], $v['id']);
5168
                            }
5169
                        } else {
5170
                            $this->undeleteRecord($v['table'], $v['id']);
5171
                        }
5172
                    }
5173
                }
5174
            }
5175
        } elseif ($this->isReferenceField($conf)) {
5176
            $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5177
            $dbAnalysis = $this->createRelationHandlerInstance();
5178
            $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
5179
            foreach ($dbAnalysis->itemArray as $v) {
5180
                $this->updateRefIndex($v['table'], $v['id']);
5181
            }
5182
        }
5183
    }
5184
5185
    /**
5186
     * Find l10n-overlay records and perform the requested delete action for these records.
5187
     *
5188
     * @param string $table Record Table
5189
     * @param string $uid Record UID
5190
     * @internal should only be used from within DataHandler
5191
     */
5192
    public function deleteL10nOverlayRecords($table, $uid)
5193
    {
5194
        // Check whether table can be localized
5195
        if (!BackendUtility::isTableLocalizable($table)) {
5196
            return;
5197
        }
5198
5199
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5200
        $queryBuilder->getRestrictions()
5201
            ->removeAll()
5202
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
5203
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->BE_USER->workspace));
5204
5205
        $queryBuilder->select('*')
5206
            ->from($table)
5207
            ->where(
5208
                $queryBuilder->expr()->eq(
5209
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5210
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5211
                )
5212
            );
5213
5214
        $result = $queryBuilder->execute();
5215
        while ($record = $result->fetch()) {
5216
            // Ignore workspace delete placeholders. Those records have been marked for
5217
            // deletion before - deleting them again in a workspace would revert that state.
5218
            if ((int)$this->BE_USER->workspace > 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
5219
                BackendUtility::workspaceOL($table, $record, $this->BE_USER->workspace);
5220
                if (VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
5221
                    continue;
5222
                }
5223
            }
5224
            $this->deleteAction($table, (int)$record['t3ver_oid'] > 0 ? (int)$record['t3ver_oid'] : (int)$record['uid']);
5225
        }
5226
    }
5227
5228
    /*********************************************
5229
     *
5230
     * Cmd: Workspace discard & flush
5231
     *
5232
     ********************************************/
5233
5234
    /**
5235
     * Discard a versioned record from this workspace. This deletes records from the database - no soft delete.
5236
     * This main entry method is called recursive for sub pages, localizations, relations and records on a page.
5237
     * The method checks user access and gathers facts about this record to hand the deletion over to detail methods.
5238
     *
5239
     * The incoming $uid or $row can be anything: The workspace of current user is respected and only records
5240
     * of current user workspace are discarded. If giving a live record uid, the versioned overly will be fetched.
5241
     *
5242
     * @param string $table Database table name
5243
     * @param int|null $uid Uid of live or versioned record to be discarded, or null if $record is given
5244
     * @param array|null $record Record row that should be discarded. Used instead of $uid within recursion.
5245
     */
5246
    public function discard(string $table, ?int $uid, array $record = null): void
5247
    {
5248
        if ($uid === null && $record === null) {
5249
            throw new \RuntimeException('Either record $uid or $record row must be given', 1600373491);
5250
        }
5251
5252
        // Fetch record we are dealing with if not given
5253
        if ($record === null) {
5254
            $record = BackendUtility::getRecord($table, $uid);
5255
        }
5256
        if (!is_array($record)) {
5257
            return;
5258
        }
5259
        $uid = (int)$record['uid'];
5260
5261
        // Call hook and return if hook took care of the element
5262
        $recordWasDiscarded = false;
5263
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] as $className) {
5264
            $hookObj = GeneralUtility::makeInstance($className);
5265
            if (method_exists($hookObj, 'processCmdmap_discardAction')) {
5266
                $hookObj->processCmdmap_discardAction($table, $uid, $record, $recordWasDiscarded);
5267
            }
5268
        }
5269
5270
        $userWorkspace = (int)$this->BE_USER->workspace;
5271
        if ($recordWasDiscarded
5272
            || $userWorkspace === 0
5273
            || !BackendUtility::isTableWorkspaceEnabled($table)
5274
            || $this->hasDeletedRecord($table, $uid)
5275
        ) {
5276
            return;
5277
        }
5278
5279
        // Gather versioned and placeholder record if there are any
5280
        $versionRecord = null;
5281
        $placeholderRecord = null;
5282
        if ((int)$record['t3ver_wsid'] === 0) {
5283
            $record = BackendUtility::getWorkspaceVersionOfRecord($userWorkspace, $table, $uid);
5284
        }
5285
        if (!is_array($record)) {
5286
            return;
5287
        }
5288
        $recordState = VersionState::cast($record['t3ver_state']);
5289
        if ($recordState->equals(VersionState::NEW_PLACEHOLDER)) {
5290
            $placeholderRecord = $record;
5291
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($userWorkspace, $table, $uid);
5292
            if (!is_array($versionRecord)) {
5293
                return;
5294
            }
5295
        } elseif ($recordState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5296
            $versionRecord = $record;
5297
            $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, $uid);
5298
            if (!is_array($placeholderRecord)) {
5299
                return;
5300
            }
5301
        } else {
5302
            $versionRecord = $record;
5303
        }
5304
        // Do not use $record, $recordState and $uid below anymore, rely on $versionRecord and $placeholderRecord
5305
5306
        // User access checks
5307
        if ($userWorkspace !== (int)$versionRecord['t3ver_wsid']) {
5308
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: Different workspace', SystemLogErrorClassification::USER_ERROR);
5309
            return;
5310
        }
5311
        if ($errorCode = $this->BE_USER->workspaceCannotEditOfflineVersion($table, $versionRecord['uid'])) {
5312
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR);
5313
            return;
5314
        }
5315
        if (!$this->checkRecordUpdateAccess($table, $versionRecord['uid'])) {
5316
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: User has no edit access', SystemLogErrorClassification::USER_ERROR);
5317
            return;
5318
        }
5319
        $fullLanguageAccessCheck = !($table === 'pages' && (int)$versionRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0);
5320
        if (!$this->BE_USER->recordEditAccessInternals($table, $versionRecord, false, true, $fullLanguageAccessCheck)) {
5321
            $this->newlog('Attempt to discard workspace record ' . $table . ':' . $versionRecord['uid'] . ' failed: User has no delete access', SystemLogErrorClassification::USER_ERROR);
5322
            return;
5323
        }
5324
5325
        // Perform discard operations
5326
        $versionState = VersionState::cast($versionRecord['t3ver_state']);
5327
        if ($table === 'pages' && $versionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5328
            // When discarding a new page page, there can be new sub pages and new records.
5329
            // Those need to be discarded, otherwise they'd end up as records without parent page.
5330
            $this->discardSubPagesAndRecordsOnPage($versionRecord);
5331
        }
5332
5333
        $this->discardLocalizationOverlayRecords($table, $versionRecord);
5334
        $this->discardRecordRelations($table, $versionRecord);
5335
        $this->hardDeleteSingleRecord($table, (int)$versionRecord['uid']);
5336
        $this->deletedRecords[$table][] = (int)$versionRecord['uid'];
5337
        $this->registerReferenceIndexRowsForDrop($table, (int)$versionRecord['uid'], $userWorkspace);
5338
        $this->getRecordHistoryStore()->deleteRecord($table, (int)$versionRecord['uid'], $this->correlationId);
5339
        $this->log(
5340
            $table,
5341
            (int)$versionRecord['uid'],
5342
            SystemLogDatabaseAction::DELETE,
5343
            0,
5344
            SystemLogErrorClassification::MESSAGE,
5345
            'Record ' . $table . ':' . $versionRecord['uid'] . ' was deleted unrecoverable from page ' . $versionRecord['pid'],
5346
            0,
5347
            [],
5348
            (int)$versionRecord['pid']
5349
        );
5350
5351
        if ($versionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5352
            // Drop placeholder records if any
5353
            $this->hardDeleteSingleRecord($table, (int)$placeholderRecord['uid']);
5354
            $this->deletedRecords[$table][] = (int)$placeholderRecord['uid'];
5355
        }
5356
    }
5357
5358
    /**
5359
     * Also discard any sub pages and records of a new parent page if this page is discarded.
5360
     * Discarding only in specific localization, if needed.
5361
     *
5362
     * @param array $page Page record row
5363
     */
5364
    protected function discardSubPagesAndRecordsOnPage(array $page): void
5365
    {
5366
        $isLocalizedPage = false;
5367
        $sysLanguageId = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
5368
        if ($sysLanguageId > 0) {
5369
            // New or moved localized page.
5370
            // Discard records on this page localization, but no sub pages.
5371
            // Records of a translated page have the pid set to the default language page uid. Found in l10n_parent.
5372
            // @todo: Discard other page translations that inherit from this?! (l10n_source field)
5373
            $isLocalizedPage = true;
5374
            $pid = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
5375
        } else {
5376
            // New or moved default language page.
5377
            // Discard any sub pages and all other records of this page, including any page localizations.
5378
            // The t3ver_state=-1 record is incoming here. Records on this page have their pid field set to the uid
5379
            // of the t3ver_state=1 record, which is in the t3ver_oid field of the incoming record.
5380
            $pid = (int)$page['t3ver_oid'];
5381
        }
5382
        $tables = $this->compileAdminTables();
5383
        foreach ($tables as $table) {
5384
            if (($isLocalizedPage && $table === 'pages')
5385
                || ($isLocalizedPage && !BackendUtility::isTableLocalizable($table))
5386
                || !BackendUtility::isTableWorkspaceEnabled($table)
5387
            ) {
5388
                continue;
5389
            }
5390
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5391
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5392
            $queryBuilder->select('*')
5393
                ->from($table)
5394
                ->where(
5395
                    $queryBuilder->expr()->eq(
5396
                        'pid',
5397
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
5398
                    ),
5399
                    $queryBuilder->expr()->eq(
5400
                        't3ver_wsid',
5401
                        $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, \PDO::PARAM_INT)
5402
                    )
5403
                );
5404
            if ($isLocalizedPage) {
5405
                // Add sys_language_uid = x restriction if discarding a localized page
5406
                $queryBuilder->andWhere(
5407
                    $queryBuilder->expr()->eq(
5408
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
5409
                        $queryBuilder->createNamedParameter($sysLanguageId, \PDO::PARAM_INT)
5410
                    )
5411
                );
5412
            }
5413
            $statement = $queryBuilder->execute();
5414
            while ($row = $statement->fetch()) {
5415
                $this->discard($table, null, $row);
5416
            }
5417
        }
5418
    }
5419
5420
    /**
5421
     * Discard record relations like inline and MM of a record.
5422
     *
5423
     * @param string $table Table name of this record
5424
     * @param array $record The record row to handle
5425
     */
5426
    protected function discardRecordRelations(string $table, array $record): void
5427
    {
5428
        foreach ($record as $field => $value) {
5429
            $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null;
5430
            if (!isset($fieldConfig['type'])) {
5431
                continue;
5432
            }
5433
            if ($fieldConfig['type'] === 'inline') {
5434
                $foreignTable = $fieldConfig['foreign_table'] ?? null;
5435
                if (!$foreignTable
5436
                     || (isset($fieldConfig['behaviour']['enableCascadingDelete'])
5437
                        && (bool)$fieldConfig['behaviour']['enableCascadingDelete'] === false)
5438
                ) {
5439
                    continue;
5440
                }
5441
                $inlineType = $this->getInlineFieldType($fieldConfig);
5442
                if ($inlineType === 'list' || $inlineType === 'field') {
5443
                    $dbAnalysis = $this->createRelationHandlerInstance();
5444
                    $dbAnalysis->start($value, $fieldConfig['foreign_table'], '', (int)$record['uid'], $table, $fieldConfig);
5445
                    $dbAnalysis->undeleteRecord = true;
5446
                    foreach ($dbAnalysis->itemArray as $relationRecord) {
5447
                        $this->discard($relationRecord['table'], (int)$relationRecord['id']);
5448
                    }
5449
                }
5450
            } elseif ($this->isReferenceField($fieldConfig)) {
5451
                $allowedTables = $fieldConfig['type'] === 'group' ? $fieldConfig['allowed'] : $fieldConfig['foreign_table'];
5452
                $dbAnalysis = $this->createRelationHandlerInstance();
5453
                $dbAnalysis->start($value, $allowedTables, $fieldConfig['MM'], (int)$record['uid'], $table, $fieldConfig);
5454
                foreach ($dbAnalysis->itemArray as $relationRecord) {
5455
                    // @todo: Something should happen with these relations here ...
5456
                    // @todo: Can't use dropReferenceIndexRowsForRecord() here, this would drop sys_refindex entries we want to keep
5457
                    $this->updateRefIndex($relationRecord['table'], (int)$relationRecord['id']);
5458
                }
5459
            }
5460
        }
5461
    }
5462
5463
    /**
5464
     * Find localization overlays of a record and discard them.
5465
     *
5466
     * @param string $table Table of this record
5467
     * @param array $record Record row
5468
     */
5469
    protected function discardLocalizationOverlayRecords(string $table, array $record): void
5470
    {
5471
        if (!BackendUtility::isTableLocalizable($table)) {
5472
            return;
5473
        }
5474
        $uid = (int)$record['uid'];
5475
        $versionState = VersionState::cast($record['t3ver_state']);
5476
        if ($versionState->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
5477
            // The t3ver_state=-1 record is incoming here. Localization overlays of this record have their uid field set
5478
            // to the uid of the t3ver_state=1 record, which is in the t3ver_oid field of the incoming record.
5479
            $uid = (int)$record['t3ver_oid'];
5480
        }
5481
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
5482
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
5483
        $statement = $queryBuilder->select('*')
5484
            ->from($table)
5485
            ->where(
5486
                $queryBuilder->expr()->eq(
5487
                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
5488
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
5489
                ),
5490
                $queryBuilder->expr()->eq(
5491
                    't3ver_wsid',
5492
                    $queryBuilder->createNamedParameter((int)$this->BE_USER->workspace, \PDO::PARAM_INT)
5493
                )
5494
            )
5495
            ->execute();
5496
        while ($record = $statement->fetch()) {
5497
            $this->discard($table, null, $record);
5498
        }
5499
    }
5500
5501
    /*********************************************
5502
     *
5503
     * Cmd: Versioning
5504
     *
5505
     ********************************************/
5506
    /**
5507
     * Creates a new version of a record
5508
     * (Requires support in the table)
5509
     *
5510
     * @param string $table Table name
5511
     * @param int $id Record uid to versionize
5512
     * @param string $label Version label
5513
     * @param bool $delete If TRUE, the version is created to delete the record.
5514
     * @return int|null Returns the id of the new version (if any)
5515
     * @see copyRecord()
5516
     * @internal should only be used from within DataHandler
5517
     */
5518
    public function versionizeRecord($table, $id, $label, $delete = false)
5519
    {
5520
        $id = (int)$id;
5521
        // Stop any actions if the record is marked to be deleted:
5522
        // (this can occur if IRRE elements are versionized and child elements are removed)
5523
        if ($this->isElementToBeDeleted($table, $id)) {
5524
            return null;
5525
        }
5526
        if (!BackendUtility::isTableWorkspaceEnabled($table) || $id <= 0) {
5527
            $this->newlog('Versioning is not supported for this table "' . $table . '" / ' . $id, SystemLogErrorClassification::USER_ERROR);
5528
            return null;
5529
        }
5530
5531
        // Fetch record with permission check
5532
        $row = $this->recordInfoWithPermissionCheck($table, $id, Permission::PAGE_SHOW);
5533
5534
        // This checks if the record can be selected which is all that a copy action requires.
5535
        if ($row === false) {
5536
            $this->newlog(
5537
                'The record does not exist or you don\'t have correct permissions to make a new version (copy) of this record "' . $table . ':' . $id . '"',
5538
                SystemLogErrorClassification::USER_ERROR
5539
            );
5540
            return null;
5541
        }
5542
5543
        // Record must be online record, otherwise we would create a version of a version
5544
        if (($row['t3ver_oid'] ?? 0) > 0) {
5545
            $this->newlog('Record "' . $table . ':' . $id . '" you wanted to versionize was already a version in archive (record has an online ID)!', SystemLogErrorClassification::USER_ERROR);
5546
            return null;
5547
        }
5548
5549
        if ($delete && $this->cannotDeleteRecord($table, $id)) {
5550
            $this->newlog('Record cannot be deleted: ' . $this->cannotDeleteRecord($table, $id), SystemLogErrorClassification::USER_ERROR);
5551
            return null;
5552
        }
5553
5554
        // Set up the values to override when making a raw-copy:
5555
        $overrideArray = [
5556
            't3ver_oid' => $id,
5557
            't3ver_wsid' => $this->BE_USER->workspace,
5558
            't3ver_state' => (string)($delete ? new VersionState(VersionState::DELETE_PLACEHOLDER) : new VersionState(VersionState::DEFAULT_STATE)),
5559
            't3ver_stage' => 0,
5560
        ];
5561
        if ($GLOBALS['TCA'][$table]['ctrl']['editlock']) {
5562
            $overrideArray[$GLOBALS['TCA'][$table]['ctrl']['editlock']] = 0;
5563
        }
5564
        // Checking if the record already has a version in the current workspace of the backend user
5565
        $versionRecord = ['uid' => null];
5566
        if ($this->BE_USER->workspace !== 0) {
5567
            // Look for version already in workspace:
5568
            $versionRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $id, 'uid');
5569
        }
5570
        // Create new version of the record and return the new uid
5571
        if (empty($versionRecord['uid'])) {
5572
            // Create raw-copy and return result:
5573
            // The information of the label to be used for the workspace record
5574
            // as well as the information whether the record shall be removed
5575
            // must be forwarded (creating delete placeholders on a workspace are
5576
            // done by copying the record and override several fields).
5577
            $workspaceOptions = [
5578
                'delete' => $delete,
5579
                'label' => $label,
5580
            ];
5581
            return $this->copyRecord_raw($table, $id, (int)$row['pid'], $overrideArray, $workspaceOptions);
5582
        }
5583
        // Reuse the existing record and return its uid
5584
        // (prior to TYPO3 CMS 6.2, an error was thrown here, which
5585
        // did not make much sense since the information is available)
5586
        return $versionRecord['uid'];
5587
    }
5588
5589
    /**
5590
     * Swaps MM-relations for current/swap record, see version_swap()
5591
     *
5592
     * @param string $table Table for the two input records
5593
     * @param int $id Current record (about to go offline)
5594
     * @param int $swapWith Swap record (about to go online)
5595
     * @see version_swap()
5596
     * @internal should only be used from within DataHandler
5597
     */
5598
    public function version_remapMMForVersionSwap($table, $id, $swapWith)
5599
    {
5600
        // Actually, selecting the records fully is only need if flexforms are found inside... This could be optimized ...
5601
        $currentRec = BackendUtility::getRecord($table, $id);
5602
        $swapRec = BackendUtility::getRecord($table, $swapWith);
5603
        $this->version_remapMMForVersionSwap_reg = [];
5604
        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5605
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fConf) {
5606
            $conf = $fConf['config'];
5607
            if ($this->isReferenceField($conf)) {
5608
                $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5609
                $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5610
                if ($conf['MM']) {
5611
                    /** @var RelationHandler $dbAnalysis */
5612
                    $dbAnalysis = $this->createRelationHandlerInstance();
5613
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $id, $table, $conf);
5614
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5614
                    if (!empty($dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName))) {
Loading history...
5615
                        $this->version_remapMMForVersionSwap_reg[$id][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5616
                    }
5617
                    /** @var RelationHandler $dbAnalysis */
5618
                    $dbAnalysis = $this->createRelationHandlerInstance();
5619
                    $dbAnalysis->start('', $allowedTables, $conf['MM'], $swapWith, $table, $conf);
5620
                    if (!empty($dbAnalysis->getValueArray($prependName))) {
5621
                        $this->version_remapMMForVersionSwap_reg[$swapWith][$field] = [$dbAnalysis, $conf['MM'], $prependName];
5622
                    }
5623
                }
5624
            } elseif ($conf['type'] === 'flex') {
5625
                // Current record
5626
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5627
                    $fConf,
5628
                    $table,
5629
                    $field,
5630
                    $currentRec
5631
                );
5632
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5633
                $currentValueArray = GeneralUtility::xml2array($currentRec[$field]);
5634
                if (is_array($currentValueArray)) {
5635
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $id, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5636
                }
5637
                // Swap record
5638
                $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5639
                    $fConf,
5640
                    $table,
5641
                    $field,
5642
                    $swapRec
5643
                );
5644
                $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5645
                $currentValueArray = GeneralUtility::xml2array($swapRec[$field]);
5646
                if (is_array($currentValueArray)) {
5647
                    $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $swapWith, $field], 'version_remapMMForVersionSwap_flexFormCallBack');
5648
                }
5649
            }
5650
        }
5651
        // Execute:
5652
        $this->version_remapMMForVersionSwap_execSwap($table, $id, $swapWith);
5653
    }
5654
5655
    /**
5656
     * Callback function for traversing the FlexForm structure in relation to ...
5657
     *
5658
     * @param array $pParams Array of parameters in num-indexes: table, uid, field
5659
     * @param array $dsConf TCA field configuration (from Data Structure XML)
5660
     * @param string $dataValue The value of the flexForm field
5661
     * @param string $dataValue_ext1 Not used.
5662
     * @param string $dataValue_ext2 Not used.
5663
     * @param string $path Path in flexforms
5664
     * @see version_remapMMForVersionSwap()
5665
     * @see checkValue_flex_procInData_travDS()
5666
     * @internal should only be used from within DataHandler
5667
     */
5668
    public function version_remapMMForVersionSwap_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
5669
    {
5670
        // Extract parameters:
5671
        [$table, $uid, $field] = $pParams;
5672
        if ($this->isReferenceField($dsConf)) {
5673
            $allowedTables = $dsConf['type'] === 'group' ? $dsConf['allowed'] : $dsConf['foreign_table'];
5674
            $prependName = $dsConf['type'] === 'group' ? $dsConf['prepend_tname'] : '';
5675
            if ($dsConf['MM']) {
5676
                /** @var RelationHandler $dbAnalysis */
5677
                $dbAnalysis = $this->createRelationHandlerInstance();
5678
                $dbAnalysis->start('', $allowedTables, $dsConf['MM'], $uid, $table, $dsConf);
5679
                $this->version_remapMMForVersionSwap_reg[$uid][$field . '/' . $path] = [$dbAnalysis, $dsConf['MM'], $prependName];
5680
            }
5681
        }
5682
    }
5683
5684
    /**
5685
     * Performing the remapping operations found necessary in version_remapMMForVersionSwap()
5686
     * It must be done in three steps with an intermediate "fake" uid. The UID can be something else than -$id (fx. 9999999+$id if you dare... :-)- as long as it is unique.
5687
     *
5688
     * @param string $table Table for the two input records
5689
     * @param int $id Current record (about to go offline)
5690
     * @param int $swapWith Swap record (about to go online)
5691
     * @see version_remapMMForVersionSwap()
5692
     * @internal should only be used from within DataHandler
5693
     */
5694
    public function version_remapMMForVersionSwap_execSwap($table, $id, $swapWith)
5695
    {
5696
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5697
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5698
                $str[0]->remapMM($str[1], $id, -$id, $str[2]);
5699
            }
5700
        }
5701
        if (is_array($this->version_remapMMForVersionSwap_reg[$swapWith])) {
5702
            foreach ($this->version_remapMMForVersionSwap_reg[$swapWith] as $field => $str) {
5703
                $str[0]->remapMM($str[1], $swapWith, $id, $str[2]);
5704
            }
5705
        }
5706
        if (is_array($this->version_remapMMForVersionSwap_reg[$id])) {
5707
            foreach ($this->version_remapMMForVersionSwap_reg[$id] as $field => $str) {
5708
                $str[0]->remapMM($str[1], -$id, $swapWith, $str[2]);
5709
            }
5710
        }
5711
    }
5712
5713
    /*********************************************
5714
     *
5715
     * Cmd: Helper functions
5716
     *
5717
     ********************************************/
5718
5719
    /**
5720
     * Returns an instance of DataHandler for handling local datamaps/cmdmaps
5721
     *
5722
     * @return DataHandler
5723
     */
5724
    protected function getLocalTCE()
5725
    {
5726
        $copyTCE = GeneralUtility::makeInstance(DataHandler::class, $this->referenceIndexUpdater);
5727
        $copyTCE->copyTree = $this->copyTree;
5728
        $copyTCE->enableLogging = $this->enableLogging;
5729
        // Transformations should NOT be carried out during copy
5730
        $copyTCE->dontProcessTransformations = true;
5731
        // make sure the isImporting flag is transferred, so all hooks know if
5732
        // the current process is an import process
5733
        $copyTCE->isImporting = $this->isImporting;
5734
        $copyTCE->bypassAccessCheckForRecords = $this->bypassAccessCheckForRecords;
5735
        $copyTCE->bypassWorkspaceRestrictions = $this->bypassWorkspaceRestrictions;
5736
        return $copyTCE;
5737
    }
5738
5739
    /**
5740
     * Processes the fields with references as registered during the copy process. This includes all FlexForm fields which had references.
5741
     * @internal should only be used from within DataHandler
5742
     */
5743
    public function remapListedDBRecords()
5744
    {
5745
        if (!empty($this->registerDBList)) {
5746
            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
5747
            foreach ($this->registerDBList as $table => $records) {
5748
                foreach ($records as $uid => $fields) {
5749
                    $newData = [];
5750
                    $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5751
                    $theUidToUpdate_saveTo = BackendUtility::wsMapId($table, $theUidToUpdate);
5752
                    foreach ($fields as $fieldName => $value) {
5753
                        $conf = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
5754
                        switch ($conf['type']) {
5755
                            case 'group':
5756
                            case 'select':
5757
                                $vArray = $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5758
                                if (is_array($vArray)) {
5759
                                    $newData[$fieldName] = implode(',', $vArray);
5760
                                }
5761
                                break;
5762
                            case 'flex':
5763
                                if ($value === 'FlexForm_reference') {
5764
                                    // This will fetch the new row for the element
5765
                                    $origRecordRow = $this->recordInfo($table, $theUidToUpdate, '*');
5766
                                    if (is_array($origRecordRow)) {
5767
                                        BackendUtility::workspaceOL($table, $origRecordRow);
5768
                                        // Get current data structure and value array:
5769
                                        $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
5770
                                            ['config' => $conf],
5771
                                            $table,
5772
                                            $fieldName,
5773
                                            $origRecordRow
5774
                                        );
5775
                                        $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
5776
                                        $currentValueArray = GeneralUtility::xml2array($origRecordRow[$fieldName]);
5777
                                        // Do recursive processing of the XML data:
5778
                                        $currentValueArray['data'] = $this->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $theUidToUpdate, $fieldName], 'remapListedDBRecords_flexFormCallBack');
5779
                                        // The return value should be compiled back into XML, ready to insert directly in the field (as we call updateDB() directly later):
5780
                                        if (is_array($currentValueArray['data'])) {
5781
                                            $newData[$fieldName] = $this->checkValue_flexArray2Xml($currentValueArray, true);
5782
                                        }
5783
                                    }
5784
                                }
5785
                                break;
5786
                            case 'inline':
5787
                                $this->remapListedDBRecords_procInline($conf, $value, $uid, $table);
5788
                                break;
5789
                            default:
5790
                                $this->logger->debug('Field type should not appear here: ' . $conf['type']);
5791
                        }
5792
                    }
5793
                    // If any fields were changed, those fields are updated!
5794
                    if (!empty($newData)) {
5795
                        $this->updateDB($table, $theUidToUpdate_saveTo, $newData);
5796
                    }
5797
                }
5798
            }
5799
        }
5800
    }
5801
5802
    /**
5803
     * Callback function for traversing the FlexForm structure in relation to creating copied files of file relations inside of flex form structures.
5804
     *
5805
     * @param array $pParams Set of parameters in numeric array: table, uid, field
5806
     * @param array $dsConf TCA config for field (from Data Structure of course)
5807
     * @param string $dataValue Field value (from FlexForm XML)
5808
     * @param string $dataValue_ext1 Not used
5809
     * @param string $dataValue_ext2 Not used
5810
     * @return array Array where the "value" key carries the value.
5811
     * @see checkValue_flex_procInData_travDS()
5812
     * @see remapListedDBRecords()
5813
     * @internal should only be used from within DataHandler
5814
     */
5815
    public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2)
5816
    {
5817
        // Extract parameters:
5818
        [$table, $uid, $field] = $pParams;
5819
        // If references are set for this field, set flag so they can be corrected later:
5820
        if ($this->isReferenceField($dsConf) && (string)$dataValue !== '') {
5821
            $vArray = $this->remapListedDBRecords_procDBRefs($dsConf, $dataValue, $uid, $table);
5822
            if (is_array($vArray)) {
5823
                $dataValue = implode(',', $vArray);
5824
            }
5825
        }
5826
        // Return
5827
        return ['value' => $dataValue];
5828
    }
5829
5830
    /**
5831
     * Performs remapping of old UID values to NEW uid values for a DB reference field.
5832
     *
5833
     * @param array $conf TCA field config
5834
     * @param string $value Field value
5835
     * @param int $MM_localUid UID of local record (for MM relations - might need to change if support for FlexForms should be done!)
5836
     * @param string $table Table name
5837
     * @return array|null Returns array of items ready to implode for field content.
5838
     * @see remapListedDBRecords()
5839
     * @internal should only be used from within DataHandler
5840
     */
5841
    public function remapListedDBRecords_procDBRefs($conf, $value, $MM_localUid, $table)
5842
    {
5843
        // Initialize variables
5844
        // Will be set TRUE if an upgrade should be done...
5845
        $set = false;
5846
        // Allowed tables for references.
5847
        $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
5848
        // Table name to prepend the UID
5849
        $prependName = $conf['type'] === 'group' ? $conf['prepend_tname'] : '';
5850
        // Which tables that should possibly not be remapped
5851
        $dontRemapTables = GeneralUtility::trimExplode(',', $conf['dontRemapTablesOnCopy'], true);
5852
        // Convert value to list of references:
5853
        $dbAnalysis = $this->createRelationHandlerInstance();
5854
        $dbAnalysis->registerNonTableValues = $conf['type'] === 'select' && $conf['allowNonIdValues'];
5855
        $dbAnalysis->start($value, $allowedTables, $conf['MM'], $MM_localUid, $table, $conf);
5856
        // Traverse those references and map IDs:
5857
        foreach ($dbAnalysis->itemArray as $k => $v) {
5858
            $mapID = $this->copyMappingArray_merged[$v['table']][$v['id']];
5859
            if ($mapID && !in_array($v['table'], $dontRemapTables, true)) {
5860
                $dbAnalysis->itemArray[$k]['id'] = $mapID;
5861
                $set = true;
5862
            }
5863
        }
5864
        if (!empty($conf['MM'])) {
5865
            // Purge invalid items (live/version)
5866
            $dbAnalysis->purgeItemArray();
5867
            if ($dbAnalysis->isPurged()) {
5868
                $set = true;
5869
            }
5870
5871
            // If record has been versioned/copied in this process, handle invalid relations of the live record
5872
            $liveId = BackendUtility::getLiveVersionIdOfRecord($table, $MM_localUid);
5873
            $originalId = 0;
5874
            if (!empty($this->copyMappingArray_merged[$table])) {
5875
                $originalId = array_search($MM_localUid, $this->copyMappingArray_merged[$table]);
5876
            }
5877
            if (!empty($liveId) && !empty($originalId) && (int)$liveId === (int)$originalId) {
5878
                $liveRelations = $this->createRelationHandlerInstance();
5879
                $liveRelations->setWorkspaceId(0);
5880
                $liveRelations->start('', $allowedTables, $conf['MM'], $liveId, $table, $conf);
5881
                // Purge invalid relations in the live workspace ("0")
5882
                $liveRelations->purgeItemArray(0);
5883
                if ($liveRelations->isPurged()) {
5884
                    $liveRelations->writeMM($conf['MM'], $liveId, $prependName);
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\RelationHandler::writeMM() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5884
                    $liveRelations->writeMM($conf['MM'], $liveId, /** @scrutinizer ignore-type */ $prependName);
Loading history...
5885
                }
5886
            }
5887
        }
5888
        // If a change has been done, set the new value(s)
5889
        if ($set) {
5890
            if ($conf['MM']) {
5891
                $dbAnalysis->writeMM($conf['MM'], $MM_localUid, $prependName);
5892
            } else {
5893
                return $dbAnalysis->getValueArray($prependName);
0 ignored issues
show
Bug introduced by
It seems like $prependName can also be of type string; however, parameter $prependTableName of TYPO3\CMS\Core\Database\...andler::getValueArray() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5893
                return $dbAnalysis->getValueArray(/** @scrutinizer ignore-type */ $prependName);
Loading history...
5894
            }
5895
        }
5896
        return null;
5897
    }
5898
5899
    /**
5900
     * Performs remapping of old UID values to NEW uid values for an inline field.
5901
     *
5902
     * @param array $conf TCA field config
5903
     * @param string $value Field value
5904
     * @param int $uid The uid of the ORIGINAL record
5905
     * @param string $table Table name
5906
     * @internal should only be used from within DataHandler
5907
     */
5908
    public function remapListedDBRecords_procInline($conf, $value, $uid, $table)
5909
    {
5910
        $theUidToUpdate = $this->copyMappingArray_merged[$table][$uid];
5911
        if ($conf['foreign_table']) {
5912
            $inlineType = $this->getInlineFieldType($conf);
5913
            if ($inlineType === 'mm') {
5914
                $this->remapListedDBRecords_procDBRefs($conf, $value, $theUidToUpdate, $table);
5915
            } elseif ($inlineType !== false) {
5916
                /** @var RelationHandler $dbAnalysis */
5917
                $dbAnalysis = $this->createRelationHandlerInstance();
5918
                $dbAnalysis->start($value, $conf['foreign_table'], '', 0, $table, $conf);
5919
5920
                $updatePidForRecords = [];
5921
                // Update values for specific versioned records
5922
                foreach ($dbAnalysis->itemArray as &$item) {
5923
                    $updatePidForRecords[$item['table']][] = $item['id'];
5924
                    $versionedId = $this->getAutoVersionId($item['table'], $item['id']);
5925
                    if ($versionedId !== null) {
5926
                        $updatePidForRecords[$item['table']][] = $versionedId;
5927
                        $item['id'] = $versionedId;
5928
                    }
5929
                }
5930
5931
                // Update child records if using pointer fields ('foreign_field'):
5932
                if ($inlineType === 'field') {
5933
                    $dbAnalysis->writeForeignField($conf, $uid, $theUidToUpdate);
5934
                }
5935
                $thePidToUpdate = null;
5936
                // If the current field is set on a page record, update the pid of related child records:
5937
                if ($table === 'pages') {
5938
                    $thePidToUpdate = $theUidToUpdate;
5939
                } elseif (isset($this->registerDBPids[$table][$uid])) {
5940
                    $thePidToUpdate = $this->registerDBPids[$table][$uid];
5941
                    $thePidToUpdate = $this->copyMappingArray_merged['pages'][$thePidToUpdate];
5942
                }
5943
5944
                // Update child records if change to pid is required
5945
                if ($thePidToUpdate && !empty($updatePidForRecords)) {
5946
                    // Ensure that only the default language page is used as PID
5947
                    $thePidToUpdate = $this->getDefaultLanguagePageId($thePidToUpdate);
5948
                    // @todo: this can probably go away
5949
                    // ensure, only live page ids are used as 'pid' values
5950
                    $liveId = BackendUtility::getLiveVersionIdOfRecord('pages', $theUidToUpdate);
5951
                    if ($liveId !== null) {
5952
                        $thePidToUpdate = $liveId;
5953
                    }
5954
                    $updateValues = ['pid' => $thePidToUpdate];
5955
                    foreach ($updatePidForRecords as $tableName => $uids) {
5956
                        if (empty($tableName) || empty($uids)) {
5957
                            continue;
5958
                        }
5959
                        $conn = GeneralUtility::makeInstance(ConnectionPool::class)
5960
                            ->getConnectionForTable($tableName);
5961
                        foreach ($uids as $updateUid) {
5962
                            $conn->update($tableName, $updateValues, ['uid' => $updateUid]);
5963
                        }
5964
                    }
5965
                }
5966
            }
5967
        }
5968
    }
5969
5970
    /**
5971
     * Processes the $this->remapStack at the end of copying, inserting, etc. actions.
5972
     * The remapStack takes care about the correct mapping of new and old uids in case of relational data.
5973
     * @internal should only be used from within DataHandler
5974
     */
5975
    public function processRemapStack()
5976
    {
5977
        // Processes the remap stack:
5978
        if (is_array($this->remapStack)) {
0 ignored issues
show
introduced by
The condition is_array($this->remapStack) is always true.
Loading history...
5979
            $remapFlexForms = [];
5980
            $hookPayload = [];
5981
5982
            $newValue = null;
5983
            foreach ($this->remapStack as $remapAction) {
5984
                // If no position index for the arguments was set, skip this remap action:
5985
                if (!is_array($remapAction['pos'])) {
5986
                    continue;
5987
                }
5988
                // Load values from the argument array in remapAction:
5989
                $field = $remapAction['field'];
5990
                $id = $remapAction['args'][$remapAction['pos']['id']];
5991
                $rawId = $id;
5992
                $table = $remapAction['args'][$remapAction['pos']['table']];
5993
                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
5994
                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
5995
                $additionalData = $remapAction['additionalData'];
5996
                // The record is new and has one or more new ids (in case of versioning/workspaces):
5997
                if (strpos($id, 'NEW') !== false) {
5998
                    // Replace NEW...-ID with real uid:
5999
                    $id = $this->substNEWwithIDs[$id];
6000
                    // If the new parent record is on a non-live workspace or versionized, it has another new id:
6001
                    if (isset($this->autoVersionIdMap[$table][$id])) {
6002
                        $id = $this->autoVersionIdMap[$table][$id];
6003
                    }
6004
                    $remapAction['args'][$remapAction['pos']['id']] = $id;
6005
                }
6006
                // Replace relations to NEW...-IDs in field value (uids of child records):
6007
                if (is_array($valueArray)) {
6008
                    foreach ($valueArray as $key => $value) {
6009
                        if (strpos($value, 'NEW') !== false) {
6010
                            if (strpos($value, '_') === false) {
6011
                                $affectedTable = $tcaFieldConf['foreign_table'];
6012
                                $prependTable = false;
6013
                            } else {
6014
                                $parts = explode('_', $value);
6015
                                $value = array_pop($parts);
6016
                                $affectedTable = implode('_', $parts);
6017
                                $prependTable = true;
6018
                            }
6019
                            $value = $this->substNEWwithIDs[$value];
6020
                            // The record is new, but was also auto-versionized and has another new id:
6021
                            if (isset($this->autoVersionIdMap[$affectedTable][$value])) {
6022
                                $value = $this->autoVersionIdMap[$affectedTable][$value];
6023
                            }
6024
                            if ($prependTable) {
6025
                                $value = $affectedTable . '_' . $value;
6026
                            }
6027
                            // Set a hint that this was a new child record:
6028
                            $this->newRelatedIDs[$affectedTable][] = $value;
6029
                            $valueArray[$key] = $value;
6030
                        }
6031
                    }
6032
                    $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
6033
                }
6034
                // Process the arguments with the defined function:
6035
                if (!empty($remapAction['func'])) {
6036
                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
6037
                }
6038
                // If array is returned, check for maxitems condition, if string is returned this was already done:
6039
                if (is_array($newValue)) {
6040
                    $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
6041
                    // The reference casting is only required if
6042
                    // checkValue_group_select_processDBdata() returns an array
6043
                    $newValue = $this->castReferenceValue($newValue, $tcaFieldConf);
6044
                }
6045
                // Update in database (list of children (csv) or number of relations (foreign_field)):
6046
                if (!empty($field)) {
6047
                    $fieldArray = [$field => $newValue];
6048
                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
6049
                        $fieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
6050
                    }
6051
                    $this->updateDB($table, $id, $fieldArray);
6052
                } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
6053
                    // Collect data to update FlexForms
6054
                    $flexFormId = $additionalData['flexFormId'];
6055
                    $flexFormPath = $additionalData['flexFormPath'];
6056
6057
                    if (!isset($remapFlexForms[$flexFormId])) {
6058
                        $remapFlexForms[$flexFormId] = [];
6059
                    }
6060
6061
                    $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
6062
                }
6063
6064
                // Collect elements that shall trigger processDatamap_afterDatabaseOperations
6065
                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
6066
                    $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
6067
                    if (!isset($hookPayload[$table][$rawId])) {
6068
                        $hookPayload[$table][$rawId] = [
6069
                            'status' => $hookArgs['status'],
6070
                            'fieldArray' => $hookArgs['fieldArray'],
6071
                            'hookObjects' => $hookArgs['hookObjectsArr'],
6072
                        ];
6073
                    }
6074
                    $hookPayload[$table][$rawId]['fieldArray'][$field] = $newValue;
6075
                }
6076
            }
6077
6078
            if ($remapFlexForms) {
6079
                foreach ($remapFlexForms as $flexFormId => $modifications) {
6080
                    $this->updateFlexFormData($flexFormId, $modifications);
6081
                }
6082
            }
6083
6084
            foreach ($hookPayload as $tableName => $rawIdPayload) {
6085
                foreach ($rawIdPayload as $rawId => $payload) {
6086
                    foreach ($payload['hookObjects'] as $hookObject) {
6087
                        if (!method_exists($hookObject, 'processDatamap_afterDatabaseOperations')) {
6088
                            continue;
6089
                        }
6090
                        $hookObject->processDatamap_afterDatabaseOperations(
6091
                            $payload['status'],
6092
                            $tableName,
6093
                            $rawId,
6094
                            $payload['fieldArray'],
6095
                            $this
6096
                        );
6097
                    }
6098
                }
6099
            }
6100
        }
6101
        // Processes the remap stack actions:
6102
        if ($this->remapStackActions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->remapStackActions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
6103
            foreach ($this->remapStackActions as $action) {
6104
                if (isset($action['callback'], $action['arguments'])) {
6105
                    call_user_func_array($action['callback'], $action['arguments']);
6106
                }
6107
            }
6108
        }
6109
        // Reset:
6110
        $this->remapStack = [];
6111
        $this->remapStackRecords = [];
6112
        $this->remapStackActions = [];
6113
    }
6114
6115
    /**
6116
     * Updates FlexForm data.
6117
     *
6118
     * @param string $flexFormId e.g. <table>:<uid>:<field>
6119
     * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
6120
     */
6121
    protected function updateFlexFormData($flexFormId, array $modifications)
6122
    {
6123
        [$table, $uid, $field] = explode(':', $flexFormId, 3);
6124
6125
        if (!MathUtility::canBeInterpretedAsInteger($uid) && !empty($this->substNEWwithIDs[$uid])) {
6126
            $uid = $this->substNEWwithIDs[$uid];
6127
        }
6128
6129
        $record = $this->recordInfo($table, $uid, '*');
6130
6131
        if (!$table || !$uid || !$field || !is_array($record)) {
6132
            return;
6133
        }
6134
6135
        BackendUtility::workspaceOL($table, $record);
6136
6137
        // Get current data structure and value array:
6138
        $valueStructure = GeneralUtility::xml2array($record[$field]);
6139
6140
        // Do recursive processing of the XML data:
6141
        foreach ($modifications as $path => $value) {
6142
            $valueStructure['data'] = ArrayUtility::setValueByPath(
6143
                $valueStructure['data'],
6144
                $path,
6145
                $value
6146
            );
6147
        }
6148
6149
        if (is_array($valueStructure['data'])) {
6150
            // The return value should be compiled back into XML
6151
            $values = [
6152
                $field => $this->checkValue_flexArray2Xml($valueStructure, true),
6153
            ];
6154
6155
            $this->updateDB($table, $uid, $values);
6156
        }
6157
    }
6158
6159
    /**
6160
     * Triggers a remap action for a specific record.
6161
     *
6162
     * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).
6163
     * This method determines whether an action/modification is executed directly to a record
6164
     * or is postponed to happen after remapping data.
6165
     *
6166
     * @param string $table Name of the table
6167
     * @param string $id Id of the record (can also be a "NEW..." string)
6168
     * @param array $callback The method to be called
6169
     * @param array $arguments The arguments to be submitted to the callback method
6170
     * @param bool $forceRemapStackActions Whether to force to use the stack
6171
     * @see processRemapStack
6172
     */
6173
    protected function triggerRemapAction($table, $id, array $callback, array $arguments, $forceRemapStackActions = false)
6174
    {
6175
        // Check whether the affected record is marked to be remapped:
6176
        if (!$forceRemapStackActions && !isset($this->remapStackRecords[$table][$id]) && !isset($this->remapStackChildIds[$id])) {
6177
            call_user_func_array($callback, $arguments);
6178
        } else {
6179
            $this->addRemapAction($table, $id, $callback, $arguments);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of TYPO3\CMS\Core\DataHandl...ndler::addRemapAction(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

6179
            $this->addRemapAction($table, /** @scrutinizer ignore-type */ $id, $callback, $arguments);
Loading history...
6180
        }
6181
    }
6182
6183
    /**
6184
     * Adds an instruction to the remap action stack (used with IRRE).
6185
     *
6186
     * @param string $table The affected table
6187
     * @param int $id The affected ID
6188
     * @param array $callback The callback information (object and method)
6189
     * @param array $arguments The arguments to be used with the callback
6190
     * @internal should only be used from within DataHandler
6191
     */
6192
    public function addRemapAction($table, $id, array $callback, array $arguments)
6193
    {
6194
        $this->remapStackActions[] = [
6195
            'affects' => [
6196
                'table' => $table,
6197
                'id' => $id
6198
            ],
6199
            'callback' => $callback,
6200
            'arguments' => $arguments
6201
        ];
6202
    }
6203
6204
    /**
6205
     * If a parent record was versionized on a workspace in $this->process_datamap,
6206
     * it might be possible, that child records (e.g. on using IRRE) were affected.
6207
     * This function finds these relations and updates their uids in the $incomingFieldArray.
6208
     * The $incomingFieldArray is updated by reference!
6209
     *
6210
     * @param string $table Table name of the parent record
6211
     * @param int $id Uid of the parent record
6212
     * @param array $incomingFieldArray Reference to the incomingFieldArray of process_datamap
6213
     * @param array $registerDBList Reference to the $registerDBList array that was created/updated by versionizing calls to DataHandler in process_datamap.
6214
     * @internal should only be used from within DataHandler
6215
     */
6216
    public function getVersionizedIncomingFieldArray($table, $id, &$incomingFieldArray, &$registerDBList)
6217
    {
6218
        if (is_array($registerDBList[$table][$id])) {
6219
            foreach ($incomingFieldArray as $field => $value) {
6220
                $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
6221
                if ($registerDBList[$table][$id][$field] && ($foreignTable = $fieldConf['foreign_table'])) {
6222
                    $newValueArray = [];
6223
                    $origValueArray = is_array($value) ? $value : explode(',', $value);
6224
                    // Update the uids of the copied records, but also take care about new records:
6225
                    foreach ($origValueArray as $childId) {
6226
                        $newValueArray[] = $this->autoVersionIdMap[$foreignTable][$childId] ?: $childId;
6227
                    }
6228
                    // Set the changed value to the $incomingFieldArray
6229
                    $incomingFieldArray[$field] = implode(',', $newValueArray);
6230
                }
6231
            }
6232
            // Clean up the $registerDBList array:
6233
            unset($registerDBList[$table][$id]);
6234
            if (empty($registerDBList[$table])) {
6235
                unset($registerDBList[$table]);
6236
            }
6237
        }
6238
    }
6239
6240
    /**
6241
     * Simple helper method to hard delete one row from table ignoring delete TCA field
6242
     *
6243
     * @param string $table A row from this table should be deleted
6244
     * @param int $uid Uid of row to be deleted
6245
     */
6246
    protected function hardDeleteSingleRecord(string $table, int $uid): void
6247
    {
6248
        GeneralUtility::makeInstance(ConnectionPool::class)
6249
            ->getConnectionForTable($table)
6250
            ->delete($table, ['uid' => $uid], [\PDO::PARAM_INT]);
6251
    }
6252
6253
    /*****************************
6254
     *
6255
     * Access control / Checking functions
6256
     *
6257
     *****************************/
6258
    /**
6259
     * Checking group modify_table access list
6260
     *
6261
     * @param string $table Table name
6262
     * @return bool Returns TRUE if the user has general access to modify the $table
6263
     * @internal should only be used from within DataHandler
6264
     */
6265
    public function checkModifyAccessList($table)
6266
    {
6267
        $res = $this->admin || (!$this->tableAdminOnly($table) && isset($this->BE_USER->groupData['tables_modify']) && GeneralUtility::inList($this->BE_USER->groupData['tables_modify'], $table));
6268
        // Hook 'checkModifyAccessList': Post-processing of the state of access
6269
        foreach ($this->getCheckModifyAccessListHookObjects() as $hookObject) {
6270
            /** @var DataHandlerCheckModifyAccessListHookInterface $hookObject */
6271
            $hookObject->checkModifyAccessList($res, $table, $this);
6272
        }
6273
        return $res;
6274
    }
6275
6276
    /**
6277
     * Checking if a record with uid $id from $table is in the BE_USERS webmounts which is required for editing etc.
6278
     *
6279
     * @param string $table Table name
6280
     * @param int $id UID of record
6281
     * @return bool Returns TRUE if OK. Cached results.
6282
     * @internal should only be used from within DataHandler
6283
     */
6284
    public function isRecordInWebMount($table, $id)
6285
    {
6286
        if (!isset($this->isRecordInWebMount_Cache[$table . ':' . $id])) {
6287
            $recP = $this->getRecordProperties($table, $id);
6288
            $this->isRecordInWebMount_Cache[$table . ':' . $id] = $this->isInWebMount($recP['event_pid']);
6289
        }
6290
        return $this->isRecordInWebMount_Cache[$table . ':' . $id];
6291
    }
6292
6293
    /**
6294
     * Checks if the input page ID is in the BE_USER webmounts
6295
     *
6296
     * @param int $pid Page ID to check
6297
     * @return bool TRUE if OK. Cached results.
6298
     * @internal should only be used from within DataHandler
6299
     */
6300
    public function isInWebMount($pid)
6301
    {
6302
        if (!isset($this->isInWebMount_Cache[$pid])) {
6303
            $this->isInWebMount_Cache[$pid] = $this->BE_USER->isInWebMount($pid);
6304
        }
6305
        return $this->isInWebMount_Cache[$pid];
6306
    }
6307
6308
    /**
6309
     * Checks if user may update a record with uid=$id from $table
6310
     *
6311
     * @param string $table Record table
6312
     * @param int $id Record UID
6313
     * @param array|bool $data Record data
6314
     * @param array $hookObjectsArr Hook objects
6315
     * @return bool Returns TRUE if the user may update the record given by $table and $id
6316
     * @internal should only be used from within DataHandler
6317
     */
6318
    public function checkRecordUpdateAccess($table, $id, $data = false, $hookObjectsArr = null)
6319
    {
6320
        $res = null;
6321
        if (is_array($hookObjectsArr)) {
6322
            foreach ($hookObjectsArr as $hookObj) {
6323
                if (method_exists($hookObj, 'checkRecordUpdateAccess')) {
6324
                    $res = $hookObj->checkRecordUpdateAccess($table, $id, $data, $res, $this);
6325
                }
6326
            }
6327
            if (isset($res)) {
6328
                return (bool)$res;
6329
            }
6330
        }
6331
        $res = false;
6332
6333
        if ($GLOBALS['TCA'][$table] && (int)$id > 0) {
6334
            $cacheId = 'checkRecordUpdateAccess_' . $table . '_' . $id;
6335
6336
            // If information is cached, return it
6337
            $cachedValue = $this->runtimeCache->get($cacheId);
6338
            if (!empty($cachedValue)) {
6339
                return $cachedValue;
6340
            }
6341
6342
            if ($table === 'pages' || ($table === 'sys_file_reference' && array_key_exists('pages', $this->datamap))) {
6343
                // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6344
                $perms = Permission::PAGE_EDIT;
6345
            } else {
6346
                $perms = Permission::CONTENT_EDIT;
6347
            }
6348
            if ($this->doesRecordExist($table, $id, $perms)) {
6349
                $res = 1;
6350
            }
6351
            // Cache the result
6352
            $this->runtimeCache->set($cacheId, $res);
6353
        }
6354
        return $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $res also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
6355
    }
6356
6357
    /**
6358
     * Checks if user may insert a record from $insertTable on $pid
6359
     *
6360
     * @param string $insertTable Tablename to check
6361
     * @param int $pid Integer PID
6362
     * @param int $action For logging: Action number.
6363
     * @return bool Returns TRUE if the user may insert a record from table $insertTable on page $pid
6364
     * @internal should only be used from within DataHandler
6365
     */
6366
    public function checkRecordInsertAccess($insertTable, $pid, $action = SystemLogDatabaseAction::INSERT)
6367
    {
6368
        $pid = (int)$pid;
6369
        if ($pid < 0) {
6370
            return false;
6371
        }
6372
        // If information is cached, return it
6373
        if (isset($this->recInsertAccessCache[$insertTable][$pid])) {
6374
            return $this->recInsertAccessCache[$insertTable][$pid];
6375
        }
6376
6377
        $res = false;
6378
        if ($insertTable === 'pages') {
6379
            $perms = Permission::PAGE_NEW;
6380
        } elseif (($insertTable === 'sys_file_reference') && array_key_exists('pages', $this->datamap)) {
6381
            // @todo: find a more generic way to handle content relations of a page (without needing content editing access to that page)
6382
            $perms = Permission::PAGE_EDIT;
6383
        } else {
6384
            $perms = Permission::CONTENT_EDIT;
6385
        }
6386
        $pageExists = (bool)$this->doesRecordExist('pages', $pid, $perms);
6387
        // If either admin and root-level or if page record exists and 1) if 'pages' you may create new ones 2) if page-content, new content items may be inserted on the $pid page
6388
        if ($pageExists || $pid === 0 && ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($insertTable))) {
6389
            // Check permissions
6390
            if ($this->isTableAllowedForThisPage($pid, $insertTable)) {
6391
                $res = true;
6392
                // Cache the result
6393
                $this->recInsertAccessCache[$insertTable][$pid] = $res;
6394
            } elseif ($this->enableLogging) {
6395
                $propArr = $this->getRecordProperties('pages', $pid);
6396
                $this->log($insertTable, $pid, $action, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert record on page \'%s\' (%s) where this table, %s, is not allowed', 11, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6397
            }
6398
        } elseif ($this->enableLogging) {
6399
            $propArr = $this->getRecordProperties('pages', $pid);
6400
            $this->log($insertTable, $pid, $action, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to insert a record on page \'%s\' (%s) from table \'%s\' without permissions. Or non-existing page.', 12, [$propArr['header'], $pid, $insertTable], $propArr['event_pid']);
6401
        }
6402
        return $res;
6403
    }
6404
6405
    /**
6406
     * Checks if a table is allowed on a certain page id according to allowed tables set for the page "doktype" and its [ctrl][rootLevel]-settings if any.
6407
     *
6408
     * @param int $page_uid Page id for which to check, including 0 (zero) if checking for page tree root.
6409
     * @param string $checkTable Table name to check
6410
     * @return bool TRUE if OK
6411
     * @internal should only be used from within DataHandler
6412
     */
6413
    public function isTableAllowedForThisPage($page_uid, $checkTable)
6414
    {
6415
        $page_uid = (int)$page_uid;
6416
        $rootLevelSetting = (int)$GLOBALS['TCA'][$checkTable]['ctrl']['rootLevel'];
6417
        // Check if rootLevel flag is set and we're trying to insert on rootLevel - and reversed - and that the table is not "pages" which are allowed anywhere.
6418
        if ($checkTable !== 'pages' && $rootLevelSetting !== -1 && ($rootLevelSetting xor !$page_uid)) {
6419
            return false;
6420
        }
6421
        $allowed = false;
6422
        // Check root-level
6423
        if (!$page_uid) {
6424
            if ($this->admin || BackendUtility::isRootLevelRestrictionIgnored($checkTable)) {
6425
                $allowed = true;
6426
            }
6427
        } else {
6428
            // Check non-root-level
6429
            $doktype = $this->pageInfo($page_uid, 'doktype');
6430
            $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6431
            $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6432
            // If all tables or the table is listed as an allowed type, return TRUE
6433
            if (strpos($allowedTableList, '*') !== false || in_array($checkTable, $allowedArray, true)) {
6434
                $allowed = true;
6435
            }
6436
        }
6437
        return $allowed;
6438
    }
6439
6440
    /**
6441
     * Checks if record can be selected based on given permission criteria
6442
     *
6443
     * @param string $table Record table name
6444
     * @param int $id Record UID
6445
     * @param int $perms Permission restrictions to observe: integer that will be bitwise AND'ed.
6446
     * @return bool Returns TRUE if the record given by $table, $id and $perms can be selected
6447
     *
6448
     * @throws \RuntimeException
6449
     * @internal should only be used from within DataHandler
6450
     */
6451
    public function doesRecordExist($table, $id, int $perms)
6452
    {
6453
        return $this->recordInfoWithPermissionCheck($table, $id, $perms, 'uid, pid') !== false;
6454
    }
6455
6456
    /**
6457
     * Looks up a page based on permissions.
6458
     *
6459
     * @param int $id Page id
6460
     * @param int $perms Permission integer
6461
     * @param array $columns Columns to select
6462
     * @return bool|array
6463
     * @internal
6464
     * @see doesRecordExist()
6465
     */
6466
    protected function doesRecordExist_pageLookUp($id, $perms, $columns = ['uid'])
6467
    {
6468
        $permission = new Permission($perms);
6469
        $cacheId = md5('doesRecordExist_pageLookUp_' . $id . '_' . $perms . '_' . implode(
6470
            '_',
6471
            $columns
6472
        ) . '_' . (string)$this->admin);
6473
6474
        // If result is cached, return it
6475
        $cachedResult = $this->runtimeCache->get($cacheId);
6476
        if (!empty($cachedResult)) {
6477
            return $cachedResult;
6478
        }
6479
6480
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6481
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6482
        $queryBuilder
6483
            ->select(...$columns)
6484
            ->from('pages')
6485
            ->where($queryBuilder->expr()->eq(
6486
                'uid',
6487
                $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
6488
            ));
6489
        if (!$permission->nothingIsGranted() && !$this->admin) {
6490
            $queryBuilder->andWhere($this->BE_USER->getPagePermsClause($perms));
6491
        }
6492
        if (!$this->admin && $GLOBALS['TCA']['pages']['ctrl']['editlock'] &&
6493
            ($permission->editPagePermissionIsGranted() || $permission->deletePagePermissionIsGranted() || $permission->editContentPermissionIsGranted())
6494
        ) {
6495
            $queryBuilder->andWhere($queryBuilder->expr()->eq(
6496
                $GLOBALS['TCA']['pages']['ctrl']['editlock'],
6497
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
6498
            ));
6499
        }
6500
6501
        $row = $queryBuilder->execute()->fetch();
6502
        $this->runtimeCache->set($cacheId, $row);
6503
6504
        return $row;
6505
    }
6506
6507
    /**
6508
     * Checks if a whole branch of pages exists
6509
     *
6510
     * Tests the branch under $pid like doesRecordExist(), but it doesn't test the page with $pid as uid - use doesRecordExist() for this purpose.
6511
     * If $recurse is set, the function will follow subpages. This MUST be set, if we need the id-list for deleting pages or else we get an incomplete list
6512
     *
6513
     * @param string $inList List of page uids, this is added to and returned in the end
6514
     * @param int $pid Page ID to select subpages from.
6515
     * @param int $perms Perms integer to check each page record for.
6516
     * @param bool $recurse Recursion flag: If set, it will go out through the branch.
6517
     * @return string|int List of page IDs in branch, if there are subpages, empty string if there are none or -1 if no permission
6518
     * @internal should only be used from within DataHandler
6519
     */
6520
    public function doesBranchExist($inList, $pid, $perms, $recurse)
6521
    {
6522
        $pid = (int)$pid;
6523
        $perms = (int)$perms;
6524
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6525
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6526
        $result = $queryBuilder
6527
            ->select('uid', 'perms_userid', 'perms_groupid', 'perms_user', 'perms_group', 'perms_everybody')
6528
            ->from('pages')
6529
            ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
6530
            ->orderBy('sorting')
6531
            ->execute();
6532
        while ($row = $result->fetch()) {
6533
            // IF admin, then it's OK
6534
            if ($this->admin || $this->BE_USER->doesUserHaveAccess($row, $perms)) {
6535
                $inList .= $row['uid'] . ',';
6536
                if ($recurse) {
6537
                    // Follow the subpages recursively...
6538
                    $inList = $this->doesBranchExist($inList, $row['uid'], $perms, $recurse);
6539
                    if ($inList === -1) {
6540
                        return -1;
6541
                    }
6542
                }
6543
            } else {
6544
                // No permissions
6545
                return -1;
6546
            }
6547
        }
6548
        return $inList;
6549
    }
6550
6551
    /**
6552
     * Checks if the $table is readOnly
6553
     *
6554
     * @param string $table Table name
6555
     * @return bool TRUE, if readonly
6556
     * @internal should only be used from within DataHandler
6557
     */
6558
    public function tableReadOnly($table)
6559
    {
6560
        // Returns TRUE if table is readonly
6561
        return (bool)$GLOBALS['TCA'][$table]['ctrl']['readOnly'];
6562
    }
6563
6564
    /**
6565
     * Checks if the $table is only editable by admin-users
6566
     *
6567
     * @param string $table Table name
6568
     * @return bool TRUE, if readonly
6569
     * @internal should only be used from within DataHandler
6570
     */
6571
    public function tableAdminOnly($table)
6572
    {
6573
        // Returns TRUE if table is admin-only
6574
        return !empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly']);
6575
    }
6576
6577
    /**
6578
     * Checks if page $id is a uid in the rootline of page id $destinationId
6579
     * Used when moving a page
6580
     *
6581
     * @param int $destinationId Destination Page ID to test
6582
     * @param int $id Page ID to test for presence inside Destination
6583
     * @return bool Returns FALSE if ID is inside destination (including equal to)
6584
     * @internal should only be used from within DataHandler
6585
     */
6586
    public function destNotInsideSelf($destinationId, $id)
6587
    {
6588
        $loopCheck = 100;
6589
        $destinationId = (int)$destinationId;
6590
        $id = (int)$id;
6591
        if ($destinationId === $id) {
6592
            return false;
6593
        }
6594
        while ($destinationId !== 0 && $loopCheck > 0) {
6595
            $loopCheck--;
6596
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6597
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6598
            $result = $queryBuilder
6599
                ->select('pid', 'uid', 't3ver_oid', 't3ver_wsid')
6600
                ->from('pages')
6601
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($destinationId, \PDO::PARAM_INT)))
6602
                ->execute();
6603
            if ($row = $result->fetch()) {
6604
                BackendUtility::fixVersioningPid('pages', $row);
6605
                if ($row['pid'] == $id) {
6606
                    return false;
6607
                }
6608
                $destinationId = (int)$row['pid'];
6609
            } else {
6610
                return false;
6611
            }
6612
        }
6613
        return true;
6614
    }
6615
6616
    /**
6617
     * Generate an array of fields to be excluded from editing for the user. Based on "exclude"-field in TCA and a look up in non_exclude_fields
6618
     * Will also generate this list for admin-users so they must be check for before calling the function
6619
     *
6620
     * @return array Array of [table]-[field] pairs to exclude from editing.
6621
     * @internal should only be used from within DataHandler
6622
     */
6623
    public function getExcludeListArray()
6624
    {
6625
        $list = [];
6626
        if (isset($this->BE_USER->groupData['non_exclude_fields'])) {
6627
            $nonExcludeFieldsArray = array_flip(GeneralUtility::trimExplode(',', $this->BE_USER->groupData['non_exclude_fields']));
6628
            foreach ($GLOBALS['TCA'] as $table => $tableConfiguration) {
6629
                if (isset($tableConfiguration['columns'])) {
6630
                    foreach ($tableConfiguration['columns'] as $field => $config) {
6631
                        $isExcludeField = ($config['exclude'] ?? false);
6632
                        $isOnlyVisibleForAdmins = ($GLOBALS['TCA'][$table]['columns'][$field]['displayCond'] ?? '') === 'HIDE_FOR_NON_ADMINS';
6633
                        $editorHasPermissionForThisField = isset($nonExcludeFieldsArray[$table . ':' . $field]);
6634
                        if ($isOnlyVisibleForAdmins || ($isExcludeField && !$editorHasPermissionForThisField)) {
6635
                            $list[] = $table . '-' . $field;
6636
                        }
6637
                    }
6638
                }
6639
            }
6640
        }
6641
6642
        return $list;
6643
    }
6644
6645
    /**
6646
     * Checks if there are records on a page from tables that are not allowed
6647
     *
6648
     * @param int $page_uid Page ID
6649
     * @param int $doktype Page doktype
6650
     * @return bool|array Returns a list of the tables that are 'present' on the page but not allowed with the page_uid/doktype
6651
     * @internal should only be used from within DataHandler
6652
     */
6653
    public function doesPageHaveUnallowedTables($page_uid, $doktype)
6654
    {
6655
        $page_uid = (int)$page_uid;
6656
        if (!$page_uid) {
6657
            // Not a number. Probably a new page
6658
            return false;
6659
        }
6660
        $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
6661
        // If all tables are allowed, return early
6662
        if (strpos($allowedTableList, '*') !== false) {
6663
            return false;
6664
        }
6665
        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
6666
        $tableList = [];
6667
        $allTableNames = $this->compileAdminTables();
6668
        foreach ($allTableNames as $table) {
6669
            // If the table is not in the allowed list, check if there are records...
6670
            if (in_array($table, $allowedArray, true)) {
6671
                continue;
6672
            }
6673
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6674
            $queryBuilder->getRestrictions()->removeAll();
6675
            $count = $queryBuilder
6676
                ->count('uid')
6677
                ->from($table)
6678
                ->where($queryBuilder->expr()->eq(
6679
                    'pid',
6680
                    $queryBuilder->createNamedParameter($page_uid, \PDO::PARAM_INT)
6681
                ))
6682
                ->execute()
6683
                ->fetchColumn(0);
6684
            if ($count) {
6685
                $tableList[] = $table;
6686
            }
6687
        }
6688
        return implode(',', $tableList);
0 ignored issues
show
Bug Best Practice introduced by
The expression return implode(',', $tableList) returns the type string which is incompatible with the documented return type array|boolean.
Loading history...
6689
    }
6690
6691
    /*****************************
6692
     *
6693
     * Information lookup
6694
     *
6695
     *****************************/
6696
    /**
6697
     * Returns the value of the $field from page $id
6698
     * NOTICE; the function caches the result for faster delivery next time. You can use this function repeatedly without performance loss since it doesn't look up the same record twice!
6699
     *
6700
     * @param int $id Page uid
6701
     * @param string $field Field name for which to return value
6702
     * @return string Value of the field. Result is cached in $this->pageCache[$id][$field] and returned from there next time!
6703
     * @internal should only be used from within DataHandler
6704
     */
6705
    public function pageInfo($id, $field)
6706
    {
6707
        if (!isset($this->pageCache[$id])) {
6708
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
6709
            $queryBuilder->getRestrictions()->removeAll();
6710
            $row = $queryBuilder
6711
                ->select('*')
6712
                ->from('pages')
6713
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6714
                ->execute()
6715
                ->fetch();
6716
            if ($row) {
6717
                $this->pageCache[$id] = $row;
6718
            }
6719
        }
6720
        return $this->pageCache[$id][$field];
6721
    }
6722
6723
    /**
6724
     * Returns the row of a record given by $table and $id and $fieldList (list of fields, may be '*')
6725
     * NOTICE: No check for deleted or access!
6726
     *
6727
     * @param string $table Table name
6728
     * @param int $id UID of the record from $table
6729
     * @param string $fieldList Field list for the SELECT query, eg. "*" or "uid,pid,...
6730
     * @return array|null Returns the selected record on success, otherwise NULL.
6731
     * @internal should only be used from within DataHandler
6732
     */
6733
    public function recordInfo($table, $id, $fieldList)
6734
    {
6735
        // Skip, if searching for NEW records or there's no TCA table definition
6736
        if ((int)$id === 0 || !isset($GLOBALS['TCA'][$table])) {
6737
            return null;
6738
        }
6739
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6740
        $queryBuilder->getRestrictions()->removeAll();
6741
        $result = $queryBuilder
6742
            ->select(...GeneralUtility::trimExplode(',', $fieldList))
6743
            ->from($table)
6744
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6745
            ->execute()
6746
            ->fetch();
6747
        return $result ?: null;
6748
    }
6749
6750
    /**
6751
     * Checks if record exists with and without permission check and returns that row
6752
     *
6753
     * @param string $table Record table name
6754
     * @param int $id Record UID
6755
     * @param int $perms Permission restrictions to observe: An integer that will be bitwise AND'ed.
6756
     * @param string $fieldList - fields - default is '*'
6757
     * @throws \RuntimeException
6758
     * @return array|bool Row if exists and accessible, false otherwise
6759
     */
6760
    protected function recordInfoWithPermissionCheck(string $table, int $id, int $perms, string $fieldList = '*')
6761
    {
6762
        if ($this->bypassAccessCheckForRecords) {
6763
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6764
6765
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6766
            $queryBuilder->getRestrictions()->removeAll();
6767
6768
            $record = $queryBuilder->select(...$columns)
6769
                ->from($table)
6770
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6771
                ->execute()
6772
                ->fetch();
6773
6774
            return $record ?: false;
6775
        }
6776
        if (!$perms) {
6777
            throw new \RuntimeException('Internal ERROR: no permissions to check for non-admin user', 1270853920);
6778
        }
6779
        // For all tables: Check if record exists:
6780
        $isWebMountRestrictionIgnored = BackendUtility::isWebMountRestrictionIgnored($table);
6781
        if (is_array($GLOBALS['TCA'][$table]) && $id > 0 && ($this->admin || $isWebMountRestrictionIgnored || $this->isRecordInWebMount($table, $id))) {
6782
            $columns = GeneralUtility::trimExplode(',', $fieldList, true);
6783
            if ($table !== 'pages') {
6784
                // Find record without checking page
6785
                // @todo: This should probably check for editlock
6786
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6787
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
6788
                $output = $queryBuilder
6789
                    ->select(...$columns)
6790
                    ->from($table)
6791
                    ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
6792
                    ->execute()
6793
                    ->fetch();
6794
                BackendUtility::fixVersioningPid($table, $output, true);
6795
                // If record found, check page as well:
6796
                if (is_array($output)) {
6797
                    // Looking up the page for record:
6798
                    $pageRec = $this->doesRecordExist_pageLookUp($output['pid'], $perms);
6799
                    // Return TRUE if either a page was found OR if the PID is zero AND the user is ADMIN (in which case the record is at root-level):
6800
                    $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
6801
                    if (is_array($pageRec) || !$output['pid'] && ($this->admin || $isRootLevelRestrictionIgnored)) {
6802
                        return $output;
6803
                    }
6804
                }
6805
                return false;
6806
            }
6807
            return $this->doesRecordExist_pageLookUp($id, $perms, $columns);
6808
        }
6809
        return false;
6810
    }
6811
6812
    /**
6813
     * Returns an array with record properties, like header and pid
6814
     * No check for deleted or access is done!
6815
     * For versionized records, pid is resolved to its live versions pid.
6816
     * Used for logging
6817
     *
6818
     * @param string $table Table name
6819
     * @param int $id Uid of record
6820
     * @param bool $noWSOL If set, no workspace overlay is performed
6821
     * @return array Properties of record
6822
     * @internal should only be used from within DataHandler
6823
     */
6824
    public function getRecordProperties($table, $id, $noWSOL = false)
6825
    {
6826
        $row = $table === 'pages' && !$id ? ['title' => '[root-level]', 'uid' => 0, 'pid' => 0] : $this->recordInfo($table, $id, '*');
6827
        if (!$noWSOL) {
6828
            BackendUtility::workspaceOL($table, $row);
6829
        }
6830
        return $this->getRecordPropertiesFromRow($table, $row);
6831
    }
6832
6833
    /**
6834
     * Returns an array with record properties, like header and pid, based on the row
6835
     *
6836
     * @param string $table Table name
6837
     * @param array $row Input row
6838
     * @return array|null Output array
6839
     * @internal should only be used from within DataHandler
6840
     */
6841
    public function getRecordPropertiesFromRow($table, $row)
6842
    {
6843
        if ($GLOBALS['TCA'][$table]) {
6844
            BackendUtility::fixVersioningPid($table, $row);
6845
            $liveUid = ($row['t3ver_oid'] ?? null) ? $row['t3ver_oid'] : $row['uid'];
6846
            return [
6847
                'header' => BackendUtility::getRecordTitle($table, $row),
6848
                'pid' => $row['pid'],
6849
                'event_pid' => $this->eventPid($table, (int)$liveUid, $row['pid']),
6850
                't3ver_state' => BackendUtility::isTableWorkspaceEnabled($table) ? $row['t3ver_state'] : '',
6851
                '_ORIG_pid' => $row['_ORIG_pid']
6852
            ];
6853
        }
6854
        return null;
6855
    }
6856
6857
    /**
6858
     * @param string $table
6859
     * @param int $uid
6860
     * @param int $pid
6861
     * @return int
6862
     * @internal should only be used from within DataHandler
6863
     */
6864
    public function eventPid($table, $uid, $pid)
6865
    {
6866
        return $table === 'pages' ? $uid : $pid;
6867
    }
6868
6869
    /*********************************************
6870
     *
6871
     * Storing data to Database Layer
6872
     *
6873
     ********************************************/
6874
    /**
6875
     * Update database record
6876
     * Does not check permissions but expects them to be verified on beforehand
6877
     *
6878
     * @param string $table Record table name
6879
     * @param int $id Record uid
6880
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done.
6881
     * @internal should only be used from within DataHandler
6882
     */
6883
    public function updateDB($table, $id, $fieldArray)
6884
    {
6885
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && (int)$id) {
6886
            // Do NOT update the UID field, ever!
6887
            unset($fieldArray['uid']);
6888
            if (!empty($fieldArray)) {
6889
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6890
6891
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6892
6893
                $types = [];
6894
                $platform = $connection->getDatabasePlatform();
6895
                if ($platform instanceof SQLServerPlatform) {
6896
                    // mssql needs to set proper PARAM_LOB and others to update fields
6897
                    $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
6898
                    foreach ($fieldArray as $columnName => $columnValue) {
6899
                        $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
6900
                    }
6901
                }
6902
6903
                // Execute the UPDATE query:
6904
                $updateErrorMessage = '';
6905
                try {
6906
                    $connection->update($table, $fieldArray, ['uid' => (int)$id], $types);
6907
                } catch (DBALException $e) {
6908
                    $updateErrorMessage = $e->getPrevious()->getMessage();
6909
                }
6910
                // If succeeds, do...:
6911
                if ($updateErrorMessage === '') {
6912
                    // Update reference index:
6913
                    $this->updateRefIndex($table, $id);
6914
                    // Set History data
6915
                    $historyEntryId = 0;
6916
                    if (isset($this->historyRecords[$table . ':' . $id])) {
6917
                        $historyEntryId = $this->getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id], $this->correlationId);
6918
                    }
6919
                    if ($this->enableLogging) {
6920
                        if ($this->checkStoredRecords) {
6921
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::UPDATE);
6922
                        } else {
6923
                            $newRow = $fieldArray;
6924
                            $newRow['uid'] = $id;
6925
                        }
6926
                        // Set log entry:
6927
                        $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
6928
                        $isOfflineVersion = (bool)($newRow['t3ver_oid'] ?? 0);
6929
                        $this->log($table, $id, SystemLogDatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, 'Record \'%s\' (%s) was updated.' . ($isOfflineVersion ? ' (Offline version).' : ' (Online).'), 10, [$propArr['header'], $table . ':' . $id, 'history' => $historyEntryId], $propArr['event_pid']);
6930
                    }
6931
                    // Clear cache for relevant pages:
6932
                    $this->registerRecordIdForPageCacheClearing($table, $id);
6933
                    // Unset the pageCache for the id if table was page.
6934
                    if ($table === 'pages') {
6935
                        unset($this->pageCache[$id]);
6936
                    }
6937
                } else {
6938
                    $this->log($table, $id, SystemLogDatabaseAction::UPDATE, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$updateErrorMessage, $table . ':' . $id]);
6939
                }
6940
            }
6941
        }
6942
    }
6943
6944
    /**
6945
     * Insert into database
6946
     * Does not check permissions but expects them to be verified on beforehand
6947
     *
6948
     * @param string $table Record table name
6949
     * @param string $id "NEW...." uid string
6950
     * @param array $fieldArray Array of field=>value pairs to insert. FIELDS MUST MATCH the database FIELDS. No check is done. "pid" must point to the destination of the record!
6951
     * @param bool $newVersion Set to TRUE if new version is created.
6952
     * @param int $suggestedUid Suggested UID value for the inserted record. See the array $this->suggestedInsertUids; Admin-only feature
6953
     * @param bool $dontSetNewIdIndex If TRUE, the ->substNEWwithIDs array is not updated. Only useful in very rare circumstances!
6954
     * @return int|null Returns ID on success.
6955
     * @internal should only be used from within DataHandler
6956
     */
6957
    public function insertDB($table, $id, $fieldArray, $newVersion = false, $suggestedUid = 0, $dontSetNewIdIndex = false)
6958
    {
6959
        if (is_array($fieldArray) && is_array($GLOBALS['TCA'][$table]) && isset($fieldArray['pid'])) {
6960
            // Do NOT insert the UID field, ever!
6961
            unset($fieldArray['uid']);
6962
            if (!empty($fieldArray)) {
6963
                // Check for "suggestedUid".
6964
                // This feature is used by the import functionality to force a new record to have a certain UID value.
6965
                // This is only recommended for use when the destination server is a passive mirror of another server.
6966
                // As a security measure this feature is available only for Admin Users (for now)
6967
                $suggestedUid = (int)$suggestedUid;
6968
                if ($this->BE_USER->isAdmin() && $suggestedUid && $this->suggestedInsertUids[$table . ':' . $suggestedUid]) {
6969
                    // When the value of ->suggestedInsertUids[...] is "DELETE" it will try to remove the previous record
6970
                    if ($this->suggestedInsertUids[$table . ':' . $suggestedUid] === 'DELETE') {
6971
                        $this->hardDeleteSingleRecord($table, (int)$suggestedUid);
6972
                    }
6973
                    $fieldArray['uid'] = $suggestedUid;
6974
                }
6975
                $fieldArray = $this->insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray);
6976
                $typeArray = [];
6977
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])
6978
                    && array_key_exists($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'], $fieldArray)
6979
                ) {
6980
                    $typeArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = Connection::PARAM_LOB;
6981
                }
6982
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6983
                $insertErrorMessage = '';
6984
                try {
6985
                    // Execute the INSERT query:
6986
                    $connection->insert(
6987
                        $table,
6988
                        $fieldArray,
6989
                        $typeArray
6990
                    );
6991
                } catch (DBALException $e) {
6992
                    $insertErrorMessage = $e->getPrevious()->getMessage();
6993
                }
6994
                // If succees, do...:
6995
                if ($insertErrorMessage === '') {
6996
                    // Set mapping for NEW... -> real uid:
6997
                    // the NEW_id now holds the 'NEW....' -id
6998
                    $NEW_id = $id;
6999
                    $id = $this->postProcessDatabaseInsert($connection, $table, $suggestedUid);
7000
7001
                    if (!$dontSetNewIdIndex) {
7002
                        $this->substNEWwithIDs[$NEW_id] = $id;
7003
                        $this->substNEWwithIDs_table[$NEW_id] = $table;
7004
                    }
7005
                    $newRow = [];
7006
                    if ($this->enableLogging) {
7007
                        // Checking the record is properly saved if configured
7008
                        if ($this->checkStoredRecords) {
7009
                            $newRow = $this->checkStoredRecord($table, $id, $fieldArray, SystemLogDatabaseAction::INSERT);
7010
                        } else {
7011
                            $newRow = $fieldArray;
7012
                            $newRow['uid'] = $id;
7013
                        }
7014
                    }
7015
                    // Update reference index:
7016
                    $this->updateRefIndex($table, $id);
7017
7018
                    // Store in history
7019
                    $this->getRecordHistoryStore()->addRecord($table, $id, $newRow, $this->correlationId);
0 ignored issues
show
Bug introduced by
It seems like $newRow can also be of type null; however, parameter $payload of TYPO3\CMS\Core\DataHandl...storyStore::addRecord() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7019
                    $this->getRecordHistoryStore()->addRecord($table, $id, /** @scrutinizer ignore-type */ $newRow, $this->correlationId);
Loading history...
7020
7021
                    if ($newVersion) {
7022
                        if ($this->enableLogging) {
7023
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7024
                            $this->log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::MESSAGE, 'New version created of table \'%s\', uid \'%s\'. UID of new version is \'%s\'', 10, [$table, $fieldArray['t3ver_oid'], $id], $propArr['event_pid'], $NEW_id);
7025
                        }
7026
                    } else {
7027
                        if ($this->enableLogging) {
7028
                            $propArr = $this->getRecordPropertiesFromRow($table, $newRow);
7029
                            $page_propArr = $this->getRecordProperties('pages', $propArr['pid']);
7030
                            $this->log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::MESSAGE, 'Record \'%s\' (%s) was inserted on page \'%s\' (%s)', 10, [$propArr['header'], $table . ':' . $id, $page_propArr['header'], $newRow['pid']], $newRow['pid'], $NEW_id);
7031
                        }
7032
                        // Clear cache for relevant pages:
7033
                        $this->registerRecordIdForPageCacheClearing($table, $id);
7034
                    }
7035
                    return $id;
7036
                }
7037
                if ($this->enableLogging) {
7038
                    $this->log($table, $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $recuid of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7038
                    $this->log($table, /** @scrutinizer ignore-type */ $id, SystemLogDatabaseAction::INSERT, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'SQL error: \'%s\' (%s)', 12, [$insertErrorMessage, $table . ':' . $id]);
Loading history...
7039
                }
7040
            }
7041
        }
7042
        return null;
7043
    }
7044
7045
    /**
7046
     * Checking stored record to see if the written values are properly updated.
7047
     *
7048
     * @param string $table Record table name
7049
     * @param int $id Record uid
7050
     * @param array $fieldArray Array of field=>value pairs to insert/update
7051
     * @param string $action Action, for logging only.
7052
     * @return array|null Selected row
7053
     * @see insertDB()
7054
     * @see updateDB()
7055
     * @internal should only be used from within DataHandler
7056
     */
7057
    public function checkStoredRecord($table, $id, $fieldArray, $action)
7058
    {
7059
        $id = (int)$id;
7060
        if (is_array($GLOBALS['TCA'][$table]) && $id) {
7061
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7062
            $queryBuilder->getRestrictions()->removeAll();
7063
7064
            $row = $queryBuilder
7065
                ->select('*')
7066
                ->from($table)
7067
                ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7068
                ->execute()
7069
                ->fetch();
7070
7071
            if (!empty($row)) {
7072
                // Traverse array of values that was inserted into the database and compare with the actually stored value:
7073
                $errors = [];
7074
                foreach ($fieldArray as $key => $value) {
7075
                    if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
7076
                        if (is_float($row[$key])) {
7077
                            // if the database returns the value as double, compare it as double
7078
                            if ((double)$value !== (double)$row[$key]) {
7079
                                $errors[] = $key;
7080
                            }
7081
                        } else {
7082
                            $dbType = $GLOBALS['TCA'][$table]['columns'][$key]['config']['dbType'] ?? false;
7083
                            if ($dbType === 'datetime' || $dbType === 'time') {
7084
                                $row[$key] = $this->normalizeTimeFormat($table, $row[$key], $dbType);
7085
                            }
7086
                            if ((string)$value !== (string)$row[$key]) {
7087
                                // The is_numeric check catches cases where we want to store a float/double value
7088
                                // and database returns the field as a string with the least required amount of
7089
                                // significant digits, i.e. "0.00" being saved and "0" being read back.
7090
                                if (is_numeric($value) && is_numeric($row[$key])) {
7091
                                    if ((double)$value === (double)$row[$key]) {
7092
                                        continue;
7093
                                    }
7094
                                }
7095
                                $errors[] = $key;
7096
                            }
7097
                        }
7098
                    }
7099
                }
7100
                // Set log message if there were fields with unmatching values:
7101
                if (!empty($errors)) {
7102
                    $message = sprintf(
7103
                        'These fields of record %d in table "%s" have not been saved correctly: %s! The values might have changed due to type casting of the database.',
7104
                        $id,
7105
                        $table,
7106
                        implode(', ', $errors)
7107
                    );
7108
                    $this->log($table, $id, $action, 0, SystemLogErrorClassification::USER_ERROR, $message);
0 ignored issues
show
Bug introduced by
$action of type string is incompatible with the type integer expected by parameter $action of TYPO3\CMS\Core\DataHandling\DataHandler::log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7108
                    $this->log($table, $id, /** @scrutinizer ignore-type */ $action, 0, SystemLogErrorClassification::USER_ERROR, $message);
Loading history...
7109
                }
7110
                // Return selected rows:
7111
                return $row;
7112
            }
7113
        }
7114
        return null;
7115
    }
7116
7117
    /**
7118
     * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset())
7119
     *
7120
     * This functionality is now moved into the RecordHistoryStore and can be used instead.
7121
     *
7122
     * @param string $table Table name
7123
     * @param int $id Record ID
7124
     * @param int $logId Log entry ID, important for linking between log and history views
7125
     * @internal should only be used from within DataHandler
7126
     */
7127
    public function setHistory($table, $id, $logId)
7128
    {
7129
        if (isset($this->historyRecords[$table . ':' . $id])) {
7130
            $this->getRecordHistoryStore()->modifyRecord(
7131
                $table,
7132
                $id,
7133
                $this->historyRecords[$table . ':' . $id],
7134
                $this->correlationId
7135
            );
7136
        }
7137
    }
7138
7139
    /**
7140
     * @return RecordHistoryStore
7141
     */
7142
    protected function getRecordHistoryStore(): RecordHistoryStore
7143
    {
7144
        return GeneralUtility::makeInstance(
7145
            RecordHistoryStore::class,
7146
            RecordHistoryStore::USER_BACKEND,
7147
            $this->BE_USER->user['uid'],
7148
            $this->BE_USER->user['ses_backuserid'] ?? null,
7149
            $GLOBALS['EXEC_TIME'],
7150
            $this->BE_USER->workspace
7151
        );
7152
    }
7153
7154
    /**
7155
     * Register a table/uid combination in current user workspace for reference updating.
7156
     * Should be called on almost any update to a record which could affect references inside the record.
7157
     *
7158
     * @param string $table Table name
7159
     * @param int $uid Record UID
7160
     * @param int $workspace Workspace the record lives in
7161
     * @internal should only be used from within DataHandler
7162
     */
7163
    public function updateRefIndex($table, $uid, int $workspace = null): void
7164
    {
7165
        if ($workspace === null) {
7166
            $workspace = (int)$this->BE_USER->workspace;
7167
        }
7168
        $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $workspace);
7169
    }
7170
7171
    /**
7172
     * Delete rows from sys_refindex a table / uid combination is involved in:
7173
     * Either on left side (tablename + recuid) OR right side (ref_table + ref_uid).
7174
     * Useful in scenarios like workspace-discard where parents or children are hard deleted: The
7175
     * expensive updateRefIndex() does not need to be called since we can just drop straight ahead.
7176
     *
7177
     * @param string $table Table name, used as tablename and ref_table
7178
     * @param int $uid Record uid, used as recuid and ref_uid
7179
     * @param int $workspace Workspace the record lives in
7180
     */
7181
    public function registerReferenceIndexRowsForDrop(string $table, int $uid, int $workspace): void
7182
    {
7183
        $this->referenceIndexUpdater->registerForDrop($table, $uid, $workspace);
7184
    }
7185
7186
    /*********************************************
7187
     *
7188
     * Misc functions
7189
     *
7190
     ********************************************/
7191
    /**
7192
     * Returning sorting number for tables with a "sortby" column
7193
     * Using when new records are created and existing records are moved around.
7194
     *
7195
     * The strategy is:
7196
     *  - if no record exists: set interval as sorting number
7197
     *  - if inserted before an element: put in the middle of the existing elements
7198
     *  - if inserted behind the last element: add interval to last sorting number
7199
     *  - if collision: move all subsequent records by 2 * interval, insert new record with collision + interval
7200
     *
7201
     * How to calculate the maximum possible inserts for the worst case of adding all records to the top,
7202
     * such that the sorting number stays within INT_MAX
7203
     *
7204
     * i = interval (currently 256)
7205
     * c = number of inserts until collision
7206
     * s = max sorting number to reach (INT_MAX - 32bit)
7207
     * n = number of records (~83 million)
7208
     *
7209
     * c = 2 * g
7210
     * g = log2(i) / 2 + 1
7211
     * n = g * s / i - g + 1
7212
     *
7213
     * The algorithm can be tuned by adjusting the interval value.
7214
     * Higher value means less collisions, but also less inserts are possible to stay within INT_MAX.
7215
     *
7216
     * @param string $table Table name
7217
     * @param int $uid Uid of record to find sorting number for. May be zero in case of new.
7218
     * @param int $pid Positioning PID, either >=0 (pointing to page in which case we find sorting number for first record in page) or <0 (pointing to record in which case to find next sorting number after this record)
7219
     * @return int|array|bool|null Returns integer if PID is >=0, otherwise an array with PID and sorting number. Possibly FALSE in case of error.
7220
     * @internal should only be used from within DataHandler
7221
     */
7222
    public function getSortNumber($table, $uid, $pid)
7223
    {
7224
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7225
        if (!$sortColumn) {
7226
            return null;
7227
        }
7228
7229
        $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($table);
7230
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
7231
        $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7232
        $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7233
7234
        $queryBuilder
7235
            ->select($sortColumn, 'pid', 'uid')
7236
            ->from($table);
7237
        if ($considerWorkspaces) {
7238
            $queryBuilder->addSelect('t3ver_state');
7239
        }
7240
7241
        // find and return the sorting value for the first record on that pid
7242
        if ($pid >= 0) {
7243
            // Fetches the first record (lowest sorting) under this pid
7244
            $queryBuilder
7245
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)));
7246
7247
            if ($considerWorkspaces) {
7248
                $queryBuilder->andWhere(
7249
                    $queryBuilder->expr()->orX(
7250
                        $queryBuilder->expr()->eq('t3ver_oid', 0),
7251
                        $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER)
7252
                    )
7253
                );
7254
            }
7255
            $row = $queryBuilder
7256
                ->orderBy($sortColumn, 'ASC')
7257
                ->addOrderBy('uid', 'ASC')
7258
                ->setMaxResults(1)
7259
                ->execute()
7260
                ->fetch();
7261
7262
            if (!empty($row)) {
7263
                // The top record was the record itself, so we return its current sorting value
7264
                if ($row['uid'] == $uid) {
7265
                    return $row[$sortColumn];
7266
                }
7267
                // If the record sorting value < 1 we must resort all the records under this pid
7268
                if ($row[$sortColumn] < 1) {
7269
                    $this->increaseSortingOfFollowingRecords($table, (int)$pid);
7270
                    // Lowest sorting value after full resorting is $sortIntervals
7271
                    return $this->sortIntervals;
7272
                }
7273
                // Sorting number between current top element and zero
7274
                return floor($row[$sortColumn] / 2);
7275
            }
7276
            // No records, so we choose the default value as sorting-number
7277
            return $this->sortIntervals;
7278
        }
7279
7280
        // Find and return first possible sorting value AFTER record with given uid ($pid)
7281
        // Fetches the record which is supposed to be the prev record
7282
        $row = $queryBuilder
7283
                ->where($queryBuilder->expr()->eq(
7284
                    'uid',
7285
                    $queryBuilder->createNamedParameter(abs($pid), \PDO::PARAM_INT)
7286
                ))
7287
                ->execute()
7288
                ->fetch();
7289
7290
        // There is a previous record
7291
        if (!empty($row)) {
7292
            // Look if the record UID happens to be a versioned record. If so, find its live version.
7293
            // If this is already a moved record in workspace, this is not needed
7294
            if ((int)$row['t3ver_state'] !== VersionState::MOVE_POINTER && $lookForLiveVersion = BackendUtility::getLiveVersionOfRecord($table, $row['uid'], $sortColumn . ',pid,uid')) {
7295
                $row = $lookForLiveVersion;
7296
            } elseif ($considerWorkspaces && $this->BE_USER->workspace > 0) {
7297
                // In case the previous record is moved in the workspace, we need to fetch the information from this specific record
7298
                $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($this->BE_USER->workspace, $table, $row['uid'], $sortColumn . ',pid,uid,t3ver_state');
7299
                if (is_array($versionedRecord) && (int)$versionedRecord['t3ver_state'] === VersionState::MOVE_POINTER) {
7300
                    $row = $versionedRecord;
7301
                }
7302
            }
7303
            // If the record should be inserted after itself, keep the current sorting information:
7304
            if ((int)$row['uid'] === (int)$uid) {
7305
                $sortNumber = $row[$sortColumn];
7306
            } else {
7307
                $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
7308
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7309
7310
                $queryBuilder
7311
                        ->select($sortColumn, 'pid', 'uid')
7312
                        ->from($table)
7313
                        ->where(
7314
                            $queryBuilder->expr()->eq(
7315
                                'pid',
7316
                                $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
7317
                            ),
7318
                            $queryBuilder->expr()->gte(
7319
                                $sortColumn,
7320
                                $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7321
                            )
7322
                        )
7323
                        ->orderBy($sortColumn, 'ASC')
7324
                        ->addOrderBy('uid', 'DESC')
7325
                        ->setMaxResults(2);
7326
7327
                if ($considerWorkspaces) {
7328
                    $queryBuilder->andWhere(
7329
                        $queryBuilder->expr()->orX(
7330
                            $queryBuilder->expr()->eq('t3ver_oid', 0),
7331
                            $queryBuilder->expr()->eq('t3ver_state', VersionState::MOVE_POINTER)
7332
                        )
7333
                    );
7334
                }
7335
7336
                $subResults = $queryBuilder
7337
                    ->execute()
7338
                    ->fetchAll();
7339
                // Fetches the next record in order to calculate the in-between sortNumber
7340
                // There was a record afterwards
7341
                if (count($subResults) === 2) {
7342
                    // There was a record afterwards, fetch that
7343
                    $subrow = array_pop($subResults);
7344
                    // The sortNumber is found in between these values
7345
                    $sortNumber = $row[$sortColumn] + floor(($subrow[$sortColumn] - $row[$sortColumn]) / 2);
7346
                    // The sortNumber happened NOT to be between the two surrounding numbers, so we'll have to resort the list
7347
                    if ($sortNumber <= $row[$sortColumn] || $sortNumber >= $subrow[$sortColumn]) {
7348
                        $this->increaseSortingOfFollowingRecords($table, (int)$row['pid'], (int)$row[$sortColumn]);
7349
                        $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7350
                    }
7351
                } else {
7352
                    // If after the last record in the list, we just add the sortInterval to the last sortvalue
7353
                    $sortNumber = $row[$sortColumn] + $this->sortIntervals;
7354
                }
7355
            }
7356
            return ['pid' => $row['pid'], 'sortNumber' => $sortNumber];
7357
        }
7358
        if ($this->enableLogging) {
7359
            $propArr = $this->getRecordProperties($table, $uid);
7360
            // OK, don't insert $propArr['event_pid'] here...
7361
            $this->log($table, $uid, SystemLogDatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to move record \'%s\' (%s) to after a non-existing record (uid=%s)', 1, [$propArr['header'], $table . ':' . $uid, abs($pid)], $propArr['pid']);
7362
        }
7363
        // There MUST be a previous record or else this cannot work
7364
        return false;
7365
    }
7366
7367
    /**
7368
     * Increases sorting field value of all records with sorting higher than $sortingValue
7369
     *
7370
     * Used internally by getSortNumber() to "make space" in sorting values when inserting new record
7371
     *
7372
     * @param string $table Table name
7373
     * @param int $pid Page Uid in which to resort records
7374
     * @param int $sortingValue All sorting numbers larger than this number will be shifted
7375
     * @see getSortNumber()
7376
     */
7377
    protected function increaseSortingOfFollowingRecords(string $table, int $pid, int $sortingValue = null): void
7378
    {
7379
        $sortBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7380
        if ($sortBy) {
7381
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7382
7383
            $queryBuilder
7384
                ->update($table)
7385
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7386
                ->set($sortBy, $queryBuilder->quoteIdentifier($sortBy) . ' + ' . $this->sortIntervals . ' + ' . $this->sortIntervals, false);
7387
            if ($sortingValue !== null) {
7388
                $queryBuilder->andWhere($queryBuilder->expr()->gt($sortBy, $sortingValue));
7389
            }
7390
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
7391
                $queryBuilder
7392
                    ->andWhere(
7393
                        $queryBuilder->expr()->eq('t3ver_oid', 0)
7394
                    );
7395
            }
7396
7397
            $deleteColumn = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? '';
7398
            if ($deleteColumn) {
7399
                $queryBuilder->andWhere($queryBuilder->expr()->eq($deleteColumn, 0));
7400
            }
7401
7402
            $queryBuilder->execute();
7403
        }
7404
    }
7405
7406
    /**
7407
     * Returning uid of previous localized record, if any, for tables with a "sortby" column
7408
     * Used when new localized records are created so that localized records are sorted in the same order as the default language records
7409
     *
7410
     * For a given record (A) uid (record we're translating) it finds first default language record (from the same colpos)
7411
     * with sorting smaller than given record (B).
7412
     * Then it fetches a translated version of record B and returns it's uid.
7413
     *
7414
     * If there is no record B, or it has no translation in given language, the record A uid is returned.
7415
     * The localized record will be placed the after record which uid is returned.
7416
     *
7417
     * @param string $table Table name
7418
     * @param int $uid Uid of default language record
7419
     * @param int $pid Pid of default language record
7420
     * @param int $language Language of localization
7421
     * @return int uid of record after which the localized record should be inserted
7422
     */
7423
    protected function getPreviousLocalizedRecordUid($table, $uid, $pid, $language)
7424
    {
7425
        $previousLocalizedRecordUid = $uid;
7426
        $sortColumn = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? '';
7427
        if ($sortColumn) {
7428
            $select = [$sortColumn, 'pid', 'uid'];
7429
            // For content elements, we also need the colPos
7430
            if ($table === 'tt_content') {
7431
                $select[] = 'colPos';
7432
            }
7433
            // Get the sort value of the default language record
7434
            $row = BackendUtility::getRecord($table, $uid, implode(',', $select));
7435
            if (is_array($row)) {
7436
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7437
                $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
7438
7439
                $queryBuilder
7440
                    ->select(...$select)
7441
                    ->from($table)
7442
                    ->where(
7443
                        $queryBuilder->expr()->eq(
7444
                            'pid',
7445
                            $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
7446
                        ),
7447
                        $queryBuilder->expr()->eq(
7448
                            $GLOBALS['TCA'][$table]['ctrl']['languageField'],
7449
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
7450
                        ),
7451
                        $queryBuilder->expr()->lt(
7452
                            $sortColumn,
7453
                            $queryBuilder->createNamedParameter($row[$sortColumn], \PDO::PARAM_INT)
7454
                        )
7455
                    )
7456
                    ->orderBy($sortColumn, 'DESC')
7457
                    ->addOrderBy('uid', 'DESC')
7458
                    ->setMaxResults(1);
7459
                if ($table === 'tt_content') {
7460
                    $queryBuilder
7461
                        ->andWhere(
7462
                            $queryBuilder->expr()->eq(
7463
                                'colPos',
7464
                                $queryBuilder->createNamedParameter($row['colPos'], \PDO::PARAM_INT)
7465
                            )
7466
                        );
7467
                }
7468
                // If there is an element, find its localized record in specified localization language
7469
                if ($previousRow = $queryBuilder->execute()->fetch()) {
7470
                    $previousLocalizedRecord = BackendUtility::getRecordLocalization($table, $previousRow['uid'], $language);
7471
                    if (is_array($previousLocalizedRecord[0])) {
7472
                        $previousLocalizedRecordUid = $previousLocalizedRecord[0]['uid'];
7473
                    }
7474
                }
7475
            }
7476
        }
7477
        return $previousLocalizedRecordUid;
7478
    }
7479
7480
    /**
7481
     * Returns a fieldArray with default values. Values will be picked up from the TCA array looking at the config key "default" for each column. If values are set in ->defaultValues they will overrule though.
7482
     * Used for new records and during copy operations for defaults
7483
     *
7484
     * @param string $table Table name for which to set default values.
7485
     * @return array Array with default values.
7486
     * @internal should only be used from within DataHandler
7487
     */
7488
    public function newFieldArray($table)
7489
    {
7490
        $fieldArray = [];
7491
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
7492
            foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $content) {
7493
                if (isset($this->defaultValues[$table][$field])) {
7494
                    $fieldArray[$field] = $this->defaultValues[$table][$field];
7495
                } elseif (isset($content['config']['default'])) {
7496
                    $fieldArray[$field] = $content['config']['default'];
7497
                }
7498
            }
7499
        }
7500
        return $fieldArray;
7501
    }
7502
7503
    /**
7504
     * If a "languageField" is specified for $table this function will add a possible value to the incoming array if none is found in there already.
7505
     *
7506
     * @param string $table Table name
7507
     * @param array $incomingFieldArray Incoming array (passed by reference)
7508
     * @internal should only be used from within DataHandler
7509
     */
7510
    public function addDefaultPermittedLanguageIfNotSet($table, &$incomingFieldArray)
7511
    {
7512
        // Checking languages:
7513
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
7514
            if (!isset($incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
7515
                // Language field must be found in input row - otherwise it does not make sense.
7516
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
7517
                    ->getQueryBuilderForTable('sys_language');
7518
                $queryBuilder->getRestrictions()
7519
                    ->removeAll()
7520
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7521
                $queryBuilder
7522
                    ->select('uid')
7523
                    ->from('sys_language')
7524
                    ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
7525
                $rows = array_merge([['uid' => 0]], $queryBuilder->execute()->fetchAll(), [['uid' => -1]]);
7526
                foreach ($rows as $r) {
7527
                    if ($this->BE_USER->checkLanguageAccess($r['uid'])) {
7528
                        $incomingFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $r['uid'];
7529
                        break;
7530
                    }
7531
                }
7532
            }
7533
        }
7534
    }
7535
7536
    /**
7537
     * Returns the $data array from $table overridden in the fields defined in ->overrideValues.
7538
     *
7539
     * @param string $table Table name
7540
     * @param array $data Data array with fields from table. These will be overlaid with values in $this->overrideValues[$table]
7541
     * @return array Data array, processed.
7542
     * @internal should only be used from within DataHandler
7543
     */
7544
    public function overrideFieldArray($table, $data)
7545
    {
7546
        if (is_array($this->overrideValues[$table])) {
7547
            $data = array_merge($data, $this->overrideValues[$table]);
7548
        }
7549
        return $data;
7550
    }
7551
7552
    /**
7553
     * Compares the incoming field array with the current record and unsets all fields which are the same.
7554
     * Used for existing records being updated
7555
     *
7556
     * @param string $table Record table name
7557
     * @param int $id Record uid
7558
     * @param array $fieldArray Array of field=>value pairs intended to be inserted into the database. All keys with values matching exactly the current value will be unset!
7559
     * @return array Returns $fieldArray. If the returned array is empty, then the record should not be updated!
7560
     * @internal should only be used from within DataHandler
7561
     */
7562
    public function compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
7563
    {
7564
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
7565
        $queryBuilder = $connection->createQueryBuilder();
7566
        $queryBuilder->getRestrictions()->removeAll();
7567
        $currentRecord = $queryBuilder->select('*')
7568
            ->from($table)
7569
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
7570
            ->execute()
7571
            ->fetch();
7572
        // If the current record exists (which it should...), begin comparison:
7573
        if (is_array($currentRecord)) {
7574
            $tableDetails = $connection->getSchemaManager()->listTableDetails($table);
7575
            $columnRecordTypes = [];
7576
            foreach ($currentRecord as $columnName => $_) {
7577
                $columnRecordTypes[$columnName] = '';
7578
                $type = $tableDetails->getColumn($columnName)->getType();
7579
                if ($type instanceof IntegerType) {
7580
                    $columnRecordTypes[$columnName] = 'int';
7581
                }
7582
            }
7583
            // Unset the fields which are similar:
7584
            foreach ($fieldArray as $col => $val) {
7585
                $fieldConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
7586
                $isNullField = (!empty($fieldConfiguration['eval']) && GeneralUtility::inList($fieldConfiguration['eval'], 'null'));
7587
7588
                // Unset fields if stored and submitted values are equal - except the current field holds MM relations.
7589
                // In general this avoids to store superfluous data which also will be visualized in the editing history.
7590
                if (!$fieldConfiguration['MM'] && $this->isSubmittedValueEqualToStoredValue($val, $currentRecord[$col], $columnRecordTypes[$col], $isNullField)) {
7591
                    unset($fieldArray[$col]);
7592
                } else {
7593
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col])) {
7594
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $currentRecord[$col];
7595
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col]) {
7596
                        $this->historyRecords[$table . ':' . $id]['oldRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col];
7597
                    }
7598
                    if (!isset($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col])) {
7599
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $fieldArray[$col];
7600
                    } elseif ($this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col] != $this->mmHistoryRecords[$table . ':' . $id]['oldRecord'][$col]) {
7601
                        $this->historyRecords[$table . ':' . $id]['newRecord'][$col] = $this->mmHistoryRecords[$table . ':' . $id]['newRecord'][$col];
7602
                    }
7603
                }
7604
            }
7605
        } else {
7606
            // If the current record does not exist this is an error anyways and we just return an empty array here.
7607
            $fieldArray = [];
7608
        }
7609
        return $fieldArray;
7610
    }
7611
7612
    /**
7613
     * Determines whether submitted values and stored values are equal.
7614
     * This prevents from adding superfluous field changes which would be shown in the record history as well.
7615
     * For NULL fields (see accordant TCA definition 'eval' = 'null'), a special handling is required since
7616
     * (!strcmp(NULL, '')) would be a false-positive.
7617
     *
7618
     * @param mixed $submittedValue Value that has submitted (e.g. from a backend form)
7619
     * @param mixed $storedValue Value that is currently stored in the database
7620
     * @param string $storedType SQL type of the stored value column (see mysql_field_type(), e.g 'int', 'string',  ...)
7621
     * @param bool $allowNull Whether NULL values are allowed by accordant TCA definition ('eval' = 'null')
7622
     * @return bool Whether both values are considered to be equal
7623
     */
7624
    protected function isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, $allowNull = false)
7625
    {
7626
        // No NULL values are allowed, this is the regular behaviour.
7627
        // Thus, check whether strings are the same or whether integer values are empty ("0" or "").
7628
        if (!$allowNull) {
7629
            $result = (string)$submittedValue === (string)$storedValue || $storedType === 'int' && (int)$storedValue === (int)$submittedValue;
7630
        // Null values are allowed, but currently there's a real (not NULL) value.
7631
        // Thus, ensure no NULL value was submitted and fallback to the regular behaviour.
7632
        } elseif ($storedValue !== null) {
7633
            $result = (
7634
                $submittedValue !== null
7635
                && $this->isSubmittedValueEqualToStoredValue($submittedValue, $storedValue, $storedType, false)
7636
            );
7637
        // Null values are allowed, and currently there's a NULL value.
7638
        // Thus, check whether a NULL value was submitted.
7639
        } else {
7640
            $result = ($submittedValue === null);
7641
        }
7642
7643
        return $result;
7644
    }
7645
7646
    /**
7647
     * Converts a HTML entity (like &#123;) to the character '123'
7648
     *
7649
     * @param string $input Input string
7650
     * @return string Output string
7651
     * @internal should only be used from within DataHandler
7652
     */
7653
    public function convNumEntityToByteValue($input)
7654
    {
7655
        $token = md5(microtime());
7656
        $parts = explode($token, preg_replace('/(&#([0-9]+);)/', $token . '\\2' . $token, $input));
7657
        foreach ($parts as $k => $v) {
7658
            if ($k % 2) {
7659
                $v = (int)$v;
7660
                // Just to make sure that control bytes are not converted.
7661
                if ($v > 32) {
7662
                    $parts[$k] = chr($v);
7663
                }
7664
            }
7665
        }
7666
        return implode('', $parts);
7667
    }
7668
7669
    /**
7670
     * Disables the delete clause for fetching records.
7671
     * In general only undeleted records will be used. If the delete
7672
     * clause is disabled, also deleted records are taken into account.
7673
     */
7674
    public function disableDeleteClause()
7675
    {
7676
        $this->disableDeleteClause = true;
7677
    }
7678
7679
    /**
7680
     * Returns delete-clause for the $table
7681
     *
7682
     * @param string $table Table name
7683
     * @return string Delete clause
7684
     * @internal should only be used from within DataHandler
7685
     */
7686
    public function deleteClause($table)
7687
    {
7688
        // Returns the proper delete-clause if any for a table from TCA
7689
        if (!$this->disableDeleteClause && $GLOBALS['TCA'][$table]['ctrl']['delete']) {
7690
            return ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0';
7691
        }
7692
        return '';
7693
    }
7694
7695
    /**
7696
     * Add delete restriction if not disabled
7697
     *
7698
     * @param QueryRestrictionContainerInterface $restrictions
7699
     */
7700
    protected function addDeleteRestriction(QueryRestrictionContainerInterface $restrictions)
7701
    {
7702
        if (!$this->disableDeleteClause) {
7703
            $restrictions->add(GeneralUtility::makeInstance(DeletedRestriction::class));
7704
        }
7705
    }
7706
7707
    /**
7708
     * Gets UID of parent record. If record is deleted it will be looked up in
7709
     * an array built before the record was deleted
7710
     *
7711
     * @param string $table Table where record lives/lived
7712
     * @param int $uid Record UID
7713
     * @return int[] Parent UIDs
7714
     */
7715
    protected function getOriginalParentOfRecord($table, $uid)
7716
    {
7717
        if (isset(self::$recordPidsForDeletedRecords[$table][$uid])) {
7718
            return self::$recordPidsForDeletedRecords[$table][$uid];
7719
        }
7720
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $pid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7720
        [$parentUid] = BackendUtility::getTSCpid($table, $uid, /** @scrutinizer ignore-type */ '');
Loading history...
7721
        return [$parentUid];
7722
    }
7723
7724
    /**
7725
     * Extract entries from TSconfig for a specific table. This will merge specific and default configuration together.
7726
     *
7727
     * @param string $table Table name
7728
     * @param array $TSconfig TSconfig for page
7729
     * @return array TSconfig merged
7730
     * @internal should only be used from within DataHandler
7731
     */
7732
    public function getTableEntries($table, $TSconfig)
7733
    {
7734
        $tA = is_array($TSconfig['table.'][$table . '.']) ? $TSconfig['table.'][$table . '.'] : [];
7735
        $dA = is_array($TSconfig['default.']) ? $TSconfig['default.'] : [];
7736
        ArrayUtility::mergeRecursiveWithOverrule($dA, $tA);
7737
        return $dA;
7738
    }
7739
7740
    /**
7741
     * Returns the pid of a record from $table with $uid
7742
     *
7743
     * @param string $table Table name
7744
     * @param int $uid Record uid
7745
     * @return int|false PID value (unless the record did not exist in which case FALSE is returned)
7746
     * @internal should only be used from within DataHandler
7747
     */
7748
    public function getPID($table, $uid)
7749
    {
7750
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
7751
        $queryBuilder->getRestrictions()
7752
            ->removeAll();
7753
        $queryBuilder->select('pid')
7754
            ->from($table)
7755
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
7756
        if ($row = $queryBuilder->execute()->fetch()) {
7757
            return $row['pid'];
7758
        }
7759
        return false;
7760
    }
7761
7762
    /**
7763
     * Executing dbAnalysisStore
7764
     * This will save MM relations for new records but is executed after records are created because we need to know the ID of them
7765
     * @internal should only be used from within DataHandler
7766
     */
7767
    public function dbAnalysisStoreExec()
7768
    {
7769
        foreach ($this->dbAnalysisStore as $action) {
7770
            $id = BackendUtility::wsMapId($action[4], MathUtility::canBeInterpretedAsInteger($action[2]) ? $action[2] : $this->substNEWwithIDs[$action[2]]);
7771
            if ($id) {
7772
                $action[0]->writeMM($action[1], $id, $action[3]);
7773
            }
7774
        }
7775
    }
7776
7777
    /**
7778
     * Returns array, $CPtable, of pages under the $pid going down to $counter levels.
7779
     * Selecting ONLY pages which the user has read-access to!
7780
     *
7781
     * @param array $CPtable Accumulation of page uid=>pid pairs in branch of $pid
7782
     * @param int $pid Page ID for which to find subpages
7783
     * @param int $counter Number of levels to go down.
7784
     * @param int $rootID ID of root point for new copied branch: The idea seems to be that a copy is not made of the already new page!
7785
     * @return array Return array.
7786
     * @internal should only be used from within DataHandler
7787
     */
7788
    public function int_pageTreeInfo($CPtable, $pid, $counter, $rootID)
7789
    {
7790
        if ($counter) {
7791
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
7792
            $restrictions = $queryBuilder->getRestrictions()->removeAll();
7793
            $this->addDeleteRestriction($restrictions);
7794
            $queryBuilder
7795
                ->select('uid')
7796
                ->from('pages')
7797
                ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)))
7798
                ->orderBy('sorting', 'DESC');
7799
            if (!$this->admin) {
7800
                $queryBuilder->andWhere($this->BE_USER->getPagePermsClause(Permission::PAGE_SHOW));
7801
            }
7802
            if ((int)$this->BE_USER->workspace === 0) {
7803
                $queryBuilder->andWhere(
7804
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
7805
                );
7806
            } else {
7807
                $queryBuilder->andWhere($queryBuilder->expr()->in(
7808
                    't3ver_wsid',
7809
                    $queryBuilder->createNamedParameter([0, $this->BE_USER->workspace], Connection::PARAM_INT_ARRAY)
7810
                ));
7811
            }
7812
            $result = $queryBuilder->execute();
7813
7814
            $pages = [];
7815
            while ($row = $result->fetch()) {
7816
                $pages[$row['uid']] = $row;
7817
            }
7818
7819
            // Resolve placeholders of workspace versions
7820
            if (!empty($pages) && (int)$this->BE_USER->workspace !== 0) {
7821
                $pages = array_reverse(
7822
                    $this->resolveVersionedRecords(
7823
                        'pages',
7824
                        'uid',
7825
                        'sorting',
7826
                        array_keys($pages)
7827
                    ),
7828
                    true
7829
                );
7830
            }
7831
7832
            foreach ($pages as $page) {
7833
                if ($page['uid'] != $rootID) {
7834
                    $CPtable[$page['uid']] = $pid;
7835
                    // If the uid is NOT the rootID of the copyaction and if we are supposed to walk further down
7836
                    if ($counter - 1) {
7837
                        $CPtable = $this->int_pageTreeInfo($CPtable, $page['uid'], $counter - 1, $rootID);
7838
                    }
7839
                }
7840
            }
7841
        }
7842
        return $CPtable;
7843
    }
7844
7845
    /**
7846
     * List of all tables (those administrators has access to = array_keys of $GLOBALS['TCA'])
7847
     *
7848
     * @return array Array of all TCA table names
7849
     * @internal should only be used from within DataHandler
7850
     */
7851
    public function compileAdminTables()
7852
    {
7853
        return array_keys($GLOBALS['TCA']);
7854
    }
7855
7856
    /**
7857
     * Checks if any uniqueInPid eval input fields are in the record and if so, they are re-written to be correct.
7858
     *
7859
     * @param string $table Table name
7860
     * @param int $uid Record UID
7861
     * @internal should only be used from within DataHandler
7862
     */
7863
    public function fixUniqueInPid($table, $uid)
7864
    {
7865
        if (empty($GLOBALS['TCA'][$table])) {
7866
            return;
7867
        }
7868
7869
        $curData = $this->recordInfo($table, $uid, '*');
7870
        $newData = [];
7871
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7872
            if ($conf['config']['type'] === 'input' && (string)$curData[$field] !== '') {
7873
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7874
                if (in_array('uniqueInPid', $evalCodesArray, true)) {
7875
                    $newV = $this->getUnique($table, $field, $curData[$field], $uid, $curData['pid']);
7876
                    if ((string)$newV !== (string)$curData[$field]) {
7877
                        $newData[$field] = $newV;
7878
                    }
7879
                }
7880
            }
7881
        }
7882
        // IF there are changed fields, then update the database
7883
        if (!empty($newData)) {
7884
            $this->updateDB($table, $uid, $newData);
7885
        }
7886
    }
7887
7888
    /**
7889
     * Checks if any uniqueInSite eval fields are in the record and if so, they are re-written to be correct.
7890
     *
7891
     * @param string $table Table name
7892
     * @param int $uid Record UID
7893
     * @return bool whether the record had to be fixed or not
7894
     */
7895
    protected function fixUniqueInSite(string $table, int $uid): bool
7896
    {
7897
        $curData = $this->recordInfo($table, $uid, '*');
7898
        $workspaceId = $this->BE_USER->workspace;
7899
        $newData = [];
7900
        foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $conf) {
7901
            if ($conf['config']['type'] === 'slug' && (string)$curData[$field] !== '') {
7902
                $evalCodesArray = GeneralUtility::trimExplode(',', $conf['config']['eval'], true);
7903
                if (in_array('uniqueInSite', $evalCodesArray, true)) {
7904
                    $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $conf['config'], $workspaceId);
7905
                    $state = RecordStateFactory::forName($table)->fromArray($curData);
0 ignored issues
show
Bug introduced by
It seems like $curData can also be of type null; however, parameter $data of TYPO3\CMS\Core\DataHandl...ateFactory::fromArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7905
                    $state = RecordStateFactory::forName($table)->fromArray(/** @scrutinizer ignore-type */ $curData);
Loading history...
7906
                    $newValue = $helper->buildSlugForUniqueInSite($curData[$field], $state);
7907
                    if ((string)$newValue !== (string)$curData[$field]) {
7908
                        $newData[$field] = $newValue;
7909
                    }
7910
                }
7911
            }
7912
        }
7913
        // IF there are changed fields, then update the database
7914
        if (!empty($newData)) {
7915
            $this->updateDB($table, $uid, $newData);
7916
            return true;
7917
        }
7918
        return false;
7919
    }
7920
7921
    /**
7922
     * Check if there are subpages that need an adoption as well
7923
     * @param int $pageId
7924
     */
7925
    protected function fixUniqueInSiteForSubpages(int $pageId)
7926
    {
7927
        // Get ALL subpages to update - read-permissions are respected
7928
        $subPages = $this->int_pageTreeInfo([], $pageId, 99, $pageId);
7929
        // Now fix uniqueInSite for subpages
7930
        foreach ($subPages as $thePageUid => $thePagePid) {
7931
            $recordWasModified = $this->fixUniqueInSite('pages', $thePageUid);
7932
            if ($recordWasModified) {
7933
                // @todo: Add logging and history - but how? we don't know the data that was in the system before
7934
            }
7935
        }
7936
    }
7937
7938
    /**
7939
     * When er record is copied you can specify fields from the previous record which should be copied into the new one
7940
     * This function is also called with new elements. But then $update must be set to zero and $newData containing the data array. In that case data in the incoming array is NOT overridden. (250202)
7941
     *
7942
     * @param string $table Table name
7943
     * @param int $uid Record UID
7944
     * @param int $prevUid UID of previous record
7945
     * @param bool $update If set, updates the record
7946
     * @param array $newData Input array. If fields are already specified AND $update is not set, values are not set in output array.
7947
     * @return array Output array (For when the copying operation needs to get the information instead of updating the info)
7948
     * @internal should only be used from within DataHandler
7949
     */
7950
    public function fixCopyAfterDuplFields($table, $uid, $prevUid, $update, $newData = [])
7951
    {
7952
        if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields']) {
7953
            $prevData = $this->recordInfo($table, $prevUid, '*');
7954
            $theFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['copyAfterDuplFields'], true);
7955
            foreach ($theFields as $field) {
7956
                if ($GLOBALS['TCA'][$table]['columns'][$field] && ($update || !isset($newData[$field]))) {
7957
                    $newData[$field] = $prevData[$field];
7958
                }
7959
            }
7960
            if ($update && !empty($newData)) {
7961
                $this->updateDB($table, $uid, $newData);
7962
            }
7963
        }
7964
        return $newData;
7965
    }
7966
7967
    /**
7968
     * Casts a reference value. In case MM relations or foreign_field
7969
     * references are used. All other configurations, as well as
7970
     * foreign_table(!) could be stored as comma-separated-values
7971
     * as well. Since the system is not able to determine the default
7972
     * value automatically then, the TCA default value is used if
7973
     * it has been defined.
7974
     *
7975
     * @param int|string $value The value to be casted (e.g. '', '0', '1,2,3')
7976
     * @param array $configuration The TCA configuration of the accordant field
7977
     * @return int|string
7978
     */
7979
    protected function castReferenceValue($value, array $configuration)
7980
    {
7981
        if ((string)$value !== '') {
7982
            return $value;
7983
        }
7984
7985
        if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) {
7986
            return 0;
7987
        }
7988
7989
        if (array_key_exists('default', $configuration)) {
7990
            return $configuration['default'];
7991
        }
7992
7993
        return $value;
7994
    }
7995
7996
    /**
7997
     * Returns TRUE if the TCA/columns field type is a DB reference field
7998
     *
7999
     * @param array $conf Config array for TCA/columns field
8000
     * @return bool TRUE if DB reference field (group/db or select with foreign-table)
8001
     * @internal should only be used from within DataHandler
8002
     */
8003
    public function isReferenceField($conf)
8004
    {
8005
        return $conf['type'] === 'group' && $conf['internal_type'] === 'db' || $conf['type'] === 'select' && $conf['foreign_table'];
8006
    }
8007
8008
    /**
8009
     * Returns the subtype as a string of an inline field.
8010
     * If it's not an inline field at all, it returns FALSE.
8011
     *
8012
     * @param array $conf Config array for TCA/columns field
8013
     * @return string|bool string Inline subtype (field|mm|list), boolean: FALSE
8014
     * @internal should only be used from within DataHandler
8015
     */
8016
    public function getInlineFieldType($conf)
8017
    {
8018
        if ($conf['type'] !== 'inline' || !$conf['foreign_table']) {
8019
            return false;
8020
        }
8021
        if ($conf['foreign_field']) {
8022
            // The reference to the parent is stored in a pointer field in the child record
8023
            return 'field';
8024
        }
8025
        if ($conf['MM']) {
8026
            // Regular MM intermediate table is used to store data
8027
            return 'mm';
8028
        }
8029
        // An item list (separated by comma) is stored (like select type is doing)
8030
        return 'list';
8031
    }
8032
8033
    /**
8034
     * Get modified header for a copied record
8035
     *
8036
     * @param string $table Table name
8037
     * @param int $pid PID value in which other records to test might be
8038
     * @param string $field Field name to get header value for.
8039
     * @param string $value Current field value
8040
     * @param int $count Counter (number of recursions)
8041
     * @param string $prevTitle Previous title we checked for (in previous recursion)
8042
     * @return string The field value, possibly appended with a "copy label
8043
     * @internal should only be used from within DataHandler
8044
     */
8045
    public function getCopyHeader($table, $pid, $field, $value, $count, $prevTitle = '')
8046
    {
8047
        // Set title value to check for:
8048
        $checkTitle = $value;
8049
        if ($count > 0) {
8050
            $checkTitle = $value . rtrim(' ' . sprintf($this->prependLabel($table), $count));
8051
        }
8052
        // Do check:
8053
        if ($prevTitle != $checkTitle || $count < 100) {
8054
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8055
            $this->addDeleteRestriction($queryBuilder->getRestrictions()->removeAll());
8056
            $rowCount = $queryBuilder
8057
                ->count('uid')
8058
                ->from($table)
8059
                ->where(
8060
                    $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
8061
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($checkTitle, \PDO::PARAM_STR))
8062
                )
8063
                ->execute()
8064
                ->fetchColumn(0);
8065
            if ($rowCount) {
8066
                return $this->getCopyHeader($table, $pid, $field, $value, $count + 1, $checkTitle);
8067
            }
8068
        }
8069
        // Default is to just return the current input title if no other was returned before:
8070
        return $checkTitle;
8071
    }
8072
8073
    /**
8074
     * Return "copy" label for a table. Although the name is "prepend" it actually APPENDs the label (after ...)
8075
     *
8076
     * @param string $table Table name
8077
     * @return string Label to append, containing "%s" for the number
8078
     * @see getCopyHeader()
8079
     * @internal should only be used from within DataHandler
8080
     */
8081
    public function prependLabel($table)
8082
    {
8083
        return $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['prependAtCopy']);
8084
    }
8085
8086
    /**
8087
     * Get the final pid based on $table and $pid ($destPid type... pos/neg)
8088
     *
8089
     * @param string $table Table name
8090
     * @param int $pid "Destination pid" : If the value is >= 0 it's just returned directly (through (int)though) but if the value is <0 then the method looks up the record with the uid equal to abs($pid) (positive number) and returns the PID of that record! The idea is that negative numbers point to the record AFTER WHICH the position is supposed to be!
8091
     * @return int
8092
     * @internal should only be used from within DataHandler
8093
     */
8094
    public function resolvePid($table, $pid)
8095
    {
8096
        $pid = (int)$pid;
8097
        if ($pid < 0) {
8098
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8099
            $query->getRestrictions()
8100
                ->removeAll();
8101
            $row = $query
8102
                ->select('pid')
8103
                ->from($table)
8104
                ->where($query->expr()->eq('uid', $query->createNamedParameter(abs($pid), \PDO::PARAM_INT)))
8105
                ->execute()
8106
                ->fetch();
8107
            $pid = (int)$row['pid'];
8108
        }
8109
        return $pid;
8110
    }
8111
8112
    /**
8113
     * Removes the prependAtCopy prefix on values
8114
     *
8115
     * @param string $table Table name
8116
     * @param string $value The value to fix
8117
     * @return string Clean name
8118
     * @internal should only be used from within DataHandler
8119
     */
8120
    public function clearPrefixFromValue($table, $value)
8121
    {
8122
        $regex = '/\s' . sprintf(preg_quote($this->prependLabel($table)), '[0-9]*') . '$/';
8123
        return @preg_replace($regex, '', $value);
8124
    }
8125
8126
    /**
8127
     * Check if there are records from tables on the pages to be deleted which the current user is not allowed to
8128
     *
8129
     * @param int[] $pageIds IDs of pages which should be checked
8130
     * @return string[]|null Return null, if permission granted, otherwise an array with the tables that are not allowed to be deleted
8131
     * @see canDeletePage()
8132
     */
8133
    protected function checkForRecordsFromDisallowedTables(array $pageIds): ?array
8134
    {
8135
        if ($this->admin) {
8136
            return null;
8137
        }
8138
8139
        $disallowedTables = [];
8140
        if (!empty($pageIds)) {
8141
            $tableNames = $this->compileAdminTables();
8142
            foreach ($tableNames as $table) {
8143
                $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
8144
                $query->getRestrictions()
8145
                    ->removeAll()
8146
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8147
                $count = $query->count('uid')
8148
                    ->from($table)
8149
                    ->where($query->expr()->in(
8150
                        'pid',
8151
                        $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
8152
                    ))
8153
                    ->execute()
8154
                    ->fetchColumn(0);
8155
                if ($count && ($this->tableReadOnly($table) || !$this->checkModifyAccessList($table))) {
8156
                    $disallowedTables[] = $table;
8157
                }
8158
            }
8159
        }
8160
        return !empty($disallowedTables) ? $disallowedTables : null;
8161
    }
8162
8163
    /**
8164
     * Determine if a record was copied or if a record is the result of a copy action.
8165
     *
8166
     * @param string $table The tablename of the record
8167
     * @param int $uid The uid of the record
8168
     * @return bool Returns TRUE if the record is copied or is the result of a copy action
8169
     * @internal should only be used from within DataHandler
8170
     */
8171
    public function isRecordCopied($table, $uid)
8172
    {
8173
        // If the record was copied:
8174
        if (isset($this->copyMappingArray[$table][$uid])) {
8175
            return true;
8176
        }
8177
        if (isset($this->copyMappingArray[$table]) && in_array($uid, array_values($this->copyMappingArray[$table]))) {
8178
            return true;
8179
        }
8180
        return false;
8181
    }
8182
8183
    /******************************
8184
     *
8185
     * Clearing cache
8186
     *
8187
     ******************************/
8188
8189
    /**
8190
     * Clearing the cache based on a page being updated
8191
     * If the $table is 'pages' then cache is cleared for all pages on the same level (and subsequent?)
8192
     * Else just clear the cache for the parent page of the record.
8193
     *
8194
     * @param string $table Table name of record that was just updated.
8195
     * @param int $uid UID of updated / inserted record
8196
     * @param int $pid REAL PID of page of a deleted/moved record to get TSconfig in ClearCache.
8197
     * @internal This method is not meant to be called directly but only from the core itself or from hooks
8198
     */
8199
    public function registerRecordIdForPageCacheClearing($table, $uid, $pid = null)
8200
    {
8201
        if (!is_array(static::$recordsToClearCacheFor[$table])) {
8202
            static::$recordsToClearCacheFor[$table] = [];
8203
        }
8204
        static::$recordsToClearCacheFor[$table][] = (int)$uid;
8205
        if ($pid !== null) {
8206
            if (!is_array(static::$recordPidsForDeletedRecords[$table])) {
8207
                static::$recordPidsForDeletedRecords[$table] = [];
8208
            }
8209
            static::$recordPidsForDeletedRecords[$table][$uid][] = (int)$pid;
8210
        }
8211
    }
8212
8213
    /**
8214
     * Do the actual clear cache
8215
     */
8216
    protected function processClearCacheQueue()
8217
    {
8218
        $tagsToClear = [];
8219
        $clearCacheCommands = [];
8220
8221
        foreach (static::$recordsToClearCacheFor as $table => $uids) {
8222
            foreach (array_unique($uids) as $uid) {
8223
                if (!isset($GLOBALS['TCA'][$table]) || $uid <= 0) {
8224
                    return;
8225
                }
8226
                // For move commands we may get more then 1 parent.
8227
                $pageUids = $this->getOriginalParentOfRecord($table, $uid);
8228
                foreach ($pageUids as $originalParent) {
8229
                    [$tagsToClearFromPrepare, $clearCacheCommandsFromPrepare]
8230
                        = $this->prepareCacheFlush($table, $uid, $originalParent);
8231
                    $tagsToClear = array_merge($tagsToClear, $tagsToClearFromPrepare);
8232
                    $clearCacheCommands = array_merge($clearCacheCommands, $clearCacheCommandsFromPrepare);
8233
                }
8234
            }
8235
        }
8236
8237
        /** @var CacheManager $cacheManager */
8238
        $cacheManager = $this->getCacheManager();
8239
        $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear));
8240
8241
        // Filter duplicate cache commands from cacheQueue
8242
        $clearCacheCommands = array_unique($clearCacheCommands);
8243
        // Execute collected clear cache commands from page TSConfig
8244
        foreach ($clearCacheCommands as $command) {
8245
            $this->clear_cacheCmd($command);
8246
        }
8247
8248
        // Reset the cache clearing array
8249
        static::$recordsToClearCacheFor = [];
8250
8251
        // Reset the original pid array
8252
        static::$recordPidsForDeletedRecords = [];
8253
    }
8254
8255
    /**
8256
     * Prepare the cache clearing
8257
     *
8258
     * @param string $table Table name of record that needs to be cleared
8259
     * @param int $uid UID of record for which the cache needs to be cleared
8260
     * @param int $pid Original pid of the page of the record which the cache needs to be cleared
8261
     * @return array Array with tagsToClear and clearCacheCommands
8262
     * @internal This function is internal only it may be changed/removed also in minor version numbers.
8263
     */
8264
    protected function prepareCacheFlush($table, $uid, $pid)
8265
    {
8266
        $tagsToClear = [];
8267
        $clearCacheCommands = [];
8268
        $pageUid = 0;
8269
        // Get Page TSconfig relevant:
8270
        $TSConfig = BackendUtility::getPagesTSconfig($pid)['TCEMAIN.'] ?? [];
8271
        if (empty($TSConfig['clearCache_disable']) && $this->BE_USER->workspace === 0) {
8272
            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
8273
            // If table is "pages":
8274
            $pageIdsThatNeedCacheFlush = [];
8275
            if ($table === 'pages') {
8276
                // Find out if the record is a get the original page
8277
                $pageUid = $this->getDefaultLanguagePageId($uid);
8278
8279
                // Builds list of pages on the SAME level as this page (siblings)
8280
                $queryBuilder = $connectionPool->getQueryBuilderForTable('pages');
8281
                $queryBuilder->getRestrictions()
8282
                    ->removeAll()
8283
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8284
                $siblings = $queryBuilder
8285
                    ->select('A.pid AS pid', 'B.uid AS uid')
8286
                    ->from('pages', 'A')
8287
                    ->from('pages', 'B')
8288
                    ->where(
8289
                        $queryBuilder->expr()->eq('A.uid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)),
8290
                        $queryBuilder->expr()->eq('B.pid', $queryBuilder->quoteIdentifier('A.pid')),
8291
                        $queryBuilder->expr()->gte('A.pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8292
                    )
8293
                    ->execute();
8294
8295
                $parentPageId = 0;
8296
                while ($row_tmp = $siblings->fetch()) {
8297
                    $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['uid'];
8298
                    $parentPageId = (int)$row_tmp['pid'];
8299
                    // Add children as well:
8300
                    if ($TSConfig['clearCache_pageSiblingChildren']) {
8301
                        $siblingChildrenQuery = $connectionPool->getQueryBuilderForTable('pages');
8302
                        $siblingChildrenQuery->getRestrictions()
8303
                            ->removeAll()
8304
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8305
                        $siblingChildren = $siblingChildrenQuery
8306
                            ->select('uid')
8307
                            ->from('pages')
8308
                            ->where($siblingChildrenQuery->expr()->eq(
8309
                                'pid',
8310
                                $siblingChildrenQuery->createNamedParameter($row_tmp['uid'], \PDO::PARAM_INT)
8311
                            ))
8312
                            ->execute();
8313
                        while ($row_tmp2 = $siblingChildren->fetch()) {
8314
                            $pageIdsThatNeedCacheFlush[] = (int)$row_tmp2['uid'];
8315
                        }
8316
                    }
8317
                }
8318
                // Finally, add the parent page as well when clearing a specific page
8319
                if ($parentPageId > 0) {
8320
                    $pageIdsThatNeedCacheFlush[] = $parentPageId;
8321
                }
8322
                // Add grand-parent as well if configured
8323
                if ($TSConfig['clearCache_pageGrandParent']) {
8324
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8325
                    $parentQuery->getRestrictions()
8326
                        ->removeAll()
8327
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8328
                    $row_tmp = $parentQuery
8329
                        ->select('pid')
8330
                        ->from('pages')
8331
                        ->where($parentQuery->expr()->eq(
8332
                            'uid',
8333
                            $parentQuery->createNamedParameter($parentPageId, \PDO::PARAM_INT)
8334
                        ))
8335
                        ->execute()
8336
                        ->fetch();
8337
                    if (!empty($row_tmp)) {
8338
                        $pageIdsThatNeedCacheFlush[] = (int)$row_tmp['pid'];
8339
                    }
8340
                }
8341
            } else {
8342
                // For other tables than "pages", delete cache for the records "parent page".
8343
                $pageIdsThatNeedCacheFlush[] = $pageUid = (int)$this->getPID($table, $uid);
8344
                // Add the parent page as well
8345
                if ($TSConfig['clearCache_pageGrandParent']) {
8346
                    $parentQuery = $connectionPool->getQueryBuilderForTable('pages');
8347
                    $parentQuery->getRestrictions()
8348
                        ->removeAll()
8349
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
8350
                    $parentPageRecord = $parentQuery
8351
                        ->select('pid')
8352
                        ->from('pages')
8353
                        ->where($parentQuery->expr()->eq(
8354
                            'uid',
8355
                            $parentQuery->createNamedParameter($pageUid, \PDO::PARAM_INT)
8356
                        ))
8357
                        ->execute()
8358
                        ->fetch();
8359
                    if (!empty($parentPageRecord)) {
8360
                        $pageIdsThatNeedCacheFlush[] = (int)$parentPageRecord['pid'];
8361
                    }
8362
                }
8363
            }
8364
            // Call pre-processing function for clearing of cache for page ids:
8365
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8366
                $_params = ['pageIdArray' => &$pageIdsThatNeedCacheFlush, 'table' => $table, 'uid' => $uid, 'functionID' => 'clear_cache()'];
8367
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8368
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8369
            }
8370
            // Delete cache for selected pages:
8371
            foreach ($pageIdsThatNeedCacheFlush as $pageId) {
8372
                $tagsToClear['pageId_' . $pageId] = true;
8373
            }
8374
            // Queue delete cache for current table and record
8375
            $tagsToClear[$table] = true;
8376
            $tagsToClear[$table . '_' . $uid] = true;
8377
        }
8378
        // Clear cache for pages entered in TSconfig:
8379
        if (!empty($TSConfig['clearCacheCmd'])) {
8380
            $commands = GeneralUtility::trimExplode(',', $TSConfig['clearCacheCmd'], true);
8381
            $clearCacheCommands = array_unique($commands);
8382
        }
8383
        // Call post processing function for clear-cache:
8384
        $_params = ['table' => $table, 'uid' => $uid, 'uid_page' => $pageUid, 'TSConfig' => $TSConfig, 'tags' => $tagsToClear];
8385
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8386
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8387
        }
8388
        return [
8389
            $tagsToClear,
8390
            $clearCacheCommands
8391
        ];
8392
    }
8393
8394
    /**
8395
     * Clears the cache based on the command $cacheCmd.
8396
     *
8397
     * $cacheCmd='pages'
8398
     * Clears cache for all pages and page-based caches inside the cache manager.
8399
     * Requires admin-flag to be set for BE_USER.
8400
     *
8401
     * $cacheCmd='all'
8402
     * Clears all cache_tables. This is necessary if templates are updated.
8403
     * Requires admin-flag to be set for BE_USER.
8404
     *
8405
     * The following cache_* are intentionally not cleared by 'all'
8406
     *
8407
     * - imagesizes:	Clearing this table would cause a lot of unneeded
8408
     * Imagemagick calls because the size information has
8409
     * to be fetched again after clearing.
8410
     * - all caches inside the cache manager that are inside the group "system"
8411
     * - they are only needed to build up the core system and templates.
8412
     *   If the group of system caches needs to be deleted explicitly, use
8413
     *   flushCachesInGroup('system') of CacheManager directly.
8414
     *
8415
     * $cacheCmd=[integer]
8416
     * Clears cache for the page pointed to by $cacheCmd (an integer).
8417
     *
8418
     * $cacheCmd='cacheTag:[string]'
8419
     * Flush page and pagesection cache by given tag
8420
     *
8421
     * $cacheCmd='cacheId:[string]'
8422
     * Removes cache identifier from page and page section cache
8423
     *
8424
     * Can call a list of post processing functions as defined in
8425
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc']
8426
     * (numeric array with values being the function references, called by
8427
     * GeneralUtility::callUserFunction()).
8428
     *
8429
     *
8430
     * @param string $cacheCmd The cache command, see above description
8431
     */
8432
    public function clear_cacheCmd($cacheCmd)
8433
    {
8434
        if (is_object($this->BE_USER)) {
8435
            $this->BE_USER->writeLog(SystemLogType::CACHE, SystemLogCacheAction::CLEAR, SystemLogErrorClassification::MESSAGE, 0, 'User %s has cleared the cache (cacheCmd=%s)', [$this->BE_USER->user['username'], $cacheCmd]);
8436
        }
8437
        $userTsConfig = $this->BE_USER->getTSConfig();
8438
        switch (strtolower($cacheCmd)) {
8439
            case 'pages':
8440
                if ($this->admin || ($userTsConfig['options.']['clearCache.']['pages'] ?? false)) {
8441
                    $this->getCacheManager()->flushCachesInGroup('pages');
8442
                }
8443
                break;
8444
            case 'all':
8445
                // allow to clear all caches if the TS config option is enabled or the option is not explicitly
8446
                // disabled for admins (which could clear all caches by default). The latter option is useful
8447
                // for big production sites where it should be possible to restrict the cache clearing for some admins.
8448
                if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
8449
                    || ($this->admin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
8450
                ) {
8451
                    $this->getCacheManager()->flushCaches();
8452
                    GeneralUtility::makeInstance(ConnectionPool::class)
8453
                        ->getConnectionForTable('cache_treelist')
8454
                        ->truncate('cache_treelist');
8455
8456
                    // Delete Opcode Cache
8457
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
8458
                }
8459
                break;
8460
        }
8461
8462
        $tagsToFlush = [];
8463
        // Clear cache for a page ID!
8464
        if (MathUtility::canBeInterpretedAsInteger($cacheCmd)) {
8465
            $list_cache = [$cacheCmd];
8466
            // Call pre-processing function for clearing of cache for page ids:
8467
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearPageCacheEval'] ?? [] as $funcName) {
8468
                $_params = ['pageIdArray' => &$list_cache, 'cacheCmd' => $cacheCmd, 'functionID' => 'clear_cacheCmd()'];
8469
                // Returns the array of ids to clear, FALSE if nothing should be cleared! Never an empty array!
8470
                GeneralUtility::callUserFunction($funcName, $_params, $this);
8471
            }
8472
            // Delete cache for selected pages:
8473
            if (is_array($list_cache)) {
0 ignored issues
show
introduced by
The condition is_array($list_cache) is always true.
Loading history...
8474
                foreach ($list_cache as $pageId) {
8475
                    $tagsToFlush[] = 'pageId_' . (int)$pageId;
8476
                }
8477
            }
8478
        }
8479
        // flush cache by tag
8480
        if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
8481
            $cacheTag = substr($cacheCmd, 9);
8482
            $tagsToFlush[] = $cacheTag;
8483
        }
8484
        // process caching framework operations
8485
        if (!empty($tagsToFlush)) {
8486
            $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);
0 ignored issues
show
Bug introduced by
The method getCacheManager() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

8486
            $this->/** @scrutinizer ignore-call */ 
8487
                   getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
8487
        }
8488
8489
        // Call post processing function for clear-cache:
8490
        $_params = ['cacheCmd' => strtolower($cacheCmd), 'tags' => $tagsToFlush];
8491
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'] ?? [] as $_funcRef) {
8492
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
8493
        }
8494
    }
8495
8496
    /*****************************
8497
     *
8498
     * Logging
8499
     *
8500
     *****************************/
8501
    /**
8502
     * Logging actions from DataHandler
8503
     *
8504
     * @param string $table Table name the log entry is concerned with. Blank if NA
8505
     * @param int $recuid Record UID. Zero if NA
8506
     * @param int $action Action number: 0=No category, 1=new record, 2=update record, 3= delete record, 4= move record, 5= Check/evaluate
8507
     * @param int $recpid Normally 0 (zero). If set, it indicates that this log-entry is used to notify the backend of a record which is moved to another location
8508
     * @param int $error The severity: 0 = message, 1 = error, 2 = System Error, 3 = security notice (admin), 4 warning
8509
     * @param string $details Default error message in english
8510
     * @param int $details_nr This number is unique for every combination of $type and $action. This is the error-message number, which can later be used to translate error messages. 0 if not categorized, -1 if temporary
8511
     * @param array $data Array with special information that may go into $details by '%s' marks / sprintf() when the log is shown
8512
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
8513
     * @param string $NEWid NEW id for new records
8514
     * @return int Log entry UID (0 if no log entry was written or logging is disabled)
8515
     * @see \TYPO3\CMS\Core\SysLog\Action\Database for all available values of argument $action
8516
     * @see \TYPO3\CMS\Core\SysLog\Error for all available values of argument $error
8517
     * @internal should only be used from within TYPO3 Core
8518
     */
8519
    public function log($table, $recuid, $action, $recpid, $error, $details, $details_nr = -1, $data = [], $event_pid = -1, $NEWid = '')
8520
    {
8521
        if (!$this->enableLogging) {
8522
            return 0;
8523
        }
8524
        // Type value for DataHandler
8525
        if (!$this->storeLogMessages) {
8526
            $details = '';
8527
        }
8528
        if ($error > 0) {
8529
            $detailMessage = $details;
8530
            if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
8531
                $detailMessage = vsprintf($details, $data);
8532
            }
8533
            $this->errorLog[] = '[' . SystemLogType::DB . '.' . $action . '.' . $details_nr . ']: ' . $detailMessage;
8534
        }
8535
        return $this->BE_USER->writelog(SystemLogType::DB, $action, $error, $details_nr, $details, $data, $table, $recuid, $recpid, $event_pid, $NEWid);
8536
    }
8537
8538
    /**
8539
     * Simple logging function meant to be used when logging messages is not yet fixed.
8540
     *
8541
     * @param string $message Message string
8542
     * @param int $error Error code, see log()
8543
     * @return int Log entry UID
8544
     * @see log()
8545
     * @internal should only be used from within TYPO3 Core
8546
     */
8547
    public function newlog($message, $error = SystemLogErrorClassification::MESSAGE)
8548
    {
8549
        return $this->log('', 0, SystemLogGenericAction::UNDEFINED, 0, $error, $message, -1);
8550
    }
8551
8552
    /**
8553
     * Print log error messages from the operations of this script instance
8554
     * @internal should only be used from within TYPO3 Core
8555
     */
8556
    public function printLogErrorMessages()
8557
    {
8558
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
8559
        $queryBuilder->getRestrictions()->removeAll();
8560
        $result = $queryBuilder
8561
            ->select('*')
8562
            ->from('sys_log')
8563
            ->where(
8564
                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
8565
                $queryBuilder->expr()->lt('action', $queryBuilder->createNamedParameter(256, \PDO::PARAM_INT)),
8566
                $queryBuilder->expr()->eq(
8567
                    'userid',
8568
                    $queryBuilder->createNamedParameter($this->BE_USER->user['uid'], \PDO::PARAM_INT)
8569
                ),
8570
                $queryBuilder->expr()->eq(
8571
                    'tstamp',
8572
                    $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
8573
                ),
8574
                $queryBuilder->expr()->neq('error', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
8575
            )
8576
            ->execute();
8577
8578
        while ($row = $result->fetch()) {
8579
            $log_data = unserialize($row['log_data']);
8580
            $msg = $row['error'] . ': ' . sprintf($row['details'], $log_data[0], $log_data[1], $log_data[2], $log_data[3], $log_data[4]);
8581
            /** @var FlashMessage $flashMessage */
8582
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, '', $row['error'] === 4 ? FlashMessage::WARNING : FlashMessage::ERROR, true);
8583
            /** @var FlashMessageService $flashMessageService */
8584
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
8585
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
8586
            $defaultFlashMessageQueue->enqueue($flashMessage);
8587
        }
8588
    }
8589
8590
    /*****************************
8591
     *
8592
     * Internal (do not use outside Core!)
8593
     *
8594
     *****************************/
8595
8596
    /**
8597
     * Find out if the record is a get the original page
8598
     *
8599
     * @param int $pageId the page UID (can be the default page record, or a page translation record ID)
8600
     * @return int the page UID of the default page record
8601
     */
8602
    protected function getDefaultLanguagePageId(int $pageId): int
8603
    {
8604
        $localizationParentFieldName = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
8605
        $row = $this->recordInfo('pages', $pageId, $localizationParentFieldName);
8606
        $localizationParent = (int)$row[$localizationParentFieldName];
8607
        if ($localizationParent > 0) {
8608
            return $localizationParent;
8609
        }
8610
        return $pageId;
8611
    }
8612
8613
    /**
8614
     * Preprocesses field array based on field type. Some fields must be adjusted
8615
     * before going to database. This is done on the copy of the field array because
8616
     * original values are used in remap action later.
8617
     *
8618
     * @param string $table	Table name
8619
     * @param array $fieldArray	Field array to check
8620
     * @return array Updated field array
8621
     * @internal should only be used from within TYPO3 Core
8622
     */
8623
    public function insertUpdateDB_preprocessBasedOnFieldType($table, $fieldArray)
8624
    {
8625
        $result = $fieldArray;
8626
        foreach ($fieldArray as $field => $value) {
8627
            if (!MathUtility::canBeInterpretedAsInteger($value)
8628
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'inline'
8629
                && $GLOBALS['TCA'][$table]['columns'][$field]['config']['foreign_field']) {
8630
                $result[$field] = count(GeneralUtility::trimExplode(',', $value, true));
8631
            }
8632
        }
8633
        return $result;
8634
    }
8635
8636
    /**
8637
     * Determines whether a particular record has been deleted
8638
     * using DataHandler::deleteRecord() in this instance.
8639
     *
8640
     * @param string $tableName
8641
     * @param string $uid
8642
     * @return bool
8643
     * @internal should only be used from within TYPO3 Core
8644
     */
8645
    public function hasDeletedRecord($tableName, $uid)
8646
    {
8647
        return
8648
            !empty($this->deletedRecords[$tableName])
8649
            && in_array($uid, $this->deletedRecords[$tableName])
8650
        ;
8651
    }
8652
8653
    /**
8654
     * Gets the automatically versionized id of a record.
8655
     *
8656
     * @param string $table Name of the table
8657
     * @param int $id Uid of the record
8658
     * @return int|null
8659
     * @internal should only be used from within TYPO3 Core
8660
     */
8661
    public function getAutoVersionId($table, $id): ?int
8662
    {
8663
        $result = null;
8664
        if (isset($this->autoVersionIdMap[$table][$id])) {
8665
            $result = (int)trim($this->autoVersionIdMap[$table][$id]);
8666
        }
8667
        return $result;
8668
    }
8669
8670
    /**
8671
     * Overlays the automatically versionized id of a record.
8672
     *
8673
     * @param string $table Name of the table
8674
     * @param int $id Uid of the record
8675
     * @return int
8676
     */
8677
    protected function overlayAutoVersionId($table, $id)
8678
    {
8679
        $autoVersionId = $this->getAutoVersionId($table, $id);
8680
        if ($autoVersionId !== null) {
8681
            $id = $autoVersionId;
8682
        }
8683
        return $id;
8684
    }
8685
8686
    /**
8687
     * Adds new values to the remapStackChildIds array.
8688
     *
8689
     * @param array $idValues uid values
8690
     */
8691
    protected function addNewValuesToRemapStackChildIds(array $idValues)
8692
    {
8693
        foreach ($idValues as $idValue) {
8694
            if (strpos($idValue, 'NEW') === 0) {
8695
                $this->remapStackChildIds[$idValue] = true;
8696
            }
8697
        }
8698
    }
8699
8700
    /**
8701
     * Resolves versioned records for the current workspace scope.
8702
     * Delete placeholders are substituted and removed.
8703
     *
8704
     * @param string $tableName Name of the table to be processed
8705
     * @param string $fieldNames List of the field names to be fetched
8706
     * @param string $sortingField Name of the sorting field to be used
8707
     * @param array $liveIds Flat array of (live) record ids
8708
     * @return array
8709
     */
8710
    protected function resolveVersionedRecords($tableName, $fieldNames, $sortingField, array $liveIds)
8711
    {
8712
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
8713
            ->getConnectionForTable($tableName);
8714
        $sortingStatement = !empty($sortingField)
8715
            ? [$connection->quoteIdentifier($sortingField)]
8716
            : null;
8717
        /** @var PlainDataResolver $resolver */
8718
        $resolver = GeneralUtility::makeInstance(
8719
            PlainDataResolver::class,
8720
            $tableName,
8721
            $liveIds,
8722
            $sortingStatement
8723
        );
8724
8725
        $resolver->setWorkspaceId($this->BE_USER->workspace);
8726
        $resolver->setKeepDeletePlaceholder(false);
8727
        $resolver->setKeepMovePlaceholder(false);
8728
        $resolver->setKeepLiveIds(true);
8729
        $recordIds = $resolver->get();
8730
8731
        $records = [];
8732
        foreach ($recordIds as $recordId) {
8733
            $records[$recordId] = BackendUtility::getRecord($tableName, $recordId, $fieldNames);
8734
        }
8735
8736
        return $records;
8737
    }
8738
8739
    /**
8740
     * Gets the outer most instance of \TYPO3\CMS\Core\DataHandling\DataHandler
8741
     * Since \TYPO3\CMS\Core\DataHandling\DataHandler can create nested objects of itself,
8742
     * this method helps to determine the first (= outer most) one.
8743
     *
8744
     * @return DataHandler
8745
     */
8746
    protected function getOuterMostInstance()
8747
    {
8748
        if (!isset($this->outerMostInstance)) {
8749
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
8750
            foreach ($stack as $stackItem) {
8751
                if (isset($stackItem['object']) && $stackItem['object'] instanceof self) {
8752
                    $this->outerMostInstance = $stackItem['object'];
8753
                    break;
8754
                }
8755
            }
8756
        }
8757
        return $this->outerMostInstance;
8758
    }
8759
8760
    /**
8761
     * Determines whether the this object is the outer most instance of itself
8762
     * Since DataHandler can create nested objects of itself,
8763
     * this method helps to determine the first (= outer most) one.
8764
     *
8765
     * @return bool
8766
     */
8767
    public function isOuterMostInstance()
8768
    {
8769
        return $this->getOuterMostInstance() === $this;
8770
    }
8771
8772
    /**
8773
     * Gets an instance of the runtime cache.
8774
     *
8775
     * @return FrontendInterface
8776
     */
8777
    protected function getRuntimeCache()
8778
    {
8779
        return $this->getCacheManager()->getCache('runtime');
8780
    }
8781
8782
    /**
8783
     * Determines nested element calls.
8784
     *
8785
     * @param string $table Name of the table
8786
     * @param int $id Uid of the record
8787
     * @param string $identifier Name of the action to be checked
8788
     * @return bool
8789
     */
8790
    protected function isNestedElementCallRegistered($table, $id, $identifier)
8791
    {
8792
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8793
        return isset($nestedElementCalls[$identifier][$table][$id]);
8794
    }
8795
8796
    /**
8797
     * Registers nested elements calls.
8798
     * This is used to track nested calls (e.g. for following m:n relations).
8799
     *
8800
     * @param string $table Name of the table
8801
     * @param int $id Uid of the record
8802
     * @param string $identifier Name of the action to be tracked
8803
     */
8804
    protected function registerNestedElementCall($table, $id, $identifier)
8805
    {
8806
        $nestedElementCalls = (array)$this->runtimeCache->get($this->cachePrefixNestedElementCalls);
8807
        $nestedElementCalls[$identifier][$table][$id] = true;
8808
        $this->runtimeCache->set($this->cachePrefixNestedElementCalls, $nestedElementCalls);
8809
    }
8810
8811
    /**
8812
     * Resets the nested element calls.
8813
     */
8814
    protected function resetNestedElementCalls()
8815
    {
8816
        $this->runtimeCache->remove($this->cachePrefixNestedElementCalls);
8817
    }
8818
8819
    /**
8820
     * Determines whether an element was registered to be deleted in the registry.
8821
     *
8822
     * @param string $table Name of the table
8823
     * @param int $id Uid of the record
8824
     * @return bool
8825
     * @see registerElementsToBeDeleted
8826
     * @see resetElementsToBeDeleted
8827
     * @see copyRecord_raw
8828
     * @see versionizeRecord
8829
     */
8830
    protected function isElementToBeDeleted($table, $id)
8831
    {
8832
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8833
        return isset($elementsToBeDeleted[$table][$id]);
8834
    }
8835
8836
    /**
8837
     * Registers elements to be deleted in the registry.
8838
     *
8839
     * @see process_datamap
8840
     */
8841
    protected function registerElementsToBeDeleted()
8842
    {
8843
        $elementsToBeDeleted = (array)$this->runtimeCache->get('core-datahandler-elementsToBeDeleted');
8844
        $this->runtimeCache->set('core-datahandler-elementsToBeDeleted', array_merge($elementsToBeDeleted, $this->getCommandMapElements('delete')));
8845
    }
8846
8847
    /**
8848
     * Resets the elements to be deleted in the registry.
8849
     *
8850
     * @see process_datamap
8851
     */
8852
    protected function resetElementsToBeDeleted()
8853
    {
8854
        $this->runtimeCache->remove('core-datahandler-elementsToBeDeleted');
8855
    }
8856
8857
    /**
8858
     * Unsets elements (e.g. of the data map) that shall be deleted.
8859
     * This avoids to modify records that will be deleted later on.
8860
     *
8861
     * @param array $elements Elements to be modified
8862
     * @return array
8863
     */
8864
    protected function unsetElementsToBeDeleted(array $elements)
8865
    {
8866
        $elements = ArrayUtility::arrayDiffAssocRecursive($elements, $this->getCommandMapElements('delete'));
8867
        foreach ($elements as $key => $value) {
8868
            if (empty($value)) {
8869
                unset($elements[$key]);
8870
            }
8871
        }
8872
        return $elements;
8873
    }
8874
8875
    /**
8876
     * Gets elements of the command map that match a particular command.
8877
     *
8878
     * @param string $needle The command to be matched
8879
     * @return array
8880
     */
8881
    protected function getCommandMapElements($needle)
8882
    {
8883
        $elements = [];
8884
        foreach ($this->cmdmap as $tableName => $idArray) {
8885
            foreach ($idArray as $id => $commandArray) {
8886
                foreach ($commandArray as $command => $value) {
8887
                    if ($value && $command == $needle) {
8888
                        $elements[$tableName][$id] = true;
8889
                    }
8890
                }
8891
            }
8892
        }
8893
        return $elements;
8894
    }
8895
8896
    /**
8897
     * Controls active elements and sets NULL values if not active.
8898
     * Datamap is modified accordant to submitted control values.
8899
     */
8900
    protected function controlActiveElements()
8901
    {
8902
        if (!empty($this->control['active'])) {
8903
            $this->setNullValues(
8904
                $this->control['active'],
8905
                $this->datamap
8906
            );
8907
        }
8908
    }
8909
8910
    /**
8911
     * Sets NULL values in haystack array.
8912
     * The general behaviour in the user interface is to enable/activate fields.
8913
     * Thus, this method uses NULL as value to be stored if a field is not active.
8914
     *
8915
     * @param array $active hierarchical array with active elements
8916
     * @param array $haystack hierarchical array with haystack to be modified
8917
     */
8918
    protected function setNullValues(array $active, array &$haystack)
8919
    {
8920
        foreach ($active as $key => $value) {
8921
            // Nested data is processes recursively
8922
            if (is_array($value)) {
8923
                $this->setNullValues(
8924
                    $value,
8925
                    $haystack[$key]
8926
                );
8927
            } elseif ($value == 0) {
8928
                // Field has not been activated in the user interface,
8929
                // thus a NULL value shall be stored in the database
8930
                $haystack[$key] = null;
8931
            }
8932
        }
8933
    }
8934
8935
    /**
8936
     * @param CorrelationId $correlationId
8937
     */
8938
    public function setCorrelationId(CorrelationId $correlationId): void
8939
    {
8940
        $this->correlationId = $correlationId;
8941
    }
8942
8943
    /**
8944
     * @return CorrelationId|null
8945
     */
8946
    public function getCorrelationId(): ?CorrelationId
8947
    {
8948
        return $this->correlationId;
8949
    }
8950
8951
    /**
8952
     * Entry point to post process a database insert. Currently bails early unless a UID has been forced
8953
     * and the database platform is not MySQL.
8954
     *
8955
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8956
     * @param string $tableName
8957
     * @param int $suggestedUid
8958
     * @return int
8959
     */
8960
    protected function postProcessDatabaseInsert(Connection $connection, string $tableName, int $suggestedUid): int
8961
    {
8962
        if ($suggestedUid !== 0 && $connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
8963
            $this->postProcessPostgresqlInsert($connection, $tableName);
8964
            // The last inserted id on postgresql is actually the last value generated by the sequence.
8965
            // On a forced UID insert this might not be the actual value or the sequence might not even
8966
            // have generated a value yet.
8967
            // Return the actual ID we forced on insert as a surrogate.
8968
            return $suggestedUid;
8969
        }
8970
        if ($connection->getDatabasePlatform() instanceof SQLServerPlatform) {
8971
            return $this->postProcessSqlServerInsert($connection, $tableName);
8972
        }
8973
        $id = $connection->lastInsertId($tableName);
8974
        return (int)$id;
8975
    }
8976
8977
    /**
8978
     * Get the last insert ID from sql server
8979
     *
8980
     * - first checks whether doctrine might be able to fetch the ID from the
8981
     * sequence table
8982
     * - if that does not succeed it manually selects the current IDENTITY value
8983
     * from a table
8984
     * - returns 0 if both fail
8985
     *
8986
     * @param \TYPO3\CMS\Core\Database\Connection $connection
8987
     * @param string $tableName
8988
     * @return int
8989
     * @throws \Doctrine\DBAL\DBALException
8990
     */
8991
    protected function postProcessSqlServerInsert(Connection $connection, string $tableName): int
8992
    {
8993
        $id = $connection->lastInsertId($tableName);
8994
        if (!((int)$id > 0)) {
8995
            $table = $connection->quoteIdentifier($tableName);
8996
            $result = $connection->executeQuery('SELECT IDENT_CURRENT(\'' . $table . '\') AS id')->fetch();
8997
            if (isset($result['id']) && $result['id'] > 0) {
8998
                $id = $result['id'];
8999
            }
9000
        }
9001
        return (int)$id;
9002
    }
9003
9004
    /**
9005
     * PostgreSQL works with sequences for auto increment columns. A sequence is not updated when a value is
9006
     * written to such a column. To avoid clashes when the sequence returns an existing ID this helper will
9007
     * update the sequence to the current max value of the column.
9008
     *
9009
     * @param \TYPO3\CMS\Core\Database\Connection $connection
9010
     * @param string $tableName
9011
     */
9012
    protected function postProcessPostgresqlInsert(Connection $connection, string $tableName)
9013
    {
9014
        $queryBuilder = $connection->createQueryBuilder();
9015
        $queryBuilder->getRestrictions()->removeAll();
9016
        $row = $queryBuilder->select('PGT.schemaname', 'S.relname', 'C.attname', 'T.relname AS tablename')
9017
            ->from('pg_class', 'S')
9018
            ->from('pg_depend', 'D')
9019
            ->from('pg_class', 'T')
9020
            ->from('pg_attribute', 'C')
9021
            ->from('pg_tables', 'PGT')
9022
            ->where(
9023
                $queryBuilder->expr()->eq('S.relkind', $queryBuilder->quote('S')),
9024
                $queryBuilder->expr()->eq('S.oid', $queryBuilder->quoteIdentifier('D.objid')),
9025
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('T.oid')),
9026
                $queryBuilder->expr()->eq('D.refobjid', $queryBuilder->quoteIdentifier('C.attrelid')),
9027
                $queryBuilder->expr()->eq('D.refobjsubid', $queryBuilder->quoteIdentifier('C.attnum')),
9028
                $queryBuilder->expr()->eq('T.relname', $queryBuilder->quoteIdentifier('PGT.tablename')),
9029
                $queryBuilder->expr()->eq('PGT.tablename', $queryBuilder->quote($tableName))
9030
            )
9031
            ->setMaxResults(1)
9032
            ->execute()
9033
            ->fetch();
9034
9035
        if ($row !== false) {
9036
            $connection->exec(
9037
                sprintf(
9038
                    'SELECT SETVAL(%s, COALESCE(MAX(%s), 0)+1, FALSE) FROM %s',
9039
                    $connection->quote($row['schemaname'] . '.' . $row['relname']),
9040
                    $connection->quoteIdentifier($row['attname']),
9041
                    $connection->quoteIdentifier($row['schemaname'] . '.' . $row['tablename'])
9042
                )
9043
            );
9044
        }
9045
    }
9046
9047
    /**
9048
     * Return the cache entry identifier for field evals
9049
     *
9050
     * @param string $additionalIdentifier
9051
     * @return string
9052
     */
9053
    protected function getFieldEvalCacheIdentifier($additionalIdentifier)
9054
    {
9055
        return 'core-datahandler-eval-' . md5($additionalIdentifier);
9056
    }
9057
9058
    /**
9059
     * @return RelationHandler
9060
     */
9061
    protected function createRelationHandlerInstance()
9062
    {
9063
        $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
9064
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
9065
        $relationHandler->setWorkspaceId($this->BE_USER->workspace);
9066
        $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded);
9067
        $relationHandler->setUseLiveParentIds($isWorkspacesLoaded);
9068
        $relationHandler->setReferenceIndexUpdater($this->referenceIndexUpdater);
9069
        return $relationHandler;
9070
    }
9071
9072
    /**
9073
     * Create and returns an instance of the CacheManager
9074
     *
9075
     * @return CacheManager
9076
     */
9077
    protected function getCacheManager()
9078
    {
9079
        return GeneralUtility::makeInstance(CacheManager::class);
9080
    }
9081
9082
    /**
9083
     * Gets the resourceFactory
9084
     *
9085
     * @return ResourceFactory
9086
     */
9087
    protected function getResourceFactory()
9088
    {
9089
        return GeneralUtility::makeInstance(ResourceFactory::class);
9090
    }
9091
9092
    /**
9093
     * @return LanguageService
9094
     */
9095
    protected function getLanguageService()
9096
    {
9097
        return $GLOBALS['LANG'];
9098
    }
9099
9100
    /**
9101
     * @internal should only be used from within TYPO3 Core
9102
     * @return array
9103
     */
9104
    public function getHistoryRecords(): array
9105
    {
9106
        return $this->historyRecords;
9107
    }
9108
}
9109