← Index
NYTProf Performance Profile   « line view »
For ./view
  Run on Fri Jul 31 18:42:36 2015
Reported on Fri Jul 31 18:48:15 2015

Filename/var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Handler.pm
StatementsExecuted 1048004 statements in 3.99s
Subroutines
Calls P F Exclusive
Time
Inclusive
Time
Subroutine
26327111.20s1.85sFoswiki::Store::Rcs::Handler::::readFileFoswiki::Store::Rcs::Handler::readFile
105614211.06s3.85sFoswiki::Store::Rcs::Handler::::__ANON__[:63]Foswiki::Store::Rcs::Handler::__ANON__[:63]
4011514ms2.78sFoswiki::Store::Rcs::Handler::::getTopicNamesFoswiki::Store::Rcs::Handler::getTopicNames
5265843451ms2.41sFoswiki::Store::Rcs::Handler::::revisionHistoryExistsFoswiki::Store::Rcs::Handler::revisionHistoryExists
5295672416ms2.31sFoswiki::Store::Rcs::Handler::::storedDataExistsFoswiki::Store::Rcs::Handler::storedDataExists
2632711331ms3.21sFoswiki::Store::Rcs::Handler::::getRevisionFoswiki::Store::Rcs::Handler::getRevision
2669311237ms261msFoswiki::Store::Rcs::Handler::::newFoswiki::Store::Rcs::Handler::new
261114.9ms16.4msFoswiki::Store::Rcs::Handler::::getWebNamesFoswiki::Store::Rcs::Handler::getWebNames
1111.51ms3.73msFoswiki::Store::Rcs::Handler::::BEGIN@50Foswiki::Store::Rcs::Handler::BEGIN@50
111369µs454µsFoswiki::Store::Rcs::Handler::::BEGIN@44Foswiki::Store::Rcs::Handler::BEGIN@44
21198µs181µsFoswiki::Store::Rcs::Handler::::_getTOPICINFOFoswiki::Store::Rcs::Handler::_getTOPICINFO
21144µs252µsFoswiki::Store::Rcs::Handler::::noCheckinPendingFoswiki::Store::Rcs::Handler::noCheckinPending
21141µs530µsFoswiki::Store::Rcs::Handler::::getLatestRevisionIDFoswiki::Store::Rcs::Handler::getLatestRevisionID
21135µs652µsFoswiki::Store::Rcs::Handler::::getRevisionHistoryFoswiki::Store::Rcs::Handler::getRevisionHistory
11118µs31µsFoswiki::Store::Rcs::Handler::::BEGIN@29Foswiki::Store::Rcs::Handler::BEGIN@29
11113µs13µsFoswiki::Store::Rcs::Handler::::BEGIN@53Foswiki::Store::Rcs::Handler::BEGIN@53
11110µs361µsFoswiki::Store::Rcs::Handler::::BEGIN@37Foswiki::Store::Rcs::Handler::BEGIN@37
1119µs34µsFoswiki::Store::Rcs::Handler::::BEGIN@31Foswiki::Store::Rcs::Handler::BEGIN@31
1119µs13µsFoswiki::Store::Rcs::Handler::::BEGIN@30Foswiki::Store::Rcs::Handler::BEGIN@30
1115µs5µsFoswiki::Store::Rcs::Handler::::BEGIN@38Foswiki::Store::Rcs::Handler::BEGIN@38
1114µs4µsFoswiki::Store::Rcs::Handler::::BEGIN@45Foswiki::Store::Rcs::Handler::BEGIN@45
1114µs4µsFoswiki::Store::Rcs::Handler::::BEGIN@34Foswiki::Store::Rcs::Handler::BEGIN@34
1114µs4µsFoswiki::Store::Rcs::Handler::::BEGIN@33Foswiki::Store::Rcs::Handler::BEGIN@33
1114µs4µsFoswiki::Store::Rcs::Handler::::BEGIN@35Foswiki::Store::Rcs::Handler::BEGIN@35
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@49Foswiki::Store::Rcs::Handler::BEGIN@49
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@41Foswiki::Store::Rcs::Handler::BEGIN@41
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@42Foswiki::Store::Rcs::Handler::BEGIN@42
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@39Foswiki::Store::Rcs::Handler::BEGIN@39
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@43Foswiki::Store::Rcs::Handler::BEGIN@43
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@36Foswiki::Store::Rcs::Handler::BEGIN@36
1113µs3µsFoswiki::Store::Rcs::Handler::::BEGIN@48Foswiki::Store::Rcs::Handler::BEGIN@48
0000s0sFoswiki::Store::Rcs::Handler::::__ANON__[:64]Foswiki::Store::Rcs::Handler::__ANON__[:64]
0000s0sFoswiki::Store::Rcs::Handler::::_cacheMetaInfoFoswiki::Store::Rcs::Handler::_cacheMetaInfo
0000s0sFoswiki::Store::Rcs::Handler::::_constructAttributesForAutoAttachedFoswiki::Store::Rcs::Handler::_constructAttributesForAutoAttached
0000s0sFoswiki::Store::Rcs::Handler::::_controlFileNameFoswiki::Store::Rcs::Handler::_controlFileName
0000s0sFoswiki::Store::Rcs::Handler::::_copyFileFoswiki::Store::Rcs::Handler::_copyFile
0000s0sFoswiki::Store::Rcs::Handler::::_dirForTopicAttachmentsFoswiki::Store::Rcs::Handler::_dirForTopicAttachments
0000s0sFoswiki::Store::Rcs::Handler::::_epochToRcsDateTimeFoswiki::Store::Rcs::Handler::_epochToRcsDateTime
0000s0sFoswiki::Store::Rcs::Handler::::_fromcsFoswiki::Store::Rcs::Handler::_fromcs
0000s0sFoswiki::Store::Rcs::Handler::::_getAttachmentStatsFoswiki::Store::Rcs::Handler::_getAttachmentStats
0000s0sFoswiki::Store::Rcs::Handler::::_mktempFoswiki::Store::Rcs::Handler::_mktemp
0000s0sFoswiki::Store::Rcs::Handler::::_moveFileFoswiki::Store::Rcs::Handler::_moveFile
0000s0sFoswiki::Store::Rcs::Handler::::_rmtreeFoswiki::Store::Rcs::Handler::_rmtree
0000s0sFoswiki::Store::Rcs::Handler::::addRevisionFromStreamFoswiki::Store::Rcs::Handler::addRevisionFromStream
0000s0sFoswiki::Store::Rcs::Handler::::addRevisionFromTextFoswiki::Store::Rcs::Handler::addRevisionFromText
0000s0sFoswiki::Store::Rcs::Handler::::ciFoswiki::Store::Rcs::Handler::ci
0000s0sFoswiki::Store::Rcs::Handler::::copyAttachmentFoswiki::Store::Rcs::Handler::copyAttachment
0000s0sFoswiki::Store::Rcs::Handler::::copyTopicFoswiki::Store::Rcs::Handler::copyTopic
0000s0sFoswiki::Store::Rcs::Handler::::finishFoswiki::Store::Rcs::Handler::finish
0000s0sFoswiki::Store::Rcs::Handler::::getAttachmentListFoswiki::Store::Rcs::Handler::getAttachmentList
0000s0sFoswiki::Store::Rcs::Handler::::getInfoFoswiki::Store::Rcs::Handler::getInfo
0000s0sFoswiki::Store::Rcs::Handler::::getLatestRevisionTimeFoswiki::Store::Rcs::Handler::getLatestRevisionTime
0000s0sFoswiki::Store::Rcs::Handler::::getLeaseFoswiki::Store::Rcs::Handler::getLease
0000s0sFoswiki::Store::Rcs::Handler::::getNextRevisionIDFoswiki::Store::Rcs::Handler::getNextRevisionID
0000s0sFoswiki::Store::Rcs::Handler::::getTimestampFoswiki::Store::Rcs::Handler::getTimestamp
0000s0sFoswiki::Store::Rcs::Handler::::hidePathFoswiki::Store::Rcs::Handler::hidePath
0000s0sFoswiki::Store::Rcs::Handler::::initFoswiki::Store::Rcs::Handler::init
0000s0sFoswiki::Store::Rcs::Handler::::isAsciiDefaultFoswiki::Store::Rcs::Handler::isAsciiDefault
0000s0sFoswiki::Store::Rcs::Handler::::isLockedFoswiki::Store::Rcs::Handler::isLocked
0000s0sFoswiki::Store::Rcs::Handler::::mkPathToFoswiki::Store::Rcs::Handler::mkPathTo
0000s0sFoswiki::Store::Rcs::Handler::::mkTmpFilenameFoswiki::Store::Rcs::Handler::mkTmpFilename
0000s0sFoswiki::Store::Rcs::Handler::::moveAttachmentFoswiki::Store::Rcs::Handler::moveAttachment
0000s0sFoswiki::Store::Rcs::Handler::::moveTopicFoswiki::Store::Rcs::Handler::moveTopic
0000s0sFoswiki::Store::Rcs::Handler::::moveWebFoswiki::Store::Rcs::Handler::moveWeb
0000s0sFoswiki::Store::Rcs::Handler::::openStreamFoswiki::Store::Rcs::Handler::openStream
0000s0sFoswiki::Store::Rcs::Handler::::readChangesFoswiki::Store::Rcs::Handler::readChanges
0000s0sFoswiki::Store::Rcs::Handler::::recordChangeFoswiki::Store::Rcs::Handler::recordChange
0000s0sFoswiki::Store::Rcs::Handler::::removeFoswiki::Store::Rcs::Handler::remove
0000s0sFoswiki::Store::Rcs::Handler::::removeSpuriousLeasesFoswiki::Store::Rcs::Handler::removeSpuriousLeases
0000s0sFoswiki::Store::Rcs::Handler::::repRevFoswiki::Store::Rcs::Handler::repRev
0000s0sFoswiki::Store::Rcs::Handler::::replaceRevisionFoswiki::Store::Rcs::Handler::replaceRevision
0000s0sFoswiki::Store::Rcs::Handler::::restoreLatestRevisionFoswiki::Store::Rcs::Handler::restoreLatestRevision
0000s0sFoswiki::Store::Rcs::Handler::::revisionExistsFoswiki::Store::Rcs::Handler::revisionExists
0000s0sFoswiki::Store::Rcs::Handler::::saveFileFoswiki::Store::Rcs::Handler::saveFile
0000s0sFoswiki::Store::Rcs::Handler::::savePendingCheckinFoswiki::Store::Rcs::Handler::savePendingCheckin
0000s0sFoswiki::Store::Rcs::Handler::::saveStreamFoswiki::Store::Rcs::Handler::saveStream
0000s0sFoswiki::Store::Rcs::Handler::::setLeaseFoswiki::Store::Rcs::Handler::setLease
0000s0sFoswiki::Store::Rcs::Handler::::setLockFoswiki::Store::Rcs::Handler::setLock
0000s0sFoswiki::Store::Rcs::Handler::::stringifyFoswiki::Store::Rcs::Handler::stringify
0000s0sFoswiki::Store::Rcs::Handler::::synchroniseAttachmentsListFoswiki::Store::Rcs::Handler::synchroniseAttachmentsList
0000s0sFoswiki::Store::Rcs::Handler::::testFoswiki::Store::Rcs::Handler::test
0000s0sFoswiki::Store::_MemoryFile::::CLOSE Foswiki::Store::_MemoryFile::CLOSE
0000s0sFoswiki::Store::_MemoryFile::::READ Foswiki::Store::_MemoryFile::READ
0000s0sFoswiki::Store::_MemoryFile::::READLINE Foswiki::Store::_MemoryFile::READLINE
0000s0sFoswiki::Store::_MemoryFile::::TIEHANDLE Foswiki::Store::_MemoryFile::TIEHANDLE
Call graph for these subroutines as a Graphviz dot language file.
Line State
ments
Time
on line
Calls Time
in subs
Code
1# See bottom of file for license and copyright information
2
3=begin TML
4
5---+ package Foswiki::Store::Rcs::Handler
6
7This class is PACKAGE PRIVATE to Store::VC, and should never be
8used from anywhere else. It is the base class of implementations of
9individual file handler objects used with stores that manipulate
10files stored in a version control system (phew!).
11
12The general contract of the methods on this class and its subclasses
13calls for errors to be signalled by Error::Simple exceptions.
14
15There are a number of references to RCS below; however this class is
16useful as a base class for handlers for all kinds of version control
17systems which use files on disk.
18
19A note on character encodings. The RCS handler classes treat
20web, topic and attachment *names* coming from the caller as _character_
21(i.e. UNICODE) data. *Content*, however, is always assumed to be bytes.
22This is done so that the handlers can operate on text (topic) content
23and binary (attachment) data using the same functions.
24
25=cut
26
27package Foswiki::Store::Rcs::Handler;
28
29228µs244µs
# spent 31µs (18+13) within Foswiki::Store::Rcs::Handler::BEGIN@29 which was called: # once (18µs+13µs) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 29
use strict;
# spent 31µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@29 # spent 13µs making 1 call to strict::import
30223µs217µs
# spent 13µs (9+4) within Foswiki::Store::Rcs::Handler::BEGIN@30 which was called: # once (9µs+4µs) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 30
use warnings;
# spent 13µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@30 # spent 4µs making 1 call to warnings::import
31225µs260µs
# spent 34µs (9+25) within Foswiki::Store::Rcs::Handler::BEGIN@31 which was called: # once (9µs+25µs) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 31
use Assert;
# spent 34µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@31 # spent 25µs making 1 call to Exporter::import
32
33220µs14µs
# spent 4µs within Foswiki::Store::Rcs::Handler::BEGIN@33 which was called: # once (4µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 33
use IO::File ();
# spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@33
34221µs14µs
# spent 4µs within Foswiki::Store::Rcs::Handler::BEGIN@34 which was called: # once (4µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 34
use File::Copy ();
# spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@34
35218µs14µs
# spent 4µs within Foswiki::Store::Rcs::Handler::BEGIN@35 which was called: # once (4µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 35
use File::Spec ();
# spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@35
36222µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@36 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 36
use File::Path ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@36
37232µs2712µs
# spent 361µs (10+351) within Foswiki::Store::Rcs::Handler::BEGIN@37 which was called: # once (10µs+351µs) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 37
use Fcntl qw( :DEFAULT :flock SEEK_SET );
# spent 361µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@37 # spent 351µs making 1 call to Exporter::import
38224µs15µs
# spent 5µs within Foswiki::Store::Rcs::Handler::BEGIN@38 which was called: # once (5µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 38
use Encode ();
# spent 5µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@38
39220µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@39 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 39
use JSON ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@39
40
41218µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@41 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 41
use Foswiki::Store ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@41
42217µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@42 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 42
use Foswiki::Store::Rcs::Store ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@42
43218µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@43 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 43
use Foswiki::Sandbox ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@43
44298µs1454µs
# spent 454µs (369+85) within Foswiki::Store::Rcs::Handler::BEGIN@44 which was called: # once (369µs+85µs) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 44
use Foswiki::Iterator::NumberRangeIterator ();
# spent 454µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@44
45220µs14µs
# spent 4µs within Foswiki::Store::Rcs::Handler::BEGIN@45 which was called: # once (4µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 45
use Foswiki::Attrs ();
# spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@45
46
47# Modules required for handling TOPICINFO cacheing
48218µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@48 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 48
use Foswiki::Meta ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@48
49218µs13µs
# spent 3µs within Foswiki::Store::Rcs::Handler::BEGIN@49 which was called: # once (3µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 49
use Foswiki::Serialise ();
# spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@49
502198µs13.73ms
# spent 3.73ms (1.51+2.22) within Foswiki::Store::Rcs::Handler::BEGIN@50 which was called: # once (1.51ms+2.22ms) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 50
use Foswiki::Users::BaseUserMapping ();
# spent 3.73ms making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@50
51
52# use the locale if required to ensure sort order is correct
53
# spent 13µs within Foswiki::Store::Rcs::Handler::BEGIN@53 which was called: # once (13µs+0s) by Foswiki::Store::Rcs::RcsWrapHandler::BEGIN@23 at line 65
BEGIN {
541700ns if ( $Foswiki::cfg{UseLocale} ) {
55 require locale;
56 import locale();
57 }
58
5912µs *_decode = \&Foswiki::Store::decode;
601400ns *_encode = \&Foswiki::Store::encode;
611400ns *_stat = \&Foswiki::Store::Rcs::Store::_stat;
621300ns *_unlink = \&Foswiki::Store::Rcs::Store::_unlink;
631056151.31s1056142.80s
# spent 3.85s (1.06+2.80) within Foswiki::Store::Rcs::Handler::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Handler.pm:63] which was called 105614 times, avg 36µs/call: # 52956 times (521ms+1.37s) by Foswiki::Store::Rcs::Handler::storedDataExists at line 712, avg 36µs/call # 52658 times (535ms+1.43s) by Foswiki::Store::Rcs::Handler::revisionHistoryExists at line 726, avg 37µs/call
*_e = sub { -e _encode( $_[0] ) };
# spent 2.80s making 105614 calls to Foswiki::Store::encode, avg 26µs/call
6416µs *_d = sub { -d _encode( $_[0] ) };
6515.93ms113µs}
# spent 13µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@53
66
67119µs358µsour $json = JSON->new->utf8(1)->pretty(0);
# spent 34µs making 1 call to JSON::PP::pretty # spent 19µs making 1 call to JSON::PP::new # spent 6µs making 1 call to JSON::PP::utf8
68
69=begin TML
70
71---++ ClassMethod new($store, $web, $topic, $attachment)
72
73Constructor. There is one object per stored file.
74
75$store is the Foswiki::Rcs::Store object that contains the cache for
76objects of this type. A cache is used because at some point we'll be
77smarter about the number of calls to RCS code we make.
78
79Note that $web, $topic and $attachment must be untainted, and encoded
80as utf-8 octets
81
82=cut
83
84
# spent 261ms (237+24.2) within Foswiki::Store::Rcs::Handler::new which was called 26693 times, avg 10µs/call: # 26693 times (237ms+24.2ms) by Foswiki::Store::Rcs::RcsWrapHandler::new at line 39 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/RcsWrapHandler.pm, avg 10µs/call
sub new {
852669317.7ms my ( $class, $store, $web, $topic, $attachment ) = @_;
86
87 ASSERT( $store->isa('Foswiki::Store') ) if DEBUG;
88
892669335.8ms2669324.2ms ASSERT( !ref($web) ); # defunct usage
# spent 24.2ms making 26693 calls to Assert::ASSERT, avg 908ns/call
90
91 # Reuse is good
922669346.0ms my $id = ( $web || 0 ) . '/' . ( $topic || 0 ) . '/' . ( $attachment || 0 );
9326693161ms if ( $store->{handler_cache} && $store->{handler_cache}->{$id} ) {
94 return $store->{handler_cache}->{$id};
95 }
96
97 # web, topic and attachment are all held unicode
98320816µs my $this = bless(
99 {
100 web => $web,
101 topic => $topic,
102 attachment => $attachment
103 },
104 $class
105 );
106
107 # Cache so we can re-use this object (it has no internal state
108 # so can safely be reused)
109320445µs $store->{handler_cache}->{$id} = $this;
110
111320232µs if ( $this->{web} && $this->{topic} ) {
112299203µs my $rcsSubDir = ( $Foswiki::cfg{RCS}{useSubDir} ? '/RCS' : '' );
113
114 ASSERT( UNTAINTED($web), "web $web is tainted!" ) if DEBUG;
11529922µs ASSERT( UNTAINTED($topic), "topic $topic is tainted!" ) if DEBUG;
116299111µs if ($attachment) {
117 ASSERT( UNTAINTED($attachment) ) if DEBUG;
11813µs $this->{file} =
119 "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}/$attachment";
12013µs $this->{rcsFile} =
121"$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}$rcsSubDir/$attachment,v";
122
123 }
124 else {
125298592µs $this->{file} =
126 "$Foswiki::cfg{DataDir}/$this->{web}/$this->{topic}.txt";
127298472µs $this->{rcsFile} =
128"$Foswiki::cfg{DataDir}/$this->{web}$rcsSubDir/$this->{topic}.txt,v";
129 }
130 }
131
132 # Default to remembering changes for a month
133320145µs $Foswiki::cfg{Store}{RememberChangesFor} ||= 31 * 24 * 60 * 60;
134
135320791µs return $this;
136}
137
138=begin TML
139
140---++ ObjectMethod finish()
141Break circular references.
142
143=cut
144
145# Note to developers; please undef *all* fields in the object explicitly,
146# whether they are references or not. That way this method is "golden
147# documentation" of the live fields in the object.
148sub finish {
149 my $this = shift;
150 undef $this->{file};
151 undef $this->{rcsFile};
152 undef $this->{web};
153 undef $this->{topic};
154 undef $this->{attachment};
155}
156
157# Used in subclasses for late initialisation during object creation
158# (after the object is blessed into the subclass)
159sub init {
160 my $this = shift;
161
162 return unless $this->{topic};
163
164 unless ( $this->storedDataExists() ) {
165 if ( $this->{attachment} && !$this->isAsciiDefault() ) {
166 $this->initBinary();
167 }
168 else {
169 $this->initText();
170 }
171 }
172}
173
174# Make any missing paths on the way to this file
175sub mkPathTo {
176
177 my ( $this, $file ) = @_;
178
179 $file = _encode( Foswiki::Sandbox::untaintUnchecked($file) );
180
181 ASSERT( File::Spec->file_name_is_absolute($file) ) if DEBUG;
182
183 my ( $volume, $path, undef ) = File::Spec->splitpath($file);
184 $path = File::Spec->catpath( $volume, $path, '' );
185
186 eval {
187 File::Path::mkpath( $path, 0, $Foswiki::cfg{Store}{dirPermission} );
188 };
189 if ($@) {
190 throw Error::Simple("Rcs::Handler: failed to create ${path}: $!");
191 }
192}
193
194sub _epochToRcsDateTime {
195 my ($dateTime) = @_;
196
197 # TODO: should this be gmtime or local time?
198 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) =
199 gmtime($dateTime);
200 $year += 1900 if ( $year > 99 );
201 my $rcsDateTime = sprintf '%d.%02d.%02d.%02d.%02d.%02d',
202 ( $year, $mon + 1, $mday, $hour, $min, $sec );
203 return $rcsDateTime;
204}
205
206# filenames for lock and lease files
207sub _controlFileName {
208 my ( $this, $type ) = @_;
209
210 my $fn = $this->{file} || '';
211 $fn =~ s/txt$/$type/;
212 return $fn;
213}
214
215=begin TML
216
217---++ ObjectMethod getInfo($version) -> \%info
218
219 * =$version= if 0 or undef, or out of range (version number > number of revs) will return info about the latest revision.
220
221Returns info where version is the number of the rev for which the info was recovered, date is the date of that rev (epoch s), user is the canonical user ID of the user who saved that rev, and comment is the comment associated with the rev.
222
223Designed to be overridden by subclasses, which can call up to this method
224if simple file-based rev info is required.
225
226=cut
227
228sub getInfo {
229 my $this =
230 shift; # $version is not useful here, as we have no way to record history
231
232 # We only arrive here if the implementation getInfo can't serve the info; this
233 # will usually be because the ,v is missing or the topic cache is newer.
234
235 # If there is a .txt file, grab the TOPICINFO from it.
236 # Note that we only peek at the first line of the file,
237 # which is where a "proper" save will have left the tag.
238 my $info = {};
239 if ( $this->noCheckinPending() ) {
240
241 # TOPICINFO may be OK
242 $this->_getTOPICINFO($info);
243 }
244 elsif ( $this->revisionHistoryExists() ) {
245
246 # There is a checkin pending, and there is an rcs file.
247 # Ignore TOPICINFO
248 $info->{version} = $this->_numRevisions() + 1;
249 $info->{comment} = "pending";
250 }
251 else {
252
253 # There is a checkin pending, but no RCS file.
254 $info->{version} = 1;
255 $info->{comment} = "pending";
256 }
257 $info->{date} = $this->getTimestamp() unless defined $info->{date};
258 $info->{version} = 1 unless defined $info->{version};
259 $info->{comment} = '' unless defined $info->{comment};
260 $info->{author} ||= $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID;
261 return $info;
262}
263
264# Try and read TOPICINFO
265
# spent 181µs (98+83) within Foswiki::Store::Rcs::Handler::_getTOPICINFO which was called 2 times, avg 91µs/call: # 2 times (98µs+83µs) by Foswiki::Store::Rcs::Handler::getLatestRevisionID at line 514, avg 91µs/call
sub _getTOPICINFO {
26621µs my ( $this, $info ) = @_;
2672500ns my $f;
268
269236µs if ( open( $f, '<', $this->{file} ) ) {
27025µs local $/ = "\n";
271212µs my $ti = <$f>;
272210µs close($f);
27329µs if ( defined $ti && $ti =~ /^%META:TOPICINFO\{(.*)\}%/ ) {
27428µs274µs my $a = Foswiki::Attrs->new($1);
# spent 74µs making 2 calls to Foswiki::Attrs::new, avg 37µs/call
275
276 # Default bad revs to 1, not 0, because this is coming from
277 # a topic on disk, so we know it's a "real" rev.
27825µs29µs $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} )
# spent 9µs making 2 calls to Foswiki::Store::cleanUpRevID, avg 4µs/call
279 || 1;
28021µs $info->{date} = $a->{date};
28121µs $info->{author} = $a->{author};
28224µs $info->{comment} = $a->{comment};
283 }
284 }
285}
286
287# Check to see if there is a newer non-,v file waiting to be checked in. If there is, then
288# all rev numbers have to be incremented, as they will auto-increment when it is finally
289# checked in (usually as the result of a save). This is also used to test the validity of
290# TOPICINFO, as a pending checkin does not contain valid TOPICINFO.
291
# spent 252µs (44+208) within Foswiki::Store::Rcs::Handler::noCheckinPending which was called 2 times, avg 126µs/call: # 2 times (44µs+208µs) by Foswiki::Store::Rcs::Handler::getLatestRevisionID at line 512, avg 126µs/call
sub noCheckinPending {
2922700ns my $this = shift;
2932400ns my $isValid = 0;
294
29526µs254µs if ( !$this->storedDataExists() ) {
# spent 54µs making 2 calls to Foswiki::Store::Rcs::Handler::storedDataExists, avg 27µs/call
296 $isValid = 1; # Hmmmm......
297 }
298 else {
29924µs253µs if ( $this->revisionHistoryExists() ) {
# spent 53µs making 2 calls to Foswiki::Store::Rcs::Handler::revisionHistoryExists, avg 26µs/call
300
301# Check the time on the rcs file; is the .txt newer?
302# Danger, Will Robinson! stat isn't reliable on all file systems, though [9] is claimed to be OK
303# See perldoc perlport for more on this.
30422µs local ${^WIN32_SLOPPY_STAT} =
305 1; # don't need to open the file on Win32
30628µs254µs my $rcsTime = ( _stat( $this->{rcsFile} ) )[9];
30726µs247µs my $fileTime = ( _stat( $this->{file} ) )[9];
30822µs $isValid =
309 ( $fileTime - $rcsTime > 1 ) ? 0 : 1; # grace period of one sec
310 }
311 }
31226µs return $isValid;
313}
314
315# Must be implemented by subclasses
316sub ci {
317 die "Pure virtual method";
318}
319
320# Check that the object has a history and the .txt is consistent with that history.
321# returns true when damage was saved, returns false when there's no checkin pending
322sub savePendingCheckin {
323 my $this = shift;
324 return 0 if $this->noCheckinPending();
325
326 # the version in the TOPICINFO may not be correct. We need
327 # to check the change in and update the TOPICINFO accordingly
328 my $t = $this->readFile( $this->{file} );
329
330 # If this is a topic, adjust the TOPICINFO
331 if ( defined $this->{topic} && !defined $this->{attachment} ) {
332 my $rev =
333 $this->revisionHistoryExists() ? $this->getLatestRevisionID() : 1;
334
335 $t =~ s/^%META:TOPICINFO\{.*?\}%\n//m;
336 $t =
337 '%META:TOPICINFO{author="'
338 . $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID
339 . '" comment="autosave" date="'
340 . time()
341 . '" format="1.1" version="'
342 . $rev . '"}%' . "\n$t";
343 }
344 $this->ci( 0, $t, 'autosave',
345 $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, time() );
346
347 return 1;
348}
349
350# update the topicinfo cache
351sub _cacheMetaInfo {
352 my ( $this, $text, $comment, $user, $date, $rev ) = @_;
353
354 $user = $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID
355 unless defined $user;
356 $date = time() unless defined $date;
357
358 my $info;
359
360 # remove the previous record
361 if ( $text =~ s/^%META:TOPICINFO\{(.*)\}%\n//m ) {
362 $info = Foswiki::Attrs->new($1);
363
364 }
365 else {
366 $info = Foswiki::Attrs->new();
367 }
368
369 $info->{comment} = $comment if defined $comment && $comment ne '';
370 $info->{author} = $user;
371 $info->{date} = $date;
372 $info->{version} = $rev if defined $rev;
373 $info->{version} ||= 1;
374 $info->{format} = '1.1';
375
376 $text = "%META:TOPICINFO{" . $info->stringify . "}%\n" . $text;
377
378 return $text;
379}
380
381=begin TML
382
383---++ ObjectMethod addRevisionFromText($text, $comment, $cUID, $date)
384
385Add new revision. Replace file with text.
386 * =$text= of new revision
387 * =$comment= checkin comment
388 * =$cUID= is a cUID.
389 * =$date= in epoch seconds; may be ignored
390
391=cut
392
393sub addRevisionFromText {
394 my ( $this, $text, $comment, $user, $date ) = @_;
395 $this->init();
396
397 # Commit any out-of-band damage to .txt
398 my $rev;
399
400 # get a new rev id when we saved damage
401 if ( $this->savePendingCheckin() ) {
402 $rev = $this->getNextRevisionID();
403 }
404 $comment ||= '';
405 $text = $this->_cacheMetaInfo( $text, $comment, $user, $date, $rev );
406
407 $this->ci( 0, $text, $comment, $user, $date );
408}
409
410=begin TML
411
412---++ ObjectMethod addRevisionFromStream($fh, $comment, $cUID, $date)
413
414Add new revision. Replace file with contents of stream.
415 * =$fh= filehandle for contents of new revision
416 * =$cUID= is a cUID.
417 * =$date= in epoch seconds; may be ignored
418
419=cut
420
421sub addRevisionFromStream {
422 my ( $this, $stream, $comment, $user, $date ) = @_;
423 $this->init();
424
425 # Commit any out-of-band damage to .txt
426 $this->savePendingCheckin();
427
428 $this->ci( 1, $stream, $comment, $user, $date );
429}
430
431=begin TML
432
433---++ ObjectMethod replaceRevision($text, $comment, $user, $date)
434
435Replace the top revision.
436 * =$text= is the new revision
437 * =$date= is in epoch seconds.
438 * =$user= is a cUID.
439 * =$comment= is a string
440
441=cut
442
443sub replaceRevision {
444 my ( $this, $text, $comment, $user, $date ) = @_;
445
446 unless ( $this->noCheckinPending() ) {
447
448# As this will check in a new revision, we dump the $date and use the current time.
449# Otherwise rcs will barf at us when $date is older than the last release in the revision
450# history.
451 return $this->addRevisionFromText( $text, $comment, $user, time() );
452 }
453
454 my $rev = $this->getLatestRevisionID();
455 $text = $this->_cacheMetaInfo( $text, $comment, $user, $date, $rev );
456
457 $this->repRev( $text, $comment, $user, $date );
458}
459
460# Signature as for replaceRevision
461sub repRev {
462 die "Pure virtual method";
463}
464
465=begin TML
466
467---++ ObjectMethod getRevisionHistory() -> $iterator
468
469Get an iterator over the identifiers of revisions. Returns the most
470recent revision first.
471
472The default is to return an iterator from the current version number
473down to 1. Return rev 1 if the file exists without history. Return
474an empty iterator if the file does not exist.
475
476=cut
477
478
# spent 652µs (35+618) within Foswiki::Store::Rcs::Handler::getRevisionHistory which was called 2 times, avg 326µs/call: # 2 times (35µs+618µs) by Foswiki::Store::Rcs::Store::getRevisionHistory at line 276 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 326µs/call
sub getRevisionHistory {
47921µs my $this = shift;
480 ASSERT( $this->{file} ) if DEBUG;
48126µs267µs unless ( $this->revisionHistoryExists() ) {
# spent 67µs making 2 calls to Foswiki::Store::Rcs::Handler::revisionHistoryExists, avg 34µs/call
482 require Foswiki::ListIterator;
483 if ( $this->storedDataExists() ) {
484 return Foswiki::ListIterator->new( [1] );
485 }
486 else {
487 return Foswiki::ListIterator->new( [] );
488 }
489 }
490
491 # SMELL: what happens with the working file?
49228µs2530µs my $maxRev = $this->getLatestRevisionID();
# spent 530µs making 2 calls to Foswiki::Store::Rcs::Handler::getLatestRevisionID, avg 265µs/call
493214µs220µs return Foswiki::Iterator::NumberRangeIterator->new( $maxRev, 1 );
# spent 20µs making 2 calls to Foswiki::Iterator::NumberRangeIterator::new, avg 10µs/call
494}
495
496=begin TML
497
498---++ ObjectMethod getLatestRevisionID() -> $id
499
500Get the ID of the most recent revision. This may return undef if there have
501been no revisions committed to the store.
502
503=cut
504
505
# spent 530µs (41+489) within Foswiki::Store::Rcs::Handler::getLatestRevisionID which was called 2 times, avg 265µs/call: # 2 times (41µs+489µs) by Foswiki::Store::Rcs::Handler::getRevisionHistory at line 492, avg 265µs/call
sub getLatestRevisionID {
5062800ns my $this = shift;
50724µs256µs return 0 unless $this->storedDataExists();
# spent 56µs making 2 calls to Foswiki::Store::Rcs::Handler::storedDataExists, avg 28µs/call
508
50922µs my $info = {};
5102800ns my $rev;
511
512210µs2252µs my $checkinPending = $this->noCheckinPending() ? 0 : 1;
# spent 252µs making 2 calls to Foswiki::Store::Rcs::Handler::noCheckinPending, avg 126µs/call
51321µs unless ($checkinPending) {
51426µs2181µs $this->_getTOPICINFO($info);
# spent 181µs making 2 calls to Foswiki::Store::Rcs::Handler::_getTOPICINFO, avg 91µs/call
51521µs $rev = $info->{version};
516 }
517
5182400ns unless ( defined $rev ) {
519 $rev = $this->_numRevisions() || 1;
520 }
521
522 # If there is a pending pseudo-revision, need n+1, but only if there is
523 # an existing history
5242300ns $rev++ if $checkinPending && $this->revisionHistoryExists();
52527µs return $rev;
526}
527
528=begin TML
529
530---++ ObjectMethod getNextRevisionID() -> $id
531
532Get the ID of the next (as yet uncreated) revision. The handler is required
533to implement this because the store has to be able to embed the revision
534ID into TOPICINFO before the revision is actually created.
535
536If the file exists without revisions, then rev 1 does exist, so next rev
537should be rev 2, not rev 1, so the first change with missing history
538doesn't get merged into rev 1.
539
540=cut
541
542sub getNextRevisionID {
543 my $this = shift;
544
545 my $rev = $this->getLatestRevisionID();
546 return $rev + 1
547 if $this->noCheckinPending() || !$this->revisionHistoryExists();
548 return $rev;
549}
550
551=begin TML
552
553---++ ObjectMethod getLatestRevisionTime() -> $text
554
555Get the time of the most recent revision
556
557=cut
558
559sub getLatestRevisionTime {
560 my @e = _stat( shift->{file} );
561 return $e[9] || 0;
562}
563
564=begin TML
565
566---++ ObjectMethod getTopicNames() -> @topics
567
568Get list of all topics in a web
569 * =$web= - Web name, required, e.g. ='Sandbox'=
570Return a topic list, e.g. =( 'WebChanges', 'WebHome', 'WebIndex', 'WebNotify' )=
571
572=cut
573
574
# spent 2.78s (514ms+2.27) within Foswiki::Store::Rcs::Handler::getTopicNames which was called 40 times, avg 69.6ms/call: # 40 times (514ms+2.27s) by Foswiki::Store::Rcs::Store::eachTopic at line 590 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 69.6ms/call
sub getTopicNames {
5754020µs my $this = shift;
5764011µs my $dh;
577401.02ms40865µs opendir( $dh, _encode("$Foswiki::cfg{DataDir}/$this->{web}") )
# spent 865µs making 40 calls to Foswiki::Store::encode, avg 22µs/call
578 or return ();
579
580 # the name filter is used to ensure we don't return filenames
581 # that contain illegal characters as topic names.
5825028027.2ms my @topicList =
583150600148ms map { /^(.*)\.txt$/; $1; }
584 sort
58540174ms1003202.27s grep { !/$Foswiki::cfg{NameFilter}/ && /\.txt$/ }
# spent 2.27s making 100320 calls to Foswiki::Store::decode, avg 23µs/call
586
587 # Must _decode before applying the NameFilter and sort
588 map( _decode($_), readdir($dh) );
58940226µs closedir($dh);
590405.85ms return @topicList;
591}
592
593=begin TML
594
595---++ ObjectMethod revisionExists($rev) -> $boolean
596
597Determine if the identified revision actually exists in the object
598history.
599
600=cut
601
602sub revisionExists {
603 my ( $this, $rev ) = @_;
604
605 # Rev numbers run from 1 to numRevisions
606 my $numRevs;
607 if ( $this->noCheckinPending() ) {
608
609 # TOPICINFO may be OK
610 my $info = {};
611 $this->_getTOPICINFO($info);
612 $numRevs = $info->{version} || 1;
613 }
614 else {
615 $numRevs = $this->_numRevisions();
616 }
617
618 return $rev && $rev <= $numRevs;
619}
620
621=begin TML
622
623---++ ObjectMethod getWebNames() -> @webs
624
625Gets a list of names of subwebs in the current web
626
627=cut
628
629
# spent 16.4ms (14.9+1.54) within Foswiki::Store::Rcs::Handler::getWebNames which was called 26 times, avg 632µs/call: # 26 times (14.9ms+1.54ms) by Foswiki::Store::Rcs::Store::eachWeb at line 604 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 632µs/call
sub getWebNames {
630265µs my $this = shift;
6312611µs my $dir = $Foswiki::cfg{DataDir};
6322621µs $dir .= '/' . $this->{web} if defined $this->{web};
633263µs my @tmpList;
634263µs my $dh;
6352610µs my $webid = "$Foswiki::cfg{WebPrefsTopicName}.txt";
6362660µs26558µs my $edir = _encode($dir);
# spent 558µs making 26 calls to Foswiki::Store::encode, avg 21µs/call
63726573µs if ( opendir( $dh, $edir ) ) {
6382461µs48985µs @tmpList = map {
# spent 530µs making 24 calls to Foswiki::Store::decode, avg 22µs/call # spent 455µs making 24 calls to Foswiki::Sandbox::untaint, avg 19µs/call
639245364.67ms Foswiki::Sandbox::untaint( _decode($_),
640 \&Foswiki::Sandbox::validateWebName )
641 }
642
643 # The -e on the web preferences is used in preference to a
644 # -d to avoid having to validate the web name each time. Since
645 # the definition of a Web in this handler is "a directory with a
646 # WebPreferences.txt in it", this works.
647269.27ms grep { !/\./ && -e "$edir/$_/$webid" } readdir($dh);
6482665µs closedir($dh);
649 }
650
65126127µs return @tmpList;
652}
653
654=begin TML
655
656---++ ObjectMethod moveWeb( $newWeb )
657
658Move a web.
659
660=cut
661
662sub moveWeb {
663 my ( $this, $newWeb ) = @_;
664 $this->_moveFile(
665 "$Foswiki::cfg{DataDir}/$this->{web}",
666 "$Foswiki::cfg{DataDir}/$newWeb"
667 );
668 if ( _e "$Foswiki::cfg{PubDir}/$this->{web}" ) {
669 $this->_moveFile(
670 "$Foswiki::cfg{PubDir}/$this->{web}",
671 "$Foswiki::cfg{PubDir}/$newWeb"
672 );
673 }
674}
675
676=begin TML
677
678---++ ObjectMethod getRevision($version) -> ($text, $isLatest)
679
680 * =$version= if 0 or undef, or out of range (version number > number of revs) will return the latest revision.
681
682Get the text of the given revision, and a flag indicating if this is the
683most recent revision.
684
685Designed to be overridden by subclasses, which can call up to this method
686if the main file revision is required.
687
688Note: does *not* handle the case where the latest does not exist but a history
689does; that is regarded as a "non-topic".
690
691=cut
692
693
# spent 3.21s (331ms+2.88) within Foswiki::Store::Rcs::Handler::getRevision which was called 26327 times, avg 122µs/call: # 26327 times (331ms+2.88s) by Foswiki::Store::Rcs::RcsWrapHandler::getRevision at line 261 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/RcsWrapHandler.pm, avg 122µs/call
sub getRevision {
6942632710.7ms my ($this) = @_;
69526327167ms526542.88s if ( $this->storedDataExists() ) {
# spent 1.85s making 26327 calls to Foswiki::Store::Rcs::Handler::readFile, avg 70µs/call # spent 1.03s making 26327 calls to Foswiki::Store::Rcs::Handler::storedDataExists, avg 39µs/call
696 return ( $this->readFile( $this->{file} ), 1 );
697 }
698 return ( undef, undef );
699}
700
701=begin TML
702
703---++ ObjectMethod storedDataExists() -> $boolean
704
705Establishes if there is stored data associated with this handler.
706
707=cut
708
709
# spent 2.31s (416ms+1.89) within Foswiki::Store::Rcs::Handler::storedDataExists which was called 52956 times, avg 44µs/call: # 26327 times (224ms+1.04s) by Foswiki::Store::Rcs::Store::readTopic at line 112 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 48µs/call # 26327 times (191ms+841ms) by Foswiki::Store::Rcs::Handler::getRevision at line 695, avg 39µs/call # 197 times (772µs+5.90ms) by Foswiki::Store::Rcs::Store::topicExists at line 538 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 34µs/call # 100 times (405µs+3.22ms) by Foswiki::Store::Rcs::Store::webExists at line 527 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 36µs/call # 2 times (7µs+49µs) by Foswiki::Store::Rcs::Handler::getLatestRevisionID at line 507, avg 28µs/call # 2 times (6µs+48µs) by Foswiki::Store::Rcs::Handler::noCheckinPending at line 295, avg 27µs/call # once (4µs+33µs) by Foswiki::Store::Rcs::Store::attachmentExists at line 232 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm
sub storedDataExists {
7105295612.3ms my $this = shift;
7115295621.4ms return 0 unless $this->{file};
71252956273ms529561.89s return _e $this->{file};
713}
714
715=begin TML
716
717---++ ObjectMethod revisionHistoryExists() -> $boolean
718
719Establishes if htere is history data associated with this handler.
720
721=cut
722
723
# spent 2.41s (451ms+1.96) within Foswiki::Store::Rcs::Handler::revisionHistoryExists which was called 52658 times, avg 46µs/call: # 26327 times (200ms+1.07s) by Foswiki::Store::Rcs::Store::readTopic at line 132 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 48µs/call # 26327 times (251ms+893ms) by Foswiki::Store::Rcs::RcsWrapHandler::getRevision at line 261 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/RcsWrapHandler.pm, avg 43µs/call # 2 times (7µs+60µs) by Foswiki::Store::Rcs::Handler::getRevisionHistory at line 481, avg 34µs/call # 2 times (7µs+46µs) by Foswiki::Store::Rcs::Handler::noCheckinPending at line 299, avg 26µs/call
sub revisionHistoryExists {
7245265814.1ms my $this = shift;
7255265821.0ms return 0 unless $this->{rcsFile};
72652658269ms526581.96s return _e $this->{rcsFile};
727}
728
729=begin TML
730
731---++ ObjectMethod restoreLatestRevision( $cUID )
732
733Restore the plaintext file from the revision at the head.
734
735=cut
736
737sub restoreLatestRevision {
738 my ( $this, $cUID ) = @_;
739
740 my $rev = $this->getLatestRevisionID();
741 my ($text) = $this->getRevision($rev);
742
743 # If there is no ,v, create it
744 unless ( $this->revisionHistoryExists() ) {
745 $this->addRevisionFromText( $text, "restored", $cUID, time() );
746 }
747 else {
748 $this->saveFile( $this->{file}, $text );
749 }
750}
751
752=begin TML
753
754---++ ObjectMethod remove()
755
756Destroy, utterly. Remove the data and attachments in the web.
757
758Use with great care! No backup is taken!
759
760=cut
761
762sub remove {
763 my $this = shift;
764
765 if ( !$this->{topic} ) {
766
767 # Web
768 _rmtree( _encode "$Foswiki::cfg{DataDir}/$this->{web}" );
769 _rmtree( _encode "$Foswiki::cfg{PubDir}/$this->{web}" );
770 }
771 else {
772
773 # Topic or attachment
774 _unlink( $this->{file} );
775 _unlink( $this->{rcsFile} );
776 if ( !$this->{attachment} ) {
777 _rmtree(
778 _encode "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}" );
779 }
780 }
781}
782
783=begin TML
784
785---++ ObjectMethod moveTopic( $store, $newWeb, $newTopic )
786
787Move/rename a topic.
788
789=cut
790
791sub moveTopic {
792 my ( $this, $store, $newWeb, $newTopic ) = @_;
793
794 ASSERT( $store->isa('Foswiki::Store') ) if DEBUG;
795
796 my $oldWeb = $this->{web};
797 my $oldTopic = $this->{topic};
798
799 # Move data file
800 my $new = $store->getHandler( $newWeb, $newTopic );
801 $this->_moveFile( $this->{file}, $new->{file} );
802
803 # Move history
804 $this->mkPathTo( $new->{rcsFile} );
805 if ( $this->revisionHistoryExists() ) {
806 $this->_moveFile( $this->{rcsFile}, $new->{rcsFile} );
807 }
808
809 # Move attachments
810 my $from = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}";
811 if ( _e $from ) {
812 my $to = "$Foswiki::cfg{PubDir}/$new->{web}/$new->{topic}";
813 $this->_moveFile( $from, $to );
814 }
815}
816
817=begin TML
818
819---++ ObjectMethod copyTopic( $store, $newWeb, $newTopic )
820
821Copy a topic.
822
823=cut
824
825sub copyTopic {
826 my ( $this, $store, $newWeb, $newTopic ) = @_;
827
828 ASSERT( $store->isa('Foswiki::Store') ) if DEBUG;
829
830 my $oldWeb = $this->{web};
831 my $oldTopic = $this->{topic};
832
833 my $new = $store->getHandler( $newWeb, $newTopic );
834
835 $this->_copyFile( $this->{file}, $new->{file} );
836 if ( $this->revisionHistoryExists() ) {
837 $this->_copyFile( $this->{rcsFile}, $new->{rcsFile} );
838 }
839
840 my $dh;
841 if (
842 opendir(
843 $dh, _encode("$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}")
844 )
845 )
846 {
847 for my $att ( grep { !/^\./ } readdir $dh ) {
848 $att = Foswiki::Sandbox::untaint( $att,
849 \&Foswiki::Sandbox::validateAttachmentName );
850 my $oldAtt =
851 $store->getHandler( $this->{web}, $this->{topic}, $att );
852 $oldAtt->copyAttachment( $store, $newWeb, $newTopic );
853 }
854
855 closedir $dh;
856 }
857}
858
859=begin TML
860
861---++ ObjectMethod moveAttachment( $store, $newWeb, $newTopic, $newAttachment )
862
863Move an attachment from one topic to another. The name is retained.
864
865=cut
866
867sub moveAttachment {
868 my ( $this, $store, $newWeb, $newTopic, $newAttachment ) = @_;
869
870 ASSERT( $store->isa('Foswiki::Store') ) if DEBUG;
871
872 # FIXME might want to delete old directories if empty
873 my $new = $store->getHandler( $newWeb, $newTopic, $newAttachment );
874
875 $this->_moveFile( $this->{file}, $new->{file} );
876
877 if ( $this->revisionHistoryExists() ) {
878 $this->_moveFile( $this->{rcsFile}, $new->{rcsFile} );
879 }
880}
881
882=begin TML
883
884---++ ObjectMethod copyAttachment( $store, $newWeb, $newTopic, $newAttachment )
885
886Copy an attachment from one topic to another. The name is retained unless
887$newAttachment is defined.
888
889=cut
890
891sub copyAttachment {
892 my ( $this, $store, $newWeb, $newTopic, $attachment ) = @_;
893
894 ASSERT( $store->isa('Foswiki::Store') ) if DEBUG;
895
896 my $oldWeb = $this->{web};
897 my $oldTopic = $this->{topic};
898 $attachment ||= $this->{attachment};
899
900 my $new = $store->getHandler( $newWeb, $newTopic, $attachment );
901
902 $this->_copyFile( $this->{file}, $new->{file} );
903
904 if ( $this->revisionHistoryExists() ) {
905 $this->_copyFile( $this->{rcsFile}, $new->{rcsFile} );
906 }
907}
908
909=begin TML
910
911---++ ObjectMethod isAsciiDefault ( ) -> $boolean
912
913Check if this file type is known to be an ascii type file.
914
915=cut
916
917sub isAsciiDefault {
918 my $this = shift;
919 return ( $this->{attachment} =~ /$Foswiki::cfg{RCS}{asciiFileSuffixes}/ );
920}
921
922=begin TML
923
924---++ ObjectMethod setLock($lock, $cUID)
925
926Set a lock on the topic, if $lock, otherwise clear it.
927$cUID is a cUID.
928
929SMELL: there is a tremendous amount of potential for race
930conditions using this locking approach.
931
932It would be nice to use flock to do this, but the API is unreliable
933(doesn't work on all platforms)
934
935=cut
936
937sub setLock {
938 my ( $this, $lock, $cUID ) = @_;
939
940 my $filename = $this->_controlFileName('lock');
941 ASSERT($filename);
942 if ($lock) {
943 my $lockTime = time();
944 ASSERT($filename);
945 $this->saveFile( $filename, $cUID . "\n" . $lockTime );
946 }
947 elsif ( _e $filename ) {
948 _unlink($filename)
949 || throw Error::Simple(
950 'Rcs::Handler: failed to delete ' . $filename . ': ' . $! );
951 }
952}
953
954=begin TML
955
956---++ ObjectMethod isLocked( ) -> ($cUID, $time)
957
958See if a lock exists. Return the lock user and lock time if it does.
959
960=cut
961
962sub isLocked {
963 my $this = shift;
964
965 my $filename = $this->_controlFileName('lock');
966 if ( _e $filename ) {
967 my $t = $this->readFile($filename);
968 return split( /\s+/, $t, 2 );
969 }
970 return ( undef, undef );
971}
972
973=begin TML
974
975---++ ObjectMethod setLease( $lease )
976
977 * =$lease= reference to lease hash, or undef if the existing lease is to be cleared.
978
979Set an lease on the topic.
980
981=cut
982
983sub setLease {
984 my ( $this, $lease ) = @_;
985
986 my $filename = $this->_controlFileName('lease');
987 if ($lease) {
988 $this->saveFile( $filename, join( "\n", %$lease ) );
989 }
990 elsif ( _e $filename ) {
991 _unlink($filename)
992 || throw Error::Simple(
993 'Rcs::Handler: failed to delete ' . $filename . ': ' . $! );
994 }
995}
996
997=begin TML
998
999---++ ObjectMethod getLease() -> $lease
1000
1001Get the current lease on the topic.
1002
1003=cut
1004
1005sub getLease {
1006 my ($this) = @_;
1007
1008 my $filename = $this->_controlFileName('lease');
1009 if ( _e $filename ) {
1010
1011 my $t = $this->readFile($filename);
1012 my $lease = { split( /\r?\n/, $t ) };
1013 return $lease;
1014 }
1015 return;
1016}
1017
1018=begin TML
1019
1020---++ ObjectMethod removeSpuriousLeases( $web )
1021
1022Remove leases that are not related to a topic. These can get left behind in
1023some store implementations when a topic is created, but never saved.
1024
1025=cut
1026
1027sub removeSpuriousLeases {
1028 my ($this) = @_;
1029 my $web = _encode("$Foswiki::cfg{DataDir}/$this->{web}");
1030 if ( opendir( my $W, $web ) ) {
1031 foreach my $f ( readdir($W) ) {
1032 my $file = $web . '/' . $f;
1033 if ( $file =~ /^(.*)\.lease$/ ) {
1034 if ( !-e "$1.txt,v" ) {
1035 unlink("$1.lease");
1036 }
1037 }
1038 }
1039 closedir($W);
1040 }
1041}
1042
1043sub test {
1044 my ( $this, $test ) = @_;
1045 my $f = _encode( $this->{file} );
1046 return eval "-$test '$f'";
1047}
1048
1049# Used by subclasses
1050sub saveStream {
1051 my ( $this, $fh ) = @_;
1052
1053 ASSERT($fh) if DEBUG;
1054
1055 $this->mkPathTo( $this->{file} );
1056 my $F;
1057 my $efile = _encode( $this->{file} );
1058 open( $F, '>', $efile )
1059 || throw Error::Simple(
1060 'Rcs::Handler: open ' . $this->{file} . ' failed: ' . $! );
1061 binmode($F)
1062 || throw Error::Simple(
1063 'Rcs::Handler: failed to binmode ' . $this->{file} . ': ' . $! );
1064 my $text;
1065
1066 while ( read( $fh, $text, 1024 ) ) {
1067 print $F $text;
1068 }
1069 close($F)
1070 || throw Error::Simple(
1071 'Rcs::Handler: close ' . $this->{file} . ' failed: ' . $! );
1072
1073 chmod( $Foswiki::cfg{Store}{filePermission}, $efile );
1074}
1075
1076sub _copyFile {
1077 my ( $this, $from, $to ) = @_;
1078
1079 $this->mkPathTo($to);
1080 unless ( File::Copy::copy( _encode($from), _encode($to) ) ) {
1081 throw Error::Simple(
1082 'Rcs::Handler: copy ' . $from . ' to ' . $to . ' failed: ' . $! );
1083 }
1084}
1085
1086sub _moveFile {
1087 my ( $this, $from, $to ) = @_;
1088 ASSERT( _e $from ) if DEBUG;
1089 $this->mkPathTo($to);
1090 unless ( File::Copy::move( _encode($from), _encode($to) ) ) {
1091 throw Error::Simple(
1092 'Rcs::Handler: move ' . $from . ' to ' . $to . ' failed: ' . $! );
1093 }
1094}
1095
1096# Used by subclasses
1097sub saveFile {
1098 my ( $this, $name, $text ) = @_;
1099 $this->mkPathTo($name);
1100 my $fh;
1101 open( $fh, '>', _encode($name) )
1102 or throw Error::Simple(
1103 'Rcs::Handler: failed to create file ' . $name . ': ' . $! );
1104 flock( $fh, LOCK_EX )
1105 or throw Error::Simple(
1106 'Rcs::Handler: failed to lock file ' . $name . ': ' . $! );
1107 binmode($fh)
1108 or throw Error::Simple(
1109 'Rcs::Handler: failed to binmode ' . $name . ': ' . $! );
1110 print $fh $text
1111 or throw Error::Simple(
1112 'Rcs::Handler: failed to print into ' . $name . ': ' . $! );
1113 close($fh)
1114 or throw Error::Simple(
1115 'Rcs::Handler: failed to close file ' . $name . ': ' . $! );
1116 return;
1117}
1118
1119# Used by subclasses
1120
# spent 1.85s (1.20+650ms) within Foswiki::Store::Rcs::Handler::readFile which was called 26327 times, avg 70µs/call: # 26327 times (1.20s+650ms) by Foswiki::Store::Rcs::Handler::getRevision at line 695, avg 70µs/call
sub readFile {
11212632714.0ms my ( $this, $name ) = @_;
1122 ASSERT($name) if DEBUG;
1123263272.68ms my $data;
1124263272.21ms my $IN_FILE;
1125
1126 # Note: no IO layer; we want to trap encoding errors
112726327458ms26327650ms if ( open( $IN_FILE, '<', _encode($name) ) ) {
# spent 650ms making 26327 calls to Foswiki::Store::encode, avg 25µs/call
11282632722.7ms binmode($IN_FILE);
11292632758.4ms local $/ = undef;
113026327316ms $data = <$IN_FILE>;
113126327163ms close($IN_FILE);
1132 }
113326327217ms return $data;
1134}
1135
1136# Used by subclasses
1137sub mkTmpFilename {
1138 my $tmpdir = File::Spec->tmpdir();
1139 my $file = _mktemp( 'foswikiAttachmentXXXXXX', $tmpdir );
1140 return File::Spec->catfile( $tmpdir, _encode($file) );
1141}
1142
1143# Adapted from CPAN - File::MkTemp
1144sub _mktemp {
1145 my ( $template, $dir, $ext, $keepgen, $lookup );
1146 my ( @template, @letters );
1147
1148 ASSERT( @_ == 1 || @_ == 2 || @_ == 3 ) if DEBUG;
1149
1150 ( $template, $dir, $ext ) = map { _encode($_) } @_;
1151 @template = split( //, $template );
1152
1153 ASSERT( $template =~ /XXXXXX$/ ) if DEBUG;
1154
1155 if ($dir) {
1156 ASSERT( -e $dir ) if DEBUG;
1157 }
1158
1159 @letters =
1160 split( //, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' );
1161
1162 $keepgen = 1;
1163
1164 while ($keepgen) {
1165 for ( my $i = $#template ; $i >= 0 && ( $template[$i] eq 'X' ) ; $i-- )
1166 {
1167 $template[$i] = $letters[ int( rand 52 ) ];
1168 }
1169
1170 undef $template;
1171
1172 $template = pack 'a' x @template, @template;
1173
1174 $template = $template . $ext if ($ext);
1175
1176 if ($dir) {
1177 $lookup = File::Spec->catfile( $dir, $template );
1178 $keepgen = 0 unless ( -e $lookup );
1179 }
1180 else {
1181 $keepgen = 0;
1182 }
1183
1184 next if $keepgen == 0;
1185 }
1186
1187 return ($template);
1188}
1189
1190sub _fromcs {
1191 my $s = shift;
1192}
1193
1194# remove a directory and all subdirectories
1195sub _rmtree {
1196 my $root = shift;
1197 my $D;
1198
1199 if ( opendir( $D, $root ) ) {
1200 foreach my $entry ( grep { !/^\.+$/ } readdir($D) ) {
1201 $entry =~ /^(.*)$/; # untaint
1202 $entry = $root . '/' . $1;
1203
1204 if ( -d $entry ) {
1205 _rmtree($entry);
1206 }
1207 elsif ( !unlink($entry) && -e $entry ) {
1208 if ( $Foswiki::cfg{OS} ne 'WINDOWS' ) {
1209 throw Error::Simple( 'Rcs::Handler: Failed to delete file '
1210 . _decode($entry) . ': '
1211 . $! );
1212 }
1213 else {
1214
1215 # Windows sometimes fails to delete files when
1216 # subprocesses haven't exited yet, because the
1217 # subprocess still has the file open. Live with it.
1218 print STDERR 'WARNING: Failed to delete file ',
1219 _decode($entry), ": $!\n";
1220 }
1221 }
1222 }
1223 closedir($D);
1224
1225 if ( !rmdir($root) ) {
1226 if ( $Foswiki::cfg{OS} ne 'WINDOWS' ) {
1227
1228 #print `ls -lR $root`;
1229 throw Error::Simple( 'Rcs::Handler: Failed to delete '
1230 . _decode($root) . ': '
1231 . $! );
1232 }
1233 else {
1234 print STDERR 'WARNING: Failed to delete '
1235 . _decode($root) . ': '
1236 . $!,
1237 "\n";
1238 }
1239 }
1240 }
1241}
1242
1243{
1244
1245 # Package that ties a filehandle to a memory string for reading
12461500ns package Foswiki::Store::_MemoryFile;
1247
1248 sub TIEHANDLE {
1249 my ( $class, $data ) = @_;
1250 return
1251 bless( { data => $data, size => length($data), ptr => 0 }, $class );
1252 }
1253
1254 sub READ {
1255 my $this = shift;
1256 my ( undef, $len, $offset ) = @_;
1257 if ( $this->{size} - $this->{ptr} < $len ) {
1258 $len = $this->{size} - $this->{ptr};
1259 }
1260 return 0 unless $len;
1261 $_[0] = substr( $this->{data}, $this->{ptr}, $len );
1262 $this->{ptr} += $len;
1263 return $len;
1264 }
1265
1266 sub READLINE {
1267 my $this = shift;
1268 return if $this->{ptr} == $this->{size};
1269 return substr( $this->{data}, $this->{ptr} ) if !defined $/;
1270 my $start = $this->{ptr};
1271 while ( $this->{ptr} < $this->{size}
1272 && substr( $this->{data}, $this->{ptr}, 1 ) ne $/ )
1273 {
1274 $this->{ptr}++;
1275 }
1276 $this->{ptr}++ if $this->{ptr} < $this->{size};
1277 return substr( $this->{data}, $start, $this->{ptr} - $start );
1278 }
1279
1280 sub CLOSE {
1281 my $this = shift;
1282 $this->{data} = undef;
1283 }
1284}
1285
1286=begin TML
1287
1288---++ ObjectMethod openStream($mode, %opts) -> $fh
1289
1290Opens a file handle onto the store. This method is primarily to
1291support virtual file systems.
1292
1293=$mode= can be '&lt;', '&gt;' or '&gt;&gt;' for read, write, and append
1294respectively. %
1295
1296=%opts= can take different settings depending on =$mode=.
1297 * =$mode='&lt;'=
1298 * =version= - revision of the object to open e.g. =version => 6=
1299 Default behaviour is to return the latest revision. Note that it is
1300 much more efficient to pass undef than to pass the number of the
1301 latest revision.
1302 * =$mode='&gt;'= or ='&gt;&gt;'
1303 * no options
1304
1305=cut
1306
1307sub openStream {
1308 my ( $this, $mode, %opts ) = @_;
1309 my $stream;
1310 if ( $mode eq '<' && $opts{version} ) {
1311
1312 # Bulk load the revision and tie a filehandle
1313 require Symbol;
1314 $stream = Symbol::gensym; # create an anonymous glob
1315 tie( *$stream, 'Foswiki::Store::_MemoryFile',
1316 $this->getRevision( $opts{version} ) );
1317 }
1318 else {
1319 if ( $mode =~ />/ ) {
1320 $this->mkPathTo( $this->{file} );
1321 }
1322 if ( _d $this->{file} ) {
1323 throw Error::Simple( 'Rcs::Handler: stream open '
1324 . $this->{file}
1325 . ' failed: '
1326 . 'Read requested on directory.' );
1327 }
1328 unless ( open( $stream, $mode, _encode $this->{file} ) ) {
1329 throw Error::Simple( 'Rcs::Handler: stream open '
1330 . $this->{file}
1331 . ' failed: '
1332 . $! );
1333 }
1334 binmode $stream;
1335 }
1336 return $stream;
1337}
1338
1339# as long as stat is defined, return an emulated set of attributes for that
1340# attachment.
1341sub _constructAttributesForAutoAttached {
1342 my ( $file, $stat ) = @_;
1343
1344 my %pairs = (
1345 name => $file,
1346 path => $file,
1347 version => '1',
1348 size => $stat->[7],
1349 date => $stat->[9],
1350
1351# user => 'UnknownUser', #safer _not_ to default - Foswiki will fill it in when it needs to
1352 comment => '',
1353 attr => '',
1354 autoattached => '1'
1355 );
1356
1357 if ( $#$stat > 0 ) {
1358 return \%pairs;
1359 }
1360 else {
1361 return;
1362 }
1363}
1364
1365# ---++ ObjectMethod synchroniseAttachmentsList(\@old) -> @new
1366#
1367# PACKAGE PRIVATE
1368#
1369# Synchronise the attachment list from meta-data with what's actually
1370# stored in the DB. Returns an ARRAY of FILEATTACHMENTs. These can be
1371# put in the new tom.
1372#
1373# This function is only called when the {RCS}{AutoAttachPubFiles} configuration
1374# option is set.
1375
1376# IDEA On Windows machines where the underlying filesystem can store arbitary
1377# meta data against files, this might replace/fulfil the COMMENT purpose
1378#
1379# TODO consider logging when things are added to metadata
1380
1381sub synchroniseAttachmentsList {
1382 my ( $this, $attachmentsKnownInMeta ) = @_;
1383
1384 my %filesListedInPub = $this->_getAttachmentStats();
1385 my %filesListedInMeta = ();
1386
1387 # You need the following lines if you want metadata to supplement
1388 # the filesystem
1389 if ( defined $attachmentsKnownInMeta ) {
1390 %filesListedInMeta =
1391 map { $_->{name} => $_ } @$attachmentsKnownInMeta;
1392 }
1393
1394 foreach my $file ( keys %filesListedInPub ) {
1395 if ( $filesListedInMeta{$file}
1396 && $filesListedInMeta{$file}{date} !=
1397 $filesListedInPub{$file}{date} )
1398 {
1399 # File timestamp of existing file has changed.
1400 # Bring forward any missing yet wanted attributes
1401 foreach my $field (qw(comment attr user version)) {
1402 if ( $filesListedInMeta{$file}{$field} ) {
1403 $filesListedInPub{$file}{$field} =
1404 $filesListedInMeta{$file}{$field};
1405 if ( $field eq 'version' ) {
1406 $filesListedInPub{$file}{$field}++;
1407 }
1408 }
1409 }
1410 }
1411 else {
1412 $filesListedInPub{$file} = $filesListedInMeta{$file}
1413 if ( $filesListedInMeta{$file} );
1414 }
1415 }
1416
1417 # A comparison of the keys of the $filesListedInMeta and %filesListedInPub
1418 # would show files that were in Meta but have disappeared from Pub.
1419
1420 # Do not change this from array to hash, you would lose the
1421 # proper attachment sequence
1422 my @deindexedBecauseMetaDoesnotIndexAttachments = values(%filesListedInPub);
1423
1424 return @deindexedBecauseMetaDoesnotIndexAttachments;
1425}
1426
1427=begin TML
1428
1429---++ ObjectMethod getAttachmentList() -> @list
1430
1431Get list of attachment names actually stored for topic.
1432
1433=cut
1434
1435sub getAttachmentList {
1436 my $this = shift;
1437 my $dir = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}";
1438 my $dh;
1439 my $ed = _encode($dir);
1440 opendir( $dh, $ed ) || return ();
1441 my @files =
1442 map { _decode($_) }
1443 grep { !/^[.*_]/ && !/,v$/ && -f "$ed/$_" } readdir($dh);
1444 closedir($dh);
1445 return @files;
1446}
1447
1448# returns {} of filename => { key => value, key2 => value }
1449# for any given web, topic
1450sub _getAttachmentStats {
1451 my $this = shift;
1452 my %attachmentList = ();
1453 my $dir = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}";
1454 foreach my $attachment ( $this->getAttachmentList() ) {
1455 my @stat = _stat( $dir . "/" . $attachment );
1456 $attachmentList{$attachment} =
1457 _constructAttributesForAutoAttached( $attachment, \@stat );
1458 }
1459 return %attachmentList;
1460}
1461
1462sub _dirForTopicAttachments {
1463 my ( $web, $topic ) = @_;
1464}
1465
1466=begin TML
1467
1468---++ ObjectMethod stringify()
1469
1470Generate string representation for debugging
1471
1472=cut
1473
1474sub stringify {
1475 my $this = shift;
1476 my @reply;
1477 foreach my $key (qw(web topic attachment file rcsFile)) {
1478 if ( defined $this->{$key} ) {
1479 push( @reply, "$key=$this->{$key}" );
1480 }
1481 }
1482 return join( ',', @reply );
1483}
1484
1485# Chop out recognisable path components to prevent hacking based on error
1486# messages
1487sub hidePath {
1488 my ( $this, $erf ) = @_;
1489 $erf =~ s#.*(/\w+/\w+\.[\w,]*)$#...$1#;
1490 return $erf;
1491}
1492
1493# ObjectMethod getTimestamp() -> $integer
1494# Get the timestamp of the file
1495# Returns 0 if no file, otherwise epoch seconds
1496# Used in subclasses
1497
1498sub getTimestamp {
1499 my ($this) = @_;
1500 ASSERT( $this->{file} ) if DEBUG;
1501
1502 my $date = 0;
1503 if ( $this->storedDataExists() ) {
1504
1505 # If the stat fails, stamp it with some arbitrary static
1506 # time in the past (00:40:05 on 5th Jan 1989)
1507 $date = ( stat $this->{file} )[9] || 600000000;
1508 }
1509 return $date;
1510}
1511
1512sub recordChange {
1513 my ( $this, %args ) = @_;
1514 if (DEBUG) {
1515 if ( $Foswiki::Store::STORE_FORMAT_VERSION < 1.2 ) {
1516 ASSERT( ( caller || 'undef' ) eq __PACKAGE__ );
1517 }
1518 else {
1519 ASSERT( ( caller || 'undef' ) ne __PACKAGE__ );
1520 }
1521 ASSERT( $args{verb} );
1522 ASSERT( $args{cuid} );
1523 ASSERT( $args{revision} );
1524 ASSERT( $args{path} );
1525 ASSERT( !defined $args{more} );
1526 ASSERT( !defined $args{user} );
1527 }
1528
1529 # my ( $meta, $cUID, $rev, $more ) = @_;
1530 # $more ||= '';
1531
1532 my $webpath = "$Foswiki::cfg{DataDir}/$this->{web}";
1533
1534 # Can't log changes in a non-existent web
1535 return unless ( _d $webpath );
1536
1537 my $text = '';
1538 my $t = time;
1539
1540 my @changes = $this->readChanges();
1541 my $cutoff = $t - $Foswiki::cfg{Store}{RememberChangesFor};
1542 while ( scalar(@changes) && $changes[0]->{time} < $cutoff ) {
1543 shift(@changes);
1544 }
1545
1546 # Add the new change to the end of the file
1547 $args{time} = time;
1548 push( @changes, \%args );
1549
1550 if ( $Foswiki::cfg{RCS}{TabularChangeFormat} ) {
1551 $args{topic} ||= $this->{topic};
1552 foreach (@changes) {
1553 my $hash = $_;
1554 $_ = [
1555 $hash->{topic} || '?',
1556 $hash->{cuid} || '?',
1557 $hash->{time} || '?',
1558 $hash->{revision} || '?',
1559 $json->encode($hash) || '?'
1560 ];
1561 }
1562
1563 $text = join( "\n", map { join( "\t", @$_ ) } @changes );
1564 }
1565 else {
1566 $text = $json->encode( \@changes );
1567 }
1568 my $file = "$Foswiki::cfg{DataDir}/$this->{web}/.changes";
1569 $this->saveFile( $file, $text );
1570}
1571
1572sub readChanges {
1573 my ($this) = @_;
1574
1575 my $file = "$Foswiki::cfg{DataDir}/$this->{web}/.changes";
1576 return () unless ( -r _encode($file) );
1577
1578 my $all_lines =
1579 Foswiki::Sandbox::untaintUnchecked( $this->readFile($file) );
1580
1581 # Look at the first line to deduce format
1582 if ( $all_lines =~ /^\[/s ) {
1583 my $changes;
1584 eval { $changes = $json->decode($all_lines); };
1585 print STDERR "Corrupt $file: $@\n" if ($@);
1586
1587 foreach my $entry (@$changes) {
1588 if ( $entry->{path} && $entry->{path} =~ /^(.*)\.(.*)$/ ) {
1589 $entry->{topic} = $2;
1590 }
1591 elsif ( $entry->{oldpath} && $entry->{oldpath} =~ /^(.*)\.(.*)$/ ) {
1592 $entry->{topic} = $2;
1593 }
1594 $entry->{user} =
1595 $Foswiki::Plugins::SESSION
1596 ? $Foswiki::Plugins::SESSION->{users}
1597 ->getWikiName( $entry->{cuid} )
1598 : $entry->{cuid};
1599 $entry->{more} =
1600 ( $entry->{minor} ? 'minor ' : '' ) . ( $entry->{comment} || '' );
1601 }
1602 return @$changes;
1603 }
1604
1605 # Decode the mess that was the old changes format
1606 my @changes;
1607 foreach my $line ( split( /[\r\n]+/, $all_lines ) ) {
1608 my @row = split( /\t/, $line );
1609
1610 # Old (pre 1.2) format
1611
1612 # Create a hash for this line
1613 my %row;
1614
1615 $row{topic} = Foswiki::Sandbox::untaintUnchecked( shift(@row) ) || '?';
1616 $row{user} = shift(@row) || '?';
1617 $row{time} = shift(@row) || 0;
1618 $row{revision} = shift(@row) || 1;
1619 $row{more} = shift(@row) || '';
1620
1621 # Try and decode 'more', for compatibility mode
1622 my $ok = 0;
1623 if ( $row{more} ) {
1624 eval {
1625 my $decoded = $json->decode( $row{more} );
1626 while ( my ( $k, $v ) = each %$decoded ) {
1627 $row{$k} = $v;
1628 }
1629 $ok = 1;
1630 };
1631 }
1632 if ( !$ok ) {
1633
1634 # Couldn't decode more as JSON. Fill in 1.2 fields
1635 if ( $row{revision} > 1 ) {
1636 $row{verb} = 'update';
1637 }
1638 else {
1639 $row{verb} = 'insert';
1640 }
1641 $row{minor} = ( $row{more} =~ /minor/ );
1642 $row{cuid} = $row{user};
1643 $row{path} = $this->{web};
1644 $row{path} .= ".$row{topic}" if $row{topic};
1645 $row{comment} = $row{more};
1646 if ( $row{more} =~ /Moved from (\w+)/ ) {
1647 $row{oldpath} = $1;
1648 }
1649 if ( $row{more} =~ /Deleted attachment (\S+)/ ) {
1650 $row{attachment} = $1;
1651 }
1652 }
1653 push( @changes, \%row );
1654 }
1655 return @changes;
1656}
1657
165814µs1;
1659
1660__END__