Completed
Push — master ( 5f9e66...1f3475 )
by mw
123:57 queued 89:01
created

includes/query/SMW_QueryParser.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
13
/**
14
 * Objects of this class are in charge of parsing a query string in order
15
 * to create an SMWDescription. The class and methods are not static in order
16
 * to more cleanly store the intermediate state and progress of the parser.
17
 * @ingroup SMWQuery
18
 * @author Markus Krötzsch
19
 */
20
class SMWQueryParser {
21
22
	private $separatorStack; // list of open blocks ("parentheses") that need closing at current step
23
	private $currentString; // remaining string to be parsed (parsing eats query string from the front)
24
25
	private $defaultNamespace; // description of the default namespace restriction, or NULL if not used
26
27
	private $categoryPrefix; // cache label of category namespace . ':'
28
	private $conceptPrefix; // cache label of concept namespace . ':'
29
	private $categoryPrefixCannonical; // cache canonnical label of category namespace . ':'
30
	private $conceptPrefixCannonical; // cache canonnical label of concept namespace . ':'
31
	private $queryFeatures; // query features to be supported, format similar to $smwgQFeatures
32
33
	/**
34
	 * @var DescriptionProcessor
35
	 */
36
	private $descriptionProcessor;
37
38 214
	public function __construct( $queryFeatures = false ) {
39 214
		global $wgContLang, $smwgQFeatures;
40
41 214
		$this->categoryPrefix = $wgContLang->getNsText( NS_CATEGORY ) . ':';
42 214
		$this->conceptPrefix = $wgContLang->getNsText( SMW_NS_CONCEPT ) . ':';
43 214
		$this->categoryPrefixCannonical = 'Category:';
44 214
		$this->conceptPrefixCannonical = 'Concept:';
45
46 214
		$this->defaultNamespace = null;
47 214
		$this->queryFeatures = $queryFeatures === false ? $smwgQFeatures : $queryFeatures;
48 214
		$this->descriptionProcessor = new DescriptionProcessor( $this->queryFeatures );
49 214
	}
50
51
	/**
52
	 * @since 2.4
53
	 *
54
	 * @param DIWikiPage|null $contextPage
55
	 */
56 108
	public function setContextPage( DIWikiPage $contextPage = null ) {
57 108
		$this->descriptionProcessor->setContextPage( $contextPage );
58 108
	}
59
60
	/**
61
	 * Provide an array of namespace constants that are used as default restrictions.
62
	 * If NULL is given, no such default restrictions will be added (faster).
63
	 */
64 109
	public function setDefaultNamespaces( $namespaceArray ) {
65 109
		$this->defaultNamespace = null;
66
67 109
		if ( !is_null( $namespaceArray ) ) {
68 1
			foreach ( $namespaceArray as $ns ) {
69 1
				$this->defaultNamespace = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom(
70 1
					$this->defaultNamespace,
71 1
					new NamespaceDescription( $ns )
72
				);
73
			}
74
		}
75 109
	}
76
77
	/**
78
	 * Compute an SMWDescription from a query string. Returns whatever descriptions could be
79
	 * wrestled from the given string (the most general result being SMWThingDescription if
80
	 * no meaningful condition was extracted).
81
	 *
82
	 * @param string $queryString
83
	 *
84
	 * @return Description
85
	 */
86 170
	public function getQueryDescription( $queryString ) {
87
88 170
		$this->descriptionProcessor->clear();
89 170
		$this->currentString = $queryString;
90 170
		$this->separatorStack = array();
91 170
		$setNS = false;
92 170
		$result = $this->getSubqueryDescription( $setNS );
93
94 170
		if ( !$setNS ) { // add default namespaces if applicable
95 169
			$result = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $this->defaultNamespace, $result );
96
		}
97
98 170
		if ( is_null( $result ) ) { // parsing went wrong, no default namespaces
99 1
			$result = new ThingDescription();
100
		}
101
102
103 170
		return $result;
104
	}
105
106
	/**
107
	 * Return array of error messages (possibly empty).
108
	 *
109
	 * @return array
110
	 */
111 109
	public function getErrors() {
112 109
		return $this->descriptionProcessor->getErrors();
113
	}
