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