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