14d6d17d0SAndreas Gohr<?php 2c6d8c1d9SAndreas Gohr 3*fea1a86fSAndreas Gohruse dokuwiki\ErrorHandler; 4c6d8c1d9SAndreas Gohruse dokuwiki\Extension\AuthPlugin; 5*fea1a86fSAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB; 6c6d8c1d9SAndreas Gohr 74d6d17d0SAndreas Gohr/** 84d6d17d0SAndreas Gohr * DokuWiki Plugin acknowledge (Helper Component) 94d6d17d0SAndreas Gohr * 104d6d17d0SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 114d6d17d0SAndreas Gohr * @author Andreas Gohr, Anna Dabrowska <dokuwiki@cosmocode.de> 124d6d17d0SAndreas Gohr */ 134d6d17d0SAndreas Gohrclass helper_plugin_acknowledge extends DokuWiki_Plugin 144d6d17d0SAndreas Gohr{ 154d6d17d0SAndreas Gohr 16*fea1a86fSAndreas Gohr protected $db; 17*fea1a86fSAndreas Gohr 18639d4c50SAndreas Gohr // region Database Management 19639d4c50SAndreas Gohr 20cabb51d3SAndreas Gohr /** 21*fea1a86fSAndreas Gohr * Get SQLiteDB instance 22*fea1a86fSAndreas Gohr * 23*fea1a86fSAndreas Gohr * @return SQLiteDB|null 24cabb51d3SAndreas Gohr */ 25cabb51d3SAndreas Gohr public function getDB() 26cabb51d3SAndreas Gohr { 27*fea1a86fSAndreas Gohr if ($this->db === null) { 28*fea1a86fSAndreas Gohr try { 29*fea1a86fSAndreas Gohr $this->db = new SQLiteDB('acknowledgement', __DIR__ . '/db'); 30*fea1a86fSAndreas Gohr 31*fea1a86fSAndreas Gohr // register our custom functions 32*fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('AUTH_ISMEMBER', [$this, 'auth_isMember'], -1); 33*fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('MATCHES_PAGE_PATTERN', [$this, 'matchPagePattern'], 2); 34*fea1a86fSAndreas Gohr } catch (\Exception $exception) { 35*fea1a86fSAndreas Gohr if (defined('DOKU_UNITTEST')) throw new \RuntimeException('Could not load SQLite', 0, $exception); 36*fea1a86fSAndreas Gohr ErrorHandler::logException($exception); 37cabb51d3SAndreas Gohr msg($this->getLang('error sqlite plugin missing'), -1); 38cabb51d3SAndreas Gohr return null; 39cabb51d3SAndreas Gohr } 40cabb51d3SAndreas Gohr } 41*fea1a86fSAndreas Gohr return $this->db; 429c3eae1eSAnna Dabrowska } 439c3eae1eSAnna Dabrowska 449c3eae1eSAnna Dabrowska /** 459c3eae1eSAnna Dabrowska * Wrapper function for auth_isMember which accepts groups as string 469c3eae1eSAnna Dabrowska * 479c3eae1eSAnna Dabrowska * @param string $memberList 489c3eae1eSAnna Dabrowska * @param string $user 499c3eae1eSAnna Dabrowska * @param string $groups 509c3eae1eSAnna Dabrowska * @return bool 519c3eae1eSAnna Dabrowska */ 529c3eae1eSAnna Dabrowska public function auth_isMember($memberList, $user, $groups) 539c3eae1eSAnna Dabrowska { 5495113ed8SAnna Dabrowska return auth_isMember($memberList, $user, explode('///', $groups)); 559c3eae1eSAnna Dabrowska } 569c3eae1eSAnna Dabrowska 579c3eae1eSAnna Dabrowska /** 58639d4c50SAndreas Gohr * Fills the page index with all unknown pages from the fulltext index 59639d4c50SAndreas Gohr * @return void 60639d4c50SAndreas Gohr */ 61639d4c50SAndreas Gohr public function updatePageIndex() 62639d4c50SAndreas Gohr { 63639d4c50SAndreas Gohr $sqlite = $this->getDB(); 64639d4c50SAndreas Gohr if (!$sqlite) return; 65639d4c50SAndreas Gohr 66639d4c50SAndreas Gohr $pages = idx_getIndex('page', ''); 67639d4c50SAndreas Gohr $sql = "INSERT OR IGNORE INTO pages (page, lastmod) VALUES (?,?)"; 68639d4c50SAndreas Gohr 69*fea1a86fSAndreas Gohr $sqlite->getPdo()->beginTransaction(); 70639d4c50SAndreas Gohr foreach ($pages as $page) { 71639d4c50SAndreas Gohr $page = trim($page); 72639d4c50SAndreas Gohr $lastmod = @filemtime(wikiFN($page)); 73639d4c50SAndreas Gohr if ($lastmod) { 74*fea1a86fSAndreas Gohr try { 75*fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $lastmod]); 76*fea1a86fSAndreas Gohr } catch (\Exception $exception) { 77*fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 78*fea1a86fSAndreas Gohr throw $exception; 79639d4c50SAndreas Gohr } 80639d4c50SAndreas Gohr } 81*fea1a86fSAndreas Gohr } 82*fea1a86fSAndreas Gohr $sqlite->getPdo()->commit(); 83639d4c50SAndreas Gohr } 84639d4c50SAndreas Gohr 85639d4c50SAndreas Gohr /** 86639d4c50SAndreas Gohr * Check if the given pattern matches the given page 87639d4c50SAndreas Gohr * 88639d4c50SAndreas Gohr * @param string $pattern the pattern to check against 89639d4c50SAndreas Gohr * @param string $page the cleaned pageid to check 90639d4c50SAndreas Gohr * @return bool 91639d4c50SAndreas Gohr */ 92639d4c50SAndreas Gohr public function matchPagePattern($pattern, $page) 93639d4c50SAndreas Gohr { 94639d4c50SAndreas Gohr if (trim($pattern, ':') == '**') return true; // match all 95639d4c50SAndreas Gohr 96639d4c50SAndreas Gohr // regex patterns 97639d4c50SAndreas Gohr if ($pattern[0] == '/') { 98639d4c50SAndreas Gohr return (bool)preg_match($pattern, ":$page"); 99639d4c50SAndreas Gohr } 100639d4c50SAndreas Gohr 101639d4c50SAndreas Gohr $pns = ':' . getNS($page) . ':'; 102639d4c50SAndreas Gohr 103639d4c50SAndreas Gohr $ans = ':' . cleanID($pattern) . ':'; 104639d4c50SAndreas Gohr if (substr($pattern, -2) == '**') { 105639d4c50SAndreas Gohr // upper namespaces match 106639d4c50SAndreas Gohr if (strpos($pns, $ans) === 0) { 107639d4c50SAndreas Gohr return true; 108639d4c50SAndreas Gohr } 109639d4c50SAndreas Gohr } elseif (substr($pattern, -1) == '*') { 110639d4c50SAndreas Gohr // namespaces match exact 111639d4c50SAndreas Gohr if ($ans == $pns) { 112639d4c50SAndreas Gohr return true; 113639d4c50SAndreas Gohr } 114639d4c50SAndreas Gohr } else { 115639d4c50SAndreas Gohr // exact match 116639d4c50SAndreas Gohr if (cleanID($pattern) == $page) { 117639d4c50SAndreas Gohr return true; 118639d4c50SAndreas Gohr } 119639d4c50SAndreas Gohr } 120639d4c50SAndreas Gohr 121639d4c50SAndreas Gohr return false; 122639d4c50SAndreas Gohr } 123639d4c50SAndreas Gohr 124639d4c50SAndreas Gohr // endregion 125639d4c50SAndreas Gohr // region Page Data 126639d4c50SAndreas Gohr 127639d4c50SAndreas Gohr /** 128ef3ab392SAndreas Gohr * Delete a page 129ef3ab392SAndreas Gohr * 130ef3ab392SAndreas Gohr * Cascades to delete all assigned data, etc. 131ef3ab392SAndreas Gohr * 132ef3ab392SAndreas Gohr * @param string $page Page ID 133ef3ab392SAndreas Gohr */ 134ef3ab392SAndreas Gohr public function removePage($page) 135ef3ab392SAndreas Gohr { 136ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 137ef3ab392SAndreas Gohr if (!$sqlite) return; 138ef3ab392SAndreas Gohr 139ef3ab392SAndreas Gohr $sql = "DELETE FROM pages WHERE page = ?"; 140*fea1a86fSAndreas Gohr $sqlite->exec($sql, $page); 141ef3ab392SAndreas Gohr } 142ef3ab392SAndreas Gohr 143ef3ab392SAndreas Gohr /** 1445dee13f7SAnna Dabrowska * Update last modified date of page if content has changed 145ef3ab392SAndreas Gohr * 146ef3ab392SAndreas Gohr * @param string $page Page ID 147ef3ab392SAndreas Gohr * @param int $lastmod timestamp of last non-minor change 148ef3ab392SAndreas Gohr */ 1495dee13f7SAnna Dabrowska public function storePageDate($page, $lastmod, $newContent) 150ef3ab392SAndreas Gohr { 151ed4e8871SAnna Dabrowska $changelog = new \dokuwiki\ChangeLog\PageChangeLog($page); 152789aa26fSAnna Dabrowska $revs = $changelog->getRevisions(0, 1); 153ed4e8871SAnna Dabrowska 154ed4e8871SAnna Dabrowska // compare content 155ed4e8871SAnna Dabrowska $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0]))); 156ed4e8871SAnna Dabrowska $newContent = str_replace(NL, '', $newContent); 157ed4e8871SAnna Dabrowska if ($oldContent === $newContent) return; 158ed4e8871SAnna Dabrowska 159ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 160ef3ab392SAndreas Gohr if (!$sqlite) return; 161ef3ab392SAndreas Gohr 162ef3ab392SAndreas Gohr $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)"; 163*fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $lastmod]); 164ef3ab392SAndreas Gohr } 165ef3ab392SAndreas Gohr 166639d4c50SAndreas Gohr // endregion 167639d4c50SAndreas Gohr // region Assignments 168639d4c50SAndreas Gohr 169ef3ab392SAndreas Gohr /** 170f09444ffSAndreas Gohr * Clears direct assignments for a page 171f09444ffSAndreas Gohr * 172cabb51d3SAndreas Gohr * @param string $page Page ID 173cabb51d3SAndreas Gohr */ 174f09444ffSAndreas Gohr public function clearPageAssignments($page) 175cabb51d3SAndreas Gohr { 176cabb51d3SAndreas Gohr $sqlite = $this->getDB(); 177cabb51d3SAndreas Gohr if (!$sqlite) return; 178cabb51d3SAndreas Gohr 179f09444ffSAndreas Gohr $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?"; 180*fea1a86fSAndreas Gohr $sqlite->exec($sql, $page); 181f09444ffSAndreas Gohr } 182f09444ffSAndreas Gohr 183f09444ffSAndreas Gohr /** 184639d4c50SAndreas Gohr * Set assignees for a given page as manually specified 185639d4c50SAndreas Gohr * 186639d4c50SAndreas Gohr * @param string $page Page ID 187639d4c50SAndreas Gohr * @param string $assignees 188639d4c50SAndreas Gohr * @return void 189639d4c50SAndreas Gohr */ 190639d4c50SAndreas Gohr public function setPageAssignees($page, $assignees) 191639d4c50SAndreas Gohr { 192639d4c50SAndreas Gohr $sqlite = $this->getDB(); 193639d4c50SAndreas Gohr if (!$sqlite) return; 194639d4c50SAndreas Gohr 195639d4c50SAndreas Gohr $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 196639d4c50SAndreas Gohr 197639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)"; 198*fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees]); 199639d4c50SAndreas Gohr } 200639d4c50SAndreas Gohr 201639d4c50SAndreas Gohr /** 202639d4c50SAndreas Gohr * Set assignees for a given page from the patterns 203639d4c50SAndreas Gohr * @param string $page Page ID 204639d4c50SAndreas Gohr */ 205639d4c50SAndreas Gohr public function setAutoAssignees($page) 206639d4c50SAndreas Gohr { 207639d4c50SAndreas Gohr $sqlite = $this->getDB(); 208639d4c50SAndreas Gohr if (!$sqlite) return; 209639d4c50SAndreas Gohr 210639d4c50SAndreas Gohr $patterns = $this->getAssignmentPatterns(); 211639d4c50SAndreas Gohr 212639d4c50SAndreas Gohr // given assignees 213639d4c50SAndreas Gohr $assignees = ''; 214639d4c50SAndreas Gohr 215639d4c50SAndreas Gohr // find all patterns that match the page and add the configured assignees 216639d4c50SAndreas Gohr foreach ($patterns as $pattern => $assignees) { 217639d4c50SAndreas Gohr if ($this->matchPagePattern($pattern, $page)) { 218639d4c50SAndreas Gohr $assignees .= ',' . $assignees; 219639d4c50SAndreas Gohr } 220639d4c50SAndreas Gohr } 221639d4c50SAndreas Gohr 222639d4c50SAndreas Gohr // remove duplicates and empty entries 223639d4c50SAndreas Gohr $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 224639d4c50SAndreas Gohr 225639d4c50SAndreas Gohr // store the assignees 226639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)"; 227*fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees]); 228639d4c50SAndreas Gohr } 229639d4c50SAndreas Gohr 230639d4c50SAndreas Gohr /** 231639d4c50SAndreas Gohr * Is the given user one of the assignees for this page 232639d4c50SAndreas Gohr * 233639d4c50SAndreas Gohr * @param string $page Page ID 234639d4c50SAndreas Gohr * @param string $user user name to check 235639d4c50SAndreas Gohr * @param string[] $groups groups this user is in 236639d4c50SAndreas Gohr * @return bool 237639d4c50SAndreas Gohr */ 238639d4c50SAndreas Gohr public function isUserAssigned($page, $user, $groups) 239639d4c50SAndreas Gohr { 240639d4c50SAndreas Gohr $sqlite = $this->getDB(); 241639d4c50SAndreas Gohr if (!$sqlite) return false; 242639d4c50SAndreas Gohr 243639d4c50SAndreas Gohr $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?"; 244*fea1a86fSAndreas Gohr $record = $sqlite->queryRecord($sql, $page); 245*fea1a86fSAndreas Gohr $assignees = $record['pageassignees'] . ',' . $record['autoassignees']; 246639d4c50SAndreas Gohr return auth_isMember($assignees, $user, $groups); 247639d4c50SAndreas Gohr } 248639d4c50SAndreas Gohr 249639d4c50SAndreas Gohr /** 250639d4c50SAndreas Gohr * Fetch all assignments for a given user, with additional page information, 251639d4c50SAndreas Gohr * filtering already granted acknowledgements. 252639d4c50SAndreas Gohr * 253639d4c50SAndreas Gohr * @param string $user 254639d4c50SAndreas Gohr * @param array $groups 255639d4c50SAndreas Gohr * @return array|bool 256639d4c50SAndreas Gohr */ 257639d4c50SAndreas Gohr public function getUserAssignments($user, $groups) 258639d4c50SAndreas Gohr { 259639d4c50SAndreas Gohr $sqlite = $this->getDB(); 260639d4c50SAndreas Gohr if (!$sqlite) return false; 261639d4c50SAndreas Gohr 262639d4c50SAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A 263639d4c50SAndreas Gohr JOIN pages B 264639d4c50SAndreas Gohr ON A.page = B.page 265639d4c50SAndreas Gohr LEFT JOIN acks C 266639d4c50SAndreas Gohr ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) ) 267639d4c50SAndreas Gohr WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?) 268639d4c50SAndreas Gohr AND ack IS NULL"; 269639d4c50SAndreas Gohr 270*fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, $user, $user, implode('///', $groups)); 271639d4c50SAndreas Gohr } 272639d4c50SAndreas Gohr 273639d4c50SAndreas Gohr 274639d4c50SAndreas Gohr /** 275639d4c50SAndreas Gohr * Resolve names of users assigned to a given page 276639d4c50SAndreas Gohr * 277639d4c50SAndreas Gohr * This can be slow on huge user bases! 278639d4c50SAndreas Gohr * 279639d4c50SAndreas Gohr * @param string $page 280639d4c50SAndreas Gohr * @return array|false 281639d4c50SAndreas Gohr */ 282639d4c50SAndreas Gohr public function getPageAssignees($page) 283639d4c50SAndreas Gohr { 284639d4c50SAndreas Gohr $sqlite = $this->getDB(); 285639d4c50SAndreas Gohr if (!$sqlite) return false; 286639d4c50SAndreas Gohr /** @var AuthPlugin $auth */ 287639d4c50SAndreas Gohr global $auth; 288639d4c50SAndreas Gohr 289639d4c50SAndreas Gohr $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments' 290639d4c50SAndreas Gohr FROM assignments 291639d4c50SAndreas Gohr WHERE page = ?"; 292*fea1a86fSAndreas Gohr $assignments = $sqlite->queryValue($sql, $page); 293639d4c50SAndreas Gohr 294639d4c50SAndreas Gohr $users = []; 295639d4c50SAndreas Gohr foreach (explode(',', $assignments) as $item) { 296639d4c50SAndreas Gohr $item = trim($item); 297639d4c50SAndreas Gohr if ($item === '') continue; 298639d4c50SAndreas Gohr if ($item[0] == '@') { 299639d4c50SAndreas Gohr $users = array_merge( 300639d4c50SAndreas Gohr $users, 301639d4c50SAndreas Gohr array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)])) 302639d4c50SAndreas Gohr ); 303639d4c50SAndreas Gohr } else { 304639d4c50SAndreas Gohr $users[] = $item; 305639d4c50SAndreas Gohr } 306639d4c50SAndreas Gohr } 307639d4c50SAndreas Gohr 308639d4c50SAndreas Gohr return array_unique($users); 309639d4c50SAndreas Gohr } 310639d4c50SAndreas Gohr 311639d4c50SAndreas Gohr // endregion 312639d4c50SAndreas Gohr // region Assignment Patterns 313639d4c50SAndreas Gohr 314639d4c50SAndreas Gohr /** 315f09444ffSAndreas Gohr * Get all the assignment patterns 316f09444ffSAndreas Gohr * @return array (pattern => assignees) 317f09444ffSAndreas Gohr */ 318f09444ffSAndreas Gohr public function getAssignmentPatterns() 319f09444ffSAndreas Gohr { 320f09444ffSAndreas Gohr $sqlite = $this->getDB(); 321f09444ffSAndreas Gohr if (!$sqlite) return []; 322f09444ffSAndreas Gohr 323f09444ffSAndreas Gohr $sql = "SELECT pattern, assignees FROM assignments_patterns"; 324*fea1a86fSAndreas Gohr return $sqlite->queryKeyValueList($sql); 325f09444ffSAndreas Gohr } 326f09444ffSAndreas Gohr 327f09444ffSAndreas Gohr /** 328f09444ffSAndreas Gohr * Save new assignment patterns 329f09444ffSAndreas Gohr * 330f09444ffSAndreas Gohr * This resaves all patterns and reapplies them 331f09444ffSAndreas Gohr * 332f09444ffSAndreas Gohr * @param array $patterns (pattern => assignees) 333f09444ffSAndreas Gohr */ 334639d4c50SAndreas Gohr public function saveAssignmentPatterns($patterns) 335639d4c50SAndreas Gohr { 336f09444ffSAndreas Gohr $sqlite = $this->getDB(); 337f09444ffSAndreas Gohr if (!$sqlite) return; 338f09444ffSAndreas Gohr 339*fea1a86fSAndreas Gohr $sqlite->getPdo()->beginTransaction(); 340*fea1a86fSAndreas Gohr try { 341f09444ffSAndreas Gohr 342*fea1a86fSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all assignments */ 343f09444ffSAndreas Gohr $sql = "UPDATE assignments SET autoassignees = ''"; 344*fea1a86fSAndreas Gohr $sqlite->exec($sql); 345f09444ffSAndreas Gohr 346f09444ffSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all patterns */ 347f09444ffSAndreas Gohr $sql = "DELETE FROM assignments_patterns"; 348*fea1a86fSAndreas Gohr $sqlite->exec($sql); 349f09444ffSAndreas Gohr 350f09444ffSAndreas Gohr // insert new patterns and gather affected pages 351f09444ffSAndreas Gohr $pages = []; 352f09444ffSAndreas Gohr 353f09444ffSAndreas Gohr $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)"; 354f09444ffSAndreas Gohr foreach ($patterns as $pattern => $assignees) { 355f09444ffSAndreas Gohr $pattern = trim($pattern); 356f09444ffSAndreas Gohr $assignees = trim($assignees); 357f09444ffSAndreas Gohr if (!$pattern || !$assignees) continue; 358*fea1a86fSAndreas Gohr $sqlite->exec($sql, [$pattern, $assignees]); 359f09444ffSAndreas Gohr 360f09444ffSAndreas Gohr // patterns may overlap, so we need to gather all affected pages first 361f09444ffSAndreas Gohr $affectedPages = $this->getPagesMatchingPattern($pattern); 362f09444ffSAndreas Gohr foreach ($affectedPages as $page) { 363f09444ffSAndreas Gohr if (isset($pages[$page])) { 364f09444ffSAndreas Gohr $pages[$page] .= ',' . $assignees; 365f09444ffSAndreas Gohr } else { 366f09444ffSAndreas Gohr $pages[$page] = $assignees; 367f09444ffSAndreas Gohr } 368f09444ffSAndreas Gohr } 369f09444ffSAndreas Gohr } 370f09444ffSAndreas Gohr 371f09444ffSAndreas Gohr $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?) 372f09444ffSAndreas Gohr ON CONFLICT(page) 373f09444ffSAndreas Gohr DO UPDATE SET autoassignees = ?"; 374f09444ffSAndreas Gohr foreach ($pages as $page => $assignees) { 375f09444ffSAndreas Gohr // remove duplicates and empty entries 376f09444ffSAndreas Gohr $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 377*fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees, $assignees]); 378f09444ffSAndreas Gohr } 379*fea1a86fSAndreas Gohr } catch (Exception $e) { 380*fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 381*fea1a86fSAndreas Gohr throw $e; 382*fea1a86fSAndreas Gohr } 383*fea1a86fSAndreas Gohr $sqlite->getPdo()->commit(); 384f09444ffSAndreas Gohr } 385f09444ffSAndreas Gohr 386f09444ffSAndreas Gohr /** 387f09444ffSAndreas Gohr * Get all known pages that match the given pattern 388f09444ffSAndreas Gohr * 389f09444ffSAndreas Gohr * @param $pattern 390f09444ffSAndreas Gohr * @return string[] 391f09444ffSAndreas Gohr */ 392639d4c50SAndreas Gohr public function getPagesMatchingPattern($pattern) 393639d4c50SAndreas Gohr { 394f09444ffSAndreas Gohr $sqlite = $this->getDB(); 395f09444ffSAndreas Gohr if (!$sqlite) return []; 396f09444ffSAndreas Gohr 397f09444ffSAndreas Gohr $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)"; 398*fea1a86fSAndreas Gohr $pages = $sqlite->queryAll($sql, $pattern); 399f09444ffSAndreas Gohr 400f09444ffSAndreas Gohr return array_column($pages, 'page'); 401f09444ffSAndreas Gohr } 402f09444ffSAndreas Gohr 403639d4c50SAndreas Gohr // endregion 404639d4c50SAndreas Gohr // region Acknowledgements 405ef3ab392SAndreas Gohr 406ef3ab392SAndreas Gohr /** 407ef3ab392SAndreas Gohr * Has the given user acknowledged the given page? 408ef3ab392SAndreas Gohr * 409ef3ab392SAndreas Gohr * @param string $page 410ef3ab392SAndreas Gohr * @param string $user 4115773dd37SAnna Dabrowska * @return bool|int timestamp of acknowledgement or false 412ef3ab392SAndreas Gohr */ 413ef3ab392SAndreas Gohr public function hasUserAcknowledged($page, $user) 414ef3ab392SAndreas Gohr { 415ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 416ef3ab392SAndreas Gohr if (!$sqlite) return false; 417ef3ab392SAndreas Gohr 418ef3ab392SAndreas Gohr $sql = "SELECT ack 419ef3ab392SAndreas Gohr FROM acks A, pages B 420ef3ab392SAndreas Gohr WHERE A.page = B.page 4215773dd37SAnna Dabrowska AND A.page = ? 4225773dd37SAnna Dabrowska AND A.user = ? 423ef3ab392SAndreas Gohr AND A.ack >= B.lastmod"; 424ef3ab392SAndreas Gohr 425*fea1a86fSAndreas Gohr $acktime = $sqlite->queryValue($sql, $page, $user); 426ef3ab392SAndreas Gohr 427ef3ab392SAndreas Gohr return $acktime ? (int)$acktime : false; 428ef3ab392SAndreas Gohr } 4295773dd37SAnna Dabrowska 4305773dd37SAnna Dabrowska /** 431d9a8334dSAnna Dabrowska * Timestamp of the latest acknowledgment of the given page 432d9a8334dSAnna Dabrowska * by the given user 433d9a8334dSAnna Dabrowska * 434d9a8334dSAnna Dabrowska * @param string $page 435d9a8334dSAnna Dabrowska * @param string $user 436d9a8334dSAnna Dabrowska * @return bool|string 437d9a8334dSAnna Dabrowska */ 438d9a8334dSAnna Dabrowska public function getLatestUserAcknowledgement($page, $user) 439d9a8334dSAnna Dabrowska { 440d9a8334dSAnna Dabrowska $sqlite = $this->getDB(); 441d9a8334dSAnna Dabrowska if (!$sqlite) return false; 442d9a8334dSAnna Dabrowska 443d9a8334dSAnna Dabrowska $sql = "SELECT MAX(ack) 444d9a8334dSAnna Dabrowska FROM acks 445d9a8334dSAnna Dabrowska WHERE page = ? 446d9a8334dSAnna Dabrowska AND user = ?"; 447d9a8334dSAnna Dabrowska 448*fea1a86fSAndreas Gohr return $sqlite->queryValue($sql, [$page, $user]); 449d9a8334dSAnna Dabrowska } 450d9a8334dSAnna Dabrowska 451d9a8334dSAnna Dabrowska /** 4525773dd37SAnna Dabrowska * Save user's acknowledgement for a given page 4535773dd37SAnna Dabrowska * 4545773dd37SAnna Dabrowska * @param string $page 4555773dd37SAnna Dabrowska * @param string $user 4565773dd37SAnna Dabrowska * @return bool 4575773dd37SAnna Dabrowska */ 4585773dd37SAnna Dabrowska public function saveAcknowledgement($page, $user) 4595773dd37SAnna Dabrowska { 4605773dd37SAnna Dabrowska $sqlite = $this->getDB(); 4615773dd37SAnna Dabrowska if (!$sqlite) return false; 4625773dd37SAnna Dabrowska 4638e55e483SAnna Dabrowska $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))"; 4645773dd37SAnna Dabrowska 465*fea1a86fSAndreas Gohr $sqlite->exec($sql, $page, $user); 4665773dd37SAnna Dabrowska return true; 4675773dd37SAnna Dabrowska 4685773dd37SAnna Dabrowska } 46974126d4bSAnna Dabrowska 47074126d4bSAnna Dabrowska /** 471863b6e48SAndreas Gohr * Get all pages a user needs to acknowledge and the last acknowledge date 472d6011abdSAnna Dabrowska * 473863b6e48SAndreas Gohr * @param string $user 474863b6e48SAndreas Gohr * @param array $groups 475d6011abdSAnna Dabrowska * @return array|bool 476d6011abdSAnna Dabrowska */ 477863b6e48SAndreas Gohr public function getUserAcknowledgements($user, $groups) 478d6011abdSAnna Dabrowska { 479d6011abdSAnna Dabrowska $sqlite = $this->getDB(); 480d6011abdSAnna Dabrowska if (!$sqlite) return false; 481d6011abdSAnna Dabrowska 482f09444ffSAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack 483863b6e48SAndreas Gohr FROM assignments A 484863b6e48SAndreas Gohr JOIN pages B 485863b6e48SAndreas Gohr ON A.page = B.page 486863b6e48SAndreas Gohr LEFT JOIN acks C 487863b6e48SAndreas Gohr ON A.page = C.page AND C.user = ? 488f09444ffSAndreas Gohr WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?) 489863b6e48SAndreas Gohr GROUP BY A.page 490863b6e48SAndreas Gohr ORDER BY A.page 491863b6e48SAndreas Gohr "; 492863b6e48SAndreas Gohr 493*fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, [$user, $user, implode('///', $groups)]); 494863b6e48SAndreas Gohr } 495863b6e48SAndreas Gohr 496863b6e48SAndreas Gohr /** 497c6d8c1d9SAndreas Gohr * Get ack status for all assigned users of a given page 498c6d8c1d9SAndreas Gohr * 499c6d8c1d9SAndreas Gohr * This can be slow! 500c6d8c1d9SAndreas Gohr * 501c6d8c1d9SAndreas Gohr * @param string $page 502c6d8c1d9SAndreas Gohr * @return array|false 503c6d8c1d9SAndreas Gohr */ 504c6d8c1d9SAndreas Gohr public function getPageAcknowledgements($page) 505c6d8c1d9SAndreas Gohr { 506c6d8c1d9SAndreas Gohr $users = $this->getPageAssignees($page); 507c6d8c1d9SAndreas Gohr if ($users === false) return false; 508c6d8c1d9SAndreas Gohr $sqlite = $this->getDB(); 509c6d8c1d9SAndreas Gohr if (!$sqlite) return false; 510c6d8c1d9SAndreas Gohr 511*fea1a86fSAndreas Gohr $ulist = join(',', array_map([$sqlite->getPdo(), 'quote'], $users)); 512c6d8c1d9SAndreas Gohr $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack 513c6d8c1d9SAndreas Gohr FROM pages A 514c6d8c1d9SAndreas Gohr LEFT JOIN acks B 515c6d8c1d9SAndreas Gohr ON A.page = B.page 516c6d8c1d9SAndreas Gohr AND B.user IN ($ulist) 517c6d8c1d9SAndreas Gohr WHERE A.page = ? 518c6d8c1d9SAndreas Gohr GROUP BY A.page, B.user 519c6d8c1d9SAndreas Gohr "; 520*fea1a86fSAndreas Gohr $acknowledgements = $sqlite->queryAll($sql, $page); 521c6d8c1d9SAndreas Gohr 522c6d8c1d9SAndreas Gohr // there should be at least one result, unless the page is unknown 523c6d8c1d9SAndreas Gohr if (!count($acknowledgements)) return false; 524c6d8c1d9SAndreas Gohr 525c6d8c1d9SAndreas Gohr $baseinfo = [ 526c6d8c1d9SAndreas Gohr 'page' => $acknowledgements[0]['page'], 527c6d8c1d9SAndreas Gohr 'lastmod' => $acknowledgements[0]['lastmod'], 528c6d8c1d9SAndreas Gohr 'user' => null, 529c6d8c1d9SAndreas Gohr 'ack' => null, 530c6d8c1d9SAndreas Gohr ]; 531c6d8c1d9SAndreas Gohr 532c6d8c1d9SAndreas Gohr // fill up the result with all users that never acknowledged the page 533c6d8c1d9SAndreas Gohr $combined = []; 534c6d8c1d9SAndreas Gohr foreach ($acknowledgements as $ack) { 535c6d8c1d9SAndreas Gohr if ($ack['user'] !== null) { 536c6d8c1d9SAndreas Gohr $combined[$ack['user']] = $ack; 537c6d8c1d9SAndreas Gohr } 538c6d8c1d9SAndreas Gohr } 539c6d8c1d9SAndreas Gohr foreach ($users as $user) { 540c6d8c1d9SAndreas Gohr if (!isset($combined[$user])) { 541c6d8c1d9SAndreas Gohr $combined[$user] = array_merge($baseinfo, ['user' => $user]); 542c6d8c1d9SAndreas Gohr } 543c6d8c1d9SAndreas Gohr } 544c6d8c1d9SAndreas Gohr 545c6d8c1d9SAndreas Gohr ksort($combined); 546c6d8c1d9SAndreas Gohr return array_values($combined); 547c6d8c1d9SAndreas Gohr } 548c6d8c1d9SAndreas Gohr 549c6d8c1d9SAndreas Gohr /** 550863b6e48SAndreas Gohr * Returns all acknowledgements 551863b6e48SAndreas Gohr * 552863b6e48SAndreas Gohr * @param int $limit maximum number of results 553863b6e48SAndreas Gohr * @return array|bool 554863b6e48SAndreas Gohr */ 555863b6e48SAndreas Gohr public function getAcknowledgements($limit = 100) 556863b6e48SAndreas Gohr { 557863b6e48SAndreas Gohr $sqlite = $this->getDB(); 558863b6e48SAndreas Gohr if (!$sqlite) return false; 559863b6e48SAndreas Gohr 560863b6e48SAndreas Gohr $sql = ' 56184db77b6SAndreas Gohr SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack 56284db77b6SAndreas Gohr FROM acks A, pages B 56384db77b6SAndreas Gohr WHERE A.page = B.page 56484db77b6SAndreas Gohr GROUP BY A.user, A.page 565863b6e48SAndreas Gohr ORDER BY ack DESC 566863b6e48SAndreas Gohr LIMIT ? 567863b6e48SAndreas Gohr '; 568*fea1a86fSAndreas Gohr $acknowledgements = $sqlite->queryAll($sql, $limit); 569d6011abdSAnna Dabrowska 570d6011abdSAnna Dabrowska return $acknowledgements; 571d6011abdSAnna Dabrowska } 572f09444ffSAndreas Gohr 573639d4c50SAndreas Gohr // endregion 5744d6d17d0SAndreas Gohr} 5754d6d17d0SAndreas Gohr 576