14d6d17d0SAndreas Gohr<?php 2c6d8c1d9SAndreas Gohr 3fea1a86fSAndreas Gohruse dokuwiki\ErrorHandler; 4c6d8c1d9SAndreas Gohruse dokuwiki\Extension\AuthPlugin; 5fea1a86fSAndreas 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 16fea1a86fSAndreas Gohr protected $db; 17fea1a86fSAndreas Gohr 18639d4c50SAndreas Gohr // region Database Management 19639d4c50SAndreas Gohr 20cabb51d3SAndreas Gohr /** 21fea1a86fSAndreas Gohr * Get SQLiteDB instance 22fea1a86fSAndreas Gohr * 23fea1a86fSAndreas Gohr * @return SQLiteDB|null 24cabb51d3SAndreas Gohr */ 25cabb51d3SAndreas Gohr public function getDB() 26cabb51d3SAndreas Gohr { 27fea1a86fSAndreas Gohr if ($this->db === null) { 28fea1a86fSAndreas Gohr try { 29fea1a86fSAndreas Gohr $this->db = new SQLiteDB('acknowledgement', __DIR__ . '/db'); 30fea1a86fSAndreas Gohr 31fea1a86fSAndreas Gohr // register our custom functions 32fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('AUTH_ISMEMBER', [$this, 'auth_isMember'], -1); 33fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('MATCHES_PAGE_PATTERN', [$this, 'matchPagePattern'], 2); 34fea1a86fSAndreas Gohr } catch (\Exception $exception) { 35fea1a86fSAndreas Gohr if (defined('DOKU_UNITTEST')) throw new \RuntimeException('Could not load SQLite', 0, $exception); 36fea1a86fSAndreas Gohr ErrorHandler::logException($exception); 37cabb51d3SAndreas Gohr msg($this->getLang('error sqlite plugin missing'), -1); 38cabb51d3SAndreas Gohr return null; 39cabb51d3SAndreas Gohr } 40cabb51d3SAndreas Gohr } 41fea1a86fSAndreas 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 69fea1a86fSAndreas 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) { 74fea1a86fSAndreas Gohr try { 75fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $lastmod]); 76fea1a86fSAndreas Gohr } catch (\Exception $exception) { 77fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 78fea1a86fSAndreas Gohr throw $exception; 79639d4c50SAndreas Gohr } 80639d4c50SAndreas Gohr } 81fea1a86fSAndreas Gohr } 82fea1a86fSAndreas 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 = ?"; 140fea1a86fSAndreas 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 (?,?)"; 163fea1a86fSAndreas 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 = ?"; 180fea1a86fSAndreas 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 (?,?)"; 198fea1a86fSAndreas 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 (?,?)"; 227fea1a86fSAndreas 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 = ?"; 244fea1a86fSAndreas Gohr $record = $sqlite->queryRecord($sql, $page); 245a806aa3dSSven if (!$record) return false; 246fea1a86fSAndreas Gohr $assignees = $record['pageassignees'] . ',' . $record['autoassignees']; 247639d4c50SAndreas Gohr return auth_isMember($assignees, $user, $groups); 248639d4c50SAndreas Gohr } 249639d4c50SAndreas Gohr 250639d4c50SAndreas Gohr /** 251639d4c50SAndreas Gohr * Fetch all assignments for a given user, with additional page information, 252*833123deSAnna Dabrowska * by default filtering already granted acknowledgements. 253*833123deSAnna Dabrowska * Filter can be switched off via $includeDone 254639d4c50SAndreas Gohr * 255639d4c50SAndreas Gohr * @param string $user 256639d4c50SAndreas Gohr * @param array $groups 257*833123deSAnna Dabrowska * @param bool $includeDone 258*833123deSAnna Dabrowska * 259639d4c50SAndreas Gohr * @return array|bool 260639d4c50SAndreas Gohr */ 261*833123deSAnna Dabrowska public function getUserAssignments($user, $groups, $includeDone = false) 262639d4c50SAndreas Gohr { 263639d4c50SAndreas Gohr $sqlite = $this->getDB(); 264639d4c50SAndreas Gohr if (!$sqlite) return false; 265639d4c50SAndreas Gohr 266639d4c50SAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A 267639d4c50SAndreas Gohr JOIN pages B 268639d4c50SAndreas Gohr ON A.page = B.page 269639d4c50SAndreas Gohr LEFT JOIN acks C 270639d4c50SAndreas Gohr ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) ) 271*833123deSAnna Dabrowska WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)"; 272*833123deSAnna Dabrowska 273*833123deSAnna Dabrowska if (!$includeDone) { 274*833123deSAnna Dabrowska $sql .= ' AND ack IS NULL'; 275*833123deSAnna Dabrowska } 276639d4c50SAndreas Gohr 277fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, $user, $user, implode('///', $groups)); 278639d4c50SAndreas Gohr } 279639d4c50SAndreas Gohr 280639d4c50SAndreas Gohr 281639d4c50SAndreas Gohr /** 282639d4c50SAndreas Gohr * Resolve names of users assigned to a given page 283639d4c50SAndreas Gohr * 284639d4c50SAndreas Gohr * This can be slow on huge user bases! 285639d4c50SAndreas Gohr * 286639d4c50SAndreas Gohr * @param string $page 287639d4c50SAndreas Gohr * @return array|false 288639d4c50SAndreas Gohr */ 289639d4c50SAndreas Gohr public function getPageAssignees($page) 290639d4c50SAndreas Gohr { 291639d4c50SAndreas Gohr $sqlite = $this->getDB(); 292639d4c50SAndreas Gohr if (!$sqlite) return false; 293639d4c50SAndreas Gohr /** @var AuthPlugin $auth */ 294639d4c50SAndreas Gohr global $auth; 295639d4c50SAndreas Gohr 296639d4c50SAndreas Gohr $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments' 297639d4c50SAndreas Gohr FROM assignments 298639d4c50SAndreas Gohr WHERE page = ?"; 299fea1a86fSAndreas Gohr $assignments = $sqlite->queryValue($sql, $page); 300639d4c50SAndreas Gohr 301639d4c50SAndreas Gohr $users = []; 302639d4c50SAndreas Gohr foreach (explode(',', $assignments) as $item) { 303639d4c50SAndreas Gohr $item = trim($item); 304639d4c50SAndreas Gohr if ($item === '') continue; 305639d4c50SAndreas Gohr if ($item[0] == '@') { 306639d4c50SAndreas Gohr $users = array_merge( 307639d4c50SAndreas Gohr $users, 308639d4c50SAndreas Gohr array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)])) 309639d4c50SAndreas Gohr ); 310639d4c50SAndreas Gohr } else { 311639d4c50SAndreas Gohr $users[] = $item; 312639d4c50SAndreas Gohr } 313639d4c50SAndreas Gohr } 314639d4c50SAndreas Gohr 315639d4c50SAndreas Gohr return array_unique($users); 316639d4c50SAndreas Gohr } 317639d4c50SAndreas Gohr 318639d4c50SAndreas Gohr // endregion 319639d4c50SAndreas Gohr // region Assignment Patterns 320639d4c50SAndreas Gohr 321639d4c50SAndreas Gohr /** 322f09444ffSAndreas Gohr * Get all the assignment patterns 323f09444ffSAndreas Gohr * @return array (pattern => assignees) 324f09444ffSAndreas Gohr */ 325f09444ffSAndreas Gohr public function getAssignmentPatterns() 326f09444ffSAndreas Gohr { 327f09444ffSAndreas Gohr $sqlite = $this->getDB(); 328f09444ffSAndreas Gohr if (!$sqlite) return []; 329f09444ffSAndreas Gohr 330f09444ffSAndreas Gohr $sql = "SELECT pattern, assignees FROM assignments_patterns"; 331fea1a86fSAndreas Gohr return $sqlite->queryKeyValueList($sql); 332f09444ffSAndreas Gohr } 333f09444ffSAndreas Gohr 334f09444ffSAndreas Gohr /** 335f09444ffSAndreas Gohr * Save new assignment patterns 336f09444ffSAndreas Gohr * 337f09444ffSAndreas Gohr * This resaves all patterns and reapplies them 338f09444ffSAndreas Gohr * 339f09444ffSAndreas Gohr * @param array $patterns (pattern => assignees) 340f09444ffSAndreas Gohr */ 341639d4c50SAndreas Gohr public function saveAssignmentPatterns($patterns) 342639d4c50SAndreas Gohr { 343f09444ffSAndreas Gohr $sqlite = $this->getDB(); 344f09444ffSAndreas Gohr if (!$sqlite) return; 345f09444ffSAndreas Gohr 346fea1a86fSAndreas Gohr $sqlite->getPdo()->beginTransaction(); 347fea1a86fSAndreas Gohr try { 348f09444ffSAndreas Gohr 349fea1a86fSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all assignments */ 350f09444ffSAndreas Gohr $sql = "UPDATE assignments SET autoassignees = ''"; 351fea1a86fSAndreas Gohr $sqlite->exec($sql); 352f09444ffSAndreas Gohr 353f09444ffSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all patterns */ 354f09444ffSAndreas Gohr $sql = "DELETE FROM assignments_patterns"; 355fea1a86fSAndreas Gohr $sqlite->exec($sql); 356f09444ffSAndreas Gohr 357f09444ffSAndreas Gohr // insert new patterns and gather affected pages 358f09444ffSAndreas Gohr $pages = []; 359f09444ffSAndreas Gohr 360f09444ffSAndreas Gohr $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)"; 361f09444ffSAndreas Gohr foreach ($patterns as $pattern => $assignees) { 362f09444ffSAndreas Gohr $pattern = trim($pattern); 363f09444ffSAndreas Gohr $assignees = trim($assignees); 364f09444ffSAndreas Gohr if (!$pattern || !$assignees) continue; 365fea1a86fSAndreas Gohr $sqlite->exec($sql, [$pattern, $assignees]); 366f09444ffSAndreas Gohr 367f09444ffSAndreas Gohr // patterns may overlap, so we need to gather all affected pages first 368f09444ffSAndreas Gohr $affectedPages = $this->getPagesMatchingPattern($pattern); 369f09444ffSAndreas Gohr foreach ($affectedPages as $page) { 370f09444ffSAndreas Gohr if (isset($pages[$page])) { 371f09444ffSAndreas Gohr $pages[$page] .= ',' . $assignees; 372f09444ffSAndreas Gohr } else { 373f09444ffSAndreas Gohr $pages[$page] = $assignees; 374f09444ffSAndreas Gohr } 375f09444ffSAndreas Gohr } 376f09444ffSAndreas Gohr } 377f09444ffSAndreas Gohr 378f09444ffSAndreas Gohr $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?) 379f09444ffSAndreas Gohr ON CONFLICT(page) 380f09444ffSAndreas Gohr DO UPDATE SET autoassignees = ?"; 381f09444ffSAndreas Gohr foreach ($pages as $page => $assignees) { 382f09444ffSAndreas Gohr // remove duplicates and empty entries 383f09444ffSAndreas Gohr $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 384fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees, $assignees]); 385f09444ffSAndreas Gohr } 386fea1a86fSAndreas Gohr } catch (Exception $e) { 387fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 388fea1a86fSAndreas Gohr throw $e; 389fea1a86fSAndreas Gohr } 390fea1a86fSAndreas Gohr $sqlite->getPdo()->commit(); 391f09444ffSAndreas Gohr } 392f09444ffSAndreas Gohr 393f09444ffSAndreas Gohr /** 394f09444ffSAndreas Gohr * Get all known pages that match the given pattern 395f09444ffSAndreas Gohr * 396f09444ffSAndreas Gohr * @param $pattern 397f09444ffSAndreas Gohr * @return string[] 398f09444ffSAndreas Gohr */ 399639d4c50SAndreas Gohr public function getPagesMatchingPattern($pattern) 400639d4c50SAndreas Gohr { 401f09444ffSAndreas Gohr $sqlite = $this->getDB(); 402f09444ffSAndreas Gohr if (!$sqlite) return []; 403f09444ffSAndreas Gohr 404f09444ffSAndreas Gohr $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)"; 405fea1a86fSAndreas Gohr $pages = $sqlite->queryAll($sql, $pattern); 406f09444ffSAndreas Gohr 407f09444ffSAndreas Gohr return array_column($pages, 'page'); 408f09444ffSAndreas Gohr } 409f09444ffSAndreas Gohr 410639d4c50SAndreas Gohr // endregion 411639d4c50SAndreas Gohr // region Acknowledgements 412ef3ab392SAndreas Gohr 413ef3ab392SAndreas Gohr /** 414ef3ab392SAndreas Gohr * Has the given user acknowledged the given page? 415ef3ab392SAndreas Gohr * 416ef3ab392SAndreas Gohr * @param string $page 417ef3ab392SAndreas Gohr * @param string $user 4185773dd37SAnna Dabrowska * @return bool|int timestamp of acknowledgement or false 419ef3ab392SAndreas Gohr */ 420ef3ab392SAndreas Gohr public function hasUserAcknowledged($page, $user) 421ef3ab392SAndreas Gohr { 422ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 423ef3ab392SAndreas Gohr if (!$sqlite) return false; 424ef3ab392SAndreas Gohr 425ef3ab392SAndreas Gohr $sql = "SELECT ack 426ef3ab392SAndreas Gohr FROM acks A, pages B 427ef3ab392SAndreas Gohr WHERE A.page = B.page 4285773dd37SAnna Dabrowska AND A.page = ? 4295773dd37SAnna Dabrowska AND A.user = ? 430ef3ab392SAndreas Gohr AND A.ack >= B.lastmod"; 431ef3ab392SAndreas Gohr 432fea1a86fSAndreas Gohr $acktime = $sqlite->queryValue($sql, $page, $user); 433ef3ab392SAndreas Gohr 434ef3ab392SAndreas Gohr return $acktime ? (int)$acktime : false; 435ef3ab392SAndreas Gohr } 4365773dd37SAnna Dabrowska 4375773dd37SAnna Dabrowska /** 438d9a8334dSAnna Dabrowska * Timestamp of the latest acknowledgment of the given page 439d9a8334dSAnna Dabrowska * by the given user 440d9a8334dSAnna Dabrowska * 441d9a8334dSAnna Dabrowska * @param string $page 442d9a8334dSAnna Dabrowska * @param string $user 443d9a8334dSAnna Dabrowska * @return bool|string 444d9a8334dSAnna Dabrowska */ 445d9a8334dSAnna Dabrowska public function getLatestUserAcknowledgement($page, $user) 446d9a8334dSAnna Dabrowska { 447d9a8334dSAnna Dabrowska $sqlite = $this->getDB(); 448d9a8334dSAnna Dabrowska if (!$sqlite) return false; 449d9a8334dSAnna Dabrowska 450d9a8334dSAnna Dabrowska $sql = "SELECT MAX(ack) 451d9a8334dSAnna Dabrowska FROM acks 452d9a8334dSAnna Dabrowska WHERE page = ? 453d9a8334dSAnna Dabrowska AND user = ?"; 454d9a8334dSAnna Dabrowska 455fea1a86fSAndreas Gohr return $sqlite->queryValue($sql, [$page, $user]); 456d9a8334dSAnna Dabrowska } 457d9a8334dSAnna Dabrowska 458d9a8334dSAnna Dabrowska /** 4595773dd37SAnna Dabrowska * Save user's acknowledgement for a given page 4605773dd37SAnna Dabrowska * 4615773dd37SAnna Dabrowska * @param string $page 4625773dd37SAnna Dabrowska * @param string $user 4635773dd37SAnna Dabrowska * @return bool 4645773dd37SAnna Dabrowska */ 4655773dd37SAnna Dabrowska public function saveAcknowledgement($page, $user) 4665773dd37SAnna Dabrowska { 4675773dd37SAnna Dabrowska $sqlite = $this->getDB(); 4685773dd37SAnna Dabrowska if (!$sqlite) return false; 4695773dd37SAnna Dabrowska 4708e55e483SAnna Dabrowska $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))"; 4715773dd37SAnna Dabrowska 472fea1a86fSAndreas Gohr $sqlite->exec($sql, $page, $user); 4735773dd37SAnna Dabrowska return true; 4745773dd37SAnna Dabrowska 4755773dd37SAnna Dabrowska } 47674126d4bSAnna Dabrowska 47774126d4bSAnna Dabrowska /** 478863b6e48SAndreas Gohr * Get all pages a user needs to acknowledge and the last acknowledge date 479d6011abdSAnna Dabrowska * 480863b6e48SAndreas Gohr * @param string $user 481863b6e48SAndreas Gohr * @param array $groups 482d6011abdSAnna Dabrowska * @return array|bool 483d6011abdSAnna Dabrowska */ 484863b6e48SAndreas Gohr public function getUserAcknowledgements($user, $groups) 485d6011abdSAnna Dabrowska { 486d6011abdSAnna Dabrowska $sqlite = $this->getDB(); 487d6011abdSAnna Dabrowska if (!$sqlite) return false; 488d6011abdSAnna Dabrowska 489f09444ffSAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack 490863b6e48SAndreas Gohr FROM assignments A 491863b6e48SAndreas Gohr JOIN pages B 492863b6e48SAndreas Gohr ON A.page = B.page 493863b6e48SAndreas Gohr LEFT JOIN acks C 494863b6e48SAndreas Gohr ON A.page = C.page AND C.user = ? 495f09444ffSAndreas Gohr WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?) 496863b6e48SAndreas Gohr GROUP BY A.page 497863b6e48SAndreas Gohr ORDER BY A.page 498863b6e48SAndreas Gohr "; 499863b6e48SAndreas Gohr 500fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, [$user, $user, implode('///', $groups)]); 501863b6e48SAndreas Gohr } 502863b6e48SAndreas Gohr 503863b6e48SAndreas Gohr /** 504c6d8c1d9SAndreas Gohr * Get ack status for all assigned users of a given page 505c6d8c1d9SAndreas Gohr * 506c6d8c1d9SAndreas Gohr * This can be slow! 507c6d8c1d9SAndreas Gohr * 508c6d8c1d9SAndreas Gohr * @param string $page 509c6d8c1d9SAndreas Gohr * @return array|false 510c6d8c1d9SAndreas Gohr */ 511b6817aacSAndreas Gohr public function getPageAcknowledgements($page, $max=0) 512c6d8c1d9SAndreas Gohr { 513c6d8c1d9SAndreas Gohr $users = $this->getPageAssignees($page); 514c6d8c1d9SAndreas Gohr if ($users === false) return false; 515c6d8c1d9SAndreas Gohr $sqlite = $this->getDB(); 516c6d8c1d9SAndreas Gohr if (!$sqlite) return false; 517c6d8c1d9SAndreas Gohr 518fea1a86fSAndreas Gohr $ulist = join(',', array_map([$sqlite->getPdo(), 'quote'], $users)); 519c6d8c1d9SAndreas Gohr $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack 520c6d8c1d9SAndreas Gohr FROM pages A 521c6d8c1d9SAndreas Gohr LEFT JOIN acks B 522c6d8c1d9SAndreas Gohr ON A.page = B.page 523c6d8c1d9SAndreas Gohr AND B.user IN ($ulist) 524c6d8c1d9SAndreas Gohr WHERE A.page = ? 525c6d8c1d9SAndreas Gohr GROUP BY A.page, B.user 526c6d8c1d9SAndreas Gohr "; 527b6817aacSAndreas Gohr if($max) $sql .= " LIMIT $max"; 528fea1a86fSAndreas Gohr $acknowledgements = $sqlite->queryAll($sql, $page); 529c6d8c1d9SAndreas Gohr 530c6d8c1d9SAndreas Gohr // there should be at least one result, unless the page is unknown 531c6d8c1d9SAndreas Gohr if (!count($acknowledgements)) return false; 532c6d8c1d9SAndreas Gohr 533c6d8c1d9SAndreas Gohr $baseinfo = [ 534c6d8c1d9SAndreas Gohr 'page' => $acknowledgements[0]['page'], 535c6d8c1d9SAndreas Gohr 'lastmod' => $acknowledgements[0]['lastmod'], 536c6d8c1d9SAndreas Gohr 'user' => null, 537c6d8c1d9SAndreas Gohr 'ack' => null, 538c6d8c1d9SAndreas Gohr ]; 539c6d8c1d9SAndreas Gohr 540c6d8c1d9SAndreas Gohr // fill up the result with all users that never acknowledged the page 541c6d8c1d9SAndreas Gohr $combined = []; 542c6d8c1d9SAndreas Gohr foreach ($acknowledgements as $ack) { 543c6d8c1d9SAndreas Gohr if ($ack['user'] !== null) { 544c6d8c1d9SAndreas Gohr $combined[$ack['user']] = $ack; 545c6d8c1d9SAndreas Gohr } 546c6d8c1d9SAndreas Gohr } 547c6d8c1d9SAndreas Gohr foreach ($users as $user) { 548c6d8c1d9SAndreas Gohr if (!isset($combined[$user])) { 549c6d8c1d9SAndreas Gohr $combined[$user] = array_merge($baseinfo, ['user' => $user]); 550c6d8c1d9SAndreas Gohr } 551c6d8c1d9SAndreas Gohr } 552c6d8c1d9SAndreas Gohr 553c6d8c1d9SAndreas Gohr ksort($combined); 554c6d8c1d9SAndreas Gohr return array_values($combined); 555c6d8c1d9SAndreas Gohr } 556c6d8c1d9SAndreas Gohr 557c6d8c1d9SAndreas Gohr /** 558863b6e48SAndreas Gohr * Returns all acknowledgements 559863b6e48SAndreas Gohr * 560863b6e48SAndreas Gohr * @param int $limit maximum number of results 561863b6e48SAndreas Gohr * @return array|bool 562863b6e48SAndreas Gohr */ 563863b6e48SAndreas Gohr public function getAcknowledgements($limit = 100) 564863b6e48SAndreas Gohr { 565863b6e48SAndreas Gohr $sqlite = $this->getDB(); 566863b6e48SAndreas Gohr if (!$sqlite) return false; 567863b6e48SAndreas Gohr 568863b6e48SAndreas Gohr $sql = ' 56984db77b6SAndreas Gohr SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack 57084db77b6SAndreas Gohr FROM acks A, pages B 57184db77b6SAndreas Gohr WHERE A.page = B.page 57284db77b6SAndreas Gohr GROUP BY A.user, A.page 573863b6e48SAndreas Gohr ORDER BY ack DESC 574863b6e48SAndreas Gohr LIMIT ? 575863b6e48SAndreas Gohr '; 576fea1a86fSAndreas Gohr $acknowledgements = $sqlite->queryAll($sql, $limit); 577d6011abdSAnna Dabrowska 578d6011abdSAnna Dabrowska return $acknowledgements; 579d6011abdSAnna Dabrowska } 580f09444ffSAndreas Gohr 581639d4c50SAndreas Gohr // endregion 5824d6d17d0SAndreas Gohr} 5834d6d17d0SAndreas Gohr 584