1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of the eZ Publish Kernel package. |
4
|
|
|
* |
5
|
|
|
* @copyright Copyright (C) eZ Systems AS. All rights reserved |
6
|
|
|
* @license For full copyright and license information view LICENSE file distributed with this source code |
7
|
|
|
*/ |
8
|
|
|
namespace eZ\Bundle\EzPublishCoreBundle\Command; |
9
|
|
|
|
10
|
|
|
use DateTime; |
11
|
|
|
use DateTimeZone; |
12
|
|
|
use Doctrine\DBAL\Connection; |
13
|
|
|
use RuntimeException; |
14
|
|
|
use Symfony\Component\Process\Process; |
15
|
|
|
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; |
16
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
17
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
18
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
19
|
|
|
use Symfony\Component\Console\Input\InputOption; |
20
|
|
|
use Symfony\Component\Console\Helper\ProgressBar; |
21
|
|
|
use Symfony\Component\Console\Question\ConfirmationQuestion; |
22
|
|
|
use Symfony\Component\Process\PhpExecutableFinder; |
23
|
|
|
use PDO; |
24
|
|
|
|
25
|
|
|
class UpdateTimestampsToUTCCommand extends ContainerAwareCommand |
26
|
|
|
{ |
27
|
|
|
const MAX_TIMESTAMP_VALUE = 2147483647; |
28
|
|
|
const DEFAULT_ITERATION_COUNT = 100; |
29
|
|
|
const MODES = [ |
30
|
|
|
'date' => ['ezdate'], |
31
|
|
|
'datetime' => ['ezdatetime'], |
32
|
|
|
'all' => ['ezdate', 'ezdatetime'], |
33
|
|
|
]; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var int |
37
|
|
|
*/ |
38
|
|
|
protected $done = 0; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var string |
42
|
|
|
*/ |
43
|
|
|
protected $timezone; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var string |
47
|
|
|
*/ |
48
|
|
|
private $mode; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var string |
52
|
|
|
*/ |
53
|
|
|
private $from; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
private $to; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var \Doctrine\DBAL\Connection |
62
|
|
|
*/ |
63
|
|
|
private $connection; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var string |
67
|
|
|
*/ |
68
|
|
|
private $phpPath; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var bool |
72
|
|
|
*/ |
73
|
|
|
private $dryRun; |
74
|
|
|
|
75
|
|
|
protected function configure() |
76
|
|
|
{ |
77
|
|
|
$this |
78
|
|
|
->setName('ezplatform:timestamps:to-utc') |
79
|
|
|
->setDescription('Updates ezdate & ezdatetime timestamps to UTC') |
80
|
|
|
->addArgument( |
81
|
|
|
'timezone', |
82
|
|
|
InputArgument::OPTIONAL, |
83
|
|
|
'Original timestamp\'s TimeZone', |
84
|
|
|
null |
85
|
|
|
) |
86
|
|
|
->addOption( |
87
|
|
|
'dry-run', |
88
|
|
|
null, |
89
|
|
|
InputOption::VALUE_NONE, |
90
|
|
|
'Execute a dry run' |
91
|
|
|
) |
92
|
|
|
->addOption( |
93
|
|
|
'mode', |
94
|
|
|
null, |
95
|
|
|
InputOption::VALUE_REQUIRED, |
96
|
|
|
'Select conversion scope: date, datetime, all', |
97
|
|
|
'all' |
98
|
|
|
) |
99
|
|
|
->addOption( |
100
|
|
|
'from', |
101
|
|
|
null, |
102
|
|
|
InputOption::VALUE_REQUIRED, |
103
|
|
|
'Only versions AFTER this date will be converted', |
104
|
|
|
null |
105
|
|
|
) |
106
|
|
|
->addOption( |
107
|
|
|
'to', |
108
|
|
|
null, |
109
|
|
|
InputOption::VALUE_REQUIRED, |
110
|
|
|
'Only versions BEFORE this date will be converted', |
111
|
|
|
null |
112
|
|
|
) |
113
|
|
|
->addOption( |
114
|
|
|
'offset', |
115
|
|
|
null, |
116
|
|
|
InputArgument::OPTIONAL, |
117
|
|
|
'Offset for updating records', |
118
|
|
|
0 |
119
|
|
|
) |
120
|
|
|
->addOption( |
121
|
|
|
'iteration-count', |
122
|
|
|
null, |
123
|
|
|
InputArgument::OPTIONAL, |
124
|
|
|
'Limit how much records get updated by single process', |
125
|
|
|
self::DEFAULT_ITERATION_COUNT |
126
|
|
|
) |
127
|
|
|
->setHelp( |
128
|
|
|
<<<'EOT' |
129
|
|
|
The command <info>%command.name%</info> updates field |
130
|
|
|
data_int in configured Legacy Storage database for a given field type. |
131
|
|
|
|
132
|
|
|
This is to be used either when migrating from eZ Publish to eZ Platform |
133
|
|
|
(when using platform backend instead of legacy), or when upgrading legacy |
134
|
|
|
to v2019.03 which has been adapted to use UTC. |
135
|
|
|
|
136
|
|
|
<warning>During the script execution the database should not be modified. |
137
|
|
|
|
138
|
|
|
You are advised to create a backup or execute a dry run before |
139
|
|
|
proceeding with actual update.</warning> |
140
|
|
|
|
141
|
|
|
<warning>This command should be only ran ONCE.</warning> |
142
|
|
|
|
143
|
|
|
Since this script can potentially run for a very long time, to avoid memory |
144
|
|
|
exhaustion run it in production environment using <info>--env=prod</info> switch. |
145
|
|
|
EOT |
146
|
|
|
); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
protected function initialize(InputInterface $input, OutputInterface $output) |
150
|
|
|
{ |
151
|
|
|
/* @var \Doctrine\DBAL\Connection $databaseHandler */ |
152
|
|
|
$this->connection = $this->getContainer()->get('ezpublish.api.search_engine.legacy.connection'); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @param \Symfony\Component\Console\Input\InputInterface $input |
157
|
|
|
* @param \Symfony\Component\Console\Output\OutputInterface $output |
158
|
|
|
*/ |
159
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
160
|
|
|
{ |
161
|
|
|
$iterationCount = (int) $input->getOption('iteration-count'); |
162
|
|
|
$this->dryRun = $input->getOption('dry-run'); |
|
|
|
|
163
|
|
|
$this->mode = $input->getOption('mode'); |
|
|
|
|
164
|
|
|
|
165
|
|
|
if (!isset(self::MODES[$this->mode])) { |
166
|
|
|
$output->writeln( |
167
|
|
|
sprintf('Selected mode is not supported, please use one of: %s', implode(', ', array_keys(self::MODES))) |
168
|
|
|
); |
169
|
|
|
|
170
|
|
|
return; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$from = $input->getOption('from'); |
174
|
|
|
$to = $input->getOption('to'); |
175
|
|
|
|
176
|
|
|
if ($from && !$this->validateDateTimeString($from, $output)) { |
177
|
|
|
return; |
178
|
|
|
} |
179
|
|
|
if ($to && !$this->validateDateTimeString($to, $output)) { |
180
|
|
|
return; |
181
|
|
|
} |
182
|
|
|
if ($from) { |
183
|
|
|
$this->from = $this->dateStringToTimestamp($from); |
|
|
|
|
184
|
|
|
} |
185
|
|
|
if ($to) { |
186
|
|
|
$this->to = $this->dateStringToTimestamp($to); |
|
|
|
|
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
$consoleScript = $_SERVER['argv'][0]; |
190
|
|
|
|
191
|
|
|
if (getenv('INNER_CALL')) { |
192
|
|
|
$this->timezone = $input->getArgument('timezone'); |
|
|
|
|
193
|
|
|
$this->processTimestamps((int) $input->getOption('offset'), $iterationCount, $output); |
194
|
|
|
$output->writeln($this->done); |
195
|
|
|
} else { |
196
|
|
|
$timezone = $input->getArgument('timezone'); |
197
|
|
|
$this->timezone = $this->validateTimezone($timezone, $output); |
|
|
|
|
198
|
|
|
|
199
|
|
|
$output->writeln([ |
200
|
|
|
sprintf('Converting timestamps for fields: %s', implode(', ', self::MODES[$this->mode])), |
201
|
|
|
'Calculating number of Field values to update...', |
202
|
|
|
]); |
203
|
|
|
$count = $this->countTimestampBasedFields(); |
204
|
|
|
$output->writeln([ |
205
|
|
|
sprintf('Found total of Field values for update: %d', $count), |
206
|
|
|
'', |
207
|
|
|
]); |
208
|
|
|
|
209
|
|
|
if ($count == 0) { |
210
|
|
|
$output->writeln('Nothing to process, exiting.'); |
211
|
|
|
|
212
|
|
|
return; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
$helper = $this->getHelper('question'); |
216
|
|
|
$question = new ConfirmationQuestion( |
217
|
|
|
'<question>Are you sure you want to proceed?</question> ', |
218
|
|
|
false |
219
|
|
|
); |
220
|
|
|
|
221
|
|
|
if (!$helper->ask($input, $output, $question)) { |
222
|
|
|
$output->writeln(''); |
223
|
|
|
|
224
|
|
|
return; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
$progressBar = $this->getProgressBar($count, $output); |
228
|
|
|
$progressBar->start(); |
229
|
|
|
|
230
|
|
|
for ($offset = 0; $offset < $count; $offset += $iterationCount) { |
231
|
|
|
$processScriptFragments = [ |
232
|
|
|
$this->getPhpPath(), |
233
|
|
|
$consoleScript, |
234
|
|
|
$this->getName(), |
235
|
|
|
$this->timezone, |
236
|
|
|
'--mode=' . $this->mode, |
237
|
|
|
'--offset=' . $offset, |
238
|
|
|
'--iteration-count=' . $iterationCount, |
239
|
|
|
]; |
240
|
|
|
|
241
|
|
|
if ($from) { |
242
|
|
|
$processScriptFragments[] = '--from=' . $from; |
243
|
|
|
} |
244
|
|
|
if ($to) { |
245
|
|
|
$processScriptFragments[] = '--to=' . $to; |
246
|
|
|
} |
247
|
|
|
if ($this->dryRun) { |
248
|
|
|
$processScriptFragments[] = '--dry-run'; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
$process = new Process( |
252
|
|
|
implode(' ', $processScriptFragments) |
253
|
|
|
); |
254
|
|
|
|
255
|
|
|
$process->setEnv(['INNER_CALL' => 1]); |
256
|
|
|
$process->run(); |
257
|
|
|
|
258
|
|
|
if (!$process->isSuccessful()) { |
259
|
|
|
throw new RuntimeException($process->getErrorOutput()); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$doneInProcess = (int) $process->getOutput(); |
263
|
|
|
$this->done += $doneInProcess; |
264
|
|
|
|
265
|
|
|
$progressBar->advance($doneInProcess); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
$progressBar->finish(); |
269
|
|
|
$output->writeln([ |
270
|
|
|
'', |
271
|
|
|
sprintf('Done: %d', $this->done), |
272
|
|
|
]); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* @param int $offset |
278
|
|
|
* @param int $limit |
279
|
|
|
* @param \Symfony\Component\Console\Output\OutputInterface $output |
280
|
|
|
*/ |
281
|
|
|
protected function processTimestamps($offset, $limit, $output) |
|
|
|
|
282
|
|
|
{ |
283
|
|
|
$timestampBasedFields = $this->getTimestampBasedFields($offset, $limit); |
284
|
|
|
|
285
|
|
|
$dateTimeInUTC = new DateTime(); |
286
|
|
|
$dateTimeInUTC->setTimezone(new DateTimeZone('UTC')); |
287
|
|
|
|
288
|
|
|
foreach ($timestampBasedFields as $timestampBasedField) { |
289
|
|
|
$timestamp = $timestampBasedField['data_int']; |
290
|
|
|
$dateTimeInUTC->setTimestamp($timestamp); |
291
|
|
|
$newTimestamp = $this->convertToUtcTimestamp($timestamp); |
292
|
|
|
|
293
|
|
|
//failsafe for int field limitation (dates/datetimes after 01/19/2038 @ 4:14am (UTC)) |
294
|
|
|
if ($newTimestamp <= self::MAX_TIMESTAMP_VALUE && !$this->dryRun) { |
295
|
|
|
$this->updateTimestampToUTC($timestampBasedField['id'], $newTimestamp); |
296
|
|
|
} |
297
|
|
|
++$this->done; |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @param int $offset |
303
|
|
|
* @param int $limit |
304
|
|
|
* |
305
|
|
|
* @return array |
306
|
|
|
*/ |
307
|
|
|
protected function getTimestampBasedFields($offset, $limit) |
308
|
|
|
{ |
309
|
|
|
$query = $this->connection->createQueryBuilder(); |
310
|
|
|
$query |
311
|
|
|
->select('a.id, a.data_int') |
312
|
|
|
->from('ezcontentobject_attribute', 'a') |
313
|
|
|
->join('a', 'ezcontentobject_version', 'v', 'a.contentobject_id = v.contentobject_id') |
314
|
|
|
->where( |
315
|
|
|
$query->expr()->in( |
316
|
|
|
'a.data_type_string', |
317
|
|
|
$query->createNamedParameter(self::MODES[$this->mode], Connection::PARAM_STR_ARRAY) |
318
|
|
|
) |
319
|
|
|
) |
320
|
|
|
->andWhere('a.data_int is not null') |
321
|
|
|
->andWhere('a.data_int > 0') |
322
|
|
|
->andWhere('v.version = a.version') |
323
|
|
|
->setFirstResult($offset) |
324
|
|
|
->setMaxResults($limit); |
325
|
|
|
|
326
|
|
|
if ($this->from) { |
327
|
|
|
$query |
328
|
|
|
->andWhere('v.modified >= :fromTimestamp') |
329
|
|
|
->setParameter('fromTimestamp', $this->from); |
330
|
|
|
} |
331
|
|
|
if ($this->to) { |
332
|
|
|
$query |
333
|
|
|
->andWhere('v.modified <= :toTimestamp') |
334
|
|
|
->setParameter('toTimestamp', $this->to); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
$statement = $query->execute(); |
338
|
|
|
|
339
|
|
|
return $statement->fetchAll(PDO::FETCH_ASSOC); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* Counts affected timestamp based fields using captured "mode", "from" and "to" command options. |
344
|
|
|
* |
345
|
|
|
* @return int |
346
|
|
|
*/ |
347
|
|
|
protected function countTimestampBasedFields() |
348
|
|
|
{ |
349
|
|
|
$query = $this->connection->createQueryBuilder(); |
350
|
|
|
$query |
351
|
|
|
->select('count(*) as count') |
352
|
|
|
->from('ezcontentobject_attribute', 'a') |
353
|
|
|
->join('a', 'ezcontentobject_version', 'v', 'a.contentobject_id = v.contentobject_id') |
354
|
|
|
->where( |
355
|
|
|
$query->expr()->in( |
356
|
|
|
'a.data_type_string', |
357
|
|
|
$query->createNamedParameter(self::MODES[$this->mode], Connection::PARAM_STR_ARRAY) |
358
|
|
|
) |
359
|
|
|
) |
360
|
|
|
->andWhere('a.data_int is not null') |
361
|
|
|
->andWhere('a.data_int > 0') |
362
|
|
|
->andWhere('v.version = a.version'); |
363
|
|
|
|
364
|
|
|
if ($this->from) { |
365
|
|
|
$query |
366
|
|
|
->andWhere('v.modified >= :fromTimestamp') |
367
|
|
|
->setParameter('fromTimestamp', $this->from); |
368
|
|
|
} |
369
|
|
|
if ($this->to) { |
370
|
|
|
$query |
371
|
|
|
->andWhere('v.modified <= :toTimestamp') |
372
|
|
|
->setParameter('toTimestamp', $this->to); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
$statement = $query->execute(); |
376
|
|
|
|
377
|
|
|
return (int) $statement->fetchColumn(); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* @param int $timestamp |
382
|
|
|
* @return int |
383
|
|
|
*/ |
384
|
|
|
protected function convertToUtcTimestamp($timestamp) |
385
|
|
|
{ |
386
|
|
|
$dateTimeZone = new DateTimeZone($this->timezone); |
387
|
|
|
$dateTimeZoneUTC = new DateTimeZone('UTC'); |
388
|
|
|
|
389
|
|
|
$dateTime = new DateTime(null, $dateTimeZone); |
390
|
|
|
$dateTime->setTimestamp($timestamp); |
391
|
|
|
$dateTimeUTC = new DateTime($dateTime->format('Y-m-d H:i:s'), $dateTimeZoneUTC); |
392
|
|
|
|
393
|
|
|
return $dateTimeUTC->getTimestamp(); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* @param string $dateTimeString |
398
|
|
|
* @param OutputInterface $output |
399
|
|
|
* @return bool |
400
|
|
|
*/ |
401
|
|
|
protected function validateDateTimeString($dateTimeString, OutputInterface $output) |
402
|
|
|
{ |
403
|
|
|
try { |
404
|
|
|
new \DateTime($dateTimeString); |
405
|
|
|
} catch (\Exception $exception) { |
406
|
|
|
$output->writeln('The --from and --to options must be a valid Date string.'); |
407
|
|
|
|
408
|
|
|
return false; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
return true; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* @param string $timezone |
416
|
|
|
* @param OutputInterface $output |
417
|
|
|
* @return string |
418
|
|
|
*/ |
419
|
|
|
protected function validateTimezone($timezone, OutputInterface $output) |
420
|
|
|
{ |
421
|
|
|
if (!$timezone) { |
422
|
|
|
$timezone = date_default_timezone_get(); |
423
|
|
|
$output->writeln([ |
424
|
|
|
sprintf('No Timezone set, using server Timezone: %s', $timezone), |
425
|
|
|
'', |
426
|
|
|
]); |
427
|
|
|
} else { |
428
|
|
|
if (!\in_array($timezone, timezone_identifiers_list())) { |
429
|
|
|
$output->writeln([ |
430
|
|
|
sprintf('% is not correct Timezone.', $timezone), |
431
|
|
|
'', |
432
|
|
|
]); |
433
|
|
|
|
434
|
|
|
return; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
$output->writeln([ |
438
|
|
|
sprintf('Using timezone: %s', $timezone), |
439
|
|
|
'', |
440
|
|
|
]); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
return $timezone; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* Return configured progress bar helper. |
448
|
|
|
* |
449
|
|
|
* @param int $maxSteps |
450
|
|
|
* @param \Symfony\Component\Console\Output\OutputInterface $output |
451
|
|
|
* |
452
|
|
|
* @return \Symfony\Component\Console\Helper\ProgressBar |
453
|
|
|
*/ |
454
|
|
|
protected function getProgressBar($maxSteps, OutputInterface $output) |
455
|
|
|
{ |
456
|
|
|
$progressBar = new ProgressBar($output, $maxSteps); |
457
|
|
|
$progressBar->setFormat( |
458
|
|
|
' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%' |
459
|
|
|
); |
460
|
|
|
|
461
|
|
|
return $progressBar; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* @param int $contentAttributeId |
466
|
|
|
* @param int $newTimestamp |
467
|
|
|
*/ |
468
|
|
|
protected function updateTimestampToUTC( |
469
|
|
|
$contentAttributeId, |
470
|
|
|
$newTimestamp |
471
|
|
|
) { |
472
|
|
|
$query = $this->connection->createQueryBuilder(); |
473
|
|
|
$query |
474
|
|
|
->update('ezcontentobject_attribute', 'a') |
475
|
|
|
->set('a.data_int', $newTimestamp) |
476
|
|
|
->set('a.sort_key_int', $newTimestamp) |
477
|
|
|
->where('a.id = :id') |
478
|
|
|
->setParameter(':id', $contentAttributeId); |
479
|
|
|
|
480
|
|
|
$query->execute(); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* @return string |
485
|
|
|
*/ |
486
|
|
View Code Duplication |
private function getPhpPath() |
487
|
|
|
{ |
488
|
|
|
if ($this->phpPath) { |
489
|
|
|
return $this->phpPath; |
490
|
|
|
} |
491
|
|
|
$phpFinder = new PhpExecutableFinder(); |
492
|
|
|
$this->phpPath = $phpFinder->find(); |
|
|
|
|
493
|
|
|
if (!$this->phpPath) { |
|
|
|
|
494
|
|
|
throw new RuntimeException( |
495
|
|
|
'The php executable could not be found, it\'s needed for executing parable sub processes, so add it to your PATH environment variable and try again' |
496
|
|
|
); |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
return $this->phpPath; |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
/** |
503
|
|
|
* @param $dateString string |
504
|
|
|
* @throws \Exception |
505
|
|
|
* @return int |
506
|
|
|
*/ |
507
|
|
|
private function dateStringToTimestamp($dateString) |
508
|
|
|
{ |
509
|
|
|
$date = new \DateTime($dateString); |
510
|
|
|
|
511
|
|
|
return $date->getTimestamp(); |
512
|
|
|
} |
513
|
|
|
} |
514
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.