Completed
Push — master ( f835ba...dee201 )
by mw
47:53 queued 08:25
created

InTextAnnotationParser::removeAnnotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SMW;
4
5
use Hooks;
6
use SMW\MediaWiki\MagicWordsFinder;
7
use SMW\MediaWiki\RedirectTargetFinder;
8
use SMWOutputs;
9
use Title;
10
11
/**
12
 * Class collects all functions for wiki text parsing / processing that are
13
 * relevant for SMW
14
 *
15
 * This class is contains all functions necessary for parsing wiki text before
16
 * it is displayed or previewed while identifying SMW related annotations.
17
 *
18
 * @note Settings involve smwgNamespacesWithSemanticLinks, smwgLinksInValues,
19
 * smwgInlineErrors
20
 *
21
 * @license GNU GPL v2+
22
 * @since 1.9
23
 *
24
 * @author Markus Krötzsch
25
 * @author Denny Vrandecic
26
 * @author mwjames
27
 */
28
class InTextAnnotationParser {
29
30
	/**
31
	 * Internal state for switching SMW link annotations off/on during parsing
32
	 * ([[SMW::on]] and [[SMW:off]])
33
	 */
34
	const OFF = '[[SMW::off]]';
35
	const ON = '[[SMW::on]]';
36
37
	/**
38
	 * @var ParserData
39
	 */
40
	private $parserData;
41
42
	/**
43
	 * @var MagicWordsFinder
44
	 */
45
	private $magicWordsFinder;
46
47
	/**
48
	 * @var RedirectTargetFinder
49
	 */
50
	private $redirectTargetFinder;
51
52
	/**
53
	 * @var DataValueFactory
54
	 */
55
	private $dataValueFactory = null;
56
57
	/**
58
	 * @var ApplicationFactory
59
	 */
60
	private $applicationFactory = null;
61
62
	/**
63
	 * @var Settings
64
	 */
65
	protected $settings = null;
66
67
	/**
68
	 * @var boolean
69
	 */
70
	protected $isEnabledNamespace;
71
72
	/**
73
	 * Internal state for switching SMW link annotations off/on during parsing
74
	 * ([[SMW::on]] and [[SMW:off]])
75
	 * @var boolean
76
	 */
77
	protected $isAnnotation = true;
78
79
	/**
80
	 * @var boolean
81
	 */
82
	private $strictModeState = true;
83
84 224
	/**
85 224
	 * @since 1.9
86 224
	 *
87 224
	 * @param ParserData $parserData
88 224
	 * @param MagicWordsFinder $magicWordsFinder
89 224
	 * @param RedirectTargetFinder $redirectTargetFinder
90 224
	 */
91
	public function __construct( ParserData $parserData, MagicWordsFinder $magicWordsFinder, RedirectTargetFinder $redirectTargetFinder ) {
92
		$this->parserData = $parserData;
93
		$this->magicWordsFinder = $magicWordsFinder;
94
		$this->redirectTargetFinder = $redirectTargetFinder;
95
		$this->dataValueFactory = DataValueFactory::getInstance();
96
		$this->applicationFactory = ApplicationFactory::getInstance();
97
	}
98
99
	/**
100
	 * Whether a strict interpretation (e.g [[property::value:partOfTheValue::alsoPartOfTheValue]])
101 201
	 * or a more loose interpretation (e.g. [[property1::property2::value]]) for
102 201
	 * annotations is to be applied.
103 201
	 *
104
	 * @since 2.3
105
	 *
106
	 * @param boolean $strictModeState
107
	 */
108
	public function setStrictModeState( $strictModeState ) {
109
		$this->strictModeState = (bool)$strictModeState;
110
	}
111
112
	/**
113 209
	 * Parsing text before an article is displayed or previewed, strip out
114
	 * semantic properties and add them to the ParserOutput object
115 209
	 *
116 209
	 * @since 1.9
117 209
	 *
118
	 * @param string &$text
119
	 */
120 209
	public function parse( &$text ) {
121
122 209
		$title = $this->parserData->getTitle();
123
		$this->settings = $this->applicationFactory->getSettings();
124 209
		$start = microtime( true );
125 209
126
		// Identifies the current parser run (especially when called recursively)
127 209
		$this->parserData->getSubject()->setContextReference( 'intp:' . uniqid() );
128
129 209
		$this->doStripMagicWordsFromText( $text );
130 209
131 209
		$this->isSemanticEnabled( $title );
132
		$this->addRedirectTargetAnnotation( $text );
133
134
		$linksInValues = $this->settings->get( 'smwgLinksInValues' );
135 209
136 205
		$text = preg_replace_callback(
137
			$this->getRegexpPattern( $linksInValues ),
138 205
			$linksInValues ? 'self::process' : 'self::preprocess',
139 205
			$text
140
		);
141
142
		if ( $this->isEnabledNamespace ) {
143 209
			$this->parserData->getOutput()->addModules( $this->getModules() );
144
145 209
			if ( method_exists( $this->parserData->getOutput(), 'recordOption' ) ) {
146 209
				$this->parserData->getOutput()->recordOption( 'userlang' );
147 209
			}
148
		}
149
150 209
		$this->parserData->pushSemanticDataToParserOutput();
151 209
152
		$this->parserData->addLimitReport(
153
			'intext-parsertime',
154
			number_format( ( microtime( true ) - $start ), 3 )
155
		);
156
157
		SMWOutputs::commitToParserOutput( $this->parserData->getOutput() );
158
	}
159
160 27
	/**
161 27
	 * @since 2.4
162
	 *
163
	 * @param string $text
164
	 *
165
	 * @return text
166
	 */
167
	public static function decodeSquareBracket( $text ) {
168
		return InTextAnnotationSanitizer::decodeSquareBracket( $text );
169
	}
170
171 7
	/**
172 7
	 * @since 2.4
173 7
	 *
174
	 * @param string $text
175 5
	 *
176 7
	 * @return text
177 7
	 */
178
	public static function obscureAnnotation( $text ) {
179
		return InTextAnnotationSanitizer::obscureAnnotation( $text );
180
	}
181
182
	/**
183
	 * @since 2.4
184
	 *
185
	 * @param string $text
186
	 *
187
	 * @return text
188 27
	 */
189 27
	public static function removeAnnotation( $text ) {
190 27
		return InTextAnnotationSanitizer::removeAnnotation( $text );
191 27
	}
192 8
193 8
	/**
194
	 * @since 2.1
195
	 *
196 8
	 * @param Title|null $redirectTarget
197 1
	 */
198
	public function setRedirectTarget( Title $redirectTarget = null ) {
199
		$this->redirectTargetFinder->setRedirectTarget( $redirectTarget );
200
	}
201 7
202 7
	protected function addRedirectTargetAnnotation( $text ) {
203 3
204
		if ( $this->isEnabledNamespace ) {
205
206
			$this->redirectTargetFinder->findRedirectTargetFromText( $text );
207 7
208
			$propertyAnnotatorFactory = $this->applicationFactory->newPropertyAnnotatorFactory();
209
210 7
			$propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
211 2
				$this->parserData->getSemanticData()
212
			);
213
214 5
			$redirectPropertyAnnotator = $propertyAnnotatorFactory->newRedirectPropertyAnnotator(
215 5
				$propertyAnnotator,
216 5
				$this->redirectTargetFinder
217
			);
218
219 5
			$redirectPropertyAnnotator->addAnnotation();
220 27
		}
221 27
	}
