Filename | /var/www/foswikidev/core/lib/Foswiki/Meta.pm |
Statements | Executed 31035175 statements in 35.2s |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
3323098 | 1 | 1 | 7.39s | 7.39s | dataDecode | Foswiki::Meta::
927699 | 1 | 1 | 6.20s | 6.20s | isValidEmbedding | Foswiki::Meta::
875082 | 2 | 2 | 4.02s | 4.02s | putKeyed | Foswiki::Meta::
26327 | 3 | 2 | 512ms | 76.4s | loadVersion | Foswiki::Meta::
26296 | 6 | 5 | 331ms | 77.1s | load | Foswiki::Meta::
52396 | 3 | 3 | 329ms | 329ms | put | Foswiki::Meta::
26761 | 19 | 13 | 318ms | 353ms | new (recurses: max depth 1, inclusive time 32µs) | Foswiki::Meta::
61651 | 10 | 7 | 183ms | 183ms | get | Foswiki::Meta::
122649 | 4 | 4 | 138ms | 138ms | getLoadedRev | Foswiki::Meta::
52654 | 2 | 2 | 106ms | 106ms | setLoadStatus | Foswiki::Meta::
35131 | 7 | 7 | 104ms | 138ms | text (recurses: max depth 1, inclusive time 136µs) | Foswiki::Meta::
70084 | 4 | 4 | 93.2ms | 93.2ms | latestIsLoaded | Foswiki::Meta::
44523 | 25 | 14 | 70.6ms | 70.6ms | topic | Foswiki::Meta::
26327 | 1 | 1 | 57.6ms | 57.6ms | count | Foswiki::Meta::
35941 | 28 | 12 | 53.8ms | 53.8ms | web | Foswiki::Meta::
26356 | 2 | 1 | 46.3ms | 46.3ms | addDependency | Foswiki::Meta::
1 | 1 | 1 | 2.97ms | 5.92ms | renderFormForDisplay | Foswiki::Meta::
252 | 7 | 7 | 2.79ms | 14.2ms | haveAccess | Foswiki::Meta::
222 | 2 | 2 | 1.59ms | 17.4ms | getRevisionInfo | Foswiki::Meta::
124 | 4 | 4 | 1.11ms | 23.5ms | getPreference | Foswiki::Meta::
1 | 1 | 1 | 756µs | 1.08ms | BEGIN@113 | Foswiki::Meta::
384 | 6 | 2 | 645µs | 645µs | isCacheable | Foswiki::Meta::
1 | 1 | 1 | 490µs | 609µs | BEGIN@116 | Foswiki::Meta::
172 | 4 | 3 | 421µs | 421µs | getPath | Foswiki::Meta::
100 | 4 | 3 | 408µs | 137s | expandMacros (recurses: max depth 2, inclusive time 62.6ms) | Foswiki::Meta::
40 | 1 | 1 | 391µs | 108s | query | Foswiki::Meta::
40 | 1 | 1 | 313µs | 2.79s | eachTopic | Foswiki::Meta::
29 | 1 | 1 | 182µs | 2.01ms | existsInStore | Foswiki::Meta::
30 | 2 | 2 | 92µs | 92µs | find | Foswiki::Meta::
3 | 1 | 1 | 82µs | 13.0ms | unload | Foswiki::Meta::
49 | 6 | 6 | 67µs | 67µs | session | Foswiki::Meta::
3 | 1 | 1 | 66µs | 86µs | setRevisionInfo | Foswiki::Meta::
5 | 2 | 2 | 52µs | 50.2ms | renderTML | Foswiki::Meta::
1 | 1 | 1 | 23µs | 116µs | hasAttachment | Foswiki::Meta::
3 | 1 | 1 | 20µs | 13.0ms | finish | Foswiki::Meta::
2 | 1 | 1 | 16µs | 17.9ms | eachWeb | Foswiki::Meta::
2 | 2 | 1 | 16µs | 747µs | getRevisionHistory | Foswiki::Meta::
1 | 1 | 1 | 13µs | 27µs | BEGIN@109 | Foswiki::Meta::
1 | 1 | 1 | 11µs | 32µs | BEGIN@545 | Foswiki::Meta::
1 | 1 | 1 | 10µs | 11µs | getFormName | Foswiki::Meta::
1 | 1 | 1 | 9µs | 31µs | BEGIN@112 | Foswiki::Meta::
1 | 1 | 1 | 8µs | 13µs | BEGIN@110 | Foswiki::Meta::
1 | 1 | 1 | 8µs | 43µs | BEGIN@132 | Foswiki::Meta::
1 | 1 | 1 | 7µs | 110µs | BEGIN@111 | Foswiki::Meta::
1 | 1 | 1 | 6µs | 6µs | registerMETA | Foswiki::Meta::
1 | 1 | 1 | 4µs | 4µs | BEGIN@114 | Foswiki::Meta::
1 | 1 | 1 | 4µs | 4µs | BEGIN@120 | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:1834] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:1843] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:1886] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:1890] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2000] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2003] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2148] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2152] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2303] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2309] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2386] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2390] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2453] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2457] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2994] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:2997] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:3209] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:3215] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:3296] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | __ANON__[:3302] | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _assertIsAttachment | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _assertIsTopic | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _assertIsWeb | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _atomicLock | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _atomicUnlock | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _makeSummaryTextSafe | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _summariseTextSimple | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | _summariseTextWithSearchContext | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | attach | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | clearLease | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | copyAttachment | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | copyFrom | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | deleteMostRecentRevision | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | eachAttachment | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | eachChange | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | expandNewTopic | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | fireDependency | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | forEachSelectedValue | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getAttachmentRevisionInfo | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getContainer | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getDifferences | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getEmbeddedStoreForm | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getLatestRev | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getLease | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getParent | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getRev1Info | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | getRevisionAtTime | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | isSessionTopic | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | merge | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | move | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | moveAttachment | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | onTick | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | openAttachment | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | populateNewWeb | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | putAll | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | remove | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | removeFromStore | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | renderFormFieldForDisplay | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | replaceMostRecentRevision | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | save | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | saveAs | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | setEmbeddedStoreForm | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | setLease | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | stringify | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | summariseChanges | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | summariseText | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | testAttachment | Foswiki::Meta::
0 | 0 | 0 | 0s | 0s | type | Foswiki::Meta::
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::Meta | ||||
6 | |||||
7 | Objects of this class act as handles onto real store objects. An | ||||
8 | object of this class can represent the Foswiki root, a web, or a topic. | ||||
9 | |||||
10 | Meta objects interact with the store using only the methods of | ||||
11 | Foswiki::Store. The rest of the core should interact only with Meta | ||||
12 | objects; the only exception to this are the *Exists methods that are | ||||
13 | published by the store interface (and facaded by the Foswiki class). | ||||
14 | |||||
15 | A meta object exists in one of two states; either unloaded, in which case | ||||
16 | it is simply a lightweight handle to a store location, and loaded, in | ||||
17 | which case it acts as a portal onto the actual store content of a specific | ||||
18 | revision of the topic. | ||||
19 | |||||
20 | An unloaded object is constructed by the =new= constructor on this class, | ||||
21 | passing one to three parameters depending on whether the object represents the | ||||
22 | root, a web, or a topic. | ||||
23 | |||||
24 | A loaded object may be constructed by calling the =load= constructor, or | ||||
25 | a previously constructed object may be converted to 'loaded' state by | ||||
26 | calling =loadVersion=. Once an object is loaded with a specific revision, it | ||||
27 | cannot be reloaded. | ||||
28 | |||||
29 | Unloaded objects return undef from =getLoadedRev=, or the loaded revision | ||||
30 | otherwise. | ||||
31 | |||||
32 | An unloaded object can be populated through calls to =text($text)=, =put= | ||||
33 | and =putKeyed=. Such an object can be saved using =save()= to create a new | ||||
34 | revision of the topic. | ||||
35 | |||||
36 | To the caller, a meta object carries two types of data. The first | ||||
37 | is the "plain text" of the topic, which is accessible through the =text()= | ||||
38 | method. The object also behaves as a hash of different types of | ||||
39 | meta-data (keyed on the type, such as 'FIELD' and 'FILEATTACHMENT'). | ||||
40 | |||||
41 | Each entry in the hash is an array, where each entry in the array | ||||
42 | contains another hash of the key=value pairs, corresponding to a | ||||
43 | single meta-datum. | ||||
44 | |||||
45 | If there may be multiple entries of the same top-level type (i.e. for FIELD | ||||
46 | and FILEATTACHMENT) then the array has multiple entries. These types | ||||
47 | are referred to as "keyed" types. The array entries are keyed with the | ||||
48 | attribute 'name' which must be in each entry in the array. | ||||
49 | |||||
50 | For unkeyed types, the array has only one entry. | ||||
51 | |||||
52 | Pictorially, | ||||
53 | * TOPICINFO | ||||
54 | * author => '...' | ||||
55 | * date => '...' | ||||
56 | * ... | ||||
57 | * FILEATTACHMENT | ||||
58 | * [0] = { name => 'a' ... } | ||||
59 | * [1] = { name => 'b' ... } | ||||
60 | * FIELD | ||||
61 | * [0] = { name => 'c' ... } | ||||
62 | * [1] = { name => 'd' ... } | ||||
63 | |||||
64 | Implementor note: the =_indices= field gives a quick lookup into this | ||||
65 | structure; it is a hash of top-level types, each mapping to a hash indexed | ||||
66 | on the key name. For the above example, it looks like this: | ||||
67 | * _indices => { | ||||
68 | FILEATTACHMENT => { a => 0, b => 1 }, | ||||
69 | FIELD => { c => 0, d => 1 } | ||||
70 | } | ||||
71 | It is maintained on the fly by the methods of this module, which makes it | ||||
72 | important *not* to write new data directly into the structure, but *always* | ||||
73 | to go through the methods exported from here. | ||||
74 | |||||
75 | As required by the contract with Foswiki::Store, version numbers are required | ||||
76 | to be positive, non-zero integers. When passing in version numbers, 0, | ||||
77 | undef and '' are treated as referring to the *latest* (most recent) | ||||
78 | revision of the object. Version numbers are required to increase (later | ||||
79 | version numbers are greater than earlier) but are *not* required to be | ||||
80 | sequential. | ||||
81 | |||||
82 | *IMPORTANT* the methods on =Foswiki::Meta= _do not check access permissions_ | ||||
83 | (other than =haveAccess=, obviously). | ||||
84 | This is a deliberate design decision, as these checks are expensive and many | ||||
85 | callers don't require them. For this reason, be *very careful* how you use | ||||
86 | =Foswiki::Meta=. Extension authors will almost always find the methods | ||||
87 | they want in =Foswiki::Func=, rather than in this class. | ||||
88 | |||||
89 | *Since* _date_ indicates where functions or parameters have been added since | ||||
90 | the baseline of the API (Foswiki release 4.2.3). The _date_ indicates the | ||||
91 | earliest date of a Foswiki release that will support that function or | ||||
92 | parameter. | ||||
93 | |||||
94 | *Deprecated* _date_ indicates where a function or parameters has been | ||||
95 | [[http://en.wikipedia.org/wiki/Deprecation][deprecated]]. Deprecated | ||||
96 | functions will still work, though they should | ||||
97 | _not_ be called in new plugins and should be replaced in older plugins | ||||
98 | as soon as possible. Deprecated parameters are simply ignored in Foswiki | ||||
99 | releases after _date_. | ||||
100 | |||||
101 | *Until* _date_ indicates where a function or parameter has been removed. | ||||
102 | The _date_ indicates the latest date at which Foswiki releases still supported | ||||
103 | the function or parameter. | ||||
104 | |||||
105 | =cut | ||||
106 | |||||
107 | package Foswiki::Meta; | ||||
108 | |||||
109 | 2 | 27µs | 2 | 40µs | # spent 27µs (13+13) within Foswiki::Meta::BEGIN@109 which was called:
# once (13µs+13µs) by Foswiki::BEGIN@642 at line 109 # spent 27µs making 1 call to Foswiki::Meta::BEGIN@109
# spent 13µs making 1 call to strict::import |
110 | 2 | 24µs | 2 | 17µs | # spent 13µs (8+4) within Foswiki::Meta::BEGIN@110 which was called:
# once (8µs+4µs) by Foswiki::BEGIN@642 at line 110 # spent 13µs making 1 call to Foswiki::Meta::BEGIN@110
# spent 4µs making 1 call to warnings::import |
111 | 2 | 28µs | 2 | 212µs | # spent 110µs (7+102) within Foswiki::Meta::BEGIN@111 which was called:
# once (7µs+102µs) by Foswiki::BEGIN@642 at line 111 # spent 110µs making 1 call to Foswiki::Meta::BEGIN@111
# spent 102µs making 1 call to Error::import |
112 | 2 | 26µs | 2 | 53µs | # spent 31µs (9+22) within Foswiki::Meta::BEGIN@112 which was called:
# once (9µs+22µs) by Foswiki::BEGIN@642 at line 112 # spent 31µs making 1 call to Foswiki::Meta::BEGIN@112
# spent 22µs making 1 call to Exporter::import |
113 | 2 | 120µs | 2 | 1.17ms | # spent 1.08ms (756µs+319µs) within Foswiki::Meta::BEGIN@113 which was called:
# once (756µs+319µs) by Foswiki::BEGIN@642 at line 113 # spent 1.08ms making 1 call to Foswiki::Meta::BEGIN@113
# spent 96µs making 1 call to Exporter::import |
114 | 2 | 20µs | 1 | 4µs | # spent 4µs within Foswiki::Meta::BEGIN@114 which was called:
# once (4µs+0s) by Foswiki::BEGIN@642 at line 114 # spent 4µs making 1 call to Foswiki::Meta::BEGIN@114 |
115 | |||||
116 | 2 | 105µs | 1 | 609µs | # spent 609µs (490+120) within Foswiki::Meta::BEGIN@116 which was called:
# once (490µs+120µs) by Foswiki::BEGIN@642 at line 116 # spent 609µs making 1 call to Foswiki::Meta::BEGIN@116 |
117 | |||||
118 | #use Foswiki::Iterator::NumberRangeIterator; | ||||
119 | |||||
120 | # spent 4µs within Foswiki::Meta::BEGIN@120 which was called:
# once (4µs+0s) by Foswiki::BEGIN@642 at line 125 | ||||
121 | 1 | 4µs | if ( $Foswiki::cfg{UseLocale} ) { | ||
122 | require locale; | ||||
123 | import locale(); | ||||
124 | } | ||||
125 | 1 | 39µs | 1 | 4µs | } # spent 4µs making 1 call to Foswiki::Meta::BEGIN@120 |
126 | |||||
127 | 1 | 400ns | our $VERSION = 1.2; | ||
128 | |||||
129 | 1 | 100ns | our $reason; | ||
130 | |||||
131 | # Version for the embedding format (increment when embedding format changes) | ||||
132 | 2 | 894µs | 2 | 78µs | # spent 43µs (8+35) within Foswiki::Meta::BEGIN@132 which was called:
# once (8µs+35µs) by Foswiki::BEGIN@642 at line 132 # spent 43µs making 1 call to Foswiki::Meta::BEGIN@132
# spent 35µs making 1 call to constant::import |
133 | |||||
134 | # defaults for truncation of summary text | ||||
135 | 1 | 200ns | our $SUMMARY_TMLTRUNC = 162; | ||
136 | 1 | 100ns | our $SUMMARY_MINTRUNC = 16; | ||
137 | 1 | 500ns | our $SUMMARY_ELLIPSIS = '<b>…</b>'; # Google style | ||
138 | |||||
139 | # the number of characters either side of a search term | ||||
140 | 1 | 100ns | our $SUMMARY_DEFAULT_CONTEXT = 30; | ||
141 | |||||
142 | # max number of lines in a summary (best to keep it even) | ||||
143 | 1 | 100ns | our $CHANGES_SUMMARY_LINECOUNT = 6; | ||
144 | 1 | 100ns | our $CHANGES_SUMMARY_PLAINTRUNC = 70; | ||
145 | |||||
146 | =begin TML | ||||
147 | |||||
148 | PUBLIC %VALIDATE; | ||||
149 | |||||
150 | META:x validation. This hash maps from META: names to the type record | ||||
151 | registered by registerMETA. See registerMETA for more information on what | ||||
152 | these records contain. | ||||
153 | |||||
154 | _default is set on base meta-data types (those not added by | ||||
155 | Foswiki::Func::registerMETA) to differentiate the minimum required | ||||
156 | meta-data and that added by extensions. | ||||
157 | |||||
158 | =cut | ||||
159 | |||||
160 | 1 | 15µs | our %VALIDATE = ( | ||
161 | TOPICINFO => { | ||||
162 | allow => [ | ||||
163 | qw( author version date format reprev | ||||
164 | rev comment ) | ||||
165 | ], | ||||
166 | _default => 1, | ||||
167 | alias => 'info', | ||||
168 | }, | ||||
169 | TOPICMOVED => { | ||||
170 | require => [qw( from to by date )], | ||||
171 | _default => 1, | ||||
172 | alias => 'moved', | ||||
173 | }, | ||||
174 | |||||
175 | # Special case, see Item2554; allow an empty TOPICPARENT, as this was | ||||
176 | # erroneously generated at some point in the past | ||||
177 | TOPICPARENT => { | ||||
178 | allow => [qw( name )], | ||||
179 | _default => 1, | ||||
180 | alias => 'parent', | ||||
181 | }, | ||||
182 | FILEATTACHMENT => { | ||||
183 | require => [qw( name )], | ||||
184 | other => [ | ||||
185 | qw( version path size date user | ||||
186 | comment attr ) | ||||
187 | ], | ||||
188 | _default => 1, | ||||
189 | alias => 'attachments', | ||||
190 | many => 1, | ||||
191 | }, | ||||
192 | FORM => { | ||||
193 | require => [qw( name )], | ||||
194 | _default => 1, | ||||
195 | alias => 'form', | ||||
196 | }, | ||||
197 | FIELD => { | ||||
198 | require => [qw( name value )], | ||||
199 | other => [qw( title )], | ||||
200 | _default => 1, | ||||
201 | alias => 'fields', | ||||
202 | many => 1, | ||||
203 | }, | ||||
204 | PREFERENCE => { | ||||
205 | require => [qw( name value )], | ||||
206 | other => [qw( type )], | ||||
207 | _default => 1, | ||||
208 | alias => 'preferences', | ||||
209 | many => 1, | ||||
210 | }, | ||||
211 | VERSIONS => { | ||||
212 | |||||
213 | # In trad text based data store, this does not occur in the | ||||
214 | # topic text, but is pulled on demand during queries | ||||
215 | alias => 'versions', | ||||
216 | } | ||||
217 | ); | ||||
218 | |||||
219 | our %aliases = | ||||
220 | map { $VALIDATE{$_}->{alias} => "META:$_" } | ||||
221 | 1 | 14µs | grep { $VALIDATE{$_}->{alias} } keys %VALIDATE; | ||
222 | |||||
223 | our %isArrayType = | ||||
224 | map { $_ => 1 } | ||||
225 | 1 | 5µs | grep { $VALIDATE{$_}->{many} } keys %VALIDATE; | ||
226 | |||||
227 | =begin TML | ||||
228 | |||||
229 | ---++ StaticMethod registerMETA($name, %syntax) | ||||
230 | |||||
231 | Foswiki supports embedding meta-data into topics. For example, | ||||
232 | |||||
233 | =%<nop>META:BOOK{title="Transit" author="Edmund Cooper" isbn="0-571-05724-1"}%= | ||||
234 | |||||
235 | This meta-data is validated when it is read from the store. Meta-data | ||||
236 | that is not registered, or doesn't pass validation, is ignored. This | ||||
237 | function allows you to register a new META datum, passing the name in | ||||
238 | =$name=. =%syntax= contains information about the syntax and semantics of | ||||
239 | the tag. | ||||
240 | |||||
241 | The following entries are supported in =%syntax= | ||||
242 | |||||
243 | =many=>1=. By default meta-data are single valued i.e. can only occur once | ||||
244 | in a topic. If you require the meta-data to be repeated many times (like | ||||
245 | META:FIELD and META:ATTACHMENT) then you must set this option. For example, | ||||
246 | to declare a many-valued =BOOK= meta-data type: | ||||
247 | <verbatim> | ||||
248 | registerMeta('BOOK', many => 1) | ||||
249 | </verbatim> | ||||
250 | |||||
251 | =require=>[]= is used to check that a list of named parameters are present on | ||||
252 | the tag. For example, | ||||
253 | <verbatim> | ||||
254 | registerMETA('BOOK', require => [ 'title', 'author' ]); | ||||
255 | </verbatim> | ||||
256 | can be used to check that both =title= and =author= are present. | ||||
257 | |||||
258 | =allow=>[]= lets you specify other optional parameters that are allowed | ||||
259 | on the tag. If you specify =allow= then the validation will fail if the | ||||
260 | tag contains any parameters that are _not_ in the =allow= or =require= lists. | ||||
261 | If you don't specify =allow= then all parameters will be allowed. | ||||
262 | |||||
263 | =require= and =allow= only verify the *presence* of parameters, and | ||||
264 | not their *values*. | ||||
265 | |||||
266 | =other=[]= lets you declare other legal parameters, and is provided | ||||
267 | mainly to support the initialisation of DB schema. It it is like | ||||
268 | =allow= except that it doesn't imply any exclusion of META that contains | ||||
269 | unallowed params. | ||||
270 | |||||
271 | =function=>\&fn= causes the function =fn= to be called when the | ||||
272 | datum is encountered when reading a topic, passing in the name of the | ||||
273 | macro and the argument hash. The function must return a non-zero/undef | ||||
274 | value if the tag is acceptable, or 0 otherwise. For example: | ||||
275 | <verbatim> | ||||
276 | registerMETA('BOOK', function => sub { | ||||
277 | my ($name, $args) = @_; | ||||
278 | # $name will be BOOK | ||||
279 | return isValidTitle($args->{title}); | ||||
280 | } | ||||
281 | </verbatim> | ||||
282 | can be used to check that =%META:BOOK{}= contains a valid title. | ||||
283 | |||||
284 | Checks are cumulative, so if you: | ||||
285 | <verbatim> | ||||
286 | registerMETA('BOOK', | ||||
287 | function => \&checkParameters, | ||||
288 | require => [ 'title' ], | ||||
289 | allow => [ 'author', 'isbn' ]); | ||||
290 | </verbatim> | ||||
291 | then all these conditions will be tested. Note that =require= and =allow= | ||||
292 | are tested _after_ =function= is called, to give the function a chance to | ||||
293 | rewrite the parameter list. | ||||
294 | |||||
295 | If no checker is registered for a META tag, then it will automatically | ||||
296 | be accepted into the topic meta-data. | ||||
297 | |||||
298 | =alias=>'name'= lets you set an alias for the datum that will be added to | ||||
299 | the query language. For example, =alias=>'info'= is used to alias | ||||
300 | 'META:TOPICINFO' in queries. | ||||
301 | <verbatim> | ||||
302 | registerMeta('BOOK', alias => 'book', many => 1) | ||||
303 | </verbatim> | ||||
304 | This lets you use syntax such as =books[author='Anais Nin']= in queries. | ||||
305 | See QuerySearch for more on aliases. | ||||
306 | |||||
307 | =cut | ||||
308 | |||||
309 | # spent 6µs within Foswiki::Meta::registerMETA which was called:
# once (6µs+0s) by Foswiki::Plugins::RevCommentPlugin::initPlugin at line 75 of /var/www/foswikidev/core/lib/Foswiki/Plugins/RevCommentPlugin.pm | ||||
310 | 1 | 2µs | my ( $name, %check ) = @_; | ||
311 | 1 | 2µs | $VALIDATE{$name} = \%check; | ||
312 | 1 | 500ns | $aliases{ $check{alias} } = "META:$name" if $check{alias}; | ||
313 | 1 | 4µs | $isArrayType{$name} = $check{many}; | ||
314 | } | ||||
315 | |||||
316 | ############# GENERIC METHODS ############# | ||||
317 | |||||
318 | =begin TML | ||||
319 | |||||
320 | ---++ ClassMethod new($session, $web, $topic [, $text]) | ||||
321 | * =$session= - a Foswiki object (e.g. =$Foswiki::Plugins::SESSION=) | ||||
322 | * =$web=, =$topic= - the pathname of the object. If both are undef, | ||||
323 | this object is a handle for the root container. If $topic is undef, | ||||
324 | it is the handle to a web. Otherwise it's a handle to a topic. | ||||
325 | * $text - optional raw text, which may include embedded meta-data. Will | ||||
326 | be deserialised to initialise the object. Only valid | ||||
327 | if =$web= and =$topic= are defined. | ||||
328 | Construct a new, unloaded object. This method creates lightweight | ||||
329 | handles for store objects; the full content of the actual object will | ||||
330 | *not* be loaded. If you need to interact with the existing content of | ||||
331 | the stored object, use the =load= method to load the content. | ||||
332 | |||||
333 | ---++ ClassMethod new($prototype) | ||||
334 | |||||
335 | Construct a new, unloaded object, using the session, web and topic in the | ||||
336 | prototype object (which must be type Foswiki::Meta). | ||||
337 | |||||
338 | =cut | ||||
339 | |||||
340 | # spent 353ms (318+35.0) within Foswiki::Meta::new which was called 26761 times, avg 13µs/call:
# 26296 times (313ms+34.4ms) by Foswiki::Meta::load at line 449, avg 13µs/call
# 80 times (762µs+100µs) by Foswiki::Store::Interfaces::QueryAlgorithm::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm:235] at line 223 of /var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm, avg 11µs/call
# 80 times (709µs+77µs) by Foswiki::Store::Interfaces::QueryAlgorithm::query at line 96 of /var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm, avg 10µs/call
# 54 times (480µs+66µs) by Foswiki::Prefs::_getBackend at line 146 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 10µs/call
# 40 times (414µs+48µs) by Foswiki::Search::formatResults at line 911 of /var/www/foswikidev/core/lib/Foswiki/Search.pm, avg 12µs/call
# 40 times (380µs+62µs) by Foswiki::Search::formatResults at line 743 of /var/www/foswikidev/core/lib/Foswiki/Search.pm, avg 11µs/call
# 40 times (372µs+56µs) by Foswiki::Search::searchWeb at line 261 of /var/www/foswikidev/core/lib/Foswiki/Search.pm, avg 11µs/call
# 40 times (342µs+47µs) by Foswiki::Store::QueryAlgorithms::BruteForce::_webQuery at line 140 of /var/www/foswikidev/core/lib/Foswiki/Store/QueryAlgorithms/BruteForce.pm, avg 10µs/call
# 40 times (327µs+47µs) by Foswiki::Store::QueryAlgorithms::BruteForce::_webQuery at line 78 of /var/www/foswikidev/core/lib/Foswiki/Store/QueryAlgorithms/BruteForce.pm, avg 9µs/call
# 19 times (185µs+25µs) by Foswiki::WebFilter::ok at line 50 of /var/www/foswikidev/core/lib/Foswiki/WebFilter.pm, avg 11µs/call
# 12 times (103µs+15µs) by Foswiki::VAR at line 24 of /var/www/foswikidev/core/lib/Foswiki/Macros/VAR.pm, avg 10µs/call
# 4 times (38µs+6µs) by Foswiki::inlineAlert at line 2617 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 11µs/call
# 3 times (51µs+48µs) by Foswiki::Store::Rcs::Store::getVersionInfo at line 359 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 33µs/call
# 3 times (28µs+5µs) by Foswiki::Func::expandCommonVariables at line 2656 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 11µs/call
# 3 times (28µs+5µs) by Foswiki::REVINFO at line 34 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 11µs/call
# 3 times (29µs+-29µs) by Foswiki::Meta::new at line 348, avg 0s/call
# 2 times (18µs+3µs) by Foswiki::deepWebList at line 1651 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 11µs/call
# once (14µs+2µs) by Foswiki::If::OP_allows::evaluate at line 56 of /var/www/foswikidev/core/lib/Foswiki/If/OP_allows.pm
# once (10µs+1µs) by Foswiki::_lookupIcon at line 36 of /var/www/foswikidev/core/lib/Foswiki/Macros/ICON.pm | ||||
341 | 26761 | 15.2ms | my ( $class, $session, $web, $topic, $text ) = @_; | ||
342 | |||||
343 | 26761 | 150ms | 26761 | 35.0ms | if ( $session->isa('Foswiki::Meta') ) { # spent 35.0ms making 26761 calls to UNIVERSAL::isa, avg 1µs/call |
344 | |||||
345 | # Prototype | ||||
346 | ASSERT( !defined($web) && !defined($topic) && !defined($text) ) | ||||
347 | if DEBUG; | ||||
348 | 3 | 25µs | 12 | 13µs | return $class->new( $session->session, $session->web, $session->topic ); # spent 5µs making 3 calls to Foswiki::Meta::web, avg 2µs/call
# spent 4µs making 3 calls to Foswiki::Meta::topic, avg 1µs/call
# spent 4µs making 3 calls to Foswiki::Meta::session, avg 1µs/call
# spent 32µs making 3 calls to Foswiki::Meta::new, avg 11µs/call, recursion: max depth 1, sum of overlapping time 32µs |
349 | } | ||||
350 | |||||
351 | 26758 | 57.6ms | my $this = bless( | ||
352 | { | ||||
353 | _session => $session, | ||||
354 | |||||
355 | # Index keyed on top level type mapping entry names to their | ||||
356 | # index within the data array. | ||||
357 | _indices => undef, | ||||
358 | }, | ||||
359 | ref($class) || $class | ||||
360 | ); | ||||
361 | |||||
362 | # Normalise web path (replace [./]+ with /) | ||||
363 | 26758 | 10.5ms | if ( defined $web ) { | ||
364 | ASSERT( UNTAINTED($web), 'web is tainted' ) if DEBUG; | ||||
365 | 26758 | 33.2ms | $web =~ tr#/.#/#s; | ||
366 | } | ||||
367 | |||||
368 | # Note: internal fields are prepended with _. All uppercase | ||||
369 | # fields will be assumed to be meta-data. | ||||
370 | |||||
371 | 26758 | 19.6ms | $this->{_web} = $web; | ||
372 | |||||
373 | ASSERT( UNTAINTED($topic), 'topic is tainted' ) | ||||
374 | if ( DEBUG && defined $topic ); | ||||
375 | |||||
376 | 26758 | 11.5ms | $this->{_topic} = $topic; | ||
377 | |||||
378 | #print STDERR "--new Meta($web, ".($topic||'undef').")\n"; | ||||
379 | #$this->{_text} = undef; # topics only | ||||
380 | |||||
381 | # Preferences cache object. We store a pointer, rather than looking | ||||
382 | # up the name each time, because we want to be able to invalidate the | ||||
383 | # loaded preferences if this object is loaded. | ||||
384 | #$this->{_preferences} = undef; | ||||
385 | |||||
386 | 26758 | 18.8ms | $this->{FILEATTACHMENT} = []; | ||
387 | |||||
388 | 26758 | 4.45ms | if ( defined $text ) { | ||
389 | |||||
390 | # User supplied topic body forces us to consider this as the | ||||
391 | # latest rev | ||||
392 | ASSERT( defined($web), 'web is not defined' ) if DEBUG; | ||||
393 | ASSERT( defined($topic), 'topic is not defined' ) if DEBUG; | ||||
394 | Foswiki::Serialise::deserialise( $text, 'Embedded', $this ); | ||||
395 | $this->{_latestIsLoaded} = 1; | ||||
396 | } | ||||
397 | |||||
398 | 26758 | 112ms | return $this; | ||
399 | } | ||||
400 | |||||
401 | =begin TML | ||||
402 | |||||
403 | ---++ ClassMethod load($session, $web, $topic, $rev) | ||||
404 | |||||
405 | This constructor will load (or otherwise fetch) the meta-data for a | ||||
406 | named web/topic. | ||||
407 | * =$rev= - revision to load. If undef, 0, '' or > max available rev, will | ||||
408 | load the latest rev. If the revision is in range but does not exist, | ||||
409 | then will return an unloaded meta object (getLoadedRev() will be undef) | ||||
410 | |||||
411 | This method is functionally identical to: | ||||
412 | <verbatim> | ||||
413 | $this = Foswiki::Meta->new( $session, $web, $topic ); | ||||
414 | $this->loadVersion( $rev ); | ||||
415 | </verbatim> | ||||
416 | |||||
417 | WARNING: see notes on revision numbers under =getLoadedRev=. | ||||
418 | |||||
419 | ---++ ObjectMethod load($rev) -> $metaObject | ||||
420 | |||||
421 | Load an unloaded meta-data object with a given version of the data. | ||||
422 | Once loaded, the object is locked to that revision. | ||||
423 | |||||
424 | * =$rev= - revision to load. If undef, 0, '' or > max available rev, will | ||||
425 | load the latest rev. If the revision is in range but does not exist, | ||||
426 | then will return an unloaded meta object (getLoadedRev() will be undef) | ||||
427 | |||||
428 | WARNING: see notes on revision numbers under =getLoadedRev= | ||||
429 | |||||
430 | |||||
431 | TODO: this is insane. load() can fail - but it will give you a seemingly fine Meta object anyway. | ||||
432 | |||||
433 | =cut | ||||
434 | |||||
435 | # spent 77.1s (331ms+76.7) within Foswiki::Meta::load which was called 26296 times, avg 2.93ms/call:
# 26280 times (331ms+76.7s) by Foswiki::MetaCache::addMeta at line 147 of /var/www/foswikidev/core/lib/Foswiki/MetaCache.pm, avg 2.93ms/call
# 8 times (83µs+3.91ms) by Foswiki::_includeTopic at line 180 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 499µs/call
# 5 times (59µs+15.2ms) by Foswiki::Func::readTopic at line 1660 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 3.06ms/call
# once (12µs+1.25ms) by Foswiki::Users::TopicUserMapping::_loadMapping at line 1683 of /var/www/foswikidev/core/lib/Foswiki/Users/TopicUserMapping.pm
# once (10µs+555µs) by Foswiki::UI::View::view at line 129 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm
# once (9µs+552µs) by Foswiki::Func::checkAccessPermission at line 1486 of /var/www/foswikidev/core/lib/Foswiki/Func.pm | ||||
436 | 26296 | 7.25ms | my $proto = shift; | ||
437 | 26296 | 2.76ms | my $this; | ||
438 | 26296 | 2.03ms | my $rev; | ||
439 | |||||
440 | 26296 | 10.2ms | if ( ref($proto) ) { | ||
441 | |||||
442 | # Existing unloaded object | ||||
443 | ASSERT( !$this->{_loadedRev} ) if DEBUG; | ||||
444 | $this = $proto; | ||||
445 | $rev = shift; | ||||
446 | } | ||||
447 | else { | ||||
448 | 26296 | 30.9ms | ( my $session, my $web, my $topic, $rev ) = @_; | ||
449 | 26296 | 52.6ms | 26296 | 348ms | $this = $proto->new( $session, $web, $topic ); # spent 348ms making 26296 calls to Foswiki::Meta::new, avg 13µs/call |
450 | } | ||||
451 | |||||
452 | 26296 | 8.12ms | my $session = $this->{_session}; | ||
453 | |||||
454 | # if ( defined( $this->topic ) | ||||
455 | # and ( not defined($rev) ) | ||||
456 | # and $this->existsInStore() ) | ||||
457 | # { | ||||
458 | #SVEN: sadly, Item10805 shows that the metacache is not yet multi-user safe, and as the Groups code in TopicUserMapping changes to user=admin, we can't use it here | ||||
459 | #which makes it clear I need to write a full cache validation set of tests for MetaCache | ||||
460 | #TODO: need to extract the metacache from search, and extract the additional derived info from it too | ||||
461 | #NEW: the metacache has to return a _copy_ of the cached item, otherwise code that ->finish() es its copy will also ->finish() the cached version and any other refs. | ||||
462 | # which in Sven's opinion means we need to invert things better. (I get ~10% (.2S on 2S req's) speedup on simpler SEARCH topics doing reuse) | ||||
463 | # my $m = | ||||
464 | # $session->search->metacache->getMeta( $this->web, $this->topic ); | ||||
465 | # | ||||
466 | #print STDERR "metacache->getMeta ".join(',', ( $this->web, $this->topic, ref($m) ))."\n"; | ||||
467 | # return $m if ( defined($m) ); | ||||
468 | # } | ||||
469 | |||||
470 | ASSERT( not( $this->{_latestIsLoaded} ) ) if DEBUG; | ||||
471 | |||||
472 | 26296 | 39.3ms | 26296 | 76.4s | my $loadedRev = $this->loadVersion($rev); # spent 76.4s making 26296 calls to Foswiki::Meta::loadVersion, avg 2.90ms/call |
473 | |||||
474 | 26296 | 11.2ms | if ( not defined($loadedRev) ) { | ||
475 | ASSERT( not defined( $this->{_loadedRev} ) ) if DEBUG; | ||||
476 | |||||
477 | #_latestIsloaded is mostly undef / 0 when the topic is not ondisk, except Fn_SEARCH::verify_refQuery_ForkingSearch and friends | ||||
478 | ASSERT( not( $this->{_latestIsLoaded} ) ) if DEBUG; | ||||
479 | } | ||||
480 | else { | ||||
481 | ASSERT( defined( $this->{_loadedRev} ) ) if DEBUG; | ||||
482 | 26296 | 1.77ms | ASSERT( ( $this->{_loadedRev} > 0 ), | ||
483 | "loadedRev is non-zero: $this->{_loadedRev}" ) | ||||
484 | if DEBUG; | ||||
485 | 26296 | 3.36ms | ASSERT( defined( $this->{_latestIsLoaded} ) ) if DEBUG; | ||
486 | } | ||||
487 | |||||
488 | 26296 | 75.7ms | return $this; | ||
489 | } | ||||
490 | |||||
491 | =begin TML | ||||
492 | |||||
493 | ---++ ObjectMethod unload() | ||||
494 | |||||
495 | Return the object to an unloaded state. This method should be used | ||||
496 | with the greatest of care, as it resets the load state of the object, | ||||
497 | which may have surprising effects on other code that shares the object. | ||||
498 | |||||
499 | =cut | ||||
500 | |||||
501 | # spent 13.0ms (82µs+12.9) within Foswiki::Meta::unload which was called 3 times, avg 4.34ms/call:
# 3 times (82µs+12.9ms) by Foswiki::Meta::finish at line 533, avg 4.34ms/call | ||||
502 | 3 | 900ns | my $this = shift; | ||
503 | |||||
504 | 3 | 26µs | 15 | 12.9ms | $this->{_session}->search->metacache->removeMeta( $this->web, $this->topic ) # spent 12.9ms making 3 calls to Foswiki::search, avg 4.29ms/call
# spent 35µs making 3 calls to Foswiki::MetaCache::removeMeta, avg 12µs/call
# spent 21µs making 3 calls to Foswiki::Search::metacache, avg 7µs/call
# spent 5µs making 3 calls to Foswiki::Meta::web, avg 2µs/call
# spent 4µs making 3 calls to Foswiki::Meta::topic, avg 1µs/call |
505 | if $this->{_session}; | ||||
506 | 3 | 2µs | $this->{_loadedRev} = undef; | ||
507 | 3 | 500ns | $this->{_latestIsLoaded} = undef; | ||
508 | 3 | 500ns | $this->{_text} = undef; | ||
509 | 3 | 1µs | $this->{_preferences}->finish() if defined $this->{_preferences}; | ||
510 | 3 | 2µs | undef $this->{_preferences}; | ||
511 | 3 | 1µs | $this->{_preferences} = undef; | ||
512 | |||||
513 | # Unload meta-data | ||||
514 | 3 | 8µs | foreach my $type ( keys %{ $this->{_indices} } ) { | ||
515 | 6 | 11µs | delete $this->{$type}; | ||
516 | } | ||||
517 | 3 | 9µs | undef $this->{_indices}; | ||
518 | } | ||||
519 | |||||
520 | =begin TML | ||||
521 | |||||
522 | ---++ ObjectMethod finish() | ||||
523 | Clean up the object, releasing any memory stored in it. Make sure this | ||||
524 | gets called before an object you have created goes out of scope. | ||||
525 | |||||
526 | =cut | ||||
527 | |||||
528 | # Note to developers; please undef *all* fields in the object explicitly, | ||||
529 | # whether they are references or not. That way this method is "golden | ||||
530 | # documentation" of the live fields in the object. | ||||
531 | # spent 13.0ms (20µs+13.0) within Foswiki::Meta::finish which was called 3 times, avg 4.35ms/call:
# 3 times (20µs+13.0ms) by Foswiki::Store::Rcs::Store::getVersionInfo at line 363 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 4.35ms/call | ||||
532 | 3 | 1µs | my $this = shift; | ||
533 | 3 | 7µs | 3 | 13.0ms | $this->unload(); # spent 13.0ms making 3 calls to Foswiki::Meta::unload, avg 4.34ms/call |
534 | 3 | 2µs | undef $this->{_web}; | ||
535 | 3 | 600ns | undef $this->{_topic}; | ||
536 | 3 | 600ns | undef $this->{_session}; | ||
537 | 3 | 7µs | if (DEBUG) { | ||
538 | |||||
539 | #someone keeps adding random references to Meta so to shake them out.. | ||||
540 | #if its an intentional ref to an object, please add it to the undef's above. | ||||
541 | |||||
542 | #SMELL: Sven noticed during development that something is adding a $this->{store} to a meta obj - havn't found it yet | ||||
543 | #ASSERT(not defined($this->{store})) if DEBUG; | ||||
544 | |||||
545 | 2 | 10.2ms | 2 | 52µs | # spent 32µs (11+21) within Foswiki::Meta::BEGIN@545 which was called:
# once (11µs+21µs) by Foswiki::BEGIN@642 at line 545 # spent 32µs making 1 call to Foswiki::Meta::BEGIN@545
# spent 21µs making 1 call to Exporter::import |
546 | foreach my $key (%$this) { | ||||
547 | |||||
548 | #ASSERT(not defined(blessed($this->{$key}))); | ||||
549 | } | ||||
550 | } | ||||
551 | } | ||||
552 | |||||
553 | =begin TML | ||||
554 | |||||
555 | ---++ ObjectMethod session() | ||||
556 | |||||
557 | Get the session (Foswiki) object associated with the object when | ||||
558 | it was created. | ||||
559 | |||||
560 | =cut | ||||
561 | |||||
562 | # spent 67µs within Foswiki::Meta::session which was called 49 times, avg 1µs/call:
# 16 times (24µs+0s) by Foswiki::If::OP_dollar::evaluate at line 37 of /var/www/foswikidev/core/lib/Foswiki/If/OP_dollar.pm, avg 2µs/call
# 13 times (16µs+0s) by Foswiki::If::OP_defined::evaluate at line 34 of /var/www/foswikidev/core/lib/Foswiki/If/OP_defined.pm, avg 1µs/call
# 12 times (17µs+0s) by Foswiki::If::OP_context::evaluate at line 39 of /var/www/foswikidev/core/lib/Foswiki/If/OP_context.pm, avg 1µs/call
# 4 times (5µs+0s) by Foswiki::If::OP_istopic::evaluate at line 34 of /var/www/foswikidev/core/lib/Foswiki/If/OP_istopic.pm, avg 1µs/call
# 3 times (4µs+0s) by Foswiki::Meta::new at line 348, avg 1µs/call
# once (1µs+0s) by Foswiki::If::OP_allows::evaluate at line 40 of /var/www/foswikidev/core/lib/Foswiki/If/OP_allows.pm | ||||
563 | 49 | 129µs | return $_[0]->{_session}; | ||
564 | } | ||||
565 | |||||
566 | # Assert helpers | ||||
567 | sub _assertIsTopic { | ||||
568 | my $this = shift; | ||||
569 | ASSERT( $this->isa('Foswiki::Meta') ); | ||||
570 | ASSERT( defined $this->{_web} && $this->{_topic}, 'not a topic object' ); | ||||
571 | } | ||||
572 | |||||
573 | sub _assertIsWeb { | ||||
574 | my $this = shift; | ||||
575 | ASSERT( $this->isa('Foswiki::Meta') ); | ||||
576 | ASSERT( $this->{_web} && !$this->{_topic}, 'not a web object' ); | ||||
577 | } | ||||
578 | |||||
579 | # Does not test attachment existance, just validity of the name | ||||
580 | sub _assertIsAttachment { | ||||
581 | my ( $this, $name ) = @_; | ||||
582 | $this->_assertIsTopic(); | ||||
583 | ASSERT( $name, 'not a valid attachment name' ); | ||||
584 | } | ||||
585 | |||||
586 | =begin TML | ||||
587 | |||||
588 | ---++ ObjectMethod web([$name]) | ||||
589 | * =$name= - optional, change the web name in the object | ||||
590 | * *Since* 28 Nov 2008 | ||||
591 | Get/set the web name associated with the object. | ||||
592 | |||||
593 | =cut | ||||
594 | |||||
595 | # spent 53.8ms within Foswiki::Meta::web which was called 35941 times, avg 1µs/call:
# 26327 times (39.3ms+0s) by Foswiki::Store::Rcs::Store::readTopic at line 99 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 1µs/call
# 8760 times (13.2ms+0s) by Foswiki::Search::InfoCache::addTopic at line 98 of /var/www/foswikidev/core/lib/Foswiki/Search/InfoCache.pm, avg 2µs/call
# 238 times (346µs+0s) by Foswiki::innerExpandMacros at line 3068 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 1µs/call
# 222 times (302µs+0s) by Foswiki::Prefs::loadPreferences at line 237 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 1µs/call
# 100 times (145µs+0s) by Foswiki::expandMacros at line 3595 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 1µs/call
# 100 times (144µs+0s) by Foswiki::expandMacros at line 3610 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 1µs/call
# 40 times (93µs+0s) by Foswiki::SEARCH at line 19 of /var/www/foswikidev/core/lib/Foswiki/Macros/SEARCH.pm, avg 2µs/call
# 40 times (58µs+0s) by Foswiki::Store::Rcs::Store::eachTopic at line 589 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 1µs/call
# 18 times (33µs+0s) by Foswiki::Render::_handleSquareBracketedLink at line 1562 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 2µs/call
# 14 times (21µs+0s) by Foswiki::Func::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Func.pm:662] at line 660 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 1µs/call
# 12 times (18µs+0s) by Foswiki::_includeTopic at line 143 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 10 times (17µs+0s) by Foswiki::INCLUDE at line 403 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 8 times (17µs+0s) by Foswiki::_includeTopic at line 164 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 8 times (16µs+0s) by Foswiki::Prefs::popTopicContext at line 319 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 2µs/call
# 8 times (16µs+0s) by Foswiki::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm:339] at line 206 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 8 times (14µs+0s) by Foswiki::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm:339] at line 317 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 5 times (11µs+0s) by Foswiki::Render::getRenderedVersion at line 261 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 2µs/call
# 3 times (5µs+0s) by Foswiki::Meta::unload at line 504, avg 2µs/call
# 3 times (5µs+0s) by Foswiki::REVINFO at line 20 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 2µs/call
# 3 times (5µs+0s) by Foswiki::Meta::new at line 348, avg 2µs/call
# 3 times (5µs+0s) by Foswiki::REVINFO at line 29 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 2µs/call
# 2 times (4µs+0s) by Foswiki::UI::View::revisionsAround at line 547 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm, avg 2µs/call
# 2 times (4µs+0s) by Foswiki::UI::View::revisionsAround at line 559 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm, avg 2µs/call
# 2 times (3µs+0s) by Foswiki::Store::Rcs::Store::getRevisionHistory at line 274 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 2µs/call
# 2 times (3µs+0s) by Foswiki::Store::Rcs::Store::eachWeb at line 601 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 2µs/call
# once (2µs+0s) by Foswiki::Render::Parent::render at line 35 of /var/www/foswikidev/core/lib/Foswiki/Render/Parent.pm
# once (2µs+0s) by Foswiki::UI::View::view at line 256 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm
# once (2µs+0s) by Foswiki::Store::Rcs::Store::attachmentExists at line 230 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm | ||||
596 | 35941 | 11.6ms | my ( $this, $web ) = @_; | ||
597 | 35941 | 5.11ms | $this->{_web} = $web if defined $web; | ||
598 | 35941 | 109ms | return $this->{_web}; | ||
599 | } | ||||
600 | |||||
601 | =begin TML | ||||
602 | |||||
603 | ---++ ObjectMethod topic([$name]) | ||||
604 | * =$name= - optional, change the topic name in the object | ||||
605 | * *Since* 28 Nov 2008 | ||||
606 | Get/set the topic name associated with the object. | ||||
607 | |||||
608 | =cut | ||||
609 | |||||
610 | # spent 70.6ms within Foswiki::Meta::topic which was called 44523 times, avg 2µs/call:
# 26327 times (39.1ms+0s) by Foswiki::Store::Rcs::Store::readTopic at line 99 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 1µs/call
# 8760 times (18.2ms+0s) by Foswiki::Store::Interfaces::QueryAlgorithm::getField at line 340 of /var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm, avg 2µs/call
# 8760 times (12.4ms+0s) by Foswiki::Search::InfoCache::addTopic at line 99 of /var/www/foswikidev/core/lib/Foswiki/Search/InfoCache.pm, avg 1µs/call
# 238 times (312µs+0s) by Foswiki::innerExpandMacros at line 3069 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 1µs/call
# 112 times (170µs+0s) by Foswiki::Prefs::loadPreferences at line 237 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 2µs/call
# 100 times (142µs+0s) by Foswiki::expandMacros at line 3595 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 1µs/call
# 100 times (134µs+0s) by Foswiki::expandMacros at line 3611 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 1µs/call
# 40 times (71µs+0s) by Foswiki::SEARCH at line 20 of /var/www/foswikidev/core/lib/Foswiki/Macros/SEARCH.pm, avg 2µs/call
# 14 times (25µs+0s) by Foswiki::Func::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Func.pm:662] at line 660 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 2µs/call
# 12 times (19µs+0s) by Foswiki::VAR at line 19 of /var/www/foswikidev/core/lib/Foswiki/Macros/VAR.pm, avg 2µs/call
# 10 times (16µs+0s) by Foswiki::INCLUDE at line 404 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 8 times (12µs+0s) by Foswiki::Prefs::popTopicContext at line 319 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 2µs/call
# 8 times (12µs+0s) by Foswiki::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm:339] at line 206 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 8 times (12µs+0s) by Foswiki::_includeTopic at line 164 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 2µs/call
# 5 times (8µs+0s) by Foswiki::Render::getRenderedVersion at line 261 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 2µs/call
# 3 times (5µs+0s) by Foswiki::REVINFO at line 21 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 2µs/call
# 3 times (4µs+0s) by Foswiki::Meta::unload at line 504, avg 1µs/call
# 3 times (4µs+0s) by Foswiki::Meta::new at line 348, avg 1µs/call
# 3 times (4µs+0s) by Foswiki::REVINFO at line 29 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 1µs/call
# 2 times (3µs+0s) by Foswiki::Store::Rcs::Store::getRevisionHistory at line 274 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 2µs/call
# 2 times (3µs+0s) by Foswiki::UI::View::revisionsAround at line 559 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm, avg 2µs/call
# 2 times (3µs+0s) by Foswiki::UI::View::revisionsAround at line 547 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm, avg 2µs/call
# once (2µs+0s) by Foswiki::Store::Rcs::Store::attachmentExists at line 230 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm
# once (2µs+0s) by Foswiki::Render::Parent::render at line 35 of /var/www/foswikidev/core/lib/Foswiki/Render/Parent.pm
# once (2µs+0s) by Foswiki::UI::View::view at line 256 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm | ||||
611 | 44523 | 15.4ms | my ( $this, $topic ) = @_; | ||
612 | 44523 | 5.69ms | $this->{_topic} = $topic if defined $topic; | ||
613 | 44523 | 187ms | return $this->{_topic}; | ||
614 | } | ||||
615 | |||||
616 | =begin TML | ||||
617 | |||||
618 | ---++ ObjectMethod getPath() -> $objectpath | ||||
619 | |||||
620 | Get the canonical content access path for the object. For example, | ||||
621 | a topic "MyTopic" in subweb "Subweb" of web "Myweb" will have an | ||||
622 | access path "Myweb/Subweb.MyTopic" | ||||
623 | |||||
624 | =cut | ||||
625 | |||||
626 | # spent 421µs within Foswiki::Meta::getPath which was called 172 times, avg 2µs/call:
# 112 times (270µs+0s) by Foswiki::Prefs::loadPreferences at line 229 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 2µs/call
# 54 times (130µs+0s) by Foswiki::Prefs::_getBackend at line 147 of /var/www/foswikidev/core/lib/Foswiki/Prefs.pm, avg 2µs/call
# 5 times (18µs+0s) by Foswiki::Render::getAnchorNames at line 1050 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 4µs/call
# once (3µs+0s) by Foswiki::_lookupIcon at line 53 of /var/www/foswikidev/core/lib/Foswiki/Macros/ICON.pm | ||||
627 | 172 | 50µs | my $this = shift; | ||
628 | 172 | 65µs | my $path = $this->{_web}; | ||
629 | |||||
630 | 172 | 21µs | return '' unless $path; | ||
631 | 172 | 306µs | return $path unless $this->{_topic}; | ||
632 | 61 | 33µs | $path .= '.' . $this->{_topic}; | ||
633 | 61 | 125µs | return $path; | ||
634 | } | ||||
635 | |||||
636 | =begin TML | ||||
637 | |||||
638 | ---++ ObjectMethod isSessionTopic() -> $boolean | ||||
639 | Return true if this object refers to the session topic. The session | ||||
640 | topic is established from the path used to invoke Foswiki, for example | ||||
641 | ".../view/Myweb/MyTopic" sets "Myweb.MyTopic" as the session topic. | ||||
642 | |||||
643 | =cut | ||||
644 | |||||
645 | sub isSessionTopic { | ||||
646 | my $this = shift; | ||||
647 | return 0 | ||||
648 | unless defined $this->{_web} | ||||
649 | && defined $this->{_topic} | ||||
650 | && defined $this->{_session}->{webName} | ||||
651 | && defined $this->{_session}->{topicName}; | ||||
652 | return $this->{_web} eq $this->{_session}->{webName} | ||||
653 | && $this->{_topic} eq $this->{_session}->{topicName}; | ||||
654 | } | ||||
655 | |||||
656 | =begin TML | ||||
657 | |||||
658 | ---++ ObjectMethod getPreference( $key ) -> $value | ||||
659 | |||||
660 | Get a value for a preference defined *in* the object. Note that | ||||
661 | web preferences always inherit from parent webs, but topic preferences | ||||
662 | are strictly local to topics. | ||||
663 | |||||
664 | Note that this is *not* the same as =Foswiki::Func::getPreferencesValue=, | ||||
665 | which is almost certainly what you want to call instead. | ||||
666 | |||||
667 | =cut | ||||
668 | |||||
669 | # spent 23.5ms (1.11+22.4) within Foswiki::Meta::getPreference which was called 124 times, avg 189µs/call:
# 80 times (852µs+5.07ms) by Foswiki::Store::Interfaces::QueryAlgorithm::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm:235] at line 224 of /var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm, avg 74µs/call
# 19 times (121µs+16.5ms) by Foswiki::WebFilter::ok at line 51 of /var/www/foswikidev/core/lib/Foswiki/WebFilter.pm, avg 876µs/call
# 13 times (70µs+331µs) by Foswiki::If::OP_defined::evaluate at line 45 of /var/www/foswikidev/core/lib/Foswiki/If/OP_defined.pm, avg 31µs/call
# 12 times (71µs+442µs) by Foswiki::VAR at line 27 of /var/www/foswikidev/core/lib/Foswiki/Macros/VAR.pm, avg 43µs/call | ||||
670 | 124 | 129µs | my ( $this, $key ) = @_; | ||
671 | |||||
672 | 124 | 48µs | unless ( $this->{_web} || $this->{_topic} ) { | ||
673 | return $this->{_session}->{prefs}->getPreference($key); | ||||
674 | } | ||||
675 | |||||
676 | # make sure the preferences are parsed and cached | ||||
677 | 124 | 389µs | 112 | 19.2ms | unless ( $this->{_preferences} ) { # spent 19.2ms making 112 calls to Foswiki::Prefs::loadPreferences, avg 172µs/call |
678 | $this->{_preferences} = | ||||
679 | $this->{_session}->{prefs}->loadPreferences($this); | ||||
680 | } | ||||
681 | 124 | 520µs | 124 | 3.15ms | return $this->{_preferences}->get($key); # spent 3.13ms making 111 calls to Foswiki::Prefs::Web::get, avg 28µs/call
# spent 18µs making 13 calls to Foswiki::Prefs::TopicRAM::get, avg 1µs/call |
682 | } | ||||
683 | |||||
684 | =begin TML | ||||
685 | |||||
686 | ---++ ObjectMethod getContainer() -> $containerObject | ||||
687 | |||||
688 | Get the container of this object; for example, the web that a topic is within | ||||
689 | |||||
690 | =cut | ||||
691 | |||||
692 | sub getContainer { | ||||
693 | my $this = shift; | ||||
694 | |||||
695 | if ( $this->{_topic} ) { | ||||
696 | return Foswiki::Meta->new( $this->{_session}, $this->{_web} ); | ||||
697 | } | ||||
698 | if ( $this->{_web} ) { | ||||
699 | return Foswiki::Meta->new( $this->{_session} ); | ||||
700 | } | ||||
701 | ASSERT( 0, 'no container for this object type' ) if DEBUG; | ||||
702 | return; | ||||
703 | } | ||||
704 | |||||
705 | =begin TML | ||||
706 | |||||
707 | ---++ ObjectMethod existsInStore() -> $boolean | ||||
708 | |||||
709 | A Meta object can be created for a web or topic that doesn't exist in the | ||||
710 | actual store (e.g. is in the process of being created). This method returns | ||||
711 | true if the corresponding web or topic really exists in the store. | ||||
712 | |||||
713 | =cut | ||||
714 | |||||
715 | # spent 2.01ms (182µs+1.82) within Foswiki::Meta::existsInStore which was called 29 times, avg 69µs/call:
# 29 times (182µs+1.82ms) by Foswiki::Prefs::TopicRAM::new at line 38 of /var/www/foswikidev/core/lib/Foswiki/Prefs/TopicRAM.pm, avg 69µs/call | ||||
716 | 29 | 6µs | my $this = shift; | ||
717 | 29 | 14µs | if ( defined $this->{_topic} ) { | ||
718 | |||||
719 | # only checking for a topic existence already establishes a dependency | ||||
720 | 29 | 25µs | 29 | 43µs | $this->addDependency(); # spent 43µs making 29 calls to Foswiki::Meta::addDependency, avg 1µs/call |
721 | |||||
722 | 29 | 100µs | 29 | 1.78ms | return $this->{_session}->{store} # spent 1.78ms making 29 calls to Foswiki::Store::Rcs::Store::topicExists, avg 61µs/call |
723 | ->topicExists( $this->{_web}, $this->{_topic} ); | ||||
724 | } | ||||
725 | elsif ( defined $this->{_web} ) { | ||||
726 | return $this->{_session}->{store}->webExists( $this->{_web} ); | ||||
727 | } | ||||
728 | else { | ||||
729 | return 1; # the root always exists | ||||
730 | } | ||||
731 | } | ||||
732 | |||||
733 | =begin TML | ||||
734 | |||||
735 | ---++ ObjectMethod stringify( $debug ) -> $string | ||||
736 | |||||
737 | Return a string version of the meta object. $debug adds | ||||
738 | extra debug info. | ||||
739 | |||||
740 | =cut | ||||
741 | |||||
742 | sub stringify { | ||||
743 | my ( $this, $debug ) = @_; | ||||
744 | my $s = $this->{_web}; | ||||
745 | if ( $this->{_topic} ) { | ||||
746 | $s .= ".$this->{_topic} "; | ||||
747 | $s .= | ||||
748 | ( defined $this->{_loadedRev} ) | ||||
749 | ? $this->{_loadedRev} | ||||
750 | : '(not loaded)' | ||||
751 | if $debug; | ||||
752 | $s .= "\n" . Foswiki::Serialise::serialise( $this, 'Embedded' ); | ||||
753 | } | ||||
754 | return $s; | ||||
755 | } | ||||
756 | |||||
757 | =begin TML | ||||
758 | |||||
759 | ---++ ObjectMethod addDependency() -> $this | ||||
760 | |||||
761 | This establishes a caching dependency between $this and the | ||||
762 | base topic this session is currently rendering. The dependency | ||||
763 | will be asserted during Foswiki::PageCache::cachePage(). | ||||
764 | See Foswiki::PageCache::addDependency(). | ||||
765 | |||||
766 | =cut | ||||
767 | |||||
768 | sub addDependency { | ||||
769 | 26356 | 22.7ms | my $cache = $_[0]->{_session}->{cache}; | ||
770 | 26356 | 122ms | return unless $cache; | ||
771 | return $cache->addDependency( $_[0]->{_web}, $_[0]->{_topic} ); | ||||
772 | } | ||||
773 | |||||
774 | =begin TML | ||||
775 | |||||
776 | ---++ ObjectMethod fireDependency() -> $this | ||||
777 | |||||
778 | Invalidates the cache bucket of the current meta object | ||||
779 | within the Foswiki::PageCache. See Foswiki::PageCache::fireDependency(). | ||||
780 | |||||
781 | =cut | ||||
782 | |||||
783 | sub fireDependency { | ||||
784 | my $cache = $_[0]->{_session}->{cache}; | ||||
785 | return unless $cache; | ||||
786 | return $cache->fireDependency( $_[0]->{_web}, $_[0]->{_topic} ); | ||||
787 | } | ||||
788 | |||||
789 | =begin TML | ||||
790 | |||||
791 | ---++ ObjectMethod isCacheable() -> $boolean | ||||
792 | |||||
793 | Return true if page caching is enabled and this topic object is cacheable. | ||||
794 | |||||
795 | =cut | ||||
796 | |||||
797 | # spent 645µs within Foswiki::Meta::isCacheable which was called 384 times, avg 2µs/call:
# 100 times (172µs+0s) by Foswiki::expandMacros at line 3606 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 2µs/call
# 100 times (166µs+0s) by Foswiki::expandMacros at line 3652 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 2µs/call
# 87 times (145µs+0s) by Foswiki::_processMacros at line 3252 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 2µs/call
# 87 times (141µs+0s) by Foswiki::_processMacros at line 3369 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 2µs/call
# 5 times (10µs+0s) by Foswiki::Render::getRenderedVersion at line 559 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 2µs/call
# 5 times (10µs+0s) by Foswiki::Render::getRenderedVersion at line 231 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 2µs/call | ||||
798 | 384 | 90µs | my $this = shift; | ||
799 | |||||
800 | 384 | 917µs | return 0 unless $Foswiki::cfg{Cache}{Enabled}; | ||
801 | |||||
802 | my $cache = $this->{_session}->{cache}; | ||||
803 | return 0 unless $cache; | ||||
804 | |||||
805 | return $cache->isCacheable( $this->{_web}, $this->{_topic} ); | ||||
806 | } | ||||
807 | |||||
808 | ############# WEB METHODS ############# | ||||
809 | |||||
810 | =begin TML | ||||
811 | |||||
812 | ---++ ObjectMethod populateNewWeb( [$baseWeb [, $opts]] ) | ||||
813 | |||||
814 | $baseWeb is the name of an existing web (a template web). If the | ||||
815 | base web is a system web, all topics in it | ||||
816 | will be copied into this web. If it is a normal web, only topics starting | ||||
817 | with 'Web' will be copied. If no base web is specified, an empty web | ||||
818 | (with no topics) will be created. If it is specified but does not exist, | ||||
819 | an error will be thrown. | ||||
820 | |||||
821 | $opts is a ref to a hash that contains settings to be modified in | ||||
822 | the web preferences topic in the new web. | ||||
823 | |||||
824 | =cut | ||||
825 | |||||
826 | # SMELL: there seems to be no reason to call this method 'NewWeb', it can | ||||
827 | # be used to copy into an existing web and it does not appear to be | ||||
828 | # unexpectedly destructive. | ||||
829 | # perhaps refactor into something that takes a resultset as an input list? | ||||
830 | # (users have asked to be able to copy a SEARCH'd set of topics..) | ||||
831 | sub populateNewWeb { | ||||
832 | my ( $this, $templateWeb, $opts ) = @_; | ||||
833 | _assertIsWeb($this) if DEBUG; | ||||
834 | |||||
835 | my $session = $this->{_session}; | ||||
836 | |||||
837 | my ( $parent, $new ) = $this->{_web} =~ m/^(.*)\/([^\.\/]+)$/; | ||||
838 | |||||
839 | if ($parent) { | ||||
840 | unless ( $Foswiki::cfg{EnableHierarchicalWebs} ) { | ||||
841 | throw Error::Simple( 'Unable to create ' | ||||
842 | . $this->{_web} | ||||
843 | . ' - Hierarchical webs are disabled' ); | ||||
844 | } | ||||
845 | |||||
846 | unless ( $session->webExists($parent) ) { | ||||
847 | throw Error::Simple( 'Parent web ' . $parent . ' does not exist' ); | ||||
848 | } | ||||
849 | } | ||||
850 | |||||
851 | # Validate that template web exists, or error should be thrown | ||||
852 | if ($templateWeb) { | ||||
853 | unless ( $session->webExists($templateWeb) ) { | ||||
854 | throw Error::Simple( | ||||
855 | 'Template web ' . $templateWeb . ' does not exist' ); | ||||
856 | } | ||||
857 | } | ||||
858 | |||||
859 | # Make sure there is a preferences topic; this is how we know it's a web | ||||
860 | my $prefsTopicObject; | ||||
861 | if ( | ||||
862 | !$session->topicExists( | ||||
863 | $this->{_web}, $Foswiki::cfg{WebPrefsTopicName} | ||||
864 | ) | ||||
865 | ) | ||||
866 | { | ||||
867 | my $prefsText = 'Preferences'; | ||||
868 | $prefsTopicObject = | ||||
869 | $this->new( $this->{_session}, $this->{_web}, | ||||
870 | $Foswiki::cfg{WebPrefsTopicName}, $prefsText ); | ||||
871 | $prefsTopicObject->save(); | ||||
872 | } | ||||
873 | |||||
874 | if ($templateWeb) { | ||||
875 | my $tWebObject = $this->new( $session, $templateWeb ); | ||||
876 | require Foswiki::WebFilter; | ||||
877 | my $sys = | ||||
878 | Foswiki::WebFilter->new('template')->ok( $session, $templateWeb ); | ||||
879 | my $it = $tWebObject->eachTopic(); | ||||
880 | while ( $it->hasNext() ) { | ||||
881 | my $topic = $it->next(); | ||||
882 | next unless ( $sys || $topic =~ m/^Web/ ); | ||||
883 | my $to = | ||||
884 | Foswiki::Meta->load( $this->{_session}, $templateWeb, $topic ); | ||||
885 | |||||
886 | # Open attachment filehandles | ||||
887 | my %attfh; | ||||
888 | foreach my $sfa ( $to->find('FILEATTACHMENT') ) { | ||||
889 | my $fh = $to->openAttachment( $sfa->{name}, '<' ); | ||||
890 | $attfh{ $sfa->{name} } = { | ||||
891 | fh => $fh, | ||||
892 | date => $sfa->{date}, | ||||
893 | user => $sfa->{user} || $session->{user}, | ||||
894 | comment => $sfa->{comment} | ||||
895 | }; | ||||
896 | } | ||||
897 | $to->saveAs( | ||||
898 | web => $this->{_web}, | ||||
899 | topic => $topic, | ||||
900 | forcenewrevision => 1 | ||||
901 | ); | ||||
902 | |||||
903 | # copy fileattachments | ||||
904 | while ( my ( $fa, $sfa ) = each %attfh ) { | ||||
905 | my $arev = $session->{store}->saveAttachment( | ||||
906 | $to, $fa, | ||||
907 | $sfa->{fh}, | ||||
908 | $sfa->{user}, | ||||
909 | { | ||||
910 | forcedate => $sfa->{date}, | ||||
911 | minor => 1, | ||||
912 | comment => $sfa->{comment} | ||||
913 | } | ||||
914 | ); | ||||
915 | close( $sfa->{fh} ); | ||||
916 | ASSERT($arev) if DEBUG; | ||||
917 | $this->{_session}->{store}->recordChange( | ||||
918 | verb => 'insert', | ||||
919 | cuid => $sfa->{user}, | ||||
920 | revision => $to->{_loadedRev}, | ||||
921 | path => $to->getPath(), | ||||
922 | attachment => $fa, | ||||
923 | comment => "add $arev" | ||||
924 | ); | ||||
925 | } | ||||
926 | } | ||||
927 | } | ||||
928 | |||||
929 | # patch WebPreferences in new web. We ignore permissions, because | ||||
930 | # we are creating a new web here. | ||||
931 | if ($opts) { | ||||
932 | my $prefsTopicObject = | ||||
933 | Foswiki::Meta->load( $this->{_session}, $this->{_web}, | ||||
934 | $Foswiki::cfg{WebPrefsTopicName} ); | ||||
935 | my $text = $prefsTopicObject->text; | ||||
936 | foreach my $key ( keys %$opts ) { | ||||
937 | |||||
938 | #don't create the required params to create web. | ||||
939 | next if ( $key eq 'BASEWEB' ); | ||||
940 | next if ( $key eq 'NEWWEB' ); | ||||
941 | next if ( $key eq 'NEWTOPIC' ); | ||||
942 | next if ( $key eq 'ACTION' ); | ||||
943 | |||||
944 | if ( defined( $opts->{$key} ) ) { | ||||
945 | if ( $text =~ | ||||
946 | s/($Foswiki::regex{setRegex}$key\s*=).*?$/$1 $opts->{$key}/gm | ||||
947 | ) | ||||
948 | { | ||||
949 | } | ||||
950 | else { | ||||
951 | |||||
952 | #this setting wasn't found, so we need to append it. | ||||
953 | $text .= "\n * Web Created with KEY set\n"; | ||||
954 | $text .= "\n * Set $key = $opts->{$key}\n"; | ||||
955 | } | ||||
956 | } | ||||
957 | } | ||||
958 | $prefsTopicObject->text($text); | ||||
959 | $prefsTopicObject->save(); | ||||
960 | } | ||||
961 | } | ||||
962 | |||||
963 | =begin TML | ||||
964 | |||||
965 | ---++ StaticMethod query($query, $inputTopicSet, \%options) -> $outputTopicSet | ||||
966 | |||||
967 | Search for topic information | ||||
968 | =$query= must be a =Foswiki::*::Node= object. | ||||
969 | |||||
970 | * $inputTopicSet is a reference to an iterator containing a list | ||||
971 | of topic in this web, if set to undef, the search/query algo will | ||||
972 | create a new iterator using eachTopic() | ||||
973 | and the web, topic and excludetopics options (as per SEARCH) | ||||
974 | * web option - The web/s to search in - string can have the same form | ||||
975 | as the =web= param of SEARCH | ||||
976 | |||||
977 | |||||
978 | Returns an Foswiki::Search::InfoCache iterator | ||||
979 | |||||
980 | =cut | ||||
981 | |||||
982 | # spent 108s (391µs+108) within Foswiki::Meta::query which was called 40 times, avg 2.71s/call:
# 40 times (391µs+108s) by Foswiki::Search::searchWeb at line 379 of /var/www/foswikidev/core/lib/Foswiki/Search.pm, avg 2.71s/call | ||||
983 | 40 | 48µs | my ( $query, $inputTopicSet, $options ) = @_; | ||
984 | 40 | 332µs | 40 | 108s | return $Foswiki::Plugins::SESSION->{store} # spent 108s making 40 calls to Foswiki::Store::Rcs::Store::query, avg 2.71s/call |
985 | ->query( $query, $inputTopicSet, $Foswiki::Plugins::SESSION, $options ); | ||||
986 | } | ||||
987 | |||||
988 | =begin TML | ||||
989 | |||||
990 | ---++ ObjectMethod eachWeb( $all ) -> $iterator | ||||
991 | |||||
992 | Return an iterator over each subweb. If =$all= is set, will return a | ||||
993 | list of all web names *under* the current location. Returns web pathnames | ||||
994 | relative to $this. | ||||
995 | |||||
996 | Only valid on webs and the root. | ||||
997 | |||||
998 | =cut | ||||
999 | |||||
1000 | # spent 17.9ms (16µs+17.9) within Foswiki::Meta::eachWeb which was called 2 times, avg 8.96ms/call:
# 2 times (16µs+17.9ms) by Foswiki::deepWebList at line 1652 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 8.96ms/call | ||||
1001 | 2 | 2µs | my ( $this, $all ) = @_; | ||
1002 | |||||
1003 | # Works on the root, so {_web} may be undef | ||||
1004 | ASSERT( !$this->{_topic}, 'this object may not contain webs' ) if DEBUG; | ||||
1005 | 2 | 14µs | 2 | 17.9ms | return $this->{_session}->{store}->eachWeb( $this, $all ); # spent 17.9ms making 2 calls to Foswiki::Store::Rcs::Store::eachWeb, avg 8.95ms/call |
1006 | |||||
1007 | } | ||||
1008 | |||||
1009 | =begin TML | ||||
1010 | |||||
1011 | ---++ ObjectMethod eachTopic() -> $iterator | ||||
1012 | |||||
1013 | Return an iterator over each topic name in the web. Only valid on webs. | ||||
1014 | |||||
1015 | =cut | ||||
1016 | |||||
1017 | # spent 2.79s (313µs+2.79) within Foswiki::Meta::eachTopic which was called 40 times, avg 69.8ms/call:
# 40 times (313µs+2.79s) by Foswiki::Search::InfoCache::getTopicListIterator at line 479 of /var/www/foswikidev/core/lib/Foswiki/Search/InfoCache.pm, avg 69.8ms/call | ||||
1018 | 40 | 30µs | my ($this) = @_; | ||
1019 | _assertIsWeb($this) if DEBUG; | ||||
1020 | 40 | 39µs | if ( !$this->{_web} ) { | ||
1021 | |||||
1022 | # Root | ||||
1023 | require Foswiki::ListIterator; | ||||
1024 | return new Foswiki::ListIterator( [] ); | ||||
1025 | } | ||||
1026 | 40 | 286µs | 40 | 2.79s | return $this->{_session}->{store}->eachTopic($this); # spent 2.79s making 40 calls to Foswiki::Store::Rcs::Store::eachTopic, avg 69.8ms/call |
1027 | } | ||||
1028 | |||||
1029 | =begin TML | ||||
1030 | |||||
1031 | ---++ ObjectMethod eachAttachment() -> $iterator | ||||
1032 | |||||
1033 | Return an iterator over each attachment name in the topic. | ||||
1034 | Only valid on topics. | ||||
1035 | |||||
1036 | The list of the names of attachments stored for the given topic may be a | ||||
1037 | longer list than the list that comes from the topic meta-data, which may | ||||
1038 | only lists the attachments that are normally visible to the user. | ||||
1039 | |||||
1040 | =cut | ||||
1041 | |||||
1042 | sub eachAttachment { | ||||
1043 | my ($this) = @_; | ||||
1044 | _assertIsTopic($this) if DEBUG; | ||||
1045 | return $this->{_session}->{store}->eachAttachment($this); | ||||
1046 | } | ||||
1047 | |||||
1048 | =begin TML | ||||
1049 | |||||
1050 | ---++ ObjectMethod eachChange( $time ) -> $iterator | ||||
1051 | |||||
1052 | Get an iterator over the list of all the changes to the object between | ||||
1053 | =$time= and now. $time is a time in seconds since 1st Jan 1970, and is not | ||||
1054 | guaranteed to return any changes that occurred before (now - | ||||
1055 | {Store}{RememberChangesFor}). Changes are returned in most-recent-first | ||||
1056 | order. | ||||
1057 | |||||
1058 | If the object is a web, changes for all topics within that web will be | ||||
1059 | iterated. If it's a topic, then all changes to the topic will be iterated. | ||||
1060 | |||||
1061 | Each element of the iterator is a reference to a hash: | ||||
1062 | * =verb= - the action - one of | ||||
1063 | * =update= - a web, topic or attachment has been modified | ||||
1064 | * =insert= - a web, topic or attachment is being inserted | ||||
1065 | * =remove= - a topic or attachment is being removed | ||||
1066 | * =time= - epoch-secs time of the change | ||||
1067 | * =cuid= - who is making the change | ||||
1068 | * =revision= - the revision of the topic that the change appears in | ||||
1069 | * =path= - canonical web.topic path for the affected object | ||||
1070 | * =attachment= - attachment name (optional) | ||||
1071 | * =oldpath= - canonical web.topic path for the origin of a move/rename | ||||
1072 | * =oldattachment= - origin of move | ||||
1073 | * =minor= - boolean true if this change is flagged as minor | ||||
1074 | * =comment= - descriptive text | ||||
1075 | |||||
1076 | Stores must return the following fields if compatibility with Foswiki < 2 | ||||
1077 | is required. | ||||
1078 | * =topic= - name of the topic the change occurred to | ||||
1079 | * =more= - formatted string indicating if the change was minor or not | ||||
1080 | * =user= - wikiname of the changing user | ||||
1081 | The fields are *deprecated* and should not be used by core. | ||||
1082 | |||||
1083 | =cut | ||||
1084 | |||||
1085 | sub eachChange { | ||||
1086 | my ( $this, $time ) = @_; | ||||
1087 | |||||
1088 | # not valid at root level | ||||
1089 | _assertIsWeb($this) if DEBUG; | ||||
1090 | return $this->{_session}->{store}->eachChange( $this, $time ); | ||||
1091 | } | ||||
1092 | |||||
1093 | ############# TOPIC METHODS ############# | ||||
1094 | |||||
1095 | =begin TML | ||||
1096 | |||||
1097 | ---++ ObjectMethod loadVersion($rev) -> $version | ||||
1098 | |||||
1099 | Load the object from the store. The object must not be already loaded | ||||
1100 | with a different rev (verified by an ASSERT) | ||||
1101 | |||||
1102 | See =getLoadedRev= to determine what revision is currently being viewed. | ||||
1103 | * =$rev= - revision to load. If undef, 0, '' or > max available rev, will | ||||
1104 | load the latest rev. If the revision is in range but does not exist, | ||||
1105 | then will return an unloaded meta object (getLoadedRev() will be undef) | ||||
1106 | |||||
1107 | Returns the version identifier for the loaded revision. (and undef if it failed to load) | ||||
1108 | |||||
1109 | WARNING: see notes on revision numbers under =getLoadedRev= | ||||
1110 | |||||
1111 | =cut | ||||
1112 | |||||
1113 | # spent 76.4s (512ms+75.9) within Foswiki::Meta::loadVersion which was called 26327 times, avg 2.90ms/call:
# 26296 times (512ms+75.9s) by Foswiki::Meta::load at line 472, avg 2.90ms/call
# 28 times (270µs+33.7ms) by Foswiki::Meta::text at line 1196, avg 1.21ms/call
# 3 times (30µs+1.65ms) by Foswiki::Store::Rcs::Store::getVersionInfo at line 360 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 560µs/call | ||||
1114 | 26327 | 12.8ms | my ( $this, $rev ) = @_; | ||
1115 | |||||
1116 | 26327 | 6.36ms | return unless $this->{_topic}; | ||
1117 | |||||
1118 | # If no specific rev was requested, check that the latest rev is | ||||
1119 | # loaded. | ||||
1120 | 26327 | 14.8ms | if ( !defined $rev || !$rev ) { | ||
1121 | |||||
1122 | # Trying to load the latest | ||||
1123 | if ( $this->{_latestIsLoaded} ) { | ||||
1124 | |||||
1125 | #TODO: these asserts trip up Comment Plugin | ||||
1126 | #ASSERT(defined($this->{_loadedRev})) if DEBUG; | ||||
1127 | #ASSERT($rev == $this->{_loadedRev}) if DEBUG; | ||||
1128 | return; | ||||
1129 | } | ||||
1130 | |||||
1131 | # SMELL: Sven added this assert, but i don't understand why and | ||||
1132 | # it causes PlainFile to fail for no good reason. C. | ||||
1133 | #ASSERT( not( $this->{_loadedRev} ), $this->{_loadedRev} ) if DEBUG; | ||||
1134 | } | ||||
1135 | elsif ( defined( $this->{_loadedRev} ) ) { | ||||
1136 | |||||
1137 | # Cannot load a different rev into an already-loaded | ||||
1138 | # Foswiki::Meta object | ||||
1139 | $rev = -1 unless defined $rev; | ||||
1140 | ASSERT( 0, "Attempt to reload $rev over version $this->{_loadedRev}" ) | ||||
1141 | if DEBUG; | ||||
1142 | } | ||||
1143 | |||||
1144 | # Is it already loaded? | ||||
1145 | ASSERT( !($rev) or $rev =~ m/^\s*\d+\s*/ ) if DEBUG; # looks like a number | ||||
1146 | 26327 | 3.12ms | return $this->{_loadedRev} | ||
1147 | if ( $rev && $this->{_loadedRev} && $rev == $this->{_loadedRev} ); | ||||
1148 | |||||
1149 | # SMELL: Sven added this assert, but i don't understand why and | ||||
1150 | # it causes PlainFile to fail for no good reason. C. | ||||
1151 | #ASSERT( not( $this->{_loadedRev} ) ) if DEBUG; | ||||
1152 | |||||
1153 | # Note: Since Item12472, the store implementation is expected | ||||
1154 | # to call setLoadStatus() in readTopic | ||||
1155 | 26327 | 31.5ms | 26327 | 48.6ms | $this->setLoadStatus( undef, undef ); # spent 48.6ms making 26327 calls to Foswiki::Meta::setLoadStatus, avg 2µs/call |
1156 | 26327 | 46.3ms | 26327 | 75.8s | $this->{_session}->{store}->readTopic( $this, $rev ); # spent 75.8s making 26327 calls to Foswiki::Store::Rcs::Store::readTopic, avg 2.88ms/call |
1157 | |||||
1158 | 26327 | 12.8ms | if ( defined( $this->{_loadedRev} ) ) { | ||
1159 | |||||
1160 | # Make sure text always has a value once loadVersion has been called | ||||
1161 | # once. | ||||
1162 | 26327 | 5.11ms | $this->{_text} = '' unless defined $this->{_text}; | ||
1163 | |||||
1164 | 26327 | 37.7ms | 26327 | 46.2ms | $this->addDependency(); # spent 46.2ms making 26327 calls to Foswiki::Meta::addDependency, avg 2µs/call |
1165 | } | ||||
1166 | else { | ||||
1167 | |||||
1168 | #we didn't load, so how could it be latest? | ||||
1169 | ASSERT( not $this->{_latestIsLoaded} ) if DEBUG; | ||||
1170 | } | ||||
1171 | |||||
1172 | 26327 | 91.8ms | return $this->{_loadedRev}; | ||
1173 | } | ||||
1174 | |||||
1175 | =begin TML | ||||
1176 | |||||
1177 | ---++ ObjectMethod text([$text]) -> $text | ||||
1178 | |||||
1179 | Get/set the topic body text. If $text is undef, gets the value, if it is | ||||
1180 | defined, sets the value to that and returns the new text. | ||||
1181 | |||||
1182 | Be warned - it can return undef - when a topic exists but has no topicText. | ||||
1183 | |||||
1184 | =cut | ||||
1185 | |||||
1186 | # spent 138ms (104+33.8) within Foswiki::Meta::text which was called 35131 times, avg 4µs/call:
# 26327 times (75.9ms+-136µs) by Foswiki::Serialise::Embedded::read at line 166 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 3µs/call
# 8760 times (27.6ms+0s) by Foswiki::Search::formatResults at line 789 of /var/www/foswikidev/core/lib/Foswiki/Search.pm, avg 3µs/call
# 29 times (173µs+34.0ms) by Foswiki::Prefs::Parser::parse at line 46 of /var/www/foswikidev/core/lib/Foswiki/Prefs/Parser.pm, avg 1.18ms/call
# 8 times (23µs+0s) by Foswiki::__ANON__[/var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm:339] at line 211 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 3µs/call
# 5 times (25µs+0s) by Foswiki::Func::readTopic at line 1662 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 5µs/call
# once (19µs+0s) by Foswiki::Users::TopicUserMapping::_loadMapping at line 1688 of /var/www/foswikidev/core/lib/Foswiki/Users/TopicUserMapping.pm
# once (4µs+0s) by Foswiki::UI::View::view at line 223 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm | ||||
1187 | 35131 | 18.9ms | my ( $this, $val ) = @_; | ||
1188 | _assertIsTopic($this) if DEBUG; | ||||
1189 | 35131 | 32.0ms | if ( defined($val) ) { | ||
1190 | $this->{_text} = $val; | ||||
1191 | } | ||||
1192 | else { | ||||
1193 | |||||
1194 | # Lazy load. Reload with no params will reload the _loadedRev, | ||||
1195 | # or load the latest if that is not defined. | ||||
1196 | 8804 | 3.86ms | 28 | 34.0ms | $this->loadVersion() unless defined( $this->{_text} ); # spent 34.0ms making 28 calls to Foswiki::Meta::loadVersion, avg 1.21ms/call |
1197 | } | ||||
1198 | 35131 | 142ms | return $this->{_text}; | ||
1199 | } | ||||
1200 | |||||
1201 | =begin TML | ||||
1202 | |||||
1203 | ---++ ObjectMethod put($type, \%args) | ||||
1204 | |||||
1205 | Put a hash of key=value pairs into the given type set in this meta. This | ||||
1206 | will *not* replace another value with the same name (for that see =putKeyed=) | ||||
1207 | |||||
1208 | For example, | ||||
1209 | <verbatim> | ||||
1210 | $meta->put( 'FIELD', { name => 'MaxAge', title => 'Max Age', value =>'103' } ); | ||||
1211 | </verbatim> | ||||
1212 | |||||
1213 | =cut | ||||
1214 | |||||
1215 | # spent 329ms within Foswiki::Meta::put which was called 52396 times, avg 6µs/call:
# 52390 times (329ms+0s) by Foswiki::Serialise::Embedded::_readMETA at line 194 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 6µs/call
# 3 times (18µs+0s) by Foswiki::Store::Rcs::Store::getVersionInfo at line 362 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 6µs/call
# 3 times (12µs+0s) by Foswiki::Meta::setRevisionInfo at line 1533, avg 4µs/call | ||||
1216 | 52396 | 26.5ms | my ( $this, $type, $args ) = @_; | ||
1217 | _assertIsTopic($this) if DEBUG; | ||||
1218 | 52396 | 3.57ms | ASSERT( defined $type ) if DEBUG; | ||
1219 | ASSERT( defined $args && ref($args) eq 'HASH' ) if DEBUG; | ||||
1220 | |||||
1221 | 52396 | 38.4ms | unless ( $this->{$type} ) { | ||
1222 | 52393 | 57.6ms | $this->{$type} = []; | ||
1223 | 52393 | 51.2ms | $this->{_indices}->{$type} = {}; | ||
1224 | } | ||||
1225 | |||||
1226 | 52396 | 20.1ms | my $data = $this->{$type}; | ||
1227 | 52396 | 7.90ms | my $i = 0; | ||
1228 | 52396 | 14.0ms | if ($data) { | ||
1229 | |||||
1230 | # overwrite old single value | ||||
1231 | 52396 | 11.9ms | if ( scalar(@$data) && defined $data->[0]->{name} ) { | ||
1232 | delete $this->{_indices}->{$type}->{ $data->[0]->{name} }; | ||||
1233 | } | ||||
1234 | 52396 | 35.3ms | $data->[0] = $args; | ||
1235 | } | ||||
1236 | else { | ||||
1237 | $i = push( @$data, $args ) - 1; | ||||
1238 | } | ||||
1239 | 52396 | 151ms | if ( defined $args->{name} ) { | ||
1240 | $this->{_indices}->{$type} ||= {}; | ||||
1241 | $this->{_indices}->{$type}->{ $args->{name} } = $i; | ||||
1242 | } | ||||
1243 | } | ||||
1244 | |||||
1245 | =begin TML | ||||
1246 | |||||
1247 | ---++ ObjectMethod putKeyed($type, \%args) | ||||
1248 | |||||
1249 | Put a hash of key=value pairs into the given type set in this meta, replacing | ||||
1250 | any existing value with the same key. | ||||
1251 | |||||
1252 | For example, | ||||
1253 | <verbatim> | ||||
1254 | $meta->putKeyed( 'FIELD', | ||||
1255 | { name => 'MaxAge', title => 'Max Age', value =>'103' } ); | ||||
1256 | </verbatim> | ||||
1257 | |||||
1258 | =cut | ||||
1259 | |||||
1260 | # Note: Array is used instead of a hash to preserve sequence | ||||
1261 | |||||
1262 | # spent 4.02s within Foswiki::Meta::putKeyed which was called 875082 times, avg 5µs/call:
# 875069 times (4.02s+0s) by Foswiki::Serialise::Embedded::_readMETA at line 188 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 5µs/call
# 13 times (60µs+0s) by Foswiki::Compatibility::readSymmetricallyEncodedMETA at line 367 of /var/www/foswikidev/core/lib/Foswiki/Compatibility.pm, avg 5µs/call | ||||
1263 | 875082 | 417ms | my ( $this, $type, $args ) = @_; | ||
1264 | _assertIsTopic($this) if DEBUG; | ||||
1265 | 875082 | 57.4ms | ASSERT($type) if DEBUG; | ||
1266 | ASSERT( $args && ref($args) eq 'HASH' ) if DEBUG; | ||||
1267 | 875082 | 241ms | my $keyName = $args->{name}; | ||
1268 | ASSERT( $keyName, join( ',', keys %$args ) ) if DEBUG; | ||||
1269 | |||||
1270 | 875082 | 353ms | unless ( $this->{$type} ) { | ||
1271 | 78865 | 61.8ms | $this->{$type} = []; | ||
1272 | 78865 | 58.7ms | $this->{_indices}->{$type} = {}; | ||
1273 | } | ||||
1274 | |||||
1275 | 875082 | 227ms | my $data = $this->{$type}; | ||
1276 | |||||
1277 | # The \% shouldn't be necessary, but it is | ||||
1278 | 875082 | 474ms | my $indices = \%{ $this->{_indices}->{$type} }; | ||
1279 | 875082 | 3.29s | if ( defined $indices->{$keyName} ) { | ||
1280 | $data->[ $indices->{$keyName} ] = $args; | ||||
1281 | } | ||||
1282 | else { | ||||
1283 | 875082 | 939ms | $indices->{$keyName} = push( @$data, $args ) - 1; | ||
1284 | } | ||||
1285 | } | ||||
1286 | |||||
1287 | =begin TML | ||||
1288 | |||||
1289 | ---++ ObjectMethod putAll | ||||
1290 | |||||
1291 | Replaces all the items of a given key with a new array. | ||||
1292 | |||||
1293 | For example, | ||||
1294 | <verbatim> | ||||
1295 | $meta->putAll( 'FIELD', | ||||
1296 | { name => 'MinAge', title => 'Min Age', value =>'50' }, | ||||
1297 | { name => 'MaxAge', title => 'Max Age', value =>'103' }, | ||||
1298 | { name => 'HairColour', title => 'Hair Colour', value =>'white' } | ||||
1299 | ); | ||||
1300 | </verbatim> | ||||
1301 | |||||
1302 | =cut | ||||
1303 | |||||
1304 | sub putAll { | ||||
1305 | my ( $this, $type, @array ) = @_; | ||||
1306 | _assertIsTopic($this) if DEBUG; | ||||
1307 | |||||
1308 | my %indices; | ||||
1309 | for ( my $i = 0 ; $i < scalar(@array) ; $i++ ) { | ||||
1310 | if ( defined $array[$i]->{name} ) { | ||||
1311 | $indices{ $array[$i]->{name} } = $i; | ||||
1312 | } | ||||
1313 | } | ||||
1314 | $this->{$type} = \@array; | ||||
1315 | $this->{_indices}->{$type} = \%indices; | ||||
1316 | } | ||||
1317 | |||||
1318 | =begin TML | ||||
1319 | |||||
1320 | ---++ ObjectMethod get( $type, $key ) -> \%hash | ||||
1321 | |||||
1322 | Find the value of a meta-datum in the map. If the type is | ||||
1323 | keyed (identified by a =name=), the =$key= parameter is required | ||||
1324 | to say _which_ entry you want. Otherwise you will just get the first value. | ||||
1325 | |||||
1326 | If you want all the keys of a given type use the 'find' method. | ||||
1327 | |||||
1328 | The result is a reference to the hash for the item. | ||||
1329 | |||||
1330 | For example, | ||||
1331 | <verbatim> | ||||
1332 | my $ma = $meta->get( 'FIELD', 'MinAge' ); | ||||
1333 | my $topicinfo = $meta->get( 'TOPICINFO' ); # get the TOPICINFO hash | ||||
1334 | </verbatim> | ||||
1335 | |||||
1336 | =cut | ||||
1337 | |||||
1338 | # spent 183ms within Foswiki::Meta::get which was called 61651 times, avg 3µs/call:
# 26327 times (69.8ms+0s) by Foswiki::Serialise::Embedded::read at line 79 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 3µs/call
# 26304 times (78.5ms+0s) by Foswiki::Store::Rcs::Store::readTopic at line 140 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 3µs/call
# 8760 times (34.3ms+0s) by Foswiki::Store::Interfaces::QueryAlgorithm::getField at line 350 of /var/www/foswikidev/core/lib/Foswiki/Store/Interfaces/QueryAlgorithm.pm, avg 4µs/call
# 222 times (460µs+0s) by Foswiki::Meta::getRevisionInfo at line 1591, avg 2µs/call
# 29 times (56µs+0s) by Foswiki::Prefs::Parser::parse at line 85 of /var/www/foswikidev/core/lib/Foswiki/Prefs/Parser.pm, avg 2µs/call
# 3 times (8µs+0s) by Foswiki::Meta::setRevisionInfo at line 1519, avg 3µs/call
# 3 times (6µs+0s) by Foswiki::Store::Rcs::Store::getVersionInfo at line 361 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 2µs/call
# once (2µs+0s) by Foswiki::Render::Parent::render at line 44 of /var/www/foswikidev/core/lib/Foswiki/Render/Parent.pm
# once (2µs+0s) by Foswiki::Plugins::AutoViewTemplatePlugin::initPlugin at line 51 of /var/www/foswikidev/core/lib/Foswiki/Plugins/AutoViewTemplatePlugin.pm
# once (2µs+0s) by Foswiki::Meta::getFormName at line 1803 | ||||
1339 | 61651 | 33.9ms | my ( $this, $type, $name ) = @_; | ||
1340 | _assertIsTopic($this) if DEBUG; | ||||
1341 | |||||
1342 | 61651 | 23.6ms | my $data = $this->{$type}; | ||
1343 | 61651 | 13.5ms | if ($data) { | ||
1344 | 61615 | 12.7ms | if ( defined $name ) { | ||
1345 | |||||
1346 | 8760 | 2.09ms | my $indices = $this->{_indices}; | ||
1347 | 8760 | 1.05ms | return undef unless defined $indices; | ||
1348 | 8760 | 2.59ms | $indices = $indices->{$type}; | ||
1349 | 8760 | 807µs | return undef unless defined $indices; | ||
1350 | 8760 | 3.95ms | return undef unless defined $indices->{$name}; | ||
1351 | 8760 | 34.1ms | return $data->[ $indices->{$name} ]; | ||
1352 | } | ||||
1353 | else { | ||||
1354 | 52855 | 226ms | return $data->[0]; | ||
1355 | } | ||||
1356 | } | ||||
1357 | |||||
1358 | 36 | 81µs | return undef; | ||
1359 | } | ||||
1360 | |||||
1361 | =begin TML | ||||
1362 | |||||
1363 | ---++ ObjectMethod find ( $type ) -> @values | ||||
1364 | |||||
1365 | Get all meta data for a specific type. | ||||
1366 | Returns the array stored for the type. This will be zero length | ||||
1367 | if there are no entries. | ||||
1368 | |||||
1369 | For example, | ||||
1370 | <verbatim> | ||||
1371 | my $attachments = $meta->find( 'FILEATTACHMENT' ); | ||||
1372 | </verbatim> | ||||
1373 | |||||
1374 | =cut | ||||
1375 | |||||
1376 | # spent 92µs within Foswiki::Meta::find which was called 30 times, avg 3µs/call:
# 29 times (86µs+0s) by Foswiki::Prefs::Parser::parse at line 75 of /var/www/foswikidev/core/lib/Foswiki/Prefs/Parser.pm, avg 3µs/call
# once (6µs+0s) by Foswiki::Attach::renderMetaData at line 96 of /var/www/foswikidev/core/lib/Foswiki/Attach.pm | ||||
1377 | 30 | 15µs | my ( $this, $type ) = @_; | ||
1378 | _assertIsTopic($this) if DEBUG; | ||||
1379 | |||||
1380 | 30 | 12µs | my $itemsr = $this->{$type}; | ||
1381 | 30 | 17µs | my @items = (); | ||
1382 | |||||
1383 | 30 | 9µs | if ($itemsr) { | ||
1384 | @items = @$itemsr; | ||||
1385 | } | ||||
1386 | |||||
1387 | 30 | 74µs | return @items; | ||
1388 | } | ||||
1389 | |||||
1390 | =begin TML | ||||
1391 | |||||
1392 | ---++ ObjectMethod remove($type, $key) | ||||
1393 | |||||
1394 | With no type, will remove all the meta-data in the object. | ||||
1395 | |||||
1396 | With a $type but no $key, will remove _all_ items of that type | ||||
1397 | (so for example if $type were FILEATTACHMENT it would remove all of them) | ||||
1398 | |||||
1399 | With a $type and a $key it will remove only the specific item. | ||||
1400 | |||||
1401 | =cut | ||||
1402 | |||||
1403 | sub remove { | ||||
1404 | my ( $this, $type, $name ) = @_; | ||||
1405 | _assertIsTopic($this) if DEBUG; | ||||
1406 | |||||
1407 | if ($type) { | ||||
1408 | my $data = $this->{$type}; | ||||
1409 | return unless defined $data; | ||||
1410 | if ($name) { | ||||
1411 | my $indices = $this->{_indices}->{$type}; | ||||
1412 | if ( defined $indices ) { | ||||
1413 | my $i = $indices->{$name}; | ||||
1414 | return unless defined $i; | ||||
1415 | splice( @$data, $i, 1 ); | ||||
1416 | delete $indices->{$name}; | ||||
1417 | for ( my $i = 0 ; $i < scalar(@$data) ; $i++ ) { | ||||
1418 | my $item = $data->[$i]; | ||||
1419 | next unless exists $item->{name}; | ||||
1420 | $indices->{ $item->{name} } = $i; | ||||
1421 | } | ||||
1422 | } | ||||
1423 | } | ||||
1424 | else { | ||||
1425 | delete $this->{$type}; | ||||
1426 | delete $this->{_indices}->{$type}; | ||||
1427 | } | ||||
1428 | } | ||||
1429 | else { | ||||
1430 | foreach my $entry ( keys %$this ) { | ||||
1431 | unless ( $entry =~ m/^_/ ) { | ||||
1432 | delete $this->{$entry}; | ||||
1433 | } | ||||
1434 | } | ||||
1435 | $this->{_indices} = {}; | ||||
1436 | } | ||||
1437 | } | ||||
1438 | |||||
1439 | =begin TML | ||||
1440 | |||||
1441 | ---++ ObjectMethod copyFrom( $otherMeta [, $type [, $nameFilter]] ) | ||||
1442 | |||||
1443 | Copy all entries of a type from another meta data set. This | ||||
1444 | will destroy the old values for that type, unless the | ||||
1445 | copied object doesn't contain entries for that type, in which | ||||
1446 | case it will retain the old values. | ||||
1447 | |||||
1448 | If $type is undef, will copy ALL TYPES. | ||||
1449 | |||||
1450 | If $nameFilter is defined (a perl regular expression), it will copy | ||||
1451 | only data where ={name}= matches $nameFilter. | ||||
1452 | |||||
1453 | Does *not* copy web, topic or text. | ||||
1454 | |||||
1455 | =cut | ||||
1456 | |||||
1457 | sub copyFrom { | ||||
1458 | my ( $this, $other, $type, $filter ) = @_; | ||||
1459 | _assertIsTopic($this) if DEBUG; | ||||
1460 | _assertIsTopic($other) if DEBUG; | ||||
1461 | |||||
1462 | if ($type) { | ||||
1463 | return if $type =~ m/^_/; | ||||
1464 | my @data; | ||||
1465 | foreach my $item ( @{ $other->{$type} } ) { | ||||
1466 | if ( !$filter | ||||
1467 | || ( $item->{name} && $item->{name} =~ m/$filter/ ) ) | ||||
1468 | { | ||||
1469 | ASSERT( defined($item) ) if DEBUG; | ||||
1470 | my %datum = %$item; | ||||
1471 | push( @data, \%datum ); | ||||
1472 | } | ||||
1473 | } | ||||
1474 | $this->putAll( $type, @data ); | ||||
1475 | } | ||||
1476 | else { | ||||
1477 | foreach my $k ( keys %$other ) { | ||||
1478 | unless ( $k =~ m/^_/ ) { | ||||
1479 | $this->copyFrom( $other, $k ); | ||||
1480 | } | ||||
1481 | } | ||||
1482 | } | ||||
1483 | } | ||||
1484 | |||||
1485 | =begin TML | ||||
1486 | |||||
1487 | ---++ ObjectMethod count($type) -> $integer | ||||
1488 | |||||
1489 | Return the number of entries of the given type | ||||
1490 | |||||
1491 | =cut | ||||
1492 | |||||
1493 | # spent 57.6ms within Foswiki::Meta::count which was called 26327 times, avg 2µs/call:
# 26327 times (57.6ms+0s) by Foswiki::Serialise::Embedded::read at line 125 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 2µs/call | ||||
1494 | 26327 | 12.1ms | my ( $this, $type ) = @_; | ||
1495 | _assertIsTopic($this) if DEBUG; | ||||
1496 | 26327 | 10.3ms | my $data = $this->{$type}; | ||
1497 | |||||
1498 | 26327 | 95.4ms | return scalar(@$data) if ( defined($data) ); | ||
1499 | |||||
1500 | 1 | 3µs | return 0; | ||
1501 | } | ||||
1502 | |||||
1503 | =begin TML | ||||
1504 | |||||
1505 | ---++ ObjectMethod setRevisionInfo( %opts ) | ||||
1506 | |||||
1507 | Set TOPICINFO information on the object, as specified by the parameters. | ||||
1508 | * =version= - the revision number | ||||
1509 | * =time= - the time stamp | ||||
1510 | * =author= - the user id (cUID) | ||||
1511 | * + additional data fields to save e.g. reprev, comment | ||||
1512 | |||||
1513 | =cut | ||||
1514 | |||||
1515 | # spent 86µs (66+19) within Foswiki::Meta::setRevisionInfo which was called 3 times, avg 29µs/call:
# 3 times (66µs+19µs) by Foswiki::Meta::getRevisionInfo at line 1606, avg 29µs/call | ||||
1516 | 3 | 10µs | my ( $this, %data ) = @_; | ||
1517 | _assertIsTopic($this) if DEBUG; | ||||
1518 | |||||
1519 | 3 | 5µs | 3 | 8µs | my $ti = $this->get('TOPICINFO') || {}; # spent 8µs making 3 calls to Foswiki::Meta::get, avg 3µs/call |
1520 | |||||
1521 | 3 | 7µs | foreach my $k ( keys %data ) { | ||
1522 | 21 | 14µs | $ti->{$k} = $data{$k}; | ||
1523 | } | ||||
1524 | |||||
1525 | # compatibility; older versions of the code use | ||||
1526 | # RCS rev numbers. Save with them so old code can | ||||
1527 | # read these topics | ||||
1528 | ASSERT( defined $ti->{version} ) if DEBUG; | ||||
1529 | 3 | 4µs | $ti->{version} = 1 if $ti->{version} < 1; | ||
1530 | 3 | 2µs | $ti->{version} = $ti->{version}; | ||
1531 | 3 | 2µs | $ti->{format} = EMBEDDING_FORMAT_VERSION; | ||
1532 | |||||
1533 | 3 | 14µs | 3 | 12µs | $this->put( 'TOPICINFO', $ti ); # spent 12µs making 3 calls to Foswiki::Meta::put, avg 4µs/call |
1534 | } | ||||
1535 | |||||
1536 | =begin TML | ||||
1537 | |||||
1538 | ---++ ObjectMethod getRevisionInfo([$attachment [,$rev]]) -> \%info | ||||
1539 | |||||
1540 | * =$attachment= - (optional) attachment name to get info about | ||||
1541 | * =$rev= - (optional) revision of attachment for which to get info | ||||
1542 | |||||
1543 | Return revision info for the loaded revision of a topic or | ||||
1544 | attachment with at least: | ||||
1545 | * ={date}= in epochSec | ||||
1546 | * ={author}= canonical user ID | ||||
1547 | * ={version}= the revision number | ||||
1548 | |||||
1549 | ---++ ObjectMethod getRevisionInfo() -> ( $revDate, $author, $rev, $comment ) | ||||
1550 | |||||
1551 | Limited backwards compatibility for plugins that assume the 1.0.x interface | ||||
1552 | The comment is *always* blank | ||||
1553 | |||||
1554 | =cut | ||||
1555 | |||||
1556 | # spent 17.4ms (1.59+15.8) within Foswiki::Meta::getRevisionInfo which was called 222 times, avg 78µs/call:
# 219 times (1.52ms+452µs) by Foswiki::MetaCache::get at line 230 of /var/www/foswikidev/core/lib/Foswiki/MetaCache.pm, avg 9µs/call
# 3 times (73µs+15.3ms) by Foswiki::Render::renderRevisionInfo at line 802 of /var/www/foswikidev/core/lib/Foswiki/Render.pm, avg 5.13ms/call | ||||
1557 | 222 | 102µs | my ( $this, $attachment, $rev ) = @_; | ||
1558 | |||||
1559 | _assertIsTopic($this) if DEBUG; | ||||
1560 | |||||
1561 | 222 | 42µs | if ($attachment) { | ||
1562 | return $this->{_session}->{store} | ||||
1563 | ->getVersionInfo( $this, $rev, $attachment ); | ||||
1564 | } | ||||
1565 | |||||
1566 | 222 | 23µs | my $info; | ||
1567 | 222 | 71µs | 3 | 279µs | if ( not defined( $this->{_loadedRev} ) # spent 279µs making 3 calls to Foswiki::Func::topicExists, avg 93µs/call |
1568 | and not Foswiki::Func::topicExists( $this->{_web}, $this->{_topic} ) ) | ||||
1569 | { | ||||
1570 | |||||
1571 | #print STDERR "topic does not exist - at least, _loadedRev is not set..(".$this->{_web} .' '. $this->{_topic}.")\n"; | ||||
1572 | #this does not exist on disk - no reason to goto the store for the defaults | ||||
1573 | #TODO: Sven is not 100% sure this is the right decision, but it feels better not to do a trip into the deep for an application default | ||||
1574 | $info = { | ||||
1575 | date => 0, | ||||
1576 | author => $Foswiki::Users::BaseUserMapping::DEFAULT_USER_CUID, | ||||
1577 | version => 0, | ||||
1578 | format => EMBEDDING_FORMAT_VERSION, | ||||
1579 | }; | ||||
1580 | return $info; | ||||
1581 | } | ||||
1582 | |||||
1583 | # This used to try and get revision info from the meta | ||||
1584 | # information and only kick down to the Store module for the | ||||
1585 | # same information if it was not present. However there have | ||||
1586 | # been several cases where the meta information in the cache | ||||
1587 | # is badly out of step with the store, and the conclusion is | ||||
1588 | # that it can't be trusted. For this reason, when meta is read | ||||
1589 | # TOPICINFO version field is automatically undefined, which | ||||
1590 | # forces this function to re-get it from the store. | ||||
1591 | 222 | 225µs | 222 | 460µs | my $topicinfo = $this->get('TOPICINFO'); # spent 460µs making 222 calls to Foswiki::Meta::get, avg 2µs/call |
1592 | |||||
1593 | 222 | 492µs | if ( $topicinfo && defined $topicinfo->{version} ) { | ||
1594 | $info = { | ||||
1595 | date => $topicinfo->{date}, | ||||
1596 | author => $topicinfo->{author}, | ||||
1597 | version => $topicinfo->{version}, | ||||
1598 | }; | ||||
1599 | } | ||||
1600 | else { | ||||
1601 | |||||
1602 | # Delegate to the store | ||||
1603 | 3 | 15µs | 3 | 14.9ms | $info = $this->{_session}->{store}->getVersionInfo($this); # spent 14.9ms making 3 calls to Foswiki::Store::Rcs::Store::getVersionInfo, avg 4.98ms/call |
1604 | |||||
1605 | # cache the result | ||||
1606 | 3 | 12µs | 3 | 86µs | $this->setRevisionInfo(%$info); # spent 86µs making 3 calls to Foswiki::Meta::setRevisionInfo, avg 29µs/call |
1607 | } | ||||
1608 | |||||
1609 | 222 | 65µs | if (wantarray) { | ||
1610 | |||||
1611 | # Backwards compatibility for 1.0.x plugins | ||||
1612 | return ( $info->{date}, $info->{author}, $info->{version}, '' ); | ||||
1613 | } | ||||
1614 | else { | ||||
1615 | 222 | 439µs | return $info; | ||
1616 | } | ||||
1617 | } | ||||
1618 | |||||
1619 | # Determines, and caches, the topic revision info of the base version, | ||||
1620 | # SMELL: this is a horrid little legacy of the InfoCache object, and | ||||
1621 | # should be done away with. | ||||
1622 | sub getRev1Info { | ||||
1623 | my ( $this, $attr ) = @_; | ||||
1624 | _assertIsTopic($this) if DEBUG; | ||||
1625 | |||||
1626 | #my ( $web, $topic ) = Foswiki::Func::normalizeWebTopicName( $this->{_defaultWeb}, $webtopic ); | ||||
1627 | my $web = $this->web; | ||||
1628 | my $topic = $this->topic; | ||||
1629 | |||||
1630 | if ( !defined( $this->{_getRev1Info} ) ) { | ||||
1631 | $this->{_getRev1Info} = {}; | ||||
1632 | } | ||||
1633 | my $info = $this->{_getRev1Info}; | ||||
1634 | unless ( defined $info->{$attr} ) { | ||||
1635 | my $ri = $info->{rev1info}; | ||||
1636 | unless ($ri) { | ||||
1637 | my $tmp = Foswiki::Meta->load( $this->{_session}, $web, $topic, 1 ); | ||||
1638 | $info->{rev1info} = $ri = $tmp->getRevisionInfo(); | ||||
1639 | } | ||||
1640 | |||||
1641 | if ( $attr eq 'createusername' ) { | ||||
1642 | $info->{createusername} = | ||||
1643 | $this->{_session}->{users}->getLoginName( $ri->{author} ); | ||||
1644 | } | ||||
1645 | elsif ( $attr eq 'createwikiname' ) { | ||||
1646 | $info->{createwikiname} = | ||||
1647 | $this->{_session}->{users}->getWikiName( $ri->{author} ); | ||||
1648 | } | ||||
1649 | elsif ( $attr eq 'createwikiusername' ) { | ||||
1650 | $info->{createwikiusername} = | ||||
1651 | $this->{_session}->{users}->webDotWikiName( $ri->{author} ); | ||||
1652 | } | ||||
1653 | elsif ($attr eq 'createdate' | ||||
1654 | or $attr eq 'createlongdate' | ||||
1655 | or $attr eq 'created' ) | ||||
1656 | { | ||||
1657 | $info->{created} = $ri->{date}; | ||||
1658 | |||||
1659 | # Don't pass Foswiki::Time an undef value | ||||
1660 | if ( defined $ri->{date} ) { | ||||
1661 | require Foswiki::Time; | ||||
1662 | $info->{createdate} = Foswiki::Time::formatTime( $ri->{date} ); | ||||
1663 | |||||
1664 | #TODO: wow thats disgusting. | ||||
1665 | $info->{created} = $info->{createlongdate} = | ||||
1666 | $info->{createdate}; | ||||
1667 | } | ||||
1668 | } | ||||
1669 | } | ||||
1670 | return $info->{$attr}; | ||||
1671 | } | ||||
1672 | |||||
1673 | =begin TML | ||||
1674 | |||||
1675 | ---++ ObjectMethod merge( $otherMeta, $formDef ) | ||||
1676 | |||||
1677 | * =$otherMeta= - a block of meta-data to merge with $this | ||||
1678 | * =$formDef= reference to a Foswiki::Form that gives the types of the fields in $this | ||||
1679 | |||||
1680 | Merge the data in the other meta block. | ||||
1681 | * File attachments that only appear in one set are preserved. | ||||
1682 | * Form fields that only appear in one set are preserved. | ||||
1683 | * Form field values that are different in each set are text-merged | ||||
1684 | * We don't merge for field attributes or title | ||||
1685 | * Topic info is not touched | ||||
1686 | * The =isTextMergeable= method on the form def is used to determine if that field is mergeable. If it isn't, the value currently in meta will _not_ be changed. | ||||
1687 | |||||
1688 | =cut | ||||
1689 | |||||
1690 | sub merge { | ||||
1691 | my ( $this, $other, $formDef ) = @_; | ||||
1692 | _assertIsTopic($this) if DEBUG; | ||||
1693 | _assertIsTopic($other) if DEBUG; | ||||
1694 | |||||
1695 | my $data = $other->{FIELD}; | ||||
1696 | if ($data) { | ||||
1697 | foreach my $otherD (@$data) { | ||||
1698 | my $thisD = $this->get( 'FIELD', $otherD->{name} ); | ||||
1699 | if ( $thisD && $thisD->{value} ne $otherD->{value} ) { | ||||
1700 | if ( $formDef->isTextMergeable( $thisD->{name} ) ) { | ||||
1701 | require Foswiki::Merge; | ||||
1702 | my $merged = Foswiki::Merge::merge2( | ||||
1703 | 'A', | ||||
1704 | $otherD->{value}, | ||||
1705 | 'B', | ||||
1706 | $thisD->{value}, | ||||
1707 | '.*?\s+', | ||||
1708 | $this->{_session}, | ||||
1709 | $formDef->getField( $thisD->{name} ) | ||||
1710 | ); | ||||
1711 | |||||
1712 | # SMELL: we don't merge attributes or title | ||||
1713 | $thisD->{value} = $merged; | ||||
1714 | } | ||||
1715 | } | ||||
1716 | elsif ( !$thisD ) { | ||||
1717 | $this->putKeyed( 'FIELD', $otherD ); | ||||
1718 | } | ||||
1719 | } | ||||
1720 | } | ||||
1721 | |||||
1722 | $data = $other->{FILEATTACHMENT}; | ||||
1723 | if ($data) { | ||||
1724 | foreach my $otherD (@$data) { | ||||
1725 | my $thisD = $this->get( 'FILEATTACHMENT', $otherD->{name} ); | ||||
1726 | if ( !$thisD ) { | ||||
1727 | $this->putKeyed( 'FILEATTACHMENT', $otherD ); | ||||
1728 | } | ||||
1729 | } | ||||
1730 | } | ||||
1731 | } | ||||
1732 | |||||
1733 | =begin TML | ||||
1734 | |||||
1735 | ---++ ObjectMethod forEachSelectedValue( $types, $keys, \&fn, \%options ) | ||||
1736 | |||||
1737 | Iterate over the values selected by the regular expressions in $types and | ||||
1738 | $keys. | ||||
1739 | * =$types= - regular expression matching the names of fields to be processed. Will default to qr/^[A-Z]+$/ if undef. | ||||
1740 | * =$keys= - regular expression matching the names of keys to be processed. Will default to qr/^[a-z]+$/ if undef. | ||||
1741 | |||||
1742 | Iterates over each value, calling =\&fn= on each, and replacing the value | ||||
1743 | with the result of \&fn. | ||||
1744 | |||||
1745 | \%options will be passed on to $fn, with the following additions: | ||||
1746 | * =_type= => the type name (e.g. "FILEATTACHMENT") | ||||
1747 | * =_key= => the key name (e.g. "user") | ||||
1748 | |||||
1749 | =cut | ||||
1750 | |||||
1751 | sub forEachSelectedValue { | ||||
1752 | my ( $this, $types, $keys, $fn, $options ) = @_; | ||||
1753 | _assertIsTopic($this) if DEBUG; | ||||
1754 | |||||
1755 | $types ||= qr/^[A-Z]+$/; | ||||
1756 | $keys ||= qr/^[a-z]+$/; | ||||
1757 | |||||
1758 | foreach my $type ( grep { /$types/ } keys %$this ) { | ||||
1759 | $options->{_type} = $type; | ||||
1760 | my $data = $this->{$type}; | ||||
1761 | next unless $data; | ||||
1762 | foreach my $datum (@$data) { | ||||
1763 | foreach my $key ( grep { /$keys/ } keys %$datum ) { | ||||
1764 | $options->{_key} = $key; | ||||
1765 | $datum->{$key} = &$fn( $datum->{$key}, $options ); | ||||
1766 | } | ||||
1767 | } | ||||
1768 | } | ||||
1769 | } | ||||
1770 | |||||
1771 | =begin TML | ||||
1772 | |||||
1773 | ---++ ObjectMethod getParent() -> $parent | ||||
1774 | |||||
1775 | Gets the TOPICPARENT name. Safe shortcut for =$meta->get('TOPICPARENT')->{name} | ||||
1776 | Returns the emty string if there is no parent. | ||||
1777 | |||||
1778 | =cut | ||||
1779 | |||||
1780 | sub getParent { | ||||
1781 | my ($this) = @_; | ||||
1782 | |||||
1783 | my $value = ''; | ||||
1784 | my $parent = $this->get('TOPICPARENT'); | ||||
1785 | $value = $parent->{name} if ($parent); | ||||
1786 | |||||
1787 | # Return empty string (not undef), if TOPICPARENT meta is broken | ||||
1788 | $value = '' if ( !defined $value ); | ||||
1789 | return $value; | ||||
1790 | } | ||||
1791 | |||||
1792 | =begin TML | ||||
1793 | |||||
1794 | ---++ ObjectMethod getFormName() -> $formname | ||||
1795 | |||||
1796 | Returns the name of the FORM, or '' if none. | ||||
1797 | |||||
1798 | =cut | ||||
1799 | |||||
1800 | # spent 11µs (10+2) within Foswiki::Meta::getFormName which was called:
# once (10µs+2µs) by Foswiki::Meta::renderFormForDisplay at line 1823 | ||||
1801 | 1 | 500ns | my ($this) = @_; | ||
1802 | |||||
1803 | 1 | 1µs | 1 | 2µs | my $aForm = $this->get('FORM'); # spent 2µs making 1 call to Foswiki::Meta::get |
1804 | 1 | 200ns | if ($aForm) { | ||
1805 | return $aForm->{name}; | ||||
1806 | } | ||||
1807 | 1 | 6µs | return ''; | ||
1808 | } | ||||
1809 | |||||
1810 | =begin TML | ||||
1811 | |||||
1812 | ---++ ObjectMethod renderFormForDisplay() -> $html | ||||
1813 | |||||
1814 | Render the form contained in the meta for display. | ||||
1815 | |||||
1816 | =cut | ||||
1817 | |||||
1818 | # SMELL: this is part of the View and should be moved closer to the renderer | ||||
1819 | # spent 5.92ms (2.97+2.95) within Foswiki::Meta::renderFormForDisplay which was called:
# once (2.97ms+2.95ms) by Foswiki::META at line 40 of /var/www/foswikidev/core/lib/Foswiki/Macros/META.pm | ||||
1820 | 1 | 500ns | my ($this) = @_; | ||
1821 | _assertIsTopic($this) if DEBUG; | ||||
1822 | |||||
1823 | 1 | 3µs | 1 | 11µs | my $fname = $this->getFormName(); # spent 11µs making 1 call to Foswiki::Meta::getFormName |
1824 | |||||
1825 | 1 | 116µs | require Foswiki::Form; | ||
1826 | 1 | 1µs | require Foswiki::OopsException; | ||
1827 | 1 | 8µs | return '' unless $fname; | ||
1828 | |||||
1829 | my $form; | ||||
1830 | my $result; | ||||
1831 | try { | ||||
1832 | $form = new Foswiki::Form( $this->{_session}, $this->{_web}, $fname ); | ||||
1833 | $result = $form->renderForDisplay($this); | ||||
1834 | } | ||||
1835 | catch Foswiki::OopsException with { | ||||
1836 | |||||
1837 | # Make pseudo-form from field data | ||||
1838 | $form = | ||||
1839 | new Foswiki::Form( $this->{_session}, $this->{_web}, $fname, $this ); | ||||
1840 | $result = | ||||
1841 | $this->{_session}->inlineAlert( 'alerts', 'formdef_missing', $fname ); | ||||
1842 | $result .= $form->renderForDisplay($this) if $form; | ||||
1843 | }; | ||||
1844 | |||||
1845 | return $result; | ||||
1846 | } | ||||
1847 | |||||
1848 | =begin TML | ||||
1849 | |||||
1850 | ---++ ObjectMethod renderFormFieldForDisplay($name, $format, $attrs) -> $text | ||||
1851 | |||||
1852 | Render a single formfield, using the $format. See | ||||
1853 | Foswiki::Form::FormField::renderForDisplay for a description of how the value | ||||
1854 | is rendered. | ||||
1855 | |||||
1856 | =cut | ||||
1857 | |||||
1858 | # SMELL: this is part of the View and should be moved closer to the renderer | ||||
1859 | sub renderFormFieldForDisplay { | ||||
1860 | my ( $this, $name, $format, $attrs ) = @_; | ||||
1861 | _assertIsTopic($this) if DEBUG; | ||||
1862 | |||||
1863 | my $mf = $this->get( 'FIELD', $name ); | ||||
1864 | unless ($mf) { | ||||
1865 | |||||
1866 | # Not a valid field name, maybe it's a title. | ||||
1867 | require Foswiki::Form; | ||||
1868 | $name = Foswiki::Form::fieldTitle2FieldName($name); | ||||
1869 | $mf = $this->get( 'FIELD', $name ); | ||||
1870 | } | ||||
1871 | return '' unless $mf; # field not found | ||||
1872 | |||||
1873 | my $fname = $this->getFormName(); | ||||
1874 | if ($fname) { | ||||
1875 | require Foswiki::Form; | ||||
1876 | my $result; | ||||
1877 | try { | ||||
1878 | my $form = | ||||
1879 | new Foswiki::Form( $this->{_session}, $this->{_web}, $fname ); | ||||
1880 | my $field = $form->getField($name); | ||||
1881 | if ($field) { | ||||
1882 | $attrs->{usetitle} = $mf->{title}; | ||||
1883 | $result = | ||||
1884 | $field->renderForDisplay( $format, $mf->{value}, $attrs ); | ||||
1885 | } | ||||
1886 | } | ||||
1887 | catch Foswiki::OopsException with { | ||||
1888 | |||||
1889 | # Form not found, ignore | ||||
1890 | }; | ||||
1891 | |||||
1892 | return $result if defined $result; | ||||
1893 | } | ||||
1894 | |||||
1895 | # Form or field wasn't found, do your best! | ||||
1896 | my $f = $this->get( 'FIELD', $name ); | ||||
1897 | if ($f) { | ||||
1898 | $format =~ s/\$title/$f->{title}/; | ||||
1899 | require Foswiki::Render; | ||||
1900 | my $value = | ||||
1901 | Foswiki::Render::protectFormFieldValue( $mf->{value}, $attrs ); | ||||
1902 | $format =~ s/\$value(\([^)]*\))?/$value/; | ||||
1903 | } | ||||
1904 | return $format; | ||||
1905 | } | ||||
1906 | |||||
1907 | =begin TML | ||||
1908 | |||||
1909 | ---++ ObjectMethod haveAccess($mode, $cUID) -> $boolean | ||||
1910 | |||||
1911 | * =$mode= - 'VIEW', 'CHANGE', 'CREATE', etc. (defaults to VIEW) | ||||
1912 | * =$cUID= - Canonical user id (defaults to current user) | ||||
1913 | Check if the user has the given mode of access to the topic. This call | ||||
1914 | may result in the topic being read. | ||||
1915 | |||||
1916 | =cut | ||||
1917 | |||||
1918 | # spent 14.2ms (2.79+11.4) within Foswiki::Meta::haveAccess which was called 252 times, avg 56µs/call:
# 219 times (2.41ms+3.15ms) by Foswiki::MetaCache::get at line 244 of /var/www/foswikidev/core/lib/Foswiki/MetaCache.pm, avg 25µs/call
# 19 times (200µs+254µs) by Foswiki::WebFilter::ok at line 59 of /var/www/foswikidev/core/lib/Foswiki/WebFilter.pm, avg 24µs/call
# 8 times (85µs+112µs) by Foswiki::_includeTopic at line 183 of /var/www/foswikidev/core/lib/Foswiki/Macros/INCLUDE.pm, avg 25µs/call
# 3 times (45µs+72µs) by Foswiki::REVINFO at line 39 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 39µs/call
# once (21µs+7.78ms) by Foswiki::Func::checkAccessPermission at line 1501 of /var/www/foswikidev/core/lib/Foswiki/Func.pm
# once (16µs+27µs) by Foswiki::If::OP_allows::evaluate at line 57 of /var/www/foswikidev/core/lib/Foswiki/If/OP_allows.pm
# once (12µs+17µs) by Foswiki::UI::checkAccess at line 633 of /var/www/foswikidev/core/lib/Foswiki/UI.pm | ||||
1919 | 252 | 127µs | my ( $this, $mode, $cUID ) = @_; | ||
1920 | 252 | 33µs | $mode ||= 'VIEW'; | ||
1921 | 252 | 151µs | $cUID ||= $this->{_session}->{user}; | ||
1922 | |||||
1923 | 252 | 64µs | my $session = $this->{_session}; | ||
1924 | |||||
1925 | 252 | 601µs | 504 | 10.8ms | my $ok = $session->access->haveAccess( $mode, $cUID, $this ); # spent 8.14ms making 252 calls to Foswiki::access, avg 32µs/call
# spent 2.63ms making 252 calls to Foswiki::Access::TopicACLAccess::haveAccess, avg 10µs/call |
1926 | 252 | 545µs | 504 | 640µs | $reason = $session->access->getReason(); # spent 342µs making 252 calls to Foswiki::access, avg 1µs/call
# spent 299µs making 252 calls to Foswiki::Access::getReason, avg 1µs/call |
1927 | 252 | 533µs | return $ok; | ||
1928 | } | ||||
1929 | |||||
1930 | =begin TML | ||||
1931 | |||||
1932 | ---++ ObjectMethod save( %options ) | ||||
1933 | |||||
1934 | Save the current object, invoking appropriate plugin handlers | ||||
1935 | * =%options= - Hash of options, see saveAs for list of keys | ||||
1936 | |||||
1937 | =cut | ||||
1938 | |||||
1939 | # SMELL: arguably save should only be permitted if the loaded rev of | ||||
1940 | # the object is the same as the latest rev. | ||||
1941 | sub save { | ||||
1942 | my $this = shift; | ||||
1943 | ASSERT( scalar(@_) % 2 == 0 ) if DEBUG; | ||||
1944 | my %opts = @_; | ||||
1945 | _assertIsTopic($this) if DEBUG; | ||||
1946 | |||||
1947 | my $plugins = $this->{_session}->{plugins}; | ||||
1948 | |||||
1949 | # make sure version and date in TOPICINFO are up-to-date | ||||
1950 | # (side effect of getRevisionInfo) | ||||
1951 | $this->getRevisionInfo(); | ||||
1952 | |||||
1953 | # Semantics inherited from Cairo. See | ||||
1954 | # Foswiki:Codev.BugBeforeSaveHandlerBroken | ||||
1955 | if ( !$opts{nohandlers} && $plugins->haveHandlerFor('beforeSaveHandler') ) { | ||||
1956 | |||||
1957 | # Break up the tom and write the meta into the topic text. | ||||
1958 | # Nasty compatibility requirement as some old plugins may hack the | ||||
1959 | # meta instead of using the Meta API | ||||
1960 | my $text = Foswiki::Serialise::serialise( $this, 'Embedded' ); | ||||
1961 | |||||
1962 | my $pretext = $text; # text before the handler modifies it | ||||
1963 | my $premeta = $this->stringify(); # just the meta, no text | ||||
1964 | unless ( $this->{_loadedRev} ) { | ||||
1965 | |||||
1966 | # The meta obj doesn't have a loaded rev yet, and we have to block the | ||||
1967 | # beforeSaveHandlers from loading the topic from store. We are saving, | ||||
1968 | # and anything we have in $this is going to get written anyway, so we | ||||
1969 | # can simply mark it as "the latest". | ||||
1970 | # SMELL: this may not work if the beforeSaveHandler tries to use the | ||||
1971 | # meta obj for access control checks, so that is not recommended. | ||||
1972 | $this->{_loadedRev} = $this->getLatestRev(); | ||||
1973 | } | ||||
1974 | |||||
1975 | $plugins->dispatch( 'beforeSaveHandler', $text, $this->{_topic}, | ||||
1976 | $this->{_web}, $this ); | ||||
1977 | |||||
1978 | # If the text has changed; it may be a text or meta change, or both | ||||
1979 | if ( $text ne $pretext ) { | ||||
1980 | |||||
1981 | # Create a new object to parse the changed text | ||||
1982 | my $after = | ||||
1983 | new Foswiki::Meta( $this->{_session}, $this->{_web}, | ||||
1984 | $this->{_topic}, $text ); | ||||
1985 | unless ( $this->stringify() ne $premeta ) { | ||||
1986 | |||||
1987 | # Meta-data changes in the object take priority over | ||||
1988 | # conflicting changes in the text. So if there have been | ||||
1989 | # *any* changes in the meta, ignore changes in the text. | ||||
1990 | $this->copyFrom($after); | ||||
1991 | } | ||||
1992 | $this->text( $after->text() ); | ||||
1993 | } | ||||
1994 | } | ||||
1995 | |||||
1996 | my $signal; | ||||
1997 | my $newRev; | ||||
1998 | try { | ||||
1999 | $newRev = $this->saveAs(%opts); | ||||
2000 | } | ||||
2001 | catch Error with { | ||||
2002 | $signal = shift; | ||||
2003 | }; | ||||
2004 | |||||
2005 | # Semantics inherited from TWiki. See | ||||
2006 | # TWiki:Codev.BugBeforeSaveHandlerBroken | ||||
2007 | if ( !$opts{nohandlers} | ||||
2008 | && !defined $signal | ||||
2009 | && $plugins->haveHandlerFor('afterSaveHandler') ) | ||||
2010 | { | ||||
2011 | my $text = Foswiki::Serialise::serialise( $this, 'Embedded' ); | ||||
2012 | delete $this->{_preferences}; # Make sure handler has changed prefs | ||||
2013 | my $error = $signal ? $signal->{-text} : undef; | ||||
2014 | $plugins->dispatch( 'afterSaveHandler', $text, $this->{_topic}, | ||||
2015 | $this->{_web}, $error, $this ); | ||||
2016 | } | ||||
2017 | |||||
2018 | throw $signal if $signal; | ||||
2019 | |||||
2020 | ASSERT( $newRev, $this->{_loadedRev} ) if DEBUG; | ||||
2021 | |||||
2022 | my @extras = (); | ||||
2023 | push( @extras, 'minor' ) if $opts{minor}; # don't notify | ||||
2024 | push( @extras, 'dontlog' ) if $opts{dontlog}; # don't statisticify | ||||
2025 | |||||
2026 | $this->{_session}->logger->log( | ||||
2027 | { | ||||
2028 | level => 'info', | ||||
2029 | action => 'save', | ||||
2030 | webTopic => $this->{_web} . '.' . $this->{_topic}, | ||||
2031 | extra => join( ', ', @extras ), | ||||
2032 | user => $this->{_session}->{user}, | ||||
2033 | } | ||||
2034 | ); | ||||
2035 | |||||
2036 | return $newRev; | ||||
2037 | } | ||||
2038 | |||||
2039 | =begin TML | ||||
2040 | |||||
2041 | ---++ ObjectMethod saveAs( $web, $topic, %options ) -> $rev | ||||
2042 | |||||
2043 | Save the current topic to a store location. Only works on topics. | ||||
2044 | *without* invoking plugins handlers. | ||||
2045 | * =$web.$topic= - where to move to (defaults to web.topic in the object) | ||||
2046 | * =%options= - Hash of options, may include: | ||||
2047 | * =forcenewrevision= - force an increment in the revision number, | ||||
2048 | even if content doesn't change. | ||||
2049 | * =dontlog= - don't include this change in statistics | ||||
2050 | * =minor= - don't notify this change | ||||
2051 | * =savecmd= - Save command (core use only) | ||||
2052 | * =forcedate= - force the revision date to be this (core only) | ||||
2053 | * =author= - cUID of author of change (core only - default current user) | ||||
2054 | * =nohandlers= - *do not* call plugins handlers | ||||
2055 | |||||
2056 | Note that the %options are passed on verbatim from Foswiki::Func::saveTopic, | ||||
2057 | so an extension author can in fact use all these options. However those | ||||
2058 | marked "core only" are for core use only and should *not* be used in | ||||
2059 | extensions. | ||||
2060 | |||||
2061 | Returns the saved revision number. | ||||
2062 | |||||
2063 | =cut | ||||
2064 | |||||
2065 | # SMELL: arguably save should only be permitted if the loaded rev | ||||
2066 | # of the object is the same as the latest rev. | ||||
2067 | sub saveAs { | ||||
2068 | my ( $this, %opts ) = @_; | ||||
2069 | _assertIsTopic($this) if DEBUG; | ||||
2070 | |||||
2071 | $this->{_web} = $opts{web} if $opts{web}; | ||||
2072 | $this->{_topic} = $opts{topic} if $opts{topic}; | ||||
2073 | |||||
2074 | my $cUID = $opts{author} || $this->{_session}->{user}; | ||||
2075 | _assertIsTopic($this) if DEBUG; | ||||
2076 | |||||
2077 | unless ( $this->{_topic} eq $Foswiki::cfg{WebPrefsTopicName} ) { | ||||
2078 | |||||
2079 | # Don't verify web existance for WebPreferences, as saving | ||||
2080 | # WebPreferences creates the web. | ||||
2081 | unless ( $this->{_session}->{store}->webExists( $this->{_web} ) ) { | ||||
2082 | throw Error::Simple( 'Unable to save topic ' | ||||
2083 | . $this->{_topic} | ||||
2084 | . ' - web ' | ||||
2085 | . $this->{_web} | ||||
2086 | . ' does not exist' ); | ||||
2087 | } | ||||
2088 | } | ||||
2089 | |||||
2090 | $this->_atomicLock($cUID); | ||||
2091 | my $i = $this->{_session}->{store}->getRevisionHistory($this); | ||||
2092 | my $currentRev = $i->hasNext() ? $i->next() : 1; | ||||
2093 | try { | ||||
2094 | if ( $currentRev && !$opts{forcenewrevision} ) { | ||||
2095 | |||||
2096 | # See if we want to replace the existing top revision | ||||
2097 | my $mtime1 = | ||||
2098 | $this->{_session}->{store} | ||||
2099 | ->getApproxRevTime( $this->{_web}, $this->{_topic} ); | ||||
2100 | my $mtime2 = time(); | ||||
2101 | my $dt = abs( $mtime2 - $mtime1 ); | ||||
2102 | if ( $dt <= $Foswiki::cfg{ReplaceIfEditedAgainWithin} ) { | ||||
2103 | my $info = $this->{_session}->{store}->getVersionInfo($this); | ||||
2104 | |||||
2105 | # same user? | ||||
2106 | if ( $info->{author} eq $cUID ) { | ||||
2107 | |||||
2108 | # reprev is required so we can tell when a merge is | ||||
2109 | # based on something that is *not* the original rev | ||||
2110 | # where another users' edit started. | ||||
2111 | $info->{reprev} = $info->{version}; | ||||
2112 | $info->{date} = $opts{forcedate} || time(); | ||||
2113 | $this->setRevisionInfo(%$info); | ||||
2114 | $this->{_session}->{store}->repRev( $this, $cUID, %opts ); | ||||
2115 | $this->{_loadedRev} = $currentRev; | ||||
2116 | $this->{_session}->{store}->recordChange( | ||||
2117 | verb => 'update', | ||||
2118 | cuid => $cUID, | ||||
2119 | revision => $currentRev, | ||||
2120 | path => $this->getPath(), | ||||
2121 | minor => 1, | ||||
2122 | comment => 'reprev', | ||||
2123 | ); | ||||
2124 | return $currentRev; | ||||
2125 | } | ||||
2126 | } | ||||
2127 | } | ||||
2128 | my $nextRev = $this->{_session}->{store}->getNextRevision($this); | ||||
2129 | $this->setRevisionInfo( | ||||
2130 | date => $opts{forcedate} || time(), | ||||
2131 | author => $cUID, | ||||
2132 | version => $nextRev, | ||||
2133 | ); | ||||
2134 | |||||
2135 | my $checkSave = | ||||
2136 | $this->{_session}->{store}->saveTopic( $this, $cUID, \%opts ); | ||||
2137 | ASSERT( $checkSave == $nextRev, "$checkSave != $nextRev" ) if DEBUG; | ||||
2138 | $this->{_loadedRev} = $nextRev; | ||||
2139 | $this->{_latestIsLoaded} = 1; | ||||
2140 | |||||
2141 | $this->{_session}->{store}->recordChange( | ||||
2142 | cuid => $cUID, | ||||
2143 | revision => $nextRev, | ||||
2144 | verb => $nextRev == 1 ? 'insert' : 'update', | ||||
2145 | path => $this->getPath(), | ||||
2146 | minor => $opts{minor}, | ||||
2147 | ); | ||||
2148 | } | ||||
2149 | finally { | ||||
2150 | $this->_atomicUnlock($cUID); | ||||
2151 | $this->fireDependency(); | ||||
2152 | }; | ||||
2153 | return $this->{_loadedRev}; | ||||
2154 | } | ||||
2155 | |||||
2156 | # An atomic lock will cause other | ||||
2157 | # processes that also try to claim a lock to block. A lock has a | ||||
2158 | # maximum lifetime of 2 minutes, so operations on a locked topic | ||||
2159 | # must be completed within that time. You cannot rely on the | ||||
2160 | # lock timeout clearing the lock, though; that should always | ||||
2161 | # be done by calling _atomicUnlock. The best thing to do is to guard | ||||
2162 | # the locked section with a try..finally clause. See man Error for more info. | ||||
2163 | # | ||||
2164 | # Atomic locks are _not_ the locks used when a topic is edited; those are | ||||
2165 | # Leases. | ||||
2166 | |||||
2167 | sub _atomicLock { | ||||
2168 | my ( $this, $cUID ) = @_; | ||||
2169 | if ( $this->{_topic} ) { | ||||
2170 | my $logger = $this->{_session}->logger(); | ||||
2171 | while (1) { | ||||
2172 | my ( $user, $time ) = | ||||
2173 | $this->{_session}->{store}->atomicLockInfo($this); | ||||
2174 | last if ( !$user || $cUID eq $user ); | ||||
2175 | $logger->log( 'warning', | ||||
2176 | 'Lock on ' | ||||
2177 | . $this->getPath() . ' for ' | ||||
2178 | . $cUID | ||||
2179 | . " denied by $user" ); | ||||
2180 | |||||
2181 | # see how old the lock is. If it's older than 2 minutes, | ||||
2182 | # break it anyway. Locks are atomic, and should never be | ||||
2183 | # held that long, by _any_ process. | ||||
2184 | if ( time() - $time > 2 * 60 ) { | ||||
2185 | $logger->log( 'warning', | ||||
2186 | $cUID . " broke ${user}s lock on " . $this->getPath() ); | ||||
2187 | $this->{_session}->{store}->atomicUnlock( $this, $cUID ); | ||||
2188 | last; | ||||
2189 | } | ||||
2190 | |||||
2191 | # wait a couple of seconds before trying again | ||||
2192 | sleep(2); | ||||
2193 | } | ||||
2194 | |||||
2195 | # Topic | ||||
2196 | $this->{_session}->{store}->atomicLock( $this, $cUID ); | ||||
2197 | } | ||||
2198 | else { | ||||
2199 | |||||
2200 | # Web: Recursively lock subwebs and topics | ||||
2201 | my $it = $this->eachWeb(); | ||||
2202 | while ( $it->hasNext() ) { | ||||
2203 | my $web = $this->{_web} . '/' . $it->next(); | ||||
2204 | my $meta = $this->new( $this->{_session}, $web ); | ||||
2205 | $meta->_atomicLock($cUID); | ||||
2206 | } | ||||
2207 | $it = $this->eachTopic(); | ||||
2208 | while ( $it->hasNext() ) { | ||||
2209 | my $meta = | ||||
2210 | $this->new( $this->{_session}, $this->{_web}, $it->next() ); | ||||
2211 | $meta->_atomicLock($cUID); | ||||
2212 | } | ||||
2213 | } | ||||
2214 | } | ||||
2215 | |||||
2216 | sub _atomicUnlock { | ||||
2217 | my ( $this, $cUID ) = @_; | ||||
2218 | if ( $this->{_topic} ) { | ||||
2219 | $this->{_session}->{store}->atomicUnlock($this); | ||||
2220 | } | ||||
2221 | else { | ||||
2222 | my $it = $this->eachWeb(); | ||||
2223 | while ( $it->hasNext() ) { | ||||
2224 | my $web = $this->{_web} . '/' . $it->next(); | ||||
2225 | my $meta = $this->new( $this->{_session}, $web ); | ||||
2226 | $meta->_atomicUnlock($cUID); | ||||
2227 | } | ||||
2228 | $it = $this->eachTopic(); | ||||
2229 | while ( $it->hasNext() ) { | ||||
2230 | my $meta = | ||||
2231 | $this->new( $this->{_session}, $this->{_web}, $it->next() ); | ||||
2232 | $meta->_atomicUnlock($cUID); | ||||
2233 | } | ||||
2234 | } | ||||
2235 | } | ||||
2236 | |||||
2237 | =begin TML | ||||
2238 | |||||
2239 | ---++ ObjectMethod move($to, %opts) | ||||
2240 | |||||
2241 | Move this object (web or topic) to a store location specified by the | ||||
2242 | object $to. %opts may include: | ||||
2243 | * =user= - cUID of the user doing the moving. | ||||
2244 | |||||
2245 | =cut | ||||
2246 | |||||
2247 | # will assert false if the loaded rev of the object is not | ||||
2248 | # the latest rev. | ||||
2249 | sub move { | ||||
2250 | my ( $this, $to, %opts ) = @_; | ||||
2251 | ASSERT( $this->{_web}, 'this is not a movable object' ) if DEBUG; | ||||
2252 | ASSERT( $to->isa('Foswiki::Meta') && $to->{_web}, | ||||
2253 | 'to is not a moving target' ) | ||||
2254 | if DEBUG; | ||||
2255 | |||||
2256 | my $cUID = $opts{user} || $this->{_session}->{user}; | ||||
2257 | |||||
2258 | if ( $this->{_topic} ) { | ||||
2259 | |||||
2260 | # Move topic | ||||
2261 | |||||
2262 | $this->_atomicLock($cUID); | ||||
2263 | $to->_atomicLock($cUID); | ||||
2264 | |||||
2265 | # Ensure latest rev is loaded | ||||
2266 | my $from; | ||||
2267 | if ( $this->latestIsLoaded() ) { | ||||
2268 | $from = $this; | ||||
2269 | } | ||||
2270 | else { | ||||
2271 | $from = $this->load(); | ||||
2272 | } | ||||
2273 | |||||
2274 | # Clear outstanding leases. We assume that the caller has checked | ||||
2275 | # that the lease is OK to kill. | ||||
2276 | $from->clearLease() if $from->getLease(); | ||||
2277 | try { | ||||
2278 | $from->put( | ||||
2279 | 'TOPICMOVED', | ||||
2280 | { | ||||
2281 | from => $from->getPath(), | ||||
2282 | to => $to->getPath(), | ||||
2283 | date => time(), | ||||
2284 | by => $cUID, | ||||
2285 | } | ||||
2286 | ); | ||||
2287 | |||||
2288 | # save the metadata change without logging | ||||
2289 | $this->saveAs( | ||||
2290 | dontlog => 1, # no statistics | ||||
2291 | ); | ||||
2292 | $from->{_session}->{store}->moveTopic( $from, $to, $cUID ); | ||||
2293 | $to->loadVersion(); | ||||
2294 | ASSERT( defined($to) and defined( $to->{_loadedRev} ) ) if DEBUG; | ||||
2295 | $this->{_session}->{store}->recordChange( | ||||
2296 | cuid => $cUID, | ||||
2297 | revision => $to->{_loadedRev}, | ||||
2298 | verb => 'update', | ||||
2299 | oldpath => $from->getPath(), | ||||
2300 | path => $to->getPath() | ||||
2301 | ); | ||||
2302 | |||||
2303 | } | ||||
2304 | finally { | ||||
2305 | $from->_atomicUnlock($cUID); | ||||
2306 | $to->_atomicUnlock($cUID); | ||||
2307 | $from->fireDependency(); | ||||
2308 | $to->fireDependency(); | ||||
2309 | }; | ||||
2310 | |||||
2311 | } | ||||
2312 | else { | ||||
2313 | |||||
2314 | # Move web | ||||
2315 | ASSERT( !$this->{_session}->{store}->webExists( $to->{_web} ), | ||||
2316 | "$to->{_web} does not exist" ) | ||||
2317 | if DEBUG; | ||||
2318 | $this->_atomicLock($cUID); | ||||
2319 | $this->{_session}->{store}->moveWeb( $this, $to, $cUID ); | ||||
2320 | |||||
2321 | # Record the web move as a move of the WebPreferences topic | ||||
2322 | my $from = | ||||
2323 | Foswiki::Meta->load( $this->{_session}, $this->web, | ||||
2324 | $Foswiki::cfg{WebPrefsTopicName} ); | ||||
2325 | my $to = | ||||
2326 | Foswiki::Meta->load( $this->{_session}, $to->web, | ||||
2327 | $Foswiki::cfg{WebPrefsTopicName} ); | ||||
2328 | $this->{_session}->{store}->recordChange( | ||||
2329 | cuid => $cUID, | ||||
2330 | revision => $to->{_loadedRev}, | ||||
2331 | verb => 'update', | ||||
2332 | oldpath => $from->getPath(), | ||||
2333 | path => $to->getPath(), | ||||
2334 | comment => 'moved_web' | ||||
2335 | ); | ||||
2336 | |||||
2337 | # No point in unlocking $this - it's moved! | ||||
2338 | $to->_atomicUnlock($cUID); | ||||
2339 | } | ||||
2340 | |||||
2341 | # Log rename | ||||
2342 | my $old = $this->{_web} . '.' . ( $this->{_topic} || '' ); | ||||
2343 | my $new = $to->{_web} . '.' . ( $to->{_topic} || '' ); | ||||
2344 | $this->{_session}->logger->log( | ||||
2345 | { | ||||
2346 | level => 'info', | ||||
2347 | action => 'rename', | ||||
2348 | webTopic => $old, | ||||
2349 | extra => "moved to $new", | ||||
2350 | user => $this->{_session}->{user} | ||||
2351 | } | ||||
2352 | ); | ||||
2353 | |||||
2354 | # alert plugins of topic move | ||||
2355 | $this->{_session}->{plugins} | ||||
2356 | ->dispatch( 'afterRenameHandler', $this->{_web}, $this->{_topic} || '', | ||||
2357 | '', $to->{_web}, $to->{_topic} || '', '' ); | ||||
2358 | } | ||||
2359 | |||||
2360 | =begin TML | ||||
2361 | |||||
2362 | ---++ ObjectMethod deleteMostRecentRevision(%opts) | ||||
2363 | Delete (or elide) the most recent revision of this. Only works on topics. | ||||
2364 | |||||
2365 | =%opts= may include | ||||
2366 | * =user= - cUID of user doing the unlocking | ||||
2367 | |||||
2368 | =cut | ||||
2369 | |||||
2370 | sub deleteMostRecentRevision { | ||||
2371 | my ( $this, %opts ) = @_; | ||||
2372 | _assertIsTopic($this) if DEBUG; | ||||
2373 | my $rev; | ||||
2374 | my $cUID = $opts{user} || $this->{_session}->{user}; | ||||
2375 | |||||
2376 | $this->_atomicLock($cUID); | ||||
2377 | try { | ||||
2378 | $rev = $this->{_session}->{store}->delRev( $this, $cUID ); | ||||
2379 | $this->{_session}->{store}->recordChange( | ||||
2380 | cuid => $cUID, | ||||
2381 | revision => $rev, | ||||
2382 | verb => 'update', | ||||
2383 | path => $this->getPath | ||||
2384 | ); | ||||
2385 | |||||
2386 | } | ||||
2387 | finally { | ||||
2388 | $this->_atomicUnlock($cUID); | ||||
2389 | $this->fireDependency(); | ||||
2390 | }; | ||||
2391 | |||||
2392 | # TODO: delete entry in .changes | ||||
2393 | |||||
2394 | # write log entry | ||||
2395 | $this->{_session}->logger->log( | ||||
2396 | { | ||||
2397 | level => 'info', | ||||
2398 | action => 'cmd', | ||||
2399 | webTopic => $this->{_web} . '.' . $this->{_topic}, | ||||
2400 | extra => "delRev $rev", | ||||
2401 | user => $this->{_session}->{user}, | ||||
2402 | } | ||||
2403 | ); | ||||
2404 | } | ||||
2405 | |||||
2406 | =begin TML | ||||
2407 | |||||
2408 | ---++ ObjectMethod replaceMostRecentRevision( %opts ) | ||||
2409 | Replace the most recent revision with whatever is in the memory copy. | ||||
2410 | Only works on topics. | ||||
2411 | |||||
2412 | %opts may include: | ||||
2413 | * =forcedate= - try and re-use the date of the original check | ||||
2414 | * =user= - cUID of the user doing the action | ||||
2415 | |||||
2416 | =cut | ||||
2417 | |||||
2418 | sub replaceMostRecentRevision { | ||||
2419 | my $this = shift; | ||||
2420 | my %opts = @_; | ||||
2421 | _assertIsTopic($this) if DEBUG; | ||||
2422 | |||||
2423 | my $cUID = $opts{user} || $this->{_session}->{user}; | ||||
2424 | |||||
2425 | $this->_atomicLock($cUID); | ||||
2426 | |||||
2427 | my $info = $this->getRevisionInfo(); | ||||
2428 | |||||
2429 | if ( $opts{forcedate} ) { | ||||
2430 | |||||
2431 | # We are trying to force the rev to be saved with the same date | ||||
2432 | # and user as the prior rev. However, exactly the same date may | ||||
2433 | # cause some revision control systems to barf, so to avoid this we | ||||
2434 | # add 1 minute to the rev time. Note that this mode of operation | ||||
2435 | # will normally require sysadmin privilege, as it can result in | ||||
2436 | # confused rev dates if abused. | ||||
2437 | $info->{date} += 60; | ||||
2438 | } | ||||
2439 | else { | ||||
2440 | |||||
2441 | # use defaults (current time, current user) | ||||
2442 | $info->{date} = time(); | ||||
2443 | $info->{author} = $cUID; | ||||
2444 | } | ||||
2445 | |||||
2446 | # repRev is required so we can tell when a merge is based on something | ||||
2447 | # that is *not* the original rev where another users' edit started. | ||||
2448 | $info->{reprev} = $info->{version}; | ||||
2449 | $this->setRevisionInfo(%$info); | ||||
2450 | |||||
2451 | try { | ||||
2452 | $this->{_session}->{store}->repRev( $this, $info->{author}, @_ ); | ||||
2453 | } | ||||
2454 | finally { | ||||
2455 | $this->_atomicUnlock($cUID); | ||||
2456 | $this->fireDependency(); | ||||
2457 | }; | ||||
2458 | |||||
2459 | # write log entry | ||||
2460 | require Foswiki::Time; | ||||
2461 | my @extras = ( $info->{version} ); | ||||
2462 | push( @extras, | ||||
2463 | Foswiki::Time::formatTime( $info->{date}, '$rcs', 'gmtime' ) ); | ||||
2464 | push( @extras, 'minor' ) if $opts{minor}; | ||||
2465 | push( @extras, 'dontlog' ) if $opts{dontlog}; | ||||
2466 | push( @extras, 'forced' ) if $opts{forcedate}; | ||||
2467 | $this->{_session}->logger->log( | ||||
2468 | { | ||||
2469 | level => 'info', | ||||
2470 | action => 'reprev', | ||||
2471 | webTopic => $this->getPath(), | ||||
2472 | extra => join( ', ', @extras ), | ||||
2473 | user => $cUID, | ||||
2474 | } | ||||
2475 | ); | ||||
2476 | } | ||||
2477 | |||||
2478 | =begin TML | ||||
2479 | |||||
2480 | ---++ ObjectMethod getRevisionHistory([$attachment]) -> $iterator | ||||
2481 | |||||
2482 | Get an iterator over the range of version identifiers (just the identifiers, | ||||
2483 | not the content) starting with the most recent revision. | ||||
2484 | |||||
2485 | The iterator will be empty ($iterator->hasNext() will be false) if the object | ||||
2486 | does not exist. | ||||
2487 | |||||
2488 | $attachment is optional. | ||||
2489 | |||||
2490 | Not valid on webs. | ||||
2491 | |||||
2492 | =cut | ||||
2493 | |||||
2494 | # spent 747µs (16+731) within Foswiki::Meta::getRevisionHistory which was called 2 times, avg 374µs/call:
# once (12µs+396µs) by Foswiki::UI::View::view at line 166 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm
# once (4µs+335µs) by Foswiki::UI::View::revisionsAround at line 507 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm | ||||
2495 | 2 | 1µs | my ( $this, $attachment ) = @_; | ||
2496 | _assertIsTopic($this) if DEBUG; | ||||
2497 | |||||
2498 | # if ((not defined($attachment)) and ($this->{_latestIsLoaded})) { | ||||
2499 | # #why poke around in revision history (slow) if we 'have the latest' | ||||
2500 | # return new Foswiki::Iterator::NumberRangeIterator( $this->{_loadedRev}, 1 ); | ||||
2501 | # } | ||||
2502 | |||||
2503 | 2 | 14µs | 2 | 731µs | return $this->{_session}->{store}->getRevisionHistory( $this, $attachment ); # spent 731µs making 2 calls to Foswiki::Store::Rcs::Store::getRevisionHistory, avg 366µs/call |
2504 | } | ||||
2505 | |||||
2506 | =begin TML | ||||
2507 | |||||
2508 | ---++ ObjectMethod getLatestRev[$attachment]) -> $revision | ||||
2509 | |||||
2510 | Get the revision ID of the latest revision. | ||||
2511 | |||||
2512 | $attachment is optional. | ||||
2513 | |||||
2514 | Not valid on webs. | ||||
2515 | |||||
2516 | Returns an integer revision number > 0 if the object exists. | ||||
2517 | |||||
2518 | Returns 0 if the object does not exist. | ||||
2519 | |||||
2520 | =cut | ||||
2521 | |||||
2522 | sub getLatestRev { | ||||
2523 | my $this = shift; | ||||
2524 | my $it = $this->getRevisionHistory(@_); | ||||
2525 | return 0 unless $it->hasNext(); | ||||
2526 | return $it->next(); | ||||
2527 | } | ||||
2528 | |||||
2529 | =begin TML | ||||
2530 | |||||
2531 | ---++ ObjectMethod latestIsLoaded() -> $boolean | ||||
2532 | Return true if the currently loaded rev is the latest rev. Note that there may have | ||||
2533 | been changes to the meta or text locally in the loaded meta; these changes will be | ||||
2534 | retained. | ||||
2535 | |||||
2536 | Only valid on topics. | ||||
2537 | |||||
2538 | =cut | ||||
2539 | |||||
2540 | # spent 93.2ms within Foswiki::Meta::latestIsLoaded which was called 70084 times, avg 1µs/call:
# 61320 times (82.5ms+0s) by Foswiki::MetaCache::addMeta at line 150 of /var/www/foswikidev/core/lib/Foswiki/MetaCache.pm, avg 1µs/call
# 8760 times (10.7ms+0s) by Foswiki::Store::QueryAlgorithms::BruteForce::_webQuery at line 211 of /var/www/foswikidev/core/lib/Foswiki/Store/QueryAlgorithms/BruteForce.pm, avg 1µs/call
# 3 times (8µs+0s) by Foswiki::Store::Rcs::Store::getVersionInfo at line 353 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 3µs/call
# once (3µs+0s) by Foswiki::QUERY at line 34 of /var/www/foswikidev/core/lib/Foswiki/Macros/QUERY.pm | ||||
2541 | 70084 | 13.9ms | my $this = shift; | ||
2542 | _assertIsTopic($this) if DEBUG; | ||||
2543 | 70084 | 259ms | return $this->{_latestIsLoaded} if defined $this->{_latestIsLoaded}; | ||
2544 | 3 | 8µs | return defined $this->{_loadedRev} | ||
2545 | && $this->{_loadedRev} == $this->getLatestRev(); | ||||
2546 | } | ||||
2547 | |||||
2548 | =begin TML | ||||
2549 | |||||
2550 | ---++ ObjectMethod getLoadedRev() -> $integer | ||||
2551 | |||||
2552 | Get the currently loaded revision. Result will be a revision number, or | ||||
2553 | undef if no revision has been loaded. Only valid on topics. | ||||
2554 | |||||
2555 | WARNING: some store implementations use the concept of a "working copy" of | ||||
2556 | each topic that may be modified *without* being added to the revision | ||||
2557 | control system. This means that the version number reported for the latest | ||||
2558 | rev may not be the actual latest version. | ||||
2559 | |||||
2560 | =cut | ||||
2561 | |||||
2562 | # spent 138ms within Foswiki::Meta::getLoadedRev which was called 122649 times, avg 1µs/call:
# 122640 times (138ms+0s) by Foswiki::MetaCache::addMeta at line 150 of /var/www/foswikidev/core/lib/Foswiki/MetaCache.pm, avg 1µs/call
# 3 times (8µs+0s) by Foswiki::META at line 37 of /var/www/foswikidev/core/lib/Foswiki/Macros/META.pm, avg 3µs/call
# 3 times (5µs+0s) by Foswiki::REVINFO at line 28 of /var/www/foswikidev/core/lib/Foswiki/Macros/REVINFO.pm, avg 2µs/call
# 3 times (4µs+0s) by Foswiki::Store::Rcs::Store::getVersionInfo at line 348 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 1µs/call | ||||
2563 | 122649 | 22.9ms | my $this = shift; | ||
2564 | _assertIsTopic($this) if DEBUG; | ||||
2565 | 122649 | 408ms | return $this->{_loadedRev}; | ||
2566 | } | ||||
2567 | |||||
2568 | =begin TML | ||||
2569 | |||||
2570 | ---++ ObjectMethod setLoadStatus($rev, $isLatest) | ||||
2571 | |||||
2572 | Used by the Store implementation to set the load status | ||||
2573 | when a topic is read. Must be called by implementations of | ||||
2574 | =Foswiki::Store::readTopic=. Do not use for anything else! | ||||
2575 | |||||
2576 | =cut | ||||
2577 | |||||
2578 | # spent 106ms within Foswiki::Meta::setLoadStatus which was called 52654 times, avg 2µs/call:
# 26327 times (57.6ms+0s) by Foswiki::Store::Rcs::Store::readTopic at line 187 of /var/www/foswikidev/core/lib/Foswiki/Store/Rcs/Store.pm, avg 2µs/call
# 26327 times (48.6ms+0s) by Foswiki::Meta::loadVersion at line 1155, avg 2µs/call | ||||
2579 | 52654 | 11.9ms | my $this = shift; | ||
2580 | 52654 | 238ms | ( $this->{_loadedRev}, $this->{_latestIsLoaded} ) = @_; | ||
2581 | } | ||||
2582 | |||||
2583 | =begin TML | ||||
2584 | |||||
2585 | ---++ ObjectMethod removeFromStore( $attachment ) | ||||
2586 | * =$attachment= - optional, provide to delete an attachment | ||||
2587 | |||||
2588 | Use with great care! Removes all trace of the given web, topic | ||||
2589 | or attachment from the store, possibly including all its history. | ||||
2590 | |||||
2591 | Also does not ensure consistency of the store | ||||
2592 | (for eg, if you delete an attachment, it does not update the in-topic META) | ||||
2593 | |||||
2594 | =cut | ||||
2595 | |||||
2596 | sub removeFromStore { | ||||
2597 | my ( $this, $attachment ) = @_; | ||||
2598 | my $store = $this->{_session}->{store}; | ||||
2599 | ASSERT( $this->{_web}, 'this is not a removable object' ) if DEBUG; | ||||
2600 | |||||
2601 | if ( !$store->webExists( $this->{_web} ) ) { | ||||
2602 | throw Error::Simple( 'No such web ' . $this->{_web} ); | ||||
2603 | } | ||||
2604 | if ( $this->{_topic} | ||||
2605 | && !$store->topicExists( $this->{_web}, $this->{_topic} ) ) | ||||
2606 | { | ||||
2607 | throw Error::Simple( | ||||
2608 | 'No such topic ' . $this->{_web} . '.' . $this->{_topic} ); | ||||
2609 | } | ||||
2610 | |||||
2611 | if ( $attachment && !$this->hasAttachment($attachment) ) { | ||||
2612 | ASSERT( $this->{topic}, 'this is not a removable object' ) if DEBUG; | ||||
2613 | throw Error::Simple( 'No such attachment ' | ||||
2614 | . $this->{_web} . '.' | ||||
2615 | . $this->{_topic} . '.' | ||||
2616 | . $attachment ); | ||||
2617 | } | ||||
2618 | $store->remove( $this->{_session}->{user}, $this, $attachment ); | ||||
2619 | $this->{_session}->{store}->recordChange( | ||||
2620 | verb => 'remove', | ||||
2621 | cuid => $this->{_session}->{user}, | ||||
2622 | |||||
2623 | # revision = -1 when removing webs | ||||
2624 | revision => $this->{_loadedRev} || -1, | ||||
2625 | path => $this->getPath(), | ||||
2626 | attachment => $attachment | ||||
2627 | ); | ||||
2628 | } | ||||
2629 | |||||
2630 | =begin TML | ||||
2631 | |||||
2632 | ---++ ObjectMethod getDifferences( $rev2, $contextLines ) -> \@diffArray | ||||
2633 | |||||
2634 | Get the differences between the rev loaded into this object, and another | ||||
2635 | rev of the same topic. Return reference to an array of differences. | ||||
2636 | * =$rev2= - the other revision to diff against | ||||
2637 | * =$contextLines= - number of lines of context required | ||||
2638 | |||||
2639 | Each difference is of the form [ $type, $right, $left ] where | ||||
2640 | | *type* | *Means* | | ||||
2641 | | =+= | Added | | ||||
2642 | | =-= | Deleted | | ||||
2643 | | =c= | Changed | | ||||
2644 | | =u= | Unchanged | | ||||
2645 | | =l= | Line Number | | ||||
2646 | |||||
2647 | =cut | ||||
2648 | |||||
2649 | sub getDifferences { | ||||
2650 | my ( $this, $rev2, $contextLines ) = @_; | ||||
2651 | _assertIsTopic($this) if DEBUG; | ||||
2652 | return $this->{_session}->{store} | ||||
2653 | ->getRevisionDiff( $this, $rev2, $contextLines ); | ||||
2654 | } | ||||
2655 | |||||
2656 | =begin TML | ||||
2657 | |||||
2658 | ---++ ObjectMethod getRevisionAtTime( $time ) -> $rev | ||||
2659 | * =$time= - time (in epoch secs) for the rev | ||||
2660 | |||||
2661 | Get the revision number for a topic at a specific time. | ||||
2662 | Returns a single-digit rev number or 0 if it couldn't be determined | ||||
2663 | (either because the topic isn't that old, or there was a problem) | ||||
2664 | |||||
2665 | =cut | ||||
2666 | |||||
2667 | sub getRevisionAtTime { | ||||
2668 | my ( $this, $time ) = @_; | ||||
2669 | _assertIsTopic($this) if DEBUG; | ||||
2670 | return $this->{_session}->{store}->getRevisionAtTime( $this, $time ); | ||||
2671 | } | ||||
2672 | |||||
2673 | =begin TML | ||||
2674 | |||||
2675 | ---++ ObjectMethod setLease( $length ) | ||||
2676 | |||||
2677 | Take out an lease on the given topic for this user for $length seconds. | ||||
2678 | |||||
2679 | See =getLease= for more details about Leases. | ||||
2680 | |||||
2681 | =cut | ||||
2682 | |||||
2683 | sub setLease { | ||||
2684 | my ( $this, $length ) = @_; | ||||
2685 | _assertIsTopic($this) if DEBUG; | ||||
2686 | my $t = time(); | ||||
2687 | my $lease = { | ||||
2688 | user => $this->{_session}->{user}, | ||||
2689 | expires => $t + $length, | ||||
2690 | taken => $t | ||||
2691 | }; | ||||
2692 | return $this->{_session}->{store}->setLease( $this, $lease ); | ||||
2693 | } | ||||
2694 | |||||
2695 | =begin TML | ||||
2696 | |||||
2697 | ---++ ObjectMethod getLease() -> $lease | ||||
2698 | |||||
2699 | If there is an lease on the topic, return the lease, otherwise undef. | ||||
2700 | A lease is a block of meta-information about a topic that can be | ||||
2701 | recovered (this is a hash containing =user=, =taken= and =expires=). | ||||
2702 | Leases are taken out when a topic is edited. Only one lease | ||||
2703 | can be active on a topic at a time. Leases are used to warn if | ||||
2704 | another user is already editing a topic. | ||||
2705 | |||||
2706 | =cut | ||||
2707 | |||||
2708 | sub getLease { | ||||
2709 | my $this = shift; | ||||
2710 | _assertIsTopic($this) if DEBUG; | ||||
2711 | return $this->{_session}->{store}->getLease($this); | ||||
2712 | } | ||||
2713 | |||||
2714 | =begin TML | ||||
2715 | |||||
2716 | ---++ ObjectMethod clearLease() | ||||
2717 | |||||
2718 | Cancel the current lease. | ||||
2719 | |||||
2720 | See =getLease= for more details about Leases. | ||||
2721 | |||||
2722 | =cut | ||||
2723 | |||||
2724 | sub clearLease { | ||||
2725 | my $this = shift; | ||||
2726 | _assertIsTopic($this) if DEBUG; | ||||
2727 | $this->{_session}->{store}->setLease($this); | ||||
2728 | } | ||||
2729 | |||||
2730 | =begin TML | ||||
2731 | |||||
2732 | ---++ ObjectMethod onTick($time) | ||||
2733 | |||||
2734 | Method invoked at regular intervals, usually by a cron job. The job of | ||||
2735 | this method is to prod the store into cleaning up expired leases, and | ||||
2736 | any other admin job that needs doing at regular intervals. | ||||
2737 | |||||
2738 | =cut | ||||
2739 | |||||
2740 | sub onTick { | ||||
2741 | my ( $this, $time ) = @_; | ||||
2742 | |||||
2743 | if ( !$this->{_topic} ) { | ||||
2744 | my $it = $this->eachWeb(); | ||||
2745 | while ( $it->hasNext() ) { | ||||
2746 | my $web = $it->next(); | ||||
2747 | $web = $this->getPath() . "/$web" if $this->getPath(); | ||||
2748 | my $m = $this->new( $this->{_session}, $web ); | ||||
2749 | $m->onTick($time); | ||||
2750 | } | ||||
2751 | if ( $this->{_web} ) { | ||||
2752 | $it = $this->eachTopic(); | ||||
2753 | while ( $it->hasNext() ) { | ||||
2754 | my $topic = $it->next(); | ||||
2755 | my $topicObject = | ||||
2756 | $this->new( $this->{_session}, $this->getPath(), $topic ); | ||||
2757 | $topicObject->onTick($time); | ||||
2758 | } | ||||
2759 | } | ||||
2760 | |||||
2761 | # Clean up spurious leases that may have been left behind | ||||
2762 | # during cancelled topic creation | ||||
2763 | $this->{_session}->{store}->removeSpuriousLeases( $this->getPath() ) | ||||
2764 | if $this->getPath(); | ||||
2765 | } | ||||
2766 | else { | ||||
2767 | my $lease = $this->getLease(); | ||||
2768 | if ( $lease && $lease->{expires} < $time ) { | ||||
2769 | $this->clearLease(); | ||||
2770 | } | ||||
2771 | } | ||||
2772 | } | ||||
2773 | |||||
2774 | ############# ATTACHMENTS ON TOPICS ############# | ||||
2775 | |||||
2776 | =begin TML | ||||
2777 | |||||
2778 | ---++ *Deprecated* ObjectMethod getAttachmentRevisionInfo($attachment, $rev) -> \%info | ||||
2779 | * =$attachment= - attachment name | ||||
2780 | * =$rev= - optional integer attachment revision number | ||||
2781 | Get revision info for an attachment. Only valid on topics. | ||||
2782 | |||||
2783 | $info will contain at least: date, author, version, comment | ||||
2784 | |||||
2785 | *Deprecated* 2014-11-03 use getRevisionInfo instead. | ||||
2786 | |||||
2787 | =cut | ||||
2788 | |||||
2789 | sub getAttachmentRevisionInfo { | ||||
2790 | my ( $this, $attachment, $fromrev ) = @_; | ||||
2791 | _assertIsTopic($this) if DEBUG; | ||||
2792 | |||||
2793 | return $this->{_session}->{store} | ||||
2794 | ->getVersionInfo( $this, $fromrev, $attachment ); | ||||
2795 | } | ||||
2796 | |||||
2797 | =begin TML | ||||
2798 | |||||
2799 | ---++ ObjectMethod attach ( %opts ) | ||||
2800 | |||||
2801 | * =%opts= may include: | ||||
2802 | * =name= - Name of the attachment - required | ||||
2803 | * =dontlog= - don't add to statistics | ||||
2804 | * =comment= - comment for save | ||||
2805 | * =hide= - if the attachment is to be hidden in normal topic view | ||||
2806 | * =stream= - Stream of file to upload. Uses =file= if not set. | ||||
2807 | * =file= - Name of a *server* file to use for the attachment | ||||
2808 | data. This should be passed if it is known, as it may be used | ||||
2809 | to optimise handler calls. | ||||
2810 | * =filepath= - Optional. Client path to file. | ||||
2811 | * =filesize= - Optional. Size of uploaded data. | ||||
2812 | * =filedate= - Optional. Date of file. | ||||
2813 | * =author= - Optional. cUID of author of change. Defaults to current. | ||||
2814 | * =notopicchange= - Optional. if the topic is *not* to be modified. | ||||
2815 | This may result in incorrect meta-data stored in the topic, so must | ||||
2816 | be used with care. Only has a meaning if the store implementation | ||||
2817 | stores meta-data in topics. | ||||
2818 | * =nohandlers= - *do not* call plugin handlers | ||||
2819 | |||||
2820 | Saves a new revision of the attachment, invoking plugin handlers as | ||||
2821 | appropriate. This method automatically updates the loaded rev of $this | ||||
2822 | to the latest topic revision. | ||||
2823 | |||||
2824 | If neither of =stream= or =file= are set, this is a properties-only save. | ||||
2825 | |||||
2826 | Throws an exception on error. | ||||
2827 | |||||
2828 | =cut | ||||
2829 | |||||
2830 | # SMELL: arguably should only be permitted if the loaded rev of the object is the same as the | ||||
2831 | # latest rev. | ||||
2832 | |||||
2833 | sub attach { | ||||
2834 | my $this = shift; | ||||
2835 | my %opts = @_; | ||||
2836 | my $action; | ||||
2837 | my $plugins = $this->{_session}->{plugins}; | ||||
2838 | _assertIsAttachment( $this, $opts{name} ) if DEBUG; | ||||
2839 | |||||
2840 | # make sure we don't save a half-loaded topic stub... | ||||
2841 | # which indeed - SMELL - is possible | ||||
2842 | $this->loadVersion() unless $this->latestIsLoaded(); | ||||
2843 | |||||
2844 | #ASSERT( $this->latestIsLoaded(), $this->getPath() ) if DEBUG; | ||||
2845 | #ASSERT( $this->{_loadedRev}, $this->getPath() ) if DEBUG; | ||||
2846 | |||||
2847 | if ( $opts{file} && !$opts{stream} ) { | ||||
2848 | |||||
2849 | # no stream given, but a file was given; open it. | ||||
2850 | open( $opts{stream}, '<', $opts{file} ) | ||||
2851 | || throw Error::Simple( 'Could not open ' . $opts{file} ); | ||||
2852 | binmode( $opts{stream} ) | ||||
2853 | || throw Error::Simple( $opts{file} . ' binmode failed: ' . $! ); | ||||
2854 | } | ||||
2855 | |||||
2856 | my $attrs; | ||||
2857 | if ( $opts{stream} ) { | ||||
2858 | $action = 'upload'; | ||||
2859 | |||||
2860 | $attrs = { | ||||
2861 | name => $opts{name}, | ||||
2862 | attachment => $opts{name}, | ||||
2863 | stream => $opts{stream}, | ||||
2864 | user => $opts{author} || $this->{_session}->{user}, # cUID | ||||
2865 | comment => defined $opts{comment} ? $opts{comment} : '', | ||||
2866 | }; | ||||
2867 | |||||
2868 | my $handlers_called = 0; | ||||
2869 | |||||
2870 | if ( !$opts{nohandlers} | ||||
2871 | && $plugins->haveHandlerFor('beforeAttachmentSaveHandler') ) | ||||
2872 | { | ||||
2873 | |||||
2874 | # *Deprecated* handler. | ||||
2875 | |||||
2876 | # The handler may have been called as a result of an upload, | ||||
2877 | # in which case the data is already in a file in the CGI cache, | ||||
2878 | # and the stream is valid, or it may be been arrived at via a | ||||
2879 | # call to Func::saveAttachment, in which case it's possible that | ||||
2880 | # the stream isn't open but we have a tmpFilename instead. | ||||
2881 | # | ||||
2882 | $attrs->{tmpFilename} = $opts{file}; | ||||
2883 | |||||
2884 | if ( !defined( $attrs->{tmpFilename} ) ) { | ||||
2885 | |||||
2886 | # CGI (or the caller) did not provide a temporary file | ||||
2887 | |||||
2888 | # Stream the data to a temporary file, so it can be passed | ||||
2889 | # to the handler. | ||||
2890 | |||||
2891 | require File::Temp; | ||||
2892 | |||||
2893 | my $fh = new File::Temp(); | ||||
2894 | binmode($fh); | ||||
2895 | |||||
2896 | # transfer 512KB blocks | ||||
2897 | my $transfer; | ||||
2898 | my $r; | ||||
2899 | while ( $r = sysread( $opts{stream}, $transfer, 0x80000 ) ) { | ||||
2900 | if ( !defined $r ) { | ||||
2901 | next if ( $! == Errno::EINTR ); | ||||
2902 | die "system read error: $!\n"; | ||||
2903 | } | ||||
2904 | my $offset = 0; | ||||
2905 | while ($r) { | ||||
2906 | my $w = syswrite( $fh, $transfer, $r, $offset ); | ||||
2907 | die "system write error: $!\n" unless ( defined $w ); | ||||
2908 | $offset += $w; | ||||
2909 | $r -= $w; | ||||
2910 | } | ||||
2911 | } | ||||
2912 | select( ( select($fh), $| = 1 )[0] ); | ||||
2913 | |||||
2914 | # $fh->seek only in File::Temp 0.17 and later | ||||
2915 | seek( $fh, 0, 0 ) or die "Can't seek temp: $!\n"; | ||||
2916 | $opts{stream} = $fh; | ||||
2917 | $attrs->{tmpFilename} = $fh->filename(); | ||||
2918 | } | ||||
2919 | |||||
2920 | $plugins->dispatch( 'beforeAttachmentSaveHandler', $attrs, | ||||
2921 | $this->{_topic}, $this->{_web} ); | ||||
2922 | |||||
2923 | # Have to assume it's changed, even if it hasn't. | ||||
2924 | open( $attrs->{stream}, '<', $attrs->{tmpFilename} ) | ||||
2925 | || die "Internal error: $!"; | ||||
2926 | binmode( $attrs->{stream} ); | ||||
2927 | $opts{stream} = $attrs->{stream}; | ||||
2928 | |||||
2929 | delete $attrs->{tmpFilename}; | ||||
2930 | |||||
2931 | $handlers_called = 1; | ||||
2932 | } | ||||
2933 | |||||
2934 | if ( !$opts{nohandlers} | ||||
2935 | && $plugins->haveHandlerFor('beforeUploadHandler') ) | ||||
2936 | { | ||||
2937 | |||||
2938 | # Check the stream is seekable | ||||
2939 | ASSERT( | ||||
2940 | seek( $attrs->{stream}, 0, 1 ), | ||||
2941 | 'Stream for attachment is not seekable' | ||||
2942 | ) if DEBUG; | ||||
2943 | |||||
2944 | $plugins->dispatch( 'beforeUploadHandler', $attrs, $this ); | ||||
2945 | $opts{stream} = $attrs->{stream}; | ||||
2946 | seek( $opts{stream}, 0, 0 ); # seek to beginning | ||||
2947 | binmode( $opts{stream} ); | ||||
2948 | |||||
2949 | $handlers_called = 1; | ||||
2950 | } | ||||
2951 | |||||
2952 | if ($handlers_called) { | ||||
2953 | |||||
2954 | # Force reload of the latest version | ||||
2955 | # Note that latestIsLoaded may still be false if the | ||||
2956 | # topic doesn't exist yet | ||||
2957 | $this->unload(); | ||||
2958 | $this->loadVersion(); | ||||
2959 | } | ||||
2960 | |||||
2961 | $opts{author} ||= $this->{_session}->{user}; | ||||
2962 | |||||
2963 | my $error; | ||||
2964 | try { | ||||
2965 | my $arev = | ||||
2966 | $this->{_session}->{store} | ||||
2967 | ->saveAttachment( $this, $opts{name}, $opts{stream}, | ||||
2968 | $opts{author}, \%opts ); | ||||
2969 | |||||
2970 | $attrs->{version} = $arev; | ||||
2971 | $attrs->{path} = $opts{filepath} if defined $opts{filepath}; | ||||
2972 | $attrs->{size} = $opts{filesize} if defined $opts{filesize}; | ||||
2973 | $attrs->{date} = defined $opts{filedate} ? $opts{filedate} : time(); | ||||
2974 | |||||
2975 | # Note that there will be two events; the attachment save, | ||||
2976 | # followed by the topic update | ||||
2977 | $this->{_session}->{store}->recordChange( | ||||
2978 | verb => $arev > 1 ? 'update' : 'insert', | ||||
2979 | cuid => $opts{author}, | ||||
2980 | revision => $this->{_loadedRev} || 1, | ||||
2981 | path => $this->getPath(), | ||||
2982 | attachment => $opts{name}, | ||||
2983 | comment => "add $arev" | ||||
2984 | ); | ||||
2985 | |||||
2986 | if ( !$opts{nohandlers} | ||||
2987 | && $plugins->haveHandlerFor('afterAttachmentSaveHandler') ) | ||||
2988 | { | ||||
2989 | |||||
2990 | # *Deprecated* handler | ||||
2991 | $plugins->dispatch( 'afterAttachmentSaveHandler', $attrs, | ||||
2992 | $this->{_topic}, $this->{_web} ); | ||||
2993 | } | ||||
2994 | } | ||||
2995 | finally { | ||||
2996 | $this->fireDependency(); | ||||
2997 | }; | ||||
2998 | } | ||||
2999 | else { | ||||
3000 | |||||
3001 | # Property change | ||||
3002 | $action = 'save'; | ||||
3003 | $attrs = $this->get( 'FILEATTACHMENT', $opts{name} ); | ||||
3004 | $attrs->{name} = $opts{name}; | ||||
3005 | $attrs->{comment} = $opts{comment} if ( defined( $opts{comment} ) ); | ||||
3006 | } | ||||
3007 | $attrs->{attr} = ( $opts{hide} ) ? 'h' : ''; | ||||
3008 | delete $attrs->{stream}; | ||||
3009 | delete $attrs->{tmpFilename}; | ||||
3010 | $this->putKeyed( 'FILEATTACHMENT', $attrs ); | ||||
3011 | |||||
3012 | if ( $opts{createlink} ) { | ||||
3013 | my $text = $this->text(); | ||||
3014 | $text = '' unless defined $text; | ||||
3015 | $text .= | ||||
3016 | $this->{_session}->attach->getAttachmentLink( $this, $opts{name} ); | ||||
3017 | $this->text($text); | ||||
3018 | } | ||||
3019 | |||||
3020 | $this->saveAs() unless $opts{notopicchange}; | ||||
3021 | |||||
3022 | my @extras = ( $opts{name} ); | ||||
3023 | push( @extras, 'dontlog' ) if $opts{dontlog}; # no statistics | ||||
3024 | $this->{_session}->logger->log( | ||||
3025 | { | ||||
3026 | level => 'info', | ||||
3027 | action => $action, | ||||
3028 | webTopic => $this->{_web} . '.' . $this->{_topic}, | ||||
3029 | extra => join( ', ', @extras ), | ||||
3030 | user => $this->{_session}->{user}, | ||||
3031 | } | ||||
3032 | ); | ||||
3033 | |||||
3034 | if ( !$opts{nohandlers} && $plugins->haveHandlerFor('afterUploadHandler') ) | ||||
3035 | { | ||||
3036 | $plugins->dispatch( 'afterUploadHandler', $attrs, $this ); | ||||
3037 | } | ||||
3038 | } | ||||
3039 | |||||
3040 | =begin TML | ||||
3041 | |||||
3042 | ---++ ObjectMethod hasAttachment( $name ) -> $boolean | ||||
3043 | Test if the named attachment exists. Only valid on topics. The attachment | ||||
3044 | must exist in the store (it is not sufficient for it to be referenced | ||||
3045 | in the object only) | ||||
3046 | |||||
3047 | =cut | ||||
3048 | |||||
3049 | # spent 116µs (23+93) within Foswiki::Meta::hasAttachment which was called:
# once (23µs+93µs) by Foswiki::_lookupIcon at line 53 of /var/www/foswikidev/core/lib/Foswiki/Macros/ICON.pm | ||||
3050 | 1 | 1µs | my ( $this, $name ) = @_; | ||
3051 | _assertIsAttachment( $this, $name ) if DEBUG; | ||||
3052 | 1 | 18µs | 1 | 93µs | return $this->{_session}->{store}->attachmentExists( $this, $name ); # spent 93µs making 1 call to Foswiki::Store::Rcs::Store::attachmentExists |
3053 | } | ||||
3054 | |||||
3055 | =begin TML | ||||
3056 | |||||
3057 | ---++ ObjectMethod testAttachment( $name, $test ) -> $value | ||||
3058 | |||||
3059 | Performs a type test on the given attachment file. | ||||
3060 | * =$name= - name of the attachment to test e.g =lolcat.gif= | ||||
3061 | * =$test= - the test to perform e.g. ='r'= | ||||
3062 | |||||
3063 | The return value is the value that would be returned by the standard | ||||
3064 | perl file operations, as indicated by $type | ||||
3065 | |||||
3066 | * r File is readable by current user (tests Foswiki VIEW permission) | ||||
3067 | * w File is writable by current user (tests Foswiki CHANGE permission) | ||||
3068 | * e File exists. | ||||
3069 | * z File has zero size. | ||||
3070 | * s File has nonzero size (returns size). | ||||
3071 | * T File is an ASCII text file (heuristic guess). | ||||
3072 | * B File is a "binary" file (opposite of T). | ||||
3073 | * M Last modification time (epoch seconds). | ||||
3074 | * A Last access time (epoch seconds). | ||||
3075 | |||||
3076 | Note that all these types should behave as the equivalent standard perl | ||||
3077 | operator behaves, except M and A which are independent of the script start | ||||
3078 | time (see perldoc -f -X for more information) | ||||
3079 | |||||
3080 | Other standard Perl file tests may also be supported on some store | ||||
3081 | implementations, but cannot be relied on. | ||||
3082 | |||||
3083 | Errors will be signalled by an Error::Simple exception. | ||||
3084 | |||||
3085 | =cut | ||||
3086 | |||||
3087 | sub testAttachment { | ||||
3088 | my ( $this, $attachment, $test ) = @_; | ||||
3089 | _assertIsAttachment( $this, $attachment ) if DEBUG; | ||||
3090 | |||||
3091 | $this->addDependency(); | ||||
3092 | |||||
3093 | $test =~ m/(\w)/; | ||||
3094 | $test = $1; | ||||
3095 | if ( $test eq 'r' ) { | ||||
3096 | return $this->haveAccess('VIEW'); | ||||
3097 | } | ||||
3098 | elsif ( $test eq 'w' ) { | ||||
3099 | return $this->haveAccess('CHANGE'); | ||||
3100 | } | ||||
3101 | |||||
3102 | return | ||||
3103 | return $this->{_session}->{store} | ||||
3104 | ->testAttachment( $this, $attachment, $test ); | ||||
3105 | } | ||||
3106 | |||||
3107 | =begin TML | ||||
3108 | |||||
3109 | ---+++ openAttachment($attachment, $mode, %opts) -> $fh | ||||
3110 | * =$attachment= - the attachment | ||||
3111 | * =$mode= - mode to open the attachment in | ||||
3112 | Opens a stream onto the attachment. This method is primarily to | ||||
3113 | support virtual file systems, and as such access controls are *not* | ||||
3114 | checked, plugin handlers are *not* called, and it does *not* update the | ||||
3115 | meta-data in the topicObject. | ||||
3116 | |||||
3117 | =$mode= can be '<', '>' or '>>' for read, write, and append | ||||
3118 | respectively. | ||||
3119 | |||||
3120 | =%opts= can take different settings depending on =$mode=. | ||||
3121 | * =$mode='<'= | ||||
3122 | * =version= - revision of the object to open e.g. =version => 6= | ||||
3123 | * =$mode='>'= or ='>>' | ||||
3124 | * no options | ||||
3125 | Errors will be signalled by an =Error= exception. | ||||
3126 | |||||
3127 | See also =attach= if this function is too basic for you. | ||||
3128 | |||||
3129 | =cut | ||||
3130 | |||||
3131 | sub openAttachment { | ||||
3132 | my ( $this, $attachment, $mode, @opts ) = @_; | ||||
3133 | _assertIsAttachment( $this, $attachment ) if DEBUG; | ||||
3134 | ASSERT($attachment) if DEBUG; | ||||
3135 | |||||
3136 | return $this->{_session}->{store} | ||||
3137 | ->openAttachment( $this, $attachment, $mode, @opts ); | ||||
3138 | |||||
3139 | } | ||||
3140 | |||||
3141 | =begin TML | ||||
3142 | |||||
3143 | ---++ ObjectMethod moveAttachment( $name, $to, %opts ) -> $data | ||||
3144 | Move the named attachment to the topic indicates by $to. | ||||
3145 | =%opts= may include: | ||||
3146 | * =new_name= - new name for the attachment | ||||
3147 | * =user= - cUID of user doing the moving | ||||
3148 | |||||
3149 | =cut | ||||
3150 | |||||
3151 | sub moveAttachment { | ||||
3152 | my $this = shift; | ||||
3153 | my $name = shift; | ||||
3154 | my $to = shift; | ||||
3155 | my %opts = @_; | ||||
3156 | my $cUID = $opts{user} || $this->{_session}->{user}; | ||||
3157 | _assertIsAttachment( $this, $name ) if DEBUG; | ||||
3158 | _assertIsTopic($to) if DEBUG; | ||||
3159 | |||||
3160 | my $newName = $opts{new_name} || $name; | ||||
3161 | |||||
3162 | # Make sure we have latest revs | ||||
3163 | $this = $this->load() unless $this->latestIsLoaded(); | ||||
3164 | |||||
3165 | $this->_atomicLock($cUID); | ||||
3166 | $to->_atomicLock($cUID); | ||||
3167 | |||||
3168 | try { | ||||
3169 | $this->{_session}->{store} | ||||
3170 | ->moveAttachment( $this, $name, $to, $newName, $cUID ); | ||||
3171 | |||||
3172 | # Modify the cache of the old topic | ||||
3173 | my $fileAttachment = $this->get( 'FILEATTACHMENT', $name ); | ||||
3174 | $this->remove( 'FILEATTACHMENT', $name ); | ||||
3175 | $this->saveAs( | ||||
3176 | dontlog => 1, # no statistics | ||||
3177 | comment => 'lost ' . $name | ||||
3178 | ); | ||||
3179 | |||||
3180 | # Add file attachment to new topic | ||||
3181 | $fileAttachment->{name} = $newName; | ||||
3182 | $fileAttachment->{movefrom} = $this->getPath() . '.' . $name; | ||||
3183 | $fileAttachment->{moveby} = | ||||
3184 | $this->{_session}->{users}->getLoginName($cUID); | ||||
3185 | $fileAttachment->{movedto} = $to->getPath() . '.' . $newName; | ||||
3186 | $fileAttachment->{movedwhen} = time(); | ||||
3187 | $to->loadVersion(); | ||||
3188 | $to->putKeyed( 'FILEATTACHMENT', $fileAttachment ); | ||||
3189 | |||||
3190 | if ( $this->getPath() eq $to->getPath() ) { | ||||
3191 | $to->remove( 'FILEATTACHMENT', $name ); | ||||
3192 | } | ||||
3193 | |||||
3194 | $to->saveAs( | ||||
3195 | dontlog => 1, # no statistics | ||||
3196 | comment => 'gained' . $newName | ||||
3197 | ); | ||||
3198 | |||||
3199 | $this->{_session}->{store}->recordChange( | ||||
3200 | cuid => $cUID, | ||||
3201 | revision => $to->{_loadedRev}, | ||||
3202 | verb => 'update', | ||||
3203 | oldpath => $this->getPath(), | ||||
3204 | oldattachment => $name, | ||||
3205 | path => $to->getPath(), | ||||
3206 | attachment => $newName | ||||
3207 | ); | ||||
3208 | |||||
3209 | } | ||||
3210 | finally { | ||||
3211 | $to->_atomicUnlock($cUID); | ||||
3212 | $this->_atomicUnlock($cUID); | ||||
3213 | $this->fireDependency(); | ||||
3214 | $to->fireDependency(); | ||||
3215 | }; | ||||
3216 | |||||
3217 | # alert plugins of attachment move | ||||
3218 | $this->{_session}->{plugins} | ||||
3219 | ->dispatch( 'afterRenameHandler', $this->{_web}, $this->{_topic}, $name, | ||||
3220 | $to->{_web}, $to->{_topic}, $newName ); | ||||
3221 | |||||
3222 | $this->{_session}->logger->log( | ||||
3223 | { | ||||
3224 | level => 'info', | ||||
3225 | action => 'move', | ||||
3226 | webTopic => $this->getPath() . '.' . $name, | ||||
3227 | extra => ' moved to ' . $to->getPath() . '.' . $newName, | ||||
3228 | user => $cUID, | ||||
3229 | } | ||||
3230 | ); | ||||
3231 | } | ||||
3232 | |||||
3233 | =begin TML | ||||
3234 | |||||
3235 | ---++ ObjectMethod copyAttachment( $name, $to, %opts ) -> $data | ||||
3236 | Copy the named attachment to the topic indicates by $to. | ||||
3237 | =%opts= may include: | ||||
3238 | * =new_name= - new name for the attachment | ||||
3239 | * =user= - cUID of user doing the moving | ||||
3240 | |||||
3241 | =cut | ||||
3242 | |||||
3243 | sub copyAttachment { | ||||
3244 | my $this = shift; | ||||
3245 | my $name = shift; | ||||
3246 | my $to = shift; | ||||
3247 | my %opts = @_; | ||||
3248 | my $cUID = $opts{user} || $this->{_session}->{user}; | ||||
3249 | _assertIsAttachment( $this, $name ) if DEBUG; | ||||
3250 | _assertIsTopic($to) if DEBUG; | ||||
3251 | |||||
3252 | my $newName = $opts{new_name} || $name; | ||||
3253 | |||||
3254 | # Make sure we have latest revs | ||||
3255 | my $from; | ||||
3256 | if ( $this->latestIsLoaded() ) { | ||||
3257 | $from = $this; | ||||
3258 | } | ||||
3259 | else { | ||||
3260 | $from = $this->load(); | ||||
3261 | } | ||||
3262 | |||||
3263 | $from->_atomicLock($cUID); | ||||
3264 | $to->_atomicLock($cUID); | ||||
3265 | |||||
3266 | try { | ||||
3267 | $from->{_session}->{store} | ||||
3268 | ->copyAttachment( $from, $name, $to, $newName, $cUID ); | ||||
3269 | |||||
3270 | # Add file attachment to new topic by copying the old one | ||||
3271 | my $fileAttachment = { %{ $from->get( 'FILEATTACHMENT', $name ) } }; | ||||
3272 | $fileAttachment->{name} = $newName; | ||||
3273 | |||||
3274 | $to->loadVersion() unless $to->latestIsLoaded(); | ||||
3275 | $to->putKeyed( 'FILEATTACHMENT', $fileAttachment ); | ||||
3276 | |||||
3277 | if ( $from->getPath() eq $to->getPath() ) { | ||||
3278 | $to->remove( 'FILEATTACHMENT', $name ); | ||||
3279 | } | ||||
3280 | |||||
3281 | $to->saveAs( | ||||
3282 | author => $cUID, | ||||
3283 | dontlog => 1, # no statistics | ||||
3284 | comment => 'gained' . $newName | ||||
3285 | ); | ||||
3286 | $this->{_session}->{store}->recordChange( | ||||
3287 | verb => 'copy', | ||||
3288 | cuid => $cUID, | ||||
3289 | revision => $to->{_loadedRev}, | ||||
3290 | oldpath => $from->getPath(), | ||||
3291 | oldattachment => $name, | ||||
3292 | path => $to->getPath(), | ||||
3293 | attachment => $newName | ||||
3294 | ); | ||||
3295 | |||||
3296 | } | ||||
3297 | finally { | ||||
3298 | $to->_atomicUnlock($cUID); | ||||
3299 | $from->_atomicUnlock($cUID); | ||||
3300 | $from->fireDependency(); | ||||
3301 | $to->fireDependency(); | ||||
3302 | }; | ||||
3303 | |||||
3304 | # alert plugins of attachment move | ||||
3305 | # SMELL: no defined handler for attachment copies | ||||
3306 | # $this->{_session}->{plugins} | ||||
3307 | # ->dispatch( 'afterCopyHandler', $this->{_web}, $this->{_topic}, $name, | ||||
3308 | # $to->{_web}, $to->{_topic}, $newName ); | ||||
3309 | |||||
3310 | $this->{_session}->logger->log( | ||||
3311 | { | ||||
3312 | level => 'info', | ||||
3313 | action => 'copy', | ||||
3314 | webTopic => $this->getPath() . '.' . $name, | ||||
3315 | extra => ' copied to ' . $to->getPath() . '.' . $newName, | ||||
3316 | user => $cUID, | ||||
3317 | } | ||||
3318 | ); | ||||
3319 | } | ||||
3320 | |||||
3321 | =begin TML | ||||
3322 | |||||
3323 | ---++ ObjectMethod expandNewTopic() | ||||
3324 | Expand only that subset of Foswiki variables that are | ||||
3325 | expanded during topic creation, in the body text and | ||||
3326 | PREFERENCE meta only. | ||||
3327 | |||||
3328 | The expansion is in-place in the object data. | ||||
3329 | |||||
3330 | Only valid on topics. | ||||
3331 | |||||
3332 | =cut | ||||
3333 | |||||
3334 | sub expandNewTopic { | ||||
3335 | my ($this) = @_; | ||||
3336 | _assertIsTopic($this) if DEBUG; | ||||
3337 | $this->{_session}->expandMacrosOnTopicCreation($this); | ||||
3338 | } | ||||
3339 | |||||
3340 | =begin TML | ||||
3341 | |||||
3342 | ---++ ObjectMethod expandMacros( $text ) -> $text | ||||
3343 | Expand only all Foswiki variables that are | ||||
3344 | expanded during topic view. Returns the expanded text. | ||||
3345 | Only valid on topics. | ||||
3346 | |||||
3347 | =cut | ||||
3348 | |||||
3349 | # spent 137s (408µs+137) within Foswiki::Meta::expandMacros which was called 100 times, avg 1.37s/call:
# 91 times (349µs+56µs) by Foswiki::Func::expandCommonVariables at line 2658 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 4µs/call
# 4 times (27µs+-27µs) by Foswiki::inlineAlert at line 2624 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 0s/call
# 3 times (24µs+137s) by Foswiki::UI::View::_prepare at line 479 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm, avg 45.7s/call
# 2 times (9µs+10.8ms) by Foswiki::_renderZone at line 3872 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 5.40ms/call | ||||
3350 | 100 | 58µs | my ( $this, $text ) = @_; | ||
3351 | _assertIsTopic($this) if DEBUG; | ||||
3352 | |||||
3353 | 100 | 315µs | 100 | 137s | return $this->{_session}->expandMacros( $text, $this ); # spent 137s making 100 calls to Foswiki::expandMacros, avg 1.37s/call, recursion: max depth 2, sum of overlapping time 62.2ms |
3354 | } | ||||
3355 | |||||
3356 | =begin TML | ||||
3357 | |||||
3358 | ---++ ObjectMethod renderTML( $text ) -> $text | ||||
3359 | Render all TML constructs in the text into HTML. Returns the rendered text. | ||||
3360 | Only valid on topics. | ||||
3361 | |||||
3362 | =cut | ||||
3363 | |||||
3364 | # spent 50.2ms (52µs+50.1) within Foswiki::Meta::renderTML which was called 5 times, avg 10.0ms/call:
# 3 times (39µs+44.2ms) by Foswiki::UI::View::_prepare at line 480 of /var/www/foswikidev/core/lib/Foswiki/UI/View.pm, avg 14.8ms/call
# 2 times (13µs+5.91ms) by Foswiki::_renderZone at line 3873 of /var/www/foswikidev/core/lib/Foswiki.pm, avg 2.96ms/call | ||||
3365 | 5 | 10µs | my ( $this, $text ) = @_; | ||
3366 | _assertIsTopic($this) if DEBUG; | ||||
3367 | 5 | 37µs | 10 | 50.1ms | return $this->{_session}->renderer->getRenderedVersion( $text, $this ); # spent 50.1ms making 5 calls to Foswiki::Render::getRenderedVersion, avg 10.0ms/call
# spent 12µs making 5 calls to Foswiki::renderer, avg 2µs/call |
3368 | } | ||||
3369 | |||||
3370 | =begin TML | ||||
3371 | |||||
3372 | ---++ ObjectMethod summariseText( $flags [, $text, \%searchOptions] ) -> $tml | ||||
3373 | |||||
3374 | Makes a plain text summary of the topic text by simply trimming a bit | ||||
3375 | off the top. Truncates to $TMTRUNC chars or, if a number is specified | ||||
3376 | in $flags, to that length. | ||||
3377 | |||||
3378 | If $text is defined, use it in place of the topic text. | ||||
3379 | |||||
3380 | The =\%searchOptions= hash may contain the following options: | ||||
3381 | * =type= - search type: keyword, literal, query | ||||
3382 | * =casesensitive= - false to ignore case (default true) | ||||
3383 | * =wordboundaries= - if type is 'keyword' | ||||
3384 | * =tokens= - array ref of search tokens | ||||
3385 | |||||
3386 | TODO: should this really be in Meta? it seems like a rendering issue to me. | ||||
3387 | |||||
3388 | warning: this will produce text that contains html entities - including quotes | ||||
3389 | use =$summary = Foswiki::entityEncode($summary);= to diffuse them | ||||
3390 | |||||
3391 | |||||
3392 | =cut | ||||
3393 | |||||
3394 | sub summariseText { | ||||
3395 | my ( $this, $flags, $text, $searchOptions ) = @_; | ||||
3396 | _assertIsTopic($this) if DEBUG; | ||||
3397 | |||||
3398 | $flags ||= ''; | ||||
3399 | |||||
3400 | $text = $this->text() unless defined $text; | ||||
3401 | $text = '' unless defined $text; | ||||
3402 | |||||
3403 | my $plainText = | ||||
3404 | $this->session->renderer->TML2PlainText( $text, $this, $flags ); | ||||
3405 | $plainText =~ s/\n+/ /g; | ||||
3406 | |||||
3407 | # limit to n chars | ||||
3408 | my $limit = $flags || ''; | ||||
3409 | unless ( $limit =~ s/^.*?([0-9]+).*$/$1/ ) { | ||||
3410 | $limit = $SUMMARY_TMLTRUNC; | ||||
3411 | } | ||||
3412 | $limit = $SUMMARY_MINTRUNC if ( $limit < $SUMMARY_MINTRUNC ); | ||||
3413 | |||||
3414 | if ( $flags =~ m/searchcontext/ ) { | ||||
3415 | return $this->_summariseTextWithSearchContext( $plainText, $limit, | ||||
3416 | $searchOptions ); | ||||
3417 | } | ||||
3418 | else { | ||||
3419 | return $this->_summariseTextSimple( $plainText, $limit ); | ||||
3420 | } | ||||
3421 | } | ||||
3422 | |||||
3423 | =begin TML | ||||
3424 | |||||
3425 | ---++ ObjectMethod _summariseTextSimple( $text, $limit ) -> $tml | ||||
3426 | |||||
3427 | Makes a plain text summary of the topic text by simply trimming a bit | ||||
3428 | off the top. Truncates to $TMTRUNC chars or, if a number is specified | ||||
3429 | in $flags, to that length. | ||||
3430 | |||||
3431 | TODO: should this really be in Meta? it seems like a rendering issue to me. | ||||
3432 | |||||
3433 | =cut | ||||
3434 | |||||
3435 | sub _summariseTextSimple { | ||||
3436 | my ( $this, $text, $limit ) = @_; | ||||
3437 | _assertIsTopic($this) if DEBUG; | ||||
3438 | |||||
3439 | $text =~ s/^(.{$limit}).*$/$1.../s; | ||||
3440 | |||||
3441 | return $this->_makeSummaryTextSafe($text); | ||||
3442 | } | ||||
3443 | |||||
3444 | sub _makeSummaryTextSafe { | ||||
3445 | my ( $this, $text ) = @_; | ||||
3446 | |||||
3447 | my $session = $this->session(); | ||||
3448 | my $renderer = $session->renderer(); | ||||
3449 | |||||
3450 | # We do not want the summary to contain any $variable that formatted | ||||
3451 | # searches can interpret to anything (Item3489). | ||||
3452 | # Especially new lines (Item2496) | ||||
3453 | # To not waste performance we simply replace $ by $<nop> | ||||
3454 | $text =~ s/\$/\$<nop>/g; | ||||
3455 | |||||
3456 | # Escape Interwiki links and other side effects introduced by | ||||
3457 | # plugins later in the rendering pipeline (Item4748) | ||||
3458 | $text =~ s/\:/<nop>\:/g; | ||||
3459 | $text =~ s/\s+/ /g; | ||||
3460 | |||||
3461 | return $this->session->renderer->protectPlainText($text); | ||||
3462 | } | ||||
3463 | |||||
3464 | =begin TML | ||||
3465 | |||||
3466 | ---++ ObjectMethod _summariseTextWithSearchContext( $text, $limit, $type, $searchOptions ) -> $tml | ||||
3467 | |||||
3468 | Improves the presentation of summaries for keyword, word and literal searches, by displaying topic content on either side of the search terms wherever they are found in the topic. | ||||
3469 | |||||
3470 | The =\%searchOptions= hash may contain the following options: | ||||
3471 | * =type= - search type: keyword, literal, query | ||||
3472 | * =casesensitive= - false to ignore case (default true) | ||||
3473 | * =wordboundaries= - if type is 'keyword' | ||||
3474 | * =tokens= - array ref of search tokens | ||||
3475 | |||||
3476 | =cut | ||||
3477 | |||||
3478 | sub _summariseTextWithSearchContext { | ||||
3479 | my ( $this, $text, $limit, $searchOptions ) = @_; | ||||
3480 | |||||
3481 | if ( !$searchOptions->{tokens} ) { | ||||
3482 | return $this->_summariseTextSimple( $text, $limit ); | ||||
3483 | } | ||||
3484 | |||||
3485 | my $type = $searchOptions->{type} || ''; | ||||
3486 | if ( $type ne 'keyword' && $type ne 'literal' && $type ne '' ) { | ||||
3487 | return $this->_summariseTextSimple( $text, $limit ); | ||||
3488 | } | ||||
3489 | |||||
3490 | my $caseSensitive = $searchOptions->{casesensitive} || ''; | ||||
3491 | my $wordBoundaries = $searchOptions->{wordboundaries} || ''; | ||||
3492 | |||||
3493 | #Item12166 | ||||
3494 | #NOTE: this is duplicating the F::Search::Node code, and probably the F::Q:: =~ parse | ||||
3495 | #and the SearchAlgo already deals with this issue to some degree (i'm not sure it does unmatched [ etc) | ||||
3496 | |||||
3497 | my $tToken; | ||||
3498 | my @tokens = map { | ||||
3499 | |||||
3500 | $tToken = $_; # copy $_ to avoid changing the passed token | ||||
3501 | |||||
3502 | #we get a crash if the tokem is not a valid regex. - for eg a single lone * | ||||
3503 | #actually need to escape all things that would trash the regex | ||||
3504 | #TODO: this needs to be extracted from here and Forking.pm and pushed into F::Search::Node | ||||
3505 | $tToken =~ s#([][|/\\\$\^*()+{};@?.{}])#\\$1#g if ( $type ne 'regex' ); | ||||
3506 | $tToken; | ||||
3507 | } grep { !/^!.*$/ } @{ $searchOptions->{tokens} }; | ||||
3508 | my $keystrs = join( '|', @tokens ); | ||||
3509 | |||||
3510 | if ( !$keystrs ) { | ||||
3511 | return $this->_summariseTextSimple( $text, $limit ); | ||||
3512 | } | ||||
3513 | |||||
3514 | # we don't have a means currently to set the word window through a parameter | ||||
3515 | # so we always use the default | ||||
3516 | my $context = $SUMMARY_DEFAULT_CONTEXT; | ||||
3517 | |||||
3518 | # break on words with search type 'word' (which is passed as type 'keyword' with $wordBoundaries as true | ||||
3519 | my $wordBoundaryAnchor = | ||||
3520 | ( $type eq 'keyword' && $wordBoundaries ) ? '\b' : ''; | ||||
3521 | $keystrs = $caseSensitive ? "($keystrs)" : "((?i:$keystrs))"; | ||||
3522 | my $termsPattern = $wordBoundaryAnchor . $keystrs . $wordBoundaryAnchor; | ||||
3523 | |||||
3524 | # if $wordBoundaries is false, only break on whole words at start and end, not surrounding the search term; therefore the pattern at start differs from the pattern at the end | ||||
3525 | my $beforePattern = "(\\b.{0,$context}$wordBoundaryAnchor)"; | ||||
3526 | my $afterPattern = "($wordBoundaryAnchor.{0,$context}\\b)"; | ||||
3527 | my $searchPattern = $beforePattern . $termsPattern . $afterPattern; | ||||
3528 | |||||
3529 | my $summary = ''; | ||||
3530 | my $summaryLength = 0; | ||||
3531 | while ( $summaryLength < $limit && $text =~ m/$searchPattern/gs ) { | ||||
3532 | my $before = $1 || ''; | ||||
3533 | my $term = $2 || ''; | ||||
3534 | my $after = $3 || ''; | ||||
3535 | |||||
3536 | $before = $this->_makeSummaryTextSafe($before); | ||||
3537 | $term = $this->_makeSummaryTextSafe($term); | ||||
3538 | $after = $this->_makeSummaryTextSafe($after); | ||||
3539 | |||||
3540 | $summaryLength += length "$before$term$after"; | ||||
3541 | |||||
3542 | my $startLoc = $-[0]; | ||||
3543 | |||||
3544 | # only show ellipsis when not at the start | ||||
3545 | # and when we don't have any summary text yet | ||||
3546 | if ( !$summary && $startLoc != 0 ) { | ||||
3547 | $before = "$SUMMARY_ELLIPSIS $before"; | ||||
3548 | } | ||||
3549 | |||||
3550 | my $endLoc = $+[0] || $-[0]; | ||||
3551 | $after = "$after $SUMMARY_ELLIPSIS" if $endLoc != length $text; | ||||
3552 | |||||
3553 | $summary .= $before . CGI::em( {}, $term ) . $after . ' '; | ||||
3554 | } | ||||
3555 | |||||
3556 | return $this->_summariseTextSimple( $text, $limit ) if !$summary; | ||||
3557 | |||||
3558 | return $summary; | ||||
3559 | } | ||||
3560 | |||||
3561 | =begin TML | ||||
3562 | |||||
3563 | ---++ ObjectMethod summariseChanges( $orev, $nrev, $tml, $nochecks) -> $text | ||||
3564 | |||||
3565 | Generate a (max 3 line) summary of the differences between the revs. | ||||
3566 | |||||
3567 | * =$orev= - older rev, if not defined will use ($nrev - 1) | ||||
3568 | * =$nrev= - later rev, if not defined defaults to latest | ||||
3569 | * =$tml= - if true will generate renderable TML (i.e. HTML with NOPs. | ||||
3570 | If false will generate a summary suitable for use in plain text | ||||
3571 | (mail, for example) | ||||
3572 | * =$nochecks= - if true, access control checks will be suppressed | ||||
3573 | |||||
3574 | If there is only one rev, a topic summary will be returned. | ||||
3575 | |||||
3576 | If =$tml= is not set, all HTML will be removed. | ||||
3577 | |||||
3578 | In non-tml, lines are truncated to 70 characters. Differences are shown using + and - to indicate added and removed text. | ||||
3579 | |||||
3580 | =cut | ||||
3581 | |||||
3582 | sub summariseChanges { | ||||
3583 | my ( $this, $orev, $nrev, $tml, $nochecks ) = @_; | ||||
3584 | my $summary = ''; | ||||
3585 | my $session = $this->session(); | ||||
3586 | my $renderer = $session->renderer(); | ||||
3587 | |||||
3588 | _assertIsTopic($this) if DEBUG; | ||||
3589 | $nrev = $this->getLatestRev() unless $nrev; | ||||
3590 | |||||
3591 | ASSERT( $nrev =~ m/^\s*\d+\s*/ ) if DEBUG; # looks like a number | ||||
3592 | |||||
3593 | $orev = $nrev - 1 unless defined($orev); | ||||
3594 | |||||
3595 | ASSERT( $orev =~ m/^\s*\d+\s*/ ) if DEBUG; # looks like a number | ||||
3596 | ASSERT( $orev >= 0 ) if DEBUG; | ||||
3597 | ASSERT( $nrev >= $orev ) if DEBUG; | ||||
3598 | |||||
3599 | unless ( defined $this->{_loadedRev} && $this->{_loadedRev} eq $nrev ) { | ||||
3600 | $this = $this->load($nrev); | ||||
3601 | } | ||||
3602 | |||||
3603 | my $ntext = ''; | ||||
3604 | if ( $nochecks || $this->haveAccess('VIEW') ) { | ||||
3605 | |||||
3606 | # Only get the text if we have access to nrev | ||||
3607 | $ntext = $this->text(); | ||||
3608 | } | ||||
3609 | |||||
3610 | return '' if ( $orev == $nrev ); # same rev, no differences | ||||
3611 | |||||
3612 | my $nstring = $this->stringify(); | ||||
3613 | $nstring =~ s/^%META:TOPICINFO\{.*?}%//ms; | ||||
3614 | |||||
3615 | #print "SSSSSS nstring\n($nstring)\nSSSSSS\n\n"; | ||||
3616 | |||||
3617 | $ntext = $renderer->TML2PlainText( $nstring, $this, 'showvar showmeta' ); | ||||
3618 | |||||
3619 | #print "SSSSSS ntext\n($ntext)\nSSSSSS\n\n"; | ||||
3620 | |||||
3621 | my $oldTopicObject = | ||||
3622 | Foswiki::Meta->load( $session, $this->web, $this->topic, $orev ); | ||||
3623 | unless ( $nochecks || $oldTopicObject->haveAccess('VIEW') ) { | ||||
3624 | |||||
3625 | # No access to old rev, make a blank topic object | ||||
3626 | $oldTopicObject = | ||||
3627 | Foswiki::Meta->new( $session, $this->web, $this->topic, '' ); | ||||
3628 | } | ||||
3629 | |||||
3630 | my $ostring = $oldTopicObject->stringify(); | ||||
3631 | $ostring =~ s/^%META:TOPICINFO\{.*?}%$//ms; | ||||
3632 | |||||
3633 | #print "SSSSSS ostring\n$ostring\nSSSSSS\n\n"; | ||||
3634 | |||||
3635 | my $otext = | ||||
3636 | $renderer->TML2PlainText( $ostring, $oldTopicObject, 'showvar showmeta' ); | ||||
3637 | |||||
3638 | #print "SSSSSS otext\n($otext)\nSSSSSS\n\n"; | ||||
3639 | |||||
3640 | require Foswiki::Merge; | ||||
3641 | my $blocks = Foswiki::Merge::simpleMerge( $otext, $ntext, qr/[\r\n]+/ ); | ||||
3642 | |||||
3643 | #foreach $b ( @$blocks ) { | ||||
3644 | # print "BBBB\n($b)\nBBBB\n\n"; | ||||
3645 | # } | ||||
3646 | |||||
3647 | # sort through, keeping one line of context either side of a change | ||||
3648 | my @revised; | ||||
3649 | my $getnext = 0; | ||||
3650 | my $prev = ''; | ||||
3651 | my $ellipsis = $tml ? $SUMMARY_ELLIPSIS : '...'; | ||||
3652 | my $trunc = $tml ? $SUMMARY_TMLTRUNC : $CHANGES_SUMMARY_PLAINTRUNC; | ||||
3653 | while ( scalar(@$blocks) && scalar(@revised) < $CHANGES_SUMMARY_LINECOUNT ) | ||||
3654 | { | ||||
3655 | my $block = shift(@$blocks); | ||||
3656 | next unless $block =~ m/\S/; | ||||
3657 | my $trim = length($block) > $trunc; | ||||
3658 | $block =~ s/^(.{$trunc}).*$/$1/ if ($trim); | ||||
3659 | if ( $block =~ m/^[-+]/ ) { | ||||
3660 | if ($tml) { | ||||
3661 | $block =~ s/^-(.*)$/CGI::del( {}, $1 )/se; | ||||
3662 | $block =~ s/^\+(.*)$/CGI::ins( {}, $1 )/se; | ||||
3663 | } | ||||
3664 | elsif ( $session->inContext('rss') ) { | ||||
3665 | $block =~ s/^-/REMOVED: /; | ||||
3666 | $block =~ s/^\+/INSERTED: /; | ||||
3667 | } | ||||
3668 | push( @revised, $prev ) if $prev; | ||||
3669 | $block .= $ellipsis if $trim; | ||||
3670 | push( @revised, $block ); | ||||
3671 | $getnext = 1; | ||||
3672 | $prev = ''; | ||||
3673 | } | ||||
3674 | else { | ||||
3675 | if ($getnext) { | ||||
3676 | $block .= $ellipsis if $trim; | ||||
3677 | push( @revised, $block ); | ||||
3678 | $getnext = 0; | ||||
3679 | $prev = ''; | ||||
3680 | } | ||||
3681 | else { | ||||
3682 | $prev = $block; | ||||
3683 | } | ||||
3684 | } | ||||
3685 | } | ||||
3686 | if ($tml) { | ||||
3687 | $summary = join( CGI::br(), @revised ); | ||||
3688 | } | ||||
3689 | else { | ||||
3690 | $summary = join( "\n", @revised ); | ||||
3691 | } | ||||
3692 | |||||
3693 | unless ($summary) { | ||||
3694 | return $this->summariseText( '', $ntext ); | ||||
3695 | } | ||||
3696 | |||||
3697 | #print "SUMMARY\n===================\n($summary)\n============\n\n"; | ||||
3698 | |||||
3699 | if ( !$tml ) { | ||||
3700 | $summary = $renderer->protectPlainText($summary); | ||||
3701 | } | ||||
3702 | return $summary; | ||||
3703 | } | ||||
3704 | |||||
3705 | =begin TML | ||||
3706 | |||||
3707 | ---++ *Deprecated* ObjectMethod getEmbeddedStoreForm() -> $text | ||||
3708 | |||||
3709 | Generate the embedded store form of the topic. The embedded store | ||||
3710 | form has meta-data values embedded using %META: lines. The text | ||||
3711 | stored in the meta is taken as the topic text. | ||||
3712 | |||||
3713 | *Deprecated* 2014-11-13, and will be removed in Foswiki 2.0. | ||||
3714 | It is retained for compatibility only. | ||||
3715 | use =Foswiki::Serialise::serialise($meta, 'Embedded')= instead. | ||||
3716 | |||||
3717 | =cut | ||||
3718 | |||||
3719 | sub getEmbeddedStoreForm { | ||||
3720 | my $this = shift; | ||||
3721 | |||||
3722 | _assertIsTopic($this) if DEBUG; | ||||
3723 | |||||
3724 | return Foswiki::Serialise::serialise( $this, 'Embedded' ); | ||||
3725 | } | ||||
3726 | |||||
3727 | =begin TML | ||||
3728 | |||||
3729 | ---++ *Deprecated* ObjectMethod setEmbeddedStoreForm( $text ) | ||||
3730 | |||||
3731 | Populate this object with embedded meta-data from $text. This method | ||||
3732 | is a utility provided for use with stores that store data embedded in | ||||
3733 | topic text. Only valid on topics. | ||||
3734 | |||||
3735 | Note: line endings must be normalised to \n *before* calling this method. | ||||
3736 | |||||
3737 | *Deprecated* 2014-11-13, and will be removed in Foswiki 2.0. | ||||
3738 | It is retained for compatibility only. | ||||
3739 | use =Foswiki::Serialise::deserialise($text, 'Embedded', $meta)= instead. | ||||
3740 | |||||
3741 | =cut | ||||
3742 | |||||
3743 | sub setEmbeddedStoreForm { | ||||
3744 | my ( $this, $text ) = @_; | ||||
3745 | |||||
3746 | _assertIsTopic($this) if DEBUG; | ||||
3747 | Foswiki::Serialise::deserialise( $text, 'Embedded', $this ); | ||||
3748 | } | ||||
3749 | |||||
3750 | =begin TML | ||||
3751 | |||||
3752 | ---++ StaticMethod isValidEmbedding($macro, \%args) -> $boolean | ||||
3753 | |||||
3754 | Test that the arguments defined in =\%args= are sufficient to satisfy the | ||||
3755 | requirements of the embeddable meta-data given by =$macro=. For example, | ||||
3756 | =isValidEmbedding('FILEATTACHMENT', $args)= will only succeed if $args contains | ||||
3757 | at least =name=, =date=, =user= and =attr= fields. Note that extra fields are | ||||
3758 | simply ignored (unless they are explicitly excluded). | ||||
3759 | |||||
3760 | If the macro is not registered for validation, then it will be ignored. | ||||
3761 | |||||
3762 | If the embedding is not valid, then $Foswiki::Meta::reason is set with a | ||||
3763 | message explaining why. | ||||
3764 | |||||
3765 | =cut | ||||
3766 | |||||
3767 | # spent 6.20s within Foswiki::Meta::isValidEmbedding which was called 927699 times, avg 7µs/call:
# 927699 times (6.20s+0s) by Foswiki::Serialise::Embedded::_readMETA at line 187 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 7µs/call | ||||
3768 | 927699 | 369ms | my ( $macro, $args ) = @_; | ||
3769 | |||||
3770 | 927699 | 465ms | my $validate = $VALIDATE{$macro}; | ||
3771 | 927699 | 148ms | return 1 unless $validate; # not validated | ||
3772 | |||||
3773 | 927695 | 235ms | if ( defined $validate->{function} ) { | ||
3774 | unless ( &{ $validate->{function} }( $macro, $args ) ) { | ||||
3775 | $reason = "\%META:$macro validation failed"; | ||||
3776 | return 0; | ||||
3777 | } | ||||
3778 | |||||
3779 | # Fall through to check other constraints | ||||
3780 | } | ||||
3781 | |||||
3782 | 927695 | 85.2ms | my %allowed; | ||
3783 | 927695 | 467ms | if ( defined $validate->{require} ) { | ||
3784 | 848767 | 965ms | map { $allowed{$_} = 1 } @{ $validate->{require} }; | ||
3785 | 848767 | 582ms | foreach my $p ( @{ $validate->{require} } ) { | ||
3786 | 1664776 | 982ms | if ( !defined $args->{$p} ) { | ||
3787 | $reason = "$p was missing from \%META:$macro"; | ||||
3788 | return 0; | ||||
3789 | } | ||||
3790 | } | ||||
3791 | } | ||||
3792 | |||||
3793 | 927695 | 215ms | if ( defined $validate->{allow} ) { | ||
3794 | 78928 | 176ms | map { $allowed{$_} = 1 } @{ $validate->{allow} }; | ||
3795 | 78928 | 152ms | foreach my $arg ( keys %$args ) { | ||
3796 | 206182 | 130ms | if ( !$allowed{$arg} ) { | ||
3797 | 240 | 416µs | $reason = "$arg was present in \%META:$macro"; | ||
3798 | 240 | 712µs | return 0; | ||
3799 | } | ||||
3800 | } | ||||
3801 | } | ||||
3802 | |||||
3803 | 927455 | 3.79s | return 1; | ||
3804 | } | ||||
3805 | |||||
3806 | =begin TML | ||||
3807 | |||||
3808 | ---++ StaticMethod dataDecode( $encoded ) -> $decoded | ||||
3809 | |||||
3810 | Decode escapes in a string that was encoded using dataEncode | ||||
3811 | |||||
3812 | The encoding has to be exported because Foswiki (and plugins) use | ||||
3813 | encoded field data in other places e.g. RDiff, mainly as a shorthand | ||||
3814 | for the properly parsed meta object. Some day we may be able to | ||||
3815 | eliminate that.... | ||||
3816 | |||||
3817 | =cut | ||||
3818 | |||||
3819 | # spent 7.39s within Foswiki::Meta::dataDecode which was called 3323098 times, avg 2µs/call:
# 3323098 times (7.39s+0s) by Foswiki::Serialise::Embedded::_readKeyValues at line 178 of /var/www/foswikidev/core/lib/Foswiki/Serialise/Embedded.pm, avg 2µs/call | ||||
3820 | 3323098 | 1.49s | my $datum = shift; | ||
3821 | |||||
3822 | 3323098 | 2.57s | $datum =~ s/%([\da-f]{2})/chr(hex($1))/gei; | ||
3823 | 3323098 | 12.8s | return $datum; | ||
3824 | } | ||||
3825 | |||||
3826 | =begin TML | ||||
3827 | |||||
3828 | ---++ ClassMethod type() => $resourcetype | ||||
3829 | |||||
3830 | (see Foswiki::Address::type) | ||||
3831 | |||||
3832 | Returns the resource type name. | ||||
3833 | * webpath, Eg. =Web/SubWeb/= | ||||
3834 | * topic, Eg. =Web/SubWeb. | ||||
3835 | * undef, I have no idea whats going on, we're not there yet | ||||
3836 | |||||
3837 | =cut | ||||
3838 | |||||
3839 | sub type { | ||||
3840 | my ($this) = @_; | ||||
3841 | |||||
3842 | if ( defined( $this->{_web} ) ) { | ||||
3843 | if ( defined( $this->{_topic} ) ) { | ||||
3844 | return 'topic'; | ||||
3845 | } | ||||
3846 | return 'webpath'; | ||||
3847 | } | ||||
3848 | return; | ||||
3849 | } | ||||
3850 | |||||
3851 | 1 | 16µs | 1; | ||
3852 | __END__ |