GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Issues (28)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Gerrie/Gerrie.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This file is part of the Gerrie package.
4
 *
5
 * (c) Andreas Grunwald <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
/**
12
 * @todo further improvement / data mining
13
 * This Gerrit exporter got lot of potentials.
14
 * At the moment only the current state of the Gerrit system will be exported / represented in our database.
15
 * But in the next run, we "only" update our data.
16
 * The "real" raw data won`t be saved.
17
 * e.g. During a new patchset, the commit message can be updated, but we only store the current commit message.
18
 * What about the previous commit message? Analysis like "what kind of changed did the community x apply during
19
 * the patchsets to the commit message" are not possible at the moment.
20
 * So this "history" "raw" data thingy might be a good canditdate for an amazing improvement.
21
 * Maybe you got some spare time, motivation and skill to do this? This would be cool :)
22
 * If you need more information, just contact me. Thanks :)
23
 *
24
 * @author Andy Grunwald <[email protected]>
25
 */
26
namespace Gerrie;
27
28
use Gerrie\Component\Database\Database;
29
use Gerrie\API\DataService\DataServiceInterface;
30
use Gerrie\API\Repository\ProjectRepository;
31
use Gerrie\Transformer\TransformerFactory;
32
33
class Gerrie
34
{
35
36
    /**
37
     * Base interface of PSR Logging
38
     *
39
     * @var string
40
     */
41
    const PSR_LOG_BASE = 'Psr\Log\LoggerInterface';
42
43
    /**
44
     * Base interface of Symfony console output
45
     *
46
     * @var string
47
     */
48
    const SYMFONY_CONSOLE_BASE = 'Symfony\Component\Console\Output\OutputInterface';
49
50
    /**
51
     * Database helper object
52
     *
53
     * @var Database
54
     */
55
    protected $database = null;
56
57
    /**
58
     * Storage for data services.
59
     *
60
     * @var DataServiceInterface
61
     */
62
    protected $dataService = null;
63
64
    /**
65
     * Config array
66
     *
67
     * @var array
68
     */
69
    protected $config = array();
70
71
    /**
72
     * Timer information
73
     *
74
     * @var array
75
     */
76
    protected $timer = array();
77
78
    /**
79
     * Statistics information
80
     *
81
     * @var array
82
     */
83
    protected $statsContainer = array();
84
85
    /**
86
     * @var \Symfony\Component\Console\Output\OutputInterface
87
     */
88
    protected $output = null;
89
90
    /**
91
     * Server ID for Gerrit database identification
92
     *
93
     * @var int
94
     */
95
    protected $serverId = 0;
96
97
    /**
98
     * If this is the first time that one Gerrit server is imported,
99
     * some cases must not be executed.
100
     *
101
     * @var bool
102
     */
103
    protected $serversFirstRun = false;
104
105
    /**
106
     * Green output text
107
     *
108
     * @var integer
109
     */
110
    const OUTPUT_INFO = 1;
111
112
    /**
113
     * Yellow output text
114
     *
115
     * @var integer
116
     */
117
    const OUTPUT_COMMENT = 2;
118
119
    /**
120
     * Red output text
121
     *
122
     * @var integer
123
     */
124
    const OUTPUT_ERROR = 3;
125
126
    /**
127
     * Switch for debug functionality
128
     *
129
     * @var bool
130
     */
131
    protected $debug = false;
132
133
    /**
134
     * Constructor
135
     *
136
     * @param Database $database The database helper object
137
     * @param DataServiceInterface $dataService The data service object
138
     * @param array $config The configuration array
139
     * @return \Gerrie\Gerrie
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
140
     */
141
    public function __construct(Database $database, DataServiceInterface $dataService, array $config)
142
    {
143
        $this->setDatabase($database);
144
        $this->setDataService($dataService);
145
        $this->setConfig($config);
146
    }
147
148
    /**
149
     * Sets the configuration
150
     *
151
     * @param array $config The configuration
152
     */
153
    public function setConfig($config)
154
    {
155
        $this->config = $config;
156
    }
157
158
    /**
159
     * Gets the configuration
160
     *
161
     * @return array
162
     */
163
    public function getConfig()
164
    {
165
        return $this->config;
166
    }
167
168
    /**
169
     * Sets the database object
170
     *
171
     * @param \Gerrie\Component\Database\Database $database The database object
172
     */
173
    public function setDatabase($database)
174
    {
175
        $this->database = $database;
176
    }
177
178
    /**
179
     * Gets the database object
180
     *
181
     * @return \Gerrie\Component\Database\Database
182
     */
183
    public function getDatabase()
184
    {
185
        return $this->database;
186
    }
187
188
    /**
189
     * Sets the data service
190
     *
191
     * @param DataServiceInterface $dataService Data service object
192
     * @return void
193
     */
194
    public function setDataService(DataServiceInterface $dataService)
195
    {
196
        $this->dataService = $dataService;
197
    }
198
199
    /**
200
     * Gets a data service
201
     *
202
     * @return DataServiceInterface
203
     */
204
    public function getDataService()
205
    {
206
        return $this->dataService;
207
    }
208
209
    /**
210
     * Sets the output object for CLI output
211
     *
212
     * @param \Symfony\Component\Console\Output\OutputInterface|\Monolog\Logger $output The output object
213
     * @throws \Exception
214
     * @return void
215
     */
216
    public function setOutput($output)
217
    {
218
        $psrLogBase = self::PSR_LOG_BASE;
219
        $consoleBase = self::SYMFONY_CONSOLE_BASE;
220
221
        if (($output instanceof $psrLogBase) === false && ($output instanceof $consoleBase) === false) {
222
            $className = get_class($output);
223
            $message = 'Output class "%s" not supported. Only "%s" or "%s"';
224
            throw new \Exception(sprintf($message, $className, $psrLogBase, $consoleBase), 1369595109);
225
        }
226
227
        $this->output = $output;
0 ignored issues
show
Documentation Bug introduced by
It seems like $output can also be of type object<Monolog\Logger>. However, the property $output is declared as type object<Symfony\Component...Output\OutputInterface>. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
228
    }
229
230
    /**
231
     * Gets the output object for CLI output
232
     *
233
     * @return \Symfony\Component\Console\Output\OutputInterface|null
234
     */
235
    public function getOutput()
236
    {
237
        return $this->output;
238
    }
239
240
    /**
241
     * Sets the serverFirstRun value :)
242
     *
243
     * @param bool $firstRun True = yes, false otherwise
244
     * @return void
245
     */
246
    public function setServersFirstRun($firstRun = true)
247
    {
248
        $this->serversFirstRun = (bool)$firstRun;
249
    }
250
251
    /**
252
     * Checks if this run is the first run of this Gerrit server
253
     *
254
     * @return bool
255
     */
256
    public function isServersFirstRun()
257
    {
258
        return $this->serversFirstRun;
259
    }
260
261
    /**
262
     * Output a given $message, if $this->output is set with an OutputInterface object
263
     *
264
     * @param string $message The message to output
265
     * @param int $type The color of output
266
     * @return void
267
     */
268
    protected function output($message, $type = self::OUTPUT_INFO)
269
    {
270
        $output = $this->getOutput();
271
272
        if ($output === null) {
273
            return;
274
        }
275
276
        // Chose color type of message
277
        switch ($type) {
278
            case self::OUTPUT_COMMENT:
279
                $logMethod = 'info';
280
                $prefix = '<comment>';
281
                $postfix = '</comment>';
282
                break;
283
284
            case self::OUTPUT_ERROR:
285
                $logMethod = 'critical';
286
                $prefix = '<error>';
287
                $postfix = '</error>';
288
                break;
289
290
            case self::OUTPUT_INFO:
291
            default:
292
                $logMethod = 'info';
293
                $prefix = '<info>';
294
                $postfix = '</info>';
295
                break;
296
        }
297
298
        $psrLogBase = self::PSR_LOG_BASE;
299
        $consoleBase = self::SYMFONY_CONSOLE_BASE;
300
301
        if ($output instanceof $psrLogBase) {
302
            $output->$logMethod($message);
303
304
        } else {
305
            if ($output instanceof $consoleBase) {
306
                $output->writeln($prefix . $message . $postfix);
307
            }
308
        }
309
310
    }
311
312
    /**
313
     * Saves a time next to a token.
314
     * This can be use to measure import tasks.
315
     *
316
     * @param string $token A free chosen token. e.g. starttime, endtime, ...
317
     * @param int $time A timestamp or another time identifier
318
     * @return void
319
     */
320
    protected function setTime($token, $time = 0)
321
    {
322
        if ($time == 0) {
323
            $time = time();
324
        }
325
326
        $this->timer[$token] = $time;
327
    }
328
329
    /**
330
     * Returns the saved time identifier for given $token.
331
     *
332
     * @param string $token A free chosen token. e.g. starttime, endtime, ...
333
     * @return int
334
     */
335
    protected function getTime($token)
336
    {
337
        return $this->timer[$token];
338
    }
339
340
    /**
341
     * Saves a statistic next to a token.
342
     * This can be use to collect numbers about actions (e.g. inserts, updates, ...)
343
     *
344
     * @param string $token Name of statistic
345
     * @param int $count Value of statistic
346
     * @return void
347
     */
348
    protected function setStatistic($token, $count)
349
    {
350
        $this->statsContainer[$token] = $count;
351
    }
352
353
    /**
354
     * Returns the saved statistic value for a given $token
355
     *
356
     * @param string $token Name of statistic
357
     * @return int
358
     */
359
    protected function getStatistic($token)
360
    {
361
        return $this->statsContainer[$token];
362
    }
363
364
    /**
365
     * Outputs the current memory usage
366
     *
367
     * @see http://de3.php.net/manual/de/function.memory-get-usage.php#96280
368
     *
369
     * @return void
370
     */
371
    protected function outputMemoryUsage()
372
    {
373
        $memory = memory_get_usage(true);
374
375
        $unit = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
376
        $memory = round($memory / pow(1024, ($i = floor(log($memory, 1024)))), 2) . ' ' . $unit[$i];
377
378
        $this->output('Memory usage: ' . $memory, self::OUTPUT_COMMENT);
379
    }
380
381
    /**
382
     * Outputs some statistics about the import run.
383
     * e.g. Needed time, memory, etc.
384
     *
385
     * @return void
386
     */
387
    protected function outputEndStatistics()
388
    {
389
        $runtime = $this->getTime('end') - $this->getTime('start');
390
391
        $this->output('');
392
        $this->output('Finish', self::OUTPUT_COMMENT);
393
        $this->output(
394
            'Runtime: ' . number_format(($runtime / 60), 2, ',', '.') . ' Min. (' . $runtime . ' seconds)',
395
            self::OUTPUT_COMMENT
396
        );
397
        $this->outputMemoryUsage();
398
        $this->output('');
399
    }
400
401
    /**
402
     * Sets the Gerrit system server id
403
     *
404
     * @param int $serverId Gerrit server id from the database
405
     * @return void
406
     */
407
    public function setServerId($serverId)
408
    {
409
        $this->serverId = $serverId;
410
    }
411
412
    /**
413
     * Returns the Gerrit system server id
414
     *
415
     * @return int
416
     */
417
    public function getServerId()
418
    {
419
        return $this->serverId;
420
    }
421
422
    /**
423
     * Queries all projects from the given $host and insert them to our database.
424
     *
425
     * @return void
426
     */
427
    protected function proceedProjects()
428
    {
429
        $this->outputHeadline('Proceed Projects');
430
431
        $transformerFactory = new TransformerFactory();
432
        $projectRepository = new ProjectRepository($this->getDataService(), $transformerFactory);
433
        $transformedProjects = $projectRepository->getProjects($this->isDebugFunctionalityEnabled());
434
435
        $parentMapping = [];
436
437
        // Loop over projects to proceed every single project
438
        foreach ($transformedProjects as $project) {
439
            $this->importProject($project, $parentMapping);
440
        }
441
442
        // Correct parent / child relation of projects
443
        $this->proceedProjectParentChildRelations($parentMapping);
444
    }
445
446
    /**
447
     * Entry point for external source code.
448
     * This method starts to export the data from a whole Gerrit review system
449
     *
450
     * @return bool
451
     */
452
    public function crawl()
453
    {
454
        $this->setTime('start');
455
456
        $config = $this->getConfig();
457
        $host = $this->getDataService()->getHost();
458
459
        // Here we go. Lets get the export party started.
460
        // At first, lets check if the current Gerrit review system is known by the database
461
        $this->proceedServer($config['Name'], $host);
462
463
        // After this, lets start to save all projects.
464
        // We need the projects first, because this is our "entry point" for the data mining.
465
        $this->proceedProjects();
466
467
        $this->output('');
468
        $this->outputHeadline('Export changesets per project');
469
470
        // Get all projects again and loop over every single project to import them :)
471
        $projects = $this->getGerritProjectsByServerId($this->getServerId());
472
        foreach ($projects as $project) {
473
            $this->output('Project "' . $project['name'] . '" ... Starts', self::OUTPUT_COMMENT);
474
475
            $this->proceedChangesetsOfProject($host, $project);
476
477
            $this->output('Project "' . $project['name'] . '" ... Ends', self::OUTPUT_COMMENT);
478
            $this->outputMemoryUsage();
479
            $this->output('');
480
        }
481
482
        // Clear the temp tables. The data is not needed anymore
483
        $this->cleanupTempTables();
484
485
        $this->setTime('end');
486
        $this->outputEndStatistics();
487
488
        return true;
489
    }
490
491
    /**
492
     * Imports all changes from a incoming project.
493
     *
494
     * It queries the Gerrit system via SSH API with the configured limit.
495
     * It receives  all changesets, patchsets, approvals and comments automatically and store them in our database.
496
     *
497
     * @see https://review.typo3.org/Documentation/cmd-query.html
498
     *
499
     * @param string $host Gerrit server host
500
     * @param array $project The current project
501
     * @return void
502
     */
503
    public function proceedChangesetsOfProject($host, array $project)
504
    {
505
506
        $dataService = $this->getDataService();
507
        $dataServiceName = $dataService->getName();
508
509
        // Clear the temp tables first
510
        $this->cleanupTempTables();
511
512
        // Query the data till we receive all data
513
        $startNum = 0;
514
515
        $sortKey = null;
516
        $changeSetQueryLimit = $dataService->getQueryLimit();
517
518
        do {
519
            $endNum = $startNum + $changeSetQueryLimit;
520
521
            $queryMessage = sprintf(
522
                'Querying %s via %s for changes %d...%s',
523
                $host,
524
                $dataServiceName,
525
                $startNum,
526
                $endNum
527
            );
528
            $this->output($queryMessage);
529
530
            $data = $dataService->getChangesets($project['name'], $sortKey, $startNum);
531
            $queryStatus = $this->transferJsonToArray(array_pop($data));
532
533
            $startNum += $queryStatus['rowCount'];
534
535
            $receivedMessage = sprintf('Received %d changes to process', $queryStatus['rowCount']);
536
            $this->output($receivedMessage);
537
538
            // One project can get n changesets. So lets do the same as one level above (the project loop)
539
            // Loop over all changesets and proceed (import / update) them ;)
540
            foreach ($data as $singleChangeset) {
541
                $changeSet = $this->transferJsonToArray($singleChangeset);
542
543
                $this->output($changeSet['subject']);
544
545
                // Import this changeset :)
546
                $this->proceedChangeset($changeSet, $project);
547
            }
548
549
            // First we query Gerrit for rows. If there are more rows than this limit,
550
            // we resume at the next changeset with the resume_sortkey feature.
551
            // But if the row cound lower the changeset query limit, we can exit here.
552
            // There are no more changesets in the next ssh query
553
            if ($queryStatus['rowCount'] > 0 && $queryStatus['rowCount'] < $changeSetQueryLimit) {
554
                break;
555
            }
556
557
            // We need to determine the last sortkey to continue the data reading for the next command.
558
            // How does it work? See the API documentation of Gerrit mentioned in the doc block
559
            $sortKey = $this->getLastSortKey($data);
560
        } while ($queryStatus['rowCount'] > 0);
561
562
        // Take care of 'dependsOn' relations in changesets
563
        $this->proceedChangeSetsDependsOnRelation();
564
565
        // Take care of 'neededBy' relations in changesets
566
        $this->proceedChangeSetsNeededByRelation();
567
    }
568
569
    /**
570
     * Update method to map the 'neededBy' relation from the temp table
571
     * to the correct changeset table.
572
     *
573
     * One changeset can have n neededBy relations
574
     *
575
     * @return void
576
     */
577
    protected function proceedChangeSetsNeededByRelation()
578
    {
579
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
580
581
        $tmpTable = Database::TABLE_TMP_DEPENDS_NEEDED;
582
        $neededByTable = Database::TABLE_CHANGESET_NEEDEDBY;
583
        $changeSetTable = Database::TABLE_CHANGESET;
584
        $patchSetTable = Database::TABLE_PATCHSET;
585
586
        $query = '
587
            INSERT INTO ' . $neededByTable . ' (`changeset`, `needed_by`, `tstamp`, `crdate`)
588
            SELECT
589
                ' . $changeSetTable . '.`id`,
590
                neededByChangeset.`id`,
591
                UNIX_TIMESTAMP(),
592
                UNIX_TIMESTAMP()
593
            FROM
594
                ' . $tmpTable . '
595
                INNER JOIN ' . $changeSetTable . ' AS neededByChangeset ON (
596
                    ' . $tmpTable . '.`identifier` = neededByChangeset.`identifier`
597
                    AND ' . $tmpTable . '.`number` = neededByChangeset.`number`
598
                )
599
                INNER JOIN gerrit_patchset ON (
600
                    neededByChangeset.`id` = ' . $patchSetTable . '.`changeset`
601
                    AND ' . $patchSetTable . '.`revision` = ' . $tmpTable . '.`revision`
602
                    AND ' . $patchSetTable . '.`ref` = ' . $tmpTable . '.`ref`
603
                )
604
                INNER JOIN ' . $changeSetTable . ' ON (
605
                    ' . $changeSetTable . '.`id` = ' . $tmpTable . '.`changeset`
606
                )
607
            WHERE
608
                ' . $tmpTable . '.`status` = ' . intval(Database::TMP_DEPENDS_NEEDED_STATUS_NEEDEDBY) . '
609
            GROUP BY
610
                ' . $changeSetTable . '.`id`, neededByChangeset.`id`
611
            ON DUPLICATE KEY UPDATE
612
                ' . $neededByTable . '.`tstamp` = UNIX_TIMESTAMP()';
613
614
        $dbHandle->exec($query);
615
    }
616
617
    /**
618
     * Update method to map the 'dependsOn' relation from the temp table
619
     * to the correct changeset table.
620
     *
621
     * One changeset can have one dependsOn relation.
622
     *
623
     * @see $this->proceedChangeSetsDependsOnAndNeededBy()
624
     *
625
     * @return void
626
     */
627
    protected function proceedChangeSetsDependsOnRelation()
628
    {
629
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
630
631
        $tmpTable = Database::TABLE_TMP_DEPENDS_NEEDED;
632
        $changeSetTable = Database::TABLE_CHANGESET;
633
        $patchSetTable = Database::TABLE_PATCHSET;
634
635
        $query = '
636
            UPDATE
637
                ' . $tmpTable . '
638
                INNER JOIN ' . $changeSetTable . ' AS dependsOnChangeset ON (
639
                    ' . $tmpTable . '.`identifier` = dependsOnChangeset.`identifier`
640
                    AND ' . $tmpTable . '.`number` = dependsOnChangeset.`number`
641
                )
642
                INNER JOIN ' . $patchSetTable . ' ON (
643
                    dependsOnChangeset.`id` = ' . $patchSetTable . '.`changeset`
644
                    AND ' . $patchSetTable . '.`revision` = ' . $tmpTable . '.`revision`
645
                    AND ' . $patchSetTable . '.`ref` = ' . $tmpTable . '.`ref`
646
                )
647
                INNER JOIN ' . $changeSetTable . ' ON (
648
                    ' . $changeSetTable . '.`id` = ' . $tmpTable . '.`changeset`
649
                )
650
            SET
651
                ' . $changeSetTable . '.`depends_on` = dependsOnChangeset.`id`
652
            WHERE
653
                ' . $tmpTable . '.`status` = ' . intval(Database::TMP_DEPENDS_NEEDED_STATUS_DEPENDSON);
654
655
        $dbHandle->exec($query);
656
    }
657
658
    /**
659
     * Proceed tracking ids per changeset.
660
     * Checks if they are already exists. If not, insert them.
661
     *
662
     * @param integer $changeSetId Current changeset id
663
     * @param array $trackingIds Tracking ids to proceed
664
     * @return void
665
     */
666
    protected function proceedTrackingIds($changeSetId, array $trackingIds)
667
    {
668
        // The commit message includes the tracking ids.
669
        // Via a new patchset, the commit message (inkl. tracking ids) can change.
670
        // To handle this case, we set all tracking ids to referenced_earlier => 1
671
        // and proceed the tracking ids. If the tracking id is still in the commit message
672
        // it will be set back to referenced_earlier => 0.
673
        $dataToUpdate = array(
674
            'referenced_earlier' => 1
675
        );
676
        $where = 'changeset = ' . intval($changeSetId);
677
        $this->getDatabase()->updateRecords(Database::TABLE_TRACKING_ID, $dataToUpdate, $where);
678
679
        foreach ($trackingIds as $trackingId) {
680
            $system = $this->proceedLookupTable(Database::TABLE_TRACKING_SYSTEM, 'id', 'name', $trackingId['system']);
681
682
            $trackingIdRow = $this->getGerritTrackingIdByIdentifier($changeSetId, $system, $trackingId['id']);
683
684
            // If there is no tracking id with the current changeset, system and number, insert it
685
            if ($trackingIdRow === false) {
686
                $trackingRow = array(
687
                    'changeset' => $changeSetId,
688
                    'system' => $system,
689
                    'number' => $trackingId['id']
690
                );
691
                $this->getDatabase()->insertRecord(Database::TABLE_TRACKING_ID, $trackingRow);
692
693
                // We know this tracking id, but this id was set to referenced_earlier => 1 earlier.
694
                // So lets reactivate this!
695
            } else {
696
                $dataToUpdate = array(
697
                    'referenced_earlier' => 0
698
                );
699
                $this->getDatabase()->updateRecord(Database::TABLE_TRACKING_ID, $dataToUpdate, $trackingIdRow['id']);
700
            }
701
702
            $trackingId = $this->unsetKeys($trackingId, array('id', 'system'));
703
            $this->checkIfAllValuesWereProceeded($trackingId, 'Tracking ID');
704
        }
705
    }
706
707
    /**
708
     * Returns a tracking id by the unique identifier
709
     *
710
     * @param int $changeSetId ID of a record from the changeset table
711
     * @param int $system ID of a record from the system table
712
     * @param string $trackingId Tracking id
713
     * @return mixed
714
     */
715
    protected function getGerritTrackingIdByIdentifier($changeSetId, $system, $trackingId)
716
    {
717
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
718
719
        $query = 'SELECT `id`, `changeset`, `system`, `number`
720
                  FROM ' . Database::TABLE_TRACKING_ID . '
721
                  WHERE `changeset` = :changeset
722
                        AND `system` = :system
723
                        AND `number` = :number';
724
        $statement = $dbHandle->prepare($query);
725
726
        $statement->bindParam(':changeset', $changeSetId, \PDO::PARAM_INT);
727
        $statement->bindParam(':system', $system, \PDO::PARAM_INT);
728
        $statement->bindParam(':number', $trackingId, \PDO::PARAM_STR);
729
        $executeResult = $statement->execute();
730
731
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
732
        return $statement->fetch(\PDO::FETCH_ASSOC);
733
    }
734
735
    /**
736
     * Returns the unique identifier for a lookup table value.
737
     * If the requested lookup table value ($compareValue) not available,
738
     * it will be inserted
739
     *
740
     * @param string $table Lookup table
741
     * @param string $idField Field name of unique identifier (most single primary key)
742
     * @param $compareField Compare field. e.g. 'name' in branch table
743
     * @param $compareValue Compare value. e.g. 'master' in branch table
744
     * @return int
745
     */
746
    protected function proceedLookupTable($table, $idField, $compareField, $compareValue)
747
    {
748
        $whereParts = array($compareField => $compareValue);
749
        $row = $this->getLookupTableValues($table, array($idField), $whereParts);
750
751
        // If there is no branch with the current branch name, insert it
752
        if ($row === false) {
753
            $idValue = $this->getDatabase()->insertRecord($table, array($compareField => $compareValue));
754
755
        } else {
756
            $idValue = $row[$idField];
757
        }
758
759
        return $idValue;
760
    }
761
762
    /**
763
     * Proceeds a single changeset.
764
     * One changeset can have n patchsets, n comments and so on.
765
     *
766
     * @param array $changeSet The current changeset
767
     * @param array $project The current project
768
     * @return void
769
     */
770
    protected function proceedChangeset(array $changeSet, array $project)
771
    {
772
773
        // Take care of branch
774
        $changeSet['branch'] = $this->proceedLookupTable(Database::TABLE_BRANCH, 'id', 'name', $changeSet['branch']);
775
776
        // Take care of owner
777
        $changeSet['owner'] = $this->proceedPerson($changeSet['owner']);
778
779
        // Take care of status
780
        $changeSet['status'] = $this->proceedLookupTable(Database::TABLE_STATUS, 'id', 'name', $changeSet['status']);
781
782
        $changeSetData = array(
783
            'project' => $project['id'],
784
            'branch' => $changeSet['branch'],
785
            'topic' => ((isset($changeSet['topic']) === true) ? $changeSet['topic'] : ''),
786
            'identifier' => $changeSet['id'],
787
            'number' => $changeSet['number'],
788
            'subject' => $changeSet['subject'],
789
            'owner' => $changeSet['owner']['id'],
790
            'url' => $changeSet['url'],
791
            'commit_message' => $changeSet['commitMessage'],
792
            'created_on' => $changeSet['createdOn'],
793
            'last_updated' => $changeSet['lastUpdated'],
794
            'open' => intval($changeSet['open']),
795
            'status' => $changeSet['status'],
796
        );
797
798
        if (isset($changeSet['sortKey']) === true) {
799
            $changeSetData['sort_key'] = $changeSet['sortKey'];
800
        }
801
802
        // A changeset don`t have a unique identifier.
803
        // The Gerrit documentation says that "<project>~<branch>~<Change-Id>" will be enough
804
        // @see https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
805
        // This combination seems to be "unique enough", BUT we have to add createdOn property as well.
806
        // Why? Have a look at these two changesets ...
807
        // e.g. https://review.typo3.org/#/c/1427/ and https://review.typo3.org/#/c/1423/
808
        // If anyone got an answer, why this changes got the same change id, the same branch and the same project
809
        // Let me know.
810
        // Check if this changeset already exists ...
811
        $changeSetRow = $this->getGerritChangesetByIdentifier(
812
            $project['id'],
813
            $changeSet['branch'],
814
            $changeSet['id'],
815
            $changeSet['createdOn']
816
        );
817
        if ($changeSetRow === false) {
818
            $changeSet['id'] = $this->getDatabase()->insertRecord(Database::TABLE_CHANGESET, $changeSetData);
819
820
            $this->output('=> Inserted (ID: ' . $changeSet['id'] . ')');
821
822
            // If the timestamp 'last updated' is newer than our database timestamp
823
            // There must something new ... update it! e.g.
824
            // If there is a new comment, lastUpdated will be updated with a new timestamp
825
            // If there is a new patchset, lastUpdated will be updated with a new timestamp ...
826
            // If a changeset is merged, it is not possible to push a new patch set
827
        } elseif ($changeSet['lastUpdated'] > $changeSetRow['last_updated']) {
828
829
            $this->checkIfServersFirstRun('Changeset', 1363893102, array($changeSet, $changeSetRow));
830
831
            // Calculate the difference and update it :)
832
            $changeSet['id'] = $changeSetRow['id'];
833
            $dataDiff = array_diff($changeSetData, $changeSetRow);
834
            $this->getDatabase()->updateRecord(Database::TABLE_CHANGESET, $dataDiff, $changeSet['id']);
835
836
            $this->output('=> Updated (ID: ' . $changeSet['id'] . ')');
837
838
            // If the timestamp 'last updated' is equal than our database timestamp
839
            // There are no new information for us ... stop proceeding this changeset.
840
        } elseif ($changeSet['lastUpdated'] == $changeSetRow['last_updated']) {
841
842
            $this->output('=> Nothing new. Skip it');
843
            return;
844
        }
845
846
        // Unset not needed keys, because a) memory and b) to check if there is ata which was not imported :)
847
        $keysToUnset = array(
848
            'project',
849
            'branch',
850
            'topic',
851
            'identifier',
852
            'number',
853
            'subject',
854
            'owner',
855
            'url',
856
            'commitMessage',
857
            'createdOn',
858
            'lastUpdated',
859
            'sortKey',
860
            'open',
861
            'status'
862
        );
863
        $changeSet = $this->unsetKeys($changeSet, $keysToUnset);
864
865
        // Take care of tracking ids
866
        if (isset($changeSet['trackingIds']) === true) {
867
            $this->proceedTrackingIds($changeSet['id'], $changeSet['trackingIds']);
868
            $changeSet = $this->unsetKeys($changeSet, array('trackingIds'));
869
        }
870
871
        // One changeset can have n patchsets. So do the same as the level above
872
        // Loop over the patchsets and import one for one
873
        foreach ($changeSet['patchSets'] as $patchSet) {
874
            $this->proceedPatchset($patchSet, $changeSet);
875
        }
876
        $changeSet = $this->unsetKeys($changeSet, array('patchSets'));
877
878
        // Take care of current patch set
879
        $currentPatchSetId = 0;
880
        if (isset($changeSetRow['current_patchset']) === true) {
881
            $currentPatchSetId = $changeSetRow['current_patchset'];
882
        }
883
        $this->proceedCurrentPatchSet($changeSet, $currentPatchSetId);
884
        $changeSet = $this->unsetKeys($changeSet, array('currentPatchSet'));
885
886
        // Take care of "dependsOn" and "neededBy"
887
        $this->proceedChangeSetsDependsOnAndNeededBy($changeSet);
888
        $changeSet = $this->unsetKeys($changeSet, array('dependsOn', 'neededBy'));
889
890
        // Take care of "submitRecords"
891
        $this->proceedSubmitRecords($changeSet);
892
        unset($changeSet['submitRecords']);
893
894
        // Sometimes a changeSet does not get any comments.
895
        // In this case, the comments key does not exist and we can skip it.
896
        if (isset($changeSet['comments']) === true) {
897
            foreach ($changeSet['comments'] as $key => $comment) {
898
                // Yep, i know. To trust the order of comments from an external API (Gerrit) is not very common.
899
                // But we need the "key" to save "duplicate" comments as well.
900
                // Otherwise we do not get all data.
901
                // e.g. https://review.typo3.org/#/c/564/
902
                // In this case Robert Lemke posted the same comment in the same second twice.
903
                // And we want this data as well, to get correct statistics.
904
                // We calculate +1 to the key, because in an array, php starts with 0
905
                // but in human thinnking, all stuff starts with 1.
906
                // So the first comment won`t be comment #0, it will be comment #1
907
                $this->proceedComment($comment, $changeSet, ($key + 1));
908
            }
909
            $changeSet = $this->unsetKeys($changeSet, array('comments'));
910
        }
911
912
        $changeSet = $this->unsetKeys($changeSet, array('id'));
913
914
        $this->checkIfAllValuesWereProceeded($changeSet, 'Change set');
915
916
        unset($changeSet);
917
    }
918
919
    /**
920
     * Proceeds the 'submitRecords' key of a changeset
921
     *
922
     * @param array $changeSet Current changeset
923
     * @return void
924
     */
925
    protected function proceedSubmitRecords(array $changeSet)
926
    {
927
        if (is_array($changeSet['submitRecords'] === false)) {
928
            return;
929
        }
930
931
        foreach ($changeSet['submitRecords'] as $submitRecord) {
932
            $submitRecordData = array(
933
                'changeset' => $changeSet['id'],
934
                'status' => $submitRecord['status'],
935
            );
936
937
            $wherePart = array('changeset' => $changeSet['id']);
938
            $submitRecordRow = $this->getLookupTableValues(Database::TABLE_SUBMIT_RECORDS, array('id'), $wherePart);
939
            if ($submitRecordRow === false) {
940
                $id = $this->getDatabase()->insertRecord(Database::TABLE_SUBMIT_RECORDS, $submitRecordData);
941
942
            } else {
943
                $id = $submitRecordRow['id'];
944
                $this->getDatabase()->updateRecord(Database::TABLE_SUBMIT_RECORDS, $submitRecordData, $submitRecordRow['id']);
945
            }
946
947
            $submitRecord = $this->unsetKeys($submitRecord, array('status'));
948
949
            if (is_array($submitRecord['labels']) === true) {
950
                $this->proceedSubmitRecordLabels($id, $submitRecord['labels']);
951
                $submitRecord = $this->unsetKeys($submitRecord, array('labels'));
952
            }
953
954
            $this->checkIfAllValuesWereProceeded($submitRecord, 'Submit record');
955
        }
956
    }
957
958
    /**
959
     * Proceed the labels of a 'submitRecord' key of a changeset
960
     *
961
     * @param int $submitRecordId
962
     * @param array $submitRecordLabels
963
     * @return void
964
     */
965
    protected function proceedSubmitRecordLabels($submitRecordId, array $submitRecordLabels)
966
    {
967
        foreach ($submitRecordLabels as $labelInfo) {
968
            // There must be no author of a label, let set a default one
969
            $by = array('id' => 0);
970
            if (array_key_exists('by', $labelInfo) === true) {
971
                $by = $this->proceedPerson($labelInfo['by']);
972
            }
973
974
            $submitRecordLabelRow = $this->getGerritSubmitRecordLabelByIdentifier($submitRecordId, $labelInfo['label']);
975
976
            $submitRecordLabel = array(
977
                'submit_record' => $submitRecordId,
978
                'label' => $labelInfo['label'],
979
                'status' => $labelInfo['status'],
980
                'by' => $by['id']
981
            );
982
            if ($submitRecordLabelRow === false) {
983
                $this->getDatabase()->insertRecord(Database::TABLE_SUBMIT_RECORD_LABELS, $submitRecordLabel);
984
985
            } else {
986
                $this->getDatabase()->updateRecord(
987
                    Database::TABLE_SUBMIT_RECORD_LABELS,
988
                    $submitRecordLabelRow,
989
                    $submitRecordLabelRow['id']
990
                );
991
            }
992
993
            $labelInfo = $this->unsetKeys($labelInfo, array('label', 'status', 'by'));
994
            $this->checkIfAllValuesWereProceeded($labelInfo, 'Submit record label');
995
        }
996
    }
997
998
    /**
999
     * Proceed "dependsOn" and "neededBy" for a single changeset.
1000
     * This is a little bit tricky, because there can be a dependsOn
1001
     * reference to a changeset which is not imported yet.
1002
     *
1003
     * Solution: A temp table
1004
     * We insert all "dependsOn" and "neededBy" records into one temp table.
1005
     * After we finished all changesets for this project, we got all changesets
1006
     * for our "dependsOn" and "neededBy" mapping, too.
1007
     * Then we update the changesets table :)
1008
     *
1009
     * "dependsOn" and "neededBy" ... WHAT?!
1010
     * If you don`t know what this is, have a look at https://review.typo3.org/#/c/17997/ into the dependency section
1011
     *
1012
     * @see $this->proceedChangeSetsDependsOnRelation()
1013
     *
1014
     * @param array $changeSet
1015
     * @return void
1016
     */
1017
    protected function proceedChangeSetsDependsOnAndNeededBy(array $changeSet)
1018
    {
1019
        $keysToUnset = array('id', 'number', 'revision', 'ref');
1020
1021
        // Take care of neededBy
1022
        if (isset($changeSet['neededBy']) === true) {
1023
            foreach ($changeSet['neededBy'] as $neededBy) {
1024
                $neededByData = array(
1025
                    'changeset' => $changeSet['id'],
1026
                    'identifier' => $neededBy['id'],
1027
                    'number' => $neededBy['number'],
1028
                    'revision' => $neededBy['revision'],
1029
                    'ref' => $neededBy['ref'],
1030
                    'status' => Database::TMP_DEPENDS_NEEDED_STATUS_NEEDEDBY
1031
                );
1032
                $this->getDatabase()->insertRecord(Database::TABLE_TMP_DEPENDS_NEEDED, $neededByData);
1033
1034
                $neededBy = $this->unsetKeys($neededBy, $keysToUnset);
1035
                $this->checkIfAllValuesWereProceeded($neededBy, 'neededBy');
1036
            }
1037
1038
            unset($neededBy);
1039
        }
1040
1041
        // Take care of dependsOn
1042
        if (isset($changeSet['dependsOn']) === true) {
1043
1044
            $keysToUnset[] = 'isCurrentPatchSet';
1045
1046
            foreach ($changeSet['dependsOn'] as $dependsOn) {
1047
                $dependsOnData = array(
1048
                    'changeset' => $changeSet['id'],
1049
                    'identifier' => $dependsOn['id'],
1050
                    'number' => $dependsOn['number'],
1051
                    'revision' => $dependsOn['revision'],
1052
                    'ref' => $dependsOn['ref'],
1053
                    'is_current_patchset' => (int) $dependsOn['isCurrentPatchSet'],
1054
                    'status' => Database::TMP_DEPENDS_NEEDED_STATUS_DEPENDSON
1055
                );
1056
                $this->getDatabase()->insertRecord(Database::TABLE_TMP_DEPENDS_NEEDED, $dependsOnData);
1057
1058
                $dependsOn = $this->unsetKeys($dependsOn, $keysToUnset);
1059
                $this->checkIfAllValuesWereProceeded($dependsOn, 'dependsOn');
1060
            }
1061
1062
            unset($dependsOn);
1063
        }
1064
    }
1065
1066
    /**
1067
     * Updates the current patch set for this changeset if necessary.
1068
     *
1069
     * @param array $changeSet The changeSet from Gerrit
1070
     * @param int $currentPatchSetId The current patchset id from our database
1071
     * @return void
1072
     */
1073
    protected function proceedCurrentPatchSet(array $changeSet, $currentPatchSetId)
1074
    {
1075
        $currentPatchSet = $changeSet['currentPatchSet'];
1076
        $patchSetRow = $this->getGerritPatchsetByIdentifier(
1077
            $changeSet['id'],
1078
            $currentPatchSet['number'],
1079
            $currentPatchSet['revision'],
1080
            $currentPatchSet['createdOn']
1081
        );
1082
1083
        // If the current currentPatchSet in database not equal the patchset from Gerrit, update it
1084
        if ($patchSetRow['id'] != $currentPatchSetId) {
1085
            $updateData = array('current_patchset' => $patchSetRow['id']);
1086
            $this->getDatabase()->updateRecord(Database::TABLE_CHANGESET, $updateData, $changeSet['id']);
1087
        }
1088
    }
1089
1090
    /**
1091
     * Unsets an amount of keys in given $data
1092
     *
1093
     * TODO If every API source / data set will be transformed, this function is not necessary anymore
1094
     * See Gerrie\Transformer\DebugTransformDecorator
1095
     *
1096
     * @param array $data Data array where the keys will be unset
1097
     * @param array $keyList List of keys which will be unset
1098
     * @return array
1099
     */
1100
    public function unsetKeys(array $data, array $keyList)
1101
    {
1102
        foreach ($keyList as $key) {
1103
            if (isset($data[$key]) === true) {
1104
                unset($data[$key]);
1105
            }
1106
        }
1107
1108
        return $data;
1109
    }
1110
1111
    /**
1112
     * Proceeds a single comment.
1113
     *
1114
     * Comments are not updateable. So there is no need to provide an update mechanism here.
1115
     *
1116
     * @param array $comment The current comment
1117
     * @param array $changeSet The current changeset
1118
     * @param integer $key The $key value of the foreach loop of comments (order number)
1119
     * @return void
1120
     */
1121
    protected function proceedComment(array $comment, array $changeSet, $key)
1122
    {
1123
        $reviewer = $this->proceedPerson($comment['reviewer']);
1124
1125
        // A comment don`t have a unique identifier, so we must generate an ID on our own.
1126
        // Changeset Id + Reviewer ID + Key + Timestamp. This combination is "unique enough".
1127
        // Is this comment already in database?
1128
        $commentRecord = $this->getGerritCommentByIdentifier(
1129
            $changeSet['id'],
1130
            $reviewer['id'],
1131
            $comment['timestamp'],
1132
            $key
1133
        );
1134
        if ($commentRecord === false) {
1135
            $commentData = array(
1136
                'changeset' => $changeSet['id'],
1137
                'timestamp' => $comment['timestamp'],
1138
                'reviewer' => $reviewer['id'],
1139
                'message' => $comment['message'],
1140
                'number' => $key
1141
            );
1142
            $this->getDatabase()->insertRecord(Database::TABLE_COMMENT, $commentData);
1143
1144
            $comment = $this->unsetKeys($comment, array('timestamp', 'reviewer', 'message'));
1145
            $this->checkIfAllValuesWereProceeded($comment, 'Comment');
1146
        }
1147
    }
1148
1149
    /**
1150
     * Proceeds a single patchset.
1151
     * One patchset can have n approvals, comments and so on.
1152
     *
1153
     * @param array $patchset The current patchset
1154
     * @param array $changeSet The current changeset
1155
     * @return void
1156
     */
1157
    protected function proceedPatchset(array $patchset, array $changeSet)
1158
    {
1159
1160
        // A patchset don`t have a unique identifier, so have to combine different fields
1161
        // (revision is not unique in Gerrit): ChangeSet-Id + Patchset number + Patchset revision + Patchset created on
1162
        // This combination is "unique enough".
1163
        // We have to include the number here, because in some cases, the revision and created on timestamp is the same
1164
        // e.g. http://review.typo3.org/9221
1165
        // Is this patchset already in database?
1166
        $patchSetRow = $this->getGerritPatchsetByIdentifier(
1167
            $changeSet['id'],
1168
            $patchset['number'],
1169
            $patchset['revision'],
1170
            $patchset['createdOn']
1171
        );
1172
        if ($patchSetRow === false) {
1173
            $uploader = $this->proceedPerson($patchset['uploader']);
1174
1175
            $author = array('id' => 0);
1176
            if (array_key_exists('author', $patchset) === true) {
1177
                $author = $this->proceedPerson($patchset['author']);
1178
            }
1179
1180
            $patchSetData = array(
1181
                'changeset' => $changeSet['id'],
1182
                'number' => $patchset['number'],
1183
                'revision' => $patchset['revision'],
1184
                'ref' => $patchset['ref'],
1185
                'uploader' => $uploader['id'],
1186
                'author' => $author['id'],
1187
                'size_insertions' => $patchset['sizeInsertions'],
1188
                'size_deletions' => $patchset['sizeDeletions'],
1189
                'is_draft' => ((isset($patchset['isDraft']) === true) ? (int) $patchset['isDraft']: 0),
1190
                'created_on' => $patchset['createdOn'],
1191
            );
1192
            $patchset['id'] = $this->getDatabase()->insertRecord(Database::TABLE_PATCHSET, $patchSetData);
1193
1194
            // Import files per patchset
1195
            $this->proceedFiles($patchset);
1196
1197
            // A pushed patchset can`t be updated
1198
            // So, we do not have to provide an update mechanism for patchsets.
1199
        } else {
1200
1201
            $this->checkIfServersFirstRun('Patchset', 1363893175, array($patchset, $patchSetRow));
1202
            $patchset['id'] = $patchSetRow['id'];
1203
        }
1204
1205
        $patchset = $this->unsetKeys($patchset, array('files'));
1206
1207
        // Unset not needed keys, because a) memory and b) to check if there are keys which were not imported :)
1208
        $keysToDelete = array(
1209
            'number',
1210
            'revision',
1211
            'ref',
1212
            'uploader',
1213
            'author',
1214
            'sizeInsertions',
1215
            'sizeDeletions',
1216
            'isDraft',
1217
            'createdOn',
1218
            'kind'
1219
        );
1220
        $patchset = $this->unsetKeys($patchset, $keysToDelete);
1221
1222
        // We need to set all approvals for this patchset as 'voted_earlier' => 1
1223
        // Because a user can vote both values (Code Review and Verified).
1224
        // After some time, the user can set one value e.g. Code Review back to value 0
1225
        // In this case we do not get the Code Review back from API
1226
        // If we do not set all approvals to 'voted_earlier' => 1
1227
        // we store approvals which are not active anymore
1228
        $updateData = array('voted_earlier' => 1);
1229
        $where = '`patchset` = ' . intval($patchset['id']);
1230
        $this->getDatabase()->updateRecords(Database::TABLE_APPROVAL, $updateData, $where);
1231
1232
        // Sometimes a patchset does not get any approval.
1233
        // In this case, the approvals key does not exist and we can skip it.
1234
        // e.g. https://review.typo3.org/#/c/16924/
1235
        if (isset($patchset['approvals']) === true) {
1236
            // One patchset can have n approvals. So do the same as the level above
1237
            // Loop over the approvals and import one for one
1238
            foreach ($patchset['approvals'] as $approval) {
1239
                $this->proceedApproval($approval, $patchset);
1240
            }
1241
1242
            $patchset = $this->unsetKeys($patchset, array('approvals'));
1243
        }
1244
1245
        // @todo implement 'parents'
1246
        $patchset = $this->unsetKeys($patchset, array('parents'));
1247
1248
        // Take care of comments in files
1249
        if (isset($patchset['comments']) === true) {
1250
            $this->proceedFileComments($patchset);
1251
            $patchset = $this->unsetKeys($patchset, array('comments'));
1252
        }
1253
1254
        $patchset = $this->unsetKeys($patchset, array('id'));
1255
1256
        $this->checkIfAllValuesWereProceeded($patchset, 'Patch set');
1257
1258
        unset($patchset);
1259
    }
1260
1261
    /**
1262
     * Proceeds the comments of a single patchset.
1263
     * One patchset can have n comments.
1264
     *
1265
     * @param array $patchset The current patchset
1266
     * @return void
1267
     */
1268
    protected function proceedFileComments(array $patchset)
1269
    {
1270
        /**
1271
         * To import the file comments there are (maybe) some data which is not very correct.
1272
         * For example comments can be deleted later on.
1273
         * This information is not deliviered by the API.
1274
         * The same for comments marked as "Done".
1275
         *
1276
         * Sad, but true.
1277
         * But hey! Some comments are better than nothing.
1278
         * During analysis the comments you have to take care this in mind.
1279
         */
1280
1281
        foreach ($patchset['comments'] as $comment) {
1282
1283
            // Take care of reviewer
1284
            $comment['reviewer'] = $this->proceedPerson($comment['reviewer']);
1285
1286
            // Search the file id
1287
            $whereParts = array(
1288
                'patchset' => $patchset['id'],
1289
                'file' => $comment['file']
1290
            );
1291
            $fileRow = $this->getLookupTableValues(Database::TABLE_FILES, array('id'), $whereParts);
1292
1293
            // Sometimes we got comments to files, which are not part of the current patchset.
1294
            // See https://review.typo3.org/#/c/22107/3 for example.
1295
            // File "Classes/BackEnd/Ajax.php" is not part of the 3rd patchset.
1296
            // But Oliver Klee commented this file, because this file was part of the 1st patchset.
1297
            // This file was not part of the 2nd patchset either.
1298
            // In this case "file" would be NULL during database insert and an exception would be thrown:
1299
            // [Exception] Column 'file' cannot be null (1048)
1300
            //
1301
            // Okay we got two options to solve this.
1302
            // 1. We ignore the comment and loss the information
1303
            // 2. Create a new file action "COMMENTED", insert this file with the new type and insert the comment
1304
            // We will choose option 2, because we do not accept data loss if we can store it.
1305
            //
1306
            // If you are reading this and got a better solution, contact us
1307
            // We would be happy to see your way
1308
            if ($fileRow === false) {
1309
                // Take care of file action
1310
                $type = $this->proceedLookupTable(Database::TABLE_FILEACTION, 'id', 'name', 'COMMENTED');
1311
1312
                $fileData = array(
1313
                    'patchset' => $patchset['id'],
1314
                    'file' => $comment['file'],
1315
                    'file_old' => '',
1316
                    'insertions' => 0,
1317
                    'deletions' => 0,
1318
                    'type' => $type
1319
                );
1320
                $fileRow['id'] = $this->getDatabase()->insertRecord(Database::TABLE_FILES, $fileData);
1321
            }
1322
1323
            $commentRow = array(
1324
                'patchset' => $patchset['id'],
1325
                'file' => $fileRow['id'],
1326
                'line' => $comment['line'],
1327
                'reviewer' => $comment['reviewer']['id'],
1328
                'message' => $comment['message'],
1329
                'message_crc32' => crc32($comment['message'])
1330
            );
1331
1332
            // Attention: One person can comment twice at the same line.
1333
            // We do not get any timestamp :(
1334
            // e.g. see https://review.typo3.org/#/c/11758/1/ExtendingSphinxForTYPO3/build/.gitignore,unified
1335
            // To be "unique enough" we add a CRC32 checksum of the message
1336
            $fileCommentRow = $this->getGerritFileCommentByIdentifier(
1337
                $patchset['id'],
1338
                $fileRow['id'],
1339
                $comment['line'],
1340
                $comment['reviewer']['id'],
1341
                $commentRow['message_crc32']
1342
            );
1343
            if ($fileCommentRow === false) {
1344
                $this->getDatabase()->insertRecord(Database::TABLE_FILE_COMMENTS, $commentRow);
1345
            }
1346
1347
            // Normally we would update the comment here (in a else part).
1348
            // But in this data we got from the API there is nothing to update.
1349
            // Sometimes the same comment in the same file in the same line by the same person was created
1350
            // Sadly we do not get a timestamp or something.
1351
            // This is the reason why there is no else part + no checkIfServersFirstRun check
1352
            // We just ignore duplicates here.
1353
            // I know that this might be dangerous, because we can miss comments, but until there is no additional
1354
            // identifier this can be difficult
1355
            // One idea is that the number of comment will be introduces in the select check (with a simple $i var)
1356
            // But this needs further testing if the order of the comments is always the same
1357
            // TODO check this
1358
1359
            $comment = $this->unsetKeys($comment, array('file', 'line', 'reviewer', 'message'));
1360
1361
            $this->checkIfAllValuesWereProceeded($comment, 'File comment');
1362
        }
1363
    }
1364
1365
    /**
1366
     * Method to check if all data were imported.
1367
     * Normally, this export script unsets the exported value after proceeding.
1368
     *
1369
     * If there are values left in the array, there could be
1370
     * a) a bug, because the value is not unsetted
1371
     * b) a change in the Gerrit server API
1372
     * c) a bug, because not all values are exported / proceeded
1373
     *
1374
     * This methods help to detect this :)
1375
     *
1376
     * TODO If every API source / data set will be transformed, this function is not necessary anymore
1377
     * See Gerrie\Transformer\DebugTransformDecorator
1378
     *
1379
     * @param array $data Data to inspect
1380
     * @param string $level Level of data. e.g. patchset, comment, etc.
1381
     * @throws \Exception
1382
     */
1383
    protected function checkIfAllValuesWereProceeded(array $data, $level)
1384
    {
1385
        if (count($data) > 0) {
1386
            var_dump($data);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($data); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
1387
            $message = 'Not all values were proceeded / exported. Please have a look at "' . $level . '"';
1388
            throw new \Exception($message, 1363894644);
1389
        }
1390
    }
1391
1392
    /**
1393
     * Imports files per patchset
1394
     *
1395
     * @param array $patchset The current patchset
1396
     * @return void
1397
     */
1398
    protected function proceedFiles(array $patchset)
1399
    {
1400
        if (array_key_exists('files', $patchset) === false || is_array($patchset['files']) === false) {
1401
            return;
1402
        }
1403
1404
        foreach ($patchset['files'] as $file) {
1405
            // Take care of file action
1406
            $type = $this->proceedLookupTable(Database::TABLE_FILEACTION, 'id', 'name', $file['type']);
1407
1408
            $fileData = array(
1409
                'patchset' => $patchset['id'],
1410
                'file' => $file['file'],
1411
                'file_old' => ((array_key_exists('fileOld', $file) === true) ? $file['fileOld'] : ''),
1412
                'insertions' => $file['insertions'],
1413
                'deletions' => $file['deletions'],
1414
                'type' => $type
1415
            );
1416
            $this->getDatabase()->insertRecord(Database::TABLE_FILES, $fileData);
1417
1418
            $file = $this->unsetKeys($file, array('file', 'fileOld', 'type', 'insertions', 'deletions'));
1419
            $this->checkIfAllValuesWereProceeded($file, 'File');
1420
        }
1421
    }
1422
1423
    /**
1424
     * Will proceed a single approval.
1425
     *
1426
     * @param array $approval The current approval
1427
     * @param array $patchset The current patchset
1428
     * @return void
1429
     */
1430
    protected function proceedApproval(array $approval, array $patchset)
1431
    {
1432
        $by = $this->proceedPerson($approval['by']);
1433
1434
        $approvalData = array(
1435
            'patchset' => $patchset['id'],
1436
            'type' => $approval['type'],
1437
            'description' => ((isset($approval['description']) === true) ? $approval['description'] : ''),
1438
            'value' => $approval['value'],
1439
            'granted_on' => $approval['grantedOn'],
1440
            'by' => $by['id'],
1441
            'voted_earlier' => 0
1442
        );
1443
1444
        // An approval don`t have a unique identifier, so we must generate one on our own.
1445
        // Patch-ID, type and user. This combination is "unique enough".
1446
        // Is this approval already in database?
1447
        $approvalRow = $this->getGerritApprovalByIdentifier($patchset['id'], $approval['type'], $by['id']);
1448
        if ($approvalRow === false) {
1449
            $this->getDatabase()->insertRecord(Database::TABLE_APPROVAL, $approvalData);
1450
1451
            // We know this approval. Just update it!
1452
        } else {
1453
            $this->checkIfServersFirstRun('Approval', 1363897318, array($approval, $approvalRow));
1454
1455
            $this->getDatabase()->updateRecord(Database::TABLE_APPROVAL, $approvalData, $approvalRow['id']);
1456
        }
1457
1458
        $approval = $this->unsetKeys($approval, array('type', 'description', 'value', 'grantedOn', 'by'));
1459
        $this->checkIfAllValuesWereProceeded($approval, 'Approval');
1460
1461
        unset($approval);
1462
    }
1463
1464
    /**
1465
     * Handles the import or update process of a single person.
1466
     * A person can be determined by name, an e-mail-address or by username.
1467
     *
1468
     * @param array $person The current person
1469
     * @return int ID of person
1470
     */
1471
    protected function proceedPerson(array $person)
1472
    {
1473
1474
        // If "Gerrit Code Review" posted some problems, e.g. path conflicts (e.g. https://review.typo3.org/#/c/4553/)
1475
        // there is no person information.
1476
        // Set it here, because otherwise we got empty persons ;)
1477
        if (array_key_exists('name', $person) === false &&
1478
            array_key_exists('email', $person) === false &&
1479
            array_key_exists('username', $person) === false
1480
        ) {
1481
1482
            $person['name'] = 'Unknown (Exporter)';
1483
            $person['email'] = '[email protected]';
1484
            $person['username'] = 'Unknown_export_username';
1485
        }
1486
1487
        // Sometimes the person got no name.
1488
        // We have to set a default one
1489
        if (array_key_exists('name', $person) === false) {
1490
            $person['name'] = 'Unknown (Exporter)';
1491
        }
1492
1493
        // Sometimes you got an action by "Gerrit Code Review".
1494
        // This "system user" does not have a username. Sad, isn`t ?
1495
        // We got a present for him / her. A default username :)
1496
        // e.g. https://review.typo3.org/#/c/4553/
1497
        if ($person['name'] === 'Gerrit Code Review') {
1498
            $person['username'] = 'Gerrit';
1499
        }
1500
1501
        // Sometimes the API does not return an email
1502
        $email = '';
1503
        $emailPerson = false;
1504
        if (array_key_exists('email', $person) !== false) {
1505
            $email = $person['email'];
1506
            $emailPerson = $this->getPersonBy('email', $person['email']);
1507
        }
1508
1509
        if ($emailPerson === false) {
1510
1511
            $personByName = false;
1512
            if ($person['username']) {
1513
                $personByName = $this->getPersonBy('username', $person['username']);
1514
1515
            } elseif ($person['name']) {
1516
                $personByName = $this->getPersonBy('name', $person['name']);
1517
            }
1518
1519
            // If a person does not exist, create a new one.
1520
            if ($personByName === false) {
1521
                $personData = array(
1522
                    'name' => $person['name'],
1523
                    'username' => $person['username']
1524
                );
1525
                $person['id'] = $this->getDatabase()->insertRecord(Database::TABLE_PERSON, $personData);
1526
1527
                $emailData = array(
1528
                    'person' => $person['id'],
1529
                    'email' => $email
1530
                );
1531
                $this->getDatabase()->insertRecord(Database::TABLE_EMAIL, $emailData);
1532
1533
                // Person exists, but has a new e-mail. Just add the e-mail-address to this person
1534
            } else {
1535
                $person['id'] = $personByName['id'];
1536
                $emailData = array(
1537
                    'person' => $personByName['id'],
1538
                    'email' => $email
1539
                );
1540
                $this->getDatabase()->insertRecord(Database::TABLE_EMAIL, $emailData);
1541
            }
1542
        } else {
1543
            $person['id'] = $emailPerson['id'];
1544
        }
1545
1546
        return $person;
1547
    }
1548
1549
    /**
1550
     * Determines the last SortKey of the Gerrit System.
1551
     * This is necessary to get a "pointer" to continue the data mining.
1552
     * Because it is not common to get ALL data with ONE request.
1553
     * a) There is a "limit" configured in Gerrit
1554
     * b) The memory of your server / computer is limited, too ;)
1555
     *
1556
     * @see https://review.typo3.org/Documentation/cmd-query.html
1557
     *
1558
     * @param array $data changeset data whoch was queried by Gerrit earlier
1559
     * @return string
1560
     */
1561
    protected function getLastSortKey(array $data)
1562
    {
1563
        $lastChangeSet = $this->transferJsonToArray(array_pop($data));
1564
1565
        return (isset($lastChangeSet['sortKey']) === true) ? $lastChangeSet['sortKey'] : null;
1566
    }
1567
1568
    /**
1569
     * Returns a person by $mode (email or username)
1570
     *
1571
     * @param string $mode 'email' or 'username'
1572
     * @param string $value An email adress or an username
1573
     * @return mixed
1574
     * @throws \Exception
1575
     */
1576
    protected function getPersonBy($mode, $value)
1577
    {
1578
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1579
1580
        switch ($mode) {
1581
            case 'email':
1582
                $query = '
1583
                SELECT
1584
                    person.`id`,
1585
                    person.`name`,
1586
                    person.`username`,
1587
                    email.`email`
1588
                  FROM ' . Database::TABLE_EMAIL . ' email
1589
                  INNER JOIN ' . Database::TABLE_PERSON . ' person ON (
1590
                    email.`person` = person.`id`
1591
                  )
1592
                  WHERE email.`email` = :value';
1593
                break;
1594
            case 'username':
1595
                $query = 'SELECT `id`, `name`, `username`
1596
                  FROM ' . Database::TABLE_PERSON . '
1597
                  WHERE `username` = :value';
1598
                break;
1599
            case 'name':
1600
                $query = 'SELECT `id`, `name`, `username`
1601
                  FROM ' . Database::TABLE_PERSON . '
1602
                  WHERE `name` = :value';
1603
                break;
1604
            default:
1605
                throw new \Exception('Wrong mode selected!', 1363897547);
1606
        }
1607
1608
        $statement = $dbHandle->prepare($query);
1609
1610
        $statement->bindParam(':value', $value, \PDO::PARAM_STR);
1611
        $executeResult = $statement->execute();
1612
1613
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1614
        return $statement->fetch(\PDO::FETCH_ASSOC);
1615
    }
1616
1617
    /**
1618
     * Returns all projects by server id
1619
     *
1620
     * @param int $serverId Server if of Gerrit server
1621
     * @return array
1622
     */
1623
    protected function getGerritProjectsByServerId($serverId)
1624
    {
1625
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1626
1627
        $query = 'SELECT `id`, `name` FROM ' . Database::TABLE_PROJECT . '
1628
                  WHERE `server_id` = :server_id';
1629
        $statement = $dbHandle->prepare($query);
1630
1631
        $statement->bindParam(':server_id', $serverId, \PDO::PARAM_INT);
1632
        $executeResult = $statement->execute();
1633
1634
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1635
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1636
    }
1637
1638
    /**
1639
     * Returns all projects from our database for given server id with given names.
1640
     *
1641
     * @param int $serverId Gerrit server id
1642
     * @param array $names Names who are looking for
1643
     * @return array
1644
     */
1645
    protected function getGerritProjectsByName($serverId, array $names)
1646
    {
1647
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1648
        $nameList = implode(',', $names);
1649
1650
        $query = 'SELECT `id`, `name` FROM ' . Database::TABLE_PROJECT . '
1651
                  WHERE `server_id` = :server_id AND FIND_IN_SET(BINARY `name`, :names) > 0';
1652
        $statement = $dbHandle->prepare($query);
1653
1654
        $statement->bindParam(':server_id', $serverId, \PDO::PARAM_INT);
1655
        $statement->bindParam(':names', $nameList, \PDO::PARAM_STR);
1656
        $executeResult = $statement->execute();
1657
1658
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1659
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
1660
    }
1661
1662
    /**
1663
     * Return a single project from our database for given server id with given project id.
1664
     *
1665
     * @param int $serverId Gerrit server id
1666
     * @param int $projectId Id of Gerrit project
1667
     * @return array
1668
     */
1669
    public function getGerritProjectById($serverId, $projectId)
1670
    {
1671
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1672
1673
        $query = 'SELECT `id`, `name` FROM ' . Database::TABLE_PROJECT . '
1674
                  WHERE `server_id` = :server_id AND `id` = :id';
1675
        $statement = $dbHandle->prepare($query);
1676
1677
        $statement->bindParam(':server_id', $serverId, \PDO::PARAM_INT);
1678
        $statement->bindParam(':id', $projectId, \PDO::PARAM_INT);
1679
        $executeResult = $statement->execute();
1680
1681
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1682
        return $statement->fetch(\PDO::FETCH_ASSOC);
1683
    }
1684
1685
    /**
1686
     * Returns a approval by the unique identifier
1687
     *
1688
     * @param int $patchSetId ID of a record from the patchset table
1689
     * @param string $type Type of the approval
1690
     * @param int $by Person of the approval
1691
     * @return mixed
1692
     */
1693
    protected function getGerritApprovalByIdentifier($patchSetId, $type, $by)
1694
    {
1695
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1696
1697
        $query = 'SELECT `id`, `patchset`, `type`, `description`, `value`, `granted_on`, `by`
1698
                  FROM ' . Database::TABLE_APPROVAL . '
1699
                  WHERE `patchset` = :patchset
1700
                        AND `type` = :type
1701
                        AND `by` = :by';
1702
        $statement = $dbHandle->prepare($query);
1703
1704
        $statement->bindParam(':patchset', $patchSetId, \PDO::PARAM_INT);
1705
        $statement->bindParam(':type', $type, \PDO::PARAM_STR);
1706
        $statement->bindParam(':by', $by, \PDO::PARAM_INT);
1707
        $executeResult = $statement->execute();
1708
1709
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1710
        return $statement->fetch(\PDO::FETCH_ASSOC);
1711
    }
1712
1713
    /**
1714
     * Returns a patchset by the unique identifier
1715
     *
1716
     * @param int $changeSetId ID of a record from the changeset table
1717
     * @param int $number Number of the patch set
1718
     * @param string $revision Revision of the patch set
1719
     * @param int $createdOn Timestamp of creation time
1720
     * @return mixed
1721
     */
1722
    protected function getGerritPatchsetByIdentifier($changeSetId, $number, $revision, $createdOn)
1723
    {
1724
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1725
1726
        $query = 'SELECT `id`, `changeset`, `number`, `revision`, `ref`, `uploader`, `created_on`
1727
                  FROM ' . Database::TABLE_PATCHSET . '
1728
                  WHERE `changeset` = :changeset
1729
                        AND `number` = :number
1730
                        AND `revision` = :revision
1731
                        AND `created_on` = :created_on';
1732
        $statement = $dbHandle->prepare($query);
1733
1734
        $statement->bindParam(':changeset', $changeSetId, \PDO::PARAM_INT);
1735
        $statement->bindParam(':number', $number, \PDO::PARAM_INT);
1736
        $statement->bindParam(':revision', $revision, \PDO::PARAM_STR);
1737
        $statement->bindParam(':created_on', $createdOn, \PDO::PARAM_INT);
1738
        $executeResult = $statement->execute();
1739
1740
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1741
        return $statement->fetch(\PDO::FETCH_ASSOC);
1742
    }
1743
1744
    /**
1745
     * Returns a submit record label by unique identifier
1746
     *
1747
     * @param int $submitRecordId ID of submit record
1748
     * @param string $label Label of submit record label
1749
     * @return mixed
1750
     */
1751
    protected function getGerritSubmitRecordLabelByIdentifier($submitRecordId, $label)
1752
    {
1753
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1754
1755
        $query = 'SELECT `id`, `submit_record`, `label`, `status`, `by`
1756
                  FROM ' . Database::TABLE_SUBMIT_RECORD_LABELS . '
1757
                  WHERE `submit_record` = :submit_record_id
1758
                        AND `label` = :label';
1759
        $statement = $dbHandle->prepare($query);
1760
1761
        $statement->bindParam(':submit_record_id', $submitRecordId, \PDO::PARAM_INT);
1762
        $statement->bindParam(':label', $label, \PDO::PARAM_STR);
1763
        $executeResult = $statement->execute();
1764
1765
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1766
        return $statement->fetch(\PDO::FETCH_ASSOC);
1767
    }
1768
1769
    /**
1770
     * Returns a file comment by unique identifier
1771
     *
1772
     * @param int $patchSetId Unique identifier of a patchset
1773
     * @param int $file Unique identifier of a file
1774
     * @param int $line Line no of file
1775
     * @param int $reviewer Unique identifier of the reviewer (person)
1776
     * @param int $messageCrc32 CRC32 of the comment
1777
     * @return mixed
1778
     */
1779
    protected function getGerritFileCommentByIdentifier($patchSetId, $file, $line, $reviewer, $messageCrc32)
1780
    {
1781
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1782
1783
        $query = 'SELECT `id`, `patchset`, `file`, `line`, `reviewer`, `message`
1784
                  FROM ' . Database::TABLE_FILE_COMMENTS . '
1785
                  WHERE `patchset` = :patchset
1786
                        AND `file` = :file
1787
                        AND `line` = :line
1788
                        AND `reviewer` = :reviewer
1789
                        AND `message_crc32` = :message_crc32';
1790
        $statement = $dbHandle->prepare($query);
1791
1792
        $statement->bindParam(':patchset', $patchSetId, \PDO::PARAM_INT);
1793
        $statement->bindParam(':file', $file, \PDO::PARAM_INT);
1794
        $statement->bindParam(':line', $line, \PDO::PARAM_INT);
1795
        $statement->bindParam(':reviewer', $reviewer, \PDO::PARAM_INT);
1796
        $statement->bindParam(':message_crc32', $messageCrc32, \PDO::PARAM_INT);
1797
        $executeResult = $statement->execute();
1798
1799
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1800
        return $statement->fetch(\PDO::FETCH_ASSOC);
1801
    }
1802
1803
    /**
1804
     * Returns a Comment by the unique identifier
1805
     *
1806
     * @param int $changeSetId ID of a record from the changeset table
1807
     * @param int $timestamp Timestamp of the comment
1808
     * @param int $reviewer Reviewer of the comment
1809
     * @param int $number Number (order number) of the comment
1810
     * @return mixed
1811
     */
1812
    protected function getGerritCommentByIdentifier($changeSetId, $reviewer, $timestamp, $number)
1813
    {
1814
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1815
1816
        $query = 'SELECT `id`, `changeset`, `timestamp`, `reviewer`, `message`
1817
                  FROM ' . Database::TABLE_COMMENT . '
1818
                  WHERE `changeset` = :changeset
1819
                        AND `timestamp` = :timestamp
1820
                        AND `reviewer` = :reviewer
1821
                        AND `number` = :number';
1822
        $statement = $dbHandle->prepare($query);
1823
1824
        $statement->bindParam(':changeset', $changeSetId, \PDO::PARAM_INT);
1825
        $statement->bindParam(':timestamp', $timestamp, \PDO::PARAM_INT);
1826
        $statement->bindParam(':reviewer', $reviewer, \PDO::PARAM_INT);
1827
        $statement->bindParam(':number', $number, \PDO::PARAM_STR);
1828
        $executeResult = $statement->execute();
1829
1830
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1831
        return $statement->fetch(\PDO::FETCH_ASSOC);
1832
    }
1833
1834
    /**
1835
     * Returns a changeset by the unique identifier
1836
     *
1837
     * @param int $project ID of a record from the project table
1838
     * @param int $branch ID of a record from the branch table
1839
     * @param string $id Change id of changeset
1840
     * @param int $createdOn Timestamp of creation time
1841
     * @return mixed
1842
     */
1843
    protected function getGerritChangesetByIdentifier($project, $branch, $id, $createdOn)
1844
    {
1845
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1846
1847
        $query = 'SELECT `id`, `project`, `branch`, `topic`, `identifier`, `number`, `subject`, `owner`, `url`, `commit_message`,
1848
                         `created_on`, `last_updated`, `sort_key`, `open`, `status`, `current_patchset`
1849
                  FROM ' . Database::TABLE_CHANGESET . '
1850
                  WHERE `project` = :project
1851
                        AND `branch` = :branch
1852
                        AND `identifier` = :id
1853
                        AND `created_on` = :created_on';
1854
        $statement = $dbHandle->prepare($query);
1855
1856
        $statement->bindParam(':project', $project, \PDO::PARAM_INT);
1857
        $statement->bindParam(':branch', $branch, \PDO::PARAM_INT);
1858
        $statement->bindParam(':id', $id, \PDO::PARAM_STR);
1859
        $statement->bindParam(':created_on', $createdOn, \PDO::PARAM_INT);
1860
        $executeResult = $statement->execute();
1861
1862
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
1863
        return $statement->fetch(\PDO::FETCH_ASSOC);
1864
    }
1865
1866
    /**
1867
     * Returns lookup values from a database table
1868
     *
1869
     * @param string $table Lookup tablename
1870
     * @param array $selectFields Array of fields to select
1871
     * @param array $whereParts Where parts which will be concatted with AND
1872
     * @return mixed
1873
     */
1874
    protected function getLookupTableValues($table, array $selectFields, array $whereParts)
1875
    {
1876
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
1877
1878
        $whereCondition = $whereValues = array();
1879
        foreach ($whereParts as $field => $value) {
1880
            $whereCondition[] = '`' . $field . '` = :' . $field;
1881
            $whereValues[':' . $field] = $value;
1882
        }
1883
1884
        $query = 'SELECT `' . implode('`,`', $selectFields) . '`
1885
                  FROM ' . $table . '
1886
                  WHERE ' . implode(' AND ', $whereCondition) . '
1887
                  LIMIT 1';
1888
1889
        $statement = $dbHandle->prepare($query);
1890
        $executeResult = $statement->execute($whereValues);
1891
1892
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult, $whereValues);
1893
        return $statement->fetch(\PDO::FETCH_ASSOC);
1894
    }
