xref: /plugin/acknowledge/helper.php (revision 639d4c5095add499f9d33c658a27ce7d769e3d80)
14d6d17d0SAndreas Gohr<?php
2c6d8c1d9SAndreas Gohr
3c6d8c1d9SAndreas Gohruse dokuwiki\Extension\AuthPlugin;
4c6d8c1d9SAndreas Gohr
54d6d17d0SAndreas Gohr/**
64d6d17d0SAndreas Gohr * DokuWiki Plugin acknowledge (Helper Component)
74d6d17d0SAndreas Gohr *
84d6d17d0SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
94d6d17d0SAndreas Gohr * @author  Andreas Gohr, Anna Dabrowska <dokuwiki@cosmocode.de>
104d6d17d0SAndreas Gohr */
114d6d17d0SAndreas Gohrclass helper_plugin_acknowledge extends DokuWiki_Plugin
124d6d17d0SAndreas Gohr{
134d6d17d0SAndreas Gohr
14*639d4c50SAndreas Gohr    // region Database Management
15*639d4c50SAndreas Gohr
16cabb51d3SAndreas Gohr    /**
17cabb51d3SAndreas Gohr     * @return helper_plugin_sqlite|null
18cabb51d3SAndreas Gohr     */
19cabb51d3SAndreas Gohr    public function getDB()
20cabb51d3SAndreas Gohr    {
21cabb51d3SAndreas Gohr        /** @var \helper_plugin_sqlite $sqlite */
22cabb51d3SAndreas Gohr        $sqlite = plugin_load('helper', 'sqlite');
23cabb51d3SAndreas Gohr        if ($sqlite === null) {
24cabb51d3SAndreas Gohr            msg($this->getLang('error sqlite plugin missing'), -1);
25cabb51d3SAndreas Gohr            return null;
26cabb51d3SAndreas Gohr        }
27f09444ffSAndreas Gohr        $sqlite->getAdapter()->setUseNativeAlter(true);
28cabb51d3SAndreas Gohr        if (!$sqlite->init('acknowledgement', __DIR__ . '/db')) {
29cabb51d3SAndreas Gohr            return null;
30cabb51d3SAndreas Gohr        }
31cabb51d3SAndreas Gohr
329c3eae1eSAnna Dabrowska        $this->registerUDF($sqlite);
339c3eae1eSAnna Dabrowska
34cabb51d3SAndreas Gohr        return $sqlite;
35cabb51d3SAndreas Gohr    }
36cabb51d3SAndreas Gohr
37cabb51d3SAndreas Gohr    /**
389c3eae1eSAnna Dabrowska     * Register user defined functions
399c3eae1eSAnna Dabrowska     *
409c3eae1eSAnna Dabrowska     * @param helper_plugin_sqlite $sqlite
419c3eae1eSAnna Dabrowska     */
429c3eae1eSAnna Dabrowska    protected function registerUDF($sqlite)
439c3eae1eSAnna Dabrowska    {
449c3eae1eSAnna Dabrowska        $sqlite->create_function('AUTH_ISMEMBER', [$this, 'auth_isMember'], -1);
45f09444ffSAndreas Gohr        $sqlite->create_function('MATCHES_PAGE_PATTERN', [$this, 'matchPagePattern'], 2);
469c3eae1eSAnna Dabrowska    }
479c3eae1eSAnna Dabrowska
489c3eae1eSAnna Dabrowska    /**
499c3eae1eSAnna Dabrowska     * Wrapper function for auth_isMember which accepts groups as string
509c3eae1eSAnna Dabrowska     *
519c3eae1eSAnna Dabrowska     * @param string $memberList
529c3eae1eSAnna Dabrowska     * @param string $user
539c3eae1eSAnna Dabrowska     * @param string $groups
549c3eae1eSAnna Dabrowska     * @return bool
559c3eae1eSAnna Dabrowska     */
569c3eae1eSAnna Dabrowska    public function auth_isMember($memberList, $user, $groups)
579c3eae1eSAnna Dabrowska    {
5895113ed8SAnna Dabrowska        return auth_isMember($memberList, $user, explode('///', $groups));
599c3eae1eSAnna Dabrowska    }
609c3eae1eSAnna Dabrowska
619c3eae1eSAnna Dabrowska    /**
62*639d4c50SAndreas Gohr     * Fills the page index with all unknown pages from the fulltext index
63*639d4c50SAndreas Gohr     * @return void
64*639d4c50SAndreas Gohr     */
65*639d4c50SAndreas Gohr    public function updatePageIndex()
66*639d4c50SAndreas Gohr    {
67*639d4c50SAndreas Gohr        $sqlite = $this->getDB();
68*639d4c50SAndreas Gohr        if (!$sqlite) return;
69*639d4c50SAndreas Gohr
70*639d4c50SAndreas Gohr        $pages = idx_getIndex('page', '');
71*639d4c50SAndreas Gohr        $sql = "INSERT OR IGNORE INTO pages (page, lastmod) VALUES (?,?)";
72*639d4c50SAndreas Gohr
73*639d4c50SAndreas Gohr        $sqlite->query('BEGIN TRANSACTION');
74*639d4c50SAndreas Gohr        foreach ($pages as $page) {
75*639d4c50SAndreas Gohr            $page = trim($page);
76*639d4c50SAndreas Gohr            $lastmod = @filemtime(wikiFN($page));
77*639d4c50SAndreas Gohr            if ($lastmod) {
78*639d4c50SAndreas Gohr                $sqlite->query($sql, $page, $lastmod);
79*639d4c50SAndreas Gohr            }
80*639d4c50SAndreas Gohr        }
81*639d4c50SAndreas Gohr        $sqlite->query('COMMIT TRANSACTION');
82*639d4c50SAndreas Gohr    }
83*639d4c50SAndreas Gohr
84*639d4c50SAndreas Gohr    /**
85*639d4c50SAndreas Gohr     * Check if the given pattern matches the given page
86*639d4c50SAndreas Gohr     *
87*639d4c50SAndreas Gohr     * @param string $pattern the pattern to check against
88*639d4c50SAndreas Gohr     * @param string $page the cleaned pageid to check
89*639d4c50SAndreas Gohr     * @return bool
90*639d4c50SAndreas Gohr     */
91*639d4c50SAndreas Gohr    public function matchPagePattern($pattern, $page)
92*639d4c50SAndreas Gohr    {
93*639d4c50SAndreas Gohr        if (trim($pattern, ':') == '**') return true; // match all
94*639d4c50SAndreas Gohr
95*639d4c50SAndreas Gohr        // regex patterns
96*639d4c50SAndreas Gohr        if ($pattern[0] == '/') {
97*639d4c50SAndreas Gohr            return (bool)preg_match($pattern, ":$page");
98*639d4c50SAndreas Gohr        }
99*639d4c50SAndreas Gohr
100*639d4c50SAndreas Gohr        $pns = ':' . getNS($page) . ':';
101*639d4c50SAndreas Gohr
102*639d4c50SAndreas Gohr        $ans = ':' . cleanID($pattern) . ':';
103*639d4c50SAndreas Gohr        if (substr($pattern, -2) == '**') {
104*639d4c50SAndreas Gohr            // upper namespaces match
105*639d4c50SAndreas Gohr            if (strpos($pns, $ans) === 0) {
106*639d4c50SAndreas Gohr                return true;
107*639d4c50SAndreas Gohr            }
108*639d4c50SAndreas Gohr        } elseif (substr($pattern, -1) == '*') {
109*639d4c50SAndreas Gohr            // namespaces match exact
110*639d4c50SAndreas Gohr            if ($ans == $pns) {
111*639d4c50SAndreas Gohr                return true;
112*639d4c50SAndreas Gohr            }
113*639d4c50SAndreas Gohr        } else {
114*639d4c50SAndreas Gohr            // exact match
115*639d4c50SAndreas Gohr            if (cleanID($pattern) == $page) {
116*639d4c50SAndreas Gohr                return true;
117*639d4c50SAndreas Gohr            }
118*639d4c50SAndreas Gohr        }
119*639d4c50SAndreas Gohr
120*639d4c50SAndreas Gohr        return false;
121*639d4c50SAndreas Gohr    }
122*639d4c50SAndreas Gohr
123*639d4c50SAndreas Gohr    // endregion
124*639d4c50SAndreas Gohr    // region Page Data
125*639d4c50SAndreas Gohr
126*639d4c50SAndreas Gohr    /**
127ef3ab392SAndreas Gohr     * Delete a page
128ef3ab392SAndreas Gohr     *
129ef3ab392SAndreas Gohr     * Cascades to delete all assigned data, etc.
130ef3ab392SAndreas Gohr     *
131ef3ab392SAndreas Gohr     * @param string $page Page ID
132ef3ab392SAndreas Gohr     */
133ef3ab392SAndreas Gohr    public function removePage($page)
134ef3ab392SAndreas Gohr    {
135ef3ab392SAndreas Gohr        $sqlite = $this->getDB();
136ef3ab392SAndreas Gohr        if (!$sqlite) return;
137ef3ab392SAndreas Gohr
138ef3ab392SAndreas Gohr        $sql = "DELETE FROM pages WHERE page = ?";
139ef3ab392SAndreas Gohr        $sqlite->query($sql, $page);
140ef3ab392SAndreas Gohr    }
141ef3ab392SAndreas Gohr
142ef3ab392SAndreas Gohr    /**
1435dee13f7SAnna Dabrowska     * Update last modified date of page if content has changed
144ef3ab392SAndreas Gohr     *
145ef3ab392SAndreas Gohr     * @param string $page Page ID
146ef3ab392SAndreas Gohr     * @param int $lastmod timestamp of last non-minor change
147ef3ab392SAndreas Gohr     */
1485dee13f7SAnna Dabrowska    public function storePageDate($page, $lastmod, $newContent)
149ef3ab392SAndreas Gohr    {
150ed4e8871SAnna Dabrowska        $changelog = new \dokuwiki\ChangeLog\PageChangeLog($page);
151789aa26fSAnna Dabrowska        $revs = $changelog->getRevisions(0, 1);
152ed4e8871SAnna Dabrowska
153ed4e8871SAnna Dabrowska        // compare content
154ed4e8871SAnna Dabrowska        $oldContent = str_replace(NL, '', io_readFile(wikiFN($page, $revs[0])));
155ed4e8871SAnna Dabrowska        $newContent = str_replace(NL, '', $newContent);
156ed4e8871SAnna Dabrowska        if ($oldContent === $newContent) return;
157ed4e8871SAnna Dabrowska
158ef3ab392SAndreas Gohr        $sqlite = $this->getDB();
159ef3ab392SAndreas Gohr        if (!$sqlite) return;
160ef3ab392SAndreas Gohr
161ef3ab392SAndreas Gohr        $sql = "REPLACE INTO pages (page, lastmod) VALUES (?,?)";
162ef3ab392SAndreas Gohr        $sqlite->query($sql, $page, $lastmod);
163ef3ab392SAndreas Gohr    }
164ef3ab392SAndreas Gohr
165*639d4c50SAndreas Gohr    // endregion
166*639d4c50SAndreas Gohr    // region Assignments
167*639d4c50SAndreas Gohr
168ef3ab392SAndreas Gohr    /**
169f09444ffSAndreas Gohr     * Clears direct assignments for a page
170f09444ffSAndreas Gohr     *
171cabb51d3SAndreas Gohr     * @param string $page Page ID
172cabb51d3SAndreas Gohr     */
173f09444ffSAndreas Gohr    public function clearPageAssignments($page)
174cabb51d3SAndreas Gohr    {
175cabb51d3SAndreas Gohr        $sqlite = $this->getDB();
176cabb51d3SAndreas Gohr        if (!$sqlite) return;
177cabb51d3SAndreas Gohr
178f09444ffSAndreas Gohr        $sql = "UPDATE assignments SET pageassignees = '' WHERE page = ?";
179f09444ffSAndreas Gohr        $sqlite->query($sql, $page);
180f09444ffSAndreas Gohr    }
181f09444ffSAndreas Gohr
182f09444ffSAndreas Gohr    /**
183*639d4c50SAndreas Gohr     * Set assignees for a given page as manually specified
184*639d4c50SAndreas Gohr     *
185*639d4c50SAndreas Gohr     * @param string $page Page ID
186*639d4c50SAndreas Gohr     * @param string $assignees
187*639d4c50SAndreas Gohr     * @return void
188*639d4c50SAndreas Gohr     */
189*639d4c50SAndreas Gohr    public function setPageAssignees($page, $assignees)
190*639d4c50SAndreas Gohr    {
191*639d4c50SAndreas Gohr        $sqlite = $this->getDB();
192*639d4c50SAndreas Gohr        if (!$sqlite) return;
193*639d4c50SAndreas Gohr
194*639d4c50SAndreas Gohr        $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees)))));
195*639d4c50SAndreas Gohr
196*639d4c50SAndreas Gohr        $sql = "REPLACE INTO assignments ('page', 'pageassignees') VALUES (?,?)";
197*639d4c50SAndreas Gohr        $sqlite->query($sql, $page, $assignees);
198*639d4c50SAndreas Gohr    }
199*639d4c50SAndreas Gohr
200*639d4c50SAndreas Gohr    /**
201*639d4c50SAndreas Gohr     * Set assignees for a given page from the patterns
202*639d4c50SAndreas Gohr     * @param string $page Page ID
203*639d4c50SAndreas Gohr     */
204*639d4c50SAndreas Gohr    public function setAutoAssignees($page)
205*639d4c50SAndreas Gohr    {
206*639d4c50SAndreas Gohr        $sqlite = $this->getDB();
207*639d4c50SAndreas Gohr        if (!$sqlite) return;
208*639d4c50SAndreas Gohr
209*639d4c50SAndreas Gohr        $patterns = $this->getAssignmentPatterns();
210*639d4c50SAndreas Gohr
211*639d4c50SAndreas Gohr        // given assignees
212*639d4c50SAndreas Gohr        $assignees = '';
213*639d4c50SAndreas Gohr
214*639d4c50SAndreas Gohr        // find all patterns that match the page and add the configured assignees
215*639d4c50SAndreas Gohr        foreach ($patterns as $pattern => $assignees) {
216*639d4c50SAndreas Gohr            if ($this->matchPagePattern($pattern, $page)) {
217*639d4c50SAndreas Gohr                $assignees .= ',' . $assignees;
218*639d4c50SAndreas Gohr            }
219*639d4c50SAndreas Gohr        }
220*639d4c50SAndreas Gohr
221*639d4c50SAndreas Gohr        // remove duplicates and empty entries
222*639d4c50SAndreas Gohr        $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees)))));
223*639d4c50SAndreas Gohr
224*639d4c50SAndreas Gohr        // store the assignees
225*639d4c50SAndreas Gohr        $sql = "REPLACE INTO assignments ('page', 'autoassignees') VALUES (?,?)";
226*639d4c50SAndreas Gohr        $sqlite->query($sql, $page, $assignees);
227*639d4c50SAndreas Gohr    }
228*639d4c50SAndreas Gohr
229*639d4c50SAndreas Gohr    /**
230*639d4c50SAndreas Gohr     * Is the given user one of the assignees for this page
231*639d4c50SAndreas Gohr     *
232*639d4c50SAndreas Gohr     * @param string $page Page ID
233*639d4c50SAndreas Gohr     * @param string $user user name to check
234*639d4c50SAndreas Gohr     * @param string[] $groups groups this user is in
235*639d4c50SAndreas Gohr     * @return bool
236*639d4c50SAndreas Gohr     */
237*639d4c50SAndreas Gohr    public function isUserAssigned($page, $user, $groups)
238*639d4c50SAndreas Gohr    {
239*639d4c50SAndreas Gohr        $sqlite = $this->getDB();
240*639d4c50SAndreas Gohr        if (!$sqlite) return false;
241*639d4c50SAndreas Gohr
242*639d4c50SAndreas Gohr        $sql = "SELECT pageassignees,autoassignees FROM assignments WHERE page = ?";
243*639d4c50SAndreas Gohr        $result = $sqlite->query($sql, $page);
244*639d4c50SAndreas Gohr        $row = $sqlite->res2row($result);
245*639d4c50SAndreas Gohr        $sqlite->res_close($result);
246*639d4c50SAndreas Gohr        $assignees = $row['pageassignees'] . ',' . $row['autoassignees'];
247*639d4c50SAndreas Gohr        return auth_isMember($assignees, $user, $groups);
248*639d4c50SAndreas Gohr    }
249*639d4c50SAndreas Gohr
250*639d4c50SAndreas Gohr    /**
251*639d4c50SAndreas Gohr     * Fetch all assignments for a given user, with additional page information,
252*639d4c50SAndreas Gohr     * filtering already granted acknowledgements.
253*639d4c50SAndreas Gohr     *
254*639d4c50SAndreas Gohr     * @param string $user
255*639d4c50SAndreas Gohr     * @param array $groups
256*639d4c50SAndreas Gohr     * @return array|bool
257*639d4c50SAndreas Gohr     */
258*639d4c50SAndreas Gohr    public function getUserAssignments($user, $groups)
259*639d4c50SAndreas Gohr    {
260*639d4c50SAndreas Gohr        $sqlite = $this->getDB();
261*639d4c50SAndreas Gohr        if (!$sqlite) return false;
262*639d4c50SAndreas Gohr
263*639d4c50SAndreas Gohr        $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, C.ack FROM assignments A
264*639d4c50SAndreas Gohr                JOIN pages B
265*639d4c50SAndreas Gohr                ON A.page = B.page
266*639d4c50SAndreas Gohr                LEFT JOIN acks C
267*639d4c50SAndreas Gohr                ON A.page = C.page AND ( (C.user = ? AND C.ack > B.lastmod) )
268*639d4c50SAndreas Gohr                WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees , ? , ?)
269*639d4c50SAndreas Gohr                AND ack IS NULL";
270*639d4c50SAndreas Gohr
271*639d4c50SAndreas Gohr        $result = $sqlite->query($sql, $user, $user, implode('///', $groups));
272*639d4c50SAndreas Gohr        $assignments = $sqlite->res2arr($result);
273*639d4c50SAndreas Gohr        $sqlite->res_close($result);
274*639d4c50SAndreas Gohr
275*639d4c50SAndreas Gohr        return $assignments;
276*639d4c50SAndreas Gohr    }
277*639d4c50SAndreas Gohr
278*639d4c50SAndreas Gohr
279*639d4c50SAndreas Gohr    /**
280*639d4c50SAndreas Gohr     * Resolve names of users assigned to a given page
281*639d4c50SAndreas Gohr     *
282*639d4c50SAndreas Gohr     * This can be slow on huge user bases!
283*639d4c50SAndreas Gohr     *
284*639d4c50SAndreas Gohr     * @param string $page
285*639d4c50SAndreas Gohr     * @return array|false
286*639d4c50SAndreas Gohr     */
287*639d4c50SAndreas Gohr    public function getPageAssignees($page)
288*639d4c50SAndreas Gohr    {
289*639d4c50SAndreas Gohr        $sqlite = $this->getDB();
290*639d4c50SAndreas Gohr        if (!$sqlite) return false;
291*639d4c50SAndreas Gohr        /** @var AuthPlugin $auth */
292*639d4c50SAndreas Gohr        global $auth;
293*639d4c50SAndreas Gohr
294*639d4c50SAndreas Gohr        $sql = "SELECT pageassignees || ',' || autoassignees AS 'assignments'
295*639d4c50SAndreas Gohr                  FROM assignments
296*639d4c50SAndreas Gohr                 WHERE page = ?";
297*639d4c50SAndreas Gohr        $result = $sqlite->query($sql, $page);
298*639d4c50SAndreas Gohr        $assignments = $sqlite->res2single($result);
299*639d4c50SAndreas Gohr        $sqlite->res_close($result);
300*639d4c50SAndreas Gohr
301*639d4c50SAndreas Gohr        $users = [];
302*639d4c50SAndreas Gohr        foreach (explode(',', $assignments) as $item) {
303*639d4c50SAndreas Gohr            $item = trim($item);
304*639d4c50SAndreas Gohr            if ($item === '') continue;
305*639d4c50SAndreas Gohr            if ($item[0] == '@') {
306*639d4c50SAndreas Gohr                $users = array_merge(
307*639d4c50SAndreas Gohr                    $users,
308*639d4c50SAndreas Gohr                    array_keys($auth->retrieveUsers(0, 0, ['grps' => substr($item, 1)]))
309*639d4c50SAndreas Gohr                );
310*639d4c50SAndreas Gohr            } else {
311*639d4c50SAndreas Gohr                $users[] = $item;
312*639d4c50SAndreas Gohr            }
313*639d4c50SAndreas Gohr        }
314*639d4c50SAndreas Gohr
315*639d4c50SAndreas Gohr        return array_unique($users);
316*639d4c50SAndreas Gohr    }
317*639d4c50SAndreas Gohr
318*639d4c50SAndreas Gohr    // endregion
319*639d4c50SAndreas Gohr    // region Assignment Patterns
320*639d4c50SAndreas Gohr
321*639d4c50SAndreas 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";
331f09444ffSAndreas Gohr        $result = $sqlite->query($sql);
332f09444ffSAndreas Gohr        $patterns = $sqlite->res2arr($result);
333f09444ffSAndreas Gohr        $sqlite->res_close($result);
334f09444ffSAndreas Gohr
335f09444ffSAndreas Gohr        return array_combine(
336f09444ffSAndreas Gohr            array_column($patterns, 'pattern'),
337f09444ffSAndreas Gohr            array_column($patterns, 'assignees')
338f09444ffSAndreas Gohr        );
339f09444ffSAndreas Gohr    }
340f09444ffSAndreas Gohr
341f09444ffSAndreas Gohr    /**
342f09444ffSAndreas Gohr     * Save new assignment patterns
343f09444ffSAndreas Gohr     *
344f09444ffSAndreas Gohr     * This resaves all patterns and reapplies them
345f09444ffSAndreas Gohr     *
346f09444ffSAndreas Gohr     * @param array $patterns (pattern => assignees)
347f09444ffSAndreas Gohr     */
348*639d4c50SAndreas Gohr    public function saveAssignmentPatterns($patterns)
349*639d4c50SAndreas Gohr    {
350f09444ffSAndreas Gohr        $sqlite = $this->getDB();
351f09444ffSAndreas Gohr        if (!$sqlite) return;
352f09444ffSAndreas Gohr
353f09444ffSAndreas Gohr        $sqlite->query('BEGIN TRANSACTION');
354f09444ffSAndreas Gohr
355f09444ffSAndreas Gohr        /** @noinsp0ection SqlWithoutWhere Remove all assignments */
356f09444ffSAndreas Gohr        $sql = "UPDATE assignments SET autoassignees = ''";
357f09444ffSAndreas Gohr        $sqlite->query($sql);
358f09444ffSAndreas Gohr
359f09444ffSAndreas Gohr        /** @noinspection SqlWithoutWhere Remove all patterns */
360f09444ffSAndreas Gohr        $sql = "DELETE FROM assignments_patterns";
361f09444ffSAndreas Gohr        $sqlite->query($sql);
362f09444ffSAndreas Gohr
363f09444ffSAndreas Gohr        // insert new patterns and gather affected pages
364f09444ffSAndreas Gohr        $pages = [];
365f09444ffSAndreas Gohr
366f09444ffSAndreas Gohr        $sql = "REPLACE INTO assignments_patterns (pattern, assignees) VALUES (?,?)";
367f09444ffSAndreas Gohr        foreach ($patterns as $pattern => $assignees) {
368f09444ffSAndreas Gohr            $pattern = trim($pattern);
369f09444ffSAndreas Gohr            $assignees = trim($assignees);
370f09444ffSAndreas Gohr            if (!$pattern || !$assignees) continue;
371f09444ffSAndreas Gohr            $sqlite->query($sql, $pattern, $assignees);
372f09444ffSAndreas Gohr
373f09444ffSAndreas Gohr            // patterns may overlap, so we need to gather all affected pages first
374f09444ffSAndreas Gohr            $affectedPages = $this->getPagesMatchingPattern($pattern);
375f09444ffSAndreas Gohr            foreach ($affectedPages as $page) {
376f09444ffSAndreas Gohr                if (isset($pages[$page])) {
377f09444ffSAndreas Gohr                    $pages[$page] .= ',' . $assignees;
378f09444ffSAndreas Gohr                } else {
379f09444ffSAndreas Gohr                    $pages[$page] = $assignees;
380f09444ffSAndreas Gohr                }
381f09444ffSAndreas Gohr            }
382f09444ffSAndreas Gohr        }
383f09444ffSAndreas Gohr
384f09444ffSAndreas Gohr        $sql = "INSERT INTO assignments (page, autoassignees) VALUES (?, ?)
385f09444ffSAndreas Gohr                ON CONFLICT(page)
386f09444ffSAndreas Gohr                DO UPDATE SET autoassignees = ?";
387f09444ffSAndreas Gohr        foreach ($pages as $page => $assignees) {
388f09444ffSAndreas Gohr            // remove duplicates and empty entries
389f09444ffSAndreas Gohr            $assignees = join(',', array_unique(array_filter(array_map('trim', explode(',', $assignees)))));
390f09444ffSAndreas Gohr            $sqlite->query($sql, $page, $assignees, $assignees);
391f09444ffSAndreas Gohr        }
392f09444ffSAndreas Gohr
393f09444ffSAndreas Gohr        $sqlite->query('COMMIT TRANSACTION');
394f09444ffSAndreas Gohr    }
395f09444ffSAndreas Gohr
396f09444ffSAndreas Gohr    /**
397f09444ffSAndreas Gohr     * Get all known pages that match the given pattern
398f09444ffSAndreas Gohr     *
399f09444ffSAndreas Gohr     * @param $pattern
400f09444ffSAndreas Gohr     * @return string[]
401f09444ffSAndreas Gohr     */
402*639d4c50SAndreas Gohr    public function getPagesMatchingPattern($pattern)
403*639d4c50SAndreas Gohr    {
404f09444ffSAndreas Gohr        $sqlite = $this->getDB();
405f09444ffSAndreas Gohr        if (!$sqlite) return [];
406f09444ffSAndreas Gohr
407f09444ffSAndreas Gohr        $sql = "SELECT page FROM pages WHERE MATCHES_PAGE_PATTERN(?, page)";
408f09444ffSAndreas Gohr        $result = $sqlite->query($sql, $pattern);
409f09444ffSAndreas Gohr        $pages = $sqlite->res2arr($result);
410f09444ffSAndreas Gohr        $sqlite->res_close($result);
411f09444ffSAndreas Gohr
412f09444ffSAndreas Gohr        return array_column($pages, 'page');
413f09444ffSAndreas Gohr    }
414f09444ffSAndreas Gohr
415*639d4c50SAndreas Gohr    // endregion
416*639d4c50SAndreas Gohr    // region Acknowledgements
417ef3ab392SAndreas Gohr
418ef3ab392SAndreas Gohr    /**
419ef3ab392SAndreas Gohr     * Has the given user acknowledged the given page?
420ef3ab392SAndreas Gohr     *
421ef3ab392SAndreas Gohr     * @param string $page
422ef3ab392SAndreas Gohr     * @param string $user
4235773dd37SAnna Dabrowska     * @return bool|int timestamp of acknowledgement or false
424ef3ab392SAndreas Gohr     */
425ef3ab392SAndreas Gohr    public function hasUserAcknowledged($page, $user)
426ef3ab392SAndreas Gohr    {
427ef3ab392SAndreas Gohr        $sqlite = $this->getDB();
428ef3ab392SAndreas Gohr        if (!$sqlite) return false;
429ef3ab392SAndreas Gohr
430ef3ab392SAndreas Gohr        $sql = "SELECT ack
431ef3ab392SAndreas Gohr                  FROM acks A, pages B
432ef3ab392SAndreas Gohr                 WHERE A.page = B.page
4335773dd37SAnna Dabrowska                   AND A.page = ?
4345773dd37SAnna Dabrowska                   AND A.user = ?
435ef3ab392SAndreas Gohr                   AND A.ack >= B.lastmod";
436ef3ab392SAndreas Gohr
437ef3ab392SAndreas Gohr        $result = $sqlite->query($sql, $page, $user);
438ef3ab392SAndreas Gohr        $acktime = $sqlite->res2single($result);
439ef3ab392SAndreas Gohr        $sqlite->res_close($result);
440ef3ab392SAndreas Gohr
441ef3ab392SAndreas Gohr        return $acktime ? (int)$acktime : false;
442ef3ab392SAndreas Gohr    }
4435773dd37SAnna Dabrowska
4445773dd37SAnna Dabrowska    /**
445d9a8334dSAnna Dabrowska     * Timestamp of the latest acknowledgment of the given page
446d9a8334dSAnna Dabrowska     * by the given user
447d9a8334dSAnna Dabrowska     *
448d9a8334dSAnna Dabrowska     * @param string $page
449d9a8334dSAnna Dabrowska     * @param string $user
450d9a8334dSAnna Dabrowska     * @return bool|string
451d9a8334dSAnna Dabrowska     */
452d9a8334dSAnna Dabrowska    public function getLatestUserAcknowledgement($page, $user)
453d9a8334dSAnna Dabrowska    {
454d9a8334dSAnna Dabrowska        $sqlite = $this->getDB();
455d9a8334dSAnna Dabrowska        if (!$sqlite) return false;
456d9a8334dSAnna Dabrowska
457d9a8334dSAnna Dabrowska        $sql = "SELECT MAX(ack)
458d9a8334dSAnna Dabrowska                  FROM acks
459d9a8334dSAnna Dabrowska                 WHERE page = ?
460d9a8334dSAnna Dabrowska                   AND user = ?";
461d9a8334dSAnna Dabrowska
462d9a8334dSAnna Dabrowska        $result = $sqlite->query($sql, $page, $user);
463d9a8334dSAnna Dabrowska        $latestAck = $sqlite->res2single($result);
464d9a8334dSAnna Dabrowska        $sqlite->res_close($result);
465d9a8334dSAnna Dabrowska
466d9a8334dSAnna Dabrowska        return $latestAck;
467d9a8334dSAnna Dabrowska    }
468d9a8334dSAnna Dabrowska
469d9a8334dSAnna Dabrowska    /**
4705773dd37SAnna Dabrowska     * Save user's acknowledgement for a given page
4715773dd37SAnna Dabrowska     *
4725773dd37SAnna Dabrowska     * @param string $page
4735773dd37SAnna Dabrowska     * @param string $user
4745773dd37SAnna Dabrowska     * @return bool
4755773dd37SAnna Dabrowska     */
4765773dd37SAnna Dabrowska    public function saveAcknowledgement($page, $user)
4775773dd37SAnna Dabrowska    {
4785773dd37SAnna Dabrowska        $sqlite = $this->getDB();
4795773dd37SAnna Dabrowska        if (!$sqlite) return false;
4805773dd37SAnna Dabrowska
4818e55e483SAnna Dabrowska        $sql = "INSERT INTO acks (page, user, ack) VALUES (?,?, strftime('%s','now'))";
4825773dd37SAnna Dabrowska
4835773dd37SAnna Dabrowska        $result = $sqlite->query($sql, $page, $user);
4845773dd37SAnna Dabrowska        $sqlite->res_close($result);
4855773dd37SAnna Dabrowska        return true;
4865773dd37SAnna Dabrowska
4875773dd37SAnna Dabrowska    }
48874126d4bSAnna Dabrowska
48974126d4bSAnna Dabrowska    /**
490863b6e48SAndreas Gohr     * Get all pages a user needs to acknowledge and the last acknowledge date
491d6011abdSAnna Dabrowska     *
492863b6e48SAndreas Gohr     * @param string $user
493863b6e48SAndreas Gohr     * @param array $groups
494d6011abdSAnna Dabrowska     * @return array|bool
495d6011abdSAnna Dabrowska     */
496863b6e48SAndreas Gohr    public function getUserAcknowledgements($user, $groups)
497d6011abdSAnna Dabrowska    {
498d6011abdSAnna Dabrowska        $sqlite = $this->getDB();
499d6011abdSAnna Dabrowska        if (!$sqlite) return false;
500d6011abdSAnna Dabrowska
501f09444ffSAndreas Gohr        $sql = "SELECT A.page, A.pageassignees, A.autoassignees, B.lastmod, C.user, MAX(C.ack) AS ack
502863b6e48SAndreas Gohr                  FROM assignments A
503863b6e48SAndreas Gohr                  JOIN pages B
504863b6e48SAndreas Gohr                    ON A.page = B.page
505863b6e48SAndreas Gohr             LEFT JOIN acks C
506863b6e48SAndreas Gohr                    ON A.page = C.page AND C.user = ?
507f09444ffSAndreas Gohr                 WHERE AUTH_ISMEMBER(A.pageassignees || ',' || A.autoassignees, ? , ?)
508863b6e48SAndreas Gohr            GROUP BY A.page
509863b6e48SAndreas Gohr            ORDER BY A.page
510863b6e48SAndreas Gohr            ";
511863b6e48SAndreas Gohr
512863b6e48SAndreas Gohr        $result = $sqlite->query($sql, $user, $user, implode('///', $groups));
513863b6e48SAndreas Gohr        $assignments = $sqlite->res2arr($result);
514863b6e48SAndreas Gohr        $sqlite->res_close($result);
515863b6e48SAndreas Gohr
516863b6e48SAndreas Gohr        return $assignments;
517863b6e48SAndreas Gohr    }
518863b6e48SAndreas Gohr
519863b6e48SAndreas Gohr    /**
520c6d8c1d9SAndreas Gohr     * Get ack status for all assigned users of a given page
521c6d8c1d9SAndreas Gohr     *
522c6d8c1d9SAndreas Gohr     * This can be slow!
523c6d8c1d9SAndreas Gohr     *
524c6d8c1d9SAndreas Gohr     * @param string $page
525c6d8c1d9SAndreas Gohr     * @return array|false
526c6d8c1d9SAndreas Gohr     */
527c6d8c1d9SAndreas Gohr    public function getPageAcknowledgements($page)
528c6d8c1d9SAndreas Gohr    {
529c6d8c1d9SAndreas Gohr        $users = $this->getPageAssignees($page);
530c6d8c1d9SAndreas Gohr        if ($users === false) return false;
531c6d8c1d9SAndreas Gohr        $sqlite = $this->getDB();
532c6d8c1d9SAndreas Gohr        if (!$sqlite) return false;
533c6d8c1d9SAndreas Gohr
534c6d8c1d9SAndreas Gohr        $ulist = $sqlite->quote_and_join($users);
535c6d8c1d9SAndreas Gohr        $sql = "SELECT A.page, A.lastmod, B.user, MAX(B.ack) AS ack
536c6d8c1d9SAndreas Gohr                  FROM pages A
537c6d8c1d9SAndreas Gohr             LEFT JOIN acks B
538c6d8c1d9SAndreas Gohr                    ON A.page = B.page
539c6d8c1d9SAndreas Gohr                   AND B.user IN ($ulist)
540c6d8c1d9SAndreas Gohr                WHERE  A.page = ?
541c6d8c1d9SAndreas Gohr              GROUP BY A.page, B.user
542c6d8c1d9SAndreas Gohr                 ";
543c6d8c1d9SAndreas Gohr        $result = $sqlite->query($sql, $page);
544c6d8c1d9SAndreas Gohr        $acknowledgements = $sqlite->res2arr($result);
545c6d8c1d9SAndreas Gohr        $sqlite->res_close($result);
546c6d8c1d9SAndreas Gohr
547c6d8c1d9SAndreas Gohr        // there should be at least one result, unless the page is unknown
548c6d8c1d9SAndreas Gohr        if (!count($acknowledgements)) return false;
549c6d8c1d9SAndreas Gohr
550c6d8c1d9SAndreas Gohr        $baseinfo = [
551c6d8c1d9SAndreas Gohr            'page' => $acknowledgements[0]['page'],
552c6d8c1d9SAndreas Gohr            'lastmod' => $acknowledgements[0]['lastmod'],
553c6d8c1d9SAndreas Gohr            'user' => null,
554c6d8c1d9SAndreas Gohr            'ack' => null,
555c6d8c1d9SAndreas Gohr        ];
556c6d8c1d9SAndreas Gohr
557c6d8c1d9SAndreas Gohr        // fill up the result with all users that never acknowledged the page
558c6d8c1d9SAndreas Gohr        $combined = [];
559c6d8c1d9SAndreas Gohr        foreach ($acknowledgements as $ack) {
560c6d8c1d9SAndreas Gohr            if ($ack['user'] !== null) {
561c6d8c1d9SAndreas Gohr                $combined[$ack['user']] = $ack;
562c6d8c1d9SAndreas Gohr            }
563c6d8c1d9SAndreas Gohr        }
564c6d8c1d9SAndreas Gohr        foreach ($users as $user) {
565c6d8c1d9SAndreas Gohr            if (!isset($combined[$user])) {
566c6d8c1d9SAndreas Gohr                $combined[$user] = array_merge($baseinfo, ['user' => $user]);
567c6d8c1d9SAndreas Gohr            }
568c6d8c1d9SAndreas Gohr        }
569c6d8c1d9SAndreas Gohr
570c6d8c1d9SAndreas Gohr        ksort($combined);
571c6d8c1d9SAndreas Gohr        return array_values($combined);
572c6d8c1d9SAndreas Gohr    }
573c6d8c1d9SAndreas Gohr
574c6d8c1d9SAndreas Gohr    /**
575863b6e48SAndreas Gohr     * Returns all acknowledgements
576863b6e48SAndreas Gohr     *
577863b6e48SAndreas Gohr     * @param int $limit maximum number of results
578863b6e48SAndreas Gohr     * @return array|bool
579863b6e48SAndreas Gohr     */
580863b6e48SAndreas Gohr    public function getAcknowledgements($limit = 100)
581863b6e48SAndreas Gohr    {
582863b6e48SAndreas Gohr        $sqlite = $this->getDB();
583863b6e48SAndreas Gohr        if (!$sqlite) return false;
584863b6e48SAndreas Gohr
585863b6e48SAndreas Gohr        $sql = '
58684db77b6SAndreas Gohr            SELECT A.page, A.user, B.lastmod, max(A.ack) AS ack
58784db77b6SAndreas Gohr              FROM acks A, pages B
58884db77b6SAndreas Gohr             WHERE A.page = B.page
58984db77b6SAndreas Gohr          GROUP BY A.user, A.page
590863b6e48SAndreas Gohr          ORDER BY ack DESC
591863b6e48SAndreas Gohr             LIMIT ?
592863b6e48SAndreas Gohr              ';
593863b6e48SAndreas Gohr        $result = $sqlite->query($sql, $limit);
594d6011abdSAnna Dabrowska        $acknowledgements = $sqlite->res2arr($result);
595d6011abdSAnna Dabrowska        $sqlite->res_close($result);
596d6011abdSAnna Dabrowska
597d6011abdSAnna Dabrowska        return $acknowledgements;
598d6011abdSAnna Dabrowska    }
599f09444ffSAndreas Gohr
600*639d4c50SAndreas Gohr    // endregion
6014d6d17d0SAndreas Gohr}
6024d6d17d0SAndreas Gohr
603