xref: /plugin/acknowledge/helper.php (revision 5966046c9f2860981ac6f31f94d13d9b1dc9b700)
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    /**
126c92ac04cSAnna 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    /**
504*5966046cSAnna Dabrowska     * Get all pages that a user needs to acknowledge and/or the last acknowledgement infos
505*5966046cSAnna Dabrowska     * depending on the (optional) filter based on status of the acknowledgements.
506d6011abdSAnna Dabrowska     *
507863b6e48SAndreas Gohr     * @param string $user
508863b6e48SAndreas Gohr     * @param array $groups
509*5966046cSAnna Dabrowska     * @param string $status Optional status filter, can be all (default), current or due
510*5966046cSAnna Dabrowska     *
511d6011abdSAnna Dabrowska     * @return array|bool
512d6011abdSAnna Dabrowska     */
513*5966046cSAnna Dabrowska    public function getUserAcknowledgements($user, $groups, $status = '')
514d6011abdSAnna Dabrowska    {
515d6011abdSAnna Dabrowska        $sqlite = $this->getDB();
516d6011abdSAnna Dabrowska        if (!$sqlite) return false;
517d6011abdSAnna Dabrowska
518*5966046cSAnna Dabrowska        // filter clause
519*5966046cSAnna Dabrowska        switch ($status) {
520*5966046cSAnna Dabrowska            case 'current':
521*5966046cSAnna Dabrowska                $having = ' HAVING ack >= B.lastmod ';
522*5966046cSAnna Dabrowska                break;
523*5966046cSAnna Dabrowska            case 'due':
524*5966046cSAnna Dabrowska                $having = ' HAVING (ack IS NULL) OR (ack < B.lastmod) ';
525*5966046cSAnna Dabrowska                break;
526*5966046cSAnna Dabrowska            case 'outdated':
527*5966046cSAnna Dabrowska                $having = ' HAVING ack < B.lastmod ';
528*5966046cSAnna Dabrowska                break;
529*5966046cSAnna Dabrowska            case 'all':
530*5966046cSAnna Dabrowska            default:
531*5966046cSAnna Dabrowska                $having = '';
532*5966046cSAnna Dabrowska                break;
533*5966046cSAnna Dabrowska        }
534*5966046cSAnna Dabrowska
535*5966046cSAnna Dabrowska        // query
536f09444ffSAndreas Gohr        $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack
537863b6e48SAndreas Gohr                  FROM assignments A
538863b6e48SAndreas Gohr                  JOIN pages B
539863b6e48SAndreas Gohr                    ON A.page = B.page
540863b6e48SAndreas Gohr             LEFT JOIN acks C
541863b6e48SAndreas Gohr                    ON A.page = C.page AND C.user = ?
542f09444ffSAndreas Gohr                 WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?)
543*5966046cSAnna Dabrowska              GROUP BY A.page";
544*5966046cSAnna Dabrowska        $sql .= $having;
545*5966046cSAnna Dabrowska        $sql .= "
546*5966046cSAnna Dabrowska              ORDER BY A.page";
547863b6e48SAndreas Gohr
548fea1a86fSAndreas Gohr        return $sqlite->queryAll($sql, [$user, $user, implode('///', $groups)]);
549863b6e48SAndreas Gohr    }
550863b6e48SAndreas Gohr
551863b6e48SAndreas Gohr    /**
552c6d8c1d9SAndreas Gohr     * Get ack status for all assigned users of a given page
553c6d8c1d9SAndreas Gohr     *
554c6d8c1d9SAndreas Gohr     * This can be slow!
555c6d8c1d9SAndreas Gohr     *
556c6d8c1d9SAndreas Gohr     * @param string $page
557*5966046cSAnna Dabrowska     * @param string $user
558c6d8c1d9SAndreas Gohr     * @return array|false
559c6d8c1d9SAndreas Gohr     */
560*5966046cSAnna Dabrowska    public function getPageAcknowledgements($page, $max = 0, $user = '')
561c6d8c1d9SAndreas Gohr    {
562c6d8c1d9SAndreas Gohr        $users = $this->getPageAssignees($page);
563c6d8c1d9SAndreas Gohr        if ($users === false) return false;
564c6d8c1d9SAndreas Gohr        $sqlite = $this->getDB();
565c6d8c1d9SAndreas Gohr        if (!$sqlite) return false;
566c6d8c1d9SAndreas Gohr
5673b76424dSannda        $ulist = implode(',', array_map([$sqlite->getPdo(), 'quote'], $users));
568c6d8c1d9SAndreas Gohr        $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack
569c6d8c1d9SAndreas Gohr                  FROM pages A
570c6d8c1d9SAndreas Gohr             LEFT JOIN acks B
571c6d8c1d9SAndreas Gohr                    ON A.page = B.page
572c6d8c1d9SAndreas Gohr                   AND B.user IN ($ulist)
573c6d8c1d9SAndreas Gohr                WHERE  A.page = ?
574c6d8c1d9SAndreas Gohr              GROUP BY A.page, B.user
575c6d8c1d9SAndreas Gohr                 ";
576b6817aacSAndreas Gohr        if ($max) $sql .= " LIMIT $max";
577fea1a86fSAndreas Gohr        $acknowledgements = $sqlite->queryAll($sql, $page);
578c6d8c1d9SAndreas Gohr
579c6d8c1d9SAndreas Gohr        // there should be at least one result, unless the page is unknown
580c6d8c1d9SAndreas Gohr        if (!count($acknowledgements)) return false;
581c6d8c1d9SAndreas Gohr
582c6d8c1d9SAndreas Gohr        $baseinfo = [
583c6d8c1d9SAndreas Gohr            'page' => $acknowledgements[0]['page'],
584c6d8c1d9SAndreas Gohr            'lastmod' => $acknowledgements[0]['lastmod'],
585c6d8c1d9SAndreas Gohr            'user' => null,
586c6d8c1d9SAndreas Gohr            'ack' => null,
587c6d8c1d9SAndreas Gohr        ];
588c6d8c1d9SAndreas Gohr
589c6d8c1d9SAndreas Gohr        // fill up the result with all users that never acknowledged the page
590c6d8c1d9SAndreas Gohr        $combined = [];
591c6d8c1d9SAndreas Gohr        foreach ($acknowledgements as $ack) {
592c6d8c1d9SAndreas Gohr            if ($ack['user'] !== null) {
593c6d8c1d9SAndreas Gohr                $combined[$ack['user']] = $ack;
594c6d8c1d9SAndreas Gohr            }
595c6d8c1d9SAndreas Gohr        }
596c6d8c1d9SAndreas Gohr        foreach ($users as $user) {
597c6d8c1d9SAndreas Gohr            if (!isset($combined[$user])) {
598c6d8c1d9SAndreas Gohr                $combined[$user] = array_merge($baseinfo, ['user' => $user]);
599c6d8c1d9SAndreas Gohr            }
600c6d8c1d9SAndreas Gohr        }
601c6d8c1d9SAndreas Gohr
602c6d8c1d9SAndreas Gohr        ksort($combined);
603c6d8c1d9SAndreas Gohr        return array_values($combined);
604c6d8c1d9SAndreas Gohr    }
605c6d8c1d9SAndreas Gohr
606c6d8c1d9SAndreas Gohr    /**
607863b6e48SAndreas Gohr     * Returns all acknowledgements
608863b6e48SAndreas Gohr     *
609863b6e48SAndreas Gohr     * @param int $limit maximum number of results
610863b6e48SAndreas Gohr     * @return array|bool
611863b6e48SAndreas Gohr     */
612863b6e48SAndreas Gohr    public function getAcknowledgements($limit = 100)
613863b6e48SAndreas Gohr    {
614863b6e48SAndreas Gohr        $sqlite = $this->getDB();
615863b6e48SAndreas Gohr        if (!$sqlite) return false;
616863b6e48SAndreas Gohr
617863b6e48SAndreas Gohr        $sql = '
61884db77b6SAndreas Gohr            SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack
61984db77b6SAndreas Gohr              FROM acks A, pages B
62084db77b6SAndreas Gohr             WHERE A.page = B.page
62184db77b6SAndreas Gohr          GROUP BY A.user, A.page
622863b6e48SAndreas Gohr          ORDER BY ack DESC
623863b6e48SAndreas Gohr             LIMIT ?
624863b6e48SAndreas Gohr              ';
625fea1a86fSAndreas Gohr        $acknowledgements = $sqlite->queryAll($sql, $limit);
626d6011abdSAnna Dabrowska
627d6011abdSAnna Dabrowska        return $acknowledgements;
628d6011abdSAnna Dabrowska    }
629f09444ffSAndreas Gohr
630639d4c50SAndreas Gohr    // endregion
6314d6d17d0SAndreas Gohr}
632