1895
1896
    /**
1897
     * Checks if the given Gerrit server is known by the database.
1898
     * If the don`t know this host, we save this to the database.
1899
     *
1900
     * @param string $name Configured name of Gerrit server
1901
     * @param string $host Host of Gerrit Server
1902
     * @return integer
1903
     */
1904
    public function proceedServer($name, $host)
1905
    {
1906
        $this->outputHeadline('Proceed Server');
1907
        $this->output('Server "' . $name . '" (' . $host . ')');
1908
1909
        $serverId = $this->existsGerritServer($name, $host);
1910
1911
        // If the don`t know this server, save it!
1912
        if ($serverId === false) {
1913
1914
            $serverData = array(
1915
                'name' => $name,
1916
                'host' => $host
1917
            );
1918
            $serverId = $this->getDatabase()->insertRecord(Database::TABLE_SERVER, $serverData);
1919
            $this->setServersFirstRun();
1920
1921
            $this->output('=> Inserted (ID: ' . $serverId . ')');
1922
1923
        } else {
1924
            $this->output('=> Exists');
1925
        }
1926
1927
        $this->output('');
1928
1929
        $this->setServerId($serverId);
1930
1931
        return $serverId;
1932
    }
1933
1934
    protected function outputHeadline($message)
1935
    {
1936
        $this->output('#');
1937
        $this->output('# ' . $message);
1938
        $this->output('#');
1939
    }
