Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
24 / 32
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Podcast_Feed_Locator
80.00% covered (warning)
80.00%
24 / 30
60.00% covered (warning)
60.00%
3 / 5
21.89
0.00% covered (danger)
0.00%
0 / 1
 is_feed
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 is_podcast_feed
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
5.93
 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    // @todo WordPress 6.9 changed `$file` from class `SimplePie\File` to interface `SimplePie\HTTP\Response` (which is implemented by `SimplePie\File`). Update declaration once we drop support for WordPress <6.9.
18    /**
19     * Overrides the locator is_feed function to check for
20     * appropriate podcast elements.
21     *
22     * @param SimplePie\File|SimplePie\HTTP\Response $file The file being checked.
23     * @param boolean                                $check_html Adds text/html to the mimetypes checked.
24     */
25    public function is_feed( $file, $check_html = false ) {
26        // @phan-suppress-previous-line PhanUndeclaredTypeParameter @phan-suppress-current-line UnusedSuppression -- Suppression for the WP 6.8 run. @todo Remove this line when we drop WP <6.9.
27        return parent::is_feed( $file, $check_html ) &&
28            $file instanceof SimplePie\File &&
29            $this->is_podcast_feed( $file );
30    }
31
32    /**
33     * Checks the contents of the file for elements that make
34     * it a podcast feed.
35     *
36     * @param SimplePie\File $file The file being checked.
37     */
38    private function is_podcast_feed( $file ) {
39        // If we can't read the DOM assume it's a podcast feed, we'll work
40        // it out later.
41        if ( ! class_exists( 'DOMDocument' ) ) {
42            return true;
43        }
44
45        // @todo Drop is_callable check once we drop support for WordPress <6.9
46        if ( is_callable( array( $file, 'get_body_content' ) ) ) {
47            // @phan-suppress-next-line PhanUndeclaredMethod @phan-suppress-current-line UnusedSuppression -- Tested above. @todo Remove this suppression when we drop WP <6.9.
48            $feed_dom = $this->safely_load_xml( $file->get_body_content() );
49        } else {
50            // @phan-suppress-next-line PhanDeprecatedProperty -- For compatibility only.
51            $feed_dom = $this->safely_load_xml( (string) $file->body );
52        }
53
54        // Do this as either/or but prioritise the itunes namespace. It's pretty likely
55        // that it's a podcast feed we've found if that namespace is present.
56        return $feed_dom && $this->has_itunes_ns( $feed_dom ) && $this->has_audio_enclosures( $feed_dom );
57    }
58
59    /**
60     * Safely loads an XML file
61     *
62     * @param string $xml A string of XML to load.
63     * @return DOMDocument|false A resulting DOM document or `false` if there is an error.
64     */
65    private function safely_load_xml( $xml ) {
66        if ( empty( $xml ) ) {
67            return false;
68        }
69
70        $disable_entity_loader = PHP_VERSION_ID < 80000;
71
72        if ( $disable_entity_loader ) {
73            // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading
74            // is disabled by default, so this function is no longer needed to protect against XXE attacks.
75            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
76            $loader = libxml_disable_entity_loader( true );
77        }
78
79        $errors = libxml_use_internal_errors( true );
80
81        $return = new DOMDocument();
82        if ( ! $return->loadXML( $xml ) ) {
83            return false;
84        }
85
86        libxml_use_internal_errors( $errors );
87
88        if ( $disable_entity_loader && isset( $loader ) ) {
89            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
90            libxml_disable_entity_loader( $loader );
91        }
92
93        return $return;
94    }
95
96    /**
97     * Checks the RSS feed for the presence of the itunes podcast namespace.
98     * It's pretty loose and just checks the URI for itunes.com
99     *
100     * @param DOMDocument $dom The XML document to check.
101     * @return boolean Whether the itunes namespace is defined.
102     */
103    private function has_itunes_ns( $dom ) {
104        $xpath = new DOMXPath( $dom );
105        foreach ( $xpath->query( 'namespace::*' ) as $node ) {
106            // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
107            // nodeValue is not valid, but it's part of the DOM API that we don't control.
108            if ( strstr( $node->nodeValue, 'itunes.com' ) ) {
109                return true;
110            }
111            // phpcs:enable
112        }
113        return false;
114    }
115
116    /**
117     * Checks the RSS feed for the presence of enclosures with an audio mimetype.
118     *
119     * @param DOMDocument $dom The XML document to check.
120     * @return boolean Whether enclosures were found.
121     */
122    private function has_audio_enclosures( $dom ) {
123        $xpath      = new DOMXPath( $dom );
124        $enclosures = $xpath->query( "//enclosure[starts-with(@type,'audio/')]" );
125        return ! $enclosures ? false : $enclosures->length > 0;
126    }
127}