1<?php
2
3namespace dokuwiki\plugin\extendpage\meta;
4
5/**
6 * Class Assignments
7 *
8 * Manages the assignment of header, footer pages to pages and namespaces
9 *
10 * This is a singleton. Assignment data is only loaded once per request.
11 *
12 * @package dokuwiki\plugin\extendpage\meta
13 */
14class Assignments
15{
16
17    /** @var \helper_plugin_sqlite|null */
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 instace 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_extendpage_db $helper */
49        $helper = plugin_load('helper', 'extendpage_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 assignments_patterns ORDER BY pattern';
61        $res = $this->sqlite->query($sql);
62        $this->patterns = $this->sqlite->res2arr($res);
63        $this->sqlite->res_close($res);
64    }
65
66    /**
67     * Add a new assignment pattern to the pattern table
68     *
69     * @param string $pattern
70     * @param string $page
71     * @return bool
72     */
73    public function addPattern($pattern, $page, $pos)
74    {
75        // add the pattern
76        $sql = 'REPLACE INTO assignments_patterns (pattern, page, pos) VALUES (?,?,?)';
77        $ok = (bool) $this->sqlite->query($sql, array($pattern, $page, $pos));
78
79        $sql = 'SELECT last_insert_rowid()';
80        $res = $this->sqlite->query($sql);
81
82        // reload patterns
83        $this->loadPatterns();
84        $this->propagatePageAssignments($this->sqlite->res2single($res));
85
86        return $ok;
87    }
88
89    /**
90     * Remove an existing assignment pattern from the pattern table
91     *
92     * @param string $pattern
93     * @param string $page
94     * @return bool
95     */
96    public function removePattern($id)
97    {
98        // remove the pattern
99        $sql = 'DELETE FROM assignments_patterns WHERE id = ?';
100        $ok = (bool) $this->sqlite->query($sql, array($id));
101
102        // reload patterns
103        $this->loadPatterns();
104
105        // fetch possibly affected pages
106        $sql = 'SELECT pid FROM assignments WHERE pattern_id = ?';
107        $res = $this->sqlite->query($sql, $id);
108        $pagerows = $this->sqlite->res2arr($res);
109        $this->sqlite->res_close($res);
110
111        // reevalute the pages and unassign when needed
112        foreach ($pagerows as $row) {
113            $pages = $this->getPageAssignments($row['pid'], $row['pos'], true);
114            if (!in_array($page, $pages)) {
115                $this->deassignPageExtension($row['pid'], $row['pos']);
116            }
117        }
118
119        return $ok;
120    }
121
122    /**
123     * Clear all patterns - deassigns all pages
124     *
125     * This is mostly useful for testing and not used in the interface currently
126     *
127     * @param bool $full fully delete all previous assignments
128     * @return bool
129     */
130    public function clear($full = false)
131    {
132        $sql = 'DELETE FROM assignments_patterns';
133        $ok = (bool) $this->sqlite->query($sql);
134
135        if ($full) {
136            $sql = 'DELETE FROM assignments';
137        } else {
138            $sql = 'UPDATE assignments SET assigned = 0';
139        }
140        $ok = $ok && (bool) $this->sqlite->query($sql);
141
142        // reload patterns
143        $this->loadPatterns();
144
145        return $ok;
146    }
147
148    /**
149     * Add page to assignments
150     *
151     * @param string $page
152     * @param string $ext
153     * @return bool
154     */
155    public function assignPageExtension($page, $pattern)
156    {
157        $sql = 'REPLACE INTO assignments (pid, pattern_id, assigned) VALUES (?, ?, 1)';
158        return (bool) $this->sqlite->query($sql, array($page, $pattern));
159    }
160
161    /**
162     * Remove page from assignments
163     *
164     * @param string $page
165     * @param string $ext
166     * @return bool
167     */
168    public function deassignPageExtension($page, $pattern)
169    {
170        $sql = 'REPLACE INTO assignments (pid, pattern_id, assigned) VALUES (?, ?, 0)';
171        return (bool) $this->sqlite->query($sql, array($page, $pattern));
172    }
173
174    /**
175     * Get the whole pattern table
176     *
177     * @return array
178     */
179    public function getAllPatterns()
180    {
181        return $this->patterns;
182    }
183
184    /**
185     * Returns a list of extension page names assigned to the given page and position
186     *
187     * @param string $page
188     * @param string $pos
189     * @param bool $checkpatterns Should the current patterns be re-evaluated?
190     * @return \string[] extensions assigned
191     */
192    public function getPageAssignments($page, $pos, $checkpatterns = true)
193    {
194        $extensions = array();
195        $page = cleanID($page);
196
197        if ($checkpatterns) {
198            // evaluate patterns
199            $pns = ':' . getNS($page) . ':';
200            foreach ($this->patterns as $row) {
201                if (($this->matchPagePattern($row['pattern'], $page, $pns)) &&
202                    ($row['pos'] === $pos)) {
203                    $extensions[] = array('page' => $row['page']);
204                }
205            }
206        } else {
207            // just select
208            $sql = 'SELECT assignments_patterns.page
209                    FROM assignments, assignments_patterns
210                    WHERE assignments.pattern_id = assignments_patterns.id
211                    AND assignments.pid = ?
212                    AND assignments_patterns.pos = ?
213                    AND assignments.assigned = 1';
214            $res = $this->sqlite->query($sql, array($page, $pos));
215            $list = $this->sqlite->res2arr($res);
216            $this->sqlite->res_close($res);
217            foreach ($list as $row) {
218                $extensions[] = array(
219                    'page' => $row['assignments_patterns.page']
220                );
221            }
222        }
223
224        return array_unique($extensions);
225    }
226
227    /**
228     * Check if the given pattern matches the given page
229     *
230     * @param string $pattern the pattern to check against
231     * @param string $page the cleaned pageid to check
232     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
233     * @return bool
234     */
235    protected function matchPagePattern($pattern, $page, $pns = null)
236    {
237        if (trim($pattern, ':') == '**') return true; // match all
238
239        // regex patterns
240        if ($pattern[0] == '/') {
241            return (bool) preg_match($pattern, ":$page");
242        }
243
244        if (is_null($pns)) {
245            $pns = ':' . getNS($page) . ':';
246        }
247
248        $ans = ':' . cleanID($pattern) . ':';
249        if (substr($pattern, -2) == '**') {
250            // upper namespaces match
251            if (strpos($pns, $ans) === 0) {
252                return true;
253            }
254        } elseif (substr($pattern, -1) == '*') {
255            // namespaces match exact
256            if ($ans == $pns) {
257                return true;
258            }
259        } else {
260            // exact match
261            if (cleanID($pattern) == $page) {
262                return true;
263            }
264        }
265
266        return false;
267    }
268
269    /**
270     * fetch all pages where the extension page isn't assigned, yet and reevaluate the page assignments for those pages and assign when needed
271     *
272     * @param $page
273     */
274    public function propagatePageAssignments($pattern)
275    {
276        $sql = 'SELECT assignments.pid, assignments.pattern_id, assignments_patterns.pos
277                FROM assignments, assignments_patterns
278                WHERE assignments.pattern_id = assignments_patterns.id
279                AND assignments.assigned != 1 AND assignments.pattern_id = ?';
280        $res = $this->sqlite->query($sql, $pattern);
281        $pagerows = $this->sqlite->res2arr($res);
282        $this->sqlite->res_close($res);
283
284        foreach ($pagerows as $row) {
285            $pages = $this->getPageAssignments(
286                $row['assignments.pid'],
287                $row['assignments_patterns.pos'], true
288            );
289            if (in_array($row['assignments_patterns.page'], $pages)) {
290                $this->assignPageExtension($row['assignments.pid'], $pattern);
291            }
292        }
293    }
294}
295