114
115
	/**
116
	 * Return error message or empty string if no error occurred.
117
	 *
118
	 * @return string
119
	 */
120
	public function getErrorString() {
121
		return smwfEncodeMessages( $this->getErrors() );
122
	}
123
124
	/**
125
	 * Compute an SMWDescription for current part of a query, which should
126
	 * be a standalone query (the main query or a subquery enclosed within
127
	 * "\<q\>...\</q\>". Recursively calls similar methods and returns NULL upon error.
128
	 *
129
	 * The call-by-ref parameter $setNS is a boolean. Its input specifies whether
130
	 * the query should set the current default namespace if no namespace restrictions
131
	 * were given. If false, the calling super-query is happy to set the required
132
	 * NS-restrictions by itself if needed. Otherwise the subquery has to impose the defaults.
133
	 * This is so, since outermost queries and subqueries of disjunctions will have to set
134
	 * their own default restrictions.
135
	 *
136
	 * The return value of $setNS specifies whether or not the subquery has a namespace
137
	 * specification in place. This might happen automatically if the query string imposes
138
	 * such restrictions. The return value is important for those callers that otherwise
139
	 * set up their own restrictions.
140
	 *
141
	 * Note that $setNS is no means to switch on or off default namespaces in general,
142
	 * but just controls query generation. For general effect, the default namespaces
143
	 * should be set to NULL.
144
	 *
145
	 * @return Description|null
146
	 */
147 170
	private function getSubqueryDescription( &$setNS ) {
148 170
		$conjunction = null;      // used for the current inner conjunction
149 170
		$disjuncts = array();     // (disjunctive) array of subquery conjunctions
150 170
		$hasNamespaces = false;   // does the current $conjnuction have its own namespace restrictions?
151 170
		$mustSetNS = $setNS;      // must NS restrictions be set? (may become true even if $setNS is false)
152
153 170
		$continue = ( $chunk = $this->readChunk() ) !== ''; // skip empty subquery completely, thorwing an error
154
155 170
		while ( $continue ) {
156 170
			$setsubNS = false;
157
158
			switch ( $chunk ) {
159 170
				case '[[': // start new link block
160 170
					$ld = $this->getLinkDescription( $setsubNS );
161
162 170
					if ( !is_null( $ld ) ) {
163 170
						$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $ld );
164
					}
165 170
				break;
166 170
				case 'AND':
167 170
				case '<q>': // enter new subquery, currently irrelevant but possible
168 14
					$this->pushDelimiter( '</q>' );
169 14
					$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $this->getSubqueryDescription( $setsubNS ) );
170 14
				break;
171 170
				case 'OR':
172 170
				case '||':
173 170
				case '':
174 26
				case '</q>': // finish disjunction and maybe subquery
175 170
					if ( !is_null( $this->defaultNamespace ) ) { // possibly add namespace restrictions
176 1
						if ( $hasNamespaces && !$mustSetNS ) {
177
							// add NS restrictions to all earlier conjunctions (all of which did not have them yet)
178 1
							$mustSetNS = true; // enforce NS restrictions from now on
179 1
							$newdisjuncts = array();
180
181 1
							foreach ( $disjuncts as $conj ) {
182
								$newdisjuncts[] = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conj, $this->defaultNamespace );
183
							}
184
185 1
							$disjuncts = $newdisjuncts;
186 1
						} elseif ( !$hasNamespaces && $mustSetNS ) {
187
							// add ns restriction to current result
188
							$conjunction = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $conjunction, $this->defaultNamespace );
189
						}
190
					}
191
192 170
					$disjuncts[] = $conjunction;
193
					// start anew
194 170
					$conjunction = null;
195 170
					$hasNamespaces = false;
196
197
					// finish subquery?
198 170
					if ( $chunk == '</q>' ) {
199 24
						if ( $this->popDelimiter( '</q>' ) ) {
200 24
							$continue = false; // leave the loop
201
						} else {
202
							$this->descriptionProcessor->addErrorWithMsgKey( 'smw_toomanyclosing', $chunk );
203 24
							return null;
204
						}
205 170
					} elseif ( $chunk === '' ) {
206 170
						$continue = false;
207
					}
208 170
				break;
209 2
				case '+': // "... AND true" (ignore)
