Total Complexity | 149 |
Total Lines | 1126 |
Duplicated Lines | 7.73 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like WorkspaceService 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 WorkspaceService, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
33 | class WorkspaceService implements SingletonInterface |
||
34 | { |
||
35 | /** |
||
36 | * @var array |
||
37 | */ |
||
38 | protected $pageCache = []; |
||
39 | |||
40 | /** |
||
41 | * @var array |
||
42 | */ |
||
43 | protected $versionsOnPageCache = []; |
||
44 | |||
45 | /** |
||
46 | * @var array |
||
47 | */ |
||
48 | protected $pagesWithVersionsInTable = []; |
||
49 | |||
50 | const TABLE_WORKSPACE = 'sys_workspace'; |
||
51 | const SELECT_ALL_WORKSPACES = -98; |
||
52 | const LIVE_WORKSPACE_ID = 0; |
||
53 | /** |
||
54 | * retrieves the available workspaces from the database and checks whether |
||
55 | * they're available to the current BE user |
||
56 | * |
||
57 | * @return array array of worspaces available to the current user |
||
58 | */ |
||
59 | public function getAvailableWorkspaces() |
||
60 | { |
||
61 | $availableWorkspaces = []; |
||
62 | // add default workspaces |
||
63 | if ($GLOBALS['BE_USER']->checkWorkspace(['uid' => (string)self::LIVE_WORKSPACE_ID])) { |
||
64 | $availableWorkspaces[self::LIVE_WORKSPACE_ID] = self::getWorkspaceTitle(self::LIVE_WORKSPACE_ID); |
||
65 | } |
||
66 | // add custom workspaces (selecting all, filtering by BE_USER check): |
||
67 | |||
68 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace'); |
||
69 | $queryBuilder->getRestrictions() |
||
70 | ->add(GeneralUtility::makeInstance(RootLevelRestriction::class)); |
||
71 | |||
72 | $result = $queryBuilder |
||
73 | ->select('uid', 'title', 'adminusers', 'members') |
||
74 | ->from('sys_workspace') |
||
75 | ->orderBy('title') |
||
76 | ->execute(); |
||
77 | |||
78 | while ($workspace = $result->fetch()) { |
||
79 | if ($GLOBALS['BE_USER']->checkWorkspace($workspace)) { |
||
80 | $availableWorkspaces[$workspace['uid']] = $workspace['title']; |
||
81 | } |
||
82 | } |
||
83 | return $availableWorkspaces; |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * Gets the current workspace ID. |
||
88 | * |
||
89 | * @return int The current workspace ID |
||
90 | */ |
||
91 | public function getCurrentWorkspace() |
||
92 | { |
||
93 | $workspaceId = $GLOBALS['BE_USER']->workspace; |
||
94 | $activeId = $GLOBALS['BE_USER']->getSessionData('tx_workspace_activeWorkspace'); |
||
95 | |||
96 | // Avoid invalid workspace settings |
||
97 | if ($activeId !== null && $activeId !== self::SELECT_ALL_WORKSPACES) { |
||
98 | $availableWorkspaces = $this->getAvailableWorkspaces(); |
||
99 | if (isset($availableWorkspaces[$activeId])) { |
||
100 | $workspaceId = $activeId; |
||
101 | } |
||
102 | } |
||
103 | |||
104 | return $workspaceId; |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * Find the title for the requested workspace. |
||
109 | * |
||
110 | * @param int $wsId |
||
111 | * @return string |
||
112 | * @throws \InvalidArgumentException |
||
113 | */ |
||
114 | public static function getWorkspaceTitle($wsId) |
||
115 | { |
||
116 | $title = false; |
||
117 | switch ($wsId) { |
||
118 | case self::LIVE_WORKSPACE_ID: |
||
119 | $title = $GLOBALS['LANG']->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:shortcut_onlineWS'); |
||
120 | break; |
||
121 | default: |
||
122 | $labelField = $GLOBALS['TCA']['sys_workspace']['ctrl']['label']; |
||
123 | $wsRecord = BackendUtility::getRecord('sys_workspace', $wsId, 'uid,' . $labelField); |
||
124 | if (is_array($wsRecord)) { |
||
125 | $title = $wsRecord[$labelField]; |
||
126 | } |
||
127 | } |
||
128 | if ($title === false) { |
||
129 | throw new \InvalidArgumentException('No such workspace defined', 1476045469); |
||
130 | } |
||
131 | return $title; |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Building DataHandler CMD-array for swapping all versions in a workspace. |
||
136 | * |
||
137 | * @param int $wsid Real workspace ID, cannot be ONLINE (zero). |
||
138 | * @param bool $doSwap If set, then the currently online versions are swapped into the workspace in exchange for the offline versions. Otherwise the workspace is emptied. |
||
139 | * @param int $pageId The page id |
||
140 | * @param int $language Select specific language only |
||
141 | * @return array Command array for DataHandler |
||
142 | */ |
||
143 | public function getCmdArrayForPublishWS($wsid, $doSwap, $pageId = 0, $language = null) |
||
144 | { |
||
145 | $wsid = (int)$wsid; |
||
146 | $cmd = []; |
||
147 | if ($wsid >= -1 && $wsid !== 0) { |
||
148 | // Define stage to select: |
||
149 | $stage = -99; |
||
150 | if ($wsid > 0) { |
||
151 | $workspaceRec = BackendUtility::getRecord('sys_workspace', $wsid); |
||
152 | if ($workspaceRec['publish_access'] & 1) { |
||
153 | $stage = \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_ID; |
||
154 | } |
||
155 | } |
||
156 | // Select all versions to swap: |
||
157 | $versions = $this->selectVersionsInWorkspace($wsid, 0, $stage, $pageId ?: -1, 999, 'tables_modify', $language); |
||
158 | // Traverse the selection to build CMD array: |
||
159 | foreach ($versions as $table => $records) { |
||
160 | foreach ($records as $rec) { |
||
161 | // Build the cmd Array: |
||
162 | $cmd[$table][$rec['t3ver_oid']]['version'] = ['action' => 'swap', 'swapWith' => $rec['uid'], 'swapIntoWS' => $doSwap ? 1 : 0]; |
||
163 | } |
||
164 | } |
||
165 | } |
||
166 | return $cmd; |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Building DataHandler CMD-array for releasing all versions in a workspace. |
||
171 | * |
||
172 | * @param int $wsid Real workspace ID, cannot be ONLINE (zero). |
||
173 | * @param bool $flush Run Flush (TRUE) or ClearWSID (FALSE) command |
||
174 | * @param int $pageId The page id |
||
175 | * @param int $language Select specific language only |
||
176 | * @return array Command array for DataHandler |
||
177 | */ |
||
178 | public function getCmdArrayForFlushWS($wsid, $flush = true, $pageId = 0, $language = null) |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * Select all records from workspace pending for publishing |
||
200 | * Used from backend to display workspace overview |
||
201 | * User for auto-publishing for selecting versions for publication |
||
202 | * |
||
203 | * @param int $wsid Workspace ID. If -99, will select ALL versions from ANY workspace. If -98 will select all but ONLINE. >=-1 will select from the actual workspace |
||
204 | * @param int $filter Lifecycle filter: 1 = select all drafts (never-published), 2 = select all published one or more times (archive/multiple), anything else selects all. |
||
205 | * @param int $stage Stage filter: -99 means no filtering, otherwise it will be used to select only elements with that stage. For publishing, that would be "10 |
||
206 | * @param int $pageId Page id: Live page for which to find versions in workspace! |
||
207 | * @param int $recursionLevel Recursion Level - select versions recursive - parameter is only relevant if $pageId != -1 |
||
208 | * @param string $selectionType How to collect records for "listing" or "modify" these tables. Support the permissions of each type of record, see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::check. |
||
209 | * @param int $language Select specific language only |
||
210 | * @return array Array of all records uids etc. First key is table name, second key incremental integer. Records are associative arrays with uid and t3ver_oidfields. The pid of the online record is found as "livepid" the pid of the offline record is found in "wspid |
||
211 | */ |
||
212 | public function selectVersionsInWorkspace($wsid, $filter = 0, $stage = -99, $pageId = -1, $recursionLevel = 0, $selectionType = 'tables_select', $language = null) |
||
213 | { |
||
214 | $wsid = (int)$wsid; |
||
215 | $filter = (int)$filter; |
||
216 | $output = []; |
||
217 | // Contains either nothing or a list with live-uids |
||
218 | if ($pageId != -1 && $recursionLevel > 0) { |
||
219 | $pageList = $this->getTreeUids($pageId, $wsid, $recursionLevel); |
||
220 | } elseif ($pageId != -1) { |
||
221 | $pageList = $pageId; |
||
222 | } else { |
||
223 | $pageList = ''; |
||
224 | // check if person may only see a "virtual" page-root |
||
225 | $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts()); |
||
226 | $mountPoints = array_unique($mountPoints); |
||
227 | View Code Duplication | if (!in_array(0, $mountPoints)) { |
|
228 | $tempPageIds = []; |
||
229 | foreach ($mountPoints as $mountPoint) { |
||
230 | $tempPageIds[] = $this->getTreeUids($mountPoint, $wsid, $recursionLevel); |
||
231 | } |
||
232 | $pageList = implode(',', $tempPageIds); |
||
233 | $pageList = implode(',', array_unique(explode(',', $pageList))); |
||
234 | } |
||
235 | } |
||
236 | // Traversing all tables supporting versioning: |
||
237 | foreach ($GLOBALS['TCA'] as $table => $cfg) { |
||
238 | // we do not collect records from tables without permissions on them. |
||
239 | if (!$GLOBALS['BE_USER']->check($selectionType, $table)) { |
||
240 | continue; |
||
241 | } |
||
242 | if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { |
||
243 | $recs = $this->selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language); |
||
244 | $moveRecs = $this->getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage); |
||
245 | $recs = array_merge($recs, $moveRecs); |
||
246 | $recs = $this->filterPermittedElements($recs, $table); |
||
247 | if (!empty($recs)) { |
||
248 | $output[$table] = $recs; |
||
249 | } |
||
250 | } |
||
251 | } |
||
252 | return $output; |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * Find all versionized elements except moved records. |
||
257 | * |
||
258 | * @param string $table |
||
259 | * @param string $pageList |
||
260 | * @param int $wsid |
||
261 | * @param int $filter |
||
262 | * @param int $stage |
||
263 | * @param int $language |
||
264 | * @return array |
||
265 | */ |
||
266 | protected function selectAllVersionsFromPages($table, $pageList, $wsid, $filter, $stage, $language = null) |
||
267 | { |
||
268 | // Include root level page as there might be some records with where root level |
||
269 | // restriction is ignored (e.g. FAL records) |
||
270 | if ($pageList !== '' && BackendUtility::isRootLevelRestrictionIgnored($table)) { |
||
271 | $pageList .= ',0'; |
||
272 | } |
||
273 | $isTableLocalizable = BackendUtility::isTableLocalizable($table); |
||
274 | $languageParentField = ''; |
||
275 | // If table is not localizable, but localized reocrds shall |
||
276 | // be collected, an empty result array needs to be returned: |
||
277 | if ($isTableLocalizable === false && $language > 0) { |
||
278 | return []; |
||
279 | } |
||
280 | if ($isTableLocalizable) { |
||
281 | $languageParentField = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; |
||
282 | } |
||
283 | |||
284 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); |
||
285 | $queryBuilder->getRestrictions()->removeAll() |
||
286 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
287 | |||
288 | $fields = ['A.uid', 'A.t3ver_oid', 'A.t3ver_stage', 'B.pid AS wspid', 'B.pid AS livepid']; |
||
289 | if ($isTableLocalizable) { |
||
290 | $fields[] = $languageParentField; |
||
291 | $fields[] = 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField']; |
||
292 | } |
||
293 | // Table A is the offline version and pid=-1 defines offline |
||
294 | // Table B (online) must have PID >= 0 to signify being online. |
||
295 | $constraints = [ |
||
296 | $queryBuilder->expr()->eq( |
||
297 | 'A.pid', |
||
298 | $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) |
||
299 | ), |
||
300 | $queryBuilder->expr()->gte( |
||
301 | 'B.pid', |
||
302 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
303 | ), |
||
304 | $queryBuilder->expr()->neq( |
||
305 | 'A.t3ver_state', |
||
306 | $queryBuilder->createNamedParameter( |
||
307 | (string)new VersionState(VersionState::MOVE_POINTER), |
||
308 | \PDO::PARAM_INT |
||
309 | ) |
||
310 | ) |
||
311 | ]; |
||
312 | |||
313 | View Code Duplication | if ($pageList) { |
|
314 | $pidField = $table === 'pages' ? 'uid' : 'pid'; |
||
315 | $constraints[] = $queryBuilder->expr()->in( |
||
316 | 'B.' . $pidField, |
||
317 | $queryBuilder->createNamedParameter( |
||
318 | GeneralUtility::intExplode(',', $pageList, true), |
||
319 | Connection::PARAM_INT_ARRAY |
||
320 | ) |
||
321 | ); |
||
322 | } |
||
323 | |||
324 | if ($isTableLocalizable && MathUtility::canBeInterpretedAsInteger($language)) { |
||
325 | $constraints[] = $queryBuilder->expr()->eq( |
||
326 | 'A.' . $GLOBALS['TCA'][$table]['ctrl']['languageField'], |
||
327 | $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT) |
||
328 | ); |
||
329 | } |
||
330 | |||
331 | // For "real" workspace numbers, select by that. |
||
332 | // If = -98, select all that are NOT online (zero). |
||
333 | // Anything else below -1 will not select on the wsid and therefore select all! |
||
334 | View Code Duplication | if ($wsid > self::SELECT_ALL_WORKSPACES) { |
|
335 | $constraints[] = $queryBuilder->expr()->eq( |
||
336 | 'A.t3ver_wsid', |
||
337 | $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT) |
||
338 | ); |
||
339 | } elseif ($wsid === self::SELECT_ALL_WORKSPACES) { |
||
340 | $constraints[] = $queryBuilder->expr()->neq( |
||
341 | 'A.t3ver_wsid', |
||
342 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
343 | ); |
||
344 | } |
||
345 | |||
346 | // lifecycle filter: |
||
347 | // 1 = select all drafts (never-published), |
||
348 | // 2 = select all published one or more times (archive/multiple) |
||
349 | View Code Duplication | if ($filter === 1) { |
|
350 | $constraints[] = $queryBuilder->expr()->eq( |
||
351 | 'A.t3ver_count', |
||
352 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
353 | ); |
||
354 | } elseif ($filter === 2) { |
||
355 | $constraints[] = $queryBuilder->expr()->gt( |
||
356 | 'A.t3ver_count', |
||
357 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
358 | ); |
||
359 | } |
||
360 | |||
361 | View Code Duplication | if ((int)$stage !== -99) { |
|
362 | $constraints[] = $queryBuilder->expr()->eq( |
||
363 | 'A.t3ver_stage', |
||
364 | $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT) |
||
365 | ); |
||
366 | } |
||
367 | |||
368 | // ... and finally the join between the two tables. |
||
369 | $constraints[] = $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')); |
||
370 | |||
371 | // Select all records from this table in the database from the workspace |
||
372 | // This joins the online version with the offline version as tables A and B |
||
373 | // Order by UID, mostly to have a sorting in the backend overview module which |
||
374 | // doesn't "jump around" when swapping. |
||
375 | $rows = $queryBuilder->select(...$fields) |
||
376 | ->from($table, 'A') |
||
377 | ->from($table, 'B') |
||
378 | ->where(...$constraints) |
||
379 | ->orderBy('B.uid') |
||
380 | ->execute() |
||
381 | ->fetchAll(); |
||
382 | |||
383 | return $rows; |
||
384 | } |
||
385 | |||
386 | /** |
||
387 | * Find all moved records at their new position. |
||
388 | * |
||
389 | * @param string $table |
||
390 | * @param string $pageList |
||
391 | * @param int $wsid |
||
392 | * @param int $filter |
||
393 | * @param int $stage |
||
394 | * @return array |
||
395 | */ |
||
396 | protected function getMoveToPlaceHolderFromPages($table, $pageList, $wsid, $filter, $stage) |
||
397 | { |
||
398 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); |
||
399 | $queryBuilder->getRestrictions()->removeAll() |
||
400 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
401 | |||
402 | // Aliases: |
||
403 | // A - moveTo placeholder |
||
404 | // B - online record |
||
405 | // C - moveFrom placeholder |
||
406 | $constraints = [ |
||
407 | $queryBuilder->expr()->eq( |
||
408 | 'A.t3ver_state', |
||
409 | $queryBuilder->createNamedParameter( |
||
410 | (string)new VersionState(VersionState::MOVE_PLACEHOLDER), |
||
411 | \PDO::PARAM_INT |
||
412 | ) |
||
413 | ), |
||
414 | $queryBuilder->expr()->gt( |
||
415 | 'B.pid', |
||
416 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
417 | ), |
||
418 | $queryBuilder->expr()->eq( |
||
419 | 'B.t3ver_state', |
||
420 | $queryBuilder->createNamedParameter( |
||
421 | (string)new VersionState(VersionState::DEFAULT_STATE), |
||
422 | \PDO::PARAM_INT |
||
423 | ) |
||
424 | ), |
||
425 | $queryBuilder->expr()->eq( |
||
426 | 'B.t3ver_wsid', |
||
427 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
428 | ), |
||
429 | $queryBuilder->expr()->eq( |
||
430 | 'C.pid', |
||
431 | $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) |
||
432 | ), |
||
433 | $queryBuilder->expr()->eq( |
||
434 | 'C.t3ver_state', |
||
435 | $queryBuilder->createNamedParameter( |
||
436 | (string)new VersionState(VersionState::MOVE_POINTER), |
||
437 | \PDO::PARAM_INT |
||
438 | ) |
||
439 | ), |
||
440 | $queryBuilder->expr()->eq('A.t3ver_move_id', $queryBuilder->quoteIdentifier('B.uid')), |
||
441 | $queryBuilder->expr()->eq('B.uid', $queryBuilder->quoteIdentifier('C.t3ver_oid')) |
||
442 | ]; |
||
443 | |||
444 | if ($wsid > self::SELECT_ALL_WORKSPACES) { |
||
445 | $constraints[] = $queryBuilder->expr()->eq( |
||
446 | 'A.t3ver_wsid', |
||
447 | $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT) |
||
448 | ); |
||
449 | $constraints[] = $queryBuilder->expr()->eq( |
||
450 | 'C.t3ver_wsid', |
||
451 | $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT) |
||
452 | ); |
||
453 | View Code Duplication | } elseif ($wsid === self::SELECT_ALL_WORKSPACES) { |
|
454 | $constraints[] = $queryBuilder->expr()->neq( |
||
455 | 'A.t3ver_wsid', |
||
456 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
457 | ); |
||
458 | $constraints[] = $queryBuilder->expr()->neq( |
||
459 | 'C.t3ver_wsid', |
||
460 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
461 | ); |
||
462 | } |
||
463 | |||
464 | // lifecycle filter: |
||
465 | // 1 = select all drafts (never-published), |
||
466 | // 2 = select all published one or more times (archive/multiple) |
||
467 | View Code Duplication | if ($filter === 1) { |
|
468 | $constraints[] = $queryBuilder->expr()->eq( |
||
469 | 'C.t3ver_count', |
||
470 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
471 | ); |
||
472 | } elseif ($filter === 2) { |
||
473 | $constraints[] = $queryBuilder->expr()->gt( |
||
474 | 'C.t3ver_count', |
||
475 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
476 | ); |
||
477 | } |
||
478 | |||
479 | View Code Duplication | if ((int)$stage != -99) { |
|
480 | $constraints[] = $queryBuilder->expr()->eq( |
||
481 | 'C.t3ver_stage', |
||
482 | $queryBuilder->createNamedParameter($stage, \PDO::PARAM_INT) |
||
483 | ); |
||
484 | } |
||
485 | |||
486 | View Code Duplication | if ($pageList) { |
|
487 | $pidField = $table === 'pages' ? 'B.uid' : 'A.pid'; |
||
488 | $constraints[] = $queryBuilder->expr()->in( |
||
489 | $pidField, |
||
490 | $queryBuilder->createNamedParameter( |
||
491 | GeneralUtility::intExplode(',', $pageList, true), |
||
492 | Connection::PARAM_INT_ARRAY |
||
493 | ) |
||
494 | ); |
||
495 | } |
||
496 | |||
497 | $rows = $queryBuilder |
||
498 | ->select('A.pid AS wspid', 'B.uid AS t3ver_oid', 'C.uid AS uid', 'B.pid AS livepid') |
||
499 | ->from($table, 'A') |
||
500 | ->from($table, 'B') |
||
501 | ->from($table, 'C') |
||
502 | ->where(...$constraints) |
||
503 | ->orderBy('A.uid') |
||
504 | ->execute() |
||
505 | ->fetchAll(); |
||
506 | |||
507 | return $rows; |
||
508 | } |
||
509 | |||
510 | /** |
||
511 | * Find all page uids recursive starting from a specific page |
||
512 | * |
||
513 | * @param int $pageId |
||
514 | * @param int $wsid |
||
515 | * @param int $recursionLevel |
||
516 | * @return string Comma sep. uid list |
||
517 | */ |
||
518 | protected function getTreeUids($pageId, $wsid, $recursionLevel) |
||
519 | { |
||
520 | // Reusing existing functionality with the drawback that |
||
521 | // mount points are not covered yet |
||
522 | $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1); |
||
523 | /** @var $searchObj \TYPO3\CMS\Core\Database\QueryView */ |
||
524 | $searchObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\QueryView::class); |
||
525 | if ($pageId > 0) { |
||
526 | $pageList = $searchObj->getTreeList($pageId, $recursionLevel, 0, $perms_clause); |
||
527 | } else { |
||
528 | $mountPoints = $GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint']; |
||
529 | View Code Duplication | if (!is_array($mountPoints) || empty($mountPoints)) { |
|
530 | $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts()); |
||
531 | $mountPoints = array_unique($mountPoints); |
||
532 | } |
||
533 | $newList = []; |
||
534 | foreach ($mountPoints as $mountPoint) { |
||
535 | $newList[] = $searchObj->getTreeList($mountPoint, $recursionLevel, 0, $perms_clause); |
||
536 | } |
||
537 | $pageList = implode(',', $newList); |
||
538 | } |
||
539 | unset($searchObj); |
||
540 | |||
541 | if (BackendUtility::isTableWorkspaceEnabled('pages') && $pageList) { |
||
542 | // Remove the "subbranch" if a page was moved away |
||
543 | $pageIds = GeneralUtility::intExplode(',', $pageList, true); |
||
544 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); |
||
545 | $queryBuilder->getRestrictions() |
||
546 | ->removeAll() |
||
547 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
548 | $result = $queryBuilder |
||
549 | ->select('uid', 'pid', 't3ver_move_id') |
||
550 | ->from('pages') |
||
551 | ->where( |
||
552 | $queryBuilder->expr()->in( |
||
553 | 't3ver_move_id', |
||
554 | $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY) |
||
555 | ), |
||
556 | $queryBuilder->expr()->eq( |
||
557 | 't3ver_wsid', |
||
558 | $queryBuilder->createNamedParameter($wsid, \PDO::PARAM_INT) |
||
559 | ) |
||
560 | ) |
||
561 | ->orderBy('uid') |
||
562 | ->execute(); |
||
563 | |||
564 | $movedAwayPages = []; |
||
565 | while ($row = $result->fetch()) { |
||
566 | $movedAwayPages[$row['t3ver_move_id']] = $row; |
||
567 | } |
||
568 | |||
569 | // move all pages away |
||
570 | $newList = array_diff($pageIds, array_keys($movedAwayPages)); |
||
571 | // keep current page in the list |
||
572 | $newList[] = $pageId; |
||
573 | // move back in if still connected to the "remaining" pages |
||
574 | do { |
||
575 | $changed = false; |
||
576 | foreach ($movedAwayPages as $uid => $rec) { |
||
577 | if (in_array($rec['pid'], $newList) && !in_array($uid, $newList)) { |
||
578 | $newList[] = $uid; |
||
579 | $changed = true; |
||
580 | } |
||
581 | } |
||
582 | } while ($changed); |
||
583 | |||
584 | // In case moving pages is enabled we need to replace all move-to pointer with their origin |
||
585 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); |
||
586 | $queryBuilder->getRestrictions() |
||
587 | ->removeAll() |
||
588 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
589 | $result = $queryBuilder->select('uid', 't3ver_move_id') |
||
590 | ->from('pages') |
||
591 | ->where( |
||
592 | $queryBuilder->expr()->in( |
||
593 | 'uid', |
||
594 | $queryBuilder->createNamedParameter($newList, Connection::PARAM_INT_ARRAY) |
||
595 | ) |
||
596 | ) |
||
597 | ->orderBy('uid') |
||
598 | ->execute(); |
||
599 | |||
600 | $pages = []; |
||
601 | while ($row = $result->fetch()) { |
||
602 | $pages[$row['uid']] = $row; |
||
603 | } |
||
604 | |||
605 | $pageIds = $newList; |
||
606 | if (!in_array($pageId, $pageIds)) { |
||
607 | $pageIds[] = $pageId; |
||
608 | } |
||
609 | |||
610 | $newList = []; |
||
611 | foreach ($pageIds as $pageId) { |
||
612 | if ((int)$pages[$pageId]['t3ver_move_id'] > 0) { |
||
613 | $newList[] = (int)$pages[$pageId]['t3ver_move_id']; |
||
614 | } else { |
||
615 | $newList[] = $pageId; |
||
616 | } |
||
617 | } |
||
618 | $pageList = implode(',', $newList); |
||
619 | } |
||
620 | |||
621 | return $pageList; |
||
622 | } |
||
623 | |||
624 | /** |
||
625 | * Remove all records which are not permitted for the user |
||
626 | * |
||
627 | * @param array $recs |
||
628 | * @param string $table |
||
629 | * @return array |
||
630 | */ |
||
631 | protected function filterPermittedElements($recs, $table) |
||
632 | { |
||
633 | $permittedElements = []; |
||
634 | if (is_array($recs)) { |
||
635 | foreach ($recs as $rec) { |
||
636 | if ($this->isPageAccessibleForCurrentUser($table, $rec) && $this->isLanguageAccessibleForCurrentUser($table, $rec)) { |
||
637 | $permittedElements[] = $rec; |
||
638 | } |
||
639 | } |
||
640 | } |
||
641 | return $permittedElements; |
||
642 | } |
||
643 | |||
644 | /** |
||
645 | * Checking access to the page the record is on, respecting ignored root level restrictions |
||
646 | * |
||
647 | * @param string $table Name of the table |
||
648 | * @param array $record Record row to be checked |
||
649 | * @return bool |
||
650 | */ |
||
651 | protected function isPageAccessibleForCurrentUser($table, array $record) |
||
652 | { |
||
653 | $pageIdField = $table === 'pages' ? 'uid' : 'wspid'; |
||
654 | $pageId = isset($record[$pageIdField]) ? (int)$record[$pageIdField] : null; |
||
655 | if ($pageId === null) { |
||
656 | return false; |
||
657 | } |
||
658 | if ($pageId === 0 && BackendUtility::isRootLevelRestrictionIgnored($table)) { |
||
659 | return true; |
||
660 | } |
||
661 | $page = BackendUtility::getRecord('pages', $pageId, 'uid,pid,perms_userid,perms_user,perms_groupid,perms_group,perms_everybody'); |
||
662 | |||
663 | return $GLOBALS['BE_USER']->doesUserHaveAccess($page, 1); |
||
664 | } |
||
665 | |||
666 | /** |
||
667 | * Check current be users language access on given record. |
||
668 | * |
||
669 | * @param string $table Name of the table |
||
670 | * @param array $record Record row to be checked |
||
671 | * @return bool |
||
672 | */ |
||
673 | protected function isLanguageAccessibleForCurrentUser($table, array $record) |
||
674 | { |
||
675 | if (BackendUtility::isTableLocalizable($table)) { |
||
676 | $languageUid = $record[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; |
||
677 | } else { |
||
678 | return true; |
||
679 | } |
||
680 | return $GLOBALS['BE_USER']->checkLanguageAccess($languageUid); |
||
681 | } |
||
682 | |||
683 | /** |
||
684 | * Determine whether a specific page is new and not yet available in the LIVE workspace |
||
685 | * |
||
686 | * @param int $id Primary key of the page to check |
||
687 | * @param int $language Language for which to check the page |
||
688 | * @return bool |
||
689 | */ |
||
690 | public static function isNewPage($id, $language = 0) |
||
691 | { |
||
692 | $isNewPage = false; |
||
693 | // If the language is not default, check state of overlay |
||
694 | if ($language > 0) { |
||
695 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
||
696 | ->getQueryBuilderForTable('pages'); |
||
697 | $queryBuilder->getRestrictions() |
||
698 | ->removeAll() |
||
699 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
700 | $row = $queryBuilder->select('t3ver_state') |
||
701 | ->from('pages') |
||
702 | ->where( |
||
703 | $queryBuilder->expr()->eq( |
||
704 | $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], |
||
705 | $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT) |
||
706 | ), |
||
707 | $queryBuilder->expr()->eq( |
||
708 | $GLOBALS['TCA']['pages']['ctrl']['languageField'], |
||
709 | $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT) |
||
710 | ), |
||
711 | $queryBuilder->expr()->eq( |
||
712 | 't3ver_wsid', |
||
713 | $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, \PDO::PARAM_INT) |
||
714 | ) |
||
715 | ) |
||
716 | ->setMaxResults(1) |
||
717 | ->execute() |
||
718 | ->fetch(); |
||
719 | |||
720 | if ($row !== false) { |
||
721 | $isNewPage = VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER); |
||
722 | } |
||
723 | } else { |
||
724 | $rec = BackendUtility::getRecord('pages', $id, 't3ver_state'); |
||
725 | if (is_array($rec)) { |
||
726 | $isNewPage = VersionState::cast($rec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER); |
||
727 | } |
||
728 | } |
||
729 | return $isNewPage; |
||
730 | } |
||
731 | |||
732 | /** |
||
733 | * Generates a view link for a page. |
||
734 | * |
||
735 | * @param string $table Table to be used |
||
736 | * @param int $uid Uid of the version(!) record |
||
737 | * @param array $liveRecord Optional live record data |
||
738 | * @param array $versionRecord Optional version record data |
||
739 | * @return string |
||
740 | */ |
||
741 | public static function viewSingleRecord($table, $uid, array $liveRecord = null, array $versionRecord = null) |
||
742 | { |
||
743 | if ($table === 'pages') { |
||
744 | return BackendUtility::viewOnClick(BackendUtility::getLiveVersionIdOfRecord('pages', $uid)); |
||
745 | } |
||
746 | |||
747 | if ($liveRecord === null) { |
||
748 | $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $uid); |
||
749 | } |
||
750 | if ($versionRecord === null) { |
||
751 | $versionRecord = BackendUtility::getRecord($table, $uid); |
||
752 | } |
||
753 | if (VersionState::cast($versionRecord['t3ver_state'])->equals(VersionState::MOVE_POINTER)) { |
||
754 | $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRecord['uid'], 'pid'); |
||
755 | } |
||
756 | |||
757 | // Directly use pid value and consider move placeholders |
||
758 | $previewPageId = (empty($movePlaceholder['pid']) ? $liveRecord['pid'] : $movePlaceholder['pid']); |
||
759 | $additionalParameters = '&tx_workspaces_web_workspacesworkspaces[previewWS]=' . $versionRecord['t3ver_wsid']; |
||
760 | // Add language parameter if record is a localization |
||
761 | if (BackendUtility::isTableLocalizable($table)) { |
||
762 | $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField']; |
||
763 | if ($versionRecord[$languageField] > 0) { |
||
764 | $additionalParameters .= '&L=' . $versionRecord[$languageField]; |
||
765 | } |
||
766 | } |
||
767 | |||
768 | $pageTsConfig = BackendUtility::getPagesTSconfig($previewPageId); |
||
769 | $viewUrl = ''; |
||
770 | |||
771 | // Directly use determined direct page id |
||
772 | if ($table === 'tt_content') { |
||
773 | $viewUrl = BackendUtility::viewOnClick($previewPageId, '', null, '', '', $additionalParameters); |
||
774 | } elseif (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table]) || !empty($pageTsConfig['options.']['workspaces.']['previewPageId'])) { |
||
775 | // Analyze Page TSconfig options.workspaces.previewPageId |
||
776 | if (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table])) { |
||
777 | $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId.'][$table]; |
||
778 | } else { |
||
779 | $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId']; |
||
780 | } |
||
781 | // Extract possible settings (e.g. "field:pid") |
||
782 | list($previewKey, $previewValue) = explode(':', $previewConfiguration, 2); |
||
783 | if ($previewKey === 'field') { |
||
784 | $previewPageId = (int)$liveRecord[$previewValue]; |
||
785 | } else { |
||
786 | $previewPageId = (int)$previewConfiguration; |
||
787 | } |
||
788 | $viewUrl = BackendUtility::viewOnClick($previewPageId, '', null, '', '', $additionalParameters); |
||
789 | } elseif (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord'])) { |
||
790 | // Call user function to render the single record view |
||
791 | $_params = [ |
||
792 | 'table' => $table, |
||
793 | 'uid' => $uid, |
||
794 | 'record' => $liveRecord, |
||
795 | 'liveRecord' => $liveRecord, |
||
796 | 'versionRecord' => $versionRecord, |
||
797 | ]; |
||
798 | $_funcRef = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['viewSingleRecord']; |
||
799 | $null = null; |
||
800 | $viewUrl = GeneralUtility::callUserFunction($_funcRef, $_params, $null); |
||
801 | } |
||
802 | |||
803 | return $viewUrl; |
||
804 | } |
||
805 | |||
806 | /** |
||
807 | * Determine whether this page for the current |
||
808 | * |
||
809 | * @param int $pageUid |
||
810 | * @param int $workspaceUid |
||
811 | * @return bool |
||
812 | */ |
||
813 | public function canCreatePreviewLink($pageUid, $workspaceUid) |
||
814 | { |
||
815 | $result = true; |
||
816 | if ($pageUid > 0 && $workspaceUid > 0) { |
||
817 | $pageRecord = BackendUtility::getRecord('pages', $pageUid); |
||
818 | BackendUtility::workspaceOL('pages', $pageRecord, $workspaceUid); |
||
819 | if (VersionState::cast($pageRecord['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { |
||
820 | $result = false; |
||
821 | } |
||
822 | } else { |
||
823 | $result = false; |
||
824 | } |
||
825 | return $result; |
||
826 | } |
||
827 | |||
828 | /** |
||
829 | * Generates a workspace preview link. |
||
830 | * |
||
831 | * @param int $uid The ID of the record to be linked |
||
832 | * @return string the full domain including the protocol http:// or https://, but without the trailing '/' |
||
833 | */ |
||
834 | public function generateWorkspacePreviewLink($uid) |
||
835 | { |
||
836 | $previewObject = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Hook\PreviewHook::class); |
||
837 | $timeToLiveHours = $previewObject->getPreviewLinkLifetime(); |
||
838 | $previewKeyword = $previewObject->compilePreviewKeyword('', $GLOBALS['BE_USER']->user['uid'], $timeToLiveHours * 3600, $this->getCurrentWorkspace()); |
||
839 | $linkParams = [ |
||
840 | 'ADMCMD_prev' => $previewKeyword, |
||
841 | 'id' => $uid |
||
842 | ]; |
||
843 | return BackendUtility::getViewDomain($uid) . '/index.php?' . GeneralUtility::implodeArrayForUrl('', $linkParams); |
||
844 | } |
||
845 | |||
846 | /** |
||
847 | * Generates a workspace splitted preview link. |
||
848 | * |
||
849 | * @param int $uid The ID of the record to be linked |
||
850 | * @param bool $addDomain Parameter to decide if domain should be added to the generated link, FALSE per default |
||
851 | * @return string the preview link without the trailing '/' |
||
852 | */ |
||
853 | public function generateWorkspaceSplittedPreviewLink($uid, $addDomain = false) |
||
854 | { |
||
855 | // In case a $pageUid is submitted we need to make sure it points to a live-page |
||
856 | if ($uid > 0) { |
||
857 | $uid = $this->getLivePageUid($uid); |
||
858 | } |
||
859 | /** @var $uriBuilder \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder */ |
||
860 | $uriBuilder = $this->getObjectManager()->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class); |
||
861 | $redirect = 'index.php?redirect_url='; |
||
862 | $viewScript = $uriBuilder |
||
863 | ->setArguments(['route' => '/web/WorkspacesWorkspaces/']) |
||
864 | ->uriFor('index', [], 'Preview', 'workspaces', 'web_workspacesworkspaces') . '&id='; |
||
865 | if ($addDomain === true) { |
||
866 | return BackendUtility::getViewDomain($uid) . $redirect . urlencode($viewScript) . $uid; |
||
867 | } |
||
868 | return $viewScript; |
||
869 | } |
||
870 | |||
871 | /** |
||
872 | * Generate workspace preview links for all available languages of a page |
||
873 | * |
||
874 | * @param int $uid |
||
875 | * @return array |
||
876 | */ |
||
877 | public function generateWorkspacePreviewLinksForAllLanguages($uid) |
||
878 | { |
||
879 | $previewUrl = $this->generateWorkspacePreviewLink($uid); |
||
880 | $previewLanguages = $this->getAvailableLanguages($uid); |
||
881 | $previewLinks = []; |
||
882 | |||
883 | foreach ($previewLanguages as $languageUid => $language) { |
||
884 | $previewLinks[$language] = $previewUrl . '&L=' . $languageUid; |
||
885 | } |
||
886 | |||
887 | return $previewLinks; |
||
888 | } |
||
889 | |||
890 | /** |
||
891 | * Find the Live-Uid for a given page, |
||
892 | * the results are cached at run-time to avoid too many database-queries |
||
893 | * |
||
894 | * @throws \InvalidArgumentException |
||
895 | * @param int $uid |
||
896 | * @return int |
||
897 | */ |
||
898 | public function getLivePageUid($uid) |
||
899 | { |
||
900 | if (!isset($this->pageCache[$uid])) { |
||
901 | $pageRecord = BackendUtility::getRecord('pages', $uid); |
||
902 | if (is_array($pageRecord)) { |
||
903 | $this->pageCache[$uid] = $pageRecord['t3ver_oid'] ? $pageRecord['t3ver_oid'] : $uid; |
||
904 | } else { |
||
905 | throw new \InvalidArgumentException('uid is supposed to point to an existing page - given value was: ' . $uid, 1290628113); |
||
906 | } |
||
907 | } |
||
908 | return $this->pageCache[$uid]; |
||
909 | } |
||
910 | |||
911 | /** |
||
912 | * Determines whether a page has workspace versions. |
||
913 | * |
||
914 | * @param int $workspaceId |
||
915 | * @param int $pageId |
||
916 | * @return bool |
||
917 | */ |
||
918 | public function hasPageRecordVersions($workspaceId, $pageId) |
||
919 | { |
||
920 | if ((int)$workspaceId === 0 || (int)$pageId === 0) { |
||
921 | return false; |
||
922 | } |
||
923 | |||
924 | if (isset($this->versionsOnPageCache[$workspaceId][$pageId])) { |
||
925 | return $this->versionsOnPageCache[$workspaceId][$pageId]; |
||
926 | } |
||
927 | |||
928 | $this->versionsOnPageCache[$workspaceId][$pageId] = false; |
||
929 | |||
930 | foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) { |
||
931 | if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) { |
||
932 | continue; |
||
933 | } |
||
934 | |||
935 | $pages = $this->fetchPagesWithVersionsInTable($workspaceId, $tableName); |
||
936 | // Early break on first match |
||
937 | if (!empty($pages[(string)$pageId])) { |
||
938 | $this->versionsOnPageCache[$workspaceId][$pageId] = true; |
||
939 | break; |
||
940 | } |
||
941 | } |
||
942 | |||
943 | $parameters = [ |
||
944 | 'workspaceId' => $workspaceId, |
||
945 | 'pageId' => $pageId, |
||
946 | 'versionsOnPageCache' => &$this->versionsOnPageCache, |
||
947 | ]; |
||
948 | foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\\CMS\\Workspaces\\Service\\WorkspaceService']['hasPageRecordVersions'] ?? [] as $hookFunction) { |
||
949 | GeneralUtility::callUserFunction($hookFunction, $parameters, $this); |
||
950 | } |
||
951 | |||
952 | return $this->versionsOnPageCache[$workspaceId][$pageId]; |
||
953 | } |
||
954 | |||
955 | /** |
||
956 | * Gets all pages that have workspace versions per table. |
||
957 | * |
||
958 | * Result: |
||
959 | * [ |
||
960 | * 'sys_template' => [], |
||
961 | * 'tt_content' => [ |
||
962 | * 1 => true, |
||
963 | * 11 => true, |
||
964 | * 13 => true, |
||
965 | * 15 => true |
||
966 | * ], |
||
967 | * 'tx_something => [ |
||
968 | * 15 => true, |
||
969 | * 11 => true, |
||
970 | * 21 => true |
||
971 | * ], |
||
972 | * ] |
||
973 | * |
||
974 | * @param int $workspaceId |
||
975 | * |
||
976 | * @return array |
||
977 | */ |
||
978 | public function getPagesWithVersionsInTable($workspaceId) |
||
979 | { |
||
980 | foreach ($GLOBALS['TCA'] as $tableName => $tableConfiguration) { |
||
981 | if ($tableName === 'pages' || empty($tableConfiguration['ctrl']['versioningWS'])) { |
||
982 | continue; |
||
983 | } |
||
984 | |||
985 | $this->fetchPagesWithVersionsInTable($workspaceId, $tableName); |
||
986 | } |
||
987 | |||
988 | return $this->pagesWithVersionsInTable[$workspaceId]; |
||
989 | } |
||
990 | |||
991 | /** |
||
992 | * Gets all pages that have workspace versions in a particular table. |
||
993 | * |
||
994 | * Result: |
||
995 | * [ |
||
996 | * 1 => true, |
||
997 | * 11 => true, |
||
998 | * 13 => true, |
||
999 | * 15 => true |
||
1000 | * ], |
||
1001 | * |
||
1002 | * @param int $workspaceId |
||
1003 | * @param string $tableName |
||
1004 | * @return array |
||
1005 | */ |
||
1006 | protected function fetchPagesWithVersionsInTable($workspaceId, $tableName) |
||
1091 | } |
||
1092 | |||
1093 | /** |
||
1094 | * @param string $tableName |
||
1095 | * @return QueryBuilder |
||
1096 | */ |
||
1097 | protected function createQueryBuilderForTable(string $tableName) |
||
1105 | } |
||
1106 | |||
1107 | /** |
||
1108 | * @return \TYPO3\CMS\Extbase\Object\ObjectManager |
||
1109 | */ |
||
1110 | protected function getObjectManager() |
||
1111 | { |
||
1112 | return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class); |
||
1113 | } |
||
1114 | |||
1115 | /** |
||
1116 | * Get the available languages of a certain page |
||
1117 | * |
||
1118 | * @param int $pageId |
||
1119 | * @return array |
||
1120 | */ |
||
1121 | public function getAvailableLanguages($pageId) |
||
1159 | } |
||
1160 | } |
||
1161 |