1<?php 2 3use dokuwiki\Extension\AuthPlugin; 4use dokuwiki\Extension\Plugin; 5use dokuwiki\plugin\sqlite\SQLiteDB; 6 7class helper_plugin_approve_db extends Plugin 8{ 9 protected $db; 10 11 protected $no_apr_namespaces_array; 12 13 public function __construct() 14 { 15 $this->db = new SQLiteDB('approve', DOKU_PLUGIN . 'approve/db/'); 16 $no_apr_namespaces = $this->getConf('no_apr_namespaces'); 17 $this->no_apr_namespaces_array = array_map(function ($namespace) { 18 return ltrim($namespace, ':'); 19 }, preg_split('/\s+/', $no_apr_namespaces,-1,PREG_SPLIT_NO_EMPTY)); 20 $this->initNoApproveNamespaces(); 21 } 22 23 public function getDbFile(): string 24 { 25 return $this->db->getDbFile(); 26 } 27 28 protected function initNoApproveNamespaces(): void 29 { 30 $config_key = 'no_apr_namespaces'; 31 $db_value = $this->getDbConf($config_key); 32 $config_value = $this->getConf('no_apr_namespaces'); 33 if ($db_value !== $config_value) { // $db_value might be null. In this case run the commit anyway. 34 $this->db->getPdo()->beginTransaction(); 35 $this->setDbConf($config_key, $config_value); 36 $pages_meta = $this->getPagesMetadata(); 37 foreach ($pages_meta as $page_meta) { 38 $page_id = $page_meta['page']; 39 $hidden = (int) $this->pageInHiddenNamespace($page_id); 40 $this->setPageHiddenStatus($page_id, $hidden); 41 } 42 $this->db->getPdo()->commit(); 43 } 44 } 45 46 public function getPagesMetadata(): array 47 { 48 $sql = 'SELECT page, approver, hidden FROM page'; 49 return $this->db->queryAll($sql); 50 } 51 52 public function getPageMetadata(string $page_id): ?array 53 { 54 $sql = 'SELECT approver FROM page WHERE page=? AND hidden != 1'; 55 return $this->db->queryRecord($sql, $page_id); 56 } 57 58 public function getDbConf(string $key): ?string 59 { 60 $sql = 'SELECT value FROM config WHERE key=?'; 61 return $this->db->queryValue($sql, $key); 62 } 63 64 public function setDbConf(string $key, string $value): void 65 { 66 $this->db->saveRecord('config', ['key' => $key, 'value' => $value]); 67 } 68 69 /** 70 * @param string $page_id 71 * @param int $hidden Must be int since SQLite doesn't suport bool type. 72 * @return void 73 */ 74 public function setPageHiddenStatus(string $page_id, int $hidden): void 75 { 76 $sql = 'UPDATE page SET hidden=? WHERE page=?'; 77 $this->db->query($sql, $hidden, $page_id); 78 } 79 80 public function updatePagesAssignments(): void 81 { 82 $this->db->getPdo()->beginTransaction(); 83 84 // clean current settings 85 $this->db->query('DELETE FROM page'); 86 87 $wikiPages = $this->getWikiPages(); 88 foreach ($wikiPages as $id) { 89 // update revision information 90 $this->updatePage($id); 91 } 92 $this->db->getPdo()->commit(); 93 } 94 95 public function getWikiPages(): array 96 { 97 global $conf; 98 99 $datadir = realpath($conf['datadir']); // path without ending "/" 100 $directory = new RecursiveDirectoryIterator($datadir, FilesystemIterator::SKIP_DOTS); 101 $iterator = new RecursiveIteratorIterator($directory); 102 103 $pages = []; 104 /** @var SplFileInfo $fileinfo */ 105 foreach ($iterator as $fileinfo) { 106 if (!$fileinfo->isFile()) continue; 107 108 $path = $fileinfo->getRealPath(); // it should return "/" both on windows and linux 109 // remove dir part 110 $path = substr($path, strlen($datadir)); 111 // make file a dokuwiki path 112 $id = pathID($path); 113 $pages[] = $id; 114 } 115 116 return $pages; 117 } 118 119 public function weightedAssignments(): array 120 { 121 $assignments = $this->db->queryAll('SELECT id, namespace, approver FROM maintainer'); 122 123 $weighted_assignments = []; 124 foreach ($assignments as $assignment) { 125 $ns = $assignment['namespace']; 126 // more general namespaces are overridden by more specific ones. 127 if (substr($ns, -1) == '*') { 128 $weight = substr_count($ns, ':'); 129 } else { 130 $weight = PHP_INT_MAX; 131 } 132 133 $assignment['weight'] = $weight; 134 $weighted_assignments[] = $assignment; 135 } 136 array_multisort(array_column($weighted_assignments, 'weight'), $weighted_assignments); 137 138 return $weighted_assignments; 139 } 140 141 /** 142 * Returns approver or null if page is not in $weighted_assignments. 143 * Approver can be empty string. 144 * 145 * @param string $page_id 146 * @param array $weighted_assignments 147 * @return string 148 */ 149 public function getPageAssignment(string $page_id, array $weighted_assignments): ?string 150 { 151 $page_approver = null; 152 foreach ($weighted_assignments as $assignment) { 153 $ns = ltrim($assignment['namespace'], ':'); 154 $approver = $assignment['approver']; 155 if (substr($ns, -2) == '**') { 156 //remove '**' 157 $ns = substr($ns, 0, -2); 158 if (substr($page_id, 0, strlen($ns)) == $ns) { 159 $page_approver = $approver; 160 } 161 } elseif (substr($ns, -1) == '*') { 162 //remove '*' 163 $ns = substr($ns, 0, -1); 164 $noNS = substr($page_id, strlen($ns)); 165 if (strpos($noNS, ':') === FALSE && 166 substr($page_id, 0, strlen($ns)) == $ns) { 167 $page_approver = $approver; 168 } 169 } elseif($page_id == $ns) { 170 $page_approver = $approver; 171 } 172 } 173 return $page_approver; 174 } 175 176 public function pageInHiddenNamespace(string $page_id): bool 177 { 178 $page_id = ltrim($page_id, ':'); 179 foreach ($this->no_apr_namespaces_array as $namespace) { 180 if (substr($page_id, 0, strlen($namespace)) == $namespace) { 181 return true; 182 } 183 } 184 return false; 185 } 186 187 public function getPages(string $user='', array $states=['approved', 'draft', 'ready_for_approval'], 188 string $namespace='', string $filter=''): array 189 { 190 /* @var AuthPlugin $auth */ 191 global $auth; 192 193 $sql = 'SELECT page.page AS id, page.approver, revision.rev, revision.approved, revision.approved_by, 194 revision.ready_for_approval, revision.ready_for_approval_by, 195 LENGTH(page.page) - LENGTH(REPLACE(page.page, \':\', \'\')) AS colons 196 FROM page INNER JOIN revision ON page.page = revision.page 197 WHERE page.hidden = 0 AND revision.current=1 AND page.page GLOB ? AND page.page REGEXP ? 198 ORDER BY colons, page.page'; 199 $pages = $this->db->queryAll($sql, $namespace.'*', $filter); 200 201 // add status to the page 202 $pages = array_map([$this, 'setPageStatus'], $pages); 203 204 if ($user !== '') { 205 $user_data = $auth->getUserData($user); 206 $user_groups = $user_data['grps']; 207 $pages = array_filter($pages, function ($page) use ($user, $user_groups) { 208 return $page['approver'][0] == '@' && in_array(substr($page['approver'], 1), $user_groups) || 209 $page['approver'] == $user; 210 }); 211 } 212 213 // filter by status 214 $pages = array_filter($pages, function ($page) use ($states) { 215 return in_array($page['status'], $states); 216 }); 217 218 return $pages; 219 } 220 221 public function getPageRevisions(string $page_id): array { 222 $sql = 'SELECT page AS id, rev, approved, approved_by, ready_for_approval, ready_for_approval_by 223 FROM revision WHERE page=?'; 224 $revisions = $this->db->queryAll($sql, $page_id); 225 // add status to the page 226 $revisions = array_map([$this, 'setPageStatus'], $revisions); 227 228 return $revisions; 229 } 230 231 public function getPageRevision(string $page_id, int $rev): ?array 232 { 233 $sql = 'SELECT ready_for_approval, ready_for_approval_by, approved, approved_by, version 234 FROM revision WHERE page=? AND rev=?'; 235 $page = $this->db->queryRecord($sql, $page_id, $rev); 236 237 if ($page == null) { 238 $page = [ 239 'ready_for_approval' => null, 240 'ready_for_approval_by' => null, 241 'approved' => null, 242 'approved_by' => null 243 ]; 244 } 245 $page['id'] = $page_id; 246 $page['rev'] = $rev; 247 $page = $this->setPageStatus($page); 248 249 return $page; 250 } 251 252 protected function setPageStatus(array $page): array 253 { 254 if ($page['approved'] !== null) { 255 $page['status'] = 'approved'; 256 } elseif ($page['ready_for_approval'] !== null) { 257 $page['status'] = 'ready_for_approval'; 258 } else { 259 $page['status'] = 'draft'; 260 } 261 return $page; 262 } 263 264 public function moveRevisionHistory(string $old_page_id, string $new_page_id): void 265 { 266 $this->db->exec('UPDATE revision SET page=? WHERE page=?', $new_page_id, $old_page_id); 267 } 268 269 public function getLastDbRev(string $page_id, ?string $status=null): ?int 270 { 271 if ($status == 'approved') { 272 $sql = 'SELECT rev FROM revision WHERE page=? AND approved IS NOT NULL ORDER BY rev DESC LIMIT 1'; 273 return $this->db->queryValue($sql, $page_id); 274 } elseif ($status == 'ready_for_approval') { 275 $sql = 'SELECT rev FROM revision WHERE page=? AND ready_for_approval IS NOT NULL ORDER BY rev DESC LIMIT 1'; 276 return $this->db->queryValue($sql, $page_id); 277 } 278 $sql = 'SELECT rev FROM revision WHERE page=? AND current=1'; 279 return $this->db->queryValue($sql, $page_id); 280 } 281 282 public function setApprovedStatus(string $page_id): void 283 { 284 global $INFO; 285 286 // approved IS NULL prevents from overriding already approved page 287 $sql = 'UPDATE revision SET approved=?, approved_by=?, 288 version=(SELECT IFNULL(MAX(version), 0) FROM revision WHERE page=?) + 1 289 WHERE page=? AND current=1 AND approved IS NULL'; 290 $this->db->exec($sql, date('c'), $INFO['client'], $page_id, $page_id); 291 } 292 293 public function setReadyForApprovalStatus(string $page_id): void 294 { 295 global $INFO; 296 297 // approved IS NULL prevents from overriding already approved page 298 $sql = 'UPDATE revision SET ready_for_approval=?, ready_for_approval_by=? 299 WHERE page=? AND current=1 AND ready_for_approval IS NULL'; 300 $this->db->exec($sql, date('c'), $INFO['client'], $page_id); 301 } 302 303 protected function deletePage($page_id): void 304 { 305 // delete information about availability of a page but keep the history 306 $this->db->exec('DELETE FROM page WHERE page=?', $page_id); 307 $this->db->exec('DELETE FROM revision WHERE page=? AND approved IS NULL AND ready_for_approval IS NULL' 308 , $page_id); 309 $this->db->exec('UPDATE revision SET current=0 WHERE page=? AND current=1', $page_id); 310 } 311 312 public function handlePageDelete(string $page_id): void 313 { 314 $this->db->getPdo()->beginTransaction(); 315 $this->deletePage($page_id); 316 $this->db->getPdo()->commit(); 317 } 318 319 protected function updatePage(string $page_id): void 320 { 321 // delete all unimportant revisions 322 $this->db->exec('DELETE FROM revision WHERE page=? AND approved IS NULL AND ready_for_approval IS NULL' 323 , $page_id); 324 325 $weighted_assignments = $this->weightedAssignments(); 326 $approver = $this->getPageAssignment($page_id, $weighted_assignments); 327 if ($approver !== null) { 328 $data = [ 329 'page' => $page_id, 330 'hidden' => (int) $this->pageInHiddenNamespace($page_id), 331 'approver' => $approver 332 ]; 333 $this->db->saveRecord('page', $data); // insert or replace 334 } 335 336 $last_change_date = @filemtime(wikiFN($page_id)); 337 // record for current revision exists 338 $sql = 'SELECT 1 FROM revision WHERE page=? AND rev=?'; 339 $exists = $this->db->queryValue($sql, $page_id, $last_change_date); 340 if ($exists === null) { 341 // mark previous revision as old. this may be already deleted by DELETE 342 $this->db->exec('UPDATE revision SET current=0 WHERE page=? AND current=1', $page_id); 343 // create new record 344 $this->db->saveRecord('revision', [ 345 'page' => $page_id, 346 'rev' => $last_change_date, 347 'current' => 1 348 ]); 349 } 350 351 } 352 353 public function handlePageEdit(string $page_id): void 354 { 355 $this->db->getPdo()->beginTransaction(); 356 $this->updatePage($page_id); 357 $this->db->getPdo()->commit(); 358 } 359 360 public function deleteMaintainer(int $maintainer_id): void 361 { 362 $this->db->getPdo()->beginTransaction(); 363 $this->db->exec('DELETE FROM maintainer WHERE id=?', $maintainer_id); 364 $this->db->getPdo()->commit(); 365 } 366 367 public function addMaintainer(string $namespace, string $approver): void 368 { 369 $this->db->getPdo()->beginTransaction(); 370 $this->db->saveRecord('maintainer', [ 371 'namespace' => $namespace, 372 'approver' => $approver 373 ]); 374 $this->db->getPdo()->commit(); 375 } 376 377 public function getMaintainers(): ?array 378 { 379 $sql = 'SELECT id, namespace, approver FROM maintainer ORDER BY namespace'; 380 return $this->db->queryAll($sql); 381 } 382} 383