Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Pull Request — master (#44)
by Der Mundschenk
02:15
created

PHP_Typography::get_hyphenation_languages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 *  This file is part of PHP-Typography.
4
 *
5
 *  Copyright 2014-2017 Peter Putzer.
6
 *  Copyright 2009-2011 KINGdesk, LLC.
7
 *
8
 *  This program is free software; you can redistribute it and/or modify
9
 *  it under the terms of the GNU General Public License as published by
10
 *  the Free Software Foundation; either version 2 of the License, or
11
 *  (at your option) any later version.
12
 *
13
 *  This program is distributed in the hope that it will be useful,
14
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 *  GNU General Public License for more details.
17
 *
18
 *  You should have received a copy of the GNU General Public License along
19
 *  with this program; if not, write to the Free Software Foundation, Inc.,
20
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 *
22
 *  ***
23
 *
24
 *  @package mundschenk-at/php-typography
25
 *  @license http://www.gnu.org/licenses/gpl-2.0.html
26
 */
27
28
namespace PHP_Typography;
29
30
use PHP_Typography\Fixes\Registry;
31
32
/**
33
 * Parses HTML5 (or plain text) and applies various typographic fixes to the text.
34
 *
35
 * If used with multibyte language, UTF-8 encoding is required.
36
 *
37
 * Portions of this code have been inspired by:
38
 *  - typogrify (https://code.google.com/p/typogrify/)
39
 *  - WordPress code for wptexturize (https://developer.wordpress.org/reference/functions/wptexturize/)
40
 *  - PHP SmartyPants Typographer (https://michelf.ca/projects/php-smartypants/typographer/)
41
 *
42
 *  @author Jeffrey D. King <[email protected]>
43
 *  @author Peter Putzer <[email protected]>
44
 */
45
class PHP_Typography {
46
47
	/**
48
	 * A DOM-based HTML5 parser.
49
	 *
50
	 * @var \Masterminds\HTML5
51
	 */
52
	private $html5_parser;
53
54
	/**
55
	 * The hyphenator cache.
56
	 *
57
	 * @var Hyphenator\Cache
58
	 */
59
	protected $hyphenator_cache;
60
61
	/**
62
	 * The node fixes registry.
63
	 *
64
	 * @var Registry;
65
	 */
66
	private $registry;
67
68
	/**
69
	 * Whether the Hyphenator\Cache of the $registry needs to be updated.
70
	 *
71
	 * @var bool
72
	 */
73
	private $update_registry_cache;
74
75
	/**
76
	 * Sets up a new PHP_Typography object.
77
	 *
78
	 * @param Registry|null $registry Optional. A fix registry instance. Default null,
79
	 *                                meaning the default fixes are used.
80
	 */
81
	public function __construct( Registry $registry = null ) {
82
		$this->registry              = $registry;
83
		$this->update_registry_cache = ! empty( $registry );
84
	}
85
86
	/**
87
	 * Modifies $html according to the defined settings.
88
	 *
89
	 * @param string   $html      A HTML fragment.
90
	 * @param Settings $settings  A settings object.
91
	 * @param bool     $is_title  Optional. If the HTML fragment is a title. Default false.
92
	 *
93
	 * @return string The processed $html.
94
	 */
95
	public function process( $html, Settings $settings, $is_title = false ) {
96
		return $this->process_textnodes( $html, [ $this, 'apply_fixes_to_html_node' ], $settings, $is_title );
97
	}
98
99
	/**
100
	 * Modifies $html according to the defined settings, in a way that is appropriate for RSS feeds
101
	 * (i.e. excluding processes that may not display well with limited character set intelligence).
102
	 *
103
	 * @param string   $html     A HTML fragment.
104
	 * @param Settings $settings  A settings object.
105
	 * @param bool     $is_title Optional. If the HTML fragment is a title. Default false.
106
	 *
107
	 * @return string The processed $html.
108
	 */
109
	public function process_feed( $html, Settings $settings, $is_title = false ) {
110
		return $this->process_textnodes( $html, [ $this, 'apply_fixes_to_feed_node' ], $settings, $is_title );
111
	}
112
113
	/**
114
	 * Applies specific fixes to all textnodes of the HTML fragment.
115
	 *
116
	 * @param string   $html     A HTML fragment.
117
	 * @param callable $fixer    A callback that applies typography fixes to a single textnode.
118
	 * @param Settings $settings  A settings object.
119
	 * @param bool     $is_title Optional. If the HTML fragment is a title. Default false.
120
	 *
121
	 * @return string The processed $html.
122
	 */
123
	public function process_textnodes( $html, callable $fixer, Settings $settings, $is_title = false ) {
124
		if ( isset( $settings['ignoreTags'] ) && $is_title && ( in_array( 'h1', $settings['ignoreTags'], true ) || in_array( 'h2', $settings['ignoreTags'], true ) ) ) {
125
			return $html;
126
		}
127
128
		// Lazy-load our parser (the text parser is not needed for feeds).
129
		$html5_parser = $this->get_html5_parser();
130
131
		// Parse the HTML.
132
		$dom = $this->parse_html( $html5_parser, $html, $settings );
133
134
		// Abort if there were parsing errors.
135
		if ( empty( $dom ) ) {
136
			return $html;
137
		}
138
139
		// Query some nodes in the DOM.
140
		$xpath          = new \DOMXPath( $dom );
141
		$body_node      = $xpath->query( '/html/body' )->item( 0 );
142
		$all_textnodes  = $xpath->query( '//text()', $body_node );
143
		$tags_to_ignore = $this->query_tags_to_ignore( $xpath, $body_node, $settings );
144
145
		// Start processing.
146
		foreach ( $all_textnodes as $textnode ) {
147
			if ( self::arrays_intersect( DOM::get_ancestors( $textnode ), $tags_to_ignore ) ) {
148
				continue;
149
			}
150
151
			// We won't be doing anything with spaces, so we can jump ship if that is all we have.
152
			if ( $textnode->isWhitespaceInElementContent() ) {
153
				continue;
154
			}
155
156
			// Decode all characters except < > &.
157
			$textnode->data = htmlspecialchars( $textnode->data, ENT_NOQUOTES, 'UTF-8' ); // returns < > & to encoded HTML characters (&lt; &gt; and &amp; respectively).
158
159
			// Apply fixes.
160
			call_user_func( $fixer, $textnode, $settings, $is_title );
161
162
			// Until now, we've only been working on a textnode: HTMLify result.
163
			$this->replace_node_with_html( $textnode, $textnode->data );
164
		}
165
166
		return $html5_parser->saveHTML( $body_node->childNodes );
167
	}
168
169
	/**
170
	 * Determines whether two object arrays intersect. The second array is expected
171
	 * to use the spl_object_hash for its keys.
172
	 *
173
	 * @param array $array1 The keys are ignored.
174
	 * @param array $array2 This array has to be in the form ( $spl_object_hash => $object ).
175
	 *
176
	 * @return boolean
177
	 */
178
	protected static function arrays_intersect( array $array1, array $array2 ) {
179
		foreach ( $array1 as $value ) {
180
			if ( isset( $array2[ spl_object_hash( $value ) ] ) ) {
181
				return true;
182
			}
183
		}
184
185
		return false;
186
	}
187
188
	/**
189
	 * Applies standard typography fixes to a textnode.
190
	 *
191
	 * @param \DOMText $textnode The node to process.
192
	 * @param Settings $settings The settings to apply.
193
	 * @param bool     $is_title Optional. Default false.
194
	 */
195 View Code Duplication
	protected function apply_fixes_to_html_node( \DOMText $textnode, Settings $settings, $is_title = false ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
196
		foreach ( $this->get_registry()->get_node_fixes() as $group => $fixes ) {
197
			foreach ( $fixes as $fix ) {
198
				$fix->apply( $textnode, $settings, $is_title );
199
			}
200
		}
201
	}
202
203
	/**
204
	 * Applies typography fixes specific to RSS feeds to a textnode.
205
	 *
206
	 * @param \DOMText $textnode The node to process.
207
	 * @param Settings $settings The settings to apply.
208
	 * @param bool     $is_title Optional. Default false.
209
	 */
210 View Code Duplication
	protected function apply_fixes_to_feed_node( \DOMText $textnode, Settings $settings, $is_title = false ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
		foreach ( $this->get_registry()->get_node_fixes() as $group => $fixes ) {
212
			foreach ( $fixes as $fix ) {
213
				if ( $fix->feed_compatible() ) {
214
					$fix->apply( $textnode, $settings, $is_title );
215
				}
216
			}
217
		}
218
	}
219
220
	/**
221
	 * Parse HTML5 fragment while ignoring certain warnings for invalid HTML code (e.g. duplicate IDs).
222
	 *
223
	 * @param \Masterminds\HTML5 $parser   An intialized parser object.
224
	 * @param string             $html     The HTML fragment to parse (not a complete document).
225
	 * @param Settings           $settings The settings to apply.
226
	 *
227
	 * @return \DOMDocument|null The encoding has already been set to UTF-8. Returns null if there were parsing errors.
228
	 */
229
	public function parse_html( \Masterminds\HTML5 $parser, $html, Settings $settings ) {
230
		// Silence some parsing errors for invalid HTML.
231
		set_error_handler( [ $this, 'handle_parsing_errors' ] ); // @codingStandardsIgnoreLine
232
		$xml_error_handling = libxml_use_internal_errors( true );
233
234
		// Do the actual parsing.
235
		$dom           = $parser->loadHTML( '<!DOCTYPE html><html><body>' . $html . '</body></html>' );
236
		$dom->encoding = 'UTF-8';
237
238
		// Restore original error handling.
239
		libxml_clear_errors();
240
		libxml_use_internal_errors( $xml_error_handling );
241
		restore_error_handler();
242
243
		// Handle any parser errors.
244
		$errors = $parser->getErrors();
245
		if ( ! empty( $settings['parserErrorsHandler'] ) && ! empty( $errors ) ) {
246
			$errors = call_user_func( $settings['parserErrorsHandler'], $errors );
247
		}
248
249
		// Return null if there are still unhandled parsing errors.
250
		if ( ! empty( $errors ) && ! $settings['parserErrorsIgnore'] ) {
251
			$dom = null;
252
		}
253
254
		return $dom;
255
	}
256
257
	/**
258
	 * Silently handle certain HTML parsing errors.
259
	 *
260
	 * @param int    $errno      Error number.
261
	 * @param string $errstr     Error message.
262
	 * @param string $errfile    The file in which the error occurred.
263
	 * @param int    $errline    The line in which the error occurred.
264
	 * @param array  $errcontext Calling context.
265
	 *
266
	 * @return boolean Returns true if the error was handled, false otherwise.
267
	 */
268
	public function handle_parsing_errors( $errno, $errstr, $errfile, $errline, array $errcontext ) {
0 ignored issues
show
Unused Code introduced by
The parameter $errline is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $errcontext is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
269
		if ( ! ( error_reporting() & $errno ) ) { // @codingStandardsIgnoreLine.
270
			return true; // not interesting.
271
		}
272
273
		// Ignore warnings from parser & let PHP handle the rest.
274
		return $errno & E_USER_WARNING && 0 === substr_compare( $errfile, 'DOMTreeBuilder.php', -18 );
275
	}
276
277
	/**
278
	 * Retrieves an array of nodes that should be skipped during processing.
279
	 *
280
	 * @param \DOMXPath $xpath        A valid XPath instance for the DOM to be queried.
281
	 * @param \DOMNode  $initial_node The starting node of the XPath query.
282
	 * @param Settings  $settings     The settings to apply.
283
	 *
284
	 * @return \DOMNode[] An array of \DOMNode (can be empty).
285
	 */
286
	public function query_tags_to_ignore( \DOMXPath $xpath, \DOMNode $initial_node, Settings $settings ) {
287
		$elements    = [];
288
		$query_parts = [];
289 View Code Duplication
		if ( ! empty( $settings['ignoreTags'] ) ) {
290
			$query_parts[] = '//' . implode( ' | //', $settings['ignoreTags'] );
291
		}
292 View Code Duplication
		if ( ! empty( $settings['ignoreClasses'] ) ) {
293
			$query_parts[] = "//*[contains(concat(' ', @class, ' '), ' " . implode( " ') or contains(concat(' ', @class, ' '), ' ", $settings['ignoreClasses'] ) . " ')]";
294
		}
295 View Code Duplication
		if ( ! empty( $settings['ignoreIDs'] ) ) {
296
			$query_parts[] = '//*[@id=\'' . implode( '\' or @id=\'', $settings['ignoreIDs'] ) . '\']';
297
		}
298
299 View Code Duplication
		if ( ! empty( $query_parts ) ) {
300
			$ignore_query = implode( ' | ', $query_parts );
301
302
			$nodelist = $xpath->query( $ignore_query, $initial_node );
303
			if ( false !== $nodelist ) {
304
				$elements = DOM::nodelist_to_array( $nodelist );
305
			}
306
		}
307
308
		return $elements;
309
	}
310
311
	/**
312
	 * Replaces the given node with HTML content. Uses the HTML5 parser.
313
	 *
314
	 * @param \DOMNode $node    The node to replace.
315
	 * @param string   $content The HTML fragment used to replace the node.
316
	 *
317
	 * @return \DOMNode|array An array of \DOMNode containing the new nodes or the old \DOMNode if the replacement failed.
318
	 */
319
	public function replace_node_with_html( \DOMNode $node, $content ) {
320
		$result = $node;
321
322
		$parent = $node->parentNode;
323
		if ( empty( $parent ) ) {
324
			return $node; // abort early to save cycles.
325
		}
326
327
		set_error_handler( [ $this, 'handle_parsing_errors' ] ); // @codingStandardsIgnoreLine.
328
329
		$html_fragment = $this->get_html5_parser()->loadHTMLFragment( $content );
330
		if ( ! empty( $html_fragment ) ) {
331
			$imported_fragment = $node->ownerDocument->importNode( $html_fragment, true );
332
333 View Code Duplication
			if ( ! empty( $imported_fragment ) ) {
334
				// Save the children of the imported DOMDocumentFragment before replacement.
335
				$children = DOM::nodelist_to_array( $imported_fragment->childNodes );
336
337
				if ( false !== $parent->replaceChild( $imported_fragment, $node ) ) {
338
					// Success! We return the saved array of DOMNodes as
339
					// $imported_fragment is just an empty DOMDocumentFragment now.
340
					$result = $children;
341
				}
342
			}
343
		}
344
345
		restore_error_handler();
346
347
		return $result;
348
	}
349
350
	/**
351
	 * Retrieves the fix registry.
352
	 *
353
	 * @return Registry
354
	 */
355
	public function get_registry() {
356
		if ( ! isset( $this->registry ) ) {
357
			$this->registry = Registry::create( $this->get_hyphenator_cache() );
358
		} elseif ( $this->update_registry_cache ) {
359
			$this->registry->update_hyphenator_cache( $this->get_hyphenator_cache() );
360
			$this->update_registry_cache = false;
361
		}
362
363
		return $this->registry;
364
	}
365
366
	/**
367
	 * Retrieves the HTML5 parser instance.
368
	 *
369
	 * @return \Masterminds\HTML5
370
	 */
371
	public function get_html5_parser() {
372
		// Lazy-load HTML5 parser.
373
		if ( ! isset( $this->html5_parser ) ) {
374
			$this->html5_parser = new \Masterminds\HTML5( [
375
				'disable_html_ns' => true,
376
			] );
377
		}
378
379
		return $this->html5_parser;
380
	}
381
382
	/**
383
	 * Retrieves the hyphenator cache.
384
	 *
385
	 * @return Hyphenator\Cache
386
	 */
387
	public function get_hyphenator_cache() {
388
		if ( ! isset( $this->hyphenator_cache ) ) {
389
			$this->hyphenator_cache = new Hyphenator\Cache();
390
		}
391
392
		return $this->hyphenator_cache;
393
	}
394
395
	/**
396
	 * Injects an existing Hyphenator\Cache (to facilitate persistent language caching).
397
	 *
398
	 * @param Hyphenator\Cache $cache A hyphenator cache instance.
399
	 */
400
	public function set_hyphenator_cache( Hyphenator\Cache $cache ) {
401
		$this->hyphenator_cache = $cache;
402
403
		// Change hyphenator cache for existing token fixes.
404
		if ( isset( $this->registry ) ) {
405
			$this->registry->update_hyphenator_cache( $cache );
406
		}
407
	}
408
409
	/**
410
	 * Retrieves the list of valid language plugins in the given directory.
411
	 *
412
	 * @param string $path The path in which to look for language plugin files.
413
	 *
414
	 * @return string[] An array in the form ( $language_code => $language_name ).
415
	 */
416
	private static function get_language_plugin_list( $path ) {
417
		$language_name_pattern = '/"language"\s*:\s*((".+")|(\'.+\'))\s*,/';
418
		$languages             = [];
419
		$handle                = opendir( $path );
420
421
		// Read all files in directory.
422
		$file = readdir( $handle );
423
		while ( $file ) {
424
			// We only want the JSON files.
425
			if ( '.json' === substr( $file, -5 ) ) {
426
				$file_content = file_get_contents( $path . $file );
427
				if ( preg_match( $language_name_pattern, $file_content, $matches ) ) {
428
					$language_name = substr( $matches[1], 1, -1 );
429
					$language_code = substr( $file, 0, -5 );
430
431
					$languages[ $language_code ] = $language_name;
432
				}
433
			}
434
435
			// Read next file.
436
			$file = readdir( $handle );
437
		}
438
		closedir( $handle );
439
440
		// Sort translated language names according to current locale.
441
		asort( $languages );
442
443
		return $languages;
444
	}
445
446
	/**
447
	 * Retrieves the list of valid hyphenation languages.
448
	 *
449
	 * Note that this method reads all the language files on disc, so you should
450
	 * cache the results if possible.
451
	 *
452
	 * @return string[] An array in the form of ( LANG_CODE => LANGUAGE ).
453
	 */
454
	public static function get_hyphenation_languages() {
455
		return self::get_language_plugin_list( __DIR__ . '/lang/' );
456
	}
457
458
	/**
459
	 * Retrieves the list of valid diacritic replacement languages.
460
	 *
461
	 * Note that this method reads all the language files on disc, so you should
462
	 * cache the results if possible.
463
	 *
464
	 * @return string[] An array in the form of ( LANG_CODE => LANGUAGE ).
465
	 */
466
	public static function get_diacritic_languages() {
467
		return self::get_language_plugin_list( __DIR__ . '/diacritics/' );
468
	}
469
}
470