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