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/** 10 * parses a changelog line into it's components 11 * 12 * @author Ben Coburn <btcoburn@silicodon.net> 13 * 14 * @param string $line changelog line 15 * @return array|bool parsed line or false 16 */ 17function parseChangelogLine($line) { 18 $line = rtrim($line, "\n"); 19 $tmp = explode("\t", $line); 20 if ($tmp !== false && count($tmp) > 1) { 21 $info = array(); 22 $info['date'] = (int)$tmp[0]; // unix timestamp 23 $info['ip'] = $tmp[1]; // IPv4 address (127.0.0.1) 24 $info['type'] = $tmp[2]; // log line type 25 $info['id'] = $tmp[3]; // page id 26 $info['user'] = $tmp[4]; // user name 27 $info['sum'] = $tmp[5]; // edit summary (or action reason) 28 $info['extra'] = $tmp[6]; // extra data (varies by line type) 29 if (isset($tmp[7]) && $tmp[7] !== '') { //last item has line-end|| 30 $info['sizechange'] = (int) $tmp[7]; 31 } else { 32 $info['sizechange'] = null; 33 } 34 return $info; 35 } else { 36 return false; 37 } 38} 39 40/** 41 * Adds an entry to the changelog and saves the metadata for the page 42 * 43 * Note: timestamp of the change might not be unique especially after very quick 44 * repeated edits (e.g. change checkbox via do plugin) 45 * 46 * @param int $date Timestamp of the change 47 * @param String $id Name of the affected page 48 * @param String $type Type of the change see DOKU_CHANGE_TYPE_* 49 * @param String $summary Summary of the change 50 * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page 51 * @param array $flags Additional flags in a key value array. 52 * Available flags: 53 * - ExternalEdit - mark as an external edit. 54 * @param null|int $sizechange Change of filesize 55 * 56 * @author Andreas Gohr <andi@splitbrain.org> 57 * @author Esther Brunner <wikidesign@gmail.com> 58 * @author Ben Coburn <btcoburn@silicodon.net> 59 */ 60function addLogEntry( 61 $date, 62 $id, 63 $type = DOKU_CHANGE_TYPE_EDIT, 64 $summary = '', 65 $extra = '', 66 $flags = null, 67 $sizechange = null) 68{ 69 global $conf, $INFO; 70 /** @var Input $INPUT */ 71 global $INPUT; 72 73 // check for special flags as keys 74 if (!is_array($flags)) $flags = array(); 75 $flagExternalEdit = isset($flags['ExternalEdit']); 76 77 $id = cleanid($id); 78 $file = wikiFN($id); 79 $created = @filectime($file); 80 $minor = ($type === DOKU_CHANGE_TYPE_MINOR_EDIT); 81 $wasRemoved = ($type === DOKU_CHANGE_TYPE_DELETE); 82 $wasCreated = ($type === DOKU_CHANGE_TYPE_CREATE); 83 $wasReverted = ($type === DOKU_CHANGE_TYPE_REVERT); 84 85 if (!$date) $date = time(); //use current time if none supplied 86 $remote = (!$flagExternalEdit) ? clientIP(true) : '127.0.0.1'; 87 $user = (!$flagExternalEdit) ? $INPUT->server->str('REMOTE_USER') : ''; 88 if ($sizechange === null) { 89 $sizechange = ''; 90 } else { 91 $sizechange = (int) $sizechange; 92 } 93 94 // update changelog file, the logline is also to be stored in metadata 95 $revInfo = array( 96 'date' => $date, 97 'ip' => $remote, 98 'type' => $type, 99 'id' => $id, 100 'user' => $user, 101 'sum' => $summary, 102 'extra' => $extra, 103 'sizechange' => $sizechange, 104 ); 105 $logline = (new PageChangeLog($id, 1024))->addLogEntry($revInfo); 106 107 // update metadata 108 if (!$wasRemoved) { 109 $oldmeta = p_read_metadata($id)['persistent']; 110 $meta = array(); 111 if ( 112 $wasCreated && ( 113 empty($oldmeta['date']['created']) || 114 $oldmeta['date']['created'] === $created 115 ) 116 ) { 117 // newly created 118 $meta['date']['created'] = $created; 119 if ($user) { 120 $meta['creator'] = isset($INFO) ? $INFO['userinfo']['name'] : null; 121 $meta['user'] = $user; 122 } 123 } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['date']['created'])) { 124 // re-created / restored 125 $meta['date']['created'] = $oldmeta['date']['created']; 126 $meta['date']['modified'] = $created; // use the files ctime here 127 $meta['creator'] = isset($oldmeta['creator']) ? $oldmeta['creator'] : null; 128 if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null; 129 } elseif (!$minor) { // non-minor modification 130 $meta['date']['modified'] = $date; 131 if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null; 132 } 133 $meta['last_change'] = $logline; 134 p_set_metadata($id, $meta); 135 } 136} 137 138/** 139 * Add's an entry to the media changelog 140 * 141 * @author Michael Hamann <michael@content-space.de> 142 * @author Andreas Gohr <andi@splitbrain.org> 143 * @author Esther Brunner <wikidesign@gmail.com> 144 * @author Ben Coburn <btcoburn@silicodon.net> 145 * 146 * @param int $date Timestamp of the change 147 * @param String $id Name of the affected page 148 * @param String $type Type of the change see DOKU_CHANGE_TYPE_* 149 * @param String $summary Summary of the change 150 * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page 151 * @param array $flags Additional flags in a key value array. 152 * Available flags: 153 * - (none, so far) 154 * @param null|int $sizechange Change of filesize 155 */ 156function addMediaLogEntry( 157 $date, 158 $id, 159 $type = DOKU_CHANGE_TYPE_EDIT, 160 $summary = '', 161 $extra = '', 162 $flags = null, 163 $sizechange = null) 164{ 165 global $conf; 166 /** @var Input $INPUT */ 167 global $INPUT; 168 169 // check for special flags as keys 170 if (!is_array($flags)) $flags = array(); 171 $flagExternalEdit = isset($flags['ExternalEdit']); 172 173 $id = cleanid($id); 174 175 if (!$date) $date = time(); //use current time if none supplied 176 $remote = (!$flagExternalEdit) ? clientIP(true) : '127.0.0.1'; 177 $user = (!$flagExternalEdit) ? $INPUT->server->str('REMOTE_USER') : ''; 178 if ($sizechange === null) { 179 $sizechange = ''; 180 } else { 181 $sizechange = (int) $sizechange; 182 } 183 184 // update changelog file 185 $revInfo = array( 186 'date' => $date, 187 'ip' => $remote, 188 'type' => $type, 189 'id' => $id, 190 'user' => $user, 191 'sum' => $summary, 192 'extra' => $extra, 193 'sizechange' => $sizechange, 194 ); 195 $logline = (new MediaChangeLog($id, 1024))->addLogEntry($revInfo); 196} 197 198/** 199 * returns an array of recently changed files using the changelog 200 * 201 * The following constants can be used to control which changes are 202 * included. Add them together as needed. 203 * 204 * RECENTS_SKIP_DELETED - don't include deleted pages 205 * RECENTS_SKIP_MINORS - don't include minor changes 206 * RECENTS_ONLY_CREATION - only include new created pages and media 207 * RECENTS_SKIP_SUBSPACES - don't include subspaces 208 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 209 * RECENTS_MEDIA_PAGES_MIXED - return both media changes and page changes 210 * 211 * @param int $first number of first entry returned (for paginating 212 * @param int $num return $num entries 213 * @param string $ns restrict to given namespace 214 * @param int $flags see above 215 * @return array recently changed files 216 * 217 * @author Ben Coburn <btcoburn@silicodon.net> 218 * @author Kate Arzamastseva <pshns@ukr.net> 219 */ 220function getRecents($first, $num, $ns = '', $flags = 0) { 221 global $conf; 222 $recent = array(); 223 $count = 0; 224 225 if (!$num) 226 return $recent; 227 228 // read all recent changes. (kept short) 229 if ($flags & RECENTS_MEDIA_CHANGES) { 230 $lines = @file($conf['media_changelog']) ?: []; 231 } else { 232 $lines = @file($conf['changelog']) ?: []; 233 } 234 if (!is_array($lines)) { 235 $lines = array(); 236 } 237 $lines_position = count($lines) - 1; 238 $media_lines_position = 0; 239 $media_lines = array(); 240 241 if ($flags & RECENTS_MEDIA_PAGES_MIXED) { 242 $media_lines = @file($conf['media_changelog']) ?: []; 243 if (!is_array($media_lines)) { 244 $media_lines = array(); 245 } 246 $media_lines_position = count($media_lines) - 1; 247 } 248 249 $seen = array(); // caches seen lines, _handleRecent() skips them 250 251 // handle lines 252 while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >= 0)) { 253 if (empty($rec) && $lines_position >= 0) { 254 $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen); 255 if (!$rec) { 256 $lines_position --; 257 continue; 258 } 259 } 260 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) { 261 $media_rec = _handleRecent( 262 @$media_lines[$media_lines_position], 263 $ns, 264 $flags | RECENTS_MEDIA_CHANGES, 265 $seen 266 ); 267 if (!$media_rec) { 268 $media_lines_position --; 269 continue; 270 } 271 } 272 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) { 273 $media_lines_position--; 274 $x = $media_rec; 275 $x['media'] = true; 276 $media_rec = false; 277 } else { 278 $lines_position--; 279 $x = $rec; 280 if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true; 281 $rec = false; 282 } 283 if (--$first >= 0) continue; // skip first entries 284 $recent[] = $x; 285 $count++; 286 // break when we have enough entries 287 if ($count >= $num) { break; } 288 } 289 return $recent; 290} 291 292/** 293 * returns an array of files changed since a given time using the 294 * changelog 295 * 296 * The following constants can be used to control which changes are 297 * included. Add them together as needed. 298 * 299 * RECENTS_SKIP_DELETED - don't include deleted pages 300 * RECENTS_SKIP_MINORS - don't include minor changes 301 * RECENTS_ONLY_CREATION - only include new created pages and media 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 changes, of only new items are requested 377 if ($recent['type'] !== DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false; 378 379 // skip minors 380 if ($recent['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false; 381 382 // remember in seen to skip additional sights 383 $seen[$recent['id']] = 1; 384 385 // check if it's a hidden page 386 if (isHiddenPage($recent['id'])) return false; 387 388 // filter namespace 389 if (($ns) && (strpos($recent['id'], $ns.':') !== 0)) return false; 390 391 // exclude subnamespaces 392 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false; 393 394 // check ACL 395 if ($flags & RECENTS_MEDIA_CHANGES) { 396 $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*'); 397 } else { 398 $recent['perms'] = auth_quickaclcheck($recent['id']); 399 } 400 if ($recent['perms'] < AUTH_READ) return false; 401 402 // check existance 403 if ($flags & RECENTS_SKIP_DELETED) { 404 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id'])); 405 if (!file_exists($fn)) return false; 406 } 407 408 return $recent; 409} 410