Filename | /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Handler.pm |
Statements | Executed 1048004 statements in 3.99s |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
26327 | 1 | 1 | 1.20s | 1.85s | readFile | Foswiki::Store::Rcs::Handler::
105614 | 2 | 1 | 1.06s | 3.85s | __ANON__[:63] | Foswiki::Store::Rcs::Handler::
40 | 1 | 1 | 514ms | 2.78s | getTopicNames | Foswiki::Store::Rcs::Handler::
52658 | 4 | 3 | 451ms | 2.41s | revisionHistoryExists | Foswiki::Store::Rcs::Handler::
52956 | 7 | 2 | 416ms | 2.31s | storedDataExists | Foswiki::Store::Rcs::Handler::
26327 | 1 | 1 | 331ms | 3.21s | getRevision | Foswiki::Store::Rcs::Handler::
26693 | 1 | 1 | 237ms | 261ms | new | Foswiki::Store::Rcs::Handler::
26 | 1 | 1 | 14.9ms | 16.4ms | getWebNames | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 1.51ms | 3.73ms | BEGIN@50 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 369µs | 454µs | BEGIN@44 | Foswiki::Store::Rcs::Handler::
2 | 1 | 1 | 98µs | 181µs | _getTOPICINFO | Foswiki::Store::Rcs::Handler::
2 | 1 | 1 | 44µs | 252µs | noCheckinPending | Foswiki::Store::Rcs::Handler::
2 | 1 | 1 | 41µs | 530µs | getLatestRevisionID | Foswiki::Store::Rcs::Handler::
2 | 1 | 1 | 35µs | 652µs | getRevisionHistory | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 18µs | 31µs | BEGIN@29 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 13µs | 13µs | BEGIN@53 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 10µs | 361µs | BEGIN@37 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 9µs | 34µs | BEGIN@31 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 9µs | 13µs | BEGIN@30 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 5µs | 5µs | BEGIN@38 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@45 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@34 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@33 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@35 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@49 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@41 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@42 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@39 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@43 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@36 | Foswiki::Store::Rcs::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@48 | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | __ANON__[:64] | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _cacheMetaInfo | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _constructAttributesForAutoAttached | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _controlFileName | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _copyFile | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _dirForTopicAttachments | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _epochToRcsDateTime | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _fromcs | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _getAttachmentStats | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _mktemp | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _moveFile | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | _rmtree | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | addRevisionFromStream | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | addRevisionFromText | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | ci | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | copyAttachment | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | copyTopic | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | finish | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | getAttachmentList | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | getInfo | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | getLatestRevisionTime | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | getLease | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | getNextRevisionID | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | getTimestamp | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | hidePath | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | init | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | isAsciiDefault | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | isLocked | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | mkPathTo | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | mkTmpFilename | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | moveAttachment | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | moveTopic | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | moveWeb | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | openStream | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | readChanges | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | recordChange | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | remove | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | removeSpuriousLeases | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | repRev | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | replaceRevision | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | restoreLatestRevision | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | revisionExists | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | saveFile | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | savePendingCheckin | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | saveStream | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | setLease | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | setLock | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | stringify | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | synchroniseAttachmentsList | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | test | Foswiki::Store::Rcs::Handler::
0 | 0 | 0 | 0s | 0s | CLOSE | Foswiki::Store::_MemoryFile::
0 | 0 | 0 | 0s | 0s | READ | Foswiki::Store::_MemoryFile::
0 | 0 | 0 | 0s | 0s | READLINE | Foswiki::Store::_MemoryFile::
0 | 0 | 0 | 0s | 0s | TIEHANDLE | Foswiki::Store::_MemoryFile::
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 | |||||
7 | This class is PACKAGE PRIVATE to Store::VC, and should never be | ||||
8 | used from anywhere else. It is the base class of implementations of | ||||
9 | individual file handler objects used with stores that manipulate | ||||
10 | files stored in a version control system (phew!). | ||||
11 | |||||
12 | The general contract of the methods on this class and its subclasses | ||||
13 | calls for errors to be signalled by Error::Simple exceptions. | ||||
14 | |||||
15 | There are a number of references to RCS below; however this class is | ||||
16 | useful as a base class for handlers for all kinds of version control | ||||
17 | systems which use files on disk. | ||||
18 | |||||
19 | A note on character encodings. The RCS handler classes treat | ||||
20 | web, topic and attachment *names* coming from the caller as _character_ | ||||
21 | (i.e. UNICODE) data. *Content*, however, is always assumed to be bytes. | ||||
22 | This is done so that the handlers can operate on text (topic) content | ||||
23 | and binary (attachment) data using the same functions. | ||||
24 | |||||
25 | =cut | ||||
26 | |||||
27 | package Foswiki::Store::Rcs::Handler; | ||||
28 | |||||
29 | 2 | 28µs | 2 | 44µ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 # spent 31µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@29
# spent 13µs making 1 call to strict::import |
30 | 2 | 23µs | 2 | 17µ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 # spent 13µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@30
# spent 4µs making 1 call to warnings::import |
31 | 2 | 25µs | 2 | 60µ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 # spent 34µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@31
# spent 25µs making 1 call to Exporter::import |
32 | |||||
33 | 2 | 20µs | 1 | 4µ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 # spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@33 |
34 | 2 | 21µs | 1 | 4µ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 # spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@34 |
35 | 2 | 18µs | 1 | 4µ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 # spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@35 |
36 | 2 | 22µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@36 |
37 | 2 | 32µs | 2 | 712µ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 # spent 361µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@37
# spent 351µs making 1 call to Exporter::import |
38 | 2 | 24µs | 1 | 5µ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 # spent 5µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@38 |
39 | 2 | 20µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@39 |
40 | |||||
41 | 2 | 18µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@41 |
42 | 2 | 17µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@42 |
43 | 2 | 18µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@43 |
44 | 2 | 98µs | 1 | 454µ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 # spent 454µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@44 |
45 | 2 | 20µs | 1 | 4µ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 # spent 4µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@45 |
46 | |||||
47 | # Modules required for handling TOPICINFO cacheing | ||||
48 | 2 | 18µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@48 |
49 | 2 | 18µs | 1 | 3µ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 # spent 3µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@49 |
50 | 2 | 198µs | 1 | 3.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 # 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 | ||||
54 | 1 | 700ns | if ( $Foswiki::cfg{UseLocale} ) { | ||
55 | require locale; | ||||
56 | import locale(); | ||||
57 | } | ||||
58 | |||||
59 | 1 | 2µs | *_decode = \&Foswiki::Store::decode; | ||
60 | 1 | 400ns | *_encode = \&Foswiki::Store::encode; | ||
61 | 1 | 400ns | *_stat = \&Foswiki::Store::Rcs::Store::_stat; | ||
62 | 1 | 300ns | *_unlink = \&Foswiki::Store::Rcs::Store::_unlink; | ||
63 | 105615 | 1.31s | 105614 | 2.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 # spent 2.80s making 105614 calls to Foswiki::Store::encode, avg 26µs/call |
64 | 1 | 6µs | *_d = sub { -d _encode( $_[0] ) }; | ||
65 | 1 | 5.93ms | 1 | 13µs | } # spent 13µs making 1 call to Foswiki::Store::Rcs::Handler::BEGIN@53 |
66 | |||||
67 | 1 | 19µs | 3 | 58µs | our $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 | |||||
73 | Constructor. There is one object per stored file. | ||||
74 | |||||
75 | $store is the Foswiki::Rcs::Store object that contains the cache for | ||||
76 | objects of this type. A cache is used because at some point we'll be | ||||
77 | smarter about the number of calls to RCS code we make. | ||||
78 | |||||
79 | Note that $web, $topic and $attachment must be untainted, and encoded | ||||
80 | as 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 | ||||
85 | 26693 | 17.7ms | my ( $class, $store, $web, $topic, $attachment ) = @_; | ||
86 | |||||
87 | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; | ||||
88 | |||||
89 | 26693 | 35.8ms | 26693 | 24.2ms | ASSERT( !ref($web) ); # defunct usage # spent 24.2ms making 26693 calls to Assert::ASSERT, avg 908ns/call |
90 | |||||
91 | # Reuse is good | ||||
92 | 26693 | 46.0ms | my $id = ( $web || 0 ) . '/' . ( $topic || 0 ) . '/' . ( $attachment || 0 ); | ||
93 | 26693 | 161ms | 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 | ||||
98 | 320 | 816µ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) | ||||
109 | 320 | 445µs | $store->{handler_cache}->{$id} = $this; | ||
110 | |||||
111 | 320 | 232µs | if ( $this->{web} && $this->{topic} ) { | ||
112 | 299 | 203µs | my $rcsSubDir = ( $Foswiki::cfg{RCS}{useSubDir} ? '/RCS' : '' ); | ||
113 | |||||
114 | ASSERT( UNTAINTED($web), "web $web is tainted!" ) if DEBUG; | ||||
115 | 299 | 22µs | ASSERT( UNTAINTED($topic), "topic $topic is tainted!" ) if DEBUG; | ||
116 | 299 | 111µs | if ($attachment) { | ||
117 | ASSERT( UNTAINTED($attachment) ) if DEBUG; | ||||
118 | 1 | 3µs | $this->{file} = | ||
119 | "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}/$attachment"; | ||||
120 | 1 | 3µs | $this->{rcsFile} = | ||
121 | "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}$rcsSubDir/$attachment,v"; | ||||
122 | |||||
123 | } | ||||
124 | else { | ||||
125 | 298 | 592µs | $this->{file} = | ||
126 | "$Foswiki::cfg{DataDir}/$this->{web}/$this->{topic}.txt"; | ||||
127 | 298 | 472µ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 | ||||
133 | 320 | 145µs | $Foswiki::cfg{Store}{RememberChangesFor} ||= 31 * 24 * 60 * 60; | ||
134 | |||||
135 | 320 | 791µs | return $this; | ||
136 | } | ||||
137 | |||||
138 | =begin TML | ||||
139 | |||||
140 | ---++ ObjectMethod finish() | ||||
141 | Break 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. | ||||
148 | sub 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) | ||||
159 | sub 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 | ||||
175 | sub 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 | |||||
194 | sub _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 | ||||
207 | sub _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 | |||||
221 | Returns 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 | |||||
223 | Designed to be overridden by subclasses, which can call up to this method | ||||
224 | if simple file-based rev info is required. | ||||
225 | |||||
226 | =cut | ||||
227 | |||||
228 | sub 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 | ||||
266 | 2 | 1µs | my ( $this, $info ) = @_; | ||
267 | 2 | 500ns | my $f; | ||
268 | |||||
269 | 2 | 36µs | if ( open( $f, '<', $this->{file} ) ) { | ||
270 | 2 | 5µs | local $/ = "\n"; | ||
271 | 2 | 12µs | my $ti = <$f>; | ||
272 | 2 | 10µs | close($f); | ||
273 | 2 | 9µs | if ( defined $ti && $ti =~ /^%META:TOPICINFO\{(.*)\}%/ ) { | ||
274 | 2 | 8µs | 2 | 74µ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. | ||||
278 | 2 | 5µs | 2 | 9µs | $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} ) # spent 9µs making 2 calls to Foswiki::Store::cleanUpRevID, avg 4µs/call |
279 | || 1; | ||||
280 | 2 | 1µs | $info->{date} = $a->{date}; | ||
281 | 2 | 1µs | $info->{author} = $a->{author}; | ||
282 | 2 | 4µ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 | ||||
292 | 2 | 700ns | my $this = shift; | ||
293 | 2 | 400ns | my $isValid = 0; | ||
294 | |||||
295 | 2 | 6µs | 2 | 54µ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 { | ||||
299 | 2 | 4µs | 2 | 53µ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. | ||||
304 | 2 | 2µs | local ${^WIN32_SLOPPY_STAT} = | ||
305 | 1; # don't need to open the file on Win32 | ||||
306 | 2 | 8µs | 2 | 54µs | my $rcsTime = ( _stat( $this->{rcsFile} ) )[9]; # spent 54µs making 2 calls to Foswiki::Store::Rcs::Store::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm:61], avg 27µs/call |
307 | 2 | 6µs | 2 | 47µs | my $fileTime = ( _stat( $this->{file} ) )[9]; # spent 47µs making 2 calls to Foswiki::Store::Rcs::Store::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm:61], avg 24µs/call |
308 | 2 | 2µs | $isValid = | ||
309 | ( $fileTime - $rcsTime > 1 ) ? 0 : 1; # grace period of one sec | ||||
310 | } | ||||
311 | } | ||||
312 | 2 | 6µs | return $isValid; | ||
313 | } | ||||
314 | |||||
315 | # Must be implemented by subclasses | ||||
316 | sub 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 | ||||
322 | sub 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 | ||||
351 | sub _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 | |||||
385 | Add 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 | |||||
393 | sub 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 | |||||
414 | Add 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 | |||||
421 | sub 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 | |||||
435 | Replace 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 | |||||
443 | sub 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 | ||||
461 | sub repRev { | ||||
462 | die "Pure virtual method"; | ||||
463 | } | ||||
464 | |||||
465 | =begin TML | ||||
466 | |||||
467 | ---++ ObjectMethod getRevisionHistory() -> $iterator | ||||
468 | |||||
469 | Get an iterator over the identifiers of revisions. Returns the most | ||||
470 | recent revision first. | ||||
471 | |||||
472 | The default is to return an iterator from the current version number | ||||
473 | down to 1. Return rev 1 if the file exists without history. Return | ||||
474 | an 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 | ||||
479 | 2 | 1µs | my $this = shift; | ||
480 | ASSERT( $this->{file} ) if DEBUG; | ||||
481 | 2 | 6µs | 2 | 67µ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? | ||||
492 | 2 | 8µs | 2 | 530µs | my $maxRev = $this->getLatestRevisionID(); # spent 530µs making 2 calls to Foswiki::Store::Rcs::Handler::getLatestRevisionID, avg 265µs/call |
493 | 2 | 14µs | 2 | 20µ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 | |||||
500 | Get the ID of the most recent revision. This may return undef if there have | ||||
501 | been 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 | ||||
506 | 2 | 800ns | my $this = shift; | ||
507 | 2 | 4µs | 2 | 56µs | return 0 unless $this->storedDataExists(); # spent 56µs making 2 calls to Foswiki::Store::Rcs::Handler::storedDataExists, avg 28µs/call |
508 | |||||
509 | 2 | 2µs | my $info = {}; | ||
510 | 2 | 800ns | my $rev; | ||
511 | |||||
512 | 2 | 10µs | 2 | 252µs | my $checkinPending = $this->noCheckinPending() ? 0 : 1; # spent 252µs making 2 calls to Foswiki::Store::Rcs::Handler::noCheckinPending, avg 126µs/call |
513 | 2 | 1µs | unless ($checkinPending) { | ||
514 | 2 | 6µs | 2 | 181µs | $this->_getTOPICINFO($info); # spent 181µs making 2 calls to Foswiki::Store::Rcs::Handler::_getTOPICINFO, avg 91µs/call |
515 | 2 | 1µs | $rev = $info->{version}; | ||
516 | } | ||||
517 | |||||
518 | 2 | 400ns | 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 | ||||
524 | 2 | 300ns | $rev++ if $checkinPending && $this->revisionHistoryExists(); | ||
525 | 2 | 7µs | return $rev; | ||
526 | } | ||||
527 | |||||
528 | =begin TML | ||||
529 | |||||
530 | ---++ ObjectMethod getNextRevisionID() -> $id | ||||
531 | |||||
532 | Get the ID of the next (as yet uncreated) revision. The handler is required | ||||
533 | to implement this because the store has to be able to embed the revision | ||||
534 | ID into TOPICINFO before the revision is actually created. | ||||
535 | |||||
536 | If the file exists without revisions, then rev 1 does exist, so next rev | ||||
537 | should be rev 2, not rev 1, so the first change with missing history | ||||
538 | doesn't get merged into rev 1. | ||||
539 | |||||
540 | =cut | ||||
541 | |||||
542 | sub 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 | |||||
555 | Get the time of the most recent revision | ||||
556 | |||||
557 | =cut | ||||
558 | |||||
559 | sub getLatestRevisionTime { | ||||
560 | my @e = _stat( shift->{file} ); | ||||
561 | return $e[9] || 0; | ||||
562 | } | ||||
563 | |||||
564 | =begin TML | ||||
565 | |||||
566 | ---++ ObjectMethod getTopicNames() -> @topics | ||||
567 | |||||
568 | Get list of all topics in a web | ||||
569 | * =$web= - Web name, required, e.g. ='Sandbox'= | ||||
570 | Return 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 | ||||
575 | 40 | 20µs | my $this = shift; | ||
576 | 40 | 11µs | my $dh; | ||
577 | 40 | 1.02ms | 40 | 865µ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. | ||||
582 | 50280 | 27.2ms | my @topicList = | ||
583 | 150600 | 148ms | map { /^(.*)\.txt$/; $1; } | ||
584 | sort | ||||
585 | 40 | 174ms | 100320 | 2.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) ); | ||||
589 | 40 | 226µs | closedir($dh); | ||
590 | 40 | 5.85ms | return @topicList; | ||
591 | } | ||||
592 | |||||
593 | =begin TML | ||||
594 | |||||
595 | ---++ ObjectMethod revisionExists($rev) -> $boolean | ||||
596 | |||||
597 | Determine if the identified revision actually exists in the object | ||||
598 | history. | ||||
599 | |||||
600 | =cut | ||||
601 | |||||
602 | sub 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 | |||||
625 | Gets 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 | ||||
630 | 26 | 5µs | my $this = shift; | ||
631 | 26 | 11µs | my $dir = $Foswiki::cfg{DataDir}; | ||
632 | 26 | 21µs | $dir .= '/' . $this->{web} if defined $this->{web}; | ||
633 | 26 | 3µs | my @tmpList; | ||
634 | 26 | 3µs | my $dh; | ||
635 | 26 | 10µs | my $webid = "$Foswiki::cfg{WebPrefsTopicName}.txt"; | ||
636 | 26 | 60µs | 26 | 558µs | my $edir = _encode($dir); # spent 558µs making 26 calls to Foswiki::Store::encode, avg 21µs/call |
637 | 26 | 573µs | if ( opendir( $dh, $edir ) ) { | ||
638 | 24 | 61µs | 48 | 985µ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 |
639 | 24536 | 4.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. | ||||
647 | 26 | 9.27ms | grep { !/\./ && -e "$edir/$_/$webid" } readdir($dh); | ||
648 | 26 | 65µs | closedir($dh); | ||
649 | } | ||||
650 | |||||
651 | 26 | 127µs | return @tmpList; | ||
652 | } | ||||
653 | |||||
654 | =begin TML | ||||
655 | |||||
656 | ---++ ObjectMethod moveWeb( $newWeb ) | ||||
657 | |||||
658 | Move a web. | ||||
659 | |||||
660 | =cut | ||||
661 | |||||
662 | sub 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 | |||||
682 | Get the text of the given revision, and a flag indicating if this is the | ||||
683 | most recent revision. | ||||
684 | |||||
685 | Designed to be overridden by subclasses, which can call up to this method | ||||
686 | if the main file revision is required. | ||||
687 | |||||
688 | Note: does *not* handle the case where the latest does not exist but a history | ||||
689 | does; 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 | ||||
694 | 26327 | 10.7ms | my ($this) = @_; | ||
695 | 26327 | 167ms | 52654 | 2.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 | |||||
705 | Establishes 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 | ||||
710 | 52956 | 12.3ms | my $this = shift; | ||
711 | 52956 | 21.4ms | return 0 unless $this->{file}; | ||
712 | 52956 | 273ms | 52956 | 1.89s | return _e $this->{file}; # spent 1.89s making 52956 calls to Foswiki::Store::Rcs::Handler::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Handler.pm:63], avg 36µs/call |
713 | } | ||||
714 | |||||
715 | =begin TML | ||||
716 | |||||
717 | ---++ ObjectMethod revisionHistoryExists() -> $boolean | ||||
718 | |||||
719 | Establishes 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 | ||||
724 | 52658 | 14.1ms | my $this = shift; | ||
725 | 52658 | 21.0ms | return 0 unless $this->{rcsFile}; | ||
726 | 52658 | 269ms | 52658 | 1.96s | return _e $this->{rcsFile}; # spent 1.96s making 52658 calls to Foswiki::Store::Rcs::Handler::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Handler.pm:63], avg 37µs/call |
727 | } | ||||
728 | |||||
729 | =begin TML | ||||
730 | |||||
731 | ---++ ObjectMethod restoreLatestRevision( $cUID ) | ||||
732 | |||||
733 | Restore the plaintext file from the revision at the head. | ||||
734 | |||||
735 | =cut | ||||
736 | |||||
737 | sub 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 | |||||
756 | Destroy, utterly. Remove the data and attachments in the web. | ||||
757 | |||||
758 | Use with great care! No backup is taken! | ||||
759 | |||||
760 | =cut | ||||
761 | |||||
762 | sub 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 | |||||
787 | Move/rename a topic. | ||||
788 | |||||
789 | =cut | ||||
790 | |||||
791 | sub 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 | |||||
821 | Copy a topic. | ||||
822 | |||||
823 | =cut | ||||
824 | |||||
825 | sub 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 | |||||
863 | Move an attachment from one topic to another. The name is retained. | ||||
864 | |||||
865 | =cut | ||||
866 | |||||
867 | sub 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 | |||||
886 | Copy an attachment from one topic to another. The name is retained unless | ||||
887 | $newAttachment is defined. | ||||
888 | |||||
889 | =cut | ||||
890 | |||||
891 | sub 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 | |||||
913 | Check if this file type is known to be an ascii type file. | ||||
914 | |||||
915 | =cut | ||||
916 | |||||
917 | sub 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 | |||||
926 | Set a lock on the topic, if $lock, otherwise clear it. | ||||
927 | $cUID is a cUID. | ||||
928 | |||||
929 | SMELL: there is a tremendous amount of potential for race | ||||
930 | conditions using this locking approach. | ||||
931 | |||||
932 | It 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 | |||||
937 | sub 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 | |||||
958 | See if a lock exists. Return the lock user and lock time if it does. | ||||
959 | |||||
960 | =cut | ||||
961 | |||||
962 | sub 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 | |||||
979 | Set an lease on the topic. | ||||
980 | |||||
981 | =cut | ||||
982 | |||||
983 | sub 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 | |||||
1001 | Get the current lease on the topic. | ||||
1002 | |||||
1003 | =cut | ||||
1004 | |||||
1005 | sub 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 | |||||
1022 | Remove leases that are not related to a topic. These can get left behind in | ||||
1023 | some store implementations when a topic is created, but never saved. | ||||
1024 | |||||
1025 | =cut | ||||
1026 | |||||
1027 | sub 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 | |||||
1043 | sub test { | ||||
1044 | my ( $this, $test ) = @_; | ||||
1045 | my $f = _encode( $this->{file} ); | ||||
1046 | return eval "-$test '$f'"; | ||||
1047 | } | ||||
1048 | |||||
1049 | # Used by subclasses | ||||
1050 | sub 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 | |||||
1076 | sub _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 | |||||
1086 | sub _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 | ||||
1097 | sub 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 | ||||
1121 | 26327 | 14.0ms | my ( $this, $name ) = @_; | ||
1122 | ASSERT($name) if DEBUG; | ||||
1123 | 26327 | 2.68ms | my $data; | ||
1124 | 26327 | 2.21ms | my $IN_FILE; | ||
1125 | |||||
1126 | # Note: no IO layer; we want to trap encoding errors | ||||
1127 | 26327 | 458ms | 26327 | 650ms | if ( open( $IN_FILE, '<', _encode($name) ) ) { # spent 650ms making 26327 calls to Foswiki::Store::encode, avg 25µs/call |
1128 | 26327 | 22.7ms | binmode($IN_FILE); | ||
1129 | 26327 | 58.4ms | local $/ = undef; | ||
1130 | 26327 | 316ms | $data = <$IN_FILE>; | ||
1131 | 26327 | 163ms | close($IN_FILE); | ||
1132 | } | ||||
1133 | 26327 | 217ms | return $data; | ||
1134 | } | ||||
1135 | |||||
1136 | # Used by subclasses | ||||
1137 | sub 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 | ||||
1144 | sub _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 | |||||
1190 | sub _fromcs { | ||||
1191 | my $s = shift; | ||||
1192 | } | ||||
1193 | |||||
1194 | # remove a directory and all subdirectories | ||||
1195 | sub _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 | ||||
1246 | 1 | 500ns | 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 | |||||
1290 | Opens a file handle onto the store. This method is primarily to | ||||
1291 | support virtual file systems. | ||||
1292 | |||||
1293 | =$mode= can be '<', '>' or '>>' for read, write, and append | ||||
1294 | respectively. % | ||||
1295 | |||||
1296 | =%opts= can take different settings depending on =$mode=. | ||||
1297 | * =$mode='<'= | ||||
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='>'= or ='>>' | ||||
1303 | * no options | ||||
1304 | |||||
1305 | =cut | ||||
1306 | |||||
1307 | sub 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. | ||||
1341 | sub _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 | |||||
1381 | sub 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 | |||||
1431 | Get list of attachment names actually stored for topic. | ||||
1432 | |||||
1433 | =cut | ||||
1434 | |||||
1435 | sub 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 | ||||
1450 | sub _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 | |||||
1462 | sub _dirForTopicAttachments { | ||||
1463 | my ( $web, $topic ) = @_; | ||||
1464 | } | ||||
1465 | |||||
1466 | =begin TML | ||||
1467 | |||||
1468 | ---++ ObjectMethod stringify() | ||||
1469 | |||||
1470 | Generate string representation for debugging | ||||
1471 | |||||
1472 | =cut | ||||
1473 | |||||
1474 | sub 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 | ||||
1487 | sub 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 | |||||
1498 | sub 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 | |||||
1512 | sub 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 | |||||
1572 | sub 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 | |||||
1658 | 1 | 4µs | 1; | ||
1659 | |||||
1660 | __END__ |