Completed
Push — master ( c92ef6...3f980f )
by
unknown
13:33
created

markExtensionWithMaximumVersionAsCurrent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
nc 2
nop 1
dl 0
loc 20
rs 9.7998
c 0
b 0
f 0
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\Extensionmanager\Domain\Repository;
17
18
use Doctrine\DBAL\DBALException;
19
use TYPO3\CMS\Core\Database\Connection;
20
use TYPO3\CMS\Core\Database\ConnectionPool;
21
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
22
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
23
use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
24
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
25
use TYPO3\CMS\Extensionmanager\Utility\Parser\AbstractExtensionXmlParser;
26
use TYPO3\CMS\Extensionmanager\Utility\Parser\XmlParserFactory;
27
28
/**
29
 * Importer object for extension list, which handles the XML parser and writes directly into the database.
30
 *
31
 * @internal This class is a specific domain repository implementation and is not part of the Public TYPO3 API.
32
 */
33
class BulkExtensionRepositoryWriter implements \SplObserver
34
{
35
    /**
36
     * @var string
37
     */
38
    private const TABLE_NAME = 'tx_extensionmanager_domain_model_extension';
39
40
    /**
41
     * Keeps instance of a XML parser.
42
     *
43
     * @var AbstractExtensionXmlParser
44
     */
45
    protected $parser;
46
47
    /**
48
     * Keeps number of processed version records.
49
     *
50
     * @var int
51
     */
52
    protected $sumRecords = 0;
53
54
    /**
55
     * Keeps record values to be inserted into database.
56
     *
57
     * @var array
58
     */
59
    protected $arrRows = [];
60
61
    /**
62
     * Keeps fieldnames of tx_extensionmanager_domain_model_extension table.
63
     *
64
     * @var array
65
     */
66
    protected static $fieldNames = [
67
        'extension_key',
68
        'version',
69
        'integer_version',
70
        'current_version',
71
        'alldownloadcounter',
72
        'downloadcounter',
73
        'title',
74
        'ownerusername',
75
        'author_name',
76
        'author_email',
77
        'authorcompany',
78
        'last_updated',
79
        'md5hash',
80
        'remote',
81
        'state',
82
        'review_state',
83
        'category',
84
        'description',
85
        'serialized_dependencies',
86
        'update_comment',
87
        'documentation_link'
88
    ];
89
90
    /**
91
     * Maximum of rows that can be used in a bulk insert for the current
92
     * database platform.
93
     *
94
     * @var int
95
     */
96
    protected $maxRowsPerChunk = 50;
97
98
    /**
99
     * Keeps the information from which remote the extension list was fetched.
100
     *
101
     * @var string
102
     */
103
    protected $remoteIdentifier;
104
105
    /**
106
     * @var ExtensionRepository
107
     */
108
    protected $extensionRepository;
109
110
    /**
111
     * @var Extension
112
     */
113
    protected $extensionModel;
114
115
    /**
116
     * @var ConnectionPool
117
     */
118
    protected $connectionPool;
119
120
    /**
121
     * Only import extensions newer than this date (timestamp),
122
     * see constructor
123
     *
124
     * @var int
125
     */
126
    protected $minimumDateToImport;
127
128
    /**
129
     * Method retrieves and initializes extension XML parser instance.
130
     *
131
     * @param ExtensionRepository $repository
132
     * @param Extension $extension
133
     * @param ConnectionPool $connectionPool
134
     *
135
     * @throws ExtensionManagerException
136
     * @throws DBALException
137
     */
138
    public function __construct(ExtensionRepository $repository, Extension $extension, ConnectionPool $connectionPool)
139
    {
140
        $this->extensionRepository = $repository;
141
        $this->extensionModel = $extension;
142
        $this->connectionPool = $connectionPool;
143
        $this->parser = XmlParserFactory::getParserInstance();
144
        if (is_object($this->parser)) {
145
            $this->parser->attach($this);
146
        } else {
147
            throw new ExtensionManagerException(
148
                static::class . ': No XML parser available.',
149
                1476108717
150
            );
151
        }
152
153
        $connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME);
154
        $maxBindParameters = PlatformInformation::getMaxBindParameters(
155
            $connection->getDatabasePlatform()
156
        );
157
        $countOfBindParamsPerRow = count(self::$fieldNames);
158
        // flush at least chunks of 50 elements - in case the currently used
159
        // database platform does not support that, the threshold is lowered
160
        $this->maxRowsPerChunk = min(
161
            $this->maxRowsPerChunk,
162
            floor($maxBindParameters / $countOfBindParamsPerRow)
163
        );
164
        // Only import extensions that are compatible with 7.6 or higher.
165
        // TER only allows to publish extensions with compatibility if the TYPO3 version has been released
166
        // And 7.6 was released on 10th of November 2015.
167
        // This effectively reduces the number of extensions imported into this TYPO3 installation
168
        // by more than 70%. As long as the extensions.xml from TER includes these files, we need to "hack" this
169
        // within TYPO3 Core.
170
        // For TYPO3 v11.0, this date could be set to 2018-10-02 (v9 LTS release).
171
        // Also see https://decisions.typo3.org/t/reduce-size-of-extension-manager-db-table/329/
172
        $this->minimumDateToImport = strtotime('2017-04-04T00:00:00+00:00');
173
    }
