1+ <?php
2+ namespace Cobweb \Linkhandler \Command ;
3+
4+ /*
5+ * This file is part of the TYPO3 CMS project.
6+ *
7+ * It is free software; you can redistribute it and/or modify it under
8+ * the terms of the GNU General Public License, either version 2
9+ * of the License, or any later version.
10+ *
11+ * For the full copyright and license information, please read the
12+ * LICENSE.txt file that was distributed with this source code.
13+ *
14+ * The TYPO3 project - inspiring people to share!
15+ */
16+
17+ use Cobweb \Linkhandler \Domain \Repository \GenericRepository ;
18+ use Cobweb \Linkhandler \Exception \FailedQueryException ;
19+ use TYPO3 \CMS \Core \Utility \GeneralUtility ;
20+ use TYPO3 \CMS \Extbase \Mvc \Cli \ConsoleOutput ;
21+ use TYPO3 \CMS \Extbase \Mvc \Controller \CommandController ;
22+
23+ /**
24+ * Command-line controller for migrating old record links (with a syntax like record:table:id)
25+ * to the new syntax (record:key:table:id).
26+ *
27+ * @package TYPO3\CMS\Extbase\Command
28+ */
29+ class LinkMigrationCommandController extends CommandController {
30+ /**
31+ * Default list of fields where to search for record references to migrate
32+ */
33+ const DEFAULT_FIELDS = 'tt_content.header_link, tt_content.bodytext, sys_file_reference.link ' ;
34+
35+ /**
36+ * @var array List of fields to handle, grouped by table
37+ */
38+ protected $ tablesAndFields = array ();
39+
40+ /**
41+ * @var array List of tables being linked to in record links
42+ */
43+ protected $ tablesForMigration = array ();
44+
45+ /**
46+ * @var array Structured list of records that contain data to migrate
47+ */
48+ protected $ recordsForMigration = array ();
49+
50+ /**
51+ * @var GenericRepository
52+ */
53+ protected $ genericRepository ;
54+
55+ /**
56+ * @var ConsoleOutput
57+ */
58+ protected $ console ;
59+
60+ /**
61+ * @param GenericRepository $repository
62+ * @return void
63+ */
64+ public function injectGenericRepository (GenericRepository $ repository )
65+ {
66+ $ this ->genericRepository = $ repository ;
67+ }
68+
69+ /**
70+ * @param ConsoleOutput $consoleOutput
71+ * @return void
72+ */
73+ public function injectConsole (ConsoleOutput $ consoleOutput )
74+ {
75+ $ this ->console = $ consoleOutput ;
76+ }
77+
78+ /**
79+ * Migrates old-style records links (syntax: "record:table:id") to new-style record links (syntax: "record:key:table:id").
80+ *
81+ * @param string $fields Name of the field to migrate (syntax is "table.field"; use comma-separated values for several fields). Ignore to migrate default fields (tt_content.header_link, tt_content.bodytext, sys_file_reference.link)
82+ */
83+ public function migrateCommand ($ fields = '' )
84+ {
85+ // Set default value if argument is empty
86+ if ($ fields === '' ) {
87+ $ fields = self ::DEFAULT_FIELDS ;
88+ }
89+ try {
90+ $ this ->setFields ($ fields );
91+ // Loop on all tables and fields
92+ foreach ($ this ->tablesAndFields as $ table => $ listOfFields ) {
93+ $ this ->gatherRecordsToMigrate (
94+ $ table ,
95+ $ listOfFields
96+ );
97+ }
98+ // Ask the user for configuration key for each table
99+ $ this ->getConfigurationKeys ();
100+ // Replace in fields and save modified data
101+ $ this ->migrateRecords ();
102+ }
103+ catch (\InvalidArgumentException $ e ) {
104+ $ this ->outputLine (
105+ $ e ->getMessage () . ' ( ' . $ e ->getCode () . ') '
106+ );
107+ $ this ->quit (1 );
108+ }
109+ }
110+
111+ /**
112+ * Sets the internal list of fields to handle.
113+ *
114+ * @param string $fields Comma-separated list of fields (syntax table.field)
115+ * @throws \InvalidArgumentException
116+ * @return void
117+ */
118+ protected function setFields ($ fields )
119+ {
120+ $ listOfFields = GeneralUtility::trimExplode (', ' , $ fields , true );
121+ foreach ($ listOfFields as $ aField ) {
122+ list ($ table , $ field ) = explode ('. ' , $ aField );
123+ if (empty ($ table ) || empty ($ field )) {
124+ throw new \InvalidArgumentException (
125+ sprintf (
126+ 'Invalid argument "%s". Use "table.field" syntax ' ,
127+ $ aField
128+ ),
129+ 1457434202
130+ );
131+ } else {
132+ if (!array_key_exists ($ table , $ this ->tablesAndFields )) {
133+ $ this ->tablesAndFields [$ table ] = array ();
134+ }
135+ $ this ->tablesAndFields [$ table ][] = $ field ;
136+ }
137+ }
138+ }
139+
140+ /**
141+ * Gathers all records that contain data to migrate.
142+ *
143+ * Also extracts a list of all the different tables being linked to.
144+ * This is used later to ask the user about a configuration key for each table.
145+ *
146+ * @param string $table Name of the table to check
147+ * @param array $listOfFields List of fields to check
148+ * @return void
149+ */
150+ protected function gatherRecordsToMigrate ($ table , $ listOfFields )
151+ {
152+ try {
153+ $ records = $ this ->genericRepository ->findByRecordLink (
154+ $ table ,
155+ $ listOfFields
156+ );
157+ foreach ($ records as $ record ) {
158+ $ id = (int )$ record ['uid ' ];
159+ foreach ($ listOfFields as $ field ) {
160+ $ matches = array ();
161+ // Find all element that have a syntax like "record:string:string(:string)"
162+ // The last string is optional. If it exists, it is already a 4-part record reference,
163+ // i.e. a reference using the new syntax and which does not need to be migrated.
164+ preg_match_all (
165+ '/record:(\w+):(\w+)(:\w+)?/ ' ,
166+ $ record [$ field ],
167+ $ matches
168+ );
169+ foreach ($ matches as $ index => $ match ) {
170+ // Consider only matches that have 3 parts (i.e. 4th part is empty)
171+ // NOTE: although not captured, the first part is "record:"
172+ if ($ matches [3 ][$ index ] === '' ) {
173+ $ linkedTable = $ matches [1 ][$ index ];
174+ // First, add the table to the list of table that are targeted by record links
175+ if (!array_key_exists ($ linkedTable , $ this ->tablesForMigration )) {
176+ $ this ->tablesForMigration [$ linkedTable ] = '' ;
177+ }
178+ // Next keep track of the record that needs migration
179+ // Data is stored per table, per record, per field and per record link to migrate
180+ if (!array_key_exists ($ table , $ this ->recordsForMigration )) {
181+ $ this ->recordsForMigration [$ table ] = array ();
182+ }
183+ if (!array_key_exists ($ id , $ this ->recordsForMigration [$ table ])) {
184+ $ this ->recordsForMigration [$ table ][$ id ] = array ();
185+ }
186+ if (!array_key_exists ($ field , $ this ->recordsForMigration [$ table ][$ id ])) {
187+ $ this ->recordsForMigration [$ table ][$ id ][$ field ] = array (
188+ 'content ' => $ record [$ field ],
189+ 'matches ' => array ()
190+ );
191+ }
192+ $ this ->recordsForMigration [$ table ][$ id ][$ field ]['matches ' ][] = $ matches [0 ][$ index ];
193+ }
194+ }
195+ }
196+ }
197+ }
198+ catch (FailedQueryException $ e ) {
199+ $ this ->outputLine (
200+ sprintf (
201+ 'Table "%s" skipped. An error occurred: %s ' ,
202+ $ table ,
203+ $ e ->getMessage ()
204+ )
205+ );
206+ }
207+ }
208+
209+ /**
210+ * Asks the user to give a configuration key for each table that is being linked to.
211+ *
212+ * @return void
213+ */
214+ protected function getConfigurationKeys () {
215+ foreach ($ this ->tablesForMigration as $ table => &$ dummy ) {
216+ $ key = null ;
217+ do {
218+ try {
219+ $ key = $ this ->console ->ask (
220+ sprintf (
221+ 'Please enter the configuration key to use for table "%s": ' ,
222+ $ table
223+ )
224+ );
225+ }
226+ catch (\Exception $ e ) {
227+ // Do nothing, just let it try again
228+ }
229+ } while ($ key === null );
230+ $ dummy = $ key ;
231+ }
232+ }
233+
234+ /**
235+ * Updates all fields that needed some migration and saves the modified data.
236+ *
237+ * @return void
238+ */
239+ protected function migrateRecords ()
240+ {
241+ foreach ($ this ->recordsForMigration as $ table => $ records ) {
242+ $ recordsForTable = array ();
243+ foreach ($ records as $ id => $ fields ) {
244+ foreach ($ fields as $ field => $ fieldInformation ) {
245+ $ updatedField = $ fieldInformation ['content ' ];
246+ foreach ($ fieldInformation ['matches ' ] as $ link ) {
247+ $ linkParts = explode (': ' , $ link );
248+ $ newLink = 'record: ' . $ this ->tablesForMigration [$ linkParts [1 ]] . ': ' . $ linkParts [1 ] . ': ' . $ linkParts [2 ];
249+ $ updatedField = str_replace (
250+ $ link ,
251+ $ newLink ,
252+ $ updatedField
253+ );
254+ }
255+ if (!array_key_exists ($ id , $ recordsForTable )) {
256+ $ recordsForTable [$ id ] = array ();
257+ }
258+ $ recordsForTable [$ id ][$ field ] = $ updatedField ;
259+ }
260+ }
261+ $ result = $ this ->genericRepository ->massUpdate (
262+ $ table ,
263+ $ recordsForTable
264+ );
265+ if (!$ result ) {
266+ $ this ->outputLine (
267+ 'Some database updates failed for table "%s" ' ,
268+ $ table
269+ );
270+ }
271+ }
272+ }
273+ }
0 commit comments