222
223
	/**
224
	 * Returns required resource modules
225
	 *
226
	 * @since 1.9
227
	 *
228
	 * @return array
229
	 */
230 188
	protected function getModules() {
231 188
		return array(
232 188
			'ext.smw.style',
233
			'ext.smw.tooltips'
234 209
		);
235
	}
236 209
237
	/**
238 205
	 * $smwgLinksInValues (default = false) determines which regexp pattern
239
	 * is returned, either a more complex (lib PCRE may cause segfaults if text
240 205
	 * is long) or a simpler (no segfaults found for those, but no links
241
	 * in values) pattern.
242 205
	 *
243 205
	 * If enabled (SMW accepts inputs like [[property::Some [[link]] in value]]),
244
	 * this may lead to PHP crashes (!) when very long texts are
245
	 * used as values. This is due to limitations in the library PCRE that
246 205
	 * PHP uses for pattern matching.
247
	 *
248 205
	 * @since 1.9
249
	 *
250
	 * @param boolean $linksInValues
251 205
	 *
252
	 * @return string
253 209
	 */
254
	public static function getRegexpPattern( $linksInValues ) {
255
		if ( $linksInValues ) {
256
			return '/\[\[             # Beginning of the link
257
				(?:([^:][^]]*):[=:])+ # Property name (or a list of those)
258
				(                     # After that:
259
				  (?:[^|\[\]]         #   either normal text (without |, [ or ])
260
				  |\[\[[^]]*\]\]      #   or a [[link]]
261
				  |\[[^]]*\]          #   or an [external link]
262 205
				)*)                   # all this zero or more times
