xref: /dokuwiki/inc/changelog.php (revision 9349c09b4622cbdc0c77f4eb31c0a00082924987)
17d559c7fSBen Coburn<?php
2d4f83172SAndreas Gohr
37d559c7fSBen Coburn/**
47d559c7fSBen Coburn * Changelog handling functions
57d559c7fSBen Coburn *
67d559c7fSBen Coburn * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
77d559c7fSBen Coburn * @author     Andreas Gohr <andi@splitbrain.org>
87d559c7fSBen Coburn */
9d4f83172SAndreas Gohr
1024870174SAndreas Gohruse dokuwiki\ChangeLog\MediaChangeLog;
111d11f1d3SSatoshi Saharause dokuwiki\ChangeLog\ChangeLog;
12adf3f0adSGerrit Uitslaguse dokuwiki\ChangeLog\RevisionInfo;
137fba736bSSatoshi Saharause dokuwiki\File\PageFile;
141d11f1d3SSatoshi Sahara
157d559c7fSBen Coburn/**
167d559c7fSBen Coburn * parses a changelog line into it's components
177d559c7fSBen Coburn *
184f1e2cb3SGerrit Uitslag * @param string $line changelog line
194f1e2cb3SGerrit Uitslag * @return array|bool parsed line or false
2071951841SGerrit Uitslag *
2171951841SGerrit Uitslag * @author Ben Coburn <btcoburn@silicodon.net>
2271951841SGerrit Uitslag *
2371951841SGerrit Uitslag * @deprecated 2023-09-25
247d559c7fSBen Coburn */
25d868eb89SAndreas Gohrfunction parseChangelogLine($line)
26d868eb89SAndreas Gohr{
2771951841SGerrit Uitslag    dbg_deprecated('see ' . ChangeLog::class . '::parseLogLine()');
281d11f1d3SSatoshi Sahara    return ChangeLog::parseLogLine($line);
297d559c7fSBen Coburn}
307d559c7fSBen Coburn
317d559c7fSBen Coburn/**
3263f13cadSDamien Regad * Adds an entry to the changelog and saves the metadata for the page
337d559c7fSBen Coburn *
346527839fSSatoshi Sahara * Note: timestamp of the change might not be unique especially after very quick
356527839fSSatoshi Sahara *       repeated edits (e.g. change checkbox via do plugin)
366527839fSSatoshi Sahara *
376527839fSSatoshi Sahara * @param int    $date      Timestamp of the change
38a365baeeSDominik Eckelmann * @param String $id        Name of the affected page
39a365baeeSDominik Eckelmann * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
40a365baeeSDominik Eckelmann * @param String $summary   Summary of the change
41eeda7adaSGerrit Uitslag * @param mixed  $extra     In case of a revert the revision (timestamp) of the reverted page
42a365baeeSDominik Eckelmann * @param array  $flags     Additional flags in a key value array.
434f1e2cb3SGerrit Uitslag *                             Available flags:
44a365baeeSDominik Eckelmann *                             - ExternalEdit - mark as an external edit.
45ac3ed4afSGerrit Uitslag * @param null|int $sizechange Change of filesize
46a365baeeSDominik Eckelmann *
477d559c7fSBen Coburn * @author Andreas Gohr <andi@splitbrain.org>
487d559c7fSBen Coburn * @author Esther Brunner <wikidesign@gmail.com>
497d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
5069f9b481SSatoshi Sahara * @deprecated 2021-11-28
517d559c7fSBen Coburn */
525d9428a0SSatoshi Saharafunction addLogEntry(
535d9428a0SSatoshi Sahara    $date,
545d9428a0SSatoshi Sahara    $id,
555d9428a0SSatoshi Sahara    $type = DOKU_CHANGE_TYPE_EDIT,
565d9428a0SSatoshi Sahara    $summary = '',
575d9428a0SSatoshi Sahara    $extra = '',
585d9428a0SSatoshi Sahara    $flags = null,
59d868eb89SAndreas Gohr    $sizechange = null
60d868eb89SAndreas Gohr) {
6169f9b481SSatoshi Sahara    // no more used in DokuWiki core, but left for third-party plugins
6279a2d784SGerrit Uitslag    dbg_deprecated('see ' . PageFile::class . '::saveWikiText()');
6369f9b481SSatoshi Sahara
64585bf44eSChristopher Smith    /** @var Input $INPUT */
65585bf44eSChristopher Smith    global $INPUT;
667d559c7fSBen Coburn
675aa52fafSBen Coburn    // check for special flags as keys
6824870174SAndreas Gohr    if (!is_array($flags)) $flags = [];
695aa52fafSBen Coburn    $flagExternalEdit = isset($flags['ExternalEdit']);
705aa52fafSBen Coburn
717d559c7fSBen Coburn    $id = cleanid($id);
727d559c7fSBen Coburn
737d559c7fSBen Coburn    if (!$date) $date = time(); //use current time if none supplied
7424870174SAndreas Gohr    $remote = ($flagExternalEdit) ? '127.0.0.1' : clientIP(true);
7524870174SAndreas Gohr    $user   = ($flagExternalEdit) ? '' : $INPUT->server->str('REMOTE_USER');
761d11f1d3SSatoshi Sahara    $sizechange = ($sizechange === null) ? '' : (int)$sizechange;
777d559c7fSBen Coburn
781d11f1d3SSatoshi Sahara    // update changelog file and get the added entry that is also to be stored in metadata
797fba736bSSatoshi Sahara    $pageFile = new PageFile($id);
807fba736bSSatoshi Sahara    $logEntry = $pageFile->changelog->addLogEntry([
817d559c7fSBen Coburn        'date'       => $date,
827d559c7fSBen Coburn        'ip'         => $remote,
83c7192766SSatoshi Sahara        'type'       => $type,
847d559c7fSBen Coburn        'id'         => $id,
857d559c7fSBen Coburn        'user'       => $user,
86c7192766SSatoshi Sahara        'sum'        => $summary,
87c7192766SSatoshi Sahara        'extra'      => $extra,
88c7192766SSatoshi Sahara        'sizechange' => $sizechange,
891d11f1d3SSatoshi Sahara    ]);
907d559c7fSBen Coburn
917d559c7fSBen Coburn    // update metadata
927fba736bSSatoshi Sahara    $pageFile->updateMetadata($logEntry);
937d559c7fSBen Coburn}
947d559c7fSBen Coburn
957d559c7fSBen Coburn/**
96eeda7adaSGerrit Uitslag * Adds an entry to the media changelog
9799c8d7f2Smichael *
9899c8d7f2Smichael * @author Michael Hamann <michael@content-space.de>
9999c8d7f2Smichael * @author Andreas Gohr <andi@splitbrain.org>
10099c8d7f2Smichael * @author Esther Brunner <wikidesign@gmail.com>
10199c8d7f2Smichael * @author Ben Coburn <btcoburn@silicodon.net>
1024f1e2cb3SGerrit Uitslag *
1034f1e2cb3SGerrit Uitslag * @param int    $date      Timestamp of the change
1044f1e2cb3SGerrit Uitslag * @param String $id        Name of the affected page
1054f1e2cb3SGerrit Uitslag * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
1064f1e2cb3SGerrit Uitslag * @param String $summary   Summary of the change
107eeda7adaSGerrit Uitslag * @param mixed  $extra     In case of a revert the revision (timestamp) of the reverted page
1084f1e2cb3SGerrit Uitslag * @param array  $flags     Additional flags in a key value array.
1094f1e2cb3SGerrit Uitslag *                             Available flags:
1104f1e2cb3SGerrit Uitslag *                             - (none, so far)
111ac3ed4afSGerrit Uitslag * @param null|int $sizechange Change of filesize
11299c8d7f2Smichael */
11364159a61SAndreas Gohrfunction addMediaLogEntry(
11464159a61SAndreas Gohr    $date,
11564159a61SAndreas Gohr    $id,
11664159a61SAndreas Gohr    $type = DOKU_CHANGE_TYPE_EDIT,
11764159a61SAndreas Gohr    $summary = '',
11864159a61SAndreas Gohr    $extra = '',
11964159a61SAndreas Gohr    $flags = null,
120d868eb89SAndreas Gohr    $sizechange = null
121d868eb89SAndreas Gohr) {
122585bf44eSChristopher Smith    /** @var Input $INPUT */
123585bf44eSChristopher Smith    global $INPUT;
12499c8d7f2Smichael
125facfe250SSatoshi Sahara    // check for special flags as keys
12624870174SAndreas Gohr    if (!is_array($flags)) $flags = [];
127facfe250SSatoshi Sahara    $flagExternalEdit = isset($flags['ExternalEdit']);
128facfe250SSatoshi Sahara
12999c8d7f2Smichael    $id = cleanid($id);
13099c8d7f2Smichael
13199c8d7f2Smichael    if (!$date) $date = time(); //use current time if none supplied
13224870174SAndreas Gohr    $remote = ($flagExternalEdit) ? '127.0.0.1' : clientIP(true);
13324870174SAndreas Gohr    $user   = ($flagExternalEdit) ? '' : $INPUT->server->str('REMOTE_USER');
1341d11f1d3SSatoshi Sahara    $sizechange = ($sizechange === null) ? '' : (int)$sizechange;
13599c8d7f2Smichael
1361d11f1d3SSatoshi Sahara    // update changelog file and get the added entry
13724870174SAndreas Gohr    (new MediaChangeLog($id, 1024))->addLogEntry([
13899c8d7f2Smichael        'date'       => $date,
13999c8d7f2Smichael        'ip'         => $remote,
140c7192766SSatoshi Sahara        'type'       => $type,
14199c8d7f2Smichael        'id'         => $id,
14299c8d7f2Smichael        'user'       => $user,
143c7192766SSatoshi Sahara        'sum'        => $summary,
144c7192766SSatoshi Sahara        'extra'      => $extra,
145c7192766SSatoshi Sahara        'sizechange' => $sizechange,
1461d11f1d3SSatoshi Sahara    ]);
14799c8d7f2Smichael}
14899c8d7f2Smichael
14999c8d7f2Smichael/**
150252acce3SSatoshi Sahara * returns an array of recently changed files using the changelog
1517d559c7fSBen Coburn *
1527d559c7fSBen Coburn * The following constants can be used to control which changes are
1537d559c7fSBen Coburn * included. Add them together as needed.
1547d559c7fSBen Coburn *
1557d559c7fSBen Coburn * RECENTS_SKIP_DELETED   - don't include deleted pages
1567d559c7fSBen Coburn * RECENTS_SKIP_MINORS    - don't include minor changes
15708e9b52fSPhy * RECENTS_ONLY_CREATION  - only include new created pages and media
1587d559c7fSBen Coburn * RECENTS_SKIP_SUBSPACES - don't include subspaces
1590b926329SKate Arzamastseva * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
1600b926329SKate Arzamastseva * RECENTS_MEDIA_PAGES_MIXED  - return both media changes and page changes
1617d559c7fSBen Coburn *
1627d559c7fSBen Coburn * @param int    $first   number of first entry returned (for paginating
1637d559c7fSBen Coburn * @param int    $num     return $num entries
1647d559c7fSBen Coburn * @param string $ns      restrict to given namespace
16559f20ea3SMichael Hamann * @param int    $flags   see above
16659f20ea3SMichael Hamann * @return array recently changed files
1677d559c7fSBen Coburn *
1687d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
16929778747SKate Arzamastseva * @author Kate Arzamastseva <pshns@ukr.net>
1707d559c7fSBen Coburn */
171d868eb89SAndreas Gohrfunction getRecents($first, $num, $ns = '', $flags = 0)
172d868eb89SAndreas Gohr{
1737d559c7fSBen Coburn    global $conf;
17424870174SAndreas Gohr    $recent = [];
1757d559c7fSBen Coburn    $count  = 0;
1767d559c7fSBen Coburn
177*9349c09bSGerrit Uitslag    if (!$num) {
1787d559c7fSBen Coburn        return $recent;
179*9349c09bSGerrit Uitslag    }
1807d559c7fSBen Coburn
1817d559c7fSBen Coburn    // read all recent changes. (kept short)
1820b926329SKate Arzamastseva    if ($flags & RECENTS_MEDIA_CHANGES) {
1838e3e8693SAndreas Gohr        $lines = @file($conf['media_changelog']) ?: [];
18499c8d7f2Smichael    } else {
1858e3e8693SAndreas Gohr        $lines = @file($conf['changelog']) ?: [];
18699c8d7f2Smichael    }
1871b266025SPhy    if (!is_array($lines)) {
18824870174SAndreas Gohr        $lines = [];
1891b266025SPhy    }
19029778747SKate Arzamastseva    $lines_position = count($lines) - 1;
19159f20ea3SMichael Hamann    $media_lines_position = 0;
19224870174SAndreas Gohr    $media_lines = [];
19329778747SKate Arzamastseva
1940b926329SKate Arzamastseva    if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
1958e3e8693SAndreas Gohr        $media_lines = @file($conf['media_changelog']) ?: [];
1961b266025SPhy        if (!is_array($media_lines)) {
19724870174SAndreas Gohr            $media_lines = [];
1981b266025SPhy        }
19929778747SKate Arzamastseva        $media_lines_position = count($media_lines) - 1;
20029778747SKate Arzamastseva    }
20129778747SKate Arzamastseva
202*9349c09bSGerrit Uitslag    $seen = []; // caches seen lines, _handleRecentLogLine() skips them
2037d559c7fSBen Coburn
2047d559c7fSBen Coburn    // handle lines
2050b926329SKate Arzamastseva    while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >= 0)) {
20629778747SKate Arzamastseva        if (empty($rec) && $lines_position >= 0) {
207*9349c09bSGerrit Uitslag            $rec = _handleRecentLogLine(@$lines[$lines_position], $ns, $flags, $seen);
20829778747SKate Arzamastseva            if (!$rec) {
20929778747SKate Arzamastseva                $lines_position--;
21029778747SKate Arzamastseva                continue;
21129778747SKate Arzamastseva            }
21229778747SKate Arzamastseva        }
2130b926329SKate Arzamastseva        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
214*9349c09bSGerrit Uitslag            $media_rec = _handleRecentLogLine(
21564159a61SAndreas Gohr                @$media_lines[$media_lines_position],
21664159a61SAndreas Gohr                $ns,
21764159a61SAndreas Gohr                $flags | RECENTS_MEDIA_CHANGES,
21864159a61SAndreas Gohr                $seen
21964159a61SAndreas Gohr            );
22029778747SKate Arzamastseva            if (!$media_rec) {
22129778747SKate Arzamastseva                $media_lines_position--;
22229778747SKate Arzamastseva                continue;
22329778747SKate Arzamastseva            }
22429778747SKate Arzamastseva        }
2250b926329SKate Arzamastseva        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
22629778747SKate Arzamastseva            $media_lines_position--;
22729778747SKate Arzamastseva            $x = $media_rec;
228adf3f0adSGerrit Uitslag            $x['mode'] = RevisionInfo::MODE_MEDIA;
22929778747SKate Arzamastseva            $media_rec = false;
23029778747SKate Arzamastseva        } else {
23129778747SKate Arzamastseva            $lines_position--;
23229778747SKate Arzamastseva            $x = $rec;
23335bad86aSTherealperO            if ($flags & RECENTS_MEDIA_CHANGES) {
234adf3f0adSGerrit Uitslag                $x['mode'] = RevisionInfo::MODE_MEDIA;
23535bad86aSTherealperO            } else {
236adf3f0adSGerrit Uitslag                $x['mode'] = RevisionInfo::MODE_PAGE;
23735bad86aSTherealperO            }
23829778747SKate Arzamastseva            $rec = false;
23929778747SKate Arzamastseva        }
2407d559c7fSBen Coburn        if (--$first >= 0) continue; // skip first entries
24129778747SKate Arzamastseva        $recent[] = $x;
2427d559c7fSBen Coburn        $count++;
2437d559c7fSBen Coburn        // break when we have enough entries
244177d6836SAndreas Gohr        if ($count >= $num) {
245d4f83172SAndreas Gohr            break;
246d4f83172SAndreas Gohr        }
2477d559c7fSBen Coburn    }
2487d559c7fSBen Coburn    return $recent;
2497d559c7fSBen Coburn}
2507d559c7fSBen Coburn
2517d559c7fSBen Coburn/**
25299c8d7f2Smichael * returns an array of files changed since a given time using the
25399c8d7f2Smichael * changelog
25499c8d7f2Smichael *
25599c8d7f2Smichael * The following constants can be used to control which changes are
25699c8d7f2Smichael * included. Add them together as needed.
25799c8d7f2Smichael *
25899c8d7f2Smichael * RECENTS_SKIP_DELETED   - don't include deleted pages
25999c8d7f2Smichael * RECENTS_SKIP_MINORS    - don't include minor changes
26008e9b52fSPhy * RECENTS_ONLY_CREATION  - only include new created pages and media
26199c8d7f2Smichael * RECENTS_SKIP_SUBSPACES - don't include subspaces
2620b926329SKate Arzamastseva * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
26399c8d7f2Smichael *
26499c8d7f2Smichael * @param int    $from    date of the oldest entry to return
26599c8d7f2Smichael * @param int    $to      date of the newest entry to return (for pagination, optional)
26699c8d7f2Smichael * @param string $ns      restrict to given namespace (optional)
26759f20ea3SMichael Hamann * @param int    $flags   see above (optional)
26859f20ea3SMichael Hamann * @return array of files
26999c8d7f2Smichael *
27099c8d7f2Smichael * @author Michael Hamann <michael@content-space.de>
27199c8d7f2Smichael * @author Ben Coburn <btcoburn@silicodon.net>
27299c8d7f2Smichael */
273d868eb89SAndreas Gohrfunction getRecentsSince($from, $to = null, $ns = '', $flags = 0)
274d868eb89SAndreas Gohr{
27599c8d7f2Smichael    global $conf;
27624870174SAndreas Gohr    $recent = [];
27799c8d7f2Smichael
278*9349c09bSGerrit Uitslag    if ($to && $to < $from) {
27999c8d7f2Smichael        return $recent;
280*9349c09bSGerrit Uitslag    }
28199c8d7f2Smichael
28299c8d7f2Smichael    // read all recent changes. (kept short)
2830b926329SKate Arzamastseva    if ($flags & RECENTS_MEDIA_CHANGES) {
28499c8d7f2Smichael        $lines = @file($conf['media_changelog']);
28599c8d7f2Smichael    } else {
28699c8d7f2Smichael        $lines = @file($conf['changelog']);
28799c8d7f2Smichael    }
288e920a0a1SAndreas Gohr    if (!$lines) return $recent;
28999c8d7f2Smichael
29099c8d7f2Smichael    // we start searching at the end of the list
29199c8d7f2Smichael    $lines = array_reverse($lines);
29299c8d7f2Smichael
29399c8d7f2Smichael    // handle lines
294*9349c09bSGerrit Uitslag    $seen = []; // caches seen lines, _handleRecentLogLine() skips them
29599c8d7f2Smichael
29699c8d7f2Smichael    foreach ($lines as $line) {
297*9349c09bSGerrit Uitslag        $rec = _handleRecentLogLine($line, $ns, $flags, $seen);
29899c8d7f2Smichael        if ($rec !== false) {
29999c8d7f2Smichael            if ($rec['date'] >= $from) {
30099c8d7f2Smichael                if (!$to || $rec['date'] <= $to) {
30199c8d7f2Smichael                    $recent[] = $rec;
30299c8d7f2Smichael                }
30399c8d7f2Smichael            } else {
30499c8d7f2Smichael                break;
30599c8d7f2Smichael            }
30699c8d7f2Smichael        }
30799c8d7f2Smichael    }
30899c8d7f2Smichael
30999c8d7f2Smichael    return array_reverse($recent);
31099c8d7f2Smichael}
31199c8d7f2Smichael
31299c8d7f2Smichael/**
3137d559c7fSBen Coburn * Internal function used by getRecents
314*9349c09bSGerrit Uitslag * Parse a line and checks whether it should be included
3157d559c7fSBen Coburn *
3167d559c7fSBen Coburn * don't call directly
3177d559c7fSBen Coburn *
3187d559c7fSBen Coburn * @see getRecents()
3197d559c7fSBen Coburn * @author Andreas Gohr <andi@splitbrain.org>
3207d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
3214f1e2cb3SGerrit Uitslag *
3224f1e2cb3SGerrit Uitslag * @param string $line   changelog line
3234f1e2cb3SGerrit Uitslag * @param string $ns     restrict to given namespace
3244f1e2cb3SGerrit Uitslag * @param int    $flags  flags to control which changes are included
3254f1e2cb3SGerrit Uitslag * @param array  $seen   listing of seen pages
3264f1e2cb3SGerrit Uitslag * @return array|bool    false or array with info about a change
3277d559c7fSBen Coburn */
328*9349c09bSGerrit Uitslagfunction _handleRecentLogLine($line, $ns, $flags, &$seen)
329d868eb89SAndreas Gohr{
3307d559c7fSBen Coburn    if (empty($line)) return false;   //skip empty lines
3317d559c7fSBen Coburn
3327d559c7fSBen Coburn    // split the line into parts
3331d11f1d3SSatoshi Sahara    $recent = ChangeLog::parseLogLine($line);
334252acce3SSatoshi Sahara    if ($recent === false) return false;
3357d559c7fSBen Coburn
3367d559c7fSBen Coburn    // skip seen ones
3377d559c7fSBen Coburn    if (isset($seen[$recent['id']])) return false;
3387d559c7fSBen Coburn
33908e9b52fSPhy    // skip changes, of only new items are requested
34008e9b52fSPhy    if ($recent['type'] !== DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false;
34168f43bcfSTero Kivinen
3427d559c7fSBen Coburn    // skip minors
343ebf1501fSBen Coburn    if ($recent['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
3447d559c7fSBen Coburn
3457d559c7fSBen Coburn    // remember in seen to skip additional sights
3467d559c7fSBen Coburn    $seen[$recent['id']] = 1;
3477d559c7fSBen Coburn
3487d559c7fSBen Coburn    // check if it's a hidden page
3497d559c7fSBen Coburn    if (isHiddenPage($recent['id'])) return false;
3507d559c7fSBen Coburn
3517d559c7fSBen Coburn    // filter namespace
3527d559c7fSBen Coburn    if (($ns) && (strpos($recent['id'], $ns . ':') !== 0)) return false;
3537d559c7fSBen Coburn
3547d559c7fSBen Coburn    // exclude subnamespaces
3557d559c7fSBen Coburn    if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
3567d559c7fSBen Coburn
3577d559c7fSBen Coburn    // check ACL
35842025dfdSMichael Hamann    if ($flags & RECENTS_MEDIA_CHANGES) {
35942025dfdSMichael Hamann        $recent['perms'] = auth_quickaclcheck(getNS($recent['id']) . ':*');
36042025dfdSMichael Hamann    } else {
36199c8d7f2Smichael        $recent['perms'] = auth_quickaclcheck($recent['id']);
36242025dfdSMichael Hamann    }
36399c8d7f2Smichael    if ($recent['perms'] < AUTH_READ) return false;
3647d559c7fSBen Coburn
365eeda7adaSGerrit Uitslag    // check existence
3661d901ab2SAndreas Gohr    if ($flags & RECENTS_SKIP_DELETED) {
36742025dfdSMichael Hamann        $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
36879e79377SAndreas Gohr        if (!file_exists($fn)) return false;
3691d901ab2SAndreas Gohr    }
3707d559c7fSBen Coburn
3717d559c7fSBen Coburn    return $recent;
3727d559c7fSBen Coburn}
373