xref: /plugin/acknowledge/helper.php (revision 833123dec1febd30874c2b07aea9ac69a1cd9206)
14d6d17d0SAndreas Gohr<?php
2c6d8c1d9SAndreas Gohr
3fea1a86fSAndreas Gohruse dokuwiki\ErrorHandler;
4c6d8c1d9SAndreas Gohruse dokuwiki\Extension\AuthPlugin;
5fea1a86fSAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB;
6c6d8c1d9SAndreas Gohr
74d6d17d0SAndreas Gohr/**
84d6d17d0SAndreas Gohr * DokuWiki Plugin acknowledge (Helper Component)
94d6d17d0SAndreas Gohr *
104d6d17d0SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
114d6d17d0SAndreas Gohr * @author  Andreas Gohr, Anna Dabrowska <dokuwiki@cosmocode.de>
124d6d17d0SAndreas Gohr */
134d6d17d0SAndreas Gohrclass helper_plugin_acknowledge extends DokuWiki_Plugin
144d6d17d0SAndreas Gohr{
154d6d17d0SAndreas Gohr
16fea1a86fSAndreas Gohr    protected $db;
17fea1a86fSAndreas Gohr
18639d4c50SAndreas Gohr    // region Database Management
19639d4c50SAndreas Gohr
20cabb51d3SAndreas Gohr    /**
21fea1a86fSAndreas Gohr     * Get SQLiteDB instance
22fea1a86fSAndreas Gohr     *
23fea1a86fSAndreas Gohr     * @return SQLiteDB|null
24cabb51d3SAndreas Gohr     */
25cabb51d3SAndreas Gohr    public function getDB()
26cabb51d3SAndreas Gohr    {
27fea1a86fSAndreas Gohr        if ($this->db === null) {
28fea1a86fSAndreas Gohr            try {
29fea1a86fSAndreas Gohr                $this->db = new SQLiteDB('acknowledgement', __DIR__ . '/db');
30fea1a86fSAndreas Gohr
31fea1a86fSAndreas Gohr                // register our custom functions
32fea1a86fSAndreas Gohr                $this->db->getPdo()->sqliteCreateFunction('AUTH_ISMEMBER', [$this, 'auth_isMember'], -1);
33fea1a86fSAndreas Gohr                $this->db->getPdo()->sqliteCreateFunction('MATCHES_PAGE_PATTERN', [$this, 'matchPagePattern'], 2);
34fea1a86fSAndreas Gohr            } catch (\Exception $exception) {
35fea1a86fSAndreas Gohr                if (defined('DOKU_UNITTEST')) throw new \RuntimeException('Could not load SQLite', 0, $exception);
36fea1a86fSAndreas Gohr                ErrorHandler::logException($exception);
37cabb51d3SAndreas Gohr                msg($this->getLang('error sqlite plugin missing'), -1);
38cabb51d3SAndreas Gohr                return null;
39cabb51d3SAndreas Gohr            }
40cabb51d3SAndreas Gohr        }
41fea1a86fSAndreas Gohr        return $this->db;
429c3eae1eSAnna Dabrowska    }
439c3eae1eSAnna Dabrowska
449c3eae1eSAnna Dabrowska    /**
459c3eae1eSAnna Dabrowska     * Wrapper function for auth_isMember which accepts groups as string
469c3eae1eSAnna Dabrowska     *
479c3eae1eSAnna Dabrowska     * @param string $memberList
489c3eae1eSAnna Dabrowska     * @param string $user
499c3eae1eSAnna Dabrowska     * @param string $groups
509c3eae1eSAnna Dabrowska     * @return bool
519c3eae1eSAnna Dabrowska     */
529c3eae1eSAnna Dabrowska    public function auth_isMember($memberList, $user, $groups)
539c3eae1eSAnna Dabrowska    {
5495113ed8SAnna Dabrowska        return auth_isMember($memberList, $user, explode('///', $groups));
559c3eae1eSAnna Dabrowska    }
569c3eae1eSAnna Dabrowska
579c3eae1eSAnna Dabrowska    /**
58639d4c50SAndreas Gohr     * Fills the page index with all unknown pages from the fulltext index
59639d4c50SAndreas Gohr     * @return void
60639d4c50SAndreas Gohr     */
61639d4c50SAndreas Gohr    public function updatePageIndex()
62639d4c50SAndreas Gohr    {
63639d4c50SAndreas Gohr        $sqlite = $this->getDB();
64639d4c50SAndreas Gohr        if (!$sqlite) return;
65639d4c50SAndreas Gohr
66639d4c50SAndreas Gohr        $pages = idx_getIndex('page', '');
67639d4c50SAndreas Gohr        $sql = "INSERT OR IGNORE INTO pages (page, lastmod) VALUES (?,?)";
68639d4c50SAndreas Gohr
69fea1a86fSAndreas Gohr        $sqlite->getPdo()->beginTransaction();
70639d4c50SAndreas Gohr        foreach ($pages as $page) {
71639d4c50SAndreas Gohr            $page = trim($page);
72639d4c50SAndreas Gohr            $lastmod = @filemtime(wikiFN($page));
73639d4c50SAndreas Gohr            if ($lastmod) {
74fea1a86fSAndreas Gohr                try {
75fea1a86fSAndreas Gohr                    $sqlite->exec($sql, [$page, $lastmod]);
76fea1a86fSAndreas Gohr                } catch (\Exception $exception) {
77fea1a86fSAndreas Gohr                    $sqlite->getPdo()->rollBack();
78fea1a86fSAndreas Gohr                    throw $exception;
79639d4c50SAndreas Gohr                }
80639d4c50SAndreas Gohr            }
81fea1a86fSAndreas Gohr        }
82fea1a86fSAndreas Gohr        $sqlite->getPdo()->commit();
83639d4c50SAndreas Gohr    }
84639d4c50SAndreas Gohr
85639d4c50SAndreas Gohr    /**
86639d4c50SAndreas Gohr     * Check if the given pattern matches the given page
87639d4c50SAndreas Gohr     *
88639d4c50SAndreas Gohr     * @param string $pattern the pattern to check against
89639d4c50SAndreas Gohr     * @param string $page the cleaned pageid to check
90639d4c50SAndreas Gohr     * @return bool
91639d4c50SAndreas Gohr     */
92639d4c50SAndreas Gohr    public function matchPagePattern($pattern, $page)
93639d4c50SAndreas Gohr    {
94639d4c50SAndreas Gohr        if (trim($pattern, ':') == '**') return true; // match all
95639d4c50SAndreas Gohr
96639d4c50SAndreas Gohr        // regex patterns
97639d4c50SAndreas Gohr        if ($pattern[0] == '/') {
98639d4c50SAndreas Gohr            return (bool)preg_match($pattern, ":$page");
99639d4c50SAndreas Gohr        }
100639d4c50SAndreas Gohr
101639d4c50SAndreas Gohr        $pns = ':' . getNS($page) . ':';
102639d4c50SAndreas Gohr
103639d4c50SAndreas Gohr        $ans = ':' . cleanID($pattern) . ':';
104639d4c50SAndreas Gohr        if (substr($pattern, -2) == '**') {
105639d4c50SAndreas Gohr            // upper namespaces match
106639d4c50SAndreas Gohr            if (strpos($pns, $ans) === 0) {
107639d4c50SAndreas Gohr                return true;
108639d4c50SAndreas Gohr            }
109639d4c50SAndreas Gohr        } elseif (substr($pattern, -1) == '*') {
110639d4c50SAndreas Gohr            // namespaces match exact
111639d4c50SAndreas Gohr            if ($ans == $pns) {
112639d4c50SAndreas Gohr                return true;
113639d4c50SAndreas Gohr            }
114639d4c50SAndreas Gohr        } else {
115639d4c50SAndreas Gohr            // exact match
116639d4c50SAndreas Gohr            if (cleanID($pattern) == $page) {
117639d4c50SAndreas Gohr                return true;
118639d4c50SAndreas Gohr            }
119639d4c50SAndreas Gohr        }
120639d4c50SAndreas Gohr
121639d4c50SAndreas Gohr        return false;
122639d4c50SAndreas Gohr    }
123639d4c50SAndreas Gohr
124639d4c50SAndreas Gohr    // endregion
125639d4c50SAndreas Gohr    // region Page Data
126639d4c50SAndreas Gohr
127639d4c50SAndreas Gohr    /**
128ef3ab392SAndreas Gohr     * Delete a page
129ef3ab392SAndreas Gohr     *
130ef3ab392SAndreas Gohr     * Cascades to delete all assigned data, etc.
131ef3ab392SAndreas Gohr     *
132ef3ab392SAndreas Gohr     * @param string $page Page ID
133ef3ab392SAndreas Gohr     */
134ef3ab392SAndreas Gohr    public function removePage($page)
135ef3ab392SAndreas Gohr    {
136ef3ab392SAndreas Gohr        $sqlite = $this->getDB();
137ef3ab392SAndreas Gohr        if (!$sqlite) return;
138ef3ab392SAndreas Gohr
139ef3ab392SAndreas Gohr        $sql = "DELETE FROM pages WHERE page = ?";
140fea1a86fSAndreas Gohr        $sqlite->exec($sql, $page);
141ef3ab392SAndreas Gohr    }
142ef3ab392SAndreas Gohr
143ef3ab392SAndreas Gohr    /**
1445dee13f7SAnna Dabrowska     * Update last modified date of page if content has changed
145ef3ab392SAndreas Gohr     *
146ef3ab392SAndreas Gohr     * @param string $page Page ID
147ef3ab392SAndreas Gohr     * @param int $lastmod timestamp of last non-minor change
148ef3ab392SAndreas Gohr     */
1495dee13f7SAnna Dabrowska    public function storePageDate($page, $lastmod, $newContent)
150ef3ab392SAndreas Gohr    {
151ed4e8871SAnna Dabrowska        $changelog = new \dokuwiki\ChangeLog\PageChangeLog($page);
152789aa26fSAnna Dabrowska        $revs = $changelog->getRevisions(0, 1);
153ed4e8871SAnna Dabrowska
154ed4e8871SAnna Dabrowska        // compare content
155ed4e8871SAnna Dabrowska        $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0])));
156ed4e8871SAnna Dabrowska        $newContent = str_replace(NL, '', $newContent);
157ed4e8871SAnna Dabrowska        if ($oldContent === $newContent) return;
158ed4e8871SAnna Dabrowska
159ef3ab392SAndreas Gohr        $sqlite = $this->getDB();
160ef3ab392SAndreas Gohr        if (!$sqlite) return;
161ef3ab392SAndreas Gohr
162ef3ab392SAndreas Gohr        $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)";
163fea1a86fSAndreas Gohr        $sqlite->exec($sql, [$page, $lastmod]);
164ef3ab392SAndreas Gohr    }
165ef3ab392SAndreas Gohr
166639d4c50SAndreas Gohr    // endregion
167639d4c50SAndreas Gohr    // region Assignments
168639d4c50SAndreas Gohr
169ef3ab392SAndreas Gohr    /**
170f09444ffSAndreas Gohr     * Clears direct assignments for a page
171f09444ffSAndreas Gohr     *
172cabb51d3SAndreas Gohr     * @param string $page Page ID
173cabb51d3SAndreas Gohr     */
174f09444ffSAndreas Gohr    public function clearPageAssignments($page)
175cabb51d3SAndreas Gohr    {
176cabb51d3SAndreas Gohr        $sqlite = $this->getDB();
177cabb51d3SAndreas Gohr        if (!$sqlite) return;
178cabb51d3SAndreas Gohr
179f09444ffSAndreas Gohr        $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?";
180fea1a86fSAndreas Gohr        $sqlite->exec($sql, $page);
181f09444ffSAndreas Gohr    }
182f09444ffSAndreas Gohr
183f09444ffSAndreas Gohr    /**
184639d4c50SAndreas Gohr     * Set assignees for a given page as manually specified
185639d4c50SAndreas Gohr     *
186639d4c50SAndreas Gohr     * @param string $page Page ID
187639d4c50SAndreas Gohr     * @param string $assignees
188639d4c50SAndreas Gohr     * @return void
189639d4c50SAndreas Gohr     */
190639d4c50SAndreas Gohr    public function setPageAssignees($page, $assignees)
191639d4c50SAndreas Gohr    {
192639d4c50SAndreas Gohr        $sqlite = $this->getDB();
193639d4c50SAndreas Gohr        if (!$sqlite) return;
194639d4c50SAndreas Gohr
195639d4c50SAndreas Gohr        $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees)))));
196639d4c50SAndreas Gohr
197639d4c50SAndreas Gohr        $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)";
198fea1a86fSAndreas Gohr        $sqlite->exec($sql, [$page, $assignees]);
199639d4c50SAndreas Gohr    }
200639d4c50SAndreas Gohr
201639d4c50SAndreas Gohr    /**
202639d4c50SAndreas Gohr     * Set assignees for a given page from the patterns
203639d4c50SAndreas Gohr     * @param string $page Page ID
204639d4c50SAndreas Gohr     */
205639d4c50SAndreas Gohr    public function setAutoAssignees($page)
206639d4c50SAndreas Gohr    {
207639d4c50SAndreas Gohr        $sqlite = $this->getDB();
208639d4c50SAndreas Gohr        if (!$sqlite) return;
209639d4c50SAndreas Gohr
210639d4c50SAndreas Gohr        $patterns = $this->getAssignmentPatterns();
211639d4c50SAndreas Gohr
212639d4c50SAndreas Gohr        // given assignees
213639d4c50SAndreas Gohr        $assignees = '';
214639d4c50SAndreas Gohr
215639d4c50SAndreas Gohr        // find all patterns that match the page and add the configured assignees
216639d4c50SAndreas Gohr        foreach ($patterns as $pattern => $assignees) {
217639d4c50SAndreas Gohr            if ($this->matchPagePattern($pattern, $page)) {
218639d4c50SAndreas Gohr                $assignees .= ',' . $assignees;
219639d4c50SAndreas Gohr            }
220639d4c50SAndreas Gohr        }
221639d4c50SAndreas Gohr
222639d4c50SAndreas Gohr        // remove duplicates and empty entries
223639d4c50SAndreas Gohr        $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees)))));
224639d4c50SAndreas Gohr
225639d4c50SAndreas Gohr        // store the assignees
226639d4c50SAndreas Gohr        $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)";
227fea1a86fSAndreas Gohr        $sqlite->exec($sql, [$page, $assignees]);
228639d4c50SAndreas Gohr    }
229639d4c50SAndreas Gohr
230639d4c50SAndreas Gohr    /**
231639d4c50SAndreas Gohr     * Is the given user one of the assignees for this page
232639d4c50SAndreas Gohr     *
233639d4c50SAndreas Gohr     * @param string $page Page ID
234639d4c50SAndreas Gohr     * @param string $user user name to check
235639d4c50SAndreas Gohr     * @param string[] $groups groups this user is in
236639d4c50SAndreas Gohr     * @return bool
237639d4c50SAndreas Gohr     */
238639d4c50SAndreas Gohr    public function isUserAssigned($page, $user, $groups)
239639d4c50SAndreas Gohr    {
240639d4c50SAndreas Gohr        $sqlite = $this->getDB();
241639d4c50SAndreas Gohr        if (!$sqlite) return false;
242639d4c50SAndreas Gohr
243639d4c50SAndreas Gohr        $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?";
244fea1a86fSAndreas Gohr        $record = $sqlite->queryRecord($sql, $page);
245a806aa3dSSven        if (!$record) return false;
246fea1a86fSAndreas Gohr        $assignees = $record['pageassignees'] . ',' . $record['autoassignees'];
247639d4c50SAndreas Gohr        return auth_isMember($assignees, $user, $groups);
248639d4c50SAndreas Gohr    }
249639d4c50SAndreas Gohr
250639d4c50SAndreas Gohr    /**
251639d4c50SAndreas Gohr     * Fetch all assignments for a given user, with additional page information,
252*833123deSAnna Dabrowska     * by default filtering already granted acknowledgements.
253*833123deSAnna Dabrowska     * Filter can be switched off via $includeDone
254639d4c50SAndreas Gohr     *
255639d4c50SAndreas Gohr     * @param string $user
256639d4c50SAndreas Gohr     * @param array $groups
257*833123deSAnna Dabrowska     * @param bool $includeDone
258*833123deSAnna Dabrowska     *
259639d4c50SAndreas Gohr     * @return array|bool
260639d4c50SAndreas Gohr     */
261*833123deSAnna Dabrowska    public function getUserAssignments($user, $groups, $includeDone = false)
262639d4c50SAndreas Gohr    {
263639d4c50SAndreas Gohr        $sqlite = $this->getDB();
264639d4c50SAndreas Gohr        if (!$sqlite) return false;
265639d4c50SAndreas Gohr
266639d4c50SAndreas Gohr        $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A
267639d4c50SAndreas Gohr                JOIN pages B
268639d4c50SAndreas Gohr                ON A.page = B.page
269639d4c50SAndreas Gohr                LEFT JOIN acks C
270639d4c50SAndreas Gohr                ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) )
271*833123deSAnna Dabrowska                WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)";
272*833123deSAnna Dabrowska
273*833123deSAnna Dabrowska        if (!$includeDone) {
274*833123deSAnna Dabrowska            $sql .= ' AND ack IS NULL';
275*833123deSAnna Dabrowska        }
276639d4c50SAndreas Gohr
277fea1a86fSAndreas Gohr        return $sqlite->queryAll($sql, $user, $user, implode('///', $groups));
278639d4c50SAndreas Gohr    }
279639d4c50SAndreas Gohr
280639d4c50SAndreas Gohr
281639d4c50SAndreas Gohr    /**
282639d4c50SAndreas Gohr     * Resolve names of users assigned to a given page
283639d4c50SAndreas Gohr     *
284639d4c50SAndreas Gohr     * This can be slow on huge user bases!
285639d4c50SAndreas Gohr     *
286639d4c50SAndreas Gohr     * @param string $page
287639d4c50SAndreas Gohr     * @return array|false
288639d4c50SAndreas Gohr     */
289639d4c50SAndreas Gohr    public function getPageAssignees($page)
290639d4c50SAndreas Gohr    {
291639d4c50SAndreas Gohr        $sqlite = $this->getDB();
292639d4c50SAndreas Gohr        if (!$sqlite) return false;
293639d4c50SAndreas Gohr        /** @var AuthPlugin $auth */
294639d4c50SAndreas Gohr        global $auth;
295639d4c50SAndreas Gohr
296639d4c50SAndreas Gohr        $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments'
297639d4c50SAndreas Gohr                  FROM assignments
298639d4c50SAndreas Gohr                 WHERE page = ?";
299fea1a86fSAndreas Gohr        $assignments = $sqlite->queryValue($sql, $page);
300639d4c50SAndreas Gohr
301639d4c50SAndreas Gohr        $users = [];
302639d4c50SAndreas Gohr        foreach (explode(',', $assignments) as $item) {
303639d4c50SAndreas Gohr            $item = trim($item);
304639d4c50SAndreas Gohr            if ($item === '') continue;
305639d4c50SAndreas Gohr            if ($item[0] == '@') {
306639d4c50SAndreas Gohr                $users = array_merge(
307639d4c50SAndreas Gohr                    $users,
308639d4c50SAndreas Gohr                    array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)]))
309639d4c50SAndreas Gohr                );
310639d4c50SAndreas Gohr            } else {
311639d4c50SAndreas Gohr                $users[] = $item;
312639d4c50SAndreas Gohr            }
313639d4c50SAndreas Gohr        }
314639d4c50SAndreas Gohr
315639d4c50SAndreas Gohr        return array_unique($users);
316639d4c50SAndreas Gohr    }
317639d4c50SAndreas Gohr
318639d4c50SAndreas Gohr    // endregion
319639d4c50SAndreas Gohr    // region Assignment Patterns
320639d4c50SAndreas Gohr
321639d4c50SAndreas Gohr    /**
322f09444ffSAndreas Gohr     * Get all the assignment patterns
323f09444ffSAndreas Gohr     * @return array (pattern => assignees)
324f09444ffSAndreas Gohr     */
325f09444ffSAndreas Gohr    public function getAssignmentPatterns()
326f09444ffSAndreas Gohr    {
327f09444ffSAndreas Gohr        $sqlite = $this->getDB();
328f09444ffSAndreas Gohr        if (!$sqlite) return [];
329f09444ffSAndreas Gohr
330f09444ffSAndreas Gohr        $sql = "SELECT pattern, assignees FROM assignments_patterns";
331fea1a86fSAndreas Gohr        return $sqlite->queryKeyValueList($sql);
332f09444ffSAndreas Gohr    }
333f09444ffSAndreas Gohr
334f09444ffSAndreas Gohr    /**
335f09444ffSAndreas Gohr     * Save new assignment patterns
336f09444ffSAndreas Gohr     *
337f09444ffSAndreas Gohr     * This resaves all patterns and reapplies them
338f09444ffSAndreas Gohr     *
339f09444ffSAndreas Gohr     * @param array $patterns (pattern => assignees)
340f09444ffSAndreas Gohr     */
341639d4c50SAndreas Gohr    public function saveAssignmentPatterns($patterns)
342639d4c50SAndreas Gohr    {
343f09444ffSAndreas Gohr        $sqlite = $this->getDB();
344f09444ffSAndreas Gohr        if (!$sqlite) return;
345f09444ffSAndreas Gohr
346fea1a86fSAndreas Gohr        $sqlite->getPdo()->beginTransaction();
347fea1a86fSAndreas Gohr        try {
348f09444ffSAndreas Gohr
349fea1a86fSAndreas Gohr            /** @noinspection SqlWithoutWhere Remove all assignments */
350f09444ffSAndreas Gohr            $sql = "UPDATE assignments SET autoassignees = ''";
351fea1a86fSAndreas Gohr            $sqlite->exec($sql);
352f09444ffSAndreas Gohr
353f09444ffSAndreas Gohr            /** @noinspection SqlWithoutWhere Remove all patterns */
354f09444ffSAndreas Gohr            $sql = "DELETE FROM assignments_patterns";
355fea1a86fSAndreas Gohr            $sqlite->exec($sql);
356f09444ffSAndreas Gohr
357f09444ffSAndreas Gohr            // insert new patterns and gather affected pages
358f09444ffSAndreas Gohr            $pages = [];
359f09444ffSAndreas Gohr
360f09444ffSAndreas Gohr            $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)";
361f09444ffSAndreas Gohr            foreach ($patterns as $pattern => $assignees) {
362f09444ffSAndreas Gohr                $pattern = trim($pattern);
363f09444ffSAndreas Gohr                $assignees = trim($assignees);
364f09444ffSAndreas Gohr                if (!$pattern || !$assignees) continue;
365fea1a86fSAndreas Gohr                $sqlite->exec($sql, [$pattern, $assignees]);
366f09444ffSAndreas Gohr
367f09444ffSAndreas Gohr                // patterns may overlap, so we need to gather all affected pages first
368f09444ffSAndreas Gohr                $affectedPages = $this->getPagesMatchingPattern($pattern);
369f09444ffSAndreas Gohr                foreach ($affectedPages as $page) {
370f09444ffSAndreas Gohr                    if (isset($pages[$page])) {
371f09444ffSAndreas Gohr                        $pages[$page] .= ',' . $assignees;
372f09444ffSAndreas Gohr                    } else {
373f09444ffSAndreas Gohr                        $pages[$page] = $assignees;
374f09444ffSAndreas Gohr                    }
375f09444ffSAndreas Gohr                }
376f09444ffSAndreas Gohr            }
377f09444ffSAndreas Gohr
378f09444ffSAndreas Gohr            $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?)
379f09444ffSAndreas Gohr                ON CONFLICT(page)
380f09444ffSAndreas Gohr                DO UPDATE SET autoassignees = ?";
381f09444ffSAndreas Gohr            foreach ($pages as $page => $assignees) {
382f09444ffSAndreas Gohr                // remove duplicates and empty entries
383f09444ffSAndreas Gohr                $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees)))));
384fea1a86fSAndreas Gohr                $sqlite->exec($sql, [$page, $assignees, $assignees]);
385f09444ffSAndreas Gohr            }
386fea1a86fSAndreas Gohr        } catch (Exception $e) {
387fea1a86fSAndreas Gohr            $sqlite->getPdo()->rollBack();
388fea1a86fSAndreas Gohr            throw $e;
389fea1a86fSAndreas Gohr        }
390fea1a86fSAndreas Gohr        $sqlite->getPdo()->commit();
391f09444ffSAndreas Gohr    }
392f09444ffSAndreas Gohr
393f09444ffSAndreas Gohr    /**
394f09444ffSAndreas Gohr     * Get all known pages that match the given pattern
395f09444ffSAndreas Gohr     *
396f09444ffSAndreas Gohr     * @param $pattern
397f09444ffSAndreas Gohr     * @return string[]
398f09444ffSAndreas Gohr     */
399639d4c50SAndreas Gohr    public function getPagesMatchingPattern($pattern)
400639d4c50SAndreas Gohr    {
401f09444ffSAndreas Gohr        $sqlite = $this->getDB();
402f09444ffSAndreas Gohr        if (!$sqlite) return [];
403f09444ffSAndreas Gohr
404f09444ffSAndreas Gohr        $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)";
405fea1a86fSAndreas Gohr        $pages = $sqlite->queryAll($sql, $pattern);
406f09444ffSAndreas Gohr
407f09444ffSAndreas Gohr        return array_column($pages, 'page');
408f09444ffSAndreas Gohr    }
409f09444ffSAndreas Gohr
410639d4c50SAndreas Gohr    // endregion
411639d4c50SAndreas Gohr    // region Acknowledgements
412ef3ab392SAndreas Gohr
413ef3ab392SAndreas Gohr    /**
414ef3ab392SAndreas Gohr     * Has the given user acknowledged the given page?
415ef3ab392SAndreas Gohr     *
416ef3ab392SAndreas Gohr     * @param string $page
417ef3ab392SAndreas Gohr     * @param string $user
4185773dd37SAnna Dabrowska     * @return bool|int timestamp of acknowledgement or false
419ef3ab392SAndreas Gohr     */
420ef3ab392SAndreas Gohr    public function hasUserAcknowledged($page, $user)
421ef3ab392SAndreas Gohr    {
422ef3ab392SAndreas Gohr        $sqlite = $this->getDB();
423ef3ab392SAndreas Gohr        if (!$sqlite) return false;
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
432fea1a86fSAndreas Gohr        $acktime = $sqlite->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        $sqlite = $this->getDB();
448d9a8334dSAnna Dabrowska        if (!$sqlite) return false;
449d9a8334dSAnna Dabrowska
450d9a8334dSAnna Dabrowska        $sql = "SELECT MAX(ack)
451d9a8334dSAnna Dabrowska                  FROM acks
452d9a8334dSAnna Dabrowska                 WHERE page = ?
453d9a8334dSAnna Dabrowska                   AND user = ?";
454d9a8334dSAnna Dabrowska
455fea1a86fSAndreas Gohr        return $sqlite->queryValue($sql, [$page, $user]);
456d9a8334dSAnna Dabrowska    }
457d9a8334dSAnna Dabrowska
458d9a8334dSAnna Dabrowska    /**
4595773dd37SAnna Dabrowska     * Save user's acknowledgement for a given page
4605773dd37SAnna Dabrowska     *
4615773dd37SAnna Dabrowska     * @param string $page
4625773dd37SAnna Dabrowska     * @param string $user
4635773dd37SAnna Dabrowska     * @return bool
4645773dd37SAnna Dabrowska     */
4655773dd37SAnna Dabrowska    public function saveAcknowledgement($page, $user)
4665773dd37SAnna Dabrowska    {
4675773dd37SAnna Dabrowska        $sqlite = $this->getDB();
4685773dd37SAnna Dabrowska        if (!$sqlite) return false;
4695773dd37SAnna Dabrowska
4708e55e483SAnna Dabrowska        $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))";
4715773dd37SAnna Dabrowska
472fea1a86fSAndreas Gohr        $sqlite->exec($sql, $page, $user);
4735773dd37SAnna Dabrowska        return true;
4745773dd37SAnna Dabrowska
4755773dd37SAnna Dabrowska    }
47674126d4bSAnna Dabrowska
47774126d4bSAnna Dabrowska    /**
478863b6e48SAndreas Gohr     * Get all pages a user needs to acknowledge and the last acknowledge date
479d6011abdSAnna Dabrowska     *
480863b6e48SAndreas Gohr     * @param string $user
481863b6e48SAndreas Gohr     * @param array $groups
482d6011abdSAnna Dabrowska     * @return array|bool
483d6011abdSAnna Dabrowska     */
484863b6e48SAndreas Gohr    public function getUserAcknowledgements($user, $groups)
485d6011abdSAnna Dabrowska    {
486d6011abdSAnna Dabrowska        $sqlite = $this->getDB();
487d6011abdSAnna Dabrowska        if (!$sqlite) return false;
488d6011abdSAnna Dabrowska
489f09444ffSAndreas Gohr        $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack
490863b6e48SAndreas Gohr                  FROM assignments A
491863b6e48SAndreas Gohr                  JOIN pages B
492863b6e48SAndreas Gohr                    ON A.page = B.page
493863b6e48SAndreas Gohr             LEFT JOIN acks C
494863b6e48SAndreas Gohr                    ON A.page = C.page AND C.user = ?
495f09444ffSAndreas Gohr                 WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?)
496863b6e48SAndreas Gohr            GROUP BY A.page
497863b6e48SAndreas Gohr            ORDER BY A.page
498863b6e48SAndreas Gohr            ";
499863b6e48SAndreas Gohr
500fea1a86fSAndreas Gohr        return $sqlite->queryAll($sql, [$user, $user, implode('///', $groups)]);
501863b6e48SAndreas Gohr    }
502863b6e48SAndreas Gohr
503863b6e48SAndreas Gohr    /**
504c6d8c1d9SAndreas Gohr     * Get ack status for all assigned users of a given page
505c6d8c1d9SAndreas Gohr     *
506c6d8c1d9SAndreas Gohr     * This can be slow!
507c6d8c1d9SAndreas Gohr     *
508c6d8c1d9SAndreas Gohr     * @param string $page
509c6d8c1d9SAndreas Gohr     * @return array|false
510c6d8c1d9SAndreas Gohr     */
511b6817aacSAndreas Gohr    public function getPageAcknowledgements($page, $max=0)
512c6d8c1d9SAndreas Gohr    {
513c6d8c1d9SAndreas Gohr        $users = $this->getPageAssignees($page);
514c6d8c1d9SAndreas Gohr        if ($users === false) return false;
515c6d8c1d9SAndreas Gohr        $sqlite = $this->getDB();
516c6d8c1d9SAndreas Gohr        if (!$sqlite) return false;
517c6d8c1d9SAndreas Gohr
518fea1a86fSAndreas Gohr        $ulist = join(',', array_map([$sqlite->getPdo(), 'quote'], $users));
519c6d8c1d9SAndreas Gohr        $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack
520c6d8c1d9SAndreas Gohr                  FROM pages A
521c6d8c1d9SAndreas Gohr             LEFT JOIN acks B
522c6d8c1d9SAndreas Gohr                    ON A.page = B.page
523c6d8c1d9SAndreas Gohr                   AND B.user IN ($ulist)
524c6d8c1d9SAndreas Gohr                WHERE  A.page = ?
525c6d8c1d9SAndreas Gohr              GROUP BY A.page, B.user
526c6d8c1d9SAndreas Gohr                 ";
527b6817aacSAndreas Gohr        if($max) $sql .= " LIMIT $max";
528fea1a86fSAndreas Gohr        $acknowledgements = $sqlite->queryAll($sql, $page);
529c6d8c1d9SAndreas Gohr
530c6d8c1d9SAndreas Gohr        // there should be at least one result, unless the page is unknown
531c6d8c1d9SAndreas Gohr        if (!count($acknowledgements)) return false;
532c6d8c1d9SAndreas Gohr
533c6d8c1d9SAndreas Gohr        $baseinfo = [
534c6d8c1d9SAndreas Gohr            'page' => $acknowledgements[0]['page'],
535c6d8c1d9SAndreas Gohr            'lastmod' => $acknowledgements[0]['lastmod'],
536c6d8c1d9SAndreas Gohr            'user' => null,
537c6d8c1d9SAndreas Gohr            'ack' => null,
538c6d8c1d9SAndreas Gohr        ];
539c6d8c1d9SAndreas Gohr
540c6d8c1d9SAndreas Gohr        // fill up the result with all users that never acknowledged the page
541c6d8c1d9SAndreas Gohr        $combined = [];
542c6d8c1d9SAndreas Gohr        foreach ($acknowledgements as $ack) {
543c6d8c1d9SAndreas Gohr            if ($ack['user'] !== null) {
544c6d8c1d9SAndreas Gohr                $combined[$ack['user']] = $ack;
545c6d8c1d9SAndreas Gohr            }
546c6d8c1d9SAndreas Gohr        }
547c6d8c1d9SAndreas Gohr        foreach ($users as $user) {
548c6d8c1d9SAndreas Gohr            if (!isset($combined[$user])) {
549c6d8c1d9SAndreas Gohr                $combined[$user] = array_merge($baseinfo, ['user' => $user]);
550c6d8c1d9SAndreas Gohr            }
551c6d8c1d9SAndreas Gohr        }
552c6d8c1d9SAndreas Gohr
553c6d8c1d9SAndreas Gohr        ksort($combined);
554c6d8c1d9SAndreas Gohr        return array_values($combined);
555c6d8c1d9SAndreas Gohr    }
556c6d8c1d9SAndreas Gohr
557c6d8c1d9SAndreas Gohr    /**
558863b6e48SAndreas Gohr     * Returns all acknowledgements
559863b6e48SAndreas Gohr     *
560863b6e48SAndreas Gohr     * @param int $limit maximum number of results
561863b6e48SAndreas Gohr     * @return array|bool
562863b6e48SAndreas Gohr     */
563863b6e48SAndreas Gohr    public function getAcknowledgements($limit = 100)
564863b6e48SAndreas Gohr    {
565863b6e48SAndreas Gohr        $sqlite = $this->getDB();
566863b6e48SAndreas Gohr        if (!$sqlite) return false;
567863b6e48SAndreas Gohr
568863b6e48SAndreas Gohr        $sql = '
56984db77b6SAndreas Gohr            SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack
57084db77b6SAndreas Gohr              FROM acks A, pages B
57184db77b6SAndreas Gohr             WHERE A.page = B.page
57284db77b6SAndreas Gohr          GROUP BY A.user, A.page
573863b6e48SAndreas Gohr          ORDER BY ack DESC
574863b6e48SAndreas Gohr             LIMIT ?
575863b6e48SAndreas Gohr              ';
576fea1a86fSAndreas Gohr        $acknowledgements = $sqlite->queryAll($sql, $limit);
577d6011abdSAnna Dabrowska
578d6011abdSAnna Dabrowska        return $acknowledgements;
579d6011abdSAnna Dabrowska    }
580f09444ffSAndreas Gohr
581639d4c50SAndreas Gohr    // endregion
5824d6d17d0SAndreas Gohr}
5834d6d17d0SAndreas Gohr
584