1940
1941
    /**
1942
     * If this import run is the first import run of this Gerrit server (see config['Host']),
1943
     * there must not be any update of imported records.
1944
     *
1945
     * This method is to detect errors / not unique identifiers between different Gerrit server
1946
     *
1947
     * @param string $level Level of importer. E.g. projects, changesets, ...
1948
     * @param int $exceptionCode Individual exception code for easier error detection
1949
     * @param mixed $debugInformation Any kind of debug information
1950
     * @throws \Exception
1951
     */
1952
    protected function checkIfServersFirstRun($level, $exceptionCode, $debugInformation)
1953
    {
1954
        if ($this->isServersFirstRun() === true) {
1955
            var_dump($debugInformation);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($debugInformation); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
1956
1957
            $host = $this->getDataService()->getHost();
1958
1959
            $exceptionMessage = 'UPDATE DETECTED! ';
1960
            $exceptionMessage .= 'This is the first run of server "' . $host . '". ';
1961
            $exceptionMessage .= 'There must not be an update in level ' . $level . '.';
1962
            throw new \Exception($exceptionMessage, $exceptionCode);
1963
        }
1964
    }
1965
1966
    /**
1967
     * Imports a single project.
1968
     * We save name, description and parent project.
1969
     *
1970
     * @param array $project Project info like description or parent project
1971
     * @param array $parentMapping Array where parent / child relation will be saved
1972
     * @return int
1973
     */
