xref: /plugin/approve/syntax/table.php (revision b81b11283adb1dcdb0fe36d5f4bdac1021fa065b)
1<?php
2
3use dokuwiki\plugin\approve\meta\ApproveConst;
4
5// must be run within DokuWiki
6if(!defined('DOKU_INC')) die();
7
8
9class syntax_plugin_approve_table extends DokuWiki_Syntax_Plugin {
10
11    const STATES = [ApproveConst::APPROVED, ApproveConst::READY_FOR_APPROVAL, ApproveConst::DRAFT];
12
13    function getType() {
14        return 'substition';
15    }
16
17    function getSort() {
18        return 20;
19    }
20
21    function PType() {
22        return 'block';
23    }
24
25    function connectTo($mode) {
26        $this->Lexer->addSpecialPattern('----+ *approve table *-+\n.*?----+', $mode,'plugin_approve_table');
27    }
28
29    function handle($match, $state, $pos, Doku_Handler $handler){
30        $lines = explode("\n", $match);
31        array_shift($lines);
32        array_pop($lines);
33
34        $params = [];
35        foreach ($lines as $line) {
36            $pair = explode(':', $line, 2);
37            if (count($pair) < 2) {
38                continue;
39            }
40            $key = trim($pair[0]);
41            $value = trim($pair[1]);
42            if ($key == 'states') {
43                $value = array_map('trim', explode(',', $value));
44                //normalize
45                $value = array_map('strtolower', $value);
46                $value = array_map('ucfirst', $value);
47                foreach ($value as $state) {
48                    if (!in_array($state, self::STATES)) {
49                        msg('approve plugin: unknown state "'.$state.'" should be: ' .
50                            implode(', ', self::STATES), -1);
51                        return false;
52                    }
53                }
54            } elseif($key == 'filter' && preg_match($value, null) === false) {
55                msg('approve plugin: invalid filter regex', -1);
56                return false;
57            }
58            $params[$key] = $value;
59        }
60        return $params;
61    }
62
63    function render($mode, Doku_Renderer $renderer, $params) {
64        global $conf;
65
66        if ($mode != 'xhtml') return false;
67        if ($params === false) return false;
68
69        $defaults = [
70            'namespace' => '',
71            'filter' => false,
72            'states' => self::STATES
73        ];
74
75        $params = array_replace($defaults, $params);
76
77        $namespace = cleanID(getNS($params['namespace'] . ":*"));
78
79        $pages = $this->_getPagesFromNamespace($namespace, $params['filter'], $params['states']);
80
81        usort($pages, array($this,'_pagesorter'));
82
83        // Output Table
84        $renderer->doc .= '<table><tr>';
85        $renderer->doc .= '<th>' . $this->getLang('hdr_page') . '</th>';
86        $renderer->doc .= '<th>' . $this->getLang('hdr_state') . '</th>';
87        $renderer->doc .= '<th>' . $this->getLang('hdr_updated') . '</th>';
88        $renderer->doc .= '</tr>';
89
90
91        $all_approved = 0;
92        $all_approved_ready = 0;
93        $all = 0;
94
95        $working_ns = null;
96        foreach($pages as $page) {
97            // $page: 0 -> pagename, 1 -> true -> approved else false, 2 -> last changed date
98            $this_ns = getNS($page[0]);
99
100            if($this_ns != $working_ns) {
101                $name_ns = $this_ns;
102                if($this_ns == '') { $name_ns = 'root'; }
103                $renderer->doc .= '<tr><td colspan="3"><a href="';
104                $renderer->doc .= wl($this_ns . ':' . $this->getConf('start'));
105                $renderer->doc .= '">';
106                $renderer->doc .= $name_ns;
107                $renderer->doc .= '</a> ';
108                $renderer->doc .= '</td></tr>';
109                $working_ns = $this_ns;
110            }
111
112            $updated = '<a href="' . wl($page[0]) . '">' . dformat($page[2]) . '</a>';
113
114            $class = 'plugin__approve_red';
115            $state = $this->getLang('draft');
116            $all += 1;
117
118            if ($page[1] === 'approved') {
119                $class = 'plugin__approve_green';
120                $state = $this->getLang('approved');
121                $all_approved += 1;
122            } elseif ($page[1] === 'ready for approval' && $this->getConf('ready_for_approval') === 1) {
123                $class = 'plugin__approve_ready';
124                $state = $this->getLang('marked_approve_ready');
125                $all_approved_ready += 1;
126            }
127
128            $renderer->doc .= '<tr class="'.$class.'">';
129            $renderer->doc .= '<td><a href="';
130            $renderer->doc .= wl($page[0]);
131            $renderer->doc .= '">';
132            if ($conf['useheading'] === '1') {
133                $heading = p_get_first_heading($page[0]);
134                if ($heading != '') {
135                    $renderer->doc .= $heading;
136                } else {
137                    $renderer->doc .= $page[0];
138                }
139
140            } else {
141                $renderer->doc .= $page[0];
142            }
143
144            $renderer->doc .= '</a></td><td>';
145            $renderer->doc .= '<strong>'.$state. '</strong> '. $this->getLang('by'). ' ' . $page[4];
146            $renderer->doc .= '</td><td>';
147            $renderer->doc .= $updated;
148            $renderer->doc .= '</td></tr>';
149        }
150
151        if ($this->getConf('ready_for_approval') === 1) {
152            $renderer->doc .= '<tr><td><strong>';
153            $renderer->doc .= $this->getLang('all_approved_ready');
154            $renderer->doc .= '</strong></td>';
155
156            $renderer->doc .= '<td colspan="2">';
157            $percent = 0;
158            if ($all > 0) {
159                $percent = $all_approved_ready*100/$all;
160            }
161            $renderer->doc .= $all_approved_ready.' / '.$all . sprintf(" (%.0f%%)", $percent);
162            $renderer->doc .= '</td></tr>';
163        }
164
165        $renderer->doc .= '<tr><td><strong>';
166        $renderer->doc .= $this->getLang('all_approved');
167        $renderer->doc .= '</strong></td>';
168
169        $renderer->doc .= '<td colspan="2">';
170        $percent = 0;
171        if ($all > 0) {
172            $percent = $all_approved*100/$all;
173        }
174        $renderer->doc .= $all_approved.' / '.$all . sprintf(" (%.0f%%)", $percent);
175        $renderer->doc .= '</td></tr>';
176
177
178
179        $renderer->doc .= '</table>';
180        return true;
181    }
182
183    function _search_helper(&$data, $base, $file, $type, $lvl, $opts) {
184        global $lang;
185
186        $ns = $opts[0];
187        $invalid_ns = $opts[1];
188        $page_regex = $opts[2];
189        $states = $opts[3];
190
191        if ($type == 'd') {
192            return true;
193        }
194
195        if (!preg_match('#\.txt$#', $file)) {
196            return false;
197        }
198
199        $id = pathID($ns . $file);
200        if (!empty($invalid_ns) && $this->hlp->in_namespace($invalid_ns, $id)) {
201            return false;
202        }
203
204        //check page_regex
205        if ($page_regex && !preg_match($page_regex, $id)) {
206            return false;
207        }
208
209        //check states
210
211
212        $meta = p_get_metadata($id);
213
214
215        //check states
216        $sum = $meta['last_change']['sum'];
217        if ($sum == ApproveConst::APPROVED && !in_array(ApproveConst::APPROVED, $states)) {
218            return false;
219        }
220
221        if ($sum == ApproveConst::READY_FOR_APPROVAL && !in_array(ApproveConst::READY_FOR_APPROVAL, $states)) {
222            return false;
223        }
224
225        if ($sum != ApproveConst::APPROVED && $sum != ApproveConst::READY_FOR_APPROVAL &&
226            !in_array(ApproveConst::DRAFT, $states)) {
227            return false;
228        }
229
230        $date = $meta['date']['modified'];
231        if (isset($meta['last_change']) && $meta['last_change']['sum'] === ApproveConst::APPROVED) {
232            $approved = 'approved';
233        } elseif (isset($meta['last_change']) && $meta['last_change']['sum'] === ApproveConst::READY_FOR_APPROVAL) {
234            $approved = 'ready for approval';
235        } else {
236            $approved = 'not approved';
237        }
238
239        if (isset($meta['last_change'])) {
240            $user = $meta['last_change']['user'];
241
242            if (isset($meta['contributor'][$user])) {
243                $full_name = $meta['contributor'][$user];
244            } else {
245                $full_name = $meta['creator'];
246            }
247        } else {
248            $user = '';
249            $full_name = '('.$lang['external_edit'].')';
250        }
251
252
253        $data[] = array($id, $approved, $date, $user, $full_name);
254
255        return false;
256    }
257
258    function _getPagesFromNamespace($namespace, $page_regex=false, $states=[]) {
259        global $conf;
260        $dir = $conf['datadir'] . '/' . str_replace(':', '/', $namespace);
261        $pages = array();
262        search($pages, $dir, array($this,'_search_helper'),
263               array($namespace, $this->getConf('no_apr_namespaces'), $page_regex, $states));
264
265        return $pages;
266    }
267
268
269
270    /**
271     * Custom sort callback
272     */
273    function _pagesorter($a, $b){
274        $ac = explode(':',$a[0]);
275        $bc = explode(':',$b[0]);
276        $an = count($ac);
277        $bn = count($bc);
278
279        // Same number of elements, can just string sort
280        if($an == $bn) { return strcmp($a[0], $b[0]); }
281
282        // For each level:
283        // If this is not the last element in either list:
284        //   same -> continue
285        //   otherwise strcmp
286        // If this is the last element in either list, it wins
287        $n = 0;
288        while(true) {
289            if($n + 1 == $an) { return -1; }
290            if($n + 1 == $bn) { return 1; }
291            $s = strcmp($ac[$n], $bc[$n]);
292            if($s != 0) { return $s; }
293            $n += 1;
294        }
295    }
296
297}
298