• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xemlock / htmlpurifier-html5 / 29918157479

22 Jul 2026 12:04PM UTC coverage: 97.808% (-1.5%) from 99.277%
29918157479

Pull #91

github

web-flow
Merge 64af7ddee into 13602d8c5
Pull Request #91: Add deeper srcset attribute support

86 of 112 new or added lines in 2 files covered. (76.79%)

1606 of 1642 relevant lines covered (97.81%)

3943.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.34
/library/HTMLPurifier/AttrDef/HTML5/Srcset.php
1
<?php
2

3
class HTMLPurifier_AttrDef_HTML5_Srcset extends HTMLPurifier_AttrDef
4
{
5
    public function validate($string, $config, $context)
6
    {
7
        $sources = $this->parseImageSources($string);
338✔
8
        if (empty($sources)) {
338✔
9
            return false;
39✔
10
        }
11

12
        $uriFilter = new HTMLPurifier_AttrDef_URI(true);
299✔
13

14
        $filtered = array();
299✔
15
        foreach ($sources as $source) {
299✔
16
            $uri = $source['uri'];
299✔
17
            $descriptor = $source['descriptor'];
299✔
18
            $validatedUri = $uriFilter->validate($uri, $config, $context);
299✔
19
            if (is_string($validatedUri)) {
299✔
20
                if ($descriptor) {
247✔
21
                    $filtered[] = $validatedUri . ' ' . $source['descriptor'];
208✔
22
                } else {
32✔
23
                    $filtered[] = $validatedUri;
79✔
24
                }
25
            }
38✔
26
        }
46✔
27

28
        if (empty($filtered)) {
299✔
29
            return false;
52✔
30
        }
31

32
        return implode(', ', $filtered);
247✔
33
    }
34

35
    /**
36
     * Parse the image source from srcset attribute text.
37
     * Returns false if it's found to be invalid, otherwise
38
     * returns an array of uri and descriptor combinations.
39
     *
40
     * This aims to follow the WHATWG parsing spec as per:
41
     * https://html.spec.whatwg.org/multipage/images.html#parsing-a-srcset-attribute
42
     *
43
     * @param string $string
44
     * @return array{uri: string, descriptor: string}[]|false
45
     */
46
    private function parseImageSources($string)
47
    {
48
        $imageSources = array();
338✔
49
        $asciiWhitespace = " \n\r\t\f";
338✔
50
        $asciiWhiteSpaceComma = $asciiWhitespace . ',';
338✔
51
        $input = trim($string, $asciiWhiteSpaceComma);
338✔
52

53
        if ($input === "") {
338✔
54
            return false;
13✔
55
        }
56

57
        $position = 0;
325✔
58
        while ($position < strlen($input)) {
325✔
59
            $position += strspn($input, $asciiWhitespace, $position);
325✔
60
            $urlEnd = $position + strcspn($input, $asciiWhitespace, $position);
325✔
61
            $url = substr($input, $position, $urlEnd - $position);
325✔
62
            $position = $urlEnd;
325✔
63
            $descriptors = array();
325✔
64

65
            if (strpos($url, ',') === strlen($url) - 1) {
325✔
66
                $url = rtrim($url, ',');
26✔
67
            } else {
4✔
68
                $position += strspn($input, $asciiWhitespace, $position);
325✔
69
                $currentDescriptor = '';
325✔
70
                $state = 'in_descriptor';
325✔
71
                while (true) {
325✔
72
                    if ($position < strlen($input)) {
325✔
73
                        $c = $input[$position];
286✔
74
                    } else {
44✔
75
                        $c = null;
325✔
76
                    }
77

78
                    if ($state === 'in_descriptor') {
325✔
79
                        if ($c !== null && strpos($asciiWhitespace, $c) !== false) {
325✔
NEW
80
                            if ($currentDescriptor !== '') {
×
NEW
81
                                $descriptors[] = $currentDescriptor;
×
82
                            }
NEW
83
                            $state = 'after_descriptor';
×
84
                        } else if ($c === ',') {
325✔
85
                            $position++;
91✔
86
                            if ($currentDescriptor !== '') {
91✔
87
                                $descriptors[] = $currentDescriptor;
91✔
88
                            }
14✔
89
                            break;
91✔
90
                        } else if ($c === '(') {
325✔
NEW
91
                            $currentDescriptor .= $c;
×
NEW
92
                            $state = 'in_parens';
×
93
                        } else if ($c === null) {
325✔
94
                            if ($currentDescriptor !== '') {
325✔
95
                                $descriptors[] = $currentDescriptor;
286✔
96
                            }
44✔
97
                            break;
325✔
98
                        } else {
99
                            $currentDescriptor .= $c;
286✔
100
                        }
NEW
101
                    } else if ($state === 'in_parens') {
44✔
NEW
102
                        if ($c === ')') {
×
NEW
103
                            $currentDescriptor .= $c;
×
NEW
104
                            $state = 'in_descriptor';
×
NEW
105
                        } else if ($c === null) {
×
NEW
106
                            $descriptors[] = $currentDescriptor;
×
NEW
107
                            break;
×
108
                        } else {
NEW
109
                            $currentDescriptor .= $c;
×
110
                        }
NEW
111
                    } else if ($state === 'after_descriptor') {
×
NEW
112
                        if ($c !== null && strpos($asciiWhitespace, $c) !== false) {
×
113
                            // Stay in this state
NEW
114
                        } else if ($c === null) {
×
NEW
115
                            break;
×
116
                        } else {
NEW
117
                            $state = 'in_descriptor';
×
NEW
118
                            $position--;
×
119
                        }
120
                    }
121

122
                    $position++;
286✔
123
                }
44✔
124
            }
125

126
            $descriptor = $this->formatDescriptor($descriptors);
325✔
127

128
            if ($url && $descriptor !== false) {
325✔
129
                $imageSources[] = array(
299✔
130
                    'uri' => $url,
299✔
131
                    'descriptor' => $descriptor,
299✔
132
                );
184✔
133
            }
46✔
134
        }
50✔
135

136
        return $imageSources;
325✔
137
    }
138

139
    /**
140
     * Parse and format a single descriptor from an array of potential
141
     * descriptor strings. Returns empty if valid but no descriptor.
142
     * Returns false if invalid.
143
     * @param string[] $descriptors
144
     * @return false|string
145
     */
146
    private function formatDescriptor(array $descriptors)
147
    {
148
        $error = false;
325✔
149
        $width = '';
325✔
150
        $density = '';
325✔
151
        $futureCompatH = '';
325✔
152

153
        foreach ($descriptors as $descriptor) {
325✔
154
            $descriptor = trim($descriptor);
286✔
155
            if ($descriptor === '') {
286✔
NEW
156
                continue;
×
157
            }
158

159
            $unit = $descriptor[strlen($descriptor) - 1];
286✔
160
            $number = trim(substr($descriptor, 0, -1));
286✔
161

162
            if ($unit === 'w' && filter_var($number, FILTER_VALIDATE_INT) && intval($number) >= 0) {
286✔
163
                if (!empty($width) || !empty($density) || intval($number) === 0) {
39✔
NEW
164
                    $error = true;
×
165
                }
166
                $width = $number;
39✔
167
            } else if ($unit === 'x' && filter_var($number, FILTER_VALIDATE_FLOAT)) {
275✔
168
                if (!empty($width) || !empty($density) || !empty($futureCompatH) || floatval($number) < 0) {
247✔
NEW
169
                    $error = true;
×
170
                }
171
                $density = $number;
247✔
172
            } else if ($unit === 'h' && filter_var($number, FILTER_VALIDATE_INT) && intval($number) >= 0) {
75✔
NEW
173
                if (!empty($futureCompatH) || !empty($density)) {
×
NEW
174
                    $error = true;
×
175
                }
NEW
176
                $futureCompatH = $number;
×
177
            } else {
178
                $error = true;
77✔
179
            }
180
        }
50✔
181

182
        if (!empty($futureCompatH) && empty($width)) {
325✔
NEW
183
            $error = true;
×
184
        }
185

186
        if ($error) {
325✔
187
            return false;
39✔
188
        }
189

190
        if ($width) {
299✔
191
            return $width . 'w';
39✔
192
        }
193

194
        if ($density) {
286✔
195
            return $density . 'x';
247✔
196
        }
197

198
        return '';
78✔
199
    }
200
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc