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 $lines_position = count($lines)-1; 231 $media_lines_position = 0; 232 $media_lines = array(); 233 234 if ($flags & RECENTS_MEDIA_PAGES_MIXED) { 235 $media_lines = @file($conf['media_changelog']); 236 $media_lines_position = count($media_lines)-1; 237 } 238 239 $seen = array(); // caches seen lines, _handleRecent() skips them 240 241 // handle lines 242 while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) { 243 if (empty($rec) && $lines_position >= 0) { 244 $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen); 245 if (!$rec) { 246 $lines_position --; 247 continue; 248 } 249 } 250 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) { 251 $media_rec = _handleRecent( 252 @$media_lines[$media_lines_position], 253 $ns, 254 $flags | RECENTS_MEDIA_CHANGES, 255 $seen 256 ); 257 if (!$media_rec) { 258 $media_lines_position --; 259 continue; 260 } 261 } 262 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) { 263 $media_lines_position--; 264 $x = $media_rec; 265 $x['media'] = true; 266 $media_rec = false; 267 } else { 268 $lines_position--; 269 $x = $rec; 270 if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true; 271 $rec = false; 272 } 273 if(--$first >= 0) continue; // skip first entries 274 $recent[] = $x; 275 $count++; 276 // break when we have enough entries 277 if($count >= $num){ break; } 278 } 279 return $recent; 280} 281 282/** 283 * returns an array of files changed since a given time using the 284 * changelog 285 * 286 * The following constants can be used to control which changes are 287 * included. Add them together as needed. 288 * 289 * RECENTS_SKIP_DELETED - don't include deleted pages 290 * RECENTS_SKIP_MINORS - don't include minor changes 291 * RECENTS_SKIP_SUBSPACES - don't include subspaces 292 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 293 * 294 * @param int $from date of the oldest entry to return 295 * @param int $to date of the newest entry to return (for pagination, optional) 296 * @param string $ns restrict to given namespace (optional) 297 * @param int $flags see above (optional) 298 * @return array of files 299 * 300 * @author Michael Hamann <michael@content-space.de> 301 * @author Ben Coburn <btcoburn@silicodon.net> 302 */ 303function getRecentsSince($from,$to=null,$ns='',$flags=0){ 304 global $conf; 305 $recent = array(); 306 307 if($to && $to < $from) 308 return $recent; 309 310 // read all recent changes. (kept short) 311 if ($flags & RECENTS_MEDIA_CHANGES) { 312 $lines = @file($conf['media_changelog']); 313 } else { 314 $lines = @file($conf['changelog']); 315 } 316 if(!$lines) return $recent; 317 318 // we start searching at the end of the list 319 $lines = array_reverse($lines); 320 321 // handle lines 322 $seen = array(); // caches seen lines, _handleRecent() skips them 323 324 foreach($lines as $line){ 325 $rec = _handleRecent($line, $ns, $flags, $seen); 326 if($rec !== false) { 327 if ($rec['date'] >= $from) { 328 if (!$to || $rec['date'] <= $to) { 329 $recent[] = $rec; 330 } 331 } else { 332 break; 333 } 334 } 335 } 336 337 return array_reverse($recent); 338} 339 340/** 341 * Internal function used by getRecents 342 * 343 * don't call directly 344 * 345 * @see getRecents() 346 * @author Andreas Gohr <andi@splitbrain.org> 347 * @author Ben Coburn <btcoburn@silicodon.net> 348 * 349 * @param string $line changelog line 350 * @param string $ns restrict to given namespace 351 * @param int $flags flags to control which changes are included 352 * @param array $seen listing of seen pages 353 * @return array|bool false or array with info about a change 354 */ 355function _handleRecent($line,$ns,$flags,&$seen){ 356 if(empty($line)) return false; //skip empty lines 357 358 // split the line into parts 359 $recent = parseChangelogLine($line); 360 if ($recent===false) { return false; } 361 362 // skip seen ones 363 if(isset($seen[$recent['id']])) return false; 364 365 // skip minors 366 if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false; 367 368 // remember in seen to skip additional sights 369 $seen[$recent['id']] = 1; 370 371 // check if it's a hidden page 372 if(isHiddenPage($recent['id'])) return false; 373 374 // filter namespace 375 if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false; 376 377 // exclude subnamespaces 378 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false; 379 380 // check ACL 381 if ($flags & RECENTS_MEDIA_CHANGES) { 382 $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*'); 383 } else { 384 $recent['perms'] = auth_quickaclcheck($recent['id']); 385 } 386 if ($recent['perms'] < AUTH_READ) return false; 387 388 // check existance 389 if($flags & RECENTS_SKIP_DELETED){ 390 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id'])); 391 if(!file_exists($fn)) return false; 392 } 393 394 return $recent; 395} 396