Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
21 / 28
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Podcast_Feed_Locator
80.77% covered (warning)
80.77%
21 / 26
60.00% covered (warning)
60.00%
3 / 5
19.06
0.00% covered (danger)
0.00%
0 / 1
 is_feed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_podcast_feed
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 safely_load_xml
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
7.05
 has_itunes_ns
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 has_audio_enclosures
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Extension of the SimplePie\Locator class, to detect podcast feeds
4 *
5 * @package automattic/jetpack
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9    exit( 0 );
10}
11
12/**
13 * Class Jetpack_Podcast_Feed_Locator
14 */
15class Jetpack_Podcast_Feed_Locator extends SimplePie\Locator {
16    /**
17     * Overrides the locator is_feed function to check for
18     * appropriate podcast elements.
19     *
20     * @param SimplePie\File $file The file being checked.
21     * @param boolean        $check_html Adds text/html to the mimetypes checked.
22     */
23    public function is_feed( $file, $check_html = false ) {
24        return parent::is_feed( $file, $check_html ) && $this->is_podcast_feed( $file );
25    }
26
27    /**
28     * Checks the contents of the file for elements that make
29     * it a podcast feed.
30     *
31     * @param SimplePie\File $file The file being checked.
32     */
33    private function is_podcast_feed( $file ) {
34        // If we can't read the DOM assume it's a podcast feed, we'll work
35        // it out later.
36        if ( ! class_exists( 'DOMDocument' ) ) {
37            return true;
38        }
39
40        $feed_dom = $this->safely_load_xml( (string) $file->body );
41
42        // Do this as either/or but prioritise the itunes namespace. It's pretty likely
43        // that it's a podcast feed we've found if that namespace is present.
44        return $feed_dom && $this->has_itunes_ns( $feed_dom ) && $this->has_audio_enclosures( $feed_dom );
45    }
46
47    /**
48     * Safely loads an XML file
49     *
50     * @param string $xml A string of XML to load.
51     * @return DOMDocument|false A resulting DOM document or `false` if there is an error.
52     */
53    private function safely_load_xml( $xml ) {
54        if ( empty( $xml ) ) {
55            return false;
56        }
57
58        $disable_entity_loader = PHP_VERSION_ID < 80000;
59
60        if ( $disable_entity_loader ) {
61            // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading
62            // is disabled by default, so this function is no longer needed to protect against XXE attacks.
63            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
64            $loader = libxml_disable_entity_loader( true );
65        }
66
67        $errors = libxml_use_internal_errors( true );
68
69        $return = new DOMDocument();
70        if ( ! $return->loadXML( $xml ) ) {
71            return false;
72        }
73
74        libxml_use_internal_errors( $errors );
75
76        if ( $disable_entity_loader && isset( $loader ) ) {
77            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
78            libxml_disable_entity_loader( $loader );
79        }
80
81        return $return;
82    }
83
84    /**
85     * Checks the RSS feed for the presence of the itunes podcast namespace.
86     * It's pretty loose and just checks the URI for itunes.com
87     *
88     * @param DOMDocument $dom The XML document to check.
89     * @return boolean Whether the itunes namespace is defined.
90     */
91    private function has_itunes_ns( $dom ) {
92        $xpath = new DOMXPath( $dom );
93        foreach ( $xpath->query( 'namespace::*' ) as $node ) {
94            // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
95            // nodeValue is not valid, but it's part of the DOM API that we don't control.
96            if ( strstr( $node->nodeValue, 'itunes.com' ) ) {
97                return true;
98            }
99            // phpcs:enable
100        }
101        return false;
102    }
103
104    /**
105     * Checks the RSS feed for the presence of enclosures with an audio mimetype.
106     *
107     * @param DOMDocument $dom The XML document to check.
108     * @return boolean Whether enclosures were found.
109     */
110    private function has_audio_enclosures( $dom ) {
111        $xpath      = new DOMXPath( $dom );
112        $enclosures = $xpath->query( "//enclosure[starts-with(@type,'audio/')]" );
113        return ! $enclosures ? false : $enclosures->length > 0;
114    }
115}