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