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