|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* @copyright Copyright (C) eZ Systems AS. All rights reserved. |
|
5
|
|
|
* @license For full copyright and license information view LICENSE file distributed with this source code. |
|
6
|
|
|
*/ |
|
7
|
|
|
namespace eZ\Bundle\EzPublishCoreBundle\Command; |
|
8
|
|
|
|
|
9
|
|
|
use Doctrine\DBAL\Connection; |
|
10
|
|
|
use Exception; |
|
11
|
|
|
use eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider; |
|
12
|
|
|
use eZ\Publish\API\Repository\Repository; |
|
13
|
|
|
use eZ\Publish\API\Repository\Values\Content\VersionInfo; |
|
14
|
|
|
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException; |
|
15
|
|
|
use PDO; |
|
16
|
|
|
use Symfony\Component\Console\Command\Command; |
|
17
|
|
|
use Symfony\Component\Console\Helper\ProgressBar; |
|
18
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
|
19
|
|
|
use Symfony\Component\Console\Input\InputOption; |
|
20
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
|
21
|
|
|
|
|
22
|
|
|
class CleanupVersionsCommand extends Command |
|
23
|
|
|
{ |
|
24
|
|
|
const DEFAULT_REPOSITORY_USER = 'admin'; |
|
25
|
|
|
const DEFAULT_EXCLUDED_CONTENT_TYPES = 'user'; |
|
26
|
|
|
|
|
27
|
|
|
const BEFORE_RUNNING_HINTS = <<<EOT |
|
28
|
|
|
<error>Before you continue:</error> |
|
29
|
|
|
- Make sure to back up your database. |
|
30
|
|
|
- Take the installation offline. The database should not be modified while the script is being executed. |
|
31
|
|
|
- Run this command without memory limit. |
|
32
|
|
|
- Run this command in production environment using <info>--env=prod</info> |
|
33
|
|
|
EOT; |
|
34
|
|
|
|
|
35
|
|
|
const VERSION_DRAFT = 'draft'; |
|
36
|
|
|
const VERSION_ARCHIVED = 'archived'; |
|
37
|
|
|
const VERSION_PUBLISHED = 'published'; |
|
38
|
|
|
const VERSION_ALL = 'all'; |
|
39
|
|
|
|
|
40
|
|
|
const VERSION_STATUS = [ |
|
41
|
|
|
self::VERSION_DRAFT => VersionInfo::STATUS_DRAFT, |
|
42
|
|
|
self::VERSION_ARCHIVED => VersionInfo::STATUS_ARCHIVED, |
|
43
|
|
|
self::VERSION_PUBLISHED => VersionInfo::STATUS_PUBLISHED, |
|
44
|
|
|
]; |
|
45
|
|
|
|
|
46
|
|
|
/** @var \eZ\Publish\API\Repository\Repository */ |
|
47
|
|
|
private $repository; |
|
48
|
|
|
|
|
49
|
|
|
/** @var \eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider */ |
|
50
|
|
|
private $repositoryConfigurationProvider; |
|
51
|
|
|
|
|
52
|
|
|
/** @var \Doctrine\DBAL\Driver\Connection */ |
|
53
|
|
|
private $connection; |
|
54
|
|
|
|
|
55
|
|
|
public function __construct( |
|
56
|
|
|
Repository $repository, |
|
57
|
|
|
RepositoryConfigurationProvider $repositoryConfigurationProvider, |
|
58
|
|
|
Connection $connection |
|
59
|
|
|
) { |
|
60
|
|
|
$this->repository = $repository; |
|
61
|
|
|
$this->repositoryConfigurationProvider = $repositoryConfigurationProvider; |
|
62
|
|
|
$this->connection = $connection; |
|
63
|
|
|
|
|
64
|
|
|
parent::__construct(); |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
protected function configure() |
|
68
|
|
|
{ |
|
69
|
|
|
$beforeRunningHints = self::BEFORE_RUNNING_HINTS; |
|
70
|
|
|
$this |
|
71
|
|
|
->setName('ezplatform:content:cleanup-versions') |
|
72
|
|
|
->setDescription('Removes unwanted content versions. Keeps the published version untouched. By default, also keeps the last archived/draft version.') |
|
73
|
|
|
->addOption( |
|
74
|
|
|
'status', |
|
75
|
|
|
't', |
|
76
|
|
|
InputOption::VALUE_OPTIONAL, |
|
77
|
|
|
sprintf( |
|
78
|
|
|
"Select which version types should be removed: '%s', '%s', '%s'.", |
|
79
|
|
|
self::VERSION_DRAFT, |
|
80
|
|
|
self::VERSION_ARCHIVED, |
|
81
|
|
|
self::VERSION_ALL |
|
82
|
|
|
), |
|
83
|
|
|
self::VERSION_ALL |
|
84
|
|
|
) |
|
85
|
|
|
->addOption( |
|
86
|
|
|
'keep', |
|
87
|
|
|
'k', |
|
88
|
|
|
InputOption::VALUE_OPTIONAL, |
|
89
|
|
|
"Sets the number of the most recent versions (both drafts and archived) which won't be removed.", |
|
90
|
|
|
'config_default' |
|
91
|
|
|
) |
|
92
|
|
|
->addOption( |
|
93
|
|
|
'user', |
|
94
|
|
|
'u', |
|
95
|
|
|
InputOption::VALUE_OPTIONAL, |
|
96
|
|
|
'eZ Platform username (with Role containing at least content policies: remove, read, versionread)', |
|
97
|
|
|
self::DEFAULT_REPOSITORY_USER |
|
98
|
|
|
) |
|
99
|
|
|
->addOption( |
|
100
|
|
|
'excluded-content-types', |
|
101
|
|
|
null, |
|
102
|
|
|
InputOption::VALUE_OPTIONAL, |
|
103
|
|
|
'Comma-separated list of Content Type identifiers whose versions should not be removed, for instance `article`.', |
|
104
|
|
|
self::DEFAULT_EXCLUDED_CONTENT_TYPES |
|
105
|
|
|
)->setHelp( |
|
106
|
|
|
<<<EOT |
|
107
|
|
|
The command <info>%command.name%</info> reduces content versions to a minimum. |
|
108
|
|
|
It keeps published version untouched, and by default also keeps the last archived/draft version. |
|
109
|
|
|
Note: This script can potentially run for a very long time, and in Symfony dev environment it will consume memory exponentially with the size of the dataset. |
|
110
|
|
|
|
|
111
|
|
|
{$beforeRunningHints} |
|
112
|
|
|
EOT |
|
113
|
|
|
); |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int |
|
117
|
|
|
{ |
|
118
|
|
|
// We don't load repo services or config resolver before execute() to avoid loading before SiteAccess is set. |
|
119
|
|
|
$keep = $input->getOption('keep'); |
|
120
|
|
|
if ($keep === 'config_default') { |
|
121
|
|
|
$config = $this->repositoryConfigurationProvider->getRepositoryConfig(); |
|
122
|
|
|
$keep = $config['options']['default_version_archive_limit']; |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
if (($keep = (int) $keep) < 0) { |
|
126
|
|
|
throw new InvalidArgumentException( |
|
127
|
|
|
'keep', |
|
128
|
|
|
'Keep value cannot be negative.' |
|
129
|
|
|
); |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
$userService = $this->repository->getUserService(); |
|
133
|
|
|
$contentService = $this->repository->getContentService(); |
|
134
|
|
|
$permissionResolver = $this->repository->getPermissionResolver(); |
|
135
|
|
|
|
|
136
|
|
|
$permissionResolver->setCurrentUserReference( |
|
137
|
|
|
$userService->loadUserByLogin($input->getOption('user')) |
|
|
|
|
|
|
138
|
|
|
); |
|
139
|
|
|
|
|
140
|
|
|
$status = $input->getOption('status'); |
|
141
|
|
|
|
|
142
|
|
|
$excludedContentTypes = (string) $input->getOption('excluded-content-types'); |
|
143
|
|
|
if ($excludedContentTypes === '') { |
|
144
|
|
|
$excludedContentTypes = self::DEFAULT_EXCLUDED_CONTENT_TYPES; |
|
145
|
|
|
} |
|
146
|
|
|
$excludedContentTypeIdentifiers = explode(',', $excludedContentTypes); |
|
147
|
|
|
$contentIds = $this->getObjectsIds($keep, $status, $excludedContentTypeIdentifiers); |
|
148
|
|
|
$contentIdsCount = count($contentIds); |
|
149
|
|
|
|
|
150
|
|
|
if ($contentIdsCount === 0) { |
|
151
|
|
|
$output->writeln('<info>There is no content matching the given Criteria.</info>'); |
|
152
|
|
|
|
|
153
|
|
|
return 0; |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
$output->writeln(sprintf( |
|
157
|
|
|
'<info>Found %d Content IDs matching the given Criteria.</info>', |
|
158
|
|
|
$contentIdsCount |
|
159
|
|
|
)); |
|
160
|
|
|
|
|
161
|
|
|
$displayProgressBar = !($output->isVerbose() || $output->isVeryVerbose() || $output->isDebug()); |
|
162
|
|
|
|
|
163
|
|
|
if ($displayProgressBar) { |
|
164
|
|
|
$progressBar = new ProgressBar($output, $contentIdsCount); |
|
165
|
|
|
$progressBar->setFormat( |
|
166
|
|
|
'%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%' . PHP_EOL |
|
167
|
|
|
); |
|
168
|
|
|
$progressBar->start(); |
|
169
|
|
|
} |
|
170
|
|
|
|
|
171
|
|
|
$removedVersionsCounter = 0; |
|
172
|
|
|
|
|
173
|
|
|
$removeAll = $status === self::VERSION_ALL; |
|
174
|
|
|
|
|
175
|
|
|
foreach ($contentIds as $contentId) { |
|
176
|
|
|
try { |
|
177
|
|
|
$contentInfo = $contentService->loadContentInfo((int) $contentId); |
|
178
|
|
|
$versions = $contentService->loadVersions( |
|
179
|
|
|
$contentInfo, |
|
180
|
|
|
$removeAll ? null : $this->mapStatusToVersionInfoStatus($status) |
|
181
|
|
|
); |
|
182
|
|
|
$versionsCount = count($versions); |
|
183
|
|
|
|
|
184
|
|
|
$output->writeln(sprintf( |
|
185
|
|
|
'<info>Content %d has %d version(s)</info>', |
|
186
|
|
|
(int) $contentId, |
|
187
|
|
|
$versionsCount |
|
188
|
|
|
), OutputInterface::VERBOSITY_VERBOSE); |
|
189
|
|
|
|
|
190
|
|
|
if ($removeAll) { |
|
191
|
|
|
$versions = array_filter($versions, static function (VersionInfo $version) { |
|
192
|
|
|
return $version->status !== VersionInfo::STATUS_PUBLISHED; |
|
193
|
|
|
}); |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
if ($keep > 0) { |
|
197
|
|
|
$versions = array_slice($versions, 0, -$keep); |
|
198
|
|
|
} |
|
199
|
|
|
|
|
200
|
|
|
$output->writeln(sprintf( |
|
201
|
|
|
'Found %d content (%d) version(s) to remove.', |
|
202
|
|
|
count($versions), |
|
203
|
|
|
(int) $contentId |
|
204
|
|
|
), OutputInterface::VERBOSITY_VERBOSE); |
|
205
|
|
|
|
|
206
|
|
|
/** @var \eZ\Publish\API\Repository\Values\Content\VersionInfo $version */ |
|
207
|
|
|
foreach ($versions as $version) { |
|
208
|
|
|
$contentService->deleteVersion($version); |
|
209
|
|
|
++$removedVersionsCounter; |
|
210
|
|
|
$output->writeln(sprintf( |
|
211
|
|
|
'Content (%d) version (%d) has been deleted.', |
|
212
|
|
|
$contentInfo->id, |
|
213
|
|
|
$version->id |
|
214
|
|
|
), OutputInterface::VERBOSITY_VERBOSE); |
|
215
|
|
|
} |
|
216
|
|
|
|
|
217
|
|
|
if ($displayProgressBar) { |
|
218
|
|
|
$progressBar->advance(1); |
|
|
|
|
|
|
219
|
|
|
} |
|
220
|
|
|
} catch (Exception $e) { |
|
221
|
|
|
$output->writeln(sprintf( |
|
222
|
|
|
'<error>%s</error>', |
|
223
|
|
|
$e->getMessage() |
|
224
|
|
|
)); |
|
225
|
|
|
} |
|
226
|
|
|
} |
|
227
|
|
|
|
|
228
|
|
|
$output->writeln(sprintf( |
|
229
|
|
|
'<info>Removed %d unwanted contents version(s) from %d Content item(s).</info>', |
|
230
|
|
|
$removedVersionsCounter, |
|
231
|
|
|
$contentIdsCount |
|
232
|
|
|
)); |
|
233
|
|
|
|
|
234
|
|
|
return 0; |
|
235
|
|
|
} |
|
236
|
|
|
|
|
237
|
|
|
/** |
|
238
|
|
|
* @param int $keep |
|
239
|
|
|
* @param string $status |
|
240
|
|
|
* @param string[] $excludedContentTypes |
|
241
|
|
|
* |
|
242
|
|
|
* @return array |
|
243
|
|
|
* |
|
244
|
|
|
* @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException |
|
245
|
|
|
*/ |
|
246
|
|
|
protected function getObjectsIds($keep, $status, $excludedContentTypes = []) |
|
247
|
|
|
{ |
|
248
|
|
|
$query = $this->connection->createQueryBuilder() |
|
|
|
|
|
|
249
|
|
|
->select('c.id') |
|
250
|
|
|
->from('ezcontentobject', 'c') |
|
251
|
|
|
->join('c', 'ezcontentobject_version', 'v', 'v.contentobject_id = c.id') |
|
252
|
|
|
->join('c', 'ezcontentclass', 'cl', 'cl.id = c.contentclass_id') |
|
253
|
|
|
->groupBy('c.id', 'v.status') |
|
254
|
|
|
->having('count(c.id) > :keep'); |
|
255
|
|
|
$query->setParameter('keep', $keep); |
|
256
|
|
|
|
|
257
|
|
|
if ($status !== self::VERSION_ALL) { |
|
258
|
|
|
$query->where('v.status = :status'); |
|
259
|
|
|
$query->setParameter('status', $this->mapStatusToVersionInfoStatus($status)); |
|
260
|
|
|
} else { |
|
261
|
|
|
$query->andWhere('v.status != :status'); |
|
262
|
|
|
$query->setParameter('status', $this->mapStatusToVersionInfoStatus(self::VERSION_PUBLISHED)); |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
if ($excludedContentTypes) { |
|
|
|
|
|
|
266
|
|
|
$expr = $query->expr(); |
|
267
|
|
|
$query |
|
268
|
|
|
->andWhere( |
|
269
|
|
|
$expr->notIn( |
|
270
|
|
|
'cl.identifier', |
|
271
|
|
|
':contentTypes' |
|
272
|
|
|
) |
|
273
|
|
|
)->setParameter(':contentTypes', $excludedContentTypes, Connection::PARAM_STR_ARRAY); |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
$stmt = $query->execute(); |
|
277
|
|
|
|
|
278
|
|
|
return $stmt->fetchAll(PDO::FETCH_COLUMN); |
|
279
|
|
|
} |
|
280
|
|
|
|
|
281
|
|
|
/** |
|
282
|
|
|
* @param string $status |
|
283
|
|
|
* |
|
284
|
|
|
* @return int |
|
285
|
|
|
* |
|
286
|
|
|
* @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException |
|
287
|
|
|
*/ |
|
288
|
|
|
private function mapStatusToVersionInfoStatus($status) |
|
289
|
|
|
{ |
|
290
|
|
|
if (array_key_exists($status, self::VERSION_STATUS)) { |
|
291
|
|
|
return self::VERSION_STATUS[$status]; |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
throw new InvalidArgumentException( |
|
295
|
|
|
'status', |
|
296
|
|
|
sprintf( |
|
297
|
|
|
'Status %s cannot be mapped to a VersionInfo status.', |
|
298
|
|
|
$status |
|
299
|
|
|
) |
|
300
|
|
|
); |
|
301
|
|
|
} |
|
302
|
|
|
} |
|
303
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.