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