1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
use SMW\DataTypeRegistry; |
4
|
|
|
use SMW\DIWikiPage; |
5
|
|
|
use SMW\Query\Language\ClassDescription; |
6
|
|
|
use SMW\Query\Language\ConceptDescription; |
7
|
|
|
use SMW\Query\Language\Description; |
8
|
|
|
use SMW\Query\Language\NamespaceDescription; |
9
|
|
|
use SMW\Query\Language\SomeProperty; |
10
|
|
|
use SMW\Query\Language\ThingDescription; |
11
|
|
|
use SMW\Query\Parser\DescriptionProcessor; |
12
|
|
|
use SMW\Query\QueryToken; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Objects of this class are in charge of parsing a query string in order |
16
|
|
|
* to create an SMWDescription. The class and methods are not static in order |
17
|
|
|
* to more cleanly store the intermediate state and progress of the parser. |
18
|
|
|
* @ingroup SMWQuery |
19
|
|
|
* @author Markus Krötzsch |
20
|
|
|
*/ |
21
|
|
|
class SMWQueryParser { |
|
|
|
|
22
|
|
|
|
23
|
|
|
private $separatorStack; // list of open blocks ("parentheses") that need closing at current step |
24
|
|
|
private $currentString; // remaining string to be parsed (parsing eats query string from the front) |
25
|
|
|
|
26
|
|
|
private $defaultNamespace; // description of the default namespace restriction, or NULL if not used |
27
|
|
|
|
28
|
|
|
private $categoryPrefix; // cache label of category namespace . ':' |
29
|
|
|
private $conceptPrefix; // cache label of concept namespace . ':' |
30
|
|
|
private $categoryPrefixCannonical; // cache canonnical label of category namespace . ':' |
31
|
|
|
private $conceptPrefixCannonical; // cache canonnical label of concept namespace . ':' |
32
|
|
|
private $queryFeatures; // query features to be supported, format similar to $smwgQFeatures |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var DescriptionProcessor |
36
|
|
|
*/ |
37
|
|
|
private $descriptionProcessor; |
38
|
214 |
|
|
39
|
214 |
|
/** |
40
|
|
|
* @var QueryToken |
41
|
214 |
|
*/ |
42
|
214 |
|
private $queryToken; |
43
|
214 |
|
|
44
|
214 |
|
public function __construct( $queryFeatures = false ) { |
45
|
|
|
global $wgContLang, $smwgQFeatures; |
|
|
|
|
46
|
214 |
|
|
47
|
214 |
|
$this->categoryPrefix = $wgContLang->getNsText( NS_CATEGORY ) . ':'; |
48
|
214 |
|
$this->conceptPrefix = $wgContLang->getNsText( SMW_NS_CONCEPT ) . ':'; |
49
|
214 |
|
$this->categoryPrefixCannonical = 'Category:'; |
50
|
|
|
$this->conceptPrefixCannonical = 'Concept:'; |
51
|
|
|
|
52
|
|
|
$this->defaultNamespace = null; |
53
|
|
|
$this->queryFeatures = $queryFeatures === false ? $smwgQFeatures : $queryFeatures; |
54
|
|
|
$this->descriptionProcessor = new DescriptionProcessor( $this->queryFeatures ); |
55
|
|
|
$this->queryToken = new QueryToken(); |
56
|
108 |
|
} |
57
|
108 |
|
|
58
|
108 |
|
/** |
59
|
|
|
* @since 2.5 |
60
|
|
|
* |
61
|
|
|
* @return QueryToken |
62
|
|
|
*/ |
63
|
|
|
public function getQueryToken() { |
64
|
109 |
|
return $this->queryToken; |
65
|
109 |
|
} |
66
|
|
|
|
67
|
109 |
|
/** |
68
|
1 |
|
* @since 2.4 |
69
|
1 |
|
* |
70
|
1 |
|
* @param DIWikiPage|null $contextPage |
71
|
1 |
|
*/ |
72
|
|
|
public function setContextPage( DIWikiPage $contextPage = null ) { |
73
|
|
|
$this->descriptionProcessor->setContextPage( $contextPage ); |
74
|
|
|
} |
75
|
109 |
|
|
76
|
|
|
/** |
77
|
|
|
* Provide an array of namespace constants that are used as default restrictions. |
78
|
|
|
* If NULL is given, no such default restrictions will be added (faster). |
79
|
|
|
*/ |
80
|
|
|
public function setDefaultNamespaces( $namespaceArray ) { |
81
|
|
|
$this->defaultNamespace = null; |
82
|
|
|
|
83
|
|
|
if ( !is_null( $namespaceArray ) ) { |
84
|
|
|
foreach ( $namespaceArray as $ns ) { |
85
|
|
|
$this->defaultNamespace = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( |
86
|
170 |
|
$this->defaultNamespace, |
87
|
|
|
new NamespaceDescription( $ns ) |
88
|
170 |
|
); |
89
|
170 |
|
} |
90
|
170 |
|
} |
91
|
170 |
|
} |
92
|
170 |
|
|
93
|
|
|
/** |
94
|
170 |
|
* Compute an SMWDescription from a query string. Returns whatever descriptions could be |
95
|
169 |
|
* wrestled from the given string (the most general result being SMWThingDescription if |
96
|
|
|
* no meaningful condition was extracted). |
97
|
|
|
* |
98
|
170 |
|
* @param string $queryString |
99
|
1 |
|
* |
100
|
|
|
* @return Description |
101
|
|
|
*/ |
102
|
|
|
public function getQueryDescription( $queryString ) { |
103
|
170 |
|
|
104
|
|
|
$this->descriptionProcessor->clear(); |
105
|
|
|
$this->currentString = $queryString; |
106
|
|
|
$this->separatorStack = array(); |
107
|
|
|
$setNS = false; |
108
|
|
|
$result = $this->getSubqueryDescription( $setNS ); |
109
|
|
|
|
110
|
|
|
if ( !$setNS ) { // add default namespaces if applicable |
111
|
109 |
|
$result = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $this->defaultNamespace, $result ); |
112
|
109 |
|
} |
113
|
|
|
|
114
|
|
|
if ( is_null( $result ) ) { // parsing went wrong, no default namespaces |
115
|
|
|
$result = new ThingDescription(); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
|
119
|
|
|
return $result; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Return array of error messages (possibly empty). |
124
|
|
|
* |
125
|
|
|
* @return array |
126
|
|
|
*/ |
127
|
|
|
public function getErrors() { |
128
|
|
|
return $this->descriptionProcessor->getErrors(); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Return error message or empty string if no error occurred. |
133
|
|
|
* |
134
|
|
|
* @return string |
135
|
|
|
*/ |
136
|
|
|
public function getErrorString() { |
137
|
|
|
return smwfEncodeMessages( $this->getErrors() ); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Compute an SMWDescription for current part of a query, which should |
142
|
|
|
* be a standalone query (the main query or a subquery enclosed within |
143
|
|
|
* "\<q\>...\</q\>". Recursively calls similar methods and returns NULL upon error. |
144
|
|
|
* |
145
|
|
|
* The call-by-ref parameter $setNS is a boolean. Its input specifies whether |
146
|
|
|
* the query should set the current default namespace if no namespace restrictions |
147
|
170 |
|
* were given. If false, the calling super-query is happy to set the required |
148
|
170 |
|
* NS-restrictions by itself if needed. Otherwise the subquery has to impose the defaults. |
149
|
170 |
|
* This is so, since outermost queries and subqueries of disjunctions will have to set |
150
|
170 |
|
* their own default restrictions. |
151
|
170 |
|
* |
152
|
|
|
* The return value of $setNS specifies whether or not the subquery has a namespace |
153
|
170 |
|
* specification in place. This might happen automatically if the query string imposes |
154
|
|
|
* such restrictions. The return value is important for those callers that otherwise |
155
|
170 |
|
* set up their own restrictions. |
156
|
170 |
|
* |
157
|
|
|
* Note that $setNS is no means to switch on or off default namespaces in general, |
158
|
|
|
* but just controls query generation. For general effect, the default namespaces |
159
|
170 |
|
* should be set to NULL. |
160
|
170 |
|
* |
161
|
|
|
* @return Description|null |
162
|
170 |
|
*/ |
163
|
170 |
|
private function getSubqueryDescription( &$setNS ) { |
164
|
|
|
$conjunction = null; // used for the current inner conjunction |
165
|
170 |
|
$disjuncts = array(); // (disjunctive) array of subquery conjunctions |
166
|
170 |
|
$hasNamespaces = false; // does the current $conjnuction have its own namespace restrictions? |
167
|
170 |
|
$mustSetNS = $setNS; // must NS restrictions be set? (may become true even if $setNS is false) |
168
|
14 |
|
|
169
|
14 |
|
$continue = ( $chunk = $this->readChunk() ) !== ''; // skip empty subquery completely, thorwing an error |
170
|
14 |
|
|
171
|
170 |
|
while ( $continue ) { |
172
|
170 |
|
$setsubNS = false; |
173
|
170 |
|
|
174
|
26 |
|
switch ( $chunk ) { |
175
|
170 |
|
case '[[': // start new link block |
176
|
1 |
|
$ld = $this->getLinkDescription( $setsubNS ); |
177
|
|
|
|
178
|
1 |
|
if ( !is_null( $ld ) ) { |
179
|
1 |
|
$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $ld ); |
180
|
|
|
} |
181
|
1 |
|
break; |
182
|
|
|
case 'AND': |
183
|
|
|
case '<q>': // enter new subquery, currently irrelevant but possible |
184
|
|
|
$this->pushDelimiter( '</q>' ); |
185
|
1 |
|
$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $this->getSubqueryDescription( $setsubNS ) ); |
186
|
1 |
|
break; |
187
|
|
|
case 'OR': |
188
|
|
|
case '||': |
189
|
|
|
case '': |
190
|
|
|
case '</q>': // finish disjunction and maybe subquery |
191
|
|
|
if ( !is_null( $this->defaultNamespace ) ) { // possibly add namespace restrictions |
192
|
170 |
|
if ( $hasNamespaces && !$mustSetNS ) { |
193
|
|
|
// add NS restrictions to all earlier conjunctions (all of which did not have them yet) |
194
|
170 |
|
$mustSetNS = true; // enforce NS restrictions from now on |
195
|
170 |
|
$newdisjuncts = array(); |
196
|
|
|
|
197
|
|
|
foreach ( $disjuncts as $conj ) { |
198
|
170 |
|
$newdisjuncts[] = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conj, $this->defaultNamespace ); |
199
|
24 |
|
} |
200
|
24 |
|
|
201
|
|
|
$disjuncts = $newdisjuncts; |
202
|
|
|
} elseif ( !$hasNamespaces && $mustSetNS ) { |
203
|
24 |
|
// add ns restriction to current result |
204
|
|
|
$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $this->defaultNamespace ); |
205
|
170 |
|
} |
206
|
170 |
|
} |
207
|
|
|
|
208
|
170 |
|
$disjuncts[] = $conjunction; |
209
|
2 |
|
// start anew |
210
|
|
|
$conjunction = null; |
211
|
|
|
$hasNamespaces = false; |
212
|
2 |
|
|
213
|
|
|
// finish subquery? |
214
|
|
|
if ( $chunk == '</q>' ) { |
215
|
|
|
if ( $this->popDelimiter( '</q>' ) ) { |
216
|
170 |
|
$continue = false; // leave the loop |
217
|
48 |
|
} else { |
218
|
|
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_toomanyclosing', $chunk ); |
219
|
|
|
return null; |
220
|
170 |
|
} |
221
|
170 |
|
} elseif ( $chunk === '' ) { |
222
|
|
|
$continue = false; |
223
|
|
|
} |
224
|
|
|
break; |
225
|
170 |
|
case '+': // "... AND true" (ignore) |
|
|
|
|
226
|
170 |
|
break; |
227
|
|
|
default: // error: unexpected $chunk |
228
|
170 |
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk ); |
229
|
170 |
|
// return null; // Try to go on, it can only get better ... |
|
|
|
|
230
|
1 |
|
} |
231
|
1 |
|
|
232
|
1 |
|
if ( $setsubNS ) { // namespace restrictions encountered in current conjunct |
233
|
|
|
$hasNamespaces = true; |
234
|
170 |
|
} |
235
|
|
|
|
236
|
|
|
if ( $continue ) { // read on only if $continue remained true |
237
|
|
|
$chunk = $this->readChunk(); |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if ( count( $disjuncts ) > 0 ) { // make disjunctive result |
242
|
|
|
$result = null; |
243
|
170 |
|
|
244
|
|
|
foreach ( $disjuncts as $d ) { |
245
|
170 |
|
if ( is_null( $d ) ) { |
246
|
|
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' ); |
247
|
|
|
$setNS = false; |
248
|
|
|
return null; |
249
|
|
|
} else { |
250
|
|
|
$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $d ); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
} else { |
254
|
170 |
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' ); |
255
|
|
|
$setNS = false; |
256
|
|
|
return null; |
257
|
170 |
|
} |
258
|
|
|
|
259
|
170 |
|
$setNS = $mustSetNS; // NOTE: also false if namespaces were given but no default NS descs are available |
260
|
170 |
|
|
261
|
66 |
|
return $result; |
262
|
66 |
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
155 |
|
* Compute an SMWDescription for current part of a query, which should |
266
|
|
|
* be the content of "[[ ... ]]". Returns NULL upon error. |
267
|
155 |
|
* |
268
|
125 |
|
* Parameters $setNS has the same use as in getSubqueryDescription(). |
269
|
125 |
|
*/ |
270
|
|
|
private function getLinkDescription( &$setNS ) { |
271
|
|
|
// This method is called when we encountered an opening '[['. The following |
272
|
|
|
// block could be a Category-statement, fixed object, or property statement. |
273
|
|
|
$chunk = $this->readChunk( '', true, false ); // NOTE: untrimmed, initial " " escapes prop. chains |
274
|
|
|
|
275
|
48 |
|
if ( in_array( smwfNormalTitleText( $chunk ), |
276
|
|
|
array( $this->categoryPrefix, $this->conceptPrefix, $this->categoryPrefixCannonical, $this->conceptPrefixCannonical ) ) ) { |
277
|
|
|
return $this->getClassDescription( $setNS, ( |
278
|
|
|
smwfNormalTitleText( $chunk ) == $this->categoryPrefix || smwfNormalTitleText( $chunk ) == $this->categoryPrefixCannonical |
279
|
|
|
) ); |
280
|
|
|
} else { // fixed subject, namespace restriction, property query, or subquery |
281
|
|
|
$sep = $this->readChunk( '', false ); // do not consume hit, "look ahead" |
282
|
|
|
|
283
|
|
|
if ( ( $sep == '::' ) || ( $sep == ':=' ) ) { |
284
|
|
|
if ( $chunk{0} != ':' ) { // property statement |
285
|
66 |
|
return $this->getPropertyDescription( $chunk, $setNS ); |
286
|
|
|
} else { // escaped article description, read part after :: to get full contents |
287
|
66 |
|
$chunk .= $this->readChunk( '\[\[|\]\]|\|\||\|' ); |
288
|
66 |
|
return $this->getArticleDescription( trim( $chunk ), $setNS ); |
289
|
|
|
} |
290
|
66 |
|
} else { // Fixed article/namespace restriction. $sep should be ]] or || |
291
|
66 |
|
return $this->getArticleDescription( trim( $chunk ), $setNS ); |
292
|
|
|
} |
293
|
66 |
|
} |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Parse a category description (the part of an inline query that |
298
|
66 |
|
* is in between "[[Category:" and the closing "]]" and create a |
299
|
|
|
* suitable description. |
300
|
66 |
|
*/ |
301
|
66 |
|
private function getClassDescription( &$setNS, $category = true ) { |
302
|
66 |
|
// note: no subqueries allowed here, inline disjunction allowed, wildcards allowed |
303
|
66 |
|
$result = null; |
304
|
|
|
$continue = true; |
305
|
|
|
|
306
|
|
|
while ( $continue ) { |
307
|
66 |
|
$chunk = $this->readChunk(); |
308
|
66 |
|
|
309
|
|
|
if ( $chunk == '+' ) { |
310
|
|
|
$description = new NamespaceDescription( $category ? NS_CATEGORY : SMW_NS_CONCEPT ); |
311
|
66 |
|
$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $description ); |
312
|
|
|
} else { // assume category/concept title |
313
|
|
|
/// NOTE: we add m_c...prefix to prevent problems with, e.g., [[Category:Template:Test]] |
314
|
|
|
$title = Title::newFromText( ( $category ? $this->categoryPrefix : $this->conceptPrefix ) . $chunk ); |
315
|
|
|
|
316
|
|
|
if ( !is_null( $title ) ) { |
317
|
|
|
$diWikiPage = new SMWDIWikiPage( $title->getDBkey(), $title->getNameSpace(), '' ); |
318
|
|
|
$desc = $category ? new ClassDescription( $diWikiPage ) : new ConceptDescription( $diWikiPage ); |
319
|
|
|
$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $desc ); |
320
|
125 |
|
} |
321
|
125 |
|
} |
322
|
|
|
|
323
|
|
|
$chunk = $this->readChunk(); |
324
|
125 |
|
$continue = ( $chunk == '||' ) && $category; // disjunctions only for cateories |
325
|
125 |
|
} |
326
|
125 |
|
|
327
|
125 |
|
return $this->finishLinkDescription( $chunk, false, $result, $setNS ); |
|
|
|
|
328
|
|
|
} |
329
|
125 |
|
|
330
|
125 |
|
/** |
331
|
|
|
* Parse a property description (the part of an inline query that |
332
|
|
|
* is in between "[[Some property::" and the closing "]]" and create a |
333
|
|
|
* suitable description. The "::" is the first chunk on the current |
334
|
|
|
* string. |
335
|
125 |
|
*/ |
336
|
|
|
private function getPropertyDescription( $propertyName, &$setNS ) { |
337
|
125 |
|
$this->readChunk(); // consume separator ":=" or "::" |
338
|
|
|
|
339
|
|
|
// first process property chain syntax (e.g. "property1.property2::value"), escaped by initial " ": |
340
|
|
|
$propertynames = ( $propertyName{0} == ' ' ) ? array( $propertyName ) : explode( '.', $propertyName ); |
341
|
|
|
$properties = array(); |
342
|
125 |
|
$typeid = '_wpg'; |
343
|
125 |
|
$inverse = false; |
344
|
125 |
|
|
345
|
|
|
foreach ( $propertynames as $name ) { |
346
|
|
|
if ( !$this->isPagePropertyType( $typeid ) ) { // non-final property in chain was no wikipage: not allowed |
347
|
125 |
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', $name ); |
348
|
125 |
|
return null; ///TODO: read some more chunks and try to finish [[ ]] |
349
|
|
|
} |
350
|
125 |
|
|
351
|
125 |
|
$property = SMWPropertyValue::makeUserProperty( $name ); |
352
|
|
|
|
353
|
|
|
if ( !$property->isValid() ) { // illegal property identifier |
354
|
125 |
|
$this->descriptionProcessor->addError( $property->getErrors() ); |
355
|
42 |
|
return null; ///TODO: read some more chunks and try to finish [[ ]] |
356
|
|
|
} |
357
|
|
|
|
358
|
42 |
|
$typeid = $property->getDataItem()->findPropertyTypeID(); |
|
|
|
|
359
|
|
|
$inverse = $property->isInverse(); |
|
|
|
|
360
|
42 |
|
$properties[] = $property; |
361
|
42 |
|
} ///NOTE: after iteration, $property and $typeid correspond to last value |
362
|
98 |
|
|
363
|
12 |
|
$innerdesc = null; |
364
|
12 |
|
$continue = true; |
365
|
12 |
|
|
366
|
12 |
|
while ( $continue ) { |
367
|
|
|
$chunk = $this->readChunk(); |
368
|
|
|
|
369
|
|
|
switch ( $chunk ) { |
370
|
|
|
case '+': // wildcard, add namespaces for page-type properties |
371
|
12 |
|
if ( !is_null( $this->defaultNamespace ) && ( $this->isPagePropertyType( $typeid ) || $inverse ) ) { |
372
|
12 |
|
$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->defaultNamespace ); |
373
|
|
|
} else { |
374
|
|
|
$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() ); |
375
|
98 |
|
} |
376
|
98 |
|
$chunk = $this->readChunk(); |
377
|
98 |
|
break; |
378
|
|
|
case '<q>': // subquery, set default namespaces |
379
|
98 |
|
if ( $this->isPagePropertyType( $typeid ) || $inverse ) { |
380
|
98 |
|
$this->pushDelimiter( '</q>' ); |
381
|
|
|
$setsubNS = true; |
382
|
98 |
|
$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->getSubqueryDescription( $setsubNS ) ); |
383
|
|
|
} else { // no subqueries allowed for non-pages |
384
|
|
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', end( $propertynames ) ); |
385
|
98 |
|
$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() ); |
386
|
98 |
|
} |
387
|
98 |
|
$chunk = $this->readChunk(); |
388
|
11 |
|
break; |
389
|
10 |
|
default: // normal object value |
390
|
10 |
|
// read value(s), possibly with inner [[...]] |
|
|
|
|
391
|
10 |
|
$open = 1; |
392
|
|
|
$value = $chunk; |
393
|
10 |
|
$continue2 = true; |
394
|
1 |
|
// read value with inner [[, ]], || |
|
|
|
|
395
|
|
|
while ( ( $open > 0 ) && ( $continue2 ) ) { |
396
|
|
|
$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' ); |
397
|
|
|
switch ( $chunk ) { |
398
|
98 |
|
case '[[': // open new [[ ]] |
|
|
|
|
399
|
1 |
|
$open++; |
400
|
|
|
break; |
401
|
|
|
case ']]': // close [[ ]] |
|
|
|
|
402
|
|
|
$open--; |
403
|
98 |
|
break; |
404
|
|
|
case '|': |
405
|
98 |
|
case '||': // terminates only outermost [[ ]] |
406
|
|
|
if ( $open == 1 ) { |
407
|
|
|
$open = 0; |
408
|
|
|
} |
409
|
125 |
|
break; |
410
|
|
|
case '': ///TODO: report error; this is not good right now |
411
|
|
|
$continue2 = false; |
412
|
125 |
|
break; |
413
|
|
|
} |
414
|
|
|
if ( $open != 0 ) { |
415
|
|
|
$value .= $chunk; |
416
|
|
|
} |
417
|
|
|
} ///NOTE: at this point, we normally already read one more chunk behind the value |
418
|
|
|
$outerDesription = $this->descriptionProcessor->constructDescriptionForPropertyObjectValue( |
419
|
125 |
|
$property->getDataItem(), |
|
|
|
|
420
|
|
|
$value |
421
|
125 |
|
); |
422
|
125 |
|
|
423
|
|
|
$this->queryToken->addFromDesciption( $outerDesription ); |
424
|
|
|
$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( |
425
|
125 |
|
$innerdesc, |
426
|
|
|
$outerDesription |
427
|
125 |
|
); |
428
|
|
|
|
429
|
|
|
} |
430
|
|
|
$continue = ( $chunk == '||' ); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
if ( is_null( $innerdesc ) ) { // make a wildcard search |
434
|
|
|
$innerdesc = ( !is_null( $this->defaultNamespace ) && $this->isPagePropertyType( $typeid ) ) ? |
435
|
|
|
$this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->defaultNamespace ) : |
436
|
|
|
$this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() ); |
437
|
48 |
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_propvalueproblem', $property->getWikiValue() ); |
438
|
48 |
|
} |
439
|
48 |
|
|
440
|
48 |
|
$properties = array_reverse( $properties ); |
441
|
|
|
|
442
|
48 |
|
foreach ( $properties as $property ) { |
443
|
48 |
|
$innerdesc = new SomeProperty( $property->getDataItem(), $innerdesc ); |
|
|
|
|
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
$result = $innerdesc; |
447
|
|
|
|
448
|
|
|
return $this->finishLinkDescription( $chunk, false, $result, $setNS ); |
|
|
|
|
449
|
48 |
|
} |
450
|
|
|
|
451
|
48 |
|
/** |
452
|
2 |
|
* Parse an article description (the part of an inline query that |
453
|
|
|
* is in between "[[" and the closing "]]" assuming it is not specifying |
454
|
48 |
|
* a category or property) and create a suitable description. |
455
|
|
|
* The first chunk behind the "[[" has already been read and is |
456
|
4 |
|
* passed as a parameter. |
457
|
|
|
*/ |
458
|
4 |
|
private function getArticleDescription( $firstChunk, &$setNS ) { |
459
|
4 |
|
$chunk = $firstChunk; |
460
|
|
|
$result = null; |
461
|
|
|
$continue = true; |
462
|
45 |
|
|
463
|
|
|
while ( $continue ) { |
464
|
45 |
|
if ( $chunk == '<q>' ) { // no subqueries of the form [[<q>...</q>]] (not needed) |
465
|
|
|
|
466
|
|
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsubquery' ); |
467
|
|
|
return null; |
468
|
48 |
|
} |
469
|
|
|
|
470
|
48 |
|
$list = preg_split( '/:/', $chunk, 3 ); // ":Category:Foo" "User:bar" ":baz" ":+" |
|
|
|
|
471
|
|
|
|
472
|
|
|
if ( ( $list[0] === '' ) && ( count( $list ) == 3 ) ) { |
473
|
|
|
$list = array_slice( $list, 1 ); |
474
|
48 |
|
} |
475
|
|
|
if ( ( count( $list ) == 2 ) && ( $list[1] == '+' ) ) { // try namespace restriction |
476
|
|
|
|
477
|
|
|
$idx = \SMW\Localizer::getInstance()->getNamespaceIndexByName( $list[0] ); |
478
|
48 |
|
|
479
|
|
|
if ( $idx !== false ) { |
480
|
|
|
$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, new NamespaceDescription( $idx ) ); |
|
|
|
|
481
|
170 |
|
} |
482
|
170 |
|
} else { |
483
|
|
|
$outerDesription = $this->descriptionProcessor->constructDescriptionForWikiPageValueChunk( |
484
|
170 |
|
$chunk |
485
|
|
|
); |
486
|
|
|
|
487
|
|
|
$this->queryToken->addFromDesciption( $outerDesription ); |
488
|
|
|
|
489
|
170 |
|
$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( |
490
|
|
|
$result, |
491
|
170 |
|
$outerDesription |
492
|
|
|
); |
493
|
1 |
|
} |
494
|
1 |
|
|
495
|
1 |
|
$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' ); |
496
|
1 |
|
|
497
|
1 |
|
if ( $chunk == '||' ) { |
498
|
|
|
$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' ); |
499
|
1 |
|
$continue = true; |
500
|
|
|
} else { |
501
|
|
|
$continue = false; |
502
|
170 |
|
} |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
return $this->finishLinkDescription( $chunk, true, $result, $setNS ); |
506
|
2 |
|
} |
507
|
1 |
|
|
508
|
|
|
private function finishLinkDescription( $chunk, $hasNamespaces, $result, &$setNS ) { |
509
|
|
|
if ( is_null( $result ) ) { // no useful information or concrete error found |
510
|
1 |
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk ); // was smw_badqueryatom |
511
|
|
|
} elseif ( !$hasNamespaces && $setNS && !is_null( $this->defaultNamespace ) ) { |
512
|
1 |
|
$result = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $result, $this->defaultNamespace ); |
513
|
1 |
|
$hasNamespaces = true; |
514
|
|
|
} |
515
|
|
|
|
516
|
2 |
|
$setNS = $hasNamespaces; |
517
|
1 |
|
|
518
|
|
|
if ( $chunk == '|' ) { // skip content after single |, but report a warning |
519
|
|
|
// Note: Using "|label" in query atoms used to be a way to set the mainlabel in SMW <1.0; no longer supported now |
520
|
|
|
$chunk = $this->readChunk( '\]\]' ); |
521
|
170 |
|
$labelpart = '|'; |
522
|
|
|
if ( $chunk != ']]' ) { |
523
|
|
|
$labelpart .= $chunk; |
524
|
|
|
$chunk = $this->readChunk( '\]\]' ); |
525
|
|
|
} |
526
|
|
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $labelpart ); |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
if ( $chunk != ']]' ) { |
530
|
|
|
// What happended? We found some chunk that could not be processed as |
531
|
|
|
// link content (as in [[Category:Test<q>]]), or the closing ]] are |
532
|
|
|
// just missing entirely. |
533
|
|
|
if ( $chunk !== '' ) { |
534
|
|
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsymbol', $chunk ); |
535
|
|
|
|
536
|
|
|
// try to find a later closing ]] to finish this misshaped subpart |
537
|
|
|
$chunk = $this->readChunk( '\]\]' ); |
538
|
|
|
|
539
|
|
|
if ( $chunk != ']]' ) { |
540
|
|
|
$chunk = $this->readChunk( '\]\]' ); |
541
|
170 |
|
} |
542
|
170 |
|
} |
543
|
|
|
if ( $chunk === '' ) { |
544
|
170 |
|
$this->descriptionProcessor->addErrorWithMsgKey( 'smw_noclosingbrackets' ); |
545
|
170 |
|
} |
546
|
170 |
|
} |
547
|
|
|
|
548
|
170 |
|
return $result; |
549
|
170 |
|
} |
550
|
170 |
|
|
551
|
170 |
|
/** |
552
|
|
|
* Get the next unstructured string chunk from the query string. |
553
|
|
|
* Chunks are delimited by any of the special strings used in inline queries |
554
|
170 |
|
* (such as [[, ]], <q>, ...). If the string starts with such a delimiter, |
555
|
170 |
|
* this delimiter is returned. Otherwise the first string in front of such a |
556
|
170 |
|
* delimiter is returned. |
557
|
170 |
|
* Trailing and initial spaces are ignored if $trim is true, and chunks |
558
|
170 |
|
* consisting only of spaces are not returned. |
559
|
|
|
* If there is no more qurey string left to process, the empty string is |
560
|
|
|
* returned (and in no other case). |
561
|
170 |
|
* |
562
|
|
|
* The stoppattern can be used to customise the matching, especially in order to |
563
|
169 |
|
* overread certain special symbols. |
564
|
169 |
|
* |
565
|
|
|
* $consume specifies whether the returned chunk should be removed from the |
566
|
|
|
* query string. |
567
|
169 |
|
*/ |
568
|
|
|
private function readChunk( $stoppattern = '', $consume = true, $trim = true ) { |
569
|
|
|
if ( $stoppattern === '' ) { |
570
|
|
|
$stoppattern = '\[\[|\]\]|::|:=|<q>|<\/q>' . |
571
|
|
|
'|^' . $this->categoryPrefix . '|^' . $this->categoryPrefixCannonical . |
572
|
|
|
'|^' . $this->conceptPrefix . '|^' . $this->conceptPrefixCannonical . |
573
|
|
|
'|\|\||\|'; |
574
|
|
|
} |
575
|
|
|
$chunks = preg_split( '/[\s]*(' . $stoppattern . ')/iu', $this->currentString, 2, PREG_SPLIT_DELIM_CAPTURE ); |
576
|
|
|
if ( count( $chunks ) == 1 ) { // no matches anymore, strip spaces and finish |
577
|
|
|
if ( $consume ) { |
578
|
24 |
|
$this->currentString = ''; |
579
|
24 |
|
} |
580
|
24 |
|
|
581
|
|
|
return $trim ? trim( $chunks[0] ) : $chunks[0]; |
582
|
|
|
} elseif ( count( $chunks ) == 3 ) { // this should generally happen if count is not 1 |
583
|
|
|
if ( $chunks[0] === '' ) { // string started with delimiter |
584
|
|
|
if ( $consume ) { |
585
|
|
|
$this->currentString = $chunks[2]; |
586
|
|
|
} |
587
|
24 |
|
|
588
|
24 |
|
return $trim ? trim( $chunks[1] ) : $chunks[1]; |
589
|
24 |
|
} else { |
590
|
|
|
if ( $consume ) { |
591
|
|
|
$this->currentString = $chunks[1] . $chunks[2]; |
592
|
125 |
|
} |
593
|
125 |
|
|
594
|
|
|
return $trim ? trim( $chunks[0] ) : $chunks[0]; |
595
|
|
|
} |
596
|
|
|
} else { |
597
|
|
|
return false; |
598
|
|
|
} // should never happen |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
/** |
602
|
|
|
* Enter a new subblock in the query, which must at some time be terminated by the |
603
|
|
|
* given $endstring delimiter calling popDelimiter(); |
604
|
|
|
*/ |
605
|
|
|
private function pushDelimiter( $endstring ) { |
606
|
|
|
array_push( $this->separatorStack, $endstring ); |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Exit a subblock in the query ending with the given delimiter. |
611
|
|
|
* If the delimiter does not match the top-most open block, false |
612
|
|
|
* will be returned. Otherwise return true. |
613
|
|
|
*/ |
614
|
|
|
private function popDelimiter( $endstring ) { |
615
|
|
|
$topdelim = array_pop( $this->separatorStack ); |
616
|
|
|
return ( $topdelim == $endstring ); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
private function isPagePropertyType( $typeid ) { |
620
|
|
|
return $typeid == '_wpg' || DataTypeRegistry::getInstance()->isSubDataType( $typeid ); |
621
|
|
|
} |
622
|
|
|
|
623
|
|
|
} |
624
|
|
|
|
You can fix this by adding a namespace to your class:
When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.