1<?php
2
3namespace dokuwiki\plugin\structpublish\meta;
4
5use dokuwiki\plugin\sqlite\SQLiteDB;
6
7/**
8 * Class Assignments
9 *
10 * Manages the assignment of users to pages and namespaces
11 * This is a singleton. Assignment data is only loaded once per request.
12 *
13 * @see \dokuwiki\plugin\struct\meta\Assignments
14 */
15class Assignments
16{
17    /** @var SQLiteDB */
18    protected $sqlite;
19
20    /** @var  array All the assignments patterns */
21    protected $patterns;
22
23    /** @var Assignments */
24    protected static $instance = null;
25
26    /**
27     * Get the singleton instance of the Assignments
28     *
29     * @param bool $forcereload create a new instance to reload the assignment data
30     * @return Assignments
31     */
32    public static function getInstance($forcereload = false)
33    {
34        if (is_null(self::$instance) or $forcereload) {
35            $class = get_called_class();
36            self::$instance = new $class();
37        }
38        return self::$instance;
39    }
40
41    /**
42     * Assignments constructor.
43     *
44     * Not public. Use Assignments::getInstance() instead
45     */
46    protected function __construct()
47    {
48        /** @var \helper_plugin_structpublish_db $helper */
49        $helper = plugin_load('helper', 'struct_db');
50        $this->sqlite = $helper->getDB();
51
52        $this->loadPatterns();
53    }
54
55    /**
56     * Load existing assignment patterns
57     */
58    protected function loadPatterns()
59    {
60        $sql = 'SELECT * FROM structpublish_assignments_patterns ORDER BY pattern';
61        $this->patterns = $this->sqlite->queryAll($sql);
62    }
63
64    /**
65     * Add a new assignment pattern to the pattern table
66     *
67     * @param string $pattern
68     * @param string $user
69     * @param string $status
70     * @return bool
71     */
72    public function addPattern($pattern, $user, $status)
73    {
74        // add the pattern
75        $sql = 'REPLACE INTO structpublish_assignments_patterns (pattern, user, status) VALUES (?,?,?)';
76        $ok = (bool) $this->sqlite->query($sql, [$pattern, $user, $status]);
77
78        // reload patterns
79        $this->loadPatterns();
80
81        // update assignments
82        // fetch known pages
83        /** @var \helper_plugin_structpublish_db $dbHelper */
84        $dbHelper = plugin_load('helper', 'structpublish_db');
85        $pids = $dbHelper->getPages();
86
87        // wrap in transaction
88        $this->sqlite->query('BEGIN TRANSACTION');
89
90        foreach ($pids as $pid) {
91            $this->updatePageAssignments($pid);
92        }
93
94        $ok = $ok && $this->sqlite->query('COMMIT TRANSACTION');
95        if (!$ok) {
96            $this->sqlite->query('ROLLBACK TRANSACTION');
97        }
98
99        return $ok;
100    }
101
102    /**
103     * Remove an existing assignment pattern from the pattern table
104     *
105     * @param string $pattern
106     * @param string $user
107     * @param string $status
108     * @return bool
109     */
110    public function removePattern($pattern, $user, $status)
111    {
112        // remove the pattern
113        $sql = 'DELETE FROM structpublish_assignments_patterns WHERE pattern = ? AND user = ? AND status = ?';
114        $ok = (bool) $this->sqlite->query($sql, [$pattern, $user, $status]);
115
116        // reload patterns
117        $this->loadPatterns();
118
119        // fetch possibly affected pages
120        $sql = 'SELECT pid FROM structpublish_assignments WHERE user = ? AND status = ?';
121        $pagerows = $this->sqlite->queryAll($sql, [$user, $status]);
122
123        // remove page assignments matching the pattern being removed
124        $ok = true;
125        foreach ($pagerows as $row) {
126            $ok = $ok && $this->deassignPage($row['pid'], $user, $status);
127        }
128
129        return $ok;
130    }
131
132    /**
133     * Updates all assignments of a given page against the current patterns
134     *
135     * @param string $pid
136     */
137    public function updatePageAssignments($pid, $reload = false)
138    {
139        if ($reload) {
140            $this->loadPatterns();
141        }
142        $rules = $this->getPageAssignments($pid, true);
143
144        foreach ($rules as $status => $users) {
145            foreach ($users as $user) {
146                $this->assignPage($pid, $user, $status);
147            }
148        }
149
150        // FIXME reevalute existing assignments for exclusion
151    }
152
153    /**
154     * Clear all patterns - deassigns all pages
155     *
156     * This is mostly useful for testing and not used in the interface currently
157     *
158     * @param bool $full fully delete all previous assignments
159     * @return bool
160     */
161    public function clear($full = false)
162    {
163        $sql = 'DELETE FROM structpublish_assignments_patterns';
164        $ok = (bool) $this->sqlite->query($sql);
165
166        if ($full) {
167            $sql = 'DELETE FROM structpublish_assignments';
168        } else {
169            $sql = 'UPDATE structpublish_assignments SET assigned = 0';
170        }
171        $ok = $ok && (bool) $this->sqlite->query($sql);
172
173        // reload patterns
174        $this->loadPatterns();
175
176        return $ok;
177    }
178
179    /**
180     * Add page to assignments
181     *
182     * @param string $page
183     * @param string $user
184     * @param string $status
185     * @return bool
186     */
187    public function assignPage($page, $user = null, $status = null)
188    {
189        $sql = 'REPLACE INTO structpublish_assignments (pid, user, status, assigned) VALUES (?, ?, ?, 1)';
190        return (bool) $this->sqlite->query($sql, [$page, $user, $status]);
191    }
192
193    /**
194     * Remove page from assignments
195     *
196     * @param string $page
197     * @param string $user
198     * @return bool
199     */
200    public function deassignPage($page, $user, $status)
201    {
202        $sql = 'UPDATE structpublish_assignments SET assigned = 0 WHERE pid = ? AND user = ? AND status = ?';
203        return (bool) $this->sqlite->query($sql, [$page, $user, $status]);
204    }
205
206    /**
207     * Get the whole pattern table
208     *
209     * @return array
210     */
211    public function getAllPatterns()
212    {
213        return $this->patterns;
214    }
215
216    /**
217     * Returns a list of user/group string lists per status assigned to the given page
218     *
219     * @param string $page
220     * @param bool $checkpatterns Should the current patterns be re-evaluated?
221     * @return array users assigned [role => [user, ...], ...]
222     */
223    public function getPageAssignments($page, $checkpatterns = true)
224    {
225        $rules = [];
226        $page = cleanID($page);
227
228        if ($checkpatterns) {
229            $helper = plugin_load('helper', 'structpublish_assignments');
230            // evaluate patterns
231            $pns = ':' . getNS($page) . ':';
232            foreach ($this->patterns as $row) {
233                if ($helper->matchPagePattern($row['pattern'], $page, $pns)) {
234                    $rules[$row['status']][] = $row['user'];
235                }
236            }
237        } else {
238            // just select
239            $sql = 'SELECT user, status FROM structpublish_assignments WHERE pid = ? AND assigned = 1';
240            $list = $this->sqlite->queryAll($sql, [$page]);
241            foreach ($list as $row) {
242                $rules[$row['status']][] = $row['user'];
243            }
244        }
245
246        return $rules;
247    }
248
249    /**
250     * Get the pages known to struct and their assignment state
251     *
252     * @param bool $assignedonly limit results to currently assigned only
253     * @return array
254     */
255    public function getPages($assignedOnly = false)
256    {
257        $sql = 'SELECT pid, user, status, assigned FROM structpublish_assignments WHERE 1=1';
258
259        $opts = array();
260
261        if ($assignedOnly) {
262            $sql .= ' AND assigned = 1';
263        }
264
265        $sql .= ' ORDER BY pid, user, status';
266
267        $list = $this->sqlite->queryAll($sql, $opts);
268
269        $result = array();
270        foreach ($list as $row) {
271            $pid = $row['pid'];
272            $user = $row['user'];
273            $status = $row['status'];
274            if (!isset($result[$pid])) {
275                $result[$pid] = array();
276            }
277            $result[$pid][$user][$status] = (bool) $row['assigned'];
278        }
279
280        return $result;
281    }
282
283    /**
284     * @return SQLiteDB
285     */
286    public function getSqlite()
287    {
288        return $this->sqlite;
289    }
290}
291