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