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