1974
    public function importProject(array $project, array &$parentMapping)
1975
    {
1976
        $this->output('Project "' . $project['name'] . '"');
1977
1978
        $row = $this->existsGerritProject($project['name'], $this->getServerId());
1979
1980
        $projectRow = $project;
1981
        $projectRow['server_id'] = $this->getServerId();
1982
1983
        // If we don`t know this project, save this!
1984
        if ($row === false) {
1985
            $projectRow['parent'] = 0;
1986
            $id = $this->getDatabase()->insertRecord(Database::TABLE_PROJECT, $projectRow);
1987
1988
            $this->output('=> Inserted (ID: ' . $id . ')');
1989
1990
            // If we know this project, lets check if there something new
1991
        } else {
1992
1993
            $this->checkIfServersFirstRun('Projects', 1363893021, $row);
1994
1995
            $id = $row['id'];
1996
            unset($row['id']);
1997
1998
            $diff = array_diff($projectRow, $row);
1999
2000
            // If there some new data for us, update it.
2001
            if (count($diff) > 0) {
2002
                $this->getDatabase()->updateRecord(Database::TABLE_PROJECT, $diff, $id);
2003
2004
                $this->output('=> Updated (ID: ' . $id . ')');
2005
2006
            } else {
2007
                $this->output('=> Nothing new. Skip it');
2008
            }
2009
        }
2010
2011
        // We have to save the parent / child relations of projects to execute bulk updates afterwards
2012
        if (isset($project['parent']) === true) {
2013
            $parentMapping[$project['parent']][] = intval($id);
2014
        }
2015
2016
        return $id;
2017
    }