174
175
    /**
176
     * Method initializes parsing of extension.xml.gz file.
177
     *
178
     * @param string $localExtensionListFile absolute path to extension list xml.gz
179
     * @param string $remoteIdentifier identifier of the remote when inserting records into DB
180
     */
181
    public function import(string $localExtensionListFile, string $remoteIdentifier): void
182
    {
183
        // Remove all existing entries of this remote from the database
184
        $this->connectionPool
185
            ->getConnectionForTable(self::TABLE_NAME)
186
            ->delete(
187
                self::TABLE_NAME,
188
                ['remote' => $remoteIdentifier],
189
                [\PDO::PARAM_STR]
190
            );
191
        $this->remoteIdentifier = $remoteIdentifier;
192
        $zlibStream = 'compress.zlib://';
193
        $this->sumRecords = 0;
194
        $this->parser->parseXml($zlibStream . $localExtensionListFile);
195
        // flush last rows to database if existing
196
        if (!empty($this->arrRows)) {
197
            $this->connectionPool
198
                ->getConnectionForTable(self::TABLE_NAME)
199
                ->bulkInsert(
200
                    'tx_extensionmanager_domain_model_extension',
201
                    $this->arrRows,
202
                    self::$fieldNames
203
                );
204
        }
205
        $this->markExtensionWithMaximumVersionAsCurrent($remoteIdentifier);
206
    }
207
208
    /**
209
     * Method collects and stores extension version details into the database.
210
     *
211
     * @param AbstractExtensionXmlParser $subject a subject notifying this observer
212
     */
213
    protected function loadIntoDatabase(AbstractExtensionXmlParser $subject): void
214
    {
215
        if ($this->sumRecords !== 0 && $this->sumRecords % $this->maxRowsPerChunk === 0) {
216
            $this->connectionPool
217
                ->getConnectionForTable(self::TABLE_NAME)
218
                ->bulkInsert(
219
                    self::TABLE_NAME,
220
                    $this->arrRows,
221
                    self::$fieldNames
222
                );
223
            $this->arrRows = [];
224
        }
225
        $versionRepresentations = VersionNumberUtility::convertVersionStringToArray($subject->getVersion());
226
        // order must match that of self::$fieldNames!
227
        $this->arrRows[] = [
228
            $subject->getExtkey(),
229
            $subject->getVersion(),
230
            $versionRepresentations['version_int'],
231
            // initialize current_version, correct value computed later:
232
            0,
233
            (int)$subject->getAlldownloadcounter(),
234
            (int)$subject->getDownloadcounter(),
235
            $subject->getTitle() ?? '',
236
            $subject->getOwnerusername(),
237
            $subject->getAuthorname() ?? '',
238
            $subject->getAuthoremail() ?? '',
239
            $subject->getAuthorcompany() ?? '',
240
            (int)$subject->getLastuploaddate(),
241
            $subject->getT3xfilemd5(),
242
            $this->remoteIdentifier,
243
            $this->extensionModel->getDefaultState($subject->getState() ?: ''),
244
            (int)$subject->getReviewstate(),
245
            $this->extensionModel->getCategoryIndexFromStringOrNumber($subject->getCategory() ?: ''),
246
            $subject->getDescription() ?: '',
247
            $subject->getDependencies() ?: '',
248
            $subject->getUploadcomment() ?: '',
249
            $subject->getDocumentationLink() ?: '',
250
        ];
251
        ++$this->sumRecords;
252
    }