263
				(?:\|([^]]*))?        # Display text (like "text" in [[link|text]]), optional
264 205
				\]\]                  # End of link
265
				/xu';
266
		} else {
267
			return '/\[\[             # Beginning of the link
268
				(?:([^:][^]]*):[=:])+ # Property name (or a list of those)
269
				([^\[\]]*)            # content: anything but [, |, ]
270
				\]\]                  # End of link
271
				/xu';
272
		}
273
	}
274
275
	/**
276
	 * A method that precedes the process() callback, it takes care of separating
277
	 * value and caption (instead of leaving this to a more complex regexp).
278
	 *
279
	 * @since 1.9
280
	 *
281
	 * @param array $semanticLink expects (linktext, properties, value|caption)
282
	 *
283
	 * @return string
284
	 */
285
	protected function preprocess( array $semanticLink ) {
286 218
		$value = '';
287 218
		$caption = false;
288
289
		if ( array_key_exists( 2, $semanticLink ) ) {
290
291
			// #1747 avoid a mismatch on an annotation like [[Foo|Bar::Foobar]]
292
			// where the left part of :: is split and would contain "Foo|Bar"
293
			// hence this type is categorized as no value annotation
294
			if ( strpos( $semanticLink[1], '|' ) !== false ) {
295
				return $semanticLink[0];
296
			}
297 1
298
			$parts = explode( '|', $semanticLink[2] );
299
300
			if ( array_key_exists( 0, $parts ) ) {
301
				$value = $parts[0];
302
			}
303 217
			if ( array_key_exists( 1, $parts ) ) {
304
				$caption = $parts[1];
305
			}
306
		}
307
308
		if ( $caption !== false ) {
309
			return $this->process( array( $semanticLink[0], $semanticLink[1], $value, $caption ) );
310
		}
311
312
		return $this->process( array( $semanticLink[0], $semanticLink[1], $value ) );
313
	}
314
315
	/**
316
	 * This callback function strips out the semantic attributes from a wiki
317 179
	 * link.
318 179
	 *
319 179
	 * @since 1.9
320
	 *
321 179
	 * @param array $semanticLink expects (linktext, properties, value|caption)
322
	 *
323
	 * @return string
324
	 */
325
	protected function process( array $semanticLink ) {
326 179
327 4
		$valueCaption = false;
328
		$property = '';
329
		$value = '';
330 178
331
		if ( array_key_exists( 1, $semanticLink ) ) {
332 178
333 178
			// #1252 Strict mode being disabled for support of multi property
334
			// assignments (e.g. [[property1::property2::value]])
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
335 178
336 11
			// #1066 Strict mode is to check for colon(s) produced by something
337
			// like [[Foo::Bar::Foobar]], [[Foo:::0049 30 12345678]]
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
338
			// In case a colon appears (in what is expected to be a string without a colon)
339
			// then concatenate the string again and split for the first :: occurrence
340 178
			// only
341 11
			if ( $this->strictModeState && strpos( $semanticLink[1], ':' ) !== false && isset( $semanticLink[2] ) ) {
342
				list( $semanticLink[1], $semanticLink[2] ) = explode( '::', $semanticLink[1] . '::' . $semanticLink[2], 2 );
343
			}
344 178
345
			$property = $semanticLink[1];
346
		}
347
348
		if ( array_key_exists( 2, $semanticLink ) ) {
349
			$value = $semanticLink[2];
350
		}
351
352
		if ( $value === '' ) { // silently ignore empty values
353
			return '';
354
		}
355
356
		if ( $property == 'SMW' ) {
357 180
			switch ( $value ) {
358
				case 'on':
359 180
					$this->isAnnotation = true;
360 180
					break;
361 180
				case 'off':
362
					$this->isAnnotation = false;
363 180
					break;
364
			}
365
			return '';
366
		}
367
368
		if ( array_key_exists( 3, $semanticLink ) ) {
369
			$valueCaption = $semanticLink[3];
370
		}
371
372
		// Extract annotations and create tooltip.
373 180
		$properties = preg_split( '/:[=:]/u', $property );
374 14
375
		return $this->addPropertyValue( $properties, $value, $valueCaption );
376
	}
