function handle_approve(&$event, $param) { global $ID, $REV, $INFO; if ($event->data == 'show' && isset($_GET['approve'])) { if (!$this->can_approve()) { return; } //change last commit comment to Approved $meta = p_read_metadata($ID); $meta[current][last_change][sum] = $meta[persistent][last_change][sum] = APPROVED; $meta[current][last_change][user] = $meta[persistent][last_change][user] = $INFO[client]; if (!array_key_exists($INFO[client], $meta[current][contributor])) { $meta[current][contributor][$INFO[client]] = $INFO[userinfo][name]; $meta[persistent][contributor][$INFO[client]] = $INFO[userinfo][name]; } p_save_metadata($ID, $meta); //update changelog //remove last line from file $changelog_file = metaFN($ID, '.changes'); $changes = file($changelog_file, FILE_SKIP_EMPTY_LINES); $lastLogLine = array_pop($changes); $info = parseChangelogLine($lastLogLine); $info[user] = $INFO[client]; $info[sum] = APPROVED; $logline = implode("\t", $info) . "\n"; array_push($changes, $logline); io_saveFile($changelog_file, implode('', $changes)); header('Location: ?id=' . $ID); } }
/** * Trims the recent changes cache (or imports the old changelog) as needed. * * @param media_changes If the media changelog shall be trimmed instead of * the page changelog * * @author Ben Coburn <*****@*****.**> */ function runTrimRecentChanges($media_changes = false) { global $conf; $fn = $media_changes ? $conf['media_changelog'] : $conf['changelog']; // Trim the Recent Changes // Trims the recent changes cache to the last $conf['changes_days'] recent // changes or $conf['recent'] items, which ever is larger. // The trimming is only done once a day. if (@file_exists($fn) && filectime($fn) + 86400 < time() && !@file_exists($fn . '_tmp')) { io_lock($fn); $lines = file($fn); if (count($lines) <= $conf['recent']) { // nothing to trim io_unlock($fn); return false; } io_saveFile($fn . '_tmp', ''); // presave tmp as 2nd lock $trim_time = time() - $conf['recent_days'] * 86400; $out_lines = array(); for ($i = 0; $i < count($lines); $i++) { $log = parseChangelogLine($lines[$i]); if ($log === false) { continue; } // discard junk if ($log['date'] < $trim_time) { $old_lines[$log['date'] . ".{$i}"] = $lines[$i]; // keep old lines for now (append .$i to prevent key collisions) } else { $out_lines[$log['date'] . ".{$i}"] = $lines[$i]; // definitely keep these lines } } // sort the final result, it shouldn't be necessary, // however the extra robustness in making the changelog cache self-correcting is worth it ksort($out_lines); $extra = $conf['recent'] - count($out_lines); // do we need extra lines do bring us up to minimum if ($extra > 0) { ksort($old_lines); $out_lines = array_merge(array_slice($old_lines, -$extra), $out_lines); } // save trimmed changelog io_saveFile($fn . '_tmp', implode('', $out_lines)); @unlink($fn); if (!rename($fn . '_tmp', $fn)) { // rename failed so try another way... io_unlock($fn); io_saveFile($fn, implode('', $out_lines)); @unlink($fn . '_tmp'); } else { io_unlock($fn); } return true; } // nothing done return false; }
function allRevisions($id) { $ret = array(); $lines = @file(metaFN($id, '.changes')); if (!$lines) { return $ret; } foreach ($lines as $line) { $tmp = parseChangelogLine($line); $ret[] = $tmp['date']; } return $ret; }
function _getChanges($user) { global $conf; function globr($dir, $pattern) { $files = glob($dir . '/' . $pattern); foreach (glob($dir . '/*', GLOB_ONLYDIR) as $subdir) { $subfiles = globr($subdir, $pattern); $files = array_merge($files, $subfiles); } return $files; } $changes = array(); $alllist = globr($conf['metadir'], '*.changes'); $skip = array('_comments.changes', '_dokuwiki.changes'); for ($i = 0; $i < count($alllist); $i++) { $fullname = $alllist[$i]; $filepart = basename($fullname); if (in_array($filepart, $skip)) { continue; } $f = file($fullname); for ($j = 0; $j < count($f); $j++) { $line = $f[$j]; $change = parseChangelogLine($line); if ($change['user'] == $user) { $changes[] = $change; } } /* for all lines */ } /* for all files */ function cmp($a, $b) { $time1 = $a['date']; $time2 = $b['date']; if ($time1 == $time2) { return 0; } return $time1 < $time2 ? 1 : -1; } uasort($changes, 'cmp'); return $changes; }
/** * Return a list of page revisions numbers * Does not guarantee that the revision exists in the attic, * only that a line with the date exists in the changelog. * By default the current revision is skipped. * * id: the page of interest * first: skip the first n changelog lines * num: number of revisions to return * * The current revision is automatically skipped when the page exists. * See $INFO['meta']['last_change'] for the current revision. * * For efficiency, the log lines are parsed and cached for later * calls to getRevisionInfo. Large changelog files are read * backwards in chunks until the requested number of changelog * lines are recieved. * * @author Ben Coburn <*****@*****.**> * @author Kate Arzamastseva <*****@*****.**> */ function getRevisions($id, $first, $num, $chunk_size = 8192, $media = false) { global $cache_revinfo; $cache =& $cache_revinfo; if (!isset($cache[$id])) { $cache[$id] = array(); } $revs = array(); $lines = array(); $count = 0; if ($media) { $file = mediaMetaFN($id, '.changes'); } else { $file = metaFN($id, '.changes'); } $num = max($num, 0); $chunk_size = max($chunk_size, 0); if ($first < 0) { $first = 0; } else { if (!$media && @file_exists(wikiFN($id)) || $media && @file_exists(mediaFN($id))) { // skip current revision if the page exists $first = max($first + 1, 0); } } if (!@file_exists($file)) { return $revs; } if (filesize($file) < $chunk_size || $chunk_size == 0) { // read whole file $lines = file($file); if ($lines === false) { return $revs; } } else { // read chunks backwards $fp = fopen($file, 'rb'); // "file pointer" if ($fp === false) { return $revs; } fseek($fp, 0, SEEK_END); $tail = ftell($fp); // chunk backwards $finger = max($tail - $chunk_size, 0); while ($count < $num + $first) { fseek($fp, $finger); $nl = $finger; if ($finger > 0) { fgets($fp); // slip the finger forward to a new line $nl = ftell($fp); } // was the chunk big enough? if not, take another bite if ($nl > 0 && $tail <= $nl) { $finger = max($finger - $chunk_size, 0); continue; } else { $finger = $nl; } // read chunk $chunk = ''; $read_size = max($tail - $finger, 0); // found chunk size $got = 0; while ($got < $read_size && !feof($fp)) { $tmp = @fread($fp, max($read_size - $got, 0)); if ($tmp === false) { break; } //error state $got += strlen($tmp); $chunk .= $tmp; } $tmp = explode("\n", $chunk); array_pop($tmp); // remove trailing newline // combine with previous chunk $count += count($tmp); $lines = array_merge($tmp, $lines); // next chunk if ($finger == 0) { break; } else { $tail = $finger; $finger = max($tail - $chunk_size, 0); } } fclose($fp); } // skip parsing extra lines $num = max(min(count($lines) - $first, $num), 0); if ($first > 0 && $num > 0) { $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num); } else { if ($first > 0 && $num == 0) { $lines = array_slice($lines, 0, max(count($lines) - $first, 0)); } else { if ($first == 0 && $num > 0) { $lines = array_slice($lines, max(count($lines) - $num, 0)); } } } // handle lines in reverse order for ($i = count($lines) - 1; $i >= 0; $i--) { $tmp = parseChangelogLine($lines[$i]); if ($tmp !== false) { $cache[$id][$tmp['date']] = $tmp; $revs[] = $tmp['date']; } } return $revs; }
/** * Based on _handleRecent() from inc/changelog.php * * @param string $line * @param array $ns * @param array $excludedpages * @param array $type * @param array $user * @param int $maxage * @param array $seen * @return array|bool */ protected function handleChangelogLine($line, $ns, $excludedpages, $type, $user, $maxage, &$seen) { // split the line into parts $change = parseChangelogLine($line); if ($change === false) { return false; } // skip seen ones if (isset($seen[$change['id']])) { return false; } // filter type if (!empty($type) && !in_array($change['type'], $type)) { return false; } // filter user if (!empty($user) && (empty($change['user']) || !in_array($change['user'], $user))) { return false; } // remember in seen to skip additional sights $seen[$change['id']] = 1; // show only not existing pages for delete if ($change['type'] != 'D' && !page_exists($change['id'])) { return false; } // filter maxage if ($maxage && $change['date'] < time() - $maxage) { return false; } // check if it's a hidden page if (isHiddenPage($change['id'])) { return false; } // filter included namespaces if (isset($ns['include'])) { if (!$this->isInNamespace($ns['include'], $change['id'])) { return false; } } // filter excluded namespaces if (isset($ns['exclude'])) { if ($this->isInNamespace($ns['exclude'], $change['id'])) { return false; } } // exclude pages if (!empty($excludedpages)) { foreach ($excludedpages as $page) { if ($change['id'] == $page) { return false; } } } // check ACL $change['perms'] = auth_quickaclcheck($change['id']); if ($change['perms'] < AUTH_READ) { return false; } return $change; }
/** * sometimes chuncksize is set to true */ function test_chuncksizetrue() { $rev = 1362525899; $infoexpected = parseChangelogLine($this->logline); $pagelog = new PageChangeLog($this->pageid, true); $info = $pagelog->getRevisionInfo($rev); $this->assertEquals($infoexpected, $info); }
/** * Internal function used by $this->getComments() * * don't call directly * * @see getRecentComments() * @author Andreas Gohr <*****@*****.**> * @author Ben Coburn <*****@*****.**> * @author Esther Brunner <*****@*****.**> * * @param string $line * @param string $ns * @param array $seen * @return array|bool */ function _handleRecentComment($line, $ns, &$seen) { if (empty($line)) { return false; } //skip empty lines // split the line into parts $recent = parseChangelogLine($line); if ($recent === false) { return false; } $cid = $recent['extra']; $fullcid = $recent['id'] . '#' . $recent['extra']; // skip seen ones if (isset($seen[$fullcid])) { return false; } // skip 'show comment' log entries if ($recent['type'] === 'sc') { return false; } // remember in seen to skip additional sights $seen[$fullcid] = 1; // check if it's a hidden page or comment if (isHiddenPage($recent['id'])) { return false; } if ($recent['type'] === 'hc') { return false; } // filter namespace or id if ($ns && strpos($recent['id'] . ':', $ns . ':') !== 0) { return false; } // check ACL $recent['perm'] = auth_quickaclcheck($recent['id']); if ($recent['perm'] < AUTH_READ) { return false; } // check existance $recent['file'] = wikiFN($recent['id']); $recent['exists'] = @file_exists($recent['file']); if (!$recent['exists']) { return false; } if ($recent['type'] === 'dc') { return false; } // get discussion meta file name $data = unserialize(io_readFile(metaFN($recent['id'], '.comments'), false)); // check if discussion is turned off if ($data['status'] === 0) { return false; } $parent_id = $cid; // Check for the comment and all parents if they exist and are visible. do { $tcid = $parent_id; // check if the comment still exists if (!isset($data['comments'][$tcid])) { return false; } // check if the comment is visible if ($data['comments'][$tcid]['show'] != 1) { return false; } $parent_id = $data['comments'][$tcid]['parent']; } while ($parent_id && $parent_id != $tcid); // okay, then add some additional info if (is_array($data['comments'][$cid]['user'])) { $recent['name'] = $data['comments'][$cid]['user']['name']; } else { $recent['name'] = $data['comments'][$cid]['name']; } $recent['desc'] = strip_tags($data['comments'][$cid]['xhtml']); $recent['anchor'] = 'comment_' . $cid; return $recent; }
/** * Based on _handleRecent() from inc/changelog.php */ function handleChangelogLine($line, $ns, $type, $user, &$seen) { // split the line into parts $change = parseChangelogLine($line); if ($change === false) { return false; } // skip seen ones if (isset($seen[$change['id']])) { return false; } // filter type if (!empty($type) && !in_array($change['type'], $type)) { return false; } // filter user if (!empty($user) && (empty($change['user']) || !in_array($change['user'], $user))) { return false; } // remember in seen to skip additional sights $seen[$change['id']] = 1; // check if it's a hidden page if (isHiddenPage($change['id'])) { return false; } // filter included namespaces if (isset($ns['include'])) { if (!$this->isInNamespace($ns['include'], $change['id'])) { return false; } } // filter excluded namespaces if (isset($ns['exclude'])) { if ($this->isInNamespace($ns['exclude'], $change['id'])) { return false; } } // check ACL $change['perms'] = auth_quickaclcheck($change['id']); if ($change['perms'] < AUTH_READ) { return false; } return $change; }
/** * Trims the recent comments cache to the last $conf['changes_days'] recent * changes or $conf['recent'] items, which ever is larger. * The trimming is only done once a day. * * @author Ben Coburn <*****@*****.**> */ function _trimRecentCommentsLog($changelog) { global $conf; if (@file_exists($changelog) && filectime($changelog) + 86400 < time() && !@file_exists($changelog . '_tmp')) { io_lock($changelog); $lines = file($changelog); if (count($lines) < $conf['recent']) { // nothing to trim io_unlock($changelog); return true; } io_saveFile($changelog . '_tmp', ''); // presave tmp as 2nd lock $trim_time = time() - $conf['recent_days'] * 86400; $out_lines = array(); $num = count($lines); for ($i = 0; $i < $num; $i++) { $log = parseChangelogLine($lines[$i]); if ($log === false) { continue; } // discard junk if ($log['date'] < $trim_time) { $old_lines[$log['date'] . ".{$i}"] = $lines[$i]; // keep old lines for now (append .$i to prevent key collisions) } else { $out_lines[$log['date'] . ".{$i}"] = $lines[$i]; // definitely keep these lines } } // sort the final result, it shouldn't be necessary, // however the extra robustness in making the changelog cache self-correcting is worth it ksort($out_lines); $extra = $conf['recent'] - count($out_lines); // do we need extra lines do bring us up to minimum if ($extra > 0) { ksort($old_lines); $out_lines = array_merge(array_slice($old_lines, -$extra), $out_lines); } // save trimmed changelog io_saveFile($changelog . '_tmp', implode('', $out_lines)); @unlink($changelog); if (!rename($changelog . '_tmp', $changelog)) { // rename failed so try another way... io_unlock($changelog); io_saveFile($changelog, implode('', $out_lines)); @unlink($changelog . '_tmp'); } else { io_unlock($changelog); } return true; } }
/** * Collect the $max revisions near to the timestamp $rev * * @param int $rev revision timestamp * @param int $max maximum number of revisions to be returned * @return bool|array * return array with entries: * - $requestedrevs: array of with $max revision timestamps * - $revs: all parsed revision timestamps * - $fp: filepointer only defined for chuck reading, needs closing. * - $lines: non-parsed changelog lines before the parsed revisions * - $head: position of first readed changelogline * - $lasttail: position of end of last readed changelogline * otherwise false */ protected function retrieveRevisionsAround($rev, $max) { //get lines from changelog list($fp, $lines, $starthead, $starttail, ) = $this->readloglines($rev); if (empty($lines)) { return false; } //parse chunk containing $rev, and read forward more chunks until $max/2 is reached $head = $starthead; $tail = $starttail; $revs = array(); $aftercount = $beforecount = 0; while (count($lines) > 0) { foreach ($lines as $line) { $tmp = parseChangelogLine($line); if ($tmp !== false) { $this->cache[$this->id][$tmp['date']] = $tmp; $revs[] = $tmp['date']; if ($tmp['date'] >= $rev) { //count revs after reference $rev $aftercount++; if ($aftercount == 1) { $beforecount = count($revs); } } //enough revs after reference $rev? if ($aftercount > floor($max / 2)) { break 2; } } } //retrieve next chunk list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1); } if ($aftercount == 0) { return false; } $lasttail = $tail; //read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max $lines = array(); $i = 0; if ($aftercount > 0) { $head = $starthead; $tail = $starttail; while ($head > 0) { list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1); for ($i = count($lines) - 1; $i >= 0; $i--) { $tmp = parseChangelogLine($lines[$i]); if ($tmp !== false) { $this->cache[$this->id][$tmp['date']] = $tmp; $revs[] = $tmp['date']; $beforecount++; //enough revs before reference $rev? if ($beforecount > max(floor($max / 2), $max - $aftercount)) { break 2; } } } } } sort($revs); //keep only non-parsed lines $lines = array_slice($lines, 0, $i); //trunk desired selection $requestedrevs = array_slice($revs, -$max, $max); return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail); }
/** * Internal function used by $this->getLinkbacks() * * don't call directly * * @see getRecentComments() * @author Andreas Gohr <*****@*****.**> * @author Ben Coburn <*****@*****.**> * @author Esther Brunner <*****@*****.**> * @author Gina Haeussge <*****@*****.**> */ function _handleRecentLinkback($line, $ns) { static $seen = array(); //caches seen pages and skip them if (empty($line)) { return false; } //skip empty lines // split the line into parts $recent = parseChangelogLine($line); if ($recent === false) { return false; } $lid = $recent['extra']; $fulllid = $recent['id'] . '#' . $recent['extra']; // skip seen ones if (isset($seen[$fulllid])) { return false; } // skip 'show comment' log entries if ($recent['type'] === 'sc') { return false; } // remember in seen to skip additional sights $seen[$fulllid] = 1; // check if it's a hidden page or comment if (isHiddenPage($recent['id'])) { return false; } if ($recent['type'] === 'hl') { return false; } // filter namespace or id if ($ns && strpos($recent['id'] . ':', $ns . ':') !== 0) { return false; } // check ACL $recent['perm'] = auth_quickaclcheck($recent['id']); if ($recent['perm'] < AUTH_READ) { return false; } // check existance $recent['file'] = wikiFN($recent['id']); $recent['exists'] = @file_exists($recent['file']); if (!$recent['exists']) { return false; } if ($recent['type'] === 'dc') { return false; } // get linkback meta file name $data = unserialize(io_readFile(metaFN($recent['id'], '.linkbacks'), false)); // check if discussion is turned off if (!$data['display']) { return false; } // okay, then add some additional info $recent['name'] = $data['receivedpings'][$lid]['url']; $recent['desc'] = $data['receivedpings'][$lid]['excerpt']; return $recent; }
/** * request existing rev and check cache */ function test_requestrev_checkcache() { $rev = 1362525359; $dir = 1; $revexpected = 1362525899; $infoexpected = parseChangelogLine($this->logline); $pagelog = new PageChangeLog($this->pageid, $chunk_size = 8192); $revfound = $pagelog->getRelativeRevision($rev, $dir); $this->assertEquals($revexpected, $revfound); //checked info returned from cache $info = $pagelog->getRevisionInfo($revfound); $this->assertEquals($infoexpected, $info); }
/** * Returns change log entries within the target period */ function _getLogEntries($start, $end, $logfile, $class = '') { $loglines = (array) @file($logfile); $entries = array(); for ($i = count($loglines) - 1; $i >= 0; $i--) { $entry = parseChangelogLine($loglines[$i]); if ($entry === false) { break; } if ($entry['date'] < $start) { break; } if ($entry['date'] > $end) { continue; } // some fixes for plain text outputs $entry['sum'] = trim(strtr($entry['sum'], "\n\v\r", ' ')); $entry['extra'] = trim(strtr($entry['extra'], "\n\v\r", ' ')); if ($class) { $entry['class'] = $class; } $entries[] = $entry; } return $entries; }