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 /** 22*dee5618cSAnna Dabrowska * Constructor 23fea1a86fSAndreas Gohr * 24*dee5618cSAnna Dabrowska * @return void 25*dee5618cSAnna Dabrowska * @throws Exception 26cabb51d3SAndreas Gohr */ 27*dee5618cSAnna Dabrowska public function __construct() 28cabb51d3SAndreas Gohr { 29fea1a86fSAndreas Gohr if ($this->db === null) { 30fea1a86fSAndreas Gohr try { 31fea1a86fSAndreas Gohr $this->db = new SQLiteDB('acknowledgement', __DIR__ . '/db'); 32fea1a86fSAndreas Gohr 33fea1a86fSAndreas Gohr // register our custom functions 34fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('AUTH_ISMEMBER', [$this, 'auth_isMember'], -1); 35fea1a86fSAndreas Gohr $this->db->getPdo()->sqliteCreateFunction('MATCHES_PAGE_PATTERN', [$this, 'matchPagePattern'], 2); 36fea1a86fSAndreas Gohr } catch (\Exception $exception) { 37fea1a86fSAndreas Gohr if (defined('DOKU_UNITTEST')) throw new \RuntimeException('Could not load SQLite', 0, $exception); 38fea1a86fSAndreas Gohr ErrorHandler::logException($exception); 39cabb51d3SAndreas Gohr msg($this->getLang('error sqlite plugin missing'), -1); 40*dee5618cSAnna Dabrowska throw $exception; 41cabb51d3SAndreas Gohr } 42cabb51d3SAndreas Gohr } 43*dee5618cSAnna Dabrowska } 44*dee5618cSAnna Dabrowska 45*dee5618cSAnna Dabrowska /** 46*dee5618cSAnna Dabrowska * Wrapper for test DB access 47*dee5618cSAnna Dabrowska * 48*dee5618cSAnna Dabrowska * @return SQLiteDB 49*dee5618cSAnna Dabrowska */ 50*dee5618cSAnna Dabrowska public function getDB() 51*dee5618cSAnna Dabrowska { 52fea1a86fSAndreas Gohr return $this->db; 539c3eae1eSAnna Dabrowska } 549c3eae1eSAnna Dabrowska 559c3eae1eSAnna Dabrowska /** 569c3eae1eSAnna Dabrowska * Wrapper function for auth_isMember which accepts groups as string 579c3eae1eSAnna Dabrowska * 589c3eae1eSAnna Dabrowska * @param string $memberList 599c3eae1eSAnna Dabrowska * @param string $user 609c3eae1eSAnna Dabrowska * @param string $groups 61ba917e33SAnna Dabrowska * 629c3eae1eSAnna Dabrowska * @return bool 639c3eae1eSAnna Dabrowska */ 64ba917e33SAnna Dabrowska // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 659c3eae1eSAnna Dabrowska public function auth_isMember($memberList, $user, $groups) 669c3eae1eSAnna Dabrowska { 6795113ed8SAnna Dabrowska return auth_isMember($memberList, $user, explode('///', $groups)); 689c3eae1eSAnna Dabrowska } 699c3eae1eSAnna Dabrowska 709c3eae1eSAnna Dabrowska /** 71639d4c50SAndreas Gohr * Fills the page index with all unknown pages from the fulltext index 72639d4c50SAndreas Gohr * @return void 73639d4c50SAndreas Gohr */ 74639d4c50SAndreas Gohr public function updatePageIndex() 75639d4c50SAndreas Gohr { 76639d4c50SAndreas Gohr $pages = idx_getIndex('page', ''); 77639d4c50SAndreas Gohr $sql = "INSERT OR IGNORE INTO pages (page, lastmod) VALUES (?,?)"; 78639d4c50SAndreas Gohr 79*dee5618cSAnna Dabrowska $this->db->getPdo()->beginTransaction(); 80639d4c50SAndreas Gohr foreach ($pages as $page) { 81639d4c50SAndreas Gohr $page = trim($page); 82639d4c50SAndreas Gohr $lastmod = @filemtime(wikiFN($page)); 83639d4c50SAndreas Gohr if ($lastmod) { 84fea1a86fSAndreas Gohr try { 85*dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $lastmod]); 86fea1a86fSAndreas Gohr } catch (\Exception $exception) { 87*dee5618cSAnna Dabrowska $this->db->getPdo()->rollBack(); 88fea1a86fSAndreas Gohr throw $exception; 89639d4c50SAndreas Gohr } 90639d4c50SAndreas Gohr } 91fea1a86fSAndreas Gohr } 92*dee5618cSAnna Dabrowska $this->db->getPdo()->commit(); 93639d4c50SAndreas Gohr } 94639d4c50SAndreas Gohr 95639d4c50SAndreas Gohr /** 96639d4c50SAndreas Gohr * Check if the given pattern matches the given page 97639d4c50SAndreas Gohr * 98639d4c50SAndreas Gohr * @param string $pattern the pattern to check against 99639d4c50SAndreas Gohr * @param string $page the cleaned pageid to check 100639d4c50SAndreas Gohr * @return bool 101639d4c50SAndreas Gohr */ 102639d4c50SAndreas Gohr public function matchPagePattern($pattern, $page) 103639d4c50SAndreas Gohr { 104639d4c50SAndreas Gohr if (trim($pattern, ':') == '**') return true; // match all 105639d4c50SAndreas Gohr 106639d4c50SAndreas Gohr // regex patterns 107639d4c50SAndreas Gohr if ($pattern[0] == '/') { 108639d4c50SAndreas Gohr return (bool)preg_match($pattern, ":$page"); 109639d4c50SAndreas Gohr } 110639d4c50SAndreas Gohr 111639d4c50SAndreas Gohr $pns = ':' . getNS($page) . ':'; 112639d4c50SAndreas Gohr 113639d4c50SAndreas Gohr $ans = ':' . cleanID($pattern) . ':'; 114639d4c50SAndreas Gohr if (substr($pattern, -2) == '**') { 115639d4c50SAndreas Gohr // upper namespaces match 116639d4c50SAndreas Gohr if (strpos($pns, $ans) === 0) { 117639d4c50SAndreas Gohr return true; 118639d4c50SAndreas Gohr } 119639d4c50SAndreas Gohr } elseif (substr($pattern, -1) == '*') { 120639d4c50SAndreas Gohr // namespaces match exact 1213b76424dSannda if ($ans === $pns) { 122639d4c50SAndreas Gohr return true; 123639d4c50SAndreas Gohr } 1243b76424dSannda } elseif (cleanID($pattern) == $page) { 125639d4c50SAndreas Gohr // exact match 126639d4c50SAndreas Gohr return true; 127639d4c50SAndreas Gohr } 128639d4c50SAndreas Gohr 129639d4c50SAndreas Gohr return false; 130639d4c50SAndreas Gohr } 131639d4c50SAndreas Gohr 13245240794SAnna Dabrowska /** 133c92ac04cSAnna Dabrowska * Returns all users, formatted for autocomplete 13445240794SAnna Dabrowska * 13545240794SAnna Dabrowska * @return array 13645240794SAnna Dabrowska */ 13745240794SAnna Dabrowska public function getUsers() 13845240794SAnna Dabrowska { 13945240794SAnna Dabrowska /** @var AuthPlugin $auth */ 14045240794SAnna Dabrowska global $auth; 14145240794SAnna Dabrowska 14245240794SAnna Dabrowska if (!$auth->canDo('getUsers')) { 14345240794SAnna Dabrowska return []; 14445240794SAnna Dabrowska } 14545240794SAnna Dabrowska 14645240794SAnna Dabrowska $cb = function ($k, $v) { 14745240794SAnna Dabrowska return [ 14845240794SAnna Dabrowska 'value' => $k, 14945240794SAnna Dabrowska 'label' => $k . ' (' . $v['name'] . ')' 15045240794SAnna Dabrowska ]; 15145240794SAnna Dabrowska }; 15245240794SAnna Dabrowska $users = $auth->retrieveUsers(); 15345240794SAnna Dabrowska $users = array_map($cb, array_keys($users), array_values($users)); 15445240794SAnna Dabrowska 15545240794SAnna Dabrowska return $users; 15645240794SAnna Dabrowska } 15745240794SAnna Dabrowska 158639d4c50SAndreas Gohr // endregion 159639d4c50SAndreas Gohr // region Page Data 160639d4c50SAndreas Gohr 161639d4c50SAndreas Gohr /** 162ef3ab392SAndreas Gohr * Delete a page 163ef3ab392SAndreas Gohr * 164ef3ab392SAndreas Gohr * Cascades to delete all assigned data, etc. 165ef3ab392SAndreas Gohr * 166ef3ab392SAndreas Gohr * @param string $page Page ID 167ef3ab392SAndreas Gohr */ 168ef3ab392SAndreas Gohr public function removePage($page) 169ef3ab392SAndreas Gohr { 170ef3ab392SAndreas Gohr $sql = "DELETE FROM pages WHERE page = ?"; 171*dee5618cSAnna Dabrowska $this->db->exec($sql, $page); 172ef3ab392SAndreas Gohr } 173ef3ab392SAndreas Gohr 174ef3ab392SAndreas Gohr /** 1755dee13f7SAnna Dabrowska * Update last modified date of page if content has changed 176ef3ab392SAndreas Gohr * 177ef3ab392SAndreas Gohr * @param string $page Page ID 178ef3ab392SAndreas Gohr * @param int $lastmod timestamp of last non-minor change 179ef3ab392SAndreas Gohr */ 1805dee13f7SAnna Dabrowska public function storePageDate($page, $lastmod, $newContent) 181ef3ab392SAndreas Gohr { 1823b76424dSannda $changelog = new PageChangeLog($page); 183789aa26fSAnna Dabrowska $revs = $changelog->getRevisions(0, 1); 184ed4e8871SAnna Dabrowska 185ed4e8871SAnna Dabrowska // compare content 186ed4e8871SAnna Dabrowska $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0]))); 187ed4e8871SAnna Dabrowska $newContent = str_replace(NL, '', $newContent); 188ed4e8871SAnna Dabrowska if ($oldContent === $newContent) return; 189ed4e8871SAnna Dabrowska 190ef3ab392SAndreas Gohr $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)"; 191*dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $lastmod]); 192ef3ab392SAndreas Gohr } 193ef3ab392SAndreas Gohr 194639d4c50SAndreas Gohr // endregion 195639d4c50SAndreas Gohr // region Assignments 196639d4c50SAndreas Gohr 197ef3ab392SAndreas Gohr /** 198f09444ffSAndreas Gohr * Clears direct assignments for a page 199f09444ffSAndreas Gohr * 200cabb51d3SAndreas Gohr * @param string $page Page ID 201cabb51d3SAndreas Gohr */ 202f09444ffSAndreas Gohr public function clearPageAssignments($page) 203cabb51d3SAndreas Gohr { 204f09444ffSAndreas Gohr $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?"; 205*dee5618cSAnna Dabrowska $this->db->exec($sql, $page); 206f09444ffSAndreas Gohr } 207f09444ffSAndreas Gohr 208f09444ffSAndreas Gohr /** 209639d4c50SAndreas Gohr * Set assignees for a given page as manually specified 210639d4c50SAndreas Gohr * 211639d4c50SAndreas Gohr * @param string $page Page ID 212639d4c50SAndreas Gohr * @param string $assignees 213639d4c50SAndreas Gohr * @return void 214639d4c50SAndreas Gohr */ 215639d4c50SAndreas Gohr public function setPageAssignees($page, $assignees) 216639d4c50SAndreas Gohr { 2173b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 218639d4c50SAndreas Gohr 219639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)"; 220*dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $assignees]); 221639d4c50SAndreas Gohr } 222639d4c50SAndreas Gohr 223639d4c50SAndreas Gohr /** 224639d4c50SAndreas Gohr * Set assignees for a given page from the patterns 225639d4c50SAndreas Gohr * @param string $page Page ID 226639d4c50SAndreas Gohr */ 227639d4c50SAndreas Gohr public function setAutoAssignees($page) 228639d4c50SAndreas Gohr { 229639d4c50SAndreas Gohr $patterns = $this->getAssignmentPatterns(); 230639d4c50SAndreas Gohr 231639d4c50SAndreas Gohr // given assignees 232639d4c50SAndreas Gohr $assignees = ''; 233639d4c50SAndreas Gohr 234639d4c50SAndreas Gohr // find all patterns that match the page and add the configured assignees 235639d4c50SAndreas Gohr foreach ($patterns as $pattern => $assignees) { 236639d4c50SAndreas Gohr if ($this->matchPagePattern($pattern, $page)) { 237639d4c50SAndreas Gohr $assignees .= ',' . $assignees; 238639d4c50SAndreas Gohr } 239639d4c50SAndreas Gohr } 240639d4c50SAndreas Gohr 241639d4c50SAndreas Gohr // remove duplicates and empty entries 2423b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 243639d4c50SAndreas Gohr 244639d4c50SAndreas Gohr // store the assignees 245639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)"; 246*dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $assignees]); 247639d4c50SAndreas Gohr } 248639d4c50SAndreas Gohr 249639d4c50SAndreas Gohr /** 250639d4c50SAndreas Gohr * Is the given user one of the assignees for this page 251639d4c50SAndreas Gohr * 252639d4c50SAndreas Gohr * @param string $page Page ID 253639d4c50SAndreas Gohr * @param string $user user name to check 254639d4c50SAndreas Gohr * @param string[] $groups groups this user is in 255639d4c50SAndreas Gohr * @return bool 256639d4c50SAndreas Gohr */ 257639d4c50SAndreas Gohr public function isUserAssigned($page, $user, $groups) 258639d4c50SAndreas Gohr { 259639d4c50SAndreas Gohr $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?"; 260*dee5618cSAnna Dabrowska $record = $this->db->queryRecord($sql, $page); 261a806aa3dSSven if (!$record) return false; 262fea1a86fSAndreas Gohr $assignees = $record['pageassignees'] . ',' . $record['autoassignees']; 263639d4c50SAndreas Gohr return auth_isMember($assignees, $user, $groups); 264639d4c50SAndreas Gohr } 265639d4c50SAndreas Gohr 266639d4c50SAndreas Gohr /** 267639d4c50SAndreas Gohr * Fetch all assignments for a given user, with additional page information, 268833123deSAnna Dabrowska * by default filtering already granted acknowledgements. 269833123deSAnna Dabrowska * Filter can be switched off via $includeDone 270639d4c50SAndreas Gohr * 271639d4c50SAndreas Gohr * @param string $user 272639d4c50SAndreas Gohr * @param array $groups 273833123deSAnna Dabrowska * @param bool $includeDone 274833123deSAnna Dabrowska * 275639d4c50SAndreas Gohr * @return array|bool 276639d4c50SAndreas Gohr */ 277833123deSAnna Dabrowska public function getUserAssignments($user, $groups, $includeDone = false) 278639d4c50SAndreas Gohr { 279639d4c50SAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A 280639d4c50SAndreas Gohr JOIN pages B 281639d4c50SAndreas Gohr ON A.page = B.page 282639d4c50SAndreas Gohr LEFT JOIN acks C 283639d4c50SAndreas Gohr ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) ) 284833123deSAnna Dabrowska WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)"; 285833123deSAnna Dabrowska 286833123deSAnna Dabrowska if (!$includeDone) { 287833123deSAnna Dabrowska $sql .= ' AND ack IS NULL'; 288833123deSAnna Dabrowska } 289639d4c50SAndreas Gohr 290*dee5618cSAnna Dabrowska return $this->db->queryAll($sql, $user, $user, implode('///', $groups)); 291639d4c50SAndreas Gohr } 292639d4c50SAndreas Gohr 293639d4c50SAndreas Gohr /** 294639d4c50SAndreas Gohr * Resolve names of users assigned to a given page 295639d4c50SAndreas Gohr * 296639d4c50SAndreas Gohr * This can be slow on huge user bases! 297639d4c50SAndreas Gohr * 298639d4c50SAndreas Gohr * @param string $page 299639d4c50SAndreas Gohr * @return array|false 300639d4c50SAndreas Gohr */ 301639d4c50SAndreas Gohr public function getPageAssignees($page) 302639d4c50SAndreas Gohr { 303639d4c50SAndreas Gohr /** @var AuthPlugin $auth */ 304639d4c50SAndreas Gohr global $auth; 305639d4c50SAndreas Gohr 306639d4c50SAndreas Gohr $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments' 307639d4c50SAndreas Gohr FROM assignments 308639d4c50SAndreas Gohr WHERE page = ?"; 309*dee5618cSAnna Dabrowska $assignments = $this->db->queryValue($sql, $page); 310639d4c50SAndreas Gohr 311639d4c50SAndreas Gohr $users = []; 312639d4c50SAndreas Gohr foreach (explode(',', $assignments) as $item) { 313639d4c50SAndreas Gohr $item = trim($item); 314639d4c50SAndreas Gohr if ($item === '') continue; 315639d4c50SAndreas Gohr if ($item[0] == '@') { 316639d4c50SAndreas Gohr $users = array_merge( 317639d4c50SAndreas Gohr $users, 318639d4c50SAndreas Gohr array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)])) 319639d4c50SAndreas Gohr ); 320639d4c50SAndreas Gohr } else { 321639d4c50SAndreas Gohr $users[] = $item; 322639d4c50SAndreas Gohr } 323639d4c50SAndreas Gohr } 324639d4c50SAndreas Gohr 325639d4c50SAndreas Gohr return array_unique($users); 326639d4c50SAndreas Gohr } 327639d4c50SAndreas Gohr 328639d4c50SAndreas Gohr // endregion 329639d4c50SAndreas Gohr // region Assignment Patterns 330639d4c50SAndreas Gohr 331639d4c50SAndreas Gohr /** 332f09444ffSAndreas Gohr * Get all the assignment patterns 333f09444ffSAndreas Gohr * @return array (pattern => assignees) 334f09444ffSAndreas Gohr */ 335f09444ffSAndreas Gohr public function getAssignmentPatterns() 336f09444ffSAndreas Gohr { 337f09444ffSAndreas Gohr $sql = "SELECT pattern, assignees FROM assignments_patterns"; 338*dee5618cSAnna Dabrowska return $this->db->queryKeyValueList($sql); 339f09444ffSAndreas Gohr } 340f09444ffSAndreas Gohr 341f09444ffSAndreas Gohr /** 342f09444ffSAndreas Gohr * Save new assignment patterns 343f09444ffSAndreas Gohr * 344f09444ffSAndreas Gohr * This resaves all patterns and reapplies them 345f09444ffSAndreas Gohr * 346f09444ffSAndreas Gohr * @param array $patterns (pattern => assignees) 347f09444ffSAndreas Gohr */ 348639d4c50SAndreas Gohr public function saveAssignmentPatterns($patterns) 349639d4c50SAndreas Gohr { 350*dee5618cSAnna Dabrowska $this->db->getPdo()->beginTransaction(); 351fea1a86fSAndreas Gohr try { 352f09444ffSAndreas Gohr 353fea1a86fSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all assignments */ 354f09444ffSAndreas Gohr $sql = "UPDATE assignments SET autoassignees = ''"; 355*dee5618cSAnna Dabrowska $this->db->exec($sql); 356f09444ffSAndreas Gohr 357f09444ffSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all patterns */ 358f09444ffSAndreas Gohr $sql = "DELETE FROM assignments_patterns"; 359*dee5618cSAnna Dabrowska $this->db->exec($sql); 360f09444ffSAndreas Gohr 361f09444ffSAndreas Gohr // insert new patterns and gather affected pages 362f09444ffSAndreas Gohr $pages = []; 363f09444ffSAndreas Gohr 364f09444ffSAndreas Gohr $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)"; 365f09444ffSAndreas Gohr foreach ($patterns as $pattern => $assignees) { 366f09444ffSAndreas Gohr $pattern = trim($pattern); 367f09444ffSAndreas Gohr $assignees = trim($assignees); 368f09444ffSAndreas Gohr if (!$pattern || !$assignees) continue; 369*dee5618cSAnna Dabrowska $this->db->exec($sql, [$pattern, $assignees]); 370f09444ffSAndreas Gohr 371f09444ffSAndreas Gohr // patterns may overlap, so we need to gather all affected pages first 372f09444ffSAndreas Gohr $affectedPages = $this->getPagesMatchingPattern($pattern); 373f09444ffSAndreas Gohr foreach ($affectedPages as $page) { 374f09444ffSAndreas Gohr if (isset($pages[$page])) { 375f09444ffSAndreas Gohr $pages[$page] .= ',' . $assignees; 376f09444ffSAndreas Gohr } else { 377f09444ffSAndreas Gohr $pages[$page] = $assignees; 378f09444ffSAndreas Gohr } 379f09444ffSAndreas Gohr } 380f09444ffSAndreas Gohr } 381f09444ffSAndreas Gohr 382f09444ffSAndreas Gohr $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?) 383f09444ffSAndreas Gohr ON CONFLICT(page) 384f09444ffSAndreas Gohr DO UPDATE SET autoassignees = ?"; 385f09444ffSAndreas Gohr foreach ($pages as $page => $assignees) { 386f09444ffSAndreas Gohr // remove duplicates and empty entries 3873b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 388*dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $assignees, $assignees]); 389f09444ffSAndreas Gohr } 390fea1a86fSAndreas Gohr } catch (Exception $e) { 391*dee5618cSAnna Dabrowska $this->db->getPdo()->rollBack(); 392fea1a86fSAndreas Gohr throw $e; 393fea1a86fSAndreas Gohr } 394*dee5618cSAnna Dabrowska $this->db->getPdo()->commit(); 395f09444ffSAndreas Gohr } 396f09444ffSAndreas Gohr 397f09444ffSAndreas Gohr /** 398f09444ffSAndreas Gohr * Get all known pages that match the given pattern 399f09444ffSAndreas Gohr * 400f09444ffSAndreas Gohr * @param $pattern 401f09444ffSAndreas Gohr * @return string[] 402f09444ffSAndreas Gohr */ 403639d4c50SAndreas Gohr public function getPagesMatchingPattern($pattern) 404639d4c50SAndreas Gohr { 405f09444ffSAndreas Gohr $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)"; 406*dee5618cSAnna Dabrowska $pages = $this->db->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 $sql = "SELECT ack 424ef3ab392SAndreas Gohr FROM acks A, pages B 425ef3ab392SAndreas Gohr WHERE A.page = B.page 4265773dd37SAnna Dabrowska AND A.page = ? 4275773dd37SAnna Dabrowska AND A.user = ? 428ef3ab392SAndreas Gohr AND A.ack >= B.lastmod"; 429ef3ab392SAndreas Gohr 430*dee5618cSAnna Dabrowska $acktime = $this->db->queryValue($sql, $page, $user); 431ef3ab392SAndreas Gohr 432ef3ab392SAndreas Gohr return $acktime ? (int)$acktime : false; 433ef3ab392SAndreas Gohr } 4345773dd37SAnna Dabrowska 4355773dd37SAnna Dabrowska /** 436d9a8334dSAnna Dabrowska * Timestamp of the latest acknowledgment of the given page 437d9a8334dSAnna Dabrowska * by the given user 438d9a8334dSAnna Dabrowska * 439d9a8334dSAnna Dabrowska * @param string $page 440d9a8334dSAnna Dabrowska * @param string $user 441d9a8334dSAnna Dabrowska * @return bool|string 442d9a8334dSAnna Dabrowska */ 443d9a8334dSAnna Dabrowska public function getLatestUserAcknowledgement($page, $user) 444d9a8334dSAnna Dabrowska { 445d9a8334dSAnna Dabrowska $sql = "SELECT MAX(ack) 446d9a8334dSAnna Dabrowska FROM acks 447d9a8334dSAnna Dabrowska WHERE page = ? 448d9a8334dSAnna Dabrowska AND user = ?"; 449d9a8334dSAnna Dabrowska 450*dee5618cSAnna Dabrowska return $this->db->queryValue($sql, [$page, $user]); 451d9a8334dSAnna Dabrowska } 452d9a8334dSAnna Dabrowska 453d9a8334dSAnna Dabrowska /** 4545773dd37SAnna Dabrowska * Save user's acknowledgement for a given page 4555773dd37SAnna Dabrowska * 4565773dd37SAnna Dabrowska * @param string $page 4575773dd37SAnna Dabrowska * @param string $user 4585773dd37SAnna Dabrowska * @return bool 4595773dd37SAnna Dabrowska */ 4605773dd37SAnna Dabrowska public function saveAcknowledgement($page, $user) 4615773dd37SAnna Dabrowska { 4628e55e483SAnna Dabrowska $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))"; 4635773dd37SAnna Dabrowska 464*dee5618cSAnna Dabrowska $this->db->exec($sql, $page, $user); 4655773dd37SAnna Dabrowska return true; 4665773dd37SAnna Dabrowska } 46774126d4bSAnna Dabrowska 46874126d4bSAnna Dabrowska /** 4695966046cSAnna Dabrowska * Get all pages that a user needs to acknowledge and/or the last acknowledgement infos 4705966046cSAnna Dabrowska * depending on the (optional) filter based on status of the acknowledgements. 471d6011abdSAnna Dabrowska * 472863b6e48SAndreas Gohr * @param string $user 473863b6e48SAndreas Gohr * @param array $groups 4745966046cSAnna Dabrowska * @param string $status Optional status filter, can be all (default), current or due 4755966046cSAnna Dabrowska * 476d6011abdSAnna Dabrowska * @return array|bool 477d6011abdSAnna Dabrowska */ 4785966046cSAnna Dabrowska public function getUserAcknowledgements($user, $groups, $status = '') 479d6011abdSAnna Dabrowska { 480*dee5618cSAnna Dabrowska $filterClause = $this->getFilterClause($status, 'B'); 4815966046cSAnna Dabrowska 4825966046cSAnna Dabrowska // query 483f09444ffSAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack 484863b6e48SAndreas Gohr FROM assignments A 485863b6e48SAndreas Gohr JOIN pages B 486863b6e48SAndreas Gohr ON A.page = B.page 487863b6e48SAndreas Gohr LEFT JOIN acks C 488863b6e48SAndreas Gohr ON A.page = C.page AND C.user = ? 489f09444ffSAndreas Gohr WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?) 4905966046cSAnna Dabrowska GROUP BY A.page"; 491*dee5618cSAnna Dabrowska $sql .= $filterClause; 4925966046cSAnna Dabrowska $sql .= " 4935966046cSAnna Dabrowska ORDER BY A.page"; 494863b6e48SAndreas Gohr 495*dee5618cSAnna Dabrowska return $this->db->queryAll($sql, [$user, $user, implode('///', $groups)]); 496863b6e48SAndreas Gohr } 497863b6e48SAndreas Gohr 498863b6e48SAndreas Gohr /** 499c6d8c1d9SAndreas Gohr * Get ack status for all assigned users of a given page 500c6d8c1d9SAndreas Gohr * 501c6d8c1d9SAndreas Gohr * This can be slow! 502c6d8c1d9SAndreas Gohr * 503c6d8c1d9SAndreas Gohr * @param string $page 5045966046cSAnna Dabrowska * @param string $user 505*dee5618cSAnna Dabrowska * @param string $status 506*dee5618cSAnna Dabrowska * @param int $max 507*dee5618cSAnna Dabrowska * 508*dee5618cSAnna Dabrowska * @return array 509c6d8c1d9SAndreas Gohr */ 510*dee5618cSAnna Dabrowska public function getPageAcknowledgements($page, $user = '', $status = '', $max = 0) 511c6d8c1d9SAndreas Gohr { 512*dee5618cSAnna Dabrowska $userClause = ''; 513*dee5618cSAnna Dabrowska $params[] = $page; 514c6d8c1d9SAndreas Gohr 515*dee5618cSAnna Dabrowska // filtering for user from input or using saved assignees? 516*dee5618cSAnna Dabrowska if ($user) { 517*dee5618cSAnna Dabrowska $users = [$user]; 518*dee5618cSAnna Dabrowska $userClause = ' AND (B.user = ? OR B.user IS NULL) '; 519*dee5618cSAnna Dabrowska $params[] = $user; 520*dee5618cSAnna Dabrowska } else { 521*dee5618cSAnna Dabrowska $users = $this->getPageAssignees($page); 522*dee5618cSAnna Dabrowska if (!$users) return []; 523*dee5618cSAnna Dabrowska } 524*dee5618cSAnna Dabrowska 525*dee5618cSAnna Dabrowska $ulist = implode(',', array_map([$this->db->getPdo(), 'quote'], $users)); 526c6d8c1d9SAndreas Gohr $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack 527c6d8c1d9SAndreas Gohr FROM pages A 528c6d8c1d9SAndreas Gohr LEFT JOIN acks B 529c6d8c1d9SAndreas Gohr ON A.page = B.page 530c6d8c1d9SAndreas Gohr AND B.user IN ($ulist) 531*dee5618cSAnna Dabrowska WHERE A.page = ? $userClause"; 532*dee5618cSAnna Dabrowska $sql .= " GROUP BY A.page, B.user "; 533b6817aacSAndreas Gohr if ($max) $sql .= " LIMIT $max"; 534*dee5618cSAnna Dabrowska 535*dee5618cSAnna Dabrowska $acknowledgements = $this->db->queryAll($sql, $params); 536*dee5618cSAnna Dabrowska 537*dee5618cSAnna Dabrowska if ($status === 'current') { 538*dee5618cSAnna Dabrowska return $acknowledgements; 539*dee5618cSAnna Dabrowska } 540c6d8c1d9SAndreas Gohr 541c6d8c1d9SAndreas Gohr // there should be at least one result, unless the page is unknown 542*dee5618cSAnna Dabrowska if (!count($acknowledgements)) return $acknowledgements; 543c6d8c1d9SAndreas Gohr 544c6d8c1d9SAndreas Gohr $baseinfo = [ 545c6d8c1d9SAndreas Gohr 'page' => $acknowledgements[0]['page'], 546c6d8c1d9SAndreas Gohr 'lastmod' => $acknowledgements[0]['lastmod'], 547c6d8c1d9SAndreas Gohr 'user' => null, 548c6d8c1d9SAndreas Gohr 'ack' => null, 549c6d8c1d9SAndreas Gohr ]; 550c6d8c1d9SAndreas Gohr 551c6d8c1d9SAndreas Gohr // fill up the result with all users that never acknowledged the page 552c6d8c1d9SAndreas Gohr $combined = []; 553c6d8c1d9SAndreas Gohr foreach ($acknowledgements as $ack) { 554c6d8c1d9SAndreas Gohr if ($ack['user'] !== null) { 555c6d8c1d9SAndreas Gohr $combined[$ack['user']] = $ack; 556c6d8c1d9SAndreas Gohr } 557c6d8c1d9SAndreas Gohr } 558c6d8c1d9SAndreas Gohr foreach ($users as $user) { 559c6d8c1d9SAndreas Gohr if (!isset($combined[$user])) { 560c6d8c1d9SAndreas Gohr $combined[$user] = array_merge($baseinfo, ['user' => $user]); 561c6d8c1d9SAndreas Gohr } 562c6d8c1d9SAndreas Gohr } 563c6d8c1d9SAndreas Gohr 564*dee5618cSAnna Dabrowska // finally remove current acknowledgements if filter is used 565*dee5618cSAnna Dabrowska // this cannot be done in SQL without loss of data, 566*dee5618cSAnna Dabrowska // filtering must happen last, otherwise removed current acks will be re-added as due 567*dee5618cSAnna Dabrowska if ($status === 'due') { 568*dee5618cSAnna Dabrowska $combined = array_filter($combined, function($info) { 569*dee5618cSAnna Dabrowska return $info['ack'] < $info['lastmod']; 570*dee5618cSAnna Dabrowska }); 571*dee5618cSAnna Dabrowska } 572*dee5618cSAnna Dabrowska 573c6d8c1d9SAndreas Gohr ksort($combined); 574c6d8c1d9SAndreas Gohr return array_values($combined); 575c6d8c1d9SAndreas Gohr } 576c6d8c1d9SAndreas Gohr 577c6d8c1d9SAndreas Gohr /** 578863b6e48SAndreas Gohr * Returns all acknowledgements 579863b6e48SAndreas Gohr * 580863b6e48SAndreas Gohr * @param int $limit maximum number of results 581*dee5618cSAnna Dabrowska * @return array 582863b6e48SAndreas Gohr */ 583863b6e48SAndreas Gohr public function getAcknowledgements($limit = 100) 584863b6e48SAndreas Gohr { 585863b6e48SAndreas Gohr $sql = ' 58684db77b6SAndreas Gohr SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack 58784db77b6SAndreas Gohr FROM acks A, pages B 58884db77b6SAndreas Gohr WHERE A.page = B.page 58984db77b6SAndreas Gohr GROUP BY A.user, A.page 590863b6e48SAndreas Gohr ORDER BY ack DESC 591863b6e48SAndreas Gohr LIMIT ? 592863b6e48SAndreas Gohr '; 593*dee5618cSAnna Dabrowska return $this->db->queryAll($sql, $limit); 594*dee5618cSAnna Dabrowska } 595d6011abdSAnna Dabrowska 596*dee5618cSAnna Dabrowska /** 597*dee5618cSAnna Dabrowska * Returns a filter clause for acknowledgement queries depending on wanted status. 598*dee5618cSAnna Dabrowska * 599*dee5618cSAnna Dabrowska * @param string $status 600*dee5618cSAnna Dabrowska * @param string $alias Table alias used in the SQL query 601*dee5618cSAnna Dabrowska * @return string 602*dee5618cSAnna Dabrowska */ 603*dee5618cSAnna Dabrowska protected function getFilterClause(string $status, string $alias): string 604*dee5618cSAnna Dabrowska { 605*dee5618cSAnna Dabrowska switch ($status) { 606*dee5618cSAnna Dabrowska case 'current': 607*dee5618cSAnna Dabrowska $filterClause = " HAVING ack >= $alias.lastmod "; 608*dee5618cSAnna Dabrowska break; 609*dee5618cSAnna Dabrowska case 'due': 610*dee5618cSAnna Dabrowska $filterClause = " HAVING (ack IS NULL) OR (ack < $alias.lastmod) "; 611*dee5618cSAnna Dabrowska break; 612*dee5618cSAnna Dabrowska case 'outdated': 613*dee5618cSAnna Dabrowska $filterClause = " HAVING ack < $alias.lastmod "; 614*dee5618cSAnna Dabrowska break; 615*dee5618cSAnna Dabrowska case 'all': 616*dee5618cSAnna Dabrowska default: 617*dee5618cSAnna Dabrowska $filterClause = ''; 618*dee5618cSAnna Dabrowska break; 619*dee5618cSAnna Dabrowska } 620*dee5618cSAnna Dabrowska return $filterClause; 621d6011abdSAnna Dabrowska } 622f09444ffSAndreas Gohr 623639d4c50SAndreas Gohr // endregion 6244d6d17d0SAndreas Gohr} 625