210
				break;
211
				default: // error: unexpected $chunk
212 2
					$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk );
213
					// return null; // Try to go on, it can only get better ...
214
			}
215
216 170
			if ( $setsubNS ) { // namespace restrictions encountered in current conjunct
217 48
				$hasNamespaces = true;
218
			}
219
220 170
			if ( $continue ) { // read on only if $continue remained true
221 170
				$chunk = $this->readChunk();
222
			}
223
		}
224
225 170
		if ( count( $disjuncts ) > 0 ) { // make disjunctive result
226 170
			$result = null;
227
228 170
			foreach ( $disjuncts as $d ) {
229 170
				if ( is_null( $d ) ) {
230 1
					$this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' );
231 1
					$setNS = false;
232 1
					return null;
233
				} else {
234 170
					$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $d );
235
				}
236
			}
237
		} else {
238
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' );
239
			$setNS = false;
240
			return null;
241
		}
242
243 170
		$setNS = $mustSetNS; // NOTE: also false if namespaces were given but no default NS descs are available
244
245 170
		return $result;
246
	}
247
248
	/**
249
	 * Compute an SMWDescription for current part of a query, which should
250
	 * be the content of "[[ ... ]]". Returns NULL upon error.
251
	 *
252
	 * Parameters $setNS has the same use as in getSubqueryDescription().
253
	 */
254 170
	private function getLinkDescription( &$setNS ) {
255
		// This method is called when we encountered an opening '[['. The following
256
		// block could be a Category-statement, fixed object, or property statement.
257 170
		$chunk = $this->readChunk( '', true, false ); // NOTE: untrimmed, initial " " escapes prop. chains
258
259 170
		if ( in_array( smwfNormalTitleText( $chunk ),
260 170
			array( $this->categoryPrefix, $this->conceptPrefix, $this->categoryPrefixCannonical, $this->conceptPrefixCannonical ) ) ) {
261 66
			return $this->getClassDescription( $setNS, (
262 66
				smwfNormalTitleText( $chunk ) == $this->categoryPrefix || smwfNormalTitleText( $chunk ) == $this->categoryPrefixCannonical
263
			) );
264
		} else { // fixed subject, namespace restriction, property query, or subquery
265 155
			$sep = $this->readChunk( '', false ); // do not consume hit, "look ahead"
266
267 155
			if ( ( $sep == '::' ) || ( $sep == ':=' ) ) {
268 125
				if ( $chunk{0} != ':' ) { // property statement
269 125
					return $this->getPropertyDescription( $chunk, $setNS );
270
				} else { // escaped article description, read part after :: to get full contents
271
					$chunk .= $this->readChunk( '\[\[|\]\]|\|\||\|' );
272
					return $this->getArticleDescription( trim( $chunk ), $setNS );
273
				}
274
			} else { // Fixed article/namespace restriction. $sep should be ]] or ||
275 48
				return $this->getArticleDescription( trim( $chunk ), $setNS );
276
			}
277
		}
278
	}
279
280
	/**
281
	 * Parse a category description (the part of an inline query that
282
	 * is in between "[[Category:" and the closing "]]" and create a
283
	 * suitable description.
284
	 */
285 66
	private function getClassDescription( &$setNS, $category = true ) {
286
		// note: no subqueries allowed here, inline disjunction allowed, wildcards allowed
287 66
		$result = null;
288 66
		$continue = true;
289
290 66
		while ( $continue ) {
291 66
			$chunk = $this->readChunk();
292
293 66
			if ( $chunk == '+' ) {
294
				$description = new NamespaceDescription( $category ? NS_CATEGORY : SMW_NS_CONCEPT );
295
				$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $description );
296
			} else { // assume category/concept title
297
				/// NOTE: we add m_c...prefix to prevent problems with, e.g., [[Category:Template:Test]]
298 66
				$title = Title::newFromText( ( $category ? $this->categoryPrefix : $this->conceptPrefix ) . $chunk );
299
300 66
				if ( !is_null( $title ) ) {
301 66
					$diWikiPage = new SMWDIWikiPage( $title->getDBkey(), $title->getNameSpace(), '' );
302 66
					$desc = $category ? new ClassDescription( $diWikiPage ) : new ConceptDescription( $diWikiPage );
303 66
					$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, $desc );
304
				}
305
			}
