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 /** 22dee5618cSAnna Dabrowska * Constructor 23fea1a86fSAndreas Gohr * 24dee5618cSAnna Dabrowska * @return void 25dee5618cSAnna Dabrowska * @throws Exception 26cabb51d3SAndreas Gohr */ 27dee5618cSAnna 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); 40dee5618cSAnna Dabrowska throw $exception; 41cabb51d3SAndreas Gohr } 42cabb51d3SAndreas Gohr } 43dee5618cSAnna Dabrowska } 44dee5618cSAnna Dabrowska 45dee5618cSAnna Dabrowska /** 46dee5618cSAnna Dabrowska * Wrapper for test DB access 47dee5618cSAnna Dabrowska * 48dee5618cSAnna Dabrowska * @return SQLiteDB 49dee5618cSAnna Dabrowska */ 50dee5618cSAnna Dabrowska public function getDB() 51dee5618cSAnna 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 79dee5618cSAnna 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 { 85dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $lastmod]); 86fea1a86fSAndreas Gohr } catch (\Exception $exception) { 87dee5618cSAnna Dabrowska $this->db->getPdo()->rollBack(); 88fea1a86fSAndreas Gohr throw $exception; 89639d4c50SAndreas Gohr } 90639d4c50SAndreas Gohr } 91fea1a86fSAndreas Gohr } 92dee5618cSAnna 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 = ?"; 171dee5618cSAnna 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 186*635f6fb7SAnna Dabrowska if (!empty($revs)) { 187ed4e8871SAnna Dabrowska $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0]))); 188ed4e8871SAnna Dabrowska $newContent = str_replace(NL, '', $newContent); 189ed4e8871SAnna Dabrowska if ($oldContent === $newContent) return; 190*635f6fb7SAnna Dabrowska } 191ed4e8871SAnna Dabrowska 192ef3ab392SAndreas Gohr $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)"; 193dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $lastmod]); 194ef3ab392SAndreas Gohr } 195ef3ab392SAndreas Gohr 196639d4c50SAndreas Gohr // endregion 197639d4c50SAndreas Gohr // region Assignments 198639d4c50SAndreas Gohr 199ef3ab392SAndreas Gohr /** 200f09444ffSAndreas Gohr * Clears direct assignments for a page 201f09444ffSAndreas Gohr * 202cabb51d3SAndreas Gohr * @param string $page Page ID 203cabb51d3SAndreas Gohr */ 204f09444ffSAndreas Gohr public function clearPageAssignments($page) 205cabb51d3SAndreas Gohr { 206f09444ffSAndreas Gohr $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?"; 207dee5618cSAnna Dabrowska $this->db->exec($sql, $page); 208f09444ffSAndreas Gohr } 209f09444ffSAndreas Gohr 210f09444ffSAndreas Gohr /** 211639d4c50SAndreas Gohr * Set assignees for a given page as manually specified 212639d4c50SAndreas Gohr * 213639d4c50SAndreas Gohr * @param string $page Page ID 214639d4c50SAndreas Gohr * @param string $assignees 215639d4c50SAndreas Gohr * @return void 216639d4c50SAndreas Gohr */ 217639d4c50SAndreas Gohr public function setPageAssignees($page, $assignees) 218639d4c50SAndreas Gohr { 2193b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 220639d4c50SAndreas Gohr 221639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)"; 222dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $assignees]); 223639d4c50SAndreas Gohr } 224639d4c50SAndreas Gohr 225639d4c50SAndreas Gohr /** 226639d4c50SAndreas Gohr * Set assignees for a given page from the patterns 227639d4c50SAndreas Gohr * @param string $page Page ID 228639d4c50SAndreas Gohr */ 229639d4c50SAndreas Gohr public function setAutoAssignees($page) 230639d4c50SAndreas Gohr { 231639d4c50SAndreas Gohr $patterns = $this->getAssignmentPatterns(); 232639d4c50SAndreas Gohr 233639d4c50SAndreas Gohr // given assignees 234639d4c50SAndreas Gohr $assignees = ''; 235639d4c50SAndreas Gohr 236639d4c50SAndreas Gohr // find all patterns that match the page and add the configured assignees 237639d4c50SAndreas Gohr foreach ($patterns as $pattern => $assignees) { 238639d4c50SAndreas Gohr if ($this->matchPagePattern($pattern, $page)) { 239639d4c50SAndreas Gohr $assignees .= ',' . $assignees; 240639d4c50SAndreas Gohr } 241639d4c50SAndreas Gohr } 242639d4c50SAndreas Gohr 243639d4c50SAndreas Gohr // remove duplicates and empty entries 2443b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 245639d4c50SAndreas Gohr 246639d4c50SAndreas Gohr // store the assignees 247639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)"; 248dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $assignees]); 249639d4c50SAndreas Gohr } 250639d4c50SAndreas Gohr 251639d4c50SAndreas Gohr /** 252639d4c50SAndreas Gohr * Is the given user one of the assignees for this page 253639d4c50SAndreas Gohr * 254639d4c50SAndreas Gohr * @param string $page Page ID 255639d4c50SAndreas Gohr * @param string $user user name to check 256639d4c50SAndreas Gohr * @param string[] $groups groups this user is in 257639d4c50SAndreas Gohr * @return bool 258639d4c50SAndreas Gohr */ 259639d4c50SAndreas Gohr public function isUserAssigned($page, $user, $groups) 260639d4c50SAndreas Gohr { 261639d4c50SAndreas Gohr $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?"; 262dee5618cSAnna Dabrowska $record = $this->db->queryRecord($sql, $page); 263a806aa3dSSven if (!$record) return false; 264fea1a86fSAndreas Gohr $assignees = $record['pageassignees'] . ',' . $record['autoassignees']; 265639d4c50SAndreas Gohr return auth_isMember($assignees, $user, $groups); 266639d4c50SAndreas Gohr } 267639d4c50SAndreas Gohr 268639d4c50SAndreas Gohr /** 269639d4c50SAndreas Gohr * Fetch all assignments for a given user, with additional page information, 270833123deSAnna Dabrowska * by default filtering already granted acknowledgements. 271833123deSAnna Dabrowska * Filter can be switched off via $includeDone 272639d4c50SAndreas Gohr * 273639d4c50SAndreas Gohr * @param string $user 274639d4c50SAndreas Gohr * @param array $groups 275833123deSAnna Dabrowska * @param bool $includeDone 276833123deSAnna Dabrowska * 277639d4c50SAndreas Gohr * @return array|bool 278639d4c50SAndreas Gohr */ 279833123deSAnna Dabrowska public function getUserAssignments($user, $groups, $includeDone = false) 280639d4c50SAndreas Gohr { 281639d4c50SAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A 282639d4c50SAndreas Gohr JOIN pages B 283639d4c50SAndreas Gohr ON A.page = B.page 284639d4c50SAndreas Gohr LEFT JOIN acks C 285639d4c50SAndreas Gohr ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) ) 286833123deSAnna Dabrowska WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)"; 287833123deSAnna Dabrowska 288833123deSAnna Dabrowska if (!$includeDone) { 289833123deSAnna Dabrowska $sql .= ' AND ack IS NULL'; 290833123deSAnna Dabrowska } 291639d4c50SAndreas Gohr 292dee5618cSAnna Dabrowska return $this->db->queryAll($sql, $user, $user, implode('///', $groups)); 293639d4c50SAndreas Gohr } 294639d4c50SAndreas Gohr 295639d4c50SAndreas Gohr /** 296639d4c50SAndreas Gohr * Resolve names of users assigned to a given page 297639d4c50SAndreas Gohr * 298639d4c50SAndreas Gohr * This can be slow on huge user bases! 299639d4c50SAndreas Gohr * 300639d4c50SAndreas Gohr * @param string $page 301639d4c50SAndreas Gohr * @return array|false 302639d4c50SAndreas Gohr */ 303639d4c50SAndreas Gohr public function getPageAssignees($page) 304639d4c50SAndreas Gohr { 305639d4c50SAndreas Gohr /** @var AuthPlugin $auth */ 306639d4c50SAndreas Gohr global $auth; 307639d4c50SAndreas Gohr 308639d4c50SAndreas Gohr $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments' 309639d4c50SAndreas Gohr FROM assignments 310639d4c50SAndreas Gohr WHERE page = ?"; 311dee5618cSAnna Dabrowska $assignments = $this->db->queryValue($sql, $page); 312639d4c50SAndreas Gohr 313639d4c50SAndreas Gohr $users = []; 314639d4c50SAndreas Gohr foreach (explode(',', $assignments) as $item) { 315639d4c50SAndreas Gohr $item = trim($item); 316639d4c50SAndreas Gohr if ($item === '') continue; 317639d4c50SAndreas Gohr if ($item[0] == '@') { 318639d4c50SAndreas Gohr $users = array_merge( 319639d4c50SAndreas Gohr $users, 320639d4c50SAndreas Gohr array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)])) 321639d4c50SAndreas Gohr ); 322639d4c50SAndreas Gohr } else { 323639d4c50SAndreas Gohr $users[] = $item; 324639d4c50SAndreas Gohr } 325639d4c50SAndreas Gohr } 326639d4c50SAndreas Gohr 327639d4c50SAndreas Gohr return array_unique($users); 328639d4c50SAndreas Gohr } 329639d4c50SAndreas Gohr 330639d4c50SAndreas Gohr // endregion 331639d4c50SAndreas Gohr // region Assignment Patterns 332639d4c50SAndreas Gohr 333639d4c50SAndreas Gohr /** 334f09444ffSAndreas Gohr * Get all the assignment patterns 335f09444ffSAndreas Gohr * @return array (pattern => assignees) 336f09444ffSAndreas Gohr */ 337f09444ffSAndreas Gohr public function getAssignmentPatterns() 338f09444ffSAndreas Gohr { 339f09444ffSAndreas Gohr $sql = "SELECT pattern, assignees FROM assignments_patterns"; 340dee5618cSAnna Dabrowska return $this->db->queryKeyValueList($sql); 341f09444ffSAndreas Gohr } 342f09444ffSAndreas Gohr 343f09444ffSAndreas Gohr /** 344f09444ffSAndreas Gohr * Save new assignment patterns 345f09444ffSAndreas Gohr * 346f09444ffSAndreas Gohr * This resaves all patterns and reapplies them 347f09444ffSAndreas Gohr * 348f09444ffSAndreas Gohr * @param array $patterns (pattern => assignees) 349f09444ffSAndreas Gohr */ 350639d4c50SAndreas Gohr public function saveAssignmentPatterns($patterns) 351639d4c50SAndreas Gohr { 352dee5618cSAnna Dabrowska $this->db->getPdo()->beginTransaction(); 353fea1a86fSAndreas Gohr try { 354f09444ffSAndreas Gohr 355fea1a86fSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all assignments */ 356f09444ffSAndreas Gohr $sql = "UPDATE assignments SET autoassignees = ''"; 357dee5618cSAnna Dabrowska $this->db->exec($sql); 358f09444ffSAndreas Gohr 359f09444ffSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all patterns */ 360f09444ffSAndreas Gohr $sql = "DELETE FROM assignments_patterns"; 361dee5618cSAnna Dabrowska $this->db->exec($sql); 362f09444ffSAndreas Gohr 363f09444ffSAndreas Gohr // insert new patterns and gather affected pages 364f09444ffSAndreas Gohr $pages = []; 365f09444ffSAndreas Gohr 366f09444ffSAndreas Gohr $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)"; 367f09444ffSAndreas Gohr foreach ($patterns as $pattern => $assignees) { 368f09444ffSAndreas Gohr $pattern = trim($pattern); 369f09444ffSAndreas Gohr $assignees = trim($assignees); 370f09444ffSAndreas Gohr if (!$pattern || !$assignees) continue; 371dee5618cSAnna Dabrowska $this->db->exec($sql, [$pattern, $assignees]); 372f09444ffSAndreas Gohr 373f09444ffSAndreas Gohr // patterns may overlap, so we need to gather all affected pages first 374f09444ffSAndreas Gohr $affectedPages = $this->getPagesMatchingPattern($pattern); 375f09444ffSAndreas Gohr foreach ($affectedPages as $page) { 376f09444ffSAndreas Gohr if (isset($pages[$page])) { 377f09444ffSAndreas Gohr $pages[$page] .= ',' . $assignees; 378f09444ffSAndreas Gohr } else { 379f09444ffSAndreas Gohr $pages[$page] = $assignees; 380f09444ffSAndreas Gohr } 381f09444ffSAndreas Gohr } 382f09444ffSAndreas Gohr } 383f09444ffSAndreas Gohr 384f09444ffSAndreas Gohr $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?) 385f09444ffSAndreas Gohr ON CONFLICT(page) 386f09444ffSAndreas Gohr DO UPDATE SET autoassignees = ?"; 387f09444ffSAndreas Gohr foreach ($pages as $page => $assignees) { 388f09444ffSAndreas Gohr // remove duplicates and empty entries 3893b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 390dee5618cSAnna Dabrowska $this->db->exec($sql, [$page, $assignees, $assignees]); 391f09444ffSAndreas Gohr } 392fea1a86fSAndreas Gohr } catch (Exception $e) { 393dee5618cSAnna Dabrowska $this->db->getPdo()->rollBack(); 394fea1a86fSAndreas Gohr throw $e; 395fea1a86fSAndreas Gohr } 396dee5618cSAnna Dabrowska $this->db->getPdo()->commit(); 397f09444ffSAndreas Gohr } 398f09444ffSAndreas Gohr 399f09444ffSAndreas Gohr /** 400f09444ffSAndreas Gohr * Get all known pages that match the given pattern 401f09444ffSAndreas Gohr * 402f09444ffSAndreas Gohr * @param $pattern 403f09444ffSAndreas Gohr * @return string[] 404f09444ffSAndreas Gohr */ 405639d4c50SAndreas Gohr public function getPagesMatchingPattern($pattern) 406639d4c50SAndreas Gohr { 407f09444ffSAndreas Gohr $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)"; 408dee5618cSAnna Dabrowska $pages = $this->db->queryAll($sql, $pattern); 409f09444ffSAndreas Gohr 410f09444ffSAndreas Gohr return array_column($pages, 'page'); 411f09444ffSAndreas Gohr } 412f09444ffSAndreas Gohr 413639d4c50SAndreas Gohr // endregion 414639d4c50SAndreas Gohr // region Acknowledgements 415ef3ab392SAndreas Gohr 416ef3ab392SAndreas Gohr /** 417ef3ab392SAndreas Gohr * Has the given user acknowledged the given page? 418ef3ab392SAndreas Gohr * 419ef3ab392SAndreas Gohr * @param string $page 420ef3ab392SAndreas Gohr * @param string $user 4215773dd37SAnna Dabrowska * @return bool|int timestamp of acknowledgement or false 422ef3ab392SAndreas Gohr */ 423ef3ab392SAndreas Gohr public function hasUserAcknowledged($page, $user) 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 432dee5618cSAnna Dabrowska $acktime = $this->db->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 $sql = "SELECT MAX(ack) 448d9a8334dSAnna Dabrowska FROM acks 449d9a8334dSAnna Dabrowska WHERE page = ? 450d9a8334dSAnna Dabrowska AND user = ?"; 451d9a8334dSAnna Dabrowska 452dee5618cSAnna Dabrowska return $this->db->queryValue($sql, [$page, $user]); 453d9a8334dSAnna Dabrowska } 454d9a8334dSAnna Dabrowska 455d9a8334dSAnna Dabrowska /** 4565773dd37SAnna Dabrowska * Save user's acknowledgement for a given page 4575773dd37SAnna Dabrowska * 4585773dd37SAnna Dabrowska * @param string $page 4595773dd37SAnna Dabrowska * @param string $user 4605773dd37SAnna Dabrowska * @return bool 4615773dd37SAnna Dabrowska */ 4625773dd37SAnna Dabrowska public function saveAcknowledgement($page, $user) 4635773dd37SAnna Dabrowska { 4648e55e483SAnna Dabrowska $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))"; 4655773dd37SAnna Dabrowska 466dee5618cSAnna Dabrowska $this->db->exec($sql, $page, $user); 4675773dd37SAnna Dabrowska return true; 4685773dd37SAnna Dabrowska } 46974126d4bSAnna Dabrowska 47074126d4bSAnna Dabrowska /** 4715966046cSAnna Dabrowska * Get all pages that a user needs to acknowledge and/or the last acknowledgement infos 4725966046cSAnna Dabrowska * depending on the (optional) filter based on status of the acknowledgements. 473d6011abdSAnna Dabrowska * 474863b6e48SAndreas Gohr * @param string $user 475863b6e48SAndreas Gohr * @param array $groups 4765966046cSAnna Dabrowska * @param string $status Optional status filter, can be all (default), current or due 4775966046cSAnna Dabrowska * 478d6011abdSAnna Dabrowska * @return array|bool 479d6011abdSAnna Dabrowska */ 4805966046cSAnna Dabrowska public function getUserAcknowledgements($user, $groups, $status = '') 481d6011abdSAnna Dabrowska { 482dee5618cSAnna Dabrowska $filterClause = $this->getFilterClause($status, 'B'); 4835966046cSAnna Dabrowska 4845966046cSAnna Dabrowska // query 485f09444ffSAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack 486863b6e48SAndreas Gohr FROM assignments A 487863b6e48SAndreas Gohr JOIN pages B 488863b6e48SAndreas Gohr ON A.page = B.page 489863b6e48SAndreas Gohr LEFT JOIN acks C 490863b6e48SAndreas Gohr ON A.page = C.page AND C.user = ? 491f09444ffSAndreas Gohr WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?) 4925966046cSAnna Dabrowska GROUP BY A.page"; 493dee5618cSAnna Dabrowska $sql .= $filterClause; 4945966046cSAnna Dabrowska $sql .= " 4955966046cSAnna Dabrowska ORDER BY A.page"; 496863b6e48SAndreas Gohr 497dee5618cSAnna Dabrowska return $this->db->queryAll($sql, [$user, $user, implode('///', $groups)]); 498863b6e48SAndreas Gohr } 499863b6e48SAndreas Gohr 500863b6e48SAndreas Gohr /** 501c6d8c1d9SAndreas Gohr * Get ack status for all assigned users of a given page 502c6d8c1d9SAndreas Gohr * 503c6d8c1d9SAndreas Gohr * This can be slow! 504c6d8c1d9SAndreas Gohr * 505c6d8c1d9SAndreas Gohr * @param string $page 5065966046cSAnna Dabrowska * @param string $user 507dee5618cSAnna Dabrowska * @param string $status 508dee5618cSAnna Dabrowska * @param int $max 509dee5618cSAnna Dabrowska * 510dee5618cSAnna Dabrowska * @return array 511c6d8c1d9SAndreas Gohr */ 512dee5618cSAnna Dabrowska public function getPageAcknowledgements($page, $user = '', $status = '', $max = 0) 513c6d8c1d9SAndreas Gohr { 514dee5618cSAnna Dabrowska $userClause = ''; 515b271ebc7SAnna Dabrowska $filterClause = ''; 516dee5618cSAnna Dabrowska $params[] = $page; 517c6d8c1d9SAndreas Gohr 518dee5618cSAnna Dabrowska // filtering for user from input or using saved assignees? 519dee5618cSAnna Dabrowska if ($user) { 520dee5618cSAnna Dabrowska $users = [$user]; 521dee5618cSAnna Dabrowska $userClause = ' AND (B.user = ? OR B.user IS NULL) '; 522dee5618cSAnna Dabrowska $params[] = $user; 523dee5618cSAnna Dabrowska } else { 524dee5618cSAnna Dabrowska $users = $this->getPageAssignees($page); 525dee5618cSAnna Dabrowska if (!$users) return []; 526dee5618cSAnna Dabrowska } 527dee5618cSAnna Dabrowska 528b271ebc7SAnna Dabrowska if ($status === 'current') { 529b271ebc7SAnna Dabrowska $filterClause = ' AND ACK >= A.lastmod '; 530b271ebc7SAnna Dabrowska } 531b271ebc7SAnna Dabrowska 532dee5618cSAnna Dabrowska $ulist = implode(',', array_map([$this->db->getPdo(), 'quote'], $users)); 533c6d8c1d9SAndreas Gohr $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack 534c6d8c1d9SAndreas Gohr FROM pages A 535c6d8c1d9SAndreas Gohr LEFT JOIN acks B 536c6d8c1d9SAndreas Gohr ON A.page = B.page 537c6d8c1d9SAndreas Gohr AND B.user IN ($ulist) 538b271ebc7SAnna Dabrowska WHERE A.page = ? $userClause $filterClause"; 539dee5618cSAnna Dabrowska $sql .= " GROUP BY A.page, B.user "; 540b6817aacSAndreas Gohr if ($max) $sql .= " LIMIT $max"; 541dee5618cSAnna Dabrowska 542dee5618cSAnna Dabrowska $acknowledgements = $this->db->queryAll($sql, $params); 543dee5618cSAnna Dabrowska 544dee5618cSAnna Dabrowska if ($status === 'current') { 545dee5618cSAnna Dabrowska return $acknowledgements; 546dee5618cSAnna Dabrowska } 547c6d8c1d9SAndreas Gohr 548c6d8c1d9SAndreas Gohr // there should be at least one result, unless the page is unknown 549dee5618cSAnna Dabrowska if (!count($acknowledgements)) return $acknowledgements; 550c6d8c1d9SAndreas Gohr 551c6d8c1d9SAndreas Gohr $baseinfo = [ 552c6d8c1d9SAndreas Gohr 'page' => $acknowledgements[0]['page'], 553c6d8c1d9SAndreas Gohr 'lastmod' => $acknowledgements[0]['lastmod'], 554c6d8c1d9SAndreas Gohr 'user' => null, 555c6d8c1d9SAndreas Gohr 'ack' => null, 556c6d8c1d9SAndreas Gohr ]; 557c6d8c1d9SAndreas Gohr 558c6d8c1d9SAndreas Gohr // fill up the result with all users that never acknowledged the page 559c6d8c1d9SAndreas Gohr $combined = []; 560c6d8c1d9SAndreas Gohr foreach ($acknowledgements as $ack) { 561c6d8c1d9SAndreas Gohr if ($ack['user'] !== null) { 562c6d8c1d9SAndreas Gohr $combined[$ack['user']] = $ack; 563c6d8c1d9SAndreas Gohr } 564c6d8c1d9SAndreas Gohr } 565c6d8c1d9SAndreas Gohr foreach ($users as $user) { 566c6d8c1d9SAndreas Gohr if (!isset($combined[$user])) { 567c6d8c1d9SAndreas Gohr $combined[$user] = array_merge($baseinfo, ['user' => $user]); 568c6d8c1d9SAndreas Gohr } 569c6d8c1d9SAndreas Gohr } 570c6d8c1d9SAndreas Gohr 571dee5618cSAnna Dabrowska // finally remove current acknowledgements if filter is used 572dee5618cSAnna Dabrowska // this cannot be done in SQL without loss of data, 573dee5618cSAnna Dabrowska // filtering must happen last, otherwise removed current acks will be re-added as due 574dee5618cSAnna Dabrowska if ($status === 'due') { 575dee5618cSAnna Dabrowska $combined = array_filter($combined, function ($info) { 576dee5618cSAnna Dabrowska return $info['ack'] < $info['lastmod']; 577dee5618cSAnna Dabrowska }); 578dee5618cSAnna Dabrowska } 579dee5618cSAnna Dabrowska 580c6d8c1d9SAndreas Gohr ksort($combined); 581c6d8c1d9SAndreas Gohr return array_values($combined); 582c6d8c1d9SAndreas Gohr } 583c6d8c1d9SAndreas Gohr 584c6d8c1d9SAndreas Gohr /** 585863b6e48SAndreas Gohr * Returns all acknowledgements 586863b6e48SAndreas Gohr * 587863b6e48SAndreas Gohr * @param int $limit maximum number of results 588dee5618cSAnna Dabrowska * @return array 589863b6e48SAndreas Gohr */ 590863b6e48SAndreas Gohr public function getAcknowledgements($limit = 100) 591863b6e48SAndreas Gohr { 592863b6e48SAndreas Gohr $sql = ' 59384db77b6SAndreas Gohr SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack 59484db77b6SAndreas Gohr FROM acks A, pages B 59584db77b6SAndreas Gohr WHERE A.page = B.page 59684db77b6SAndreas Gohr GROUP BY A.user, A.page 597863b6e48SAndreas Gohr ORDER BY ack DESC 598863b6e48SAndreas Gohr LIMIT ? 599863b6e48SAndreas Gohr '; 600dee5618cSAnna Dabrowska return $this->db->queryAll($sql, $limit); 601dee5618cSAnna Dabrowska } 602d6011abdSAnna Dabrowska 603dee5618cSAnna Dabrowska /** 604dee5618cSAnna Dabrowska * Returns a filter clause for acknowledgement queries depending on wanted status. 605dee5618cSAnna Dabrowska * 606dee5618cSAnna Dabrowska * @param string $status 607dee5618cSAnna Dabrowska * @param string $alias Table alias used in the SQL query 608dee5618cSAnna Dabrowska * @return string 609dee5618cSAnna Dabrowska */ 61027e483c3SAnna Dabrowska protected function getFilterClause($status, $alias) 611dee5618cSAnna Dabrowska { 612dee5618cSAnna Dabrowska switch ($status) { 613dee5618cSAnna Dabrowska case 'current': 614dee5618cSAnna Dabrowska $filterClause = " HAVING ack >= $alias.lastmod "; 615dee5618cSAnna Dabrowska break; 616dee5618cSAnna Dabrowska case 'due': 617dee5618cSAnna Dabrowska $filterClause = " HAVING (ack IS NULL) OR (ack < $alias.lastmod) "; 618dee5618cSAnna Dabrowska break; 619dee5618cSAnna Dabrowska case 'outdated': 620dee5618cSAnna Dabrowska $filterClause = " HAVING ack < $alias.lastmod "; 621dee5618cSAnna Dabrowska break; 622dee5618cSAnna Dabrowska case 'all': 623dee5618cSAnna Dabrowska default: 624dee5618cSAnna Dabrowska $filterClause = ''; 625dee5618cSAnna Dabrowska break; 626dee5618cSAnna Dabrowska } 627dee5618cSAnna Dabrowska return $filterClause; 628d6011abdSAnna Dabrowska } 629f09444ffSAndreas Gohr 630639d4c50SAndreas Gohr // endregion 6314d6d17d0SAndreas Gohr} 632