_construct();
}
/**
* The parameter-less constructor.
*
* @since 4.7
*/
protected function _construct()
{
$wprss = wprss();
add_action( 'wprss_fetch_feed_before', array( $this, 'set_feed_options' ), 10, 2 );
add_action( 'wprss_settings_array', array( $this, 'add_settings' ) );
add_action( 'wprss_default_settings_general', array( $this, 'add_default_settings' ) );
$wprss->on('fields', array($this, 'add_feed_source_fields'));
}
/**
* Sets the path to the certificate, which will be used by WPRSS to fetch remote content.
*
* @since 4.7
* @param string $path Absolute path to the certificate file.
* @return \WPRSS_Feed_Access This instance.
*/
public function set_certificate_file_path( $path ) {
$this->_certificate_file_path = $path;
return $this;
}
/**
* Gets the path to the certificate, which will be used by WPRSS to fetch remote content.
*
* @since 4.7
* @see get_certificate_path_setting()
* @return string Absolute path to the certificate file. By default will use the option.
*/
public function get_certificate_file_path() {
if ( empty( $this->_certificate_file_path ) )
$this->_certificate_file_path = $this->get_certificate_path_setting();
return $this->_certificate_file_path;
}
/**
* Gets the value of the option that stores the path to the certificate file.
* Relative paths will be converted to absolute, as if relative to WP root.
*
* @since 4.7
* @return string Absolute path to the certificate file.
*/
public function get_certificate_path_setting() {
$path = wprss_get_general_setting( self::SETTING_KEY_CERTIFICATE_PATH );
if ( empty( $path ) )
return $path;
if ( !path_is_absolute( $path ) )
$path = ABSPATH . $path;
return $path;
}
/**
* Return the value of the useragent setting.
*
* The setting key is determined by the SETTING_KEY_FEED_REQUEST_USERAGENT class constant.
*
* @since 4.8.2
* @return string The value of the useragent setting.
*/
public function get_useragent_setting()
{
return wprss_get_general_setting( self::SETTING_KEY_FEED_REQUEST_USERAGENT );
}
/**
* Get the useragent string that will be sent with feed requests.
*
* @since 4.8.2
*
* @param int|null $feedSourceId The ID of the feed source, which to get the useragent for.
* Leave `null` to get a useragent independently from any feed source.
*
* @return string The useragent string that will be sent together with feed requests.
* If empty, the value of SIMPLEPIE_USERAGENT will be used.
*/
public function get_useragent($feedSourceId = null)
{
$useragent = !is_null($feedSourceId)
? get_post_meta($feedSourceId, self::SETTING_KEY_FEED_REQUEST_USERAGENT, true)
: $this->get_useragent_setting();
if (!strlen(trim($useragent))) {
$useragent = $this->get_useragent_setting();
}
$useragent = !strlen(trim($useragent))
? $this->getDefaultUseragent()
: $useragent;
wprss()->event('feed_access_useragent', array(
'useragent' => &$useragent,
'feed_source_id' => $feedSourceId
));
return $useragent;
}
/**
* Retrieve the useragent string that will be used by default.
*
* @since 4.10
*
* @return string The useragent string.
*/
public function getDefaultUseragent()
{
return SIMPLEPIE_USERAGENT;
}
/**
* This happens immediately before feed initialization.
* Handles the `wprss_fetch_feed_before` action.
*
* @since 4.7
* @param SimplePie $feed The instance of the object that represents the feed to be fetched.
* @param int $feedSourceId ID of the feed source, which is accessing the feed.
* Leave `null` to skip setting feed source specific options.
* @param string $url The URL, from which the feed is going to be fetched.
*/
public function set_feed_options($feed, $feedSourceId = null)
{
$feed->set_file_class( static::RESOURCE_CLASS );
$feed->set_useragent($this->get_useragent($feedSourceId));
WPRSS_SimplePie_File::set_default_certificate_file_path($this->get_certificate_file_path());
/*
* Setting the file resource object for the feed to use.
* Note: this object will only be used if cache is disabled for
* the feed. This is why running {@see SimplePie::set_file_class()}
* is still necessary. Like this, the correct file class will
* still be used, although the file object set below will
* have absolutely no effect on the feed retrieval process.
*/
if (!$feed->file) {
$feed->file = $this->create_resource_from_feed($feed);
}
}
/**
* Implements a `wprss_settings_array` filter.
*
* @since 4.7
* @param array $settings The current settings array, where 1st dimension is secion code, 2nd is setting code, 3rd is setting option(s).
* @return array The new settings array.
*/
public function add_settings( $settings ) {
$settings['general'][ self::SETTING_KEY_CERTIFICATE_PATH ] = array(
'label' => __( 'Certificate Path', WPRSS_TEXT_DOMAIN ),
'callback' => array( $this, 'render_certificate_path_setting' )
);
/** @since 4.8.2 */
$settings['general'][ self::SETTING_KEY_FEED_REQUEST_USERAGENT ] = array(
'label' => __( 'Feed Request Useragent', WPRSS_TEXT_DOMAIN ),
'callback' => array( $this, 'render_feed_request_useragent_setting' )
);
return $settings;
}
/**
* Adding feed source specific settings.
*
* @since 4.10
*
* @param array $fields An array containing all existing fields by ID.
*/
public function add_feed_source_fields($fields)
{
$wprss = wprss();
$fields[self::SETTING_KEY_FEED_REQUEST_USERAGENT] = array(
'id' => self::SETTING_KEY_FEED_REQUEST_USERAGENT,
'label' => $wprss->__('Feed Request Useragent'),
'placeholder' => $wprss->__('Leave blank to inherit general setting')
);
return $fields;
}
/**
* @since 4.7
* @param array $settings The array of settings, where key is
* @return array The new array of default settings
*/
public function add_default_settings( $settings ) {
$settings[ self::SETTING_KEY_CERTIFICATE_PATH ] = implode( '/', array( WPINC, 'certificates', 'ca-bundle.crt' ) );
/** @since 4.8.2 */
$settings[ self::SETTING_KEY_FEED_REQUEST_USERAGENT ] = '';
return $settings;
}
/**
* Renders the setting field for the certificate path.
*
* @since 4.7
* @see wprss_admin_init
* @param array $field Data of this field.
*/
public function render_certificate_path_setting( $field ) {
$feed_limit = wprss_get_general_setting( $field['field_id'] );
?>
implode(', ', apply_filters('wprss_feed_default_headers_accept', array(
'application/atom+xml',
'application/rss+xmlm',
'application/rdf+xml;q=0.9',
'application/xml;q=0.8',
'text/xml;q=0.8',
'text/html;q=0.7',
'unknown/unknown;q=0.1',
'application/unknown;q=0.1',
'*/*;q=0.1')))
);
$headers = array_merge_recursive_distinct($defaultHeaders, $additionalHeaders);
$headers = apply_filters('wprss_feed_default_headers', $headers);
return $headers;
}
/**
* Creates a new object that is responsible for retrieving a remote resource.
*
* @since 4.10
*
* @see SimplePie_File::__construct()
*
* @param string $url
* @param int $timeout
* @param int $redirects
* @param array $headers If null, {@link default_headers() default headers} will be used.
* @param string $useragent
* @param bool $force_fsockopen
* @return \SimplePie_File
*/
public function create_resource($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false)
{
if (is_null($headers)) {
$headers = $this->default_headers();
}
if (!class_exists($resourceClass = static::RESOURCE_CLASS)) {
throw new Exception(sprintf('Could not create resource: resource class "$1$s" does not exist', $resourceClass));
}
return new $resourceClass($url, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
}
/**
* Creates a new object that is responsible for retrieving a remote resource, using values from a feed.
*
* @since 4.10
*
* @see wprss_feed_create_resource()
*
* @param \SimplePie $feed The feed, based on which to create the resource.
* @return \SimplePie_File
*/
public function create_resource_from_feed(SimplePie $feed)
{
return $this->create_resource($feed->feed_url, $feed->timeout, static::D_REDIRECTS, null, $feed->useragent, $feed->force_fsockopen);
}
}
// Initialize
add_action('wprss_init', function() {
WPRSS_Feed_Access::instance();
});
/**
* A padding layer used to give WPRSS more control over fetching of feed resources.
* @since 4.7
*/
class WPRSS_SimplePie_File extends SimplePie_File {
protected static $_default_certificate_file_path;
protected $_certificate_file_path;
/**
* Copied from {@see SimplePie_File#__construct()}.
* Adds call to {@see _before_curl_exec()}.
*
* @since 4.7
*/
public function __construct( $url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false ) {
if ( class_exists( 'idna_convert' ) ) {
$idn = new idna_convert();
$parsed = SimplePie_Misc::parse_url( $url );
$url = SimplePie_Misc::compress_parse_url( $parsed['scheme'], $idn->encode( $parsed['authority'] ), $parsed['path'], $parsed['query'], $parsed['fragment'] );
wprss_log_obj('Converted IDNA URL', $url, null, WPRSS_LOG_LEVEL_SYSTEM);
}
$this->url = $url;
$this->useragent = $useragent;
if ( preg_match( '/^http(s)?:\/\//i', $url ) ) {
if ( $useragent === null ) {
$useragent = ini_get( 'user_agent' );
$this->useragent = $useragent;
}
if ( !is_array( $headers ) ) {
$headers = array();
}
if ( !$force_fsockopen && function_exists( 'curl_exec' ) ) {
$this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_CURL;
$fp = curl_init();
$headers2 = array();
foreach ( $headers as $key => $value ) {
$headers2[] = "$key: $value";
}
if ( version_compare( SimplePie_Misc::get_curl_version(), '7.10.5', '>=' ) ) {
curl_setopt( $fp, CURLOPT_ENCODING, '' );
}
curl_setopt( $fp, CURLOPT_URL, $url );
curl_setopt( $fp, CURLOPT_HEADER, 1 );
curl_setopt( $fp, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $fp, CURLOPT_TIMEOUT, $timeout );
curl_setopt( $fp, CURLOPT_CONNECTTIMEOUT, $timeout );
curl_setopt( $fp, CURLOPT_REFERER, $url );
curl_setopt( $fp, CURLOPT_USERAGENT, $useragent );
curl_setopt( $fp, CURLOPT_HTTPHEADER, $headers2 );
if ( !ini_get( 'open_basedir' ) && !ini_get( 'safe_mode' ) && version_compare( SimplePie_Misc::get_curl_version(), '7.15.2', '>=' ) ) {
curl_setopt( $fp, CURLOPT_FOLLOWLOCATION, 1 );
curl_setopt( $fp, CURLOPT_MAXREDIRS, $redirects );
}
$this->_before_curl_exec( $fp, $url );
$this->headers = curl_exec( $fp );
if ( curl_errno( $fp ) === 23 || curl_errno( $fp ) === 61 ) {
curl_setopt( $fp, CURLOPT_ENCODING, 'none' );
$this->headers = curl_exec( $fp );
}
if ( curl_errno( $fp ) ) {
$this->error = 'cURL error ' . curl_errno( $fp ) . ': ' . curl_error( $fp );
$this->success = false;
} else {
$info = curl_getinfo( $fp );
curl_close( $fp );
$this->headers = explode( "\r\n\r\n", $this->headers, $info['redirect_count'] + 1 );
$this->headers = array_pop( $this->headers );
$parser = new SimplePie_HTTP_Parser( $this->headers );
if ( $parser->parse() ) {
$this->headers = $parser->headers;
$this->body = trim( $parser->body );
$this->status_code = $parser->status_code;
if ( (in_array( $this->status_code, array( 300, 301, 302, 303, 307 ) ) || $this->status_code > 307 && $this->status_code < 400) && isset( $this->headers['location'] ) && $this->redirects < $redirects ) {
$this->redirects++;
$location = SimplePie_Misc::absolutize_url( $this->headers['location'], $url );
return $this->__construct( $location, $timeout, $redirects, $headers, $useragent, $force_fsockopen );
}
$this->_afterCurlHeadersParsed($info);
}
}
} else {
$this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_FSOCKOPEN;
$url_parts = parse_url( $url );
$socket_host = $url_parts['host'];
if ( isset( $url_parts['scheme'] ) && strtolower( $url_parts['scheme'] ) === 'https' ) {
$socket_host = "ssl://{$url_parts['host']}";
$url_parts['port'] = 443;
}
if ( !isset( $url_parts['port'] ) ) {
$url_parts['port'] = 80;
}
$fp = @fsockopen( $socket_host, $url_parts['port'], $errno, $errstr, $timeout );
if ( !$fp ) {
$this->error = 'fsockopen error: ' . $errstr;
$this->success = false;
} else {
stream_set_timeout( $fp, $timeout );
if ( isset( $url_parts['path'] ) ) {
if ( isset( $url_parts['query'] ) ) {
$get = "{$url_parts['path']}?{$url_parts['query']}";
} else {
$get = $url_parts['path'];
}
} else {
$get = '/';
}
$out = "GET $get HTTP/1.1\r\n";
$out .= "Host: {$url_parts['host']}\r\n";
$out .= "User-Agent: $useragent\r\n";
if ( extension_loaded( 'zlib' ) ) {
$out .= "Accept-Encoding: x-gzip,gzip,deflate\r\n";
}
if ( isset( $url_parts['user'] ) && isset( $url_parts['pass'] ) ) {
$out .= "Authorization: Basic " . base64_encode( "{$url_parts['user']}:{$url_parts['pass']}" ) . "\r\n";
}
foreach ( $headers as $key => $value ) {
$out .= "$key: $value\r\n";
}
$out .= "Connection: Close\r\n\r\n";
fwrite( $fp, $out );
$info = stream_get_meta_data( $fp );
$this->headers = '';
while ( !$info['eof'] && !$info['timed_out'] ) {
$this->headers .= fread( $fp, 1160 );
$info = stream_get_meta_data( $fp );
}
if ( !$info['timed_out'] ) {
$parser = new SimplePie_HTTP_Parser( $this->headers );
if ( $parser->parse() ) {
$this->headers = $parser->headers;
$this->body = trim( $parser->body );
$this->status_code = $parser->status_code;
if ( (in_array( $this->status_code, array( 300, 301, 302, 303, 307 ) ) || $this->status_code > 307 && $this->status_code < 400) && isset( $this->headers['location'] ) && $this->redirects < $redirects ) {
$this->redirects++;
$location = SimplePie_Misc::absolutize_url( $this->headers['location'], $url );
return $this->__construct( $location, $timeout, $redirects, $headers, $useragent, $force_fsockopen );
}
if ( isset( $this->headers['content-encoding'] ) ) {
// Hey, we act dumb elsewhere, so let's do that here too
switch ( strtolower( trim( $this->headers['content-encoding'], "\x09\x0A\x0D\x20" ) ) ) {
case 'gzip':
case 'x-gzip':
$decoder = new SimplePie_gzdecode( $this->body );
if ( !$decoder->parse() ) {
$this->error = 'Unable to decode HTTP "gzip" stream';
$this->success = false;
} else {
$this->body = $decoder->data;
}
break;
case 'deflate':
if ( ($decompressed = gzinflate( $this->body )) !== false ) {
$this->body = $decompressed;
} else if ( ($decompressed = gzuncompress( $this->body )) !== false ) {
$this->body = $decompressed;
} else if ( function_exists( 'gzdecode' ) && ($decompressed = gzdecode( $this->body )) !== false ) {
$this->body = $decompressed;
} else {
$this->error = 'Unable to decode HTTP "deflate" stream';
$this->success = false;
}
break;
default:
$this->error = 'Unknown content coding';
$this->success = false;
}
}
}
} else {
$this->error = 'fsocket timed out';
$this->success = false;
}
fclose( $fp );
}
}
} else {
$this->method = SIMPLEPIE_FILE_SOURCE_LOCAL | SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS;
if ( !$this->body = file_get_contents( $url ) ) {
$this->error = 'file_get_contents could not read the file';
$this->success = false;
}
}
}
/**
* Additional preparation of the curl request.
* Sets the {@link CURLOPT_CAINFO http://php.net/manual/en/function.curl-setopt.php}
* cURL option to a value determined by {@see get_default_certificate_file_path}.
* If the value is empty, leaves it as is.
*
* @since 4.7
* @param resource $fp Pointer to a resource created by {@see curl_init()}.
* @param string $url The URL, to which the cURL request is being made.
* @return \WPRSS_SimplePie_File This instance.
*/
protected function _before_curl_exec( $fp, $url ) {
if ( ($ca_path = self::get_default_certificate_file_path()) && !empty( $ca_path ) ) {
$this->_certificate_file_path = $ca_path;
curl_setopt( $fp, CURLOPT_CAINFO, $this->_certificate_file_path );
}
do_action( 'wprss_before_curl_exec', $fp );
return $this;
}
/**
* Gets the path to the certificate, which will be used by this instance
* to fetch remote content.
*
* @since 4.7
* @return string Path to the certificate file.
*/
public function get_certificate_file_path() {
return $this->_certificate_file_path;
}
/**
* Gets the path to the certificate file, which will be used by future
* instances of this class.
*
* @since 4.7
* @return string Path to the certificate file.
*/
public static function get_default_certificate_file_path() {
return self::$_default_certificate_file_path;
}
/**
* Sets the path to the certificate file.
* This path will be used by future instances of this class.
*
* @since 4.7
* @param string $path The path to the certificate file.
*/
public static function set_default_certificate_file_path( $path ) {
self::$_default_certificate_file_path = $path;
}
/**
* Called right after a cURL request returns, and headers are parsed.
*
* This method will not be called if a cURL error is encountered.
*
* @param $curlInfo Result of a call to {@see curl_getinfo()} on the cURL resource.
* @since 4.8.2
*/
protected function _afterCurlHeadersParsed($curlInfo)
{
$bodyMaxLength = 150;
$code = $this->status_code;
$body = $this->body;
$bodyLength = strlen($body);
$bodyOutput = strlen($body) < $bodyMaxLength
? $body
: substr($body, 0, $bodyMaxLength);
$outputLength = strlen($bodyOutput);
$error = implode("\n", array(
'The resource could not be retrieved because of a %1$s error with code %2$d',
'Server returned %3$d characters' . ($outputLength < $bodyMaxLength ? '' : ', of which ' . $outputLength . ' are below') . ':',
'%4$s'
));
if ($code >= 400 && $code < 500 ) { // Client error
$this->error = sprintf($error, 'client', $code, $bodyLength, $bodyOutput);
$this->success = false;
}
if ($code >= 500 && $code < 600 ) { // Server error
$this->error = sprintf($error, 'server', $code, $bodyLength, $bodyOutput);
$this->success = false;
}
}
}