306
307 66
			$chunk = $this->readChunk();
308 66
			$continue = ( $chunk == '||' ) && $category; // disjunctions only for cateories
309
		}
310
311 66
		return $this->finishLinkDescription( $chunk, false, $result, $setNS );
312
	}
313
314
	/**
315
	 * Parse a property description (the part of an inline query that
316
	 * is in between "[[Some property::" and the closing "]]" and create a
317
	 * suitable description. The "::" is the first chunk on the current
318
	 * string.
319
	 */
320 125
	private function getPropertyDescription( $propertyName, &$setNS ) {
321 125
		$this->readChunk(); // consume separator ":=" or "::"
322
323
		// first process property chain syntax (e.g. "property1.property2::value"), escaped by initial " ":
324 125
		$propertynames = ( $propertyName{0} == ' ' ) ? array( $propertyName ) : explode( '.', $propertyName );
325 125
		$properties = array();
326 125
		$typeid = '_wpg';
327 125
		$inverse = false;
328
329 125
		foreach ( $propertynames as $name ) {
330 125
			if ( !$this->isPagePropertyType( $typeid ) ) { // non-final property in chain was no wikipage: not allowed
331
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', $name );
332
				return null; ///TODO: read some more chunks and try to finish [[ ]]
333
			}
334
335 125
			$property = SMWPropertyValue::makeUserProperty( $name );
336
337 125
			if ( !$property->isValid() ) { // illegal property identifier
338
				$this->descriptionProcessor->addError( $property->getErrors() );
339
				return null; ///TODO: read some more chunks and try to finish [[ ]]
340
			}
341
342 125
			$typeid = $property->getDataItem()->findPropertyTypeID();
343 125
			$inverse = $property->isInverse();
344 125
			$properties[] = $property;
345
		} ///NOTE: after iteration, $property and $typeid correspond to last value
346
347 125
		$innerdesc = null;
348 125
		$continue = true;
349
350 125
		while ( $continue ) {
351 125
			$chunk = $this->readChunk();
352
353
			switch ( $chunk ) {
354 125
				case '+': // wildcard, add namespaces for page-type properties
355 42
					if ( !is_null( $this->defaultNamespace ) && ( $this->isPagePropertyType( $typeid ) || $inverse ) ) {
356
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->defaultNamespace );
357
					} else {
358 42
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() );
359
					}
360 42
					$chunk = $this->readChunk();
361 42
				break;
362 98
				case '<q>': // subquery, set default namespaces
363 12
					if ( $this->isPagePropertyType( $typeid ) || $inverse ) {
364 12
						$this->pushDelimiter( '</q>' );
365 12
						$setsubNS = true;
366 12
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->getSubqueryDescription( $setsubNS ) );
367
					} else { // no subqueries allowed for non-pages
368
						$this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', end( $propertynames ) );
369
						$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() );
370
					}
371 12
					$chunk = $this->readChunk();
372 12
				break;
373
				default: // normal object value
374
					// read value(s), possibly with inner [[...]]
375 98
					$open = 1;
376 98
					$value = $chunk;
377 98
					$continue2 = true;
378
					// read value with inner [[, ]], ||
379 98
					while ( ( $open > 0 ) && ( $continue2 ) ) {
380 98
						$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
381
						switch ( $chunk ) {
382 98
							case '[[': // open new [[ ]]
383
								$open++;
384
							break;
385 98
							case ']]': // close [[ ]]
386 98
								$open--;
387 98
							break;
388 11
							case '|':
389 10
							case '||': // terminates only outermost [[ ]]
390 10
								if ( $open == 1 ) {
391 10
									$open = 0;
392
								}
393 10
							break;
394 1
							case '': ///TODO: report error; this is not good right now
395
								$continue2 = false;
396
							break;
397
						}
398 98
						if ( $open != 0 ) {
399 1
							$value .= $chunk;
400
						}
401
					} ///NOTE: at this point, we normally already read one more chunk behind the value
