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