14d6d17d0SAndreas Gohr<?php 2c6d8c1d9SAndreas Gohr 33b76424dSanndause dokuwiki\Extension\Plugin; 43b76424dSanndause dokuwiki\ChangeLog\PageChangeLog; 5fea1a86fSAndreas Gohruse dokuwiki\ErrorHandler; 6c6d8c1d9SAndreas Gohruse dokuwiki\Extension\AuthPlugin; 7fea1a86fSAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB; 8c6d8c1d9SAndreas Gohr 94d6d17d0SAndreas Gohr/** 104d6d17d0SAndreas Gohr * DokuWiki Plugin acknowledge (Helper Component) 114d6d17d0SAndreas Gohr * 124d6d17d0SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 134d6d17d0SAndreas Gohr * @author Andreas Gohr, Anna Dabrowska <dokuwiki@cosmocode.de> 144d6d17d0SAndreas Gohr */ 153b76424dSanndaclass helper_plugin_acknowledge extends Plugin 164d6d17d0SAndreas Gohr{ 17fea1a86fSAndreas Gohr protected $db; 18fea1a86fSAndreas Gohr 19639d4c50SAndreas Gohr // region Database Management 20639d4c50SAndreas Gohr 21cabb51d3SAndreas Gohr /** 22fea1a86fSAndreas Gohr * Get SQLiteDB instance 23fea1a86fSAndreas Gohr * 24fea1a86fSAndreas Gohr * @return SQLiteDB|null 25cabb51d3SAndreas Gohr */ 26cabb51d3SAndreas Gohr public function getDB() 27cabb51d3SAndreas Gohr { 28fea1a86fSAndreas Gohr if ($this->db === null) { 29fea1a86fSAndreas Gohr try { 30fea1a86fSAndreas Gohr $this->db = new SQLiteDB('acknowledgement', __DIR__ . '/db'); 31fea1a86fSAndreas Gohr 32fea1a86fSAndreas Gohr // register our custom functions 33fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('AUTH_ISMEMBER', [$this, 'auth_isMember'], -1); 34fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('MATCHES_PAGE_PATTERN', [$this, 'matchPagePattern'], 2); 35fea1a86fSAndreas Gohr } catch (\Exception $exception) { 36fea1a86fSAndreas Gohr if (defined('DOKU_UNITTEST')) throw new \RuntimeException('Could not load SQLite', 0, $exception); 37fea1a86fSAndreas Gohr ErrorHandler::logException($exception); 38cabb51d3SAndreas Gohr msg($this->getLang('error sqlite plugin missing'), -1); 39cabb51d3SAndreas Gohr return null; 40cabb51d3SAndreas Gohr } 41cabb51d3SAndreas Gohr } 42fea1a86fSAndreas Gohr return $this->db; 439c3eae1eSAnna Dabrowska } 449c3eae1eSAnna Dabrowska 459c3eae1eSAnna Dabrowska /** 469c3eae1eSAnna Dabrowska * Wrapper function for auth_isMember which accepts groups as string 479c3eae1eSAnna Dabrowska * 489c3eae1eSAnna Dabrowska * @param string $memberList 499c3eae1eSAnna Dabrowska * @param string $user 509c3eae1eSAnna Dabrowska * @param string $groups 51*ba917e33SAnna Dabrowska * 529c3eae1eSAnna Dabrowska * @return bool 539c3eae1eSAnna Dabrowska */ 54*ba917e33SAnna Dabrowska // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 559c3eae1eSAnna Dabrowska public function auth_isMember($memberList, $user, $groups) 569c3eae1eSAnna Dabrowska { 5795113ed8SAnna Dabrowska return auth_isMember($memberList, $user, explode('///', $groups)); 589c3eae1eSAnna Dabrowska } 599c3eae1eSAnna Dabrowska 609c3eae1eSAnna Dabrowska /** 61639d4c50SAndreas Gohr * Fills the page index with all unknown pages from the fulltext index 62639d4c50SAndreas Gohr * @return void 63639d4c50SAndreas Gohr */ 64639d4c50SAndreas Gohr public function updatePageIndex() 65639d4c50SAndreas Gohr { 66639d4c50SAndreas Gohr $sqlite = $this->getDB(); 67639d4c50SAndreas Gohr if (!$sqlite) return; 68639d4c50SAndreas Gohr 69639d4c50SAndreas Gohr $pages = idx_getIndex('page', ''); 70639d4c50SAndreas Gohr $sql = "INSERT OR IGNORE INTO pages (page, lastmod) VALUES (?,?)"; 71639d4c50SAndreas Gohr 72fea1a86fSAndreas Gohr $sqlite->getPdo()->beginTransaction(); 73639d4c50SAndreas Gohr foreach ($pages as $page) { 74639d4c50SAndreas Gohr $page = trim($page); 75639d4c50SAndreas Gohr $lastmod = @filemtime(wikiFN($page)); 76639d4c50SAndreas Gohr if ($lastmod) { 77fea1a86fSAndreas Gohr try { 78fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $lastmod]); 79fea1a86fSAndreas Gohr } catch (\Exception $exception) { 80fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 81fea1a86fSAndreas Gohr throw $exception; 82639d4c50SAndreas Gohr } 83639d4c50SAndreas Gohr } 84fea1a86fSAndreas Gohr } 85fea1a86fSAndreas Gohr $sqlite->getPdo()->commit(); 86639d4c50SAndreas Gohr } 87639d4c50SAndreas Gohr 88639d4c50SAndreas Gohr /** 89639d4c50SAndreas Gohr * Check if the given pattern matches the given page 90639d4c50SAndreas Gohr * 91639d4c50SAndreas Gohr * @param string $pattern the pattern to check against 92639d4c50SAndreas Gohr * @param string $page the cleaned pageid to check 93639d4c50SAndreas Gohr * @return bool 94639d4c50SAndreas Gohr */ 95639d4c50SAndreas Gohr public function matchPagePattern($pattern, $page) 96639d4c50SAndreas Gohr { 97639d4c50SAndreas Gohr if (trim($pattern, ':') == '**') return true; // match all 98639d4c50SAndreas Gohr 99639d4c50SAndreas Gohr // regex patterns 100639d4c50SAndreas Gohr if ($pattern[0] == '/') { 101639d4c50SAndreas Gohr return (bool)preg_match($pattern, ":$page"); 102639d4c50SAndreas Gohr } 103639d4c50SAndreas Gohr 104639d4c50SAndreas Gohr $pns = ':' . getNS($page) . ':'; 105639d4c50SAndreas Gohr 106639d4c50SAndreas Gohr $ans = ':' . cleanID($pattern) . ':'; 107639d4c50SAndreas Gohr if (substr($pattern, -2) == '**') { 108639d4c50SAndreas Gohr // upper namespaces match 109639d4c50SAndreas Gohr if (strpos($pns, $ans) === 0) { 110639d4c50SAndreas Gohr return true; 111639d4c50SAndreas Gohr } 112639d4c50SAndreas Gohr } elseif (substr($pattern, -1) == '*') { 113639d4c50SAndreas Gohr // namespaces match exact 1143b76424dSannda if ($ans === $pns) { 115639d4c50SAndreas Gohr return true; 116639d4c50SAndreas Gohr } 1173b76424dSannda } elseif (cleanID($pattern) == $page) { 118639d4c50SAndreas Gohr // exact match 119639d4c50SAndreas Gohr return true; 120639d4c50SAndreas Gohr } 121639d4c50SAndreas Gohr 122639d4c50SAndreas Gohr return false; 123639d4c50SAndreas Gohr } 124639d4c50SAndreas Gohr 125639d4c50SAndreas Gohr // endregion 126639d4c50SAndreas Gohr // region Page Data 127639d4c50SAndreas Gohr 128639d4c50SAndreas Gohr /** 129ef3ab392SAndreas Gohr * Delete a page 130ef3ab392SAndreas Gohr * 131ef3ab392SAndreas Gohr * Cascades to delete all assigned data, etc. 132ef3ab392SAndreas Gohr * 133ef3ab392SAndreas Gohr * @param string $page Page ID 134ef3ab392SAndreas Gohr */ 135ef3ab392SAndreas Gohr public function removePage($page) 136ef3ab392SAndreas Gohr { 137ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 138ef3ab392SAndreas Gohr if (!$sqlite) return; 139ef3ab392SAndreas Gohr 140ef3ab392SAndreas Gohr $sql = "DELETE FROM pages WHERE page = ?"; 141fea1a86fSAndreas Gohr $sqlite->exec($sql, $page); 142ef3ab392SAndreas Gohr } 143ef3ab392SAndreas Gohr 144ef3ab392SAndreas Gohr /** 1455dee13f7SAnna Dabrowska * Update last modified date of page if content has changed 146ef3ab392SAndreas Gohr * 147ef3ab392SAndreas Gohr * @param string $page Page ID 148ef3ab392SAndreas Gohr * @param int $lastmod timestamp of last non-minor change 149ef3ab392SAndreas Gohr */ 1505dee13f7SAnna Dabrowska public function storePageDate($page, $lastmod, $newContent) 151ef3ab392SAndreas Gohr { 1523b76424dSannda $changelog = new PageChangeLog($page); 153789aa26fSAnna Dabrowska $revs = $changelog->getRevisions(0, 1); 154ed4e8871SAnna Dabrowska 155ed4e8871SAnna Dabrowska // compare content 156ed4e8871SAnna Dabrowska $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0]))); 157ed4e8871SAnna Dabrowska $newContent = str_replace(NL, '', $newContent); 158ed4e8871SAnna Dabrowska if ($oldContent === $newContent) return; 159ed4e8871SAnna Dabrowska 160ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 161ef3ab392SAndreas Gohr if (!$sqlite) return; 162ef3ab392SAndreas Gohr 163ef3ab392SAndreas Gohr $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)"; 164fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $lastmod]); 165ef3ab392SAndreas Gohr } 166ef3ab392SAndreas Gohr 167639d4c50SAndreas Gohr // endregion 168639d4c50SAndreas Gohr // region Assignments 169639d4c50SAndreas Gohr 170ef3ab392SAndreas Gohr /** 171f09444ffSAndreas Gohr * Clears direct assignments for a page 172f09444ffSAndreas Gohr * 173cabb51d3SAndreas Gohr * @param string $page Page ID 174cabb51d3SAndreas Gohr */ 175f09444ffSAndreas Gohr public function clearPageAssignments($page) 176cabb51d3SAndreas Gohr { 177cabb51d3SAndreas Gohr $sqlite = $this->getDB(); 178cabb51d3SAndreas Gohr if (!$sqlite) return; 179cabb51d3SAndreas Gohr 180f09444ffSAndreas Gohr $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?"; 181fea1a86fSAndreas Gohr $sqlite->exec($sql, $page); 182f09444ffSAndreas Gohr } 183f09444ffSAndreas Gohr 184f09444ffSAndreas Gohr /** 185639d4c50SAndreas Gohr * Set assignees for a given page as manually specified 186639d4c50SAndreas Gohr * 187639d4c50SAndreas Gohr * @param string $page Page ID 188639d4c50SAndreas Gohr * @param string $assignees 189639d4c50SAndreas Gohr * @return void 190639d4c50SAndreas Gohr */ 191639d4c50SAndreas Gohr public function setPageAssignees($page, $assignees) 192639d4c50SAndreas Gohr { 193639d4c50SAndreas Gohr $sqlite = $this->getDB(); 194639d4c50SAndreas Gohr if (!$sqlite) return; 195639d4c50SAndreas Gohr 1963b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 197639d4c50SAndreas Gohr 198639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)"; 199fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees]); 200639d4c50SAndreas Gohr } 201639d4c50SAndreas Gohr 202639d4c50SAndreas Gohr /** 203639d4c50SAndreas Gohr * Set assignees for a given page from the patterns 204639d4c50SAndreas Gohr * @param string $page Page ID 205639d4c50SAndreas Gohr */ 206639d4c50SAndreas Gohr public function setAutoAssignees($page) 207639d4c50SAndreas Gohr { 208639d4c50SAndreas Gohr $sqlite = $this->getDB(); 209639d4c50SAndreas Gohr if (!$sqlite) return; 210639d4c50SAndreas Gohr 211639d4c50SAndreas Gohr $patterns = $this->getAssignmentPatterns(); 212639d4c50SAndreas Gohr 213639d4c50SAndreas Gohr // given assignees 214639d4c50SAndreas Gohr $assignees = ''; 215639d4c50SAndreas Gohr 216639d4c50SAndreas Gohr // find all patterns that match the page and add the configured assignees 217639d4c50SAndreas Gohr foreach ($patterns as $pattern => $assignees) { 218639d4c50SAndreas Gohr if ($this->matchPagePattern($pattern, $page)) { 219639d4c50SAndreas Gohr $assignees .= ',' . $assignees; 220639d4c50SAndreas Gohr } 221639d4c50SAndreas Gohr } 222639d4c50SAndreas Gohr 223639d4c50SAndreas Gohr // remove duplicates and empty entries 2243b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 225639d4c50SAndreas Gohr 226639d4c50SAndreas Gohr // store the assignees 227639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)"; 228fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees]); 229639d4c50SAndreas Gohr } 230639d4c50SAndreas Gohr 231639d4c50SAndreas Gohr /** 232639d4c50SAndreas Gohr * Is the given user one of the assignees for this page 233639d4c50SAndreas Gohr * 234639d4c50SAndreas Gohr * @param string $page Page ID 235639d4c50SAndreas Gohr * @param string $user user name to check 236639d4c50SAndreas Gohr * @param string[] $groups groups this user is in 237639d4c50SAndreas Gohr * @return bool 238639d4c50SAndreas Gohr */ 239639d4c50SAndreas Gohr public function isUserAssigned($page, $user, $groups) 240639d4c50SAndreas Gohr { 241639d4c50SAndreas Gohr $sqlite = $this->getDB(); 242639d4c50SAndreas Gohr if (!$sqlite) return false; 243639d4c50SAndreas Gohr 244639d4c50SAndreas Gohr $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?"; 245fea1a86fSAndreas Gohr $record = $sqlite->queryRecord($sql, $page); 246a806aa3dSSven if (!$record) return false; 247fea1a86fSAndreas Gohr $assignees = $record['pageassignees'] . ',' . $record['autoassignees']; 248639d4c50SAndreas Gohr return auth_isMember($assignees, $user, $groups); 249639d4c50SAndreas Gohr } 250639d4c50SAndreas Gohr 251639d4c50SAndreas Gohr /** 252639d4c50SAndreas Gohr * Fetch all assignments for a given user, with additional page information, 253833123deSAnna Dabrowska * by default filtering already granted acknowledgements. 254833123deSAnna Dabrowska * Filter can be switched off via $includeDone 255639d4c50SAndreas Gohr * 256639d4c50SAndreas Gohr * @param string $user 257639d4c50SAndreas Gohr * @param array $groups 258833123deSAnna Dabrowska * @param bool $includeDone 259833123deSAnna Dabrowska * 260639d4c50SAndreas Gohr * @return array|bool 261639d4c50SAndreas Gohr */ 262833123deSAnna Dabrowska public function getUserAssignments($user, $groups, $includeDone = false) 263639d4c50SAndreas Gohr { 264639d4c50SAndreas Gohr $sqlite = $this->getDB(); 265639d4c50SAndreas Gohr if (!$sqlite) return false; 266639d4c50SAndreas Gohr 267639d4c50SAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A 268639d4c50SAndreas Gohr JOIN pages B 269639d4c50SAndreas Gohr ON A.page = B.page 270639d4c50SAndreas Gohr LEFT JOIN acks C 271639d4c50SAndreas Gohr ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) ) 272833123deSAnna Dabrowska WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)"; 273833123deSAnna Dabrowska 274833123deSAnna Dabrowska if (!$includeDone) { 275833123deSAnna Dabrowska $sql .= ' AND ack IS NULL'; 276833123deSAnna Dabrowska } 277639d4c50SAndreas Gohr 278fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, $user, $user, implode('///', $groups)); 279639d4c50SAndreas Gohr } 280639d4c50SAndreas Gohr 281639d4c50SAndreas Gohr 282639d4c50SAndreas Gohr /** 283639d4c50SAndreas Gohr * Resolve names of users assigned to a given page 284639d4c50SAndreas Gohr * 285639d4c50SAndreas Gohr * This can be slow on huge user bases! 286639d4c50SAndreas Gohr * 287639d4c50SAndreas Gohr * @param string $page 288639d4c50SAndreas Gohr * @return array|false 289639d4c50SAndreas Gohr */ 290639d4c50SAndreas Gohr public function getPageAssignees($page) 291639d4c50SAndreas Gohr { 292639d4c50SAndreas Gohr $sqlite = $this->getDB(); 293639d4c50SAndreas Gohr if (!$sqlite) return false; 294639d4c50SAndreas Gohr /** @var AuthPlugin $auth */ 295639d4c50SAndreas Gohr global $auth; 296639d4c50SAndreas Gohr 297639d4c50SAndreas Gohr $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments' 298639d4c50SAndreas Gohr FROM assignments 299639d4c50SAndreas Gohr WHERE page = ?"; 300fea1a86fSAndreas Gohr $assignments = $sqlite->queryValue($sql, $page); 301639d4c50SAndreas Gohr 302639d4c50SAndreas Gohr $users = []; 303639d4c50SAndreas Gohr foreach (explode(',', $assignments) as $item) { 304639d4c50SAndreas Gohr $item = trim($item); 305639d4c50SAndreas Gohr if ($item === '') continue; 306639d4c50SAndreas Gohr if ($item[0] == '@') { 307639d4c50SAndreas Gohr $users = array_merge( 308639d4c50SAndreas Gohr $users, 309639d4c50SAndreas Gohr array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)])) 310639d4c50SAndreas Gohr ); 311639d4c50SAndreas Gohr } else { 312639d4c50SAndreas Gohr $users[] = $item; 313639d4c50SAndreas Gohr } 314639d4c50SAndreas Gohr } 315639d4c50SAndreas Gohr 316639d4c50SAndreas Gohr return array_unique($users); 317639d4c50SAndreas Gohr } 318639d4c50SAndreas Gohr 319639d4c50SAndreas Gohr // endregion 320639d4c50SAndreas Gohr // region Assignment Patterns 321639d4c50SAndreas Gohr 322639d4c50SAndreas Gohr /** 323f09444ffSAndreas Gohr * Get all the assignment patterns 324f09444ffSAndreas Gohr * @return array (pattern => assignees) 325f09444ffSAndreas Gohr */ 326f09444ffSAndreas Gohr public function getAssignmentPatterns() 327f09444ffSAndreas Gohr { 328f09444ffSAndreas Gohr $sqlite = $this->getDB(); 329f09444ffSAndreas Gohr if (!$sqlite) return []; 330f09444ffSAndreas Gohr 331f09444ffSAndreas Gohr $sql = "SELECT pattern, assignees FROM assignments_patterns"; 332fea1a86fSAndreas Gohr return $sqlite->queryKeyValueList($sql); 333f09444ffSAndreas Gohr } 334f09444ffSAndreas Gohr 335f09444ffSAndreas Gohr /** 336f09444ffSAndreas Gohr * Save new assignment patterns 337f09444ffSAndreas Gohr * 338f09444ffSAndreas Gohr * This resaves all patterns and reapplies them 339f09444ffSAndreas Gohr * 340f09444ffSAndreas Gohr * @param array $patterns (pattern => assignees) 341f09444ffSAndreas Gohr */ 342639d4c50SAndreas Gohr public function saveAssignmentPatterns($patterns) 343639d4c50SAndreas Gohr { 344f09444ffSAndreas Gohr $sqlite = $this->getDB(); 345f09444ffSAndreas Gohr if (!$sqlite) return; 346f09444ffSAndreas Gohr 347fea1a86fSAndreas Gohr $sqlite->getPdo()->beginTransaction(); 348fea1a86fSAndreas Gohr try { 349f09444ffSAndreas Gohr 350fea1a86fSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all assignments */ 351f09444ffSAndreas Gohr $sql = "UPDATE assignments SET autoassignees = ''"; 352fea1a86fSAndreas Gohr $sqlite->exec($sql); 353f09444ffSAndreas Gohr 354f09444ffSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all patterns */ 355f09444ffSAndreas Gohr $sql = "DELETE FROM assignments_patterns"; 356fea1a86fSAndreas Gohr $sqlite->exec($sql); 357f09444ffSAndreas Gohr 358f09444ffSAndreas Gohr // insert new patterns and gather affected pages 359f09444ffSAndreas Gohr $pages = []; 360f09444ffSAndreas Gohr 361f09444ffSAndreas Gohr $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)"; 362f09444ffSAndreas Gohr foreach ($patterns as $pattern => $assignees) { 363f09444ffSAndreas Gohr $pattern = trim($pattern); 364f09444ffSAndreas Gohr $assignees = trim($assignees); 365f09444ffSAndreas Gohr if (!$pattern || !$assignees) continue; 366fea1a86fSAndreas Gohr $sqlite->exec($sql, [$pattern, $assignees]); 367f09444ffSAndreas Gohr 368f09444ffSAndreas Gohr // patterns may overlap, so we need to gather all affected pages first 369f09444ffSAndreas Gohr $affectedPages = $this->getPagesMatchingPattern($pattern); 370f09444ffSAndreas Gohr foreach ($affectedPages as $page) { 371f09444ffSAndreas Gohr if (isset($pages[$page])) { 372f09444ffSAndreas Gohr $pages[$page] .= ',' . $assignees; 373f09444ffSAndreas Gohr } else { 374f09444ffSAndreas Gohr $pages[$page] = $assignees; 375f09444ffSAndreas Gohr } 376f09444ffSAndreas Gohr } 377f09444ffSAndreas Gohr } 378f09444ffSAndreas Gohr 379f09444ffSAndreas Gohr $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?) 380f09444ffSAndreas Gohr ON CONFLICT(page) 381f09444ffSAndreas Gohr DO UPDATE SET autoassignees = ?"; 382f09444ffSAndreas Gohr foreach ($pages as $page => $assignees) { 383f09444ffSAndreas Gohr // remove duplicates and empty entries 3843b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 385fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees, $assignees]); 386f09444ffSAndreas Gohr } 387fea1a86fSAndreas Gohr } catch (Exception $e) { 388fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 389fea1a86fSAndreas Gohr throw $e; 390fea1a86fSAndreas Gohr } 391fea1a86fSAndreas Gohr $sqlite->getPdo()->commit(); 392f09444ffSAndreas Gohr } 393f09444ffSAndreas Gohr 394f09444ffSAndreas Gohr /** 395f09444ffSAndreas Gohr * Get all known pages that match the given pattern 396f09444ffSAndreas Gohr * 397f09444ffSAndreas Gohr * @param $pattern 398f09444ffSAndreas Gohr * @return string[] 399f09444ffSAndreas Gohr */ 400639d4c50SAndreas Gohr public function getPagesMatchingPattern($pattern) 401639d4c50SAndreas Gohr { 402f09444ffSAndreas Gohr $sqlite = $this->getDB(); 403f09444ffSAndreas Gohr if (!$sqlite) return []; 404f09444ffSAndreas Gohr 405f09444ffSAndreas Gohr $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)"; 406fea1a86fSAndreas Gohr $pages = $sqlite->queryAll($sql, $pattern); 407f09444ffSAndreas Gohr 408f09444ffSAndreas Gohr return array_column($pages, 'page'); 409f09444ffSAndreas Gohr } 410f09444ffSAndreas Gohr 411639d4c50SAndreas Gohr // endregion 412639d4c50SAndreas Gohr // region Acknowledgements 413ef3ab392SAndreas Gohr 414ef3ab392SAndreas Gohr /** 415ef3ab392SAndreas Gohr * Has the given user acknowledged the given page? 416ef3ab392SAndreas Gohr * 417ef3ab392SAndreas Gohr * @param string $page 418ef3ab392SAndreas Gohr * @param string $user 4195773dd37SAnna Dabrowska * @return bool|int timestamp of acknowledgement or false 420ef3ab392SAndreas Gohr */ 421ef3ab392SAndreas Gohr public function hasUserAcknowledged($page, $user) 422ef3ab392SAndreas Gohr { 423ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 424ef3ab392SAndreas Gohr if (!$sqlite) return false; 425ef3ab392SAndreas Gohr 426ef3ab392SAndreas Gohr $sql = "SELECT ack 427ef3ab392SAndreas Gohr FROM acks A, pages B 428ef3ab392SAndreas Gohr WHERE A.page = B.page 4295773dd37SAnna Dabrowska AND A.page = ? 4305773dd37SAnna Dabrowska AND A.user = ? 431ef3ab392SAndreas Gohr AND A.ack >= B.lastmod"; 432ef3ab392SAndreas Gohr 433fea1a86fSAndreas Gohr $acktime = $sqlite->queryValue($sql, $page, $user); 434ef3ab392SAndreas Gohr 435ef3ab392SAndreas Gohr return $acktime ? (int)$acktime : false; 436ef3ab392SAndreas Gohr } 4375773dd37SAnna Dabrowska 4385773dd37SAnna Dabrowska /** 439d9a8334dSAnna Dabrowska * Timestamp of the latest acknowledgment of the given page 440d9a8334dSAnna Dabrowska * by the given user 441d9a8334dSAnna Dabrowska * 442d9a8334dSAnna Dabrowska * @param string $page 443d9a8334dSAnna Dabrowska * @param string $user 444d9a8334dSAnna Dabrowska * @return bool|string 445d9a8334dSAnna Dabrowska */ 446d9a8334dSAnna Dabrowska public function getLatestUserAcknowledgement($page, $user) 447d9a8334dSAnna Dabrowska { 448d9a8334dSAnna Dabrowska $sqlite = $this->getDB(); 449d9a8334dSAnna Dabrowska if (!$sqlite) return false; 450d9a8334dSAnna Dabrowska 451d9a8334dSAnna Dabrowska $sql = "SELECT MAX(ack) 452d9a8334dSAnna Dabrowska FROM acks 453d9a8334dSAnna Dabrowska WHERE page = ? 454d9a8334dSAnna Dabrowska AND user = ?"; 455d9a8334dSAnna Dabrowska 456fea1a86fSAndreas Gohr return $sqlite->queryValue($sql, [$page, $user]); 457d9a8334dSAnna Dabrowska } 458d9a8334dSAnna Dabrowska 459d9a8334dSAnna Dabrowska /** 4605773dd37SAnna Dabrowska * Save user's acknowledgement for a given page 4615773dd37SAnna Dabrowska * 4625773dd37SAnna Dabrowska * @param string $page 4635773dd37SAnna Dabrowska * @param string $user 4645773dd37SAnna Dabrowska * @return bool 4655773dd37SAnna Dabrowska */ 4665773dd37SAnna Dabrowska public function saveAcknowledgement($page, $user) 4675773dd37SAnna Dabrowska { 4685773dd37SAnna Dabrowska $sqlite = $this->getDB(); 4695773dd37SAnna Dabrowska if (!$sqlite) return false; 4705773dd37SAnna Dabrowska 4718e55e483SAnna Dabrowska $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))"; 4725773dd37SAnna Dabrowska 473fea1a86fSAndreas Gohr $sqlite->exec($sql, $page, $user); 4745773dd37SAnna Dabrowska return true; 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 5183b76424dSannda $ulist = implode(',', 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} 583