253
254
    /**
255
     * Method receives an update from a subject.
256
     *
257
     * @param \SplSubject $subject a subject notifying this observer
258
     */
259
    public function update(\SplSubject $subject): void
260
    {
261
        if (is_subclass_of($subject, AbstractExtensionXmlParser::class)) {
262
            if ((int)$subject->getLastuploaddate() > $this->minimumDateToImport) {
0 ignored issues
show
Bug introduced by
The method getLastuploaddate() does not exist on SplSubject. It seems like you code against a sub-type of SplSubject such as TYPO3\CMS\Extensionmanag...tractExtensionXmlParser. ( Ignorable by Annotation )

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

262
            if ((int)$subject->/** @scrutinizer ignore-call */ getLastuploaddate() > $this->minimumDateToImport) {
Loading history...
263
                $this->loadIntoDatabase($subject);
264
            }
265
        }
266
    }
267
268
    /**
269
     * Sets current_version = 1 for all extensions where the extension version is maximal.
270
     *
271
     * For performance reasons, the "native" database connection is used here directly.
272
     *
273
     * @param string $remoteIdentifier
274
     */
275
    protected function markExtensionWithMaximumVersionAsCurrent(string $remoteIdentifier): void
276
    {
277
        $uidsOfCurrentVersion = $this->fetchMaximalVersionsForAllExtensions($remoteIdentifier);
278
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
279
        $connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME);
280
        $maxBindParameters = PlatformInformation::getMaxBindParameters(
281
            $connection->getDatabasePlatform()
282
        );
283
284
        foreach (array_chunk($uidsOfCurrentVersion, $maxBindParameters - 10) as $chunk) {
285
            $queryBuilder
286
                ->update(self::TABLE_NAME)
287
                ->where(
288
                    $queryBuilder->expr()->in(
289
                        'uid',
290
                        $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
291
                    )
292
                )
293
                ->set('current_version', 1)
294
                ->execute();
295
        }
296
    }
297
298
    /**
299
     * Fetches the UIDs of all maximal versions for all extensions.
300
     * This is done by doing a LEFT JOIN to itself ("a" and "b") and comparing
301
     * both integer_version fields.
302
     *
303
     * @param string $remoteIdentifier
304
     * @return array
305
     */
306
    protected function fetchMaximalVersionsForAllExtensions(string $remoteIdentifier): array
307
    {
308
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
309
310
        $queryResult = $queryBuilder
311
            ->select('a.uid AS uid')
312
            ->from(self::TABLE_NAME, 'a')
313
            ->leftJoin(
314
                'a',
315
                self::TABLE_NAME,
316
                'b',
317
                $queryBuilder->expr()->andX(
318
                    $queryBuilder->expr()->eq('a.remote', $queryBuilder->quoteIdentifier('b.remote')),
319
                    $queryBuilder->expr()->eq('a.extension_key', $queryBuilder->quoteIdentifier('b.extension_key')),
320
                    $queryBuilder->expr()->lt('a.integer_version', $queryBuilder->quoteIdentifier('b.integer_version'))
321
                )
322
            )
323
            ->where(
324
                $queryBuilder->expr()->eq(
325
                    'a.remote',
326
                    $queryBuilder->createNamedParameter($remoteIdentifier)
327
                ),
328
                $queryBuilder->expr()->isNull('b.extension_key')
329
            )
330
            ->orderBy('a.uid')
331
            ->execute();
332
333
        $extensionUids = [];
334
        while ($row = $queryResult->fetch()) {
335
            $extensionUids[] = $row['uid'];
336
        }
337
338
        return $extensionUids;
339
    }
340
}
341