2018
2019
    /**
2020
     * Set correct parent / child relation of projects in database
2021
     *
2022
     * @param array $parentMapping
2023
     * @return void
2024
     */
2025
    public function proceedProjectParentChildRelations(array $parentMapping)
2026
    {
2027
        // If there are no parent / child relations for projects, skip it.
2028
        if (count($parentMapping) == 0) {
2029
            return;
2030
        }
2031
2032
        $this->output('');
2033
        $this->outputHeadline('Proceed Project parent / child relation');
2034
2035
        $parentProjects = $this->getGerritProjectsByName($this->getServerId(), array_keys($parentMapping));
2036
        foreach ($parentProjects as $parentProject) {
2037
            $dataToUpdate = array(
2038
                'parent' => intval($parentProject['id'])
2039
            );
2040
2041
            // The IN(id list) is not working here. I don`t know why.
2042
            // If anyone has an idea, please let me know.
2043
            $where = 'FIND_IN_SET(`id`, \'' . implode(',', $parentMapping[$parentProject['name']]) . '\') > 0 ';
2044
            $where .= 'AND  parent != ' . $dataToUpdate['parent'];
2045
            $updatedRows = $this->getDatabase()->updateRecords(Database::TABLE_PROJECT, $dataToUpdate, $where);
2046
2047
            $this->output(
2048
                '=> ' . $updatedRows . ' projects updated (with "' . $parentProject['name'] . '" as parent project)'
2049
            );
2050
        }
2051
    }
