1<?php 2/** 3 * Changelog handling functions 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9// Constants for known core changelog line types. 10// Use these in place of string literals for more readable code. 11define('DOKU_CHANGE_TYPE_CREATE', 'C'); 12define('DOKU_CHANGE_TYPE_EDIT', 'E'); 13define('DOKU_CHANGE_TYPE_MINOR_EDIT', 'e'); 14define('DOKU_CHANGE_TYPE_DELETE', 'D'); 15define('DOKU_CHANGE_TYPE_REVERT', 'R'); 16 17/** 18 * parses a changelog line into it's components 19 * 20 * @author Ben Coburn <btcoburn@silicodon.net> 21 * 22 * @param string $line changelog line 23 * @return array|bool parsed line or false 24 */ 25function parseChangelogLine($line) { 26 $line = rtrim($line, "\n"); 27 $tmp = explode("\t", $line); 28 if ($tmp!==false && count($tmp)>1) { 29 $info = array(); 30 $info['date'] = (int)$tmp[0]; // unix timestamp 31 $info['ip'] = $tmp[1]; // IPv4 address (127.0.0.1) 32 $info['type'] = $tmp[2]; // log line type 33 $info['id'] = $tmp[3]; // page id 34 $info['user'] = $tmp[4]; // user name 35 $info['sum'] = $tmp[5]; // edit summary (or action reason) 36 $info['extra'] = $tmp[6]; // extra data (varies by line type) 37 if(isset($tmp[7]) && $tmp[7] !== '') { //last item has line-end|| 38 $info['sizechange'] = (int) $tmp[7]; 39 } else { 40 $info['sizechange'] = null; 41 } 42 return $info; 43 } else { 44 return false; 45 } 46} 47 48/** 49 * Add's an entry to the changelog and saves the metadata for the page 50 * 51 * @param int $date Timestamp of the change 52 * @param String $id Name of the affected page 53 * @param String $type Type of the change see DOKU_CHANGE_TYPE_* 54 * @param String $summary Summary of the change 55 * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page 56 * @param array $flags Additional flags in a key value array. 57 * Available flags: 58 * - ExternalEdit - mark as an external edit. 59 * @param null|int $sizechange Change of filesize 60 * 61 * @author Andreas Gohr <andi@splitbrain.org> 62 * @author Esther Brunner <wikidesign@gmail.com> 63 * @author Ben Coburn <btcoburn@silicodon.net> 64 */ 65function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null, $sizechange = null){ 66 global $conf, $INFO; 67 /** @var Input $INPUT */ 68 global $INPUT; 69 70 // check for special flags as keys 71 if (!is_array($flags)) { $flags = array(); } 72 $flagExternalEdit = isset($flags['ExternalEdit']); 73 74 $id = cleanid($id); 75 $file = wikiFN($id); 76 $created = @filectime($file); 77 $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT); 78 $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE); 79 80 if(!$date) $date = time(); //use current time if none supplied 81 $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1'; 82 $user = (!$flagExternalEdit)?$INPUT->server->str('REMOTE_USER'):''; 83 if($sizechange === null) { 84 $sizechange = ''; 85 } else { 86 $sizechange = (int) $sizechange; 87 } 88 89 $strip = array("\t", "\n"); 90 $logline = array( 91 'date' => $date, 92 'ip' => $remote, 93 'type' => str_replace($strip, '', $type), 94 'id' => $id, 95 'user' => $user, 96 'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255), 97 'extra' => str_replace($strip, '', $extra), 98 'sizechange' => $sizechange 99 ); 100 101 $wasCreated = ($type===DOKU_CHANGE_TYPE_CREATE); 102 $wasReverted = ($type===DOKU_CHANGE_TYPE_REVERT); 103 // update metadata 104 if (!$wasRemoved) { 105 $oldmeta = p_read_metadata($id); 106 $meta = array(); 107 if ( 108 $wasCreated && ( 109 empty($oldmeta['persistent']['date']['created']) || 110 $oldmeta['persistent']['date']['created'] === $created 111 ) 112 ){ 113 // newly created 114 $meta['date']['created'] = $created; 115 if ($user){ 116 $meta['creator'] = $INFO['userinfo']['name']; 117 $meta['user'] = $user; 118 } 119 } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['persistent']['date']['created'])) { 120 // re-created / restored 121 $meta['date']['created'] = $oldmeta['persistent']['date']['created']; 122 $meta['date']['modified'] = $created; // use the files ctime here 123 $meta['creator'] = $oldmeta['persistent']['creator']; 124 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 125 } elseif (!$minor) { // non-minor modification 126 $meta['date']['modified'] = $date; 127 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 128 } 129 $meta['last_change'] = $logline; 130 p_set_metadata($id, $meta); 131 } 132 133 // add changelog lines 134 $logline = implode("\t", $logline)."\n"; 135 io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog 136 io_saveFile($conf['changelog'],$logline,true); //global changelog cache 137} 138 139/** 140 * Add's an entry to the media changelog 141 * 142 * @author Michael Hamann <michael@content-space.de> 143 * @author Andreas Gohr <andi@splitbrain.org> 144 * @author Esther Brunner <wikidesign@gmail.com> 145 * @author Ben Coburn <btcoburn@silicodon.net> 146 * 147 * @param int $date Timestamp of the change 148 * @param String $id Name of the affected page 149 * @param String $type Type of the change see DOKU_CHANGE_TYPE_* 150 * @param String $summary Summary of the change 151 * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page 152 * @param array $flags Additional flags in a key value array. 153 * Available flags: 154 * - (none, so far) 155 * @param null|int $sizechange Change of filesize 156 */ 157function addMediaLogEntry( 158 $date, 159 $id, 160 $type=DOKU_CHANGE_TYPE_EDIT, 161 $summary='', 162 $extra='', 163 $flags=null, 164 $sizechange = null) 165{ 166 global $conf; 167 /** @var Input $INPUT */ 168 global $INPUT; 169 170 $id = cleanid($id); 171 172 if(!$date) $date = time(); //use current time if none supplied 173 $remote = clientIP(true); 174 $user = $INPUT->server->str('REMOTE_USER'); 175 if($sizechange === null) { 176 $sizechange = ''; 177 } else { 178 $sizechange = (int) $sizechange; 179 } 180 181 $strip = array("\t", "\n"); 182 $logline = array( 183 'date' => $date, 184 'ip' => $remote, 185 'type' => str_replace($strip, '', $type), 186 'id' => $id, 187 'user' => $user, 188 'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255), 189 'extra' => str_replace($strip, '', $extra), 190 'sizechange' => $sizechange 191 ); 192 193 // add changelog lines 194 $logline = implode("\t", $logline)."\n"; 195 io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache 196 io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog 197} 198 199/** 200 * returns an array of recently changed files using the 201 * changelog 202 * 203 * The following constants can be used to control which changes are 204 * included. Add them together as needed. 205 * 206 * RECENTS_SKIP_DELETED - don't include deleted pages 207 * RECENTS_SKIP_MINORS - don't include minor changes 208 * RECENTS_SKIP_SUBSPACES - don't include subspaces 209 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 210 * RECENTS_MEDIA_PAGES_MIXED - return both media changes and page changes 211 * 212 * @param int $first number of first entry returned (for paginating 213 * @param int $num return $num entries 214 * @param string $ns restrict to given namespace 215 * @param int $flags see above 216 * @return array recently changed files 217 * 218 * @author Ben Coburn <btcoburn@silicodon.net> 219 * @author Kate Arzamastseva <pshns@ukr.net> 220 */ 221function getRecents($first,$num,$ns='',$flags=0){ 222 global $conf; 223 $recent = array(); 224 $count = 0; 225 226 if(!$num) 227 return $recent; 228 229 // read all recent changes. (kept short) 230 if ($flags & RECENTS_MEDIA_CHANGES) { 231 $lines = @file($conf['media_changelog']) ?: []; 232 } else { 233 $lines = @file($conf['changelog']) ?: []; 234 } 235 if (!is_array($lines)) { 236 $lines = array(); 237 } 238 $lines_position = count($lines)-1; 239 $media_lines_position = 0; 240 $media_lines = array(); 241 242 if ($flags & RECENTS_MEDIA_PAGES_MIXED) { 243 $media_lines = @file($conf['media_changelog']) ?: []; 244 if (!is_array($media_lines)) { 245 $media_lines = array(); 246 } 247 $media_lines_position = count($media_lines)-1; 248 } 249 250 $seen = array(); // caches seen lines, _handleRecent() skips them 251 252 // handle lines 253 while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) { 254 if (empty($rec) && $lines_position >= 0) { 255 $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen); 256 if (!$rec) { 257 $lines_position --; 258 continue; 259 } 260 } 261 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) { 262 $media_rec = _handleRecent( 263 @$media_lines[$media_lines_position], 264 $ns, 265 $flags | RECENTS_MEDIA_CHANGES, 266 $seen 267 ); 268 if (!$media_rec) { 269 $media_lines_position --; 270 continue; 271 } 272 } 273 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) { 274 $media_lines_position--; 275 $x = $media_rec; 276 $x['media'] = true; 277 $media_rec = false; 278 } else { 279 $lines_position--; 280 $x = $rec; 281 if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true; 282 $rec = false; 283 } 284 if(--$first >= 0) continue; // skip first entries 285 $recent[] = $x; 286 $count++; 287 // break when we have enough entries 288 if($count >= $num){ break; } 289 } 290 return $recent; 291} 292 293/** 294 * returns an array of files changed since a given time using the 295 * changelog 296 * 297 * The following constants can be used to control which changes are 298 * included. Add them together as needed. 299 * 300 * RECENTS_SKIP_DELETED - don't include deleted pages 301 * RECENTS_SKIP_MINORS - don't include minor changes 302 * RECENTS_SKIP_SUBSPACES - don't include subspaces 303 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 304 * 305 * @param int $from date of the oldest entry to return 306 * @param int $to date of the newest entry to return (for pagination, optional) 307 * @param string $ns restrict to given namespace (optional) 308 * @param int $flags see above (optional) 309 * @return array of files 310 * 311 * @author Michael Hamann <michael@content-space.de> 312 * @author Ben Coburn <btcoburn@silicodon.net> 313 */ 314function getRecentsSince($from,$to=null,$ns='',$flags=0){ 315 global $conf; 316 $recent = array(); 317 318 if($to && $to < $from) 319 return $recent; 320 321 // read all recent changes. (kept short) 322 if ($flags & RECENTS_MEDIA_CHANGES) { 323 $lines = @file($conf['media_changelog']); 324 } else { 325 $lines = @file($conf['changelog']); 326 } 327 if(!$lines) return $recent; 328 329 // we start searching at the end of the list 330 $lines = array_reverse($lines); 331 332 // handle lines 333 $seen = array(); // caches seen lines, _handleRecent() skips them 334 335 foreach($lines as $line){ 336 $rec = _handleRecent($line, $ns, $flags, $seen); 337 if($rec !== false) { 338 if ($rec['date'] >= $from) { 339 if (!$to || $rec['date'] <= $to) { 340 $recent[] = $rec; 341 } 342 } else { 343 break; 344 } 345 } 346 } 347 348 return array_reverse($recent); 349} 350 351/** 352 * Internal function used by getRecents 353 * 354 * don't call directly 355 * 356 * @see getRecents() 357 * @author Andreas Gohr <andi@splitbrain.org> 358 * @author Ben Coburn <btcoburn@silicodon.net> 359 * 360 * @param string $line changelog line 361 * @param string $ns restrict to given namespace 362 * @param int $flags flags to control which changes are included 363 * @param array $seen listing of seen pages 364 * @return array|bool false or array with info about a change 365 */ 366function _handleRecent($line,$ns,$flags,&$seen){ 367 if(empty($line)) return false; //skip empty lines 368 369 // split the line into parts 370 $recent = parseChangelogLine($line); 371 if ($recent===false) { return false; } 372 373 // skip seen ones 374 if(isset($seen[$recent['id']])) return false; 375 376 // skip minors 377 if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false; 378 379 // remember in seen to skip additional sights 380 $seen[$recent['id']] = 1; 381 382 // check if it's a hidden page 383 if(isHiddenPage($recent['id'])) return false; 384 385 // filter namespace 386 if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false; 387 388 // exclude subnamespaces 389 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false; 390 391 // check ACL 392 if ($flags & RECENTS_MEDIA_CHANGES) { 393 $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*'); 394 } else { 395 $recent['perms'] = auth_quickaclcheck($recent['id']); 396 } 397 if ($recent['perms'] < AUTH_READ) return false; 398 399 // check existance 400 if($flags & RECENTS_SKIP_DELETED){ 401 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id'])); 402 if(!file_exists($fn)) return false; 403 } 404 405 return $recent; 406} 407