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' => utf8_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 ($wasCreated && empty($oldmeta['persistent']['date']['created'])){ 108 // newly created 109 $meta['date']['created'] = $created; 110 if ($user){ 111 $meta['creator'] = $INFO['userinfo']['name']; 112 $meta['user'] = $user; 113 } 114 } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['persistent']['date']['created'])) { 115 // re-created / restored 116 $meta['date']['created'] = $oldmeta['persistent']['date']['created']; 117 $meta['date']['modified'] = $created; // use the files ctime here 118 $meta['creator'] = $oldmeta['persistent']['creator']; 119 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 120 } elseif (!$minor) { // non-minor modification 121 $meta['date']['modified'] = $date; 122 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 123 } 124 $meta['last_change'] = $logline; 125 p_set_metadata($id, $meta); 126 } 127 128 // add changelog lines 129 $logline = implode("\t", $logline)."\n"; 130 io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog 131 io_saveFile($conf['changelog'],$logline,true); //global changelog cache 132} 133 134/** 135 * Add's an entry to the media changelog 136 * 137 * @author Michael Hamann <michael@content-space.de> 138 * @author Andreas Gohr <andi@splitbrain.org> 139 * @author Esther Brunner <wikidesign@gmail.com> 140 * @author Ben Coburn <btcoburn@silicodon.net> 141 * 142 * @param int $date Timestamp of the change 143 * @param String $id Name of the affected page 144 * @param String $type Type of the change see DOKU_CHANGE_TYPE_* 145 * @param String $summary Summary of the change 146 * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page 147 * @param array $flags Additional flags in a key value array. 148 * Available flags: 149 * - (none, so far) 150 * @param null|int $sizechange Change of filesize 151 */ 152function addMediaLogEntry( 153 $date, 154 $id, 155 $type=DOKU_CHANGE_TYPE_EDIT, 156 $summary='', 157 $extra='', 158 $flags=null, 159 $sizechange = null) 160{ 161 global $conf; 162 /** @var Input $INPUT */ 163 global $INPUT; 164 165 $id = cleanid($id); 166 167 if(!$date) $date = time(); //use current time if none supplied 168 $remote = clientIP(true); 169 $user = $INPUT->server->str('REMOTE_USER'); 170 if($sizechange === null) { 171 $sizechange = ''; 172 } else { 173 $sizechange = (int) $sizechange; 174 } 175 176 $strip = array("\t", "\n"); 177 $logline = array( 178 'date' => $date, 179 'ip' => $remote, 180 'type' => str_replace($strip, '', $type), 181 'id' => $id, 182 'user' => $user, 183 'sum' => utf8_substr(str_replace($strip, '', $summary), 0, 255), 184 'extra' => str_replace($strip, '', $extra), 185 'sizechange' => $sizechange 186 ); 187 188 // add changelog lines 189 $logline = implode("\t", $logline)."\n"; 190 io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache 191 io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog 192} 193 194/** 195 * returns an array of recently changed files using the 196 * changelog 197 * 198 * The following constants can be used to control which changes are 199 * included. Add them together as needed. 200 * 201 * RECENTS_SKIP_DELETED - don't include deleted pages 202 * RECENTS_SKIP_MINORS - don't include minor changes 203 * RECENTS_SKIP_SUBSPACES - don't include subspaces 204 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 205 * RECENTS_MEDIA_PAGES_MIXED - return both media changes and page changes 206 * 207 * @param int $first number of first entry returned (for paginating 208 * @param int $num return $num entries 209 * @param string $ns restrict to given namespace 210 * @param int $flags see above 211 * @return array recently changed files 212 * 213 * @author Ben Coburn <btcoburn@silicodon.net> 214 * @author Kate Arzamastseva <pshns@ukr.net> 215 */ 216function getRecents($first,$num,$ns='',$flags=0){ 217 global $conf; 218 $recent = array(); 219 $count = 0; 220 221 if(!$num) 222 return $recent; 223 224 // read all recent changes. (kept short) 225 if ($flags & RECENTS_MEDIA_CHANGES) { 226 $lines = @file($conf['media_changelog']); 227 } else { 228 $lines = @file($conf['changelog']); 229 } 230 if (!is_array($lines)) { 231 $lines = array(); 232 } 233 $lines_position = count($lines)-1; 234 $media_lines_position = 0; 235 $media_lines = array(); 236 237 if ($flags & RECENTS_MEDIA_PAGES_MIXED) { 238 $media_lines = @file($conf['media_changelog']); 239 if (!is_array($media_lines)) { 240 $media_lines = array(); 241 } 242 $media_lines_position = count($media_lines)-1; 243 } 244 245 $seen = array(); // caches seen lines, _handleRecent() skips them 246 247 // handle lines 248 while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) { 249 if (empty($rec) && $lines_position >= 0) { 250 $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen); 251 if (!$rec) { 252 $lines_position --; 253 continue; 254 } 255 } 256 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) { 257 $media_rec = _handleRecent( 258 @$media_lines[$media_lines_position], 259 $ns, 260 $flags | RECENTS_MEDIA_CHANGES, 261 $seen 262 ); 263 if (!$media_rec) { 264 $media_lines_position --; 265 continue; 266 } 267 } 268 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) { 269 $media_lines_position--; 270 $x = $media_rec; 271 $x['media'] = true; 272 $media_rec = false; 273 } else { 274 $lines_position--; 275 $x = $rec; 276 if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true; 277 $rec = false; 278 } 279 if(--$first >= 0) continue; // skip first entries 280 $recent[] = $x; 281 $count++; 282 // break when we have enough entries 283 if($count >= $num){ break; } 284 } 285 return $recent; 286} 287 288/** 289 * returns an array of files changed since a given time using the 290 * changelog 291 * 292 * The following constants can be used to control which changes are 293 * included. Add them together as needed. 294 * 295 * RECENTS_SKIP_DELETED - don't include deleted pages 296 * RECENTS_SKIP_MINORS - don't include minor changes 297 * RECENTS_SKIP_SUBSPACES - don't include subspaces 298 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 299 * 300 * @param int $from date of the oldest entry to return 301 * @param int $to date of the newest entry to return (for pagination, optional) 302 * @param string $ns restrict to given namespace (optional) 303 * @param int $flags see above (optional) 304 * @return array of files 305 * 306 * @author Michael Hamann <michael@content-space.de> 307 * @author Ben Coburn <btcoburn@silicodon.net> 308 */ 309function getRecentsSince($from,$to=null,$ns='',$flags=0){ 310 global $conf; 311 $recent = array(); 312 313 if($to && $to < $from) 314 return $recent; 315 316 // read all recent changes. (kept short) 317 if ($flags & RECENTS_MEDIA_CHANGES) { 318 $lines = @file($conf['media_changelog']); 319 } else { 320 $lines = @file($conf['changelog']); 321 } 322 if(!$lines) return $recent; 323 324 // we start searching at the end of the list 325 $lines = array_reverse($lines); 326 327 // handle lines 328 $seen = array(); // caches seen lines, _handleRecent() skips them 329 330 foreach($lines as $line){ 331 $rec = _handleRecent($line, $ns, $flags, $seen); 332 if($rec !== false) { 333 if ($rec['date'] >= $from) { 334 if (!$to || $rec['date'] <= $to) { 335 $recent[] = $rec; 336 } 337 } else { 338 break; 339 } 340 } 341 } 342 343 return array_reverse($recent); 344} 345 346/** 347 * Internal function used by getRecents 348 * 349 * don't call directly 350 * 351 * @see getRecents() 352 * @author Andreas Gohr <andi@splitbrain.org> 353 * @author Ben Coburn <btcoburn@silicodon.net> 354 * 355 * @param string $line changelog line 356 * @param string $ns restrict to given namespace 357 * @param int $flags flags to control which changes are included 358 * @param array $seen listing of seen pages 359 * @return array|bool false or array with info about a change 360 */ 361function _handleRecent($line,$ns,$flags,&$seen){ 362 if(empty($line)) return false; //skip empty lines 363 364 // split the line into parts 365 $recent = parseChangelogLine($line); 366 if ($recent===false) { return false; } 367 368 // skip seen ones 369 if(isset($seen[$recent['id']])) return false; 370 371 // skip minors 372 if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false; 373 374 // remember in seen to skip additional sights 375 $seen[$recent['id']] = 1; 376 377 // check if it's a hidden page 378 if(isHiddenPage($recent['id'])) return false; 379 380 // filter namespace 381 if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false; 382 383 // exclude subnamespaces 384 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false; 385 386 // check ACL 387 if ($flags & RECENTS_MEDIA_CHANGES) { 388 $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*'); 389 } else { 390 $recent['perms'] = auth_quickaclcheck($recent['id']); 391 } 392 if ($recent['perms'] < AUTH_READ) return false; 393 394 // check existance 395 if($flags & RECENTS_SKIP_DELETED){ 396 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id'])); 397 if(!file_exists($fn)) return false; 398 } 399 400 return $recent; 401} 402