402
403 98
					$innerdesc = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom(
404
						$innerdesc,
405 98
						$this->descriptionProcessor->constructDescriptionForPropertyObjectValue( $property->getDataItem(), $value )
0 ignored issues
show
The variable $property does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
$property->getDataItem() of type object<SMWDataItem> is not a sub-type of object<SMW\DIProperty>. It seems like you assume a child class of the class SMWDataItem to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
406
					);
407
408
			}
409 125
			$continue = ( $chunk == '||' );
410
		}
411
412 125
		if ( is_null( $innerdesc ) ) { // make a wildcard search
413
			$innerdesc = ( !is_null( $this->defaultNamespace ) && $this->isPagePropertyType( $typeid ) ) ?
414
							$this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, $this->defaultNamespace ) :
415
							$this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $innerdesc, new ThingDescription() );
416
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_propvalueproblem', $property->getWikiValue() );
417
		}
418
419 125
		$properties = array_reverse( $properties );
420
421 125
		foreach ( $properties as $property ) {
422 125
			$innerdesc = new SomeProperty( $property->getDataItem(), $innerdesc );
423
		}
424
425 125
		$result = $innerdesc;
426
427 125
		return $this->finishLinkDescription( $chunk, false, $result, $setNS );
0 ignored issues
show
The variable $chunk does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
428
	}
429
430
	/**
431
	 * Parse an article description (the part of an inline query that
432
	 * is in between "[[" and the closing "]]" assuming it is not specifying
433
	 * a category or property) and create a suitable description.
434
	 * The first chunk behind the "[[" has already been read and is
435
	 * passed as a parameter.
436
	 */
437 48
	private function getArticleDescription( $firstChunk, &$setNS ) {
438 48
		$chunk = $firstChunk;
439 48
		$result = null;
440 48
		$continue = true;
441
442 48
		while ( $continue ) {
443 48
			if ( $chunk == '<q>' ) { // no subqueries of the form [[<q>...</q>]] (not needed)
444
445
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsubquery' );
446
				return null;
447
			}
448
449 48
			$list = preg_split( '/:/', $chunk, 3 ); // ":Category:Foo" "User:bar"  ":baz" ":+"
450
451 48
			if ( ( $list[0] === '' ) && ( count( $list ) == 3 ) ) {
452 2
				$list = array_slice( $list, 1 );
453
			}
454 48
			if ( ( count( $list ) == 2 ) && ( $list[1] == '+' ) ) { // try namespace restriction
455
456 4
				$idx = \SMW\Localizer::getInstance()->getNamespaceIndexByName( $list[0] );
457
458 4
				if ( $idx !== false ) {
459 4
					$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom( $result, new NamespaceDescription( $idx ) );
460
				}
461
			} else {
462 45
				$result = $this->descriptionProcessor->constructDisjunctiveCompoundDescriptionFrom(
463
					$result,
464 45
					$this->descriptionProcessor->constructDescriptionForWikiPageValueChunk( $chunk )
465
				);
466
			}
467
468 48
			$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
469
470 48
			if ( $chunk == '||' ) {
471
				$chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
472
				$continue = true;
473
			} else {
474 48
				$continue = false;
475
			}
476
		}
477
478 48
		return $this->finishLinkDescription( $chunk, true, $result, $setNS );
479
	}
480
481 170
	private function finishLinkDescription( $chunk, $hasNamespaces, $result, &$setNS ) {
482 170
		if ( is_null( $result ) ) { // no useful information or concrete error found
483
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk ); // was smw_badqueryatom
484 170
		} elseif ( !$hasNamespaces && $setNS && !is_null( $this->defaultNamespace  ) ) {
485
			$result = $this->descriptionProcessor->constructConjunctiveCompoundDescriptionFrom( $result, $this->defaultNamespace );
486
			$hasNamespaces = true;
487
		}
488
489 170
		$setNS = $hasNamespaces;
490
491 170
		if ( $chunk == '|' ) { // skip content after single |, but report a warning
492
			// Note: Using "|label" in query atoms used to be a way to set the mainlabel in SMW <1.0; no longer supported now
493 1
			$chunk = $this->readChunk( '\]\]' );
494 1
			$labelpart = '|';
495 1
			if ( $chunk != ']]' ) {
496 1
				$labelpart .= $chunk;
497 1
				$chunk = $this->readChunk( '\]\]' );
498
			}
499 1
			$this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $labelpart );
