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 51ba917e33SAnna Dabrowska * 529c3eae1eSAnna Dabrowska * @return bool 539c3eae1eSAnna Dabrowska */ 54ba917e33SAnna 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 12545240794SAnna Dabrowska /** 126*c92ac04cSAnna Dabrowska * Returns all users, formatted for autocomplete 12745240794SAnna Dabrowska * 12845240794SAnna Dabrowska * @return array 12945240794SAnna Dabrowska */ 13045240794SAnna Dabrowska public function getUsers() 13145240794SAnna Dabrowska { 13245240794SAnna Dabrowska /** @var AuthPlugin $auth */ 13345240794SAnna Dabrowska global $auth; 13445240794SAnna Dabrowska 13545240794SAnna Dabrowska if (!$auth->canDo('getUsers')) { 13645240794SAnna Dabrowska return []; 13745240794SAnna Dabrowska } 13845240794SAnna Dabrowska 13945240794SAnna Dabrowska $cb = function ($k, $v) { 14045240794SAnna Dabrowska return [ 14145240794SAnna Dabrowska 'value' => $k, 14245240794SAnna Dabrowska 'label' => $k . ' (' . $v['name'] . ')' 14345240794SAnna Dabrowska ]; 14445240794SAnna Dabrowska }; 14545240794SAnna Dabrowska $users = $auth->retrieveUsers(); 14645240794SAnna Dabrowska $users = array_map($cb, array_keys($users), array_values($users)); 14745240794SAnna Dabrowska 14845240794SAnna Dabrowska return $users; 14945240794SAnna Dabrowska } 15045240794SAnna Dabrowska 151639d4c50SAndreas Gohr // endregion 152639d4c50SAndreas Gohr // region Page Data 153639d4c50SAndreas Gohr 154639d4c50SAndreas Gohr /** 155ef3ab392SAndreas Gohr * Delete a page 156ef3ab392SAndreas Gohr * 157ef3ab392SAndreas Gohr * Cascades to delete all assigned data, etc. 158ef3ab392SAndreas Gohr * 159ef3ab392SAndreas Gohr * @param string $page Page ID 160ef3ab392SAndreas Gohr */ 161ef3ab392SAndreas Gohr public function removePage($page) 162ef3ab392SAndreas Gohr { 163ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 164ef3ab392SAndreas Gohr if (!$sqlite) return; 165ef3ab392SAndreas Gohr 166ef3ab392SAndreas Gohr $sql = "DELETE FROM pages WHERE page = ?"; 167fea1a86fSAndreas Gohr $sqlite->exec($sql, $page); 168ef3ab392SAndreas Gohr } 169ef3ab392SAndreas Gohr 170ef3ab392SAndreas Gohr /** 1715dee13f7SAnna Dabrowska * Update last modified date of page if content has changed 172ef3ab392SAndreas Gohr * 173ef3ab392SAndreas Gohr * @param string $page Page ID 174ef3ab392SAndreas Gohr * @param int $lastmod timestamp of last non-minor change 175ef3ab392SAndreas Gohr */ 1765dee13f7SAnna Dabrowska public function storePageDate($page, $lastmod, $newContent) 177ef3ab392SAndreas Gohr { 1783b76424dSannda $changelog = new PageChangeLog($page); 179789aa26fSAnna Dabrowska $revs = $changelog->getRevisions(0, 1); 180ed4e8871SAnna Dabrowska 181ed4e8871SAnna Dabrowska // compare content 182ed4e8871SAnna Dabrowska $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0]))); 183ed4e8871SAnna Dabrowska $newContent = str_replace(NL, '', $newContent); 184ed4e8871SAnna Dabrowska if ($oldContent === $newContent) return; 185ed4e8871SAnna Dabrowska 186ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 187ef3ab392SAndreas Gohr if (!$sqlite) return; 188ef3ab392SAndreas Gohr 189ef3ab392SAndreas Gohr $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)"; 190fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $lastmod]); 191ef3ab392SAndreas Gohr } 192ef3ab392SAndreas Gohr 193639d4c50SAndreas Gohr // endregion 194639d4c50SAndreas Gohr // region Assignments 195639d4c50SAndreas Gohr 196ef3ab392SAndreas Gohr /** 197f09444ffSAndreas Gohr * Clears direct assignments for a page 198f09444ffSAndreas Gohr * 199cabb51d3SAndreas Gohr * @param string $page Page ID 200cabb51d3SAndreas Gohr */ 201f09444ffSAndreas Gohr public function clearPageAssignments($page) 202cabb51d3SAndreas Gohr { 203cabb51d3SAndreas Gohr $sqlite = $this->getDB(); 204cabb51d3SAndreas Gohr if (!$sqlite) return; 205cabb51d3SAndreas Gohr 206f09444ffSAndreas Gohr $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?"; 207fea1a86fSAndreas Gohr $sqlite->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 { 219639d4c50SAndreas Gohr $sqlite = $this->getDB(); 220639d4c50SAndreas Gohr if (!$sqlite) return; 221639d4c50SAndreas Gohr 2223b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 223639d4c50SAndreas Gohr 224639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)"; 225fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees]); 226639d4c50SAndreas Gohr } 227639d4c50SAndreas Gohr 228639d4c50SAndreas Gohr /** 229639d4c50SAndreas Gohr * Set assignees for a given page from the patterns 230639d4c50SAndreas Gohr * @param string $page Page ID 231639d4c50SAndreas Gohr */ 232639d4c50SAndreas Gohr public function setAutoAssignees($page) 233639d4c50SAndreas Gohr { 234639d4c50SAndreas Gohr $sqlite = $this->getDB(); 235639d4c50SAndreas Gohr if (!$sqlite) return; 236639d4c50SAndreas Gohr 237639d4c50SAndreas Gohr $patterns = $this->getAssignmentPatterns(); 238639d4c50SAndreas Gohr 239639d4c50SAndreas Gohr // given assignees 240639d4c50SAndreas Gohr $assignees = ''; 241639d4c50SAndreas Gohr 242639d4c50SAndreas Gohr // find all patterns that match the page and add the configured assignees 243639d4c50SAndreas Gohr foreach ($patterns as $pattern => $assignees) { 244639d4c50SAndreas Gohr if ($this->matchPagePattern($pattern, $page)) { 245639d4c50SAndreas Gohr $assignees .= ',' . $assignees; 246639d4c50SAndreas Gohr } 247639d4c50SAndreas Gohr } 248639d4c50SAndreas Gohr 249639d4c50SAndreas Gohr // remove duplicates and empty entries 2503b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 251639d4c50SAndreas Gohr 252639d4c50SAndreas Gohr // store the assignees 253639d4c50SAndreas Gohr $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)"; 254fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees]); 255639d4c50SAndreas Gohr } 256639d4c50SAndreas Gohr 257639d4c50SAndreas Gohr /** 258639d4c50SAndreas Gohr * Is the given user one of the assignees for this page 259639d4c50SAndreas Gohr * 260639d4c50SAndreas Gohr * @param string $page Page ID 261639d4c50SAndreas Gohr * @param string $user user name to check 262639d4c50SAndreas Gohr * @param string[] $groups groups this user is in 263639d4c50SAndreas Gohr * @return bool 264639d4c50SAndreas Gohr */ 265639d4c50SAndreas Gohr public function isUserAssigned($page, $user, $groups) 266639d4c50SAndreas Gohr { 267639d4c50SAndreas Gohr $sqlite = $this->getDB(); 268639d4c50SAndreas Gohr if (!$sqlite) return false; 269639d4c50SAndreas Gohr 270639d4c50SAndreas Gohr $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?"; 271fea1a86fSAndreas Gohr $record = $sqlite->queryRecord($sql, $page); 272a806aa3dSSven if (!$record) return false; 273fea1a86fSAndreas Gohr $assignees = $record['pageassignees'] . ',' . $record['autoassignees']; 274639d4c50SAndreas Gohr return auth_isMember($assignees, $user, $groups); 275639d4c50SAndreas Gohr } 276639d4c50SAndreas Gohr 277639d4c50SAndreas Gohr /** 278639d4c50SAndreas Gohr * Fetch all assignments for a given user, with additional page information, 279833123deSAnna Dabrowska * by default filtering already granted acknowledgements. 280833123deSAnna Dabrowska * Filter can be switched off via $includeDone 281639d4c50SAndreas Gohr * 282639d4c50SAndreas Gohr * @param string $user 283639d4c50SAndreas Gohr * @param array $groups 284833123deSAnna Dabrowska * @param bool $includeDone 285833123deSAnna Dabrowska * 286639d4c50SAndreas Gohr * @return array|bool 287639d4c50SAndreas Gohr */ 288833123deSAnna Dabrowska public function getUserAssignments($user, $groups, $includeDone = false) 289639d4c50SAndreas Gohr { 290639d4c50SAndreas Gohr $sqlite = $this->getDB(); 291639d4c50SAndreas Gohr if (!$sqlite) return false; 292639d4c50SAndreas Gohr 293639d4c50SAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A 294639d4c50SAndreas Gohr JOIN pages B 295639d4c50SAndreas Gohr ON A.page = B.page 296639d4c50SAndreas Gohr LEFT JOIN acks C 297639d4c50SAndreas Gohr ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) ) 298833123deSAnna Dabrowska WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)"; 299833123deSAnna Dabrowska 300833123deSAnna Dabrowska if (!$includeDone) { 301833123deSAnna Dabrowska $sql .= ' AND ack IS NULL'; 302833123deSAnna Dabrowska } 303639d4c50SAndreas Gohr 304fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, $user, $user, implode('///', $groups)); 305639d4c50SAndreas Gohr } 306639d4c50SAndreas Gohr 307639d4c50SAndreas Gohr 308639d4c50SAndreas Gohr /** 309639d4c50SAndreas Gohr * Resolve names of users assigned to a given page 310639d4c50SAndreas Gohr * 311639d4c50SAndreas Gohr * This can be slow on huge user bases! 312639d4c50SAndreas Gohr * 313639d4c50SAndreas Gohr * @param string $page 314639d4c50SAndreas Gohr * @return array|false 315639d4c50SAndreas Gohr */ 316639d4c50SAndreas Gohr public function getPageAssignees($page) 317639d4c50SAndreas Gohr { 318639d4c50SAndreas Gohr $sqlite = $this->getDB(); 319639d4c50SAndreas Gohr if (!$sqlite) return false; 320639d4c50SAndreas Gohr /** @var AuthPlugin $auth */ 321639d4c50SAndreas Gohr global $auth; 322639d4c50SAndreas Gohr 323639d4c50SAndreas Gohr $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments' 324639d4c50SAndreas Gohr FROM assignments 325639d4c50SAndreas Gohr WHERE page = ?"; 326fea1a86fSAndreas Gohr $assignments = $sqlite->queryValue($sql, $page); 327639d4c50SAndreas Gohr 328639d4c50SAndreas Gohr $users = []; 329639d4c50SAndreas Gohr foreach (explode(',', $assignments) as $item) { 330639d4c50SAndreas Gohr $item = trim($item); 331639d4c50SAndreas Gohr if ($item === '') continue; 332639d4c50SAndreas Gohr if ($item[0] == '@') { 333639d4c50SAndreas Gohr $users = array_merge( 334639d4c50SAndreas Gohr $users, 335639d4c50SAndreas Gohr array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)])) 336639d4c50SAndreas Gohr ); 337639d4c50SAndreas Gohr } else { 338639d4c50SAndreas Gohr $users[] = $item; 339639d4c50SAndreas Gohr } 340639d4c50SAndreas Gohr } 341639d4c50SAndreas Gohr 342639d4c50SAndreas Gohr return array_unique($users); 343639d4c50SAndreas Gohr } 344639d4c50SAndreas Gohr 345639d4c50SAndreas Gohr // endregion 346639d4c50SAndreas Gohr // region Assignment Patterns 347639d4c50SAndreas Gohr 348639d4c50SAndreas Gohr /** 349f09444ffSAndreas Gohr * Get all the assignment patterns 350f09444ffSAndreas Gohr * @return array (pattern => assignees) 351f09444ffSAndreas Gohr */ 352f09444ffSAndreas Gohr public function getAssignmentPatterns() 353f09444ffSAndreas Gohr { 354f09444ffSAndreas Gohr $sqlite = $this->getDB(); 355f09444ffSAndreas Gohr if (!$sqlite) return []; 356f09444ffSAndreas Gohr 357f09444ffSAndreas Gohr $sql = "SELECT pattern, assignees FROM assignments_patterns"; 358fea1a86fSAndreas Gohr return $sqlite->queryKeyValueList($sql); 359f09444ffSAndreas Gohr } 360f09444ffSAndreas Gohr 361f09444ffSAndreas Gohr /** 362f09444ffSAndreas Gohr * Save new assignment patterns 363f09444ffSAndreas Gohr * 364f09444ffSAndreas Gohr * This resaves all patterns and reapplies them 365f09444ffSAndreas Gohr * 366f09444ffSAndreas Gohr * @param array $patterns (pattern => assignees) 367f09444ffSAndreas Gohr */ 368639d4c50SAndreas Gohr public function saveAssignmentPatterns($patterns) 369639d4c50SAndreas Gohr { 370f09444ffSAndreas Gohr $sqlite = $this->getDB(); 371f09444ffSAndreas Gohr if (!$sqlite) return; 372f09444ffSAndreas Gohr 373fea1a86fSAndreas Gohr $sqlite->getPdo()->beginTransaction(); 374fea1a86fSAndreas Gohr try { 375f09444ffSAndreas Gohr 376fea1a86fSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all assignments */ 377f09444ffSAndreas Gohr $sql = "UPDATE assignments SET autoassignees = ''"; 378fea1a86fSAndreas Gohr $sqlite->exec($sql); 379f09444ffSAndreas Gohr 380f09444ffSAndreas Gohr /** @noinspection SqlWithoutWhere Remove all patterns */ 381f09444ffSAndreas Gohr $sql = "DELETE FROM assignments_patterns"; 382fea1a86fSAndreas Gohr $sqlite->exec($sql); 383f09444ffSAndreas Gohr 384f09444ffSAndreas Gohr // insert new patterns and gather affected pages 385f09444ffSAndreas Gohr $pages = []; 386f09444ffSAndreas Gohr 387f09444ffSAndreas Gohr $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)"; 388f09444ffSAndreas Gohr foreach ($patterns as $pattern => $assignees) { 389f09444ffSAndreas Gohr $pattern = trim($pattern); 390f09444ffSAndreas Gohr $assignees = trim($assignees); 391f09444ffSAndreas Gohr if (!$pattern || !$assignees) continue; 392fea1a86fSAndreas Gohr $sqlite->exec($sql, [$pattern, $assignees]); 393f09444ffSAndreas Gohr 394f09444ffSAndreas Gohr // patterns may overlap, so we need to gather all affected pages first 395f09444ffSAndreas Gohr $affectedPages = $this->getPagesMatchingPattern($pattern); 396f09444ffSAndreas Gohr foreach ($affectedPages as $page) { 397f09444ffSAndreas Gohr if (isset($pages[$page])) { 398f09444ffSAndreas Gohr $pages[$page] .= ',' . $assignees; 399f09444ffSAndreas Gohr } else { 400f09444ffSAndreas Gohr $pages[$page] = $assignees; 401f09444ffSAndreas Gohr } 402f09444ffSAndreas Gohr } 403f09444ffSAndreas Gohr } 404f09444ffSAndreas Gohr 405f09444ffSAndreas Gohr $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?) 406f09444ffSAndreas Gohr ON CONFLICT(page) 407f09444ffSAndreas Gohr DO UPDATE SET autoassignees = ?"; 408f09444ffSAndreas Gohr foreach ($pages as $page => $assignees) { 409f09444ffSAndreas Gohr // remove duplicates and empty entries 4103b76424dSannda $assignees = implode(',', array_unique(array_filter(array_map('trim', explode(',', $assignees))))); 411fea1a86fSAndreas Gohr $sqlite->exec($sql, [$page, $assignees, $assignees]); 412f09444ffSAndreas Gohr } 413fea1a86fSAndreas Gohr } catch (Exception $e) { 414fea1a86fSAndreas Gohr $sqlite->getPdo()->rollBack(); 415fea1a86fSAndreas Gohr throw $e; 416fea1a86fSAndreas Gohr } 417fea1a86fSAndreas Gohr $sqlite->getPdo()->commit(); 418f09444ffSAndreas Gohr } 419f09444ffSAndreas Gohr 420f09444ffSAndreas Gohr /** 421f09444ffSAndreas Gohr * Get all known pages that match the given pattern 422f09444ffSAndreas Gohr * 423f09444ffSAndreas Gohr * @param $pattern 424f09444ffSAndreas Gohr * @return string[] 425f09444ffSAndreas Gohr */ 426639d4c50SAndreas Gohr public function getPagesMatchingPattern($pattern) 427639d4c50SAndreas Gohr { 428f09444ffSAndreas Gohr $sqlite = $this->getDB(); 429f09444ffSAndreas Gohr if (!$sqlite) return []; 430f09444ffSAndreas Gohr 431f09444ffSAndreas Gohr $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)"; 432fea1a86fSAndreas Gohr $pages = $sqlite->queryAll($sql, $pattern); 433f09444ffSAndreas Gohr 434f09444ffSAndreas Gohr return array_column($pages, 'page'); 435f09444ffSAndreas Gohr } 436f09444ffSAndreas Gohr 437639d4c50SAndreas Gohr // endregion 438639d4c50SAndreas Gohr // region Acknowledgements 439ef3ab392SAndreas Gohr 440ef3ab392SAndreas Gohr /** 441ef3ab392SAndreas Gohr * Has the given user acknowledged the given page? 442ef3ab392SAndreas Gohr * 443ef3ab392SAndreas Gohr * @param string $page 444ef3ab392SAndreas Gohr * @param string $user 4455773dd37SAnna Dabrowska * @return bool|int timestamp of acknowledgement or false 446ef3ab392SAndreas Gohr */ 447ef3ab392SAndreas Gohr public function hasUserAcknowledged($page, $user) 448ef3ab392SAndreas Gohr { 449ef3ab392SAndreas Gohr $sqlite = $this->getDB(); 450ef3ab392SAndreas Gohr if (!$sqlite) return false; 451ef3ab392SAndreas Gohr 452ef3ab392SAndreas Gohr $sql = "SELECT ack 453ef3ab392SAndreas Gohr FROM acks A, pages B 454ef3ab392SAndreas Gohr WHERE A.page = B.page 4555773dd37SAnna Dabrowska AND A.page = ? 4565773dd37SAnna Dabrowska AND A.user = ? 457ef3ab392SAndreas Gohr AND A.ack >= B.lastmod"; 458ef3ab392SAndreas Gohr 459fea1a86fSAndreas Gohr $acktime = $sqlite->queryValue($sql, $page, $user); 460ef3ab392SAndreas Gohr 461ef3ab392SAndreas Gohr return $acktime ? (int)$acktime : false; 462ef3ab392SAndreas Gohr } 4635773dd37SAnna Dabrowska 4645773dd37SAnna Dabrowska /** 465d9a8334dSAnna Dabrowska * Timestamp of the latest acknowledgment of the given page 466d9a8334dSAnna Dabrowska * by the given user 467d9a8334dSAnna Dabrowska * 468d9a8334dSAnna Dabrowska * @param string $page 469d9a8334dSAnna Dabrowska * @param string $user 470d9a8334dSAnna Dabrowska * @return bool|string 471d9a8334dSAnna Dabrowska */ 472d9a8334dSAnna Dabrowska public function getLatestUserAcknowledgement($page, $user) 473d9a8334dSAnna Dabrowska { 474d9a8334dSAnna Dabrowska $sqlite = $this->getDB(); 475d9a8334dSAnna Dabrowska if (!$sqlite) return false; 476d9a8334dSAnna Dabrowska 477d9a8334dSAnna Dabrowska $sql = "SELECT MAX(ack) 478d9a8334dSAnna Dabrowska FROM acks 479d9a8334dSAnna Dabrowska WHERE page = ? 480d9a8334dSAnna Dabrowska AND user = ?"; 481d9a8334dSAnna Dabrowska 482fea1a86fSAndreas Gohr return $sqlite->queryValue($sql, [$page, $user]); 483d9a8334dSAnna Dabrowska } 484d9a8334dSAnna Dabrowska 485d9a8334dSAnna Dabrowska /** 4865773dd37SAnna Dabrowska * Save user's acknowledgement for a given page 4875773dd37SAnna Dabrowska * 4885773dd37SAnna Dabrowska * @param string $page 4895773dd37SAnna Dabrowska * @param string $user 4905773dd37SAnna Dabrowska * @return bool 4915773dd37SAnna Dabrowska */ 4925773dd37SAnna Dabrowska public function saveAcknowledgement($page, $user) 4935773dd37SAnna Dabrowska { 4945773dd37SAnna Dabrowska $sqlite = $this->getDB(); 4955773dd37SAnna Dabrowska if (!$sqlite) return false; 4965773dd37SAnna Dabrowska 4978e55e483SAnna Dabrowska $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))"; 4985773dd37SAnna Dabrowska 499fea1a86fSAndreas Gohr $sqlite->exec($sql, $page, $user); 5005773dd37SAnna Dabrowska return true; 5015773dd37SAnna Dabrowska } 50274126d4bSAnna Dabrowska 50374126d4bSAnna Dabrowska /** 504863b6e48SAndreas Gohr * Get all pages a user needs to acknowledge and the last acknowledge date 505d6011abdSAnna Dabrowska * 506863b6e48SAndreas Gohr * @param string $user 507863b6e48SAndreas Gohr * @param array $groups 508d6011abdSAnna Dabrowska * @return array|bool 509d6011abdSAnna Dabrowska */ 510863b6e48SAndreas Gohr public function getUserAcknowledgements($user, $groups) 511d6011abdSAnna Dabrowska { 512d6011abdSAnna Dabrowska $sqlite = $this->getDB(); 513d6011abdSAnna Dabrowska if (!$sqlite) return false; 514d6011abdSAnna Dabrowska 515f09444ffSAndreas Gohr $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack 516863b6e48SAndreas Gohr FROM assignments A 517863b6e48SAndreas Gohr JOIN pages B 518863b6e48SAndreas Gohr ON A.page = B.page 519863b6e48SAndreas Gohr LEFT JOIN acks C 520863b6e48SAndreas Gohr ON A.page = C.page AND C.user = ? 521f09444ffSAndreas Gohr WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?) 522863b6e48SAndreas Gohr GROUP BY A.page 523863b6e48SAndreas Gohr ORDER BY A.page 524863b6e48SAndreas Gohr "; 525863b6e48SAndreas Gohr 526fea1a86fSAndreas Gohr return $sqlite->queryAll($sql, [$user, $user, implode('///', $groups)]); 527863b6e48SAndreas Gohr } 528863b6e48SAndreas Gohr 529863b6e48SAndreas Gohr /** 530c6d8c1d9SAndreas Gohr * Get ack status for all assigned users of a given page 531c6d8c1d9SAndreas Gohr * 532c6d8c1d9SAndreas Gohr * This can be slow! 533c6d8c1d9SAndreas Gohr * 534c6d8c1d9SAndreas Gohr * @param string $page 535c6d8c1d9SAndreas Gohr * @return array|false 536c6d8c1d9SAndreas Gohr */ 537b6817aacSAndreas Gohr public function getPageAcknowledgements($page, $max = 0) 538c6d8c1d9SAndreas Gohr { 539c6d8c1d9SAndreas Gohr $users = $this->getPageAssignees($page); 540c6d8c1d9SAndreas Gohr if ($users === false) return false; 541c6d8c1d9SAndreas Gohr $sqlite = $this->getDB(); 542c6d8c1d9SAndreas Gohr if (!$sqlite) return false; 543c6d8c1d9SAndreas Gohr 5443b76424dSannda $ulist = implode(',', array_map([$sqlite->getPdo(), 'quote'], $users)); 545c6d8c1d9SAndreas Gohr $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack 546c6d8c1d9SAndreas Gohr FROM pages A 547c6d8c1d9SAndreas Gohr LEFT JOIN acks B 548c6d8c1d9SAndreas Gohr ON A.page = B.page 549c6d8c1d9SAndreas Gohr AND B.user IN ($ulist) 550c6d8c1d9SAndreas Gohr WHERE A.page = ? 551c6d8c1d9SAndreas Gohr GROUP BY A.page, B.user 552c6d8c1d9SAndreas Gohr "; 553b6817aacSAndreas Gohr if ($max) $sql .= " LIMIT $max"; 554fea1a86fSAndreas Gohr $acknowledgements = $sqlite->queryAll($sql, $page); 555c6d8c1d9SAndreas Gohr 556c6d8c1d9SAndreas Gohr // there should be at least one result, unless the page is unknown 557c6d8c1d9SAndreas Gohr if (!count($acknowledgements)) return false; 558c6d8c1d9SAndreas Gohr 559c6d8c1d9SAndreas Gohr $baseinfo = [ 560c6d8c1d9SAndreas Gohr 'page' => $acknowledgements[0]['page'], 561c6d8c1d9SAndreas Gohr 'lastmod' => $acknowledgements[0]['lastmod'], 562c6d8c1d9SAndreas Gohr 'user' => null, 563c6d8c1d9SAndreas Gohr 'ack' => null, 564c6d8c1d9SAndreas Gohr ]; 565c6d8c1d9SAndreas Gohr 566c6d8c1d9SAndreas Gohr // fill up the result with all users that never acknowledged the page 567c6d8c1d9SAndreas Gohr $combined = []; 568c6d8c1d9SAndreas Gohr foreach ($acknowledgements as $ack) { 569c6d8c1d9SAndreas Gohr if ($ack['user'] !== null) { 570c6d8c1d9SAndreas Gohr $combined[$ack['user']] = $ack; 571c6d8c1d9SAndreas Gohr } 572c6d8c1d9SAndreas Gohr } 573c6d8c1d9SAndreas Gohr foreach ($users as $user) { 574c6d8c1d9SAndreas Gohr if (!isset($combined[$user])) { 575c6d8c1d9SAndreas Gohr $combined[$user] = array_merge($baseinfo, ['user' => $user]); 576c6d8c1d9SAndreas Gohr } 577c6d8c1d9SAndreas Gohr } 578c6d8c1d9SAndreas Gohr 579c6d8c1d9SAndreas Gohr ksort($combined); 580c6d8c1d9SAndreas Gohr return array_values($combined); 581c6d8c1d9SAndreas Gohr } 582c6d8c1d9SAndreas Gohr 583c6d8c1d9SAndreas Gohr /** 584863b6e48SAndreas Gohr * Returns all acknowledgements 585863b6e48SAndreas Gohr * 586863b6e48SAndreas Gohr * @param int $limit maximum number of results 587863b6e48SAndreas Gohr * @return array|bool 588863b6e48SAndreas Gohr */ 589863b6e48SAndreas Gohr public function getAcknowledgements($limit = 100) 590863b6e48SAndreas Gohr { 591863b6e48SAndreas Gohr $sqlite = $this->getDB(); 592863b6e48SAndreas Gohr if (!$sqlite) return false; 593863b6e48SAndreas Gohr 594863b6e48SAndreas Gohr $sql = ' 59584db77b6SAndreas Gohr SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack 59684db77b6SAndreas Gohr FROM acks A, pages B 59784db77b6SAndreas Gohr WHERE A.page = B.page 59884db77b6SAndreas Gohr GROUP BY A.user, A.page 599863b6e48SAndreas Gohr ORDER BY ack DESC 600863b6e48SAndreas Gohr LIMIT ? 601863b6e48SAndreas Gohr '; 602fea1a86fSAndreas Gohr $acknowledgements = $sqlite->queryAll($sql, $limit); 603d6011abdSAnna Dabrowska 604d6011abdSAnna Dabrowska return $acknowledgements; 605d6011abdSAnna Dabrowska } 606f09444ffSAndreas Gohr 607639d4c50SAndreas Gohr // endregion 6084d6d17d0SAndreas Gohr} 609