1<?php
2/*
3Copyright (C) 2014-2018,2020, Siemens AG
4Author: Johannes Najjar
5
6This program is free software; you can redistribute it and/or
7modify it under the terms of the GNU General Public License
8version 2 as published by the Free Software Foundation.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License along
16with this program; if not, write to the Free Software Foundation, Inc.,
1751 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18*/
19
20namespace Fossology\Lib\Dao;
21
22use Fossology\Lib\BusinessRules\ClearingDecisionProcessor;
23use Fossology\Lib\Data\AgentRef;
24use Fossology\Lib\Data\Clearing\ClearingEvent;
25use Fossology\Lib\Data\Clearing\ClearingEventBuilder;
26use Fossology\Lib\Data\Clearing\ClearingEventTypes;
27use Fossology\Lib\Data\ClearingDecision;
28use Fossology\Lib\Data\ClearingDecisionBuilder;
29use Fossology\Lib\Data\DecisionScopes;
30use Fossology\Lib\Data\DecisionTypes;
31use Fossology\Lib\Data\LicenseRef;
32use Fossology\Lib\Data\Tree\ItemTreeBounds;
33use Fossology\Lib\Db\DbManager;
34use Fossology\Lib\Proxy\UploadTreeProxy;
35use Fossology\Lib\Proxy\ScanJobProxy;
36use Fossology\Lib\Util\StringOperation;
37use Monolog\Logger;
38
39class ClearingDao
40{
41  /** @var DbManager */
42  private $dbManager;
43  /** @var Logger */
44  private $logger;
45  /** @var UploadDao */
46  private $uploadDao;
47  /** @var CopyrightDao */
48  private $copyrightDao;
49  /** @var LicenseRef[] */
50  private $licenseRefCache;
51
52  /**
53   * @param DbManager $dbManager
54   * @param UploadDao $uploadDao
55   */
56  function __construct(DbManager $dbManager, UploadDao $uploadDao)
57  {
58    $this->dbManager = $dbManager;
59    $this->logger = new Logger(self::class);
60    $this->uploadDao = $uploadDao;
61    $this->licenseRefCache = array();
62    global $container;
63    $this->copyrightDao = $container->get('dao.copyright');
64  }
65
66  private function getRelevantDecisionsCte(ItemTreeBounds $itemTreeBounds, $groupId, $onlyCurrent, &$statementName, &$params, $condition="")
67  {
68    $uploadTreeTable = $itemTreeBounds->getUploadTreeTableName();
69
70    $params[] = DecisionTypes::WIP; $p1 = "$". count($params);
71    $params[] = $groupId; $p2 = "$". count($params);
72
73    $sql_upload = "";
74    if ('uploadtree' === $uploadTreeTable || 'uploadtree_a' === $uploadTreeTable) {
75      $params[] = $itemTreeBounds->getUploadId(); $p = "$". count($params);
76      $sql_upload = " AND ut.upload_fk=$p";
77    }
78    if (!empty($condition)) {
79      $statementName .= ".(".$condition.")";
80      $condition = " AND $condition";
81    }
82
83    $filterClause = $onlyCurrent ? "DISTINCT ON(itemid)" : "";
84    $sortClause = $onlyCurrent ? "ORDER BY itemid, scope, id DESC" : "";
85
86    $statementName .= "." . $uploadTreeTable . ($onlyCurrent ? ".current": "");
87
88    $globalScope = DecisionScopes::REPO;
89    $localScope = DecisionScopes::ITEM;
90
91    return "WITH decision AS (
92              SELECT
93                $filterClause
94                cd.clearing_decision_pk AS id,
95                cd.pfile_fk AS pfile_id,
96                ut.uploadtree_pk AS itemid,
97                cd.user_fk AS user_id,
98                cd.decision_type AS type_id,
99                cd.scope AS scope,
100                EXTRACT(EPOCH FROM cd.date_added) AS ts_added
101              FROM clearing_decision cd
102                INNER JOIN $uploadTreeTable ut
103                  ON (
104                    (ut.pfile_fk = cd.pfile_fk AND cd.scope = $globalScope)
105                    OR (ut.uploadtree_pk = cd.uploadtree_fk
106                      AND cd.scope = $localScope AND cd.group_fk = $p2))
107                  $sql_upload $condition
108              WHERE cd.decision_type != $p1
109              $sortClause
110            )";
111  }
112
113  /**
114   * @param ItemTreeBounds $itemTreeBounds
115   * @param int $groupId
116   * @return LicenseRef[]
117   */
118  function getClearedLicenses(ItemTreeBounds $itemTreeBounds, $groupId)
119  {
120    $statementName = __METHOD__;
121
122    $params = array($itemTreeBounds->getLeft(), $itemTreeBounds->getRight());
123    $condition = "ut.lft BETWEEN $1 AND $2";
124
125    $decisionsCte = $this->getRelevantDecisionsCte($itemTreeBounds, $groupId, $onlyCurrent=true, $statementName, $params, $condition);
126    $params[] = DecisionTypes::IRRELEVANT;
127    $sql = "$decisionsCte
128            SELECT
129              lr.rf_pk AS license_id,
130              lr.rf_shortname AS shortname,
131              lr.rf_fullname AS fullname
132            FROM decision
133              INNER JOIN clearing_decision_event cde ON cde.clearing_decision_fk = decision.id
134              INNER JOIN clearing_event ce ON
135                (ce.clearing_event_pk = cde.clearing_event_fk AND NOT ce.removed)
136              INNER JOIN license_ref lr ON lr.rf_pk = ce.rf_fk
137            WHERE type_id != $".count($params)."
138            GROUP BY license_id,shortname,fullname";
139
140    $this->dbManager->prepare($statementName, $sql);
141
142    $res = $this->dbManager->execute($statementName, $params);
143
144    $licenses = array();
145    while ($row = $this->dbManager->fetchArray($res)) {
146      $licenses[] = new LicenseRef($row['license_id'], $row['shortname'], $row['fullname']);
147    }
148    $this->dbManager->freeResult($res);
149
150    return $licenses;
151  }
152
153
154   /**
155   * @param ItemTreeBounds $itemTreeBounds
156   * @param int $groupId
157   * @param bool $onlyCurrent
158   * @return ClearingDecision[]
159   */
160  function getFileClearings(ItemTreeBounds $itemTreeBounds, $groupId, $onlyCurrent=true, $forClearingHistory=false)
161  {
162    $this->dbManager->begin();
163
164    $statementName = __METHOD__;
165
166    $params = array($itemTreeBounds->getItemId());
167    $condition = "ut.uploadtree_pk = $1";
168
169    $decisionsCte = $this->getRelevantDecisionsCte($itemTreeBounds, $groupId, $onlyCurrent, $statementName, $params, $condition);
170
171    $clearingsWithLicensesArray = $this->getDecisionsFromCte($decisionsCte, $statementName, $params, $forClearingHistory);
172
173    $this->dbManager->commit();
174    return $clearingsWithLicensesArray;
175  }
176
177  /**
178   * @param ItemTreeBounds $itemTreeBounds
179   * @param int $groupId
180   * @param bool $includeSubFolders
181   * @param bool $onlyCurrent
182   * @return ClearingDecision[]
183   */
184  function getFileClearingsFolder(ItemTreeBounds $itemTreeBounds, $groupId, $includeSubFolders=true, $onlyCurrent=true)
185  {
186    $this->dbManager->begin();
187
188    $statementName = __METHOD__;
189
190    if (!$includeSubFolders) {
191      $params = array($itemTreeBounds->getItemId());
192      $condition = "ut.realparent = $1";
193    } else {
194      $params = array($itemTreeBounds->getLeft(), $itemTreeBounds->getRight());
195      $condition = "ut.lft BETWEEN $1 AND $2";
196    }
197
198    $decisionsCte = $this->getRelevantDecisionsCte($itemTreeBounds, $groupId, $onlyCurrent, $statementName, $params, $condition);
199
200    $clearingsWithLicensesArray = $this->getDecisionsFromCte($decisionsCte, $statementName, $params);
201
202    $this->dbManager->commit();
203    return $clearingsWithLicensesArray;
204  }
205
206  /**
207   * @param string $decisionsCte
208   * @param string $statementName
209   * @param array $params
210   * @return ClearingDecision[]
211   */
212  private function getDecisionsFromCte($decisionsCte, $statementName, $params, $forClearingHistory=false)
213  {
214    $sql = "$decisionsCte
215            SELECT
216              decision.*,
217              users.user_name AS user_name,
218              ce.clearing_event_pk as event_id,
219              ce.user_fk as event_user_id,
220              ce.group_fk as event_group_id,
221              lr.rf_pk AS license_id,
222              lr.rf_shortname AS shortname,
223              lr.rf_fullname AS fullname,
224              ce.removed AS removed,
225              ce.type_fk AS event_type_id,
226              ce.reportinfo AS reportinfo,
227              ce.comment AS comment,
228              ce.acknowledgement AS acknowledgement
229            FROM decision
230            LEFT JOIN users ON decision.user_id = users.user_pk
231            LEFT JOIN clearing_decision_event cde ON cde.clearing_decision_fk = decision.id
232            LEFT JOIN clearing_event ce ON ce.clearing_event_pk = cde.clearing_event_fk
233            LEFT JOIN license_ref lr ON lr.rf_pk = ce.rf_fk
234            ORDER BY decision.id DESC, event_id ASC";
235
236    $this->dbManager->prepare($statementName, $sql);
237
238    $result = $this->dbManager->execute($statementName, $params);
239    $clearingsWithLicensesArray = array();
240
241    $previousClearingId = -1;
242    $previousItemId = -1;
243    $clearingEvents = array();
244    $clearingEventCache = array();
245    $clearingDecisionBuilder = ClearingDecisionBuilder::create();
246    $firstMatch = true;
247    while ($row = $this->dbManager->fetchArray($result)) {
248      $clearingId = $row['id'];
249      $itemId = $row['itemid'];
250      $licenseId = $row['license_id'];
251      $eventId = $row['event_id'];
252      $licenseShortName = $row['shortname'];
253      $licenseName = $row['fullname'];
254      $licenseIsRemoved = $row['removed'];
255
256      $eventType = $row['event_type_id'];
257      $eventUserId = $row['event_user_id'];
258      $eventGroupId = $row['event_group_id'];
259      $comment = $row['comment'];
260      $reportInfo = $row['reportinfo'];
261      $acknowledgement = $row['acknowledgement'];
262
263      if ($clearingId !== $previousClearingId && $itemId !== $previousItemId) {
264        //store the old one
265        if (!$firstMatch) {
266          $clearingsWithLicensesArray[] = $clearingDecisionBuilder->setClearingEvents($clearingEvents)->build();
267        }
268
269        $firstMatch = false;
270        //prepare the new one
271        if ($forClearingHistory) {
272          $previousClearingId = $clearingId;
273        } else {
274          $previousItemId = $itemId;
275        }
276        $clearingEvents = array();
277        $clearingDecisionBuilder = ClearingDecisionBuilder::create()
278            ->setClearingId($row['id'])
279            ->setUploadTreeId($itemId)
280            ->setPfileId($row['pfile_id'])
281            ->setUserName($row['user_name'])
282            ->setUserId($row['user_id'])
283            ->setType(intval($row['type_id']))
284            ->setScope(intval($row['scope']))
285            ->setTimeStamp($row['ts_added']);
286      }
287
288      if ($licenseId !== null) {
289        if (!array_key_exists($eventId, $clearingEventCache)) {
290          if (!array_key_exists($licenseId, $this->licenseRefCache)) {
291            $this->licenseRefCache[$licenseId] = new LicenseRef($licenseId, $licenseShortName, $licenseName);
292          }
293          $licenseRef = $this->licenseRefCache[$licenseId];
294          $clearingEventCache[$eventId] = $this->buildClearingEvent($eventId, $eventUserId, $eventGroupId, $licenseRef, $licenseIsRemoved, $eventType, $reportInfo, $comment, $acknowledgement);
295        }
296        $clearingEvents[] = $clearingEventCache[$eventId];
297      }
298    }
299
300    //! Add the last match
301    if (!$firstMatch) {
302      $clearingsWithLicensesArray[] = $clearingDecisionBuilder->setClearingEvents($clearingEvents)->build();
303    }
304    $this->dbManager->freeResult($result);
305
306    return $clearingsWithLicensesArray;
307  }
308  /**
309   * @param ItemTreeBounds $itemTreeBounds
310   * @param int $groupId
311   * @return ClearingDecision|null
312   */
313  public function getRelevantClearingDecision(ItemTreeBounds $itemTreeBounds, $groupId)
314  {
315    $clearingDecisions = $this->getFileClearings($itemTreeBounds, $groupId);
316    if (count($clearingDecisions) > 0) {
317      return $clearingDecisions[0];
318    }
319    return null;
320  }
321
322  /**
323   * @param int $uploadTreeId
324   * @param int $groupId
325   */
326  public function removeWipClearingDecision($uploadTreeId, $groupId)
327  {
328    $sql = "DELETE FROM clearing_decision WHERE uploadtree_fk=$1 AND group_fk=$2 AND decision_type=$3";
329    $this->dbManager->prepare($stmt = __METHOD__, $sql);
330    $this->dbManager->freeResult($this->dbManager->execute($stmt, array($uploadTreeId, $groupId, DecisionTypes::WIP)));
331  }
332
333  /**
334   * @param int $uploadTreeId
335   * @param int $userId
336   * @param int $groupId
337   * @param int $decType
338   * @param int $scope
339   */
340  public function createDecisionFromEvents($uploadTreeId, $userId, $groupId, $decType, $scope, $eventIds)
341  {
342    if ( ($scope == DecisionScopes::REPO) &&
343         !empty($this->getCandidateLicenseCountForCurrentDecisions($uploadTreeId))) {
344      throw new \Exception( _("Cannot add candidate license as global decision\n") );
345    }
346
347    $itemTreeBounds = $this->uploadDao->getItemTreeBounds($uploadTreeId);
348    $uploadId = $itemTreeBounds->getUploadId();
349    $uploadTreeTable = $this->uploadDao->getUploadtreeTableName($uploadId);
350    $itemTreeBounds = $this->uploadDao->getItemTreeBounds($uploadTreeId, $uploadTreeTable);
351
352    if ($this->isDecisionIrrelevant($uploadTreeId, $groupId)) {
353      $this->copyrightDao->updateTable($itemTreeBounds, '', '', $userId, 'copyright', 'rollback');
354    } else if ($decType == DecisionTypes::IRRELEVANT) {
355      $this->copyrightDao->updateTable($itemTreeBounds, '', '', $userId, 'copyright', 'delete', '2');
356    }
357
358    $this->dbManager->begin();
359
360    $this->removeWipClearingDecision($uploadTreeId, $groupId);
361
362    $statementName = __METHOD__;
363    $this->dbManager->prepare($statementName,
364        "
365INSERT INTO clearing_decision (
366  uploadtree_fk,
367  pfile_fk,
368  user_fk,
369  group_fk,
370  decision_type,
371  scope
372) VALUES (
373  $1,
374  (SELECT pfile_fk FROM uploadtree WHERE uploadtree_pk=$1),
375  $2,
376  $3,
377  $4,
378  $5) RETURNING clearing_decision_pk
379  ");
380    $res = $this->dbManager->execute($statementName,
381        array($uploadTreeId, $userId,  $groupId, $decType, $scope));
382    $result = $this->dbManager->fetchArray($res);
383    $clearingDecisionId = $result['clearing_decision_pk'];
384    $this->dbManager->freeResult($res);
385
386    $statementNameClearingDecisionEventInsert = __METHOD__ . ".insertClearingDecisionEvent";
387    $this->dbManager->prepare($statementNameClearingDecisionEventInsert,
388      "INSERT INTO clearing_decision_event (clearing_decision_fk, clearing_event_fk) VALUES($1, $2)"
389    );
390
391    foreach ($eventIds as $eventId) {
392      $this->dbManager->freeResult($this->dbManager->execute($statementNameClearingDecisionEventInsert, array($clearingDecisionId, $eventId)));
393    }
394
395    $this->dbManager->commit();
396  }
397
398  /**
399   * @param ItemTreeBounds $itemTreeBounds
400   * @param int $groupId
401   * @return ClearingEvent[] sorted by ts_added
402   */
403  public function getRelevantClearingEvents($itemTreeBounds, $groupId, $includeSubFolders=true)
404  {
405    $decision = $this->getFileClearingsFolder($itemTreeBounds, $groupId, $includeSubFolders, $onlyCurrent=true);
406    $events = array();
407    $date = 0;
408
409    if (count($decision)) {
410      foreach ($decision[0]->getClearingEvents() as $event) {
411        $events[$event->getLicenseId()] = $event;
412      }
413      $date = $decision[0]->getTimeStamp();
414    }
415
416    $stmt = __METHOD__;
417    $sql = 'SELECT rf_fk,rf_shortname,rf_fullname,clearing_event_pk,comment,type_fk,removed,reportinfo,acknowledgement, EXTRACT(EPOCH FROM date_added) AS ts_added
418             FROM clearing_event LEFT JOIN license_ref ON rf_fk=rf_pk
419             WHERE uploadtree_fk=$1 AND group_fk=$2 AND date_added>to_timestamp($3)
420             ORDER BY clearing_event_pk ASC';
421    $this->dbManager->prepare($stmt, $sql);
422    $res = $this->dbManager->execute($stmt,array($itemTreeBounds->getItemId(),$groupId,$date));
423
424    while ($row = $this->dbManager->fetchArray($res)) {
425      $licenseRef = new LicenseRef($row['rf_fk'],$row['rf_shortname'],$row['rf_fullname']);
426      $events[$row['rf_fk']] = ClearingEventBuilder::create()
427              ->setEventId($row['clearing_event_pk'])
428              ->setComment($row['comment'])
429              ->setTimeStamp($row['ts_added'])
430              ->setEventType($row['type_fk'])
431              ->setLicenseRef($licenseRef)
432              ->setRemoved($this->dbManager->booleanFromDb($row['removed']))
433              ->setReportinfo($row['reportinfo'])
434              ->setAcknowledgement($row['acknowledgement'])
435              ->setUploadTreeId($itemTreeBounds->getItemId())
436              ->build();
437    }
438    $this->dbManager->freeResult($res);
439    return $events;
440  }
441
442  /**
443   * @param int $uploadTreeId
444   * @param int $userId
445   * @param int $groupId
446   * @param int $licenseId
447   * @param string $what
448   * @param string $changeTo
449   */
450  public function updateClearingEvent($uploadTreeId, $userId, $groupId, $licenseId, $what, $changeTo)
451  {
452    $this->dbManager->begin();
453
454    $statementGetOldata = "SELECT * FROM clearing_event WHERE uploadtree_fk=$1 AND rf_fk=$2 AND group_fk=$3  ORDER BY clearing_event_pk DESC LIMIT 1";
455    $statementName = __METHOD__ . 'getOld';
456    $params = array($uploadTreeId, $licenseId, $groupId);
457    $row = $this->dbManager->getSingleRow($statementGetOldata, $params, $statementName);
458
459    if (!$row) {  //The license was not added as user decision yet -> we promote it here
460      $type = ClearingEventTypes::USER;
461      $row['type_fk'] = $type;
462      $row['comment'] = "";
463      $row['reportinfo'] = "";
464      $row['acknowledgement'] = "";
465    }
466
467    $changeTo = StringOperation::replaceUnicodeControlChar($changeTo, false);
468    if ($what == 'reportinfo') {
469      $reportInfo = $changeTo;
470      $comment = $row['comment'];
471      $acknowledgement = $row['acknowledgement'];
472    } elseif ($what == 'comment') {
473      $reportInfo = $row['reportinfo'];
474      $comment = $changeTo;
475      $acknowledgement = $row['acknowledgement'];
476    } else {
477      $reportInfo = $row['reportinfo'];
478      $comment = $row['comment'];
479      $acknowledgement = $changeTo;
480    }
481    $this->insertClearingEvent($uploadTreeId, $userId, $groupId, $licenseId, false, $row['type_fk'], $reportInfo, $comment, $acknowledgement);
482
483    $this->dbManager->commit();
484
485  }
486
487  public function copyEventIdTo($eventId, $itemId, $userId, $groupId)
488  {
489    $stmt = __METHOD__;
490    $this->dbManager->prepare($stmt,
491      "INSERT INTO clearing_event(uploadtree_fk, user_fk, group_fk, type_fk, rf_fk, removed, reportinfo, comment, acknowledgement)
492        SELECT $2, $3, $4, type_fk, rf_fk, removed, reportinfo, comment, acknowledgement FROM clearing_event WHERE clearing_event_pk = $1"
493      );
494
495    $this->dbManager->freeResult($this->dbManager->execute($stmt, array($eventId, $itemId, $userId, $groupId)));
496  }
497
498  /**
499   * @param int $uploadTreeId
500   * @param int $userId
501   * @param int $groupId
502   * @param int $licenseId
503   * @param bool $isRemoved
504   * @param int $type ClearingEventTypes
505   * @param string $reportInfo
506   * @param string $comment
507   * @param int $jobId
508   * @return int $clearing_event_pk
509   */
510  public function insertClearingEvent($uploadTreeId, $userId, $groupId, $licenseId, $isRemoved, $type = ClearingEventTypes::USER, $reportInfo = '', $comment = '', $acknowledgement = '', $jobId=0)
511  {
512    $insertIsRemoved = $this->dbManager->booleanToDb($isRemoved);
513
514    $reportInfo = StringOperation::replaceUnicodeControlChar($reportInfo);
515    $comment = StringOperation::replaceUnicodeControlChar($comment);
516    $acknowledgement = StringOperation::replaceUnicodeControlChar($acknowledgement);
517
518    $stmt = __METHOD__;
519    $params = array($uploadTreeId, $userId, $groupId, $type, $licenseId, $insertIsRemoved, $reportInfo, $comment, $acknowledgement);
520    $columns = "uploadtree_fk, user_fk, group_fk, type_fk, rf_fk, removed, reportinfo, comment, acknowledgement";
521    $values = "$1,$2,$3,$4,$5,$6,$7,$8,$9";
522
523    if ($jobId > 0) {
524      $stmt.= ".jobId";
525      $params[] = $jobId;
526      $columns .= ", job_fk";
527      $values .= ",$".count($params);
528    } else {
529      $this->markDecisionAsWip($uploadTreeId, $userId, $groupId);
530    }
531
532    $this->dbManager->prepare($stmt, "INSERT INTO clearing_event ($columns) VALUES($values) RETURNING clearing_event_pk");
533    $res = $this->dbManager->execute($stmt, $params);
534
535    $row = $this->dbManager->fetchArray($res);
536    $this->dbManager->freeResult($res);
537
538    return intval($row['clearing_event_pk']);
539  }
540
541  /**
542   * @param int $jobId
543   * @return int[][] eventIds indexed by itemId and licenseId
544   */
545  public function getEventIdsOfJob($jobId)
546  {
547    $statementName = __METHOD__;
548    $this->dbManager->prepare(
549        $statementName,
550        "SELECT uploadtree_fk, clearing_event_pk, rf_fk FROM clearing_event WHERE job_fk = $1"
551    );
552
553    $res = $this->dbManager->execute($statementName, array($jobId));
554
555    $events = array();
556    while ($row = $this->dbManager->fetchArray($res)) {
557      $itemId = intval($row['uploadtree_fk']);
558      $eventId = intval($row['clearing_event_pk']);
559      $licenseId = intval($row['rf_fk']);
560
561      $events[$itemId][$licenseId] = $eventId;
562    }
563    $this->dbManager->freeResult($res);
564
565    return $events;
566  }
567
568  /**
569   * @param int $eventId
570   * @param int $userId
571   * @param int $groupId
572   * @param int $licenseRef
573   * @param $licenseIsRemoved
574   * @param $type
575   * @param $reportInfo
576   * @param string $comment
577   * @return ClearingEvent
578   */
579  protected function buildClearingEvent($eventId, $userId, $groupId, $licenseRef, $licenseIsRemoved, $type, $reportInfo, $comment, $acknowledgement)
580  {
581    $removed = $this->dbManager->booleanFromDb($licenseIsRemoved);
582
583    return ClearingEventBuilder::create()
584      ->setEventId($eventId)
585      ->setUserId($userId)
586      ->setGroupId($groupId)
587      ->setEventType($type)
588      ->setLicenseRef($licenseRef)
589      ->setRemoved($removed)
590      ->setReportInfo($reportInfo)
591      ->setAcknowledgement($acknowledgement)
592      ->setComment($comment)
593      ->build();
594  }
595
596  /**
597   * @param int $uploadTreeId
598   * @param int $userId
599   * @param int $groupId
600   */
601  public function markDecisionAsWip($uploadTreeId, $userId, $groupId)
602  {
603    $statementName = __METHOD__;
604
605    $this->dbManager->prepare($statementName,
606        "INSERT INTO clearing_decision (uploadtree_fk,pfile_fk,user_fk,group_fk,decision_type,scope) VALUES (
607            $1, (SELECT pfile_fk FROM uploadtree WHERE uploadtree_pk=$1), $2, $3, $4, $5)");
608    $res = $this->dbManager->execute($statementName,
609        array($uploadTreeId, $userId, $groupId, DecisionTypes::WIP, DecisionScopes::ITEM));
610    $this->dbManager->freeResult($res);
611  }
612
613  public function isDecisionWip($uploadTreeId, $groupId)
614  {
615    $sql = "SELECT decision_type FROM clearing_decision WHERE uploadtree_fk=$1 AND group_fk = $2 ORDER BY date_added DESC LIMIT 1";
616    $latestDec = $this->dbManager->getSingleRow($sql,
617                 array($uploadTreeId, $groupId), $sqlLog = __METHOD__);
618    if ($latestDec === false) {
619      return false;
620    }
621    return ($latestDec['decision_type'] == DecisionTypes::WIP);
622  }
623
624  public function isDecisionTBD($uploadTreeId, $groupId)
625  {
626    $sql = "SELECT decision_type FROM clearing_decision WHERE uploadtree_fk=$1 AND group_fk = $2 ORDER BY date_added DESC LIMIT 1";
627    $latestDec = $this->dbManager->getSingleRow($sql,
628                 array($uploadTreeId, $groupId), $sqlLog = __METHOD__);
629    if ($latestDec === false) {
630      return false;
631    }
632    return ($latestDec['decision_type'] == DecisionTypes::TO_BE_DISCUSSED);
633  }
634
635  public function isDecisionDNU($uploadTreeId, $groupId)
636  {
637    $sql = "SELECT decision_type FROM clearing_decision
638              WHERE uploadtree_fk=$1 AND group_fk = $2
639            ORDER BY clearing_decision_pk DESC LIMIT 1";
640    $latestDec = $this->dbManager->getSingleRow($sql,
641                 array($uploadTreeId, $groupId), $sqlLog = __METHOD__);
642    if ($latestDec === false) {
643      return false;
644    }
645    return ($latestDec['decision_type'] == DecisionTypes::DO_NOT_USE);
646  }
647
648  public function getClearingType($uploadTreeId, $groupId, $type)
649  {
650    $sql = "SELECT decision_type, scope FROM clearing_decision
651              WHERE uploadtree_fk=$1 AND group_fk = $2
652            ORDER BY clearing_decision_pk DESC LIMIT 1";
653    $latestDec = $this->dbManager->getSingleRow($sql,
654                 array($uploadTreeId, $groupId), $sqlLog = __METHOD__);
655    return $latestDec;
656  }
657
658  public function isDecisionIrrelevant($uploadTreeId, $groupId)
659  {
660    $sql = "SELECT decision_type FROM clearing_decision
661              WHERE uploadtree_fk=$1 AND group_fk = $2
662            ORDER BY clearing_decision_pk DESC LIMIT 1";
663    $latestDec = $this->dbManager->getSingleRow($sql,
664                 array($uploadTreeId, $groupId), $sqlLog = __METHOD__);
665    if ($latestDec === false) {
666      return false;
667    }
668    return ($latestDec['decision_type'] == DecisionTypes::IRRELEVANT);
669  }
670
671  /**
672   * @param ItemTreeBounds $itemTreeBound
673   * @param int $groupId
674   * @param boolean $onlyTried
675   * @return array[] where array has keys ("bulkId","id","text","matched","tried","removedLicenses","addedLicenses")
676   */
677  public function getBulkHistory(ItemTreeBounds $itemTreeBound, $groupId, $onlyTried = true)
678  {
679    $uploadTreeTableName = $itemTreeBound->getUploadTreeTableName();
680    $itemId = $itemTreeBound->getItemId();
681    $uploadId = $itemTreeBound->getUploadId();
682    $left = $itemTreeBound->getLeft();
683
684    $params = array($uploadId, $itemId, $left, $groupId);
685    $stmt = __METHOD__ . "." . $uploadTreeTableName;
686
687    $triedExpr = "$3 between ut2.lft and ut2.rgt";
688    $triedFilter = "";
689    if ($onlyTried) {
690      $triedFilter = "and " . $triedExpr;
691      $stmt .= ".tried";
692    }
693
694    $sql = "WITH alltried AS (
695            SELECT lr.lrb_pk, ce.clearing_event_pk ce_pk, lr.rf_text, ce.uploadtree_fk,
696              $triedExpr AS tried
697            FROM license_ref_bulk lr
698              LEFT JOIN highlight_bulk h ON lrb_fk = lrb_pk
699              LEFT JOIN clearing_event ce ON ce.clearing_event_pk = h.clearing_event_fk
700              LEFT JOIN $uploadTreeTableName ut ON ut.uploadtree_pk = ce.uploadtree_fk
701              INNER JOIN $uploadTreeTableName ut2 ON ut2.uploadtree_pk = lr.uploadtree_fk
702            WHERE ut2.upload_fk = $1 AND lr.group_fk = $4
703              $triedFilter
704              ORDER BY lr.lrb_pk
705            ), aggregated_tried AS (
706            SELECT DISTINCT ON(lrb_pk) lrb_pk, ce_pk, rf_text AS text, tried, matched
707            FROM (
708              SELECT DISTINCT ON(lrb_pk) lrb_pk, ce_pk, rf_text, tried, true AS matched FROM alltried WHERE uploadtree_fk = $2
709              UNION ALL
710              SELECT DISTINCT ON(lrb_pk) lrb_pk, ce_pk, rf_text, tried, false AS matched FROM alltried WHERE uploadtree_fk != $2 OR uploadtree_fk IS NULL
711            ) AS result ORDER BY lrb_pk, matched DESC)
712            SELECT lrb_pk, text, rf_shortname, removing, tried, ce_pk, matched
713            FROM aggregated_tried
714              INNER JOIN license_set_bulk lsb ON lsb.lrb_fk = lrb_pk
715              INNER JOIN license_ref lrf ON lsb.rf_fk = lrf.rf_pk
716            ORDER BY lrb_pk";
717
718    $this->dbManager->prepare($stmt, $sql);
719    $res = $this->dbManager->execute($stmt, $params);
720
721    $bulks = array();
722    while ($row = $this->dbManager->fetchArray($res)) {
723      $bulkRun = $row['lrb_pk'];
724      if (!array_key_exists($bulkRun, $bulks)) {
725        $bulks[$bulkRun] = array(
726            "bulkId" => $row['lrb_pk'],
727            "id" => $row['ce_pk'],
728            "text" => $row['text'],
729            "matched" => $this->dbManager->booleanFromDb($row['matched']),
730            "tried" => $this->dbManager->booleanFromDb($row['tried']),
731            "removedLicenses" => array(),
732            "addedLicenses" => array());
733      }
734      $key = $this->dbManager->booleanFromDb($row['removing']) ? 'removedLicenses' : 'addedLicenses';
735      $bulks[$bulkRun][$key][] = $row['rf_shortname'];
736    }
737
738    $this->dbManager->freeResult($res);
739    return $bulks;
740  }
741
742
743  public function getBulkMatches($bulkId, $groupId)
744  {
745    $stmt = __METHOD__;
746    $sql = "SELECT uploadtree_fk AS itemid
747            FROM clearing_event ce
748            INNER JOIN highlight_bulk h
749            ON ce.clearing_event_pk = h.clearing_event_fk
750            WHERE lrb_fk = $1 AND group_fk = $2";
751
752    $this->dbManager->prepare($stmt, $sql);
753    $res = $this->dbManager->execute($stmt, array($bulkId, $groupId));
754
755    $result = $this->dbManager->fetchAll($res);
756    $this->dbManager->freeResult($res);
757    return $result;
758  }
759
760  /**
761   * @param ItemTreeBounds $itemTreeBounds
762   * @param int $groupId
763   * @return array mapping 'shortname'=>'count'
764   */
765  function getClearedLicenseIdAndMultiplicities(ItemTreeBounds $itemTreeBounds, $groupId)
766  {
767    $statementName = __METHOD__;
768
769    $params = array($itemTreeBounds->getLeft(), $itemTreeBounds->getRight());
770    $condition = "ut.lft BETWEEN $1 AND $2";
771
772    $decisionsCte = $this->getRelevantDecisionsCte($itemTreeBounds, $groupId, $onlyCurrent=true, $statementName, $params, $condition);
773    $params[] = DecisionTypes::IRRELEVANT;
774    $sql = "$decisionsCte
775            SELECT
776              COUNT(DISTINCT itemid) AS count,
777              lr.rf_shortname AS shortname,
778              rf_pk
779            FROM decision
780              LEFT JOIN clearing_decision_event cde ON cde.clearing_decision_fk = decision.id
781              LEFT JOIN clearing_event ce ON ce.clearing_event_pk = cde.clearing_event_fk
782              LEFT JOIN license_ref lr ON lr.rf_pk = ce.rf_fk
783            WHERE (NOT ce.removed OR clearing_event_pk IS NULL) AND type_id!=$".count($params)."
784            GROUP BY shortname,rf_pk";
785
786    $this->dbManager->prepare($statementName, $sql);
787    $res = $this->dbManager->execute($statementName, $params);
788    $multiplicity = array();
789    while ($row = $this->dbManager->fetchArray($res)) {
790      $shortname= empty($row['rf_pk']) ? LicenseDao::NO_LICENSE_FOUND : $row['shortname'];
791      $multiplicity[$shortname] = $row;
792    }
793    $this->dbManager->freeResult($res);
794
795    return $multiplicity;
796  }
797
798  /**
799   * @param ItemTreeBounds $itemTreeBounds
800   * @param int $groupId
801   * @param int $userId
802   */
803  public function markDirectoryAsDecisionType(ItemTreeBounds $itemTreeBounds, $groupId, $userId, $decisionMark)
804  {
805    if ($decisionMark == "doNotUse") {
806      $decisionMark = DecisionTypes::DO_NOT_USE;
807    } else {
808      $decisionMark = DecisionTypes::IRRELEVANT;
809    }
810    $this->markDirectoryAsDecisionTypeRec($itemTreeBounds, $groupId, $userId, false, $decisionMark);
811  }
812
813  /**
814   * @param ItemTreeBounds $itemTreeBounds
815   * @param int $groupId
816   * @param int $userId
817   */
818  public function deleteDecisionTypeFromDirectory(ItemTreeBounds $itemTreeBounds, $groupId, $userId, $decisionMark)
819  {
820    if ($decisionMark == "deleteDoNotUse") {
821      $decisionMark = DecisionTypes::DO_NOT_USE;
822    } else {
823      $decisionMark = DecisionTypes::IRRELEVANT;
824    }
825    $this->markDirectoryAsDecisionTypeRec($itemTreeBounds, $groupId, $userId, true, $decisionMark);
826  }
827
828  /**
829   * @param ItemTreeBounds $itemTreeBounds
830   * @param int $groupId
831   * @param int $userId
832   */
833  protected function markDirectoryAsDecisionTypeRec(ItemTreeBounds $itemTreeBounds, $groupId, $userId, $removeDecision=false, $decisionMark=DecisionTypes::IRRELEVANT)
834  {
835    $params = array($itemTreeBounds->getLeft(), $itemTreeBounds->getRight());
836    $params[] = $groupId;
837    $a = count($params);
838    $options = array(UploadTreeProxy::OPT_SKIP_THESE=>'noLicense',
839                     UploadTreeProxy::OPT_ITEM_FILTER=>' AND (lft BETWEEN $1 AND $2)',
840                     UploadTreeProxy::OPT_GROUP_ID=>'$'.$a.'');
841    $uploadTreeProxy = new UploadTreeProxy($itemTreeBounds->getUploadId(), $options, $itemTreeBounds->getUploadTreeTableName());
842    if (!$removeDecision) {
843      $sql = $uploadTreeProxy->asCTE() .
844        ' SELECT uploadtree_pk FROM UploadTreeView;';
845      $itemRows = $this->dbManager->getRows($sql, $params,
846        __METHOD__ . ".getRevelantItems");
847      $uploadTreeTableName = $itemTreeBounds->getUploadTreeTableName();
848      /** @var ClearingDecisionProcessor $clearingDecisionEventProcessor */
849      $clearingDecisionEventProcessor = $GLOBALS['container']->get(
850        'businessrules.clearing_decision_processor');
851      foreach ($itemRows as $itemRow) {
852        $itemBounds = $this->uploadDao->getItemTreeBounds(
853          $itemRow['uploadtree_pk'], $uploadTreeTableName);
854        $clearingDecisionEventProcessor->makeDecisionFromLastEvents(
855          $itemBounds, $userId, $groupId, $decisionMark, DecisionScopes::ITEM);
856      }
857    } else {
858      $this->dbManager->begin();
859      $params[] = $decisionMark;
860      $sql = $uploadTreeProxy->asCTE() .
861        ' DELETE FROM clearing_decision WHERE clearing_decision_pk IN (
862            SELECT clearing_decision_pk FROM clearing_decision cd
863            INNER JOIN (
864              SELECT MAX(date_added) AS date_added, uploadtree_fk
865                FROM clearing_decision WHERE uploadtree_fk IN (
866                  SELECT uploadtree_pk FROM UploadTreeView)
867              GROUP BY uploadtree_fk) cd2
868            ON cd.uploadtree_fk = cd2.uploadtree_fk
869            AND cd.date_added = cd2.date_added
870            AND decision_type = $' . ($a + 1) . ')
871         RETURNING clearing_decision_pk;';
872      $clearingDecisionRows = $this->dbManager->getRows($sql, $params,
873        __METHOD__ . ".getRelevantDecisions");
874      $clearingDecisions = array_map(function($x) {
875        return $x['clearing_decision_pk'];
876      }, $clearingDecisionRows);
877      $clearingDecisions = "{" . join(",", $clearingDecisions) . "}";
878
879      $delEventSql = "DELETE FROM clearing_event WHERE clearing_event_pk IN (" .
880        "SELECT clearing_event_fk FROM clearing_decision_event " .
881        "WHERE clearing_decision_fk = ANY($1::int[]));";
882      $this->dbManager->getSingleRow($delEventSql, array($clearingDecisions),
883        __METHOD__ . ".deleteEvent");
884
885      $delCdEventSql = "DELETE FROM clearing_decision_event WHERE " .
886        "clearing_decision_fk = ANY($1::int[]);";
887      $this->dbManager->getSingleRow($delCdEventSql, array($clearingDecisions),
888        __METHOD__ . ".deleteCdEvent");
889      $this->dbManager->commit();
890      $this->copyrightDao->updateTable($itemTreeBounds, '', '', $userId,
891        'copyright', 'rollback');
892    }
893  }
894
895  /**
896   * @param uploadId
897   * @param int $groupId
898   * @return array $ids
899   */
900  public function getMainLicenseIds($uploadId, $groupId)
901  {
902    $stmt = __METHOD__;
903    $sql = "SELECT rf_fk FROM upload_clearing_license WHERE upload_fk=$1 AND group_fk=$2";
904    $this->dbManager->prepare($stmt, $sql);
905    $res = $this->dbManager->execute($stmt,array($uploadId,$groupId));
906    $ids = array();
907    while ($row = $this->dbManager->fetchArray($res)) {
908      $ids[$row['rf_fk']] = $row['rf_fk'];
909    }
910    $this->dbManager->freeResult($res);
911    return $ids;
912  }
913
914  /**
915   * @param $uploadId
916   * @param int $groupId
917   * @param int $licenseId
918   */
919  public function makeMainLicense($uploadId, $groupId, $licenseId)
920  {
921    $this->dbManager->insertTableRow('upload_clearing_license',
922            array('upload_fk'=>$uploadId,'group_fk'=>$groupId,'rf_fk'=>$licenseId));
923  }
924
925  /**
926   * @param uploadId
927   * @param int $groupId
928   * @param int $licenseId
929   */
930  public function removeMainLicense($uploadId, $groupId, $licenseId)
931  {
932    $this->dbManager->getSingleRow('DELETE FROM upload_clearing_license WHERE upload_fk=$1 AND group_fk=$2 AND rf_fk=$3',
933            array($uploadId,$groupId,$licenseId));
934  }
935
936  /**
937   * @param ItemTreeBounds $itemTreeBounds
938   * @param int $groupId
939   * @param bool $onlyCurrent
940   * @return ClearingDecision[]
941   */
942  function getFilesForDecisionTypeFolderLevel(ItemTreeBounds $itemTreeBounds, $groupId, $onlyCurrent=true, $decisionMark="")
943  {
944    if (!empty($decisionMark)) {
945      $decisionMark = DecisionTypes::DO_NOT_USE;
946    } else {
947      $decisionMark = DecisionTypes::IRRELEVANT;
948    }
949    $statementName = __METHOD__;
950    $params = array();
951    $decisionsCte = $this->getRelevantDecisionsCte($itemTreeBounds, $groupId, $onlyCurrent, $statementName, $params);
952    $params[] = $decisionMark;
953    $sql = "$decisionsCte
954            SELECT
955	    itemid as uploadtree_pk,
956            lr.rf_shortname AS shortname,
957	    comment
958            FROM decision
959            LEFT JOIN clearing_decision_event cde ON cde.clearing_decision_fk = decision.id
960            LEFT JOIN clearing_event ce ON ce.clearing_event_pk = cde.clearing_event_fk
961            LEFT JOIN license_ref lr ON lr.rf_pk = ce.rf_fk
962            WHERE type_id=$".count($params);
963    $this->dbManager->prepare($statementName, $sql);
964    $res = $this->dbManager->execute($statementName, $params);
965    $irrelevantFiles = $this->dbManager->fetchAll($res);
966    $this->dbManager->freeResult($res);
967    return $irrelevantFiles;
968  }
969
970  /**
971   * @param int $uploadId
972   * @param int $groupId
973   * @param int $userId
974   */
975  public function getPreviousBulkIds($uploadId, $groupId, $userId, $onlyCount=0)
976  {
977    $stmt = __METHOD__;
978    $bulkIds = array();
979    $sql = "SELECT jq_args FROM upload_reuse, jobqueue, job
980            WHERE upload_fk=$1 AND group_fk=$2
981             AND EXISTS(SELECT * FROM group_user_member gum WHERE gum.group_fk=upload_reuse.group_fk AND gum.user_fk=$3)
982             AND jq_type=$4 AND jq_job_fk=job_pk
983             AND job_upload_fk=reused_upload_fk AND job_group_fk=reused_group_fk";
984    $this->dbManager->prepare($stmt, $sql);
985    $res = $this->dbManager->execute($stmt,array($uploadId, $groupId, $userId,'monkbulk'));
986    while ($row=  $this->dbManager->fetchArray($res)) {
987      $bulkIds = array_merge($bulkIds,explode("\n", $row['jq_args']));
988    }
989    $this->dbManager->freeResult($res);
990    if (empty($onlyCount)) {
991      return array_unique($bulkIds);
992    } else {
993      return count(array_unique($bulkIds));
994    }
995  }
996
997  /**
998   * @param uploadTreeId
999   * @return count
1000   */
1001  public function getCandidateLicenseCountForCurrentDecisions($uploadTreeId, $uploadId=0)
1002  {
1003    $params = array();
1004    if (!empty($uploadId)) {
1005      $itemTreeBounds = $this->uploadDao->getParentItemBounds($uploadId, $uploadTreeTableName);
1006      $uploadTreeTableName = $this->uploadDao->getUploadtreeTableName($uploadId);
1007      $params[] = $itemTreeBounds->getLeft();
1008      $params[] = $itemTreeBounds->getRight();
1009      $condition = "UT.lft BETWEEN $1 AND $2";
1010      $uploadtreeStatement = " uploadtree_fk IN (SELECT uploadtree_pk FROM $uploadTreeTableName UT WHERE $condition)";
1011    } else {
1012      $params = array($uploadTreeId);
1013      $uploadtreeStatement = " uploadtree_fk = $1";
1014    }
1015
1016    $sql = "WITH latestEvents AS (
1017      SELECT rf_fk, date_added, removed FROM (
1018        SELECT rf_fk, date_added, removed, row_number()
1019          OVER (PARTITION BY rf_fk ORDER BY date_added DESC) AS ROWNUM
1020        FROM clearing_event WHERE $uploadtreeStatement) SORTABLE
1021          WHERE ROWNUM = 1 ORDER BY rf_fk)
1022      SELECT count(*) FROM license_candidate WHERE license_candidate.rf_pk IN
1023        (SELECT rf_fk FROM latestEvents WHERE removed=false);";
1024    $countCandidate = $this->dbManager->getSingleRow($sql,
1025                 $params, $sqlLog = __METHOD__);
1026
1027    return $countCandidate['count'];
1028  }
1029
1030  /**
1031   * @param uploadId
1032   * @return count
1033   */
1034  public function marklocalDecisionsAsGlobal($uploadId)
1035  {
1036    $statementName = __METHOD__ . $uploadId;
1037
1038    $sql = "WITH latestDecisions AS (
1039              SELECT clearing_decision_pk FROM (
1040                SELECT clearing_decision_pk, uploadtree_fk, date_added, row_number()
1041                  OVER (PARTITION BY uploadtree_fk ORDER BY date_added DESC) AS ROWNUM
1042              FROM clearing_decision WHERE uploadtree_fk IN
1043                   (SELECT uploadtree_pk FROM uploadtree WHERE upload_fk = $1)) SORTABLE
1044                WHERE ROWNUM = $2 ORDER BY uploadtree_fk)
1045             UPDATE clearing_decision SET scope = $2 WHERE clearing_decision_pk IN (
1046               SELECT clearing_decision_pk FROM latestDecisions) RETURNING clearing_decision_pk";
1047
1048    $countUpdated = $this->dbManager->getSingleRow($sql,
1049                 array($uploadId, DecisionScopes::REPO), $statementName);
1050
1051    return count($countUpdated);
1052  }
1053}
1054