2052
2053
    /**
2054
     * If the incoming $json is a string it will be decoded to an array
2055
     *
2056
     * @param mixed $json JSON String to be decoded
2057
     * @return array
2058
     */
2059
    protected function transferJsonToArray($json)
2060
    {
2061
        if (is_array($json) === true) {
2062
            return $json;
2063
        }
2064
2065
        return json_decode($json, true);
2066
    }
2067
2068
    /**
2069
     * Checks if a Gerrit server is known by our database.
2070
     *
2071
     * @param string $name Name of configured Gerrit server
2072
     * @param string $host Host of configured Gerrit server
2073
     * @return string
2074
     */
2075
    protected function existsGerritServer($name, $host)
2076
    {
2077
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
2078
2079
        $query = 'SELECT `id`
2080
                  FROM ' . Database::TABLE_SERVER . '
2081
                  WHERE `name` = :name AND `host` = :host LIMIT 1';
2082
2083
2084
        $values = array(
2085
            ':name' => $name,
2086
            ':host' => $host
2087
        );
2088
2089
        $statement = $dbHandle->prepare($query);
2090
        $executeResult = $statement->execute($values);
2091
2092
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult, $values);
2093
        return $statement->fetchColumn();
2094
    }
2095
2096
    /**
2097
     * Checks if a given project $name is known for the given Gerrit server $serverId
2098
     *
2099
     * @param string $name Name of project
2100
     * @param int $serverId Server id of Gerrit server
2101
     * @return array
2102
     */