500
		}
501
502 170
		if ( $chunk != ']]' ) {
503
			// What happended? We found some chunk that could not be processed as
504
			// link content (as in [[Category:Test<q>]]), or the closing ]] are
505
			// just missing entirely.
506 2
			if ( $chunk !== '' ) {
507 1
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsymbol', $chunk );
508
509
				// try to find a later closing ]] to finish this misshaped subpart
510 1
				$chunk = $this->readChunk( '\]\]' );
511
512 1
				if ( $chunk != ']]' ) {
513 1
					$chunk = $this->readChunk( '\]\]' );
514
				}
515
			}
516 2
			if ( $chunk === '' ) {
517 1
				$this->descriptionProcessor->addErrorWithMsgKey( 'smw_noclosingbrackets' );
518
			}
519
		}
520
521 170
		return $result;
522
	}
523
524
	/**
525
	 * Get the next unstructured string chunk from the query string.
526
	 * Chunks are delimited by any of the special strings used in inline queries
527
	 * (such as [[, ]], <q>, ...). If the string starts with such a delimiter,
528
	 * this delimiter is returned. Otherwise the first string in front of such a
529
	 * delimiter is returned.
530
	 * Trailing and initial spaces are ignored if $trim is true, and chunks
531
	 * consisting only of spaces are not returned.
532
	 * If there is no more qurey string left to process, the empty string is
533
	 * returned (and in no other case).
534
	 *
535
	 * The stoppattern can be used to customise the matching, especially in order to
536
	 * overread certain special symbols.
537
	 *
538
	 * $consume specifies whether the returned chunk should be removed from the
539
	 * query string.
540
	 */
541 170
	private function readChunk( $stoppattern = '', $consume = true, $trim = true ) {
542 170
		if ( $stoppattern === '' ) {
543
			$stoppattern = '\[\[|\]\]|::|:=|<q>|<\/q>' .
544 170
				'|^' . $this->categoryPrefix . '|^' . $this->categoryPrefixCannonical .
545 170
				'|^' . $this->conceptPrefix . '|^' . $this->conceptPrefixCannonical .
546 170
				'|\|\||\|';
547
		}
548 170
		$chunks = preg_split( '/[\s]*(' . $stoppattern . ')/iu', $this->currentString, 2, PREG_SPLIT_DELIM_CAPTURE );
549 170
		if ( count( $chunks ) == 1 ) { // no matches anymore, strip spaces and finish
550 170
			if ( $consume ) {
551 170
				$this->currentString = '';
552
			}
553
554 170
			return $trim ? trim( $chunks[0] ) : $chunks[0];
555 170
		} elseif ( count( $chunks ) == 3 ) { // this should generally happen if count is not 1
556 170
			if ( $chunks[0] === '' ) { // string started with delimiter
557 170
				if ( $consume ) {
558 170
					$this->currentString = $chunks[2];
559
				}
560
561 170
				return $trim ? trim( $chunks[1] ) : $chunks[1];
562
			} else {
563 169
				if ( $consume ) {
564 169
					$this->currentString = $chunks[1] . $chunks[2];
565
				}
566
567 169
				return $trim ? trim( $chunks[0] ) : $chunks[0];
568
			}
569
		} else {
570
			return false;
571
		} // should never happen
572
	}
573
574
	/**
575
	 * Enter a new subblock in the query, which must at some time be terminated by the
576
	 * given $endstring delimiter calling popDelimiter();
577
	 */
578 24
	private function pushDelimiter( $endstring ) {
579 24
		array_push( $this->separatorStack, $endstring );
580 24
	}
581
582
	/**
583
	 * Exit a subblock in the query ending with the given delimiter.
584
	 * If the delimiter does not match the top-most open block, false
585
	 * will be returned. Otherwise return true.
586
	 */
587 24
	private function popDelimiter( $endstring ) {
588 24
		$topdelim = array_pop( $this->separatorStack );
589 24
		return ( $topdelim == $endstring );
590
	}
591
592 125
	private function isPagePropertyType( $typeid ) {
593 125
		return $typeid == '_wpg' || DataTypeRegistry::getInstance()->isSubDataType( $typeid );
594
	}
595
596
}
597