377 180
378
	/**
379
	 * Adds property values to the ParserOutput instance
380 180
	 *
381 180
	 * @since 1.9
382
	 *
383
	 * @param array $properties
384 180
	 *
385 1
	 * @return string
386
	 */
387
	protected function addPropertyValue( array $properties, $value, $valueCaption ) {
388 180
389
		$subject = $this->parserData->getSubject();
390 8
391 8
		if ( ( $propertyLink = $this->getPropertyLink( $subject, $properties, $value, $valueCaption ) ) !== '' ) {
392 8
			return $propertyLink;
393 8
		}
394 8
395 8
		// Add properties to the semantic container
396
		foreach ( $properties as $property ) {
397 8
			$dataValue = $this->dataValueFactory->newDataValueByText(
398
				$property,
399
				$value,
400 178
				$valueCaption,
401 12
				$subject
402
			);
403
404
			if (
405 178
				$this->isEnabledNamespace &&
406
				$this->isAnnotation &&
407 178
				$this->parserData->canModifySemanticData() ) {
408
				$this->parserData->addDataValue( $dataValue );
409
			}
410
		}
411
412
		// Return the text representation
413
		$result = $dataValue->getShortWikitext( true );
0 ignored issues
show
Bug introduced by
The variable $dataValue 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...
414
415
		// If necessary add an error text
416
		if ( ( $this->settings->get( 'smwgInlineErrors' ) &&
417
			$this->isEnabledNamespace && $this->isAnnotation ) &&
418
			( !$dataValue->isValid() ) ) {
419 178
			// Encode `:` to avoid a comment block and instead of the nowiki tag
420
			// use &#58; as placeholder
421 178
			$result = str_replace( ':', '&#58;', $result ) . $dataValue->getErrorText();
422
		}
423 178
424 1
		return $result;
425
	}
426
427
	protected function doStripMagicWordsFromText( &$text ) {
428 177
429 177
		$words = array();
430
431
		$this->magicWordsFinder->setOutput( $this->parserData->getOutput() );
432
433
		$magicWords = array(
434
			'SMW_NOFACTBOX',
435
			'SMW_SHOWFACTBOX'
436
		);
437 177
438 177
		Hooks::run( 'SMW::Parser::BeforeMagicWordsFinder', array( &$magicWords ) );
439 177
440 177
		foreach ( $magicWords as $magicWord ) {
441
			$words[] = $this->magicWordsFinder->findMagicWordInText( $magicWord, $text );
442
		}
443
444
		$this->magicWordsFinder->pushMagicWordsToParserOutput( $words );
445 177
446
		return $words;
447
	}
448 177
449 177
	private function isSemanticEnabled( Title $title ) {
450 177
		$this->isEnabledNamespace = $this->applicationFactory->getNamespaceExaminer()->isSemanticEnabled( $title->getNamespace() );
451
	}
452
453 14
	private function getPropertyLink( $subject, $properties, $value, $valueCaption ) {
454
455
		// #...
456 177
		if ( strlen( $value ) == 3 && $value === '@@@' ) {
457
			$property = end( $properties );
458
459 209
			$dataValue = $this->dataValueFactory->newPropertyValueByLabel(
460
				$property,
461 209
				$valueCaption,
462
				$subject
463 209
			);
464
465
			return $dataValue->getShortWikitext( true );
466 209
		}
467
468
		return '';
469
	}
470 209
471
}
472