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 } elseif ($key == 'summarize') { 58 $value = $value == '0' ? false : true; 59 } 60 $params[$key] = $value; 61 } 62 return $params; 63 } 64 65 function render($mode, Doku_Renderer $renderer, $params) { 66 global $conf; 67 68 if ($mode != 'xhtml') return false; 69 if ($params === false) return false; 70 71 $defaults = [ 72 'namespace' => '', 73 'filter' => false, 74 'states' => self::STATES, 75 'summarize' => true, 76 ]; 77 78 $params = array_replace($defaults, $params); 79 80 $namespace = cleanID(getNS($params['namespace'] . ":*")); 81 82 $pages = $this->_getPagesFromNamespace($namespace, $params['filter'], $params['states']); 83 84 usort($pages, array($this,'_pagesorter')); 85 86 // Output Table 87 $renderer->doc .= '<table><tr>'; 88 $renderer->doc .= '<th>' . $this->getLang('hdr_page') . '</th>'; 89 $renderer->doc .= '<th>' . $this->getLang('hdr_state') . '</th>'; 90 $renderer->doc .= '<th>' . $this->getLang('hdr_updated') . '</th>'; 91 $renderer->doc .= '</tr>'; 92 93 94 $all_approved = 0; 95 $all_approved_ready = 0; 96 $all = 0; 97 98 $working_ns = null; 99 foreach($pages as $page) { 100 // $page: 0 -> pagename, 1 -> true -> approved else false, 2 -> last changed date 101 $this_ns = getNS($page[0]); 102 103 if($this_ns != $working_ns) { 104 $name_ns = $this_ns; 105 if($this_ns == '') { $name_ns = 'root'; } 106 $renderer->doc .= '<tr><td colspan="3"><a href="'; 107 $renderer->doc .= wl($this_ns . ':' . $this->getConf('start')); 108 $renderer->doc .= '">'; 109 $renderer->doc .= $name_ns; 110 $renderer->doc .= '</a> '; 111 $renderer->doc .= '</td></tr>'; 112 $working_ns = $this_ns; 113 } 114 115 $updated = '<a href="' . wl($page[0]) . '">' . dformat($page[2]) . '</a>'; 116 117 $class = 'plugin__approve_red'; 118 $state = $this->getLang('draft'); 119 $all += 1; 120 121 if ($page[1] === 'approved') { 122 $class = 'plugin__approve_green'; 123 $state = $this->getLang('approved'); 124 $all_approved += 1; 125 } elseif ($page[1] === 'ready for approval' && $this->getConf('ready_for_approval') === 1) { 126 $class = 'plugin__approve_ready'; 127 $state = $this->getLang('marked_approve_ready'); 128 $all_approved_ready += 1; 129 } 130 131 $renderer->doc .= '<tr class="'.$class.'">'; 132 $renderer->doc .= '<td><a href="'; 133 $renderer->doc .= wl($page[0]); 134 $renderer->doc .= '">'; 135 if ($conf['useheading'] === '1') { 136 $heading = p_get_first_heading($page[0]); 137 if ($heading != '') { 138 $renderer->doc .= $heading; 139 } else { 140 $renderer->doc .= $page[0]; 141 } 142 143 } else { 144 $renderer->doc .= $page[0]; 145 } 146 147 $renderer->doc .= '</a></td><td>'; 148 $renderer->doc .= '<strong>'.$state. '</strong> '. $this->getLang('by'). ' ' . $page[4]; 149 $renderer->doc .= '</td><td>'; 150 $renderer->doc .= $updated; 151 $renderer->doc .= '</td></tr>'; 152 } 153 154 if ($params['summarize']) { 155 if($this->getConf('ready_for_approval') === 1) { 156 $renderer->doc .= '<tr><td><strong>'; 157 $renderer->doc .= $this->getLang('all_approved_ready'); 158 $renderer->doc .= '</strong></td>'; 159 160 $renderer->doc .= '<td colspan="2">'; 161 $percent = 0; 162 if($all > 0) { 163 $percent = $all_approved_ready * 100 / $all; 164 } 165 $renderer->doc .= $all_approved_ready . ' / ' . $all . sprintf(" (%.0f%%)", $percent); 166 $renderer->doc .= '</td></tr>'; 167 } 168 169 $renderer->doc .= '<tr><td><strong>'; 170 $renderer->doc .= $this->getLang('all_approved'); 171 $renderer->doc .= '</strong></td>'; 172 173 $renderer->doc .= '<td colspan="2">'; 174 $percent = 0; 175 if($all > 0) { 176 $percent = $all_approved * 100 / $all; 177 } 178 $renderer->doc .= $all_approved . ' / ' . $all . sprintf(" (%.0f%%)", $percent); 179 $renderer->doc .= '</td></tr>'; 180 } 181 182 $renderer->doc .= '</table>'; 183 return true; 184 } 185 186 function _search_helper(&$data, $base, $file, $type, $lvl, $opts) { 187 global $lang; 188 189 $ns = $opts[0]; 190 $invalid_ns = $opts[1]; 191 $page_regex = $opts[2]; 192 $states = $opts[3]; 193 194 if ($type == 'd') { 195 return true; 196 } 197 198 if (!preg_match('#\.txt$#', $file)) { 199 return false; 200 } 201 202 $id = pathID($ns . $file); 203 if (!empty($invalid_ns) && $this->hlp->in_namespace($invalid_ns, $id)) { 204 return false; 205 } 206 207 //check page_regex 208 if ($page_regex && !preg_match($page_regex, $id)) { 209 return false; 210 } 211 212 //check states 213 214 215 $meta = p_get_metadata($id); 216 217 218 //check states 219 $sum = $meta['last_change']['sum']; 220 if ($sum == ApproveConst::APPROVED && !in_array(ApproveConst::APPROVED, $states)) { 221 return false; 222 } 223 224 if ($sum == ApproveConst::READY_FOR_APPROVAL && !in_array(ApproveConst::READY_FOR_APPROVAL, $states)) { 225 return false; 226 } 227 228 if ($sum != ApproveConst::APPROVED && $sum != ApproveConst::READY_FOR_APPROVAL && 229 !in_array(ApproveConst::DRAFT, $states)) { 230 return false; 231 } 232 233 $date = $meta['date']['modified']; 234 if (isset($meta['last_change']) && $meta['last_change']['sum'] === ApproveConst::APPROVED) { 235 $approved = 'approved'; 236 } elseif (isset($meta['last_change']) && $meta['last_change']['sum'] === ApproveConst::READY_FOR_APPROVAL) { 237 $approved = 'ready for approval'; 238 } else { 239 $approved = 'not approved'; 240 } 241 242 if (isset($meta['last_change'])) { 243 $user = $meta['last_change']['user']; 244 245 if (isset($meta['contributor'][$user])) { 246 $full_name = $meta['contributor'][$user]; 247 } else { 248 $full_name = $meta['creator']; 249 } 250 } else { 251 $user = ''; 252 $full_name = '('.$lang['external_edit'].')'; 253 } 254 255 256 $data[] = array($id, $approved, $date, $user, $full_name); 257 258 return false; 259 } 260 261 function _getPagesFromNamespace($namespace, $page_regex=false, $states=[]) { 262 global $conf; 263 $dir = $conf['datadir'] . '/' . str_replace(':', '/', $namespace); 264 $pages = array(); 265 search($pages, $dir, array($this,'_search_helper'), 266 array($namespace, $this->getConf('no_apr_namespaces'), $page_regex, $states)); 267 268 return $pages; 269 } 270 271 272 273 /** 274 * Custom sort callback 275 */ 276 function _pagesorter($a, $b){ 277 $ac = explode(':',$a[0]); 278 $bc = explode(':',$b[0]); 279 $an = count($ac); 280 $bn = count($bc); 281 282 // Same number of elements, can just string sort 283 if($an == $bn) { return strcmp($a[0], $b[0]); } 284 285 // For each level: 286 // If this is not the last element in either list: 287 // same -> continue 288 // otherwise strcmp 289 // If this is the last element in either list, it wins 290 $n = 0; 291 while(true) { 292 if($n + 1 == $an) { return -1; } 293 if($n + 1 == $bn) { return 1; } 294 $s = strcmp($ac[$n], $bc[$n]); 295 if($s != 0) { return $s; } 296 $n += 1; 297 } 298 } 299 300} 301