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