1<?php
2/**
3 * DokuWiki Plugin publish (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Jarrod Lowe <dokuwiki@rrod.net>
7 * @author  Andreas Gohr <gohr@cosmocode.de>
8 */
9
10// must be run within Dokuwiki
11if (!defined('DOKU_INC')) die();
12
13class helper_plugin_publish extends DokuWiki_Plugin {
14
15    private $sortedApprovedRevisions = null;
16
17    /**
18     * checks if an id is within one of the namespaces in $namespace_list
19     *
20     * @param string $namespace_list
21     * @param string $id
22     *
23     * @return bool
24     */
25    function in_namespace($namespace_list, $id) {
26        // PHP apparantly does not have closures -
27        // so we will parse $valid ourselves. Wasteful.
28        $namespace_list = preg_split('/\s+/', $namespace_list);
29        //if(count($valid) == 0) { return true; }//whole wiki matches
30        if((count($namespace_list)==1) and ($namespace_list[0]=="")) { return true; }//whole wiki matches
31        $id = trim($id, ':');
32        $id = explode(':', $id);
33
34        // Check against all possible namespaces
35        foreach($namespace_list as $namespace) {
36            $namespace = explode(':', $namespace);
37            $current_ns_depth = 0;
38            $total_ns_depth = count($namespace);
39            $matching = true;
40
41            // Check each element, untill all elements of $v satisfied
42            while($current_ns_depth < $total_ns_depth) {
43                if(
44                    !isset($id[$current_ns_depth]) ||
45                    !isset($namespace[$current_ns_depth]) ||
46                    $namespace[$current_ns_depth] != $id[$current_ns_depth]
47                ) {
48                    // not a match
49                    $matching = false;
50                    break;
51                }
52                $current_ns_depth += 1;
53            }
54            if($matching) { return true; } // a match
55        }
56        return false;
57    }
58
59    /**
60     * check if given $dir contains a valid namespace or is contained in a valid namespace
61     *
62     * @param $valid_namespaces_list
63     * @param $dir
64     *
65     * @return bool
66     */
67    function is_dir_valid($valid_namespaces_list, $dir) {
68        $valid_namespaces_list = preg_split('/\s+/', $valid_namespaces_list);
69        //if(count($valid) == 0) { return true; }//whole wiki matches
70        if((count($valid_namespaces_list)==1) && ($valid_namespaces_list[0]=="")) { return true; }//whole wiki matches
71        $dir = trim($dir, ':');
72        $dir = explode(':', $dir);
73
74        // Check against all possible namespaces
75        foreach($valid_namespaces_list as $valid_namespace) {
76            $valid_namespace = explode(':', $valid_namespace);
77            $current_depth = 0;
78            $dir_depth = count($dir); //this is what is different from above!
79            $matching = true;
80
81            // Check each element, untill all elements of $v satisfied
82            while($current_depth < $dir_depth) {
83                if (empty($valid_namespace[$current_depth])) {
84                    break;
85                }
86                if($valid_namespace[$current_depth] != $dir[$current_depth]) {
87                    // not a match
88                    $matching = false;
89                    break;
90                }
91                $current_depth += 1;
92            }
93            if($matching) { return true; } // a match
94        }
95        return false;
96    }
97
98    function canApprove() {
99        global $INFO;
100        global $ID;
101
102        if (!$this->in_namespace($this->getConf('apr_namespaces'), $ID)) {
103            return false;
104        }
105
106        return ($INFO['perm'] >= AUTH_DELETE);
107    }
108
109    function getRevision($id = null) {
110        global $REV;
111        if (isset($REV) && !empty($REV)) {
112            return $REV;
113        }
114        $meta = $this->getMeta($id);
115        if($meta){
116            if(isset($meta['last_change']['date'])) return $meta['last_change']['date'];
117            if(isset($meta['date']['modified'])) return $meta['date']['modified'];
118        }
119        return 0;
120    }
121
122    function getApprovals($id = null) {
123        $meta = $this->getMeta($id);
124        if (!isset($meta['approval'])) {
125            return array();
126        }
127        $approvals = $meta['approval'];
128        if (!is_array($approvals)) {
129            return array();
130        }
131        return $approvals;
132    }
133
134    function getMeta($id = null) {
135        global $ID;
136        global $INFO;
137
138        if ($id === null) $id = $ID;
139
140        if($ID === $id && $INFO['meta']) {
141            $meta = $INFO['meta'];
142        } else {
143            $meta = p_get_metadata($id);
144        }
145
146        $this->checkApprovalFormat($meta, $id);
147
148        return $meta;
149    }
150
151    function checkApprovalFormat($meta, $id) {
152        if (isset($meta['approval_version']) && $meta['approval_version'] >= 2) {
153            return;
154        }
155
156        if (!$this->hasApprovals($meta)) {
157            return;
158        }
159
160        $approvals = $meta['approval'];
161        foreach (array_keys($approvals) as $approvedId) {
162            $keys = array_keys($approvals[$approvedId]);
163
164            if (is_array($approvals[$approvedId][$keys[0]])) {
165                continue; // current format
166            }
167
168            $newEntry = $approvals[$approvedId];
169            if (count($newEntry) !== 3) {
170                //continue; // some messed up format...
171            }
172            $newEntry[] = intval($approvedId); // revision is the time of page edit
173
174            $approvals[$approvedId] = array();
175            $approvals[$approvedId][$newEntry[0]] = $newEntry;
176        }
177        p_set_metadata($id, array('approval' => $approvals), true, true);
178        p_set_metadata($id, array('approval_version' => 2), true, true);
179    }
180
181    function hasApprovals($meta) {
182        return isset($meta['approval']) && !empty($meta['approval']);
183    }
184
185    function getApprovalsOnRevision($revision) {
186        $approvals = $this->getApprovals();
187
188        if (isset($approvals[$revision])) {
189            return $approvals[$revision];
190        }
191        return array();
192    }
193
194    function getSortedApprovedRevisions($id = null) {
195        if ($id === null) {
196            global $ID;
197            $id = $ID;
198        }
199
200        static $sortedApprovedRevisions = array();
201        if (!isset($sortedApprovedRevisions[$id])) {
202            $approvals = $this->getApprovals($id);
203            krsort($approvals);
204            $sortedApprovedRevisions[$id] = $approvals;
205        }
206
207        return $sortedApprovedRevisions[$id];
208    }
209
210    function isRevisionApproved($revision, $id = null) {
211        $approvals = $this->getApprovals($id);
212        if (!isset($approvals[$revision])) {
213            return false;
214        }
215        return (count($approvals[$revision]) >= $this->getConf('number_of_approved'));
216    }
217
218    function isCurrentRevisionApproved($id = null) {
219        return $this->isRevisionApproved($this->getRevision($id), $id);
220    }
221
222    function getLatestApprovedRevision($id = null) {
223        $approvals = $this->getSortedApprovedRevisions($id);
224        foreach ($approvals as $revision => $ignored) {
225            if ($this->isRevisionApproved($revision, $id)) {
226                return $revision;
227            }
228        }
229        return 0;
230    }
231
232    function getLastestRevision() {
233        global $INFO;
234        return $INFO['meta']['date']['modified'];
235    }
236
237    function getApprovalDate() {
238        if (!$this->isCurrentRevisionApproved()) {
239            return -1;
240        }
241
242        $approvals = $this->getApprovalsOnRevision($this->getRevision());
243        uasort($approvals, array(&$this, 'cmpApprovals'));
244        $keys = array_keys($approvals);
245        return $approvals[$keys[$this->getConf('number_of_approved') -1]][3];
246
247    }
248
249    function cmpApprovals($left, $right) {
250        if ($left[3] == $right[3]) {
251            return 0;
252        }
253        return ($left[3] < $right[3]) ? -1 : 1;
254    }
255
256    function getApprovers() {
257        $approvers = $this->getApprovalsOnRevision($this->getRevision());
258        if (count($approvers) === 0) {
259            return;
260        }
261
262        $result = array();
263        foreach ($approvers as $approver) {
264            $result[] = editorinfo($this->getApproverName($approver));
265        }
266        return $result;
267    }
268
269    function getApproverName($approver) {
270        if ($approver[1]) {
271            return $approver[1];
272        }
273        if ($approver[2]) {
274            return $approver[2];
275        }
276        return $approver[0];
277    }
278
279    function getPreviousApprovedRevision() {
280        $currentRevision = $this->getRevision();
281        $approvals = $this->getSortedApprovedRevisions();
282        foreach ($approvals as $revision => $ignored) {
283            if ($revision >= $currentRevision) {
284                continue;
285            }
286            if ($this->isRevisionApproved($revision)) {
287                return $revision;
288            }
289        }
290        return 0;
291    }
292
293    function isHidden($id = null) {
294        if (!$this->getConf('hide drafts')) {
295            return false;
296        }
297
298        // needs to check if the actual namespace belongs to the apr_namespaces
299        if ($id == null) {
300            global $ID;
301            $id = $ID;
302        }
303        if (!$this->isActive($id)) {
304            return false;
305        }
306
307        if ($this->getLatestApprovedRevision($id)) {
308            return false;
309        }
310        return true;
311    }
312
313    function isHiddenForUser($id = null) {
314        global $INPUT;
315        if (!$this->isHidden($id)) {
316            return false;
317        }
318
319        if ($id == null) {
320            global $ID;
321            $id = $ID;
322        }
323
324        $allowedGroups = array_filter(explode(' ', trim($this->getConf('author groups'))));
325        if (empty($allowedGroups)) {
326            return auth_quickaclcheck($id) < AUTH_EDIT;
327        }
328
329        if (!$INPUT->server->has('REMOTE_USER')) {
330            return true;
331        }
332
333        global $USERINFO;
334        foreach ($allowedGroups as $allowedGroup) {
335            $allowedGroup = trim($allowedGroup);
336            if (in_array($allowedGroup, $USERINFO['grps'])) {
337                return false;
338            }
339        }
340        return true;
341    }
342
343    function isActive($id = null) {
344        if ($id == null) {
345            global $ID;
346            $id = $ID;
347        }
348        if (!$this->in_namespace($this->getConf('apr_namespaces'), $id)) {
349            return false;
350        }
351
352        $no_apr_namespaces = $this->getConf('no_apr_namespaces');
353        if (!empty($no_apr_namespaces)) {
354            if ($this->in_namespace($no_apr_namespaces, $id)) {
355                return false;
356            }
357        }
358        return true;
359    }
360
361    /**
362     * Create absolute diff-link between the two given revisions
363     *
364     * @param string $id
365     * @param int $rev1
366     * @param int $rev2
367     * @return string Diff-Link or empty string if $rev1 == $rev2
368     */
369    public function getDifflink($id, $rev1, $rev2) {
370        if($rev1 == $rev2) {
371            return '';
372        }
373        $params = 'do=diff,rev2[0]=' . $rev1 . ',rev2[1]=' . $rev2 . ',difftype=sidebyside';
374        $difflink = wl($id, $params,true,'&');
375        return $difflink;
376    }
377
378    function getPagesFromNamespace($namespace) {
379        global $conf;
380        $dir = $conf['datadir'] . '/' . str_replace(':', '/', $namespace);
381        $pages = array();
382        search($pages, $dir, array($this,'_search_helper'), array($namespace, $this->getConf('apr_namespaces'),
383                                                                  $this->getConf('no_apr_namespaces')));
384        return $pages;
385    }
386
387    /**
388     * search callback function
389     *
390     * filter out pages which can't be approved by the current user
391     * then check if they need approving
392     */
393    function _search_helper(&$data, $base, $file, $type, $lvl, $opts) {
394        $ns = $opts[0];
395        $valid_ns = $opts[1];
396        $invalid_ns = $opts[2];
397
398        if ($type == 'd') {
399            return $this->is_dir_valid($valid_ns, $ns . ':' . str_replace('/', ':', $file));
400        }
401
402        if (!preg_match('#\.txt$#', $file)) {
403            return false;
404        }
405
406        $id = pathID($ns . $file);
407        if (!empty($valid_ns) && !$this->in_namespace($valid_ns, $id)) {
408            return false;
409        }
410
411        if (!empty($invalid_ns) && $this->in_namespace($invalid_ns, $id)) {
412            return false;
413        }
414
415        if (auth_quickaclcheck($id) < AUTH_DELETE) {
416            return false;
417        }
418
419        $meta = $this->getMeta($id);
420        if ($this->isCurrentRevisionApproved($id)) {
421
422            // Already approved
423            return false;
424        }
425
426        $data[] = array($id, $meta['approval'], $meta['last_change']['date']);
427        return false;
428    }
429
430    public function removeSubnamespacePages ($pages, $namespace) {
431        $cleanpages = array();
432        foreach ($pages as $page) {
433            if (getNS($page[0]) == $namespace) {
434                $cleanpages[] = $page;
435            }
436        }
437        return $cleanpages;
438    }
439
440}
441