Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.48% covered (danger)
40.48%
17 / 42
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
YamlFormat
40.48% covered (danger)
40.48%
17 / 42
37.50% covered (danger)
37.50%
3 / 8
95.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 decode
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 isParserAvailable
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 parseWith
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 parseWithPhp
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 parseWithSymfony
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 supportsFileExtension
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Settings\Source\Format;
4
5use InvalidArgumentException;
6use LogicException;
7use Symfony\Component\Yaml\Exception\ParseException;
8use Symfony\Component\Yaml\Yaml;
9use UnexpectedValueException;
10use Wikimedia\AtEase\AtEase;
11
12class YamlFormat implements SettingsFormat {
13
14    public const PARSER_PHP_YAML = 'php-yaml';
15
16    public const PARSER_SYMFONY = 'symfony';
17
18    /** @var string[] */
19    private $useParsers;
20
21    /**
22     * @param string[] $useParsers which parsers to try in order.
23     */
24    public function __construct( array $useParsers = [ self::PARSER_PHP_YAML, self::PARSER_SYMFONY ] ) {
25        $this->useParsers = $useParsers;
26    }
27
28    public function decode( string $data ): array {
29        foreach ( $this->useParsers as $parser ) {
30            if ( self::isParserAvailable( $parser ) ) {
31                return $this->parseWith( $parser, $data );
32            }
33        }
34        throw new LogicException( 'No parser available' );
35    }
36
37    /**
38     * Check whether a specific YAML parser is available.
39     *
40     * @param string $parser one of the PARSER_* constants.
41     * @return bool
42     */
43    public static function isParserAvailable( string $parser ): bool {
44        switch ( $parser ) {
45            case self::PARSER_PHP_YAML:
46                return function_exists( 'yaml_parse' );
47            case self::PARSER_SYMFONY:
48                return true;
49            default:
50                throw new InvalidArgumentException( 'Unknown parser: ' . $parser );
51        }
52    }
53
54    /**
55     * @param string $parser
56     * @param string $data
57     * @return array
58     */
59    private function parseWith( string $parser, string $data ): array {
60        switch ( $parser ) {
61            case self::PARSER_PHP_YAML:
62                return $this->parseWithPhp( $data );
63            case self::PARSER_SYMFONY:
64                return $this->parseWithSymfony( $data );
65            default:
66                throw new InvalidArgumentException( 'Unknown parser: ' . $parser );
67        }
68    }
69
70    private function parseWithPhp( string $data ): array {
71        $previousValue = ini_set( 'yaml.decode_php', false );
72        try {
73            $ndocs = 0;
74            $result = AtEase::quietCall(
75                'yaml_parse',
76                $data,
77                0,
78                $ndocs,
79                [
80                    /**
81                     * Crash if provided YAML has PHP constants in it.
82                     * We do not want to support that.
83                     *
84                     * @return never
85                     */
86                    '!php/const' => static function () {
87                        throw new UnexpectedValueException(
88                            'PHP constants are not supported'
89                        );
90                    },
91                ]
92            );
93            if ( $result === false ) {
94                throw new UnexpectedValueException( 'Failed to parse YAML' );
95            }
96            return $result;
97        } finally {
98            ini_set( 'yaml.decode_php', $previousValue );
99        }
100    }
101
102    private function parseWithSymfony( string $data ): array {
103        try {
104            return Yaml::parse( $data, Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE );
105        } catch ( ParseException $e ) {
106            throw new UnexpectedValueException(
107                'Failed to parse YAML ' . $e->getMessage()
108            );
109        }
110    }
111
112    public static function supportsFileExtension( string $ext ): bool {
113        $ext = strtolower( $ext );
114        return $ext === 'yml' || $ext === 'yaml';
115    }
116
117    public function __toString() {
118        return 'YAML';
119    }
120}