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

xemlock / htmlpurifier-html5 / 16898969866

12 Aug 2025 04:28AM UTC coverage: 99.276%. Remained the same
16898969866

Pull #87

github

web-flow
Bump actions/checkout from 4 to 5

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #87: Bump actions/checkout from 4 to 5

1508 of 1519 relevant lines covered (99.28%)

3883.48 hits per line

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

100.0
/library/HTMLPurifier/AttrDef/HTML5/Datetime.php
1
<?php
2

3
/**
4
 * Validates HTML5 date and time strings according to spec
5
 * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates-and-times
6
 *
7
 * This validator tries to parse as much data as possible and then tries to
8
 * render it in the desired format. It fails if either no datetime data can
9
 * be extracted from the input, or the extracted data is insufficient for
10
 * the desired format (with the exception of DatetimeGlobal format, which
11
 * uses server timezone offset if none is detected).
12
 */
13
class HTMLPurifier_AttrDef_HTML5_Datetime extends HTMLPurifier_AttrDef
14
{
15
    const REGEX = '/^
16
        (
17
            (?P<year>\d{4,})
18
            (
19
                -
20
                (?P<month>[01]\d)
21
                (
22
                    -
23
                    (?P<day>[0-3]\d)
24
                )?
25
            )?
26
        )?
27
        (
28
            (^|(\s+|T))
29
            (?P<hour>[0-2]\d)
30
            :
31
            (?P<minute>[0-5]\d)
32
            (
33
                :
34
                (?P<second>[0-5]\d(\.\d+)?)
35
            )?
36
        )?
37
        (
38
            (?P<tzZulu>Z)
39
            |
40
            (
41
                (?P<tzHour>[+-][0-2]\d)
42
                :?
43
                (?P<tzMinute>[0-5]\d)
44
            )
45
        )?
46
    $/xi';
47

48
    /**
49
     * Lookup table for supported formats and if they are enabled by default
50
     * @var array
51
     */
52
    protected static $formats = array(
53
        'Datetime'       => true,
54
        'DatetimeGlobal' => false,
55
        'DatetimeLocal'  => false,
56
        'Date'           => true,
57
        'Month'          => true,
58
        'Year'           => true,
59
        'Time'           => true,
60
        'TimezoneOffset' => true,
61
    );
62

63
    /**
64
     * Lookup table for allowed formats
65
     * @var array
66
     */
67
    protected $allowedFormats = array();
68

69
    /**
70
     * @param array $allowedFormats OPTIONAL
71
     * @throws HTMLPurifier_Exception If an invalid format is provided
72
     */
73
    public function __construct(array $allowedFormats = array())
74
    {
75
        // Validate allowed formats
76
        $allowedFormatsLookup = array();
12,864✔
77
        foreach ($allowedFormats as $format) {
12,864✔
78
            if (!isset(self::$formats[$format])) {
12,084✔
79
                throw new HTMLPurifier_Exception("'$format' is not a valid format");
12✔
80
            }
81
            $allowedFormatsLookup[$format] = true;
12,072✔
82
        }
2,142✔
83

84
        // Formats must be set in the same order as in self::$formats, so that
85
        // in default mode the result will be the longest matching format
86
        foreach (self::$formats as $format => $_) {
12,852✔
87
            if (isset($allowedFormatsLookup[$format])) {
12,852✔
88
                $this->allowedFormats[$format] = true;
12,202✔
89
            }
2,012✔
90
        }
2,142✔
91

92
        if (empty($this->allowedFormats)) {
12,852✔
93
            $this->allowedFormats = array_filter(self::$formats, 'intval');
11,124✔
94
        }
1,854✔
95
    }
5,355✔
96

97
    /**
98
     * @param string $string
99
     * @param HTMLPurifier_Config $config
100
     * @param HTMLPurifier_Context $context
101
     * @return bool|string
102
     */
103
    public function validate($string, $config, $context)
104
    {
105
        if (($data = $this->parse($string)) === false) {
4,140✔
106
            return false;
1,080✔
107
        }
108
        return $this->format($data);
3,156✔
109
    }
110

111
    /**
112
     * @param string $string
113
     * @return array|bool
114
     */
115
    public function parse($string)
116
    {
117
        $string = $this->parseCDATA($string);
4,140✔
118

119
        if ($string === '' || !preg_match(self::REGEX, $string, $match)) {
4,140✔
120
            return false;
696✔
121
        }
122

123
        // Make sure all named patterns are present in the match array
124
        $match += array(
2,065✔
125
            'year'     => '',
3,540✔
126
            'month'    => '',
2,655✔
127
            'day'      => '',
2,655✔
128
            'hour'     => '',
2,655✔
129
            'minute'   => '',
2,655✔
130
            'second'   => '',
2,655✔
131
            'tzZulu'   => '',
2,655✔
132
            'tzHour'   => '',
2,655✔
133
            'tzMinute' => '',
2,655✔
134
        );
2,065✔
135

136
        $year = $month = $day = null;
3,540✔
137

138
        if ($match['year'] !== '') {
3,540✔
139
            $year = (int) $match['year'];
2,832✔
140

141
            // Dates before the year one can't be represented as a datetime in HTML5
142
            if ($year <= 0) {
2,832✔
143
                return false;
84✔
144
            }
145

146
            if ($match['month'] !== '') {
2,748✔
147
                $month = (int) $match['month'];
2,568✔
148
                if ($month < 1 || $month > 12) {
2,568✔
149
                    return false;
24✔
150
                }
151

152
                if ($match['day'] !== '') {
2,544✔
153
                    $day = (int) $match['day'];
2,364✔
154
                    if (!checkdate($month, $day, $year)) {
2,364✔
155
                        return false;
96✔
156
                    }
157
                }
378✔
158
            }
408✔
159
        }
438✔
160

161
        $hour = $minute = $second = null;
3,336✔
162

163
        if ($match['hour'] !== '') {
3,336✔
164
            $hour = (int) $match['hour'];
2,352✔
165
            $minute = (int) $match['minute'];
2,352✔
166
            $second = $match['second'] !== '' ? (float) $match['second'] : null;
2,352✔
167

168
            if ($hour > 23) {
2,352✔
169
                return false;
120✔
170
            }
171
        }
372✔
172

173
        $tzHour = $tzMinute = null;
3,216✔
174

175
        if ($match['tzZulu'] !== '') {
3,216✔
176
            $tzHour = 'Z';
492✔
177

178
        } elseif ($match['tzHour'] !== '') {
2,816✔
179
            $tzHour = (int) $match['tzHour'];
1,308✔
180
            $tzMinute = (int) $match['tzMinute'];
1,308✔
181

182
            if ($tzHour < -23 || $tzHour > 23) {
1,308✔
183
                return false;
60✔
184
            }
185
        }
208✔
186

187
        return compact(
3,156✔
188
            'year', 'month', 'day', 'hour', 'minute', 'second', 'tzHour', 'tzMinute'
3,156✔
189
        );
2,367✔
190
    }
191

192
    /**
193
     * @param array $data
194
     * @return bool|string
195
     */
196
    protected function format(array $data)
197
    {
198
        foreach ($this->allowedFormats as $format => $_) {
3,156✔
199
            if (($result = call_user_func(array($this, 'format' . $format), $data)) !== false) {
3,156✔
200
                return $result;
2,986✔
201
            }
202
        }
152✔
203
        return false;
204✔
204
    }
205

206
    /**
207
     * @param array $data
208
     * @return bool|string
209
     */
210
    protected function formatYear(array $data)
211
    {
212
        if (($year = $data['year']) === null) {
516✔
213
            return false;
348✔
214
        }
215
        return sprintf('%04d', $year);
168✔
216
    }
217

218
    /**
219
     * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#months
220
     * @param array $data
221
     * @return bool|string
222
     */
223
    protected function formatMonth(array $data)
224
    {
225
        if (($year = $data['year']) === null ||
576✔
226
            ($month = $data['month']) === null
518✔
227
        ) {
96✔
228
            return false;
432✔
229
        }
230
        return sprintf('%04d-%02d', $year, $month);
144✔
231
    }
232

233
    /**
234
     * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates
235
     * @param array $data
236
     * @return bool|string
237
     */
238
    protected function formatDate(array $data)
239
    {
240
        if (($year = $data['year']) === null ||
2,748✔
241
            ($month = $data['month']) === null ||
2,352✔
242
            ($day = $data['day']) === null
2,658✔
243
        ) {
458✔
244
            return false;
684✔
245
        }
246
        return sprintf('%04d-%02d-%02d', $year, $month, $day);
2,076✔
247
    }
248

249
    /**
250
     * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#times
251
     * @param array $data
252
     * @return bool|string
253
     */
254
    protected function formatTime(array $data)
255
    {
256
        if (($hour = $data['hour']) === null ||
2,508✔
257
            ($minute = $data['minute']) === null
2,440✔
258
        ) {
418✔
259
            return false;
420✔
260
        }
261
        $time = sprintf('%02d:%02d', $hour, $minute);
2,100✔
262
        if (($second = $data['second']) !== null) {
2,100✔
263
            $sec = (int) $second;
1,380✔
264
            $time .= sprintf(':%02d', $sec);
1,380✔
265

266
            $msec = round(($second - $sec) * 1000);
1,380✔
267
            if ($msec > 0) {
1,380✔
268
                $time .= rtrim(sprintf('.%03d', $msec), '0');
672✔
269
            }
112✔
270
        }
230✔
271
        return $time;
2,100✔
272
    }
273

274
    /**
275
     * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#local-dates-and-times
276
     * @param array $data
277
     * @return bool|string
278
     */
279
    protected function formatDatetimeLocal(array $data)
280
    {
281
        if (($date = $this->formatDate($data)) === false ||
2,688✔
282
            ($time = $this->formatTime($data)) === false
2,576✔
283
        ) {
448✔
284
            return false;
912✔
285
        }
286
        // Use 'T' as normalized date/time separator, see:
287
        // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-normalised-local-date-and-time-string
288
        // Also it's the only separator recognized by input[type=datetime-local]
289
        return $date . 'T' . $time;
1,800✔
290
    }
291

292
    /**
293
     * Formats data as datetime with timezone offset. If no timezone offset
294
     * is present, the default server timezone offset is used.
295
     *
296
     * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#global-dates-and-times
297
     * @param array $data
298
     * @return bool|string
299
     */
300
    protected function formatDatetimeGlobal(array $data)
301
    {
302
        if (($datetime = $this->formatDatetimeLocal($data)) === false) {
504✔
303
            return false;
168✔
304
        }
305
        if (($timezoneOffset = $this->formatTimezoneOffset($data)) === false) {
348✔
306
            $timezoneOffset = date('P');
48✔
307
        }
8✔
308
        return $datetime . $timezoneOffset;
348✔
309
    }
310

311
    /**
312
     * Formats data as datetime with optional timezone offset.
313
     *
314
     * This is used in particular for 'datetime' attribute of <time> element.
315
     *
316
     * @param array $data
317
     * @return bool|string
318
     */
319
    protected function formatDatetime(array $data)
320
    {
321
        if (($datetime = $this->formatDatetimeLocal($data)) === false) {
2,004✔
322
            return false;
708✔
323
        }
324
        if (($timezoneOffset = $this->formatTimezoneOffset($data)) !== false) {
1,308✔
325
            $datetime .= $timezoneOffset;
1,008✔
326
        }
168✔
327
        return $datetime;
1,308✔
328
    }
329

330
    /**
331
     * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#time-zones
332
     * @param array $data
333
     * @return string
334
     */
335
    protected function formatTimezoneOffset(array $data)
336
    {
337
        if (($tzHour = $data['tzHour']) === null) {
1,956✔
338
            return false;
348✔
339
        }
340

341
        if ($tzHour === 'Z') {
1,608✔
342
            $tzOffset = 'Z';
492✔
343
        } else {
82✔
344
            $tzMinute = (int) $data['tzMinute'];
1,116✔
345
            $tzOffset = sprintf('%s%02d:%02d', $tzHour < 0 ? '-' : '+', abs($tzHour), $tzMinute);
1,116✔
346
        }
347

348
        return $tzOffset;
1,608✔
349
    }
350

351
    /**
352
     * @param string $formats
353
     * @return HTMLPurifier_AttrDef_HTML5_Datetime
354
     * @throws HTMLPurifier_Exception If an invalid format is provided
355
     */
356
    public function make($formats)
357
    {
358
        return new self(explode(',', $formats));
12✔
359
    }
360
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc