1<?php 2/** 3 * DokuWiki Plugin issuelinks (Helper Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <dokuwiki@cosmocode.de> 7 */ 8 9use dokuwiki\plugin\issuelinks\classes\Issue; 10use dokuwiki\plugin\issuelinks\classes\ServiceProvider; 11 12class helper_plugin_issuelinks_data extends DokuWiki_Plugin 13{ 14 15 /** @var helper_plugin_issuelinks_db */ 16 private $db = null; 17 18 /** 19 * constructor. loads helpers 20 */ 21 public function __construct() 22 { 23 $this->db = $this->loadHelper('issuelinks_db'); 24 } 25 26 27 /** 28 * Import all Jira issues starting at given paging offset 29 * 30 * @param string $serviceName The name of the project management service 31 * @param string $projectKey The short-key of the project to be imported 32 * 33 * @throws Exception 34 */ 35 public function importAllIssues($serviceName, $projectKey) 36 { 37 $lockfileKey = $this->getImportLockID($serviceName, $projectKey); 38 if ($this->isImportLocked($lockfileKey)) { 39 throw new RuntimeException('Import of Issues is already locked!'); 40 } 41 dbglog('start import. $lockfileKey: ' . $lockfileKey); 42 $this->lockImport($lockfileKey, json_encode(['user' => $_SERVER['REMOTE_USER'], 'status' => 'started'])); 43 44 $serviceProvider = ServiceProvider::getInstance(); 45 $services = $serviceProvider->getServices(); 46 dbglog($services); 47 dbglog($serviceName); 48 $serviceClass = $services[$serviceName]; 49 dbglog($serviceClass); 50 $service = $serviceClass::getInstance(); 51 52 $total = 0; 53 $counter = 0; 54 $startAt = 0; 55 56 try { 57 while ($issues = $service->retrieveAllIssues($projectKey, $startAt)) { 58 if (!$total) { 59 $total = $service->getTotalIssuesBeingImported(); 60 } 61 62 if ($counter > $total) { 63 break; 64 } 65 66 if (!$this->isImportLockedByMe($lockfileKey)) { 67 throw new RuntimeException('Import of Issues aborted because lock removed'); 68 } 69 70 $counter += count($issues); 71 $this->lockImport($lockfileKey, json_encode([ 72 'user' => $_SERVER['REMOTE_USER'], 73 'total' => $total, 74 'count' => $counter, 75 'status' => 'running', 76 ])); 77 } 78 } catch (\Throwable $e) { 79 dbglog( 80 "Downloading all issues from $serviceName fpr project $projectKey failed ", 81 __FILE__ . ': ' . __LINE__ 82 ); 83 if (is_a($e, \dokuwiki\plugin\issuelinks\classes\HTTPRequestException::class)) { 84 /** @var \dokuwiki\plugin\issuelinks\classes\HTTPRequestException $e */ 85 dbglog($e->getUrl()); 86 dbglog($e->getHttpError()); 87 dbglog($e->getMessage()); 88 dbglog($e->getCode()); 89 dbglog($e->getResponseBody()); 90 } 91 $this->lockImport($lockfileKey, json_encode(['status' => 'failed'])); 92 throw $e; 93 } 94 $this->unlockImport($lockfileKey); 95 } 96 97 98 public function getImportLockID($serviceName, $projectKey) 99 { 100 return "_plugin__issuelinks_import_$serviceName-$projectKey"; 101 } 102 103 /** 104 * This checks the lock for the import process it behaves differently from the dokuwiki-core checklock() function! 105 * 106 * It returns false if the lock does not exist. It returns **boolean true** if the lock exists and is mine. 107 * It returns the username/ip if the lock exists and is not mine. 108 * It is therefore important to use strict (===) checking for true! 109 * 110 * @param $id 111 * 112 * @return bool|string 113 */ 114 public function isImportLocked($id) 115 { 116 global $conf; 117 $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock'; 118 if (!file_exists($lockFN)) { 119 return false; 120 } 121 122 clearstatcache(true, $lockFN); 123 if ((time() - filemtime($lockFN)) > 120) { 124 unlink($lockFN); 125 dbglog('issuelinks: stale lock timeout'); 126 return false; 127 } 128 129 $lockData = json_decode(io_readFile($lockFN), true); 130 if (!empty($lockData['status']) && $lockData['status'] === 'done') { 131 return false; 132 } 133 134 return true; 135 } 136 137 /** 138 * Generate lock file for import of issues/commits 139 * 140 * This is mostly a reimplementation of @see lock() 141 * However we do not clean the id and prepent a underscore to avoid conflicts with locks of existing pages. 142 * 143 * @param $id 144 * @param $jsonData 145 */ 146 public function lockImport($id, $jsonData) 147 { 148 global $conf; 149 150 $lock = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock'; 151 dbglog('lock import: ' . $jsonData, __FILE__ . ': ' . __LINE__); 152 io_saveFile($lock, $jsonData); 153 } 154 155 public function isImportLockedByMe($id) 156 { 157 if (!$this->isImportLocked($id)) { 158 return false; 159 } 160 161 global $conf, $INPUT; 162 $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock'; 163 $lockData = json_decode(io_readFile($lockFN), true); 164 if ($lockData['user'] !== $INPUT->server->str('REMOTE_USER')) { 165 return false; 166 } 167 168 touch($lockFN); 169 return true; 170 } 171 172 /** 173 * Marks the import as unlocked / done 174 * 175 * @param $id 176 */ 177 public function unlockImport($id) 178 { 179 global $conf; 180 $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock'; 181 $lockData = json_decode(io_readFile($lockFN), true); 182 $lockData['status'] = 'done'; 183 $lockData['total'] = $lockData['count']; 184 io_saveFile($lockFN, json_encode($lockData)); 185 } 186 187 public function getLockContent($id) 188 { 189 global $conf; 190 $lockFN = $conf['lockdir'] . '/' . md5('_' . $id) . '.lock'; 191 if (!file_exists($lockFN)) { 192 return false; 193 } 194 return json_decode(io_readFile($lockFN), true); 195 } 196 197 public function removeLock($lockID) 198 { 199 global $conf; 200 $lockFN = $conf['lockdir'] . '/' . md5('_' . $lockID) . '.lock'; 201 unlink($lockFN); 202 } 203 204 /** 205 * Get an issue either from local DB or attempt to import it 206 * 207 * @param string $pmServiceName The name of the project management service 208 * @param string $project 209 * @param int $issueid 210 * @param bool $isMergeRequest 211 * 212 * @return bool|Issue 213 */ 214 public function getIssue($pmServiceName, $project, $issueid, $isMergeRequest) 215 { 216 $issue = Issue::getInstance($pmServiceName, $project, $issueid, $isMergeRequest); 217 if (!$issue->isValid()) { 218 try { 219 $issue->getFromService(); 220 $issue->saveToDB(); 221 } catch (Exception $e) { 222 // that's fine 223 } 224 } 225 return $issue; 226 } 227 228 public function getMergeRequestsForIssue($serviceName, $projectKey, $issueId, $isMergeRequest) 229 { 230 /** @var helper_plugin_issuelinks_db $db */ 231 $db = plugin_load('helper', 'issuelinks_db'); 232 $issues = $db->getMergeRequestsReferencingIssue($serviceName, $projectKey, $issueId, $isMergeRequest); 233 foreach ($issues as &$issueData) { 234 $issue = Issue::getInstance( 235 $issueData['service'], 236 $issueData['project_id'], 237 $issueData['issue_id'], 238 $issueData['is_mergerequest'] 239 ); 240 $issue->getFromDB(); 241 $issueData['summary'] = $issue->getSummary(); 242 $issueData['status'] = $issue->getStatus(); 243 $issueData['url'] = $issue->getIssueURL(); 244 } 245 unset($issueData); 246 247 return $issues; 248 } 249 250 /** 251 * Get Pages with links to issues 252 * 253 * @param string $pmServiceName The name of the project management service 254 * @param string $projectKey 255 * @param int $issueId the issue id 256 * @param bool $isMergeRequest 257 * 258 * @return array 259 */ 260 public function getLinkingPages($pmServiceName, $projectKey, $issueId, $isMergeRequest) 261 { 262 $pages = $this->db->getAllPageLinkingToIssue($pmServiceName, $projectKey, $issueId, $isMergeRequest); 263 $pages = $this->db->removeOldLinks($pmServiceName, $projectKey, $issueId, $isMergeRequest, $pages); 264 265 if (empty($pages)) { 266 return []; 267 } 268 269 $pages = $this->keepNewest($pages); 270 $pages = $this->filterPagesForACL($pages); 271 $pages = $this->addUserToPages($pages); 272 return $pages; 273 } 274 275 /** 276 * remove duplicate revisions of a page and keep only the newest 277 * 278 * @param array $pages Array of pages sorted(!) from newest to oldest 279 * 280 * @return array 281 */ 282 public function keepNewest($pages) 283 { 284 $uniquePages = []; 285 foreach ($pages as $page) { 286 if (!array_key_exists($page['page'], $uniquePages) || $uniquePages[$page['page']]['rev'] < $page['rev']) { 287 $uniquePages[$page['page']] = $page; 288 } 289 } 290 return array_values($uniquePages); 291 } 292 293 /** 294 * Filter the given pages for at least AUTH_READ 295 * 296 * @param array $pages 297 * 298 * @return array 299 */ 300 private function filterPagesForACL($pages) 301 { 302 $allowedPagegs = []; 303 foreach ($pages as $page) { 304 if (auth_quickaclcheck($page['page']) >= AUTH_READ) { 305 $allowedPagegs[] = $page; 306 } 307 } 308 return $allowedPagegs; 309 } 310 311 /** 312 * add the corresponding user to each revision 313 * 314 * @param array $pages 315 * 316 * @return array 317 */ 318 public function addUserToPages($pages) 319 { 320 foreach ($pages as &$page) { 321 $changelog = new PageChangelog($page['page']); 322 $revision = $changelog->getRevisionInfo($page['rev']); 323 $page['user'] = $revision['user']; 324 } 325 return $pages; 326 } 327} 328 329// vim:ts=4:sw=4:et: 330