2103
    protected function existsGerritProject($name, $serverId)
2104
    {
2105
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
2106
2107
        // We use BINARY here, because we need a case sensitive check
2108
        $query = 'SELECT `id`, `server_id`, `name`, `description`
2109
                  FROM ' . Database::TABLE_PROJECT . '
2110
                  WHERE `server_id` = :server_id
2111
                  AND BINARY `name` = :name LIMIT 1';
2112
2113
        $statement = $dbHandle->prepare($query);
2114
        $statement->bindParam(':name', $name, \PDO::PARAM_STR);
2115
        $statement->bindParam(':server_id', $serverId, \PDO::PARAM_INT);
2116
        $executeResult = $statement->execute();
2117
2118
        $statement = $this->getDatabase()->checkQueryError($statement, $executeResult);
2119
        return $statement->fetch(\PDO::FETCH_ASSOC);
2120
    }
2121
2122
    /**
2123
     * Truncates the temp tables
2124
     *
2125
     * @return void
2126
     */
2127
    protected function cleanupTempTables()
2128
    {
2129
        $dbHandle = $this->getDatabase()->getDatabaseConnection();
2130
2131
        $query = 'TRUNCATE ' . Database::TABLE_TMP_DEPENDS_NEEDED;
2132
        $dbHandle->query($query);
2133
    }
2134
2135
    /**
2136
     * Enables the debug functionality.
2137
     *
2138
     * @return void
2139
     */
2140
    public function enableDebugFunctionality()
2141
    {
2142
        $this->debug = true;
2143
    }
2144
2145
    /**
2146
     * Disables the debug functionality.
2147
     *
2148
     * @return void
2149
     */
2150
    public function disableDebugFunctionality()
2151
    {
2152
        $this->debug = false;
2153
    }
2154
2155
    /**
2156
     * Returns true if the debug functionality is enabled.
2157
     *
2158
     * @return bool
2159
     */
2160
    public function isDebugFunctionalityEnabled()
2161
    {
2162
        return $this->debug;
2163
    }
2164
}
2165