This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/.gitignore'
autoload.php
composer.lock
composer/
marienfressinaud/lib_opml/.git/
marienfressinaud/lib_opml/.gitlab-ci.yml
marienfressinaud/lib_opml/.gitlab/
marienfressinaud/lib_opml/ci/
marienfressinaud/lib_opml/examples/
marienfressinaud/lib_opml/Makefile
marienfressinaud/lib_opml/src/functions.php
marienfressinaud/lib_opml/tests/
phpgt/cssxpath/.*
phpgt/cssxpath/composer.json
phpgt/cssxpath/CONTRIBUTING.md
phpgt/cssxpath/test/
phpmailer/phpmailer/*oauth*
phpmailer/phpmailer/COMMITMENT*
phpmailer/phpmailer/composer.*
phpmailer/phpmailer/language/
phpmailer/phpmailer/SECURITY*
phpmailer/phpmailer/src/DSNConfigurator.php
phpmailer/phpmailer/src/OAuth*
phpmailer/phpmailer/src/POP3.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/.htaccess'
# Apache 2.2
<IfModule !mod_authz_core.c>
Order Allow,Deny
Deny from all
Satisfy all
</IfModule>
# Apache 2.4
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/README.md'
# Libraries
## Updating libraries
Some of the libraries in this folder can be updated semi-automatically by invoking:
```sh
cd ./FreshRSS/lib/
composer update --no-autoloader
```
Remember to read the change-logs, proof-read the changes, preserve possible local patches, add irrelevant files to [`.gitignore`](.gitignore) (minimal installation), and test before committing.
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/composer.json'
{
"name": "freshrss.org/freshrss",
"description": "A free, self-hostable aggregator",
"type": "project",
"homepage": "https://freshrss.org/",
"license": "AGPL-3.0",
"repositories": [
{
"type": "git",
"url": "https://github.com/PhpGt/CssXPath.git"
}
],
"require": {
"marienfressinaud/lib_opml": "0.5.1",
"phpgt/cssxpath": "dev-master#d99d35f7194bac19fb3f8726b70c1bc83de3e931",
"phpmailer/phpmailer": "6.9.1"
},
"config": {
"sort-packages": true,
"vendor-dir": "./"
},
"scripts": {
"post-update-cmd": "git clean -d -f -X ."
}
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/favicons.php'
<?php
declare(strict_types=1);
const FAVICONS_DIR = DATA_PATH . '/favicons/';
const DEFAULT_FAVICON = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
function isImgMime(string $content): bool {
//Based on https://github.com/ArthurHoaro/favicon/blob/3a4f93da9bb24915b21771eb7873a21bde26f5d1/src/Favicon/Favicon.php#L311-L319
if ($content == '') {
return false;
}
if (!extension_loaded('fileinfo')) {
return true;
}
$isImage = true;
/** @var finfo $fInfo */
$fInfo = finfo_open(FILEINFO_MIME_TYPE);
/** @var string $content */
$content = finfo_buffer($fInfo, $content);
$isImage = strpos($content, 'image') !== false;
finfo_close($fInfo);
return $isImage;
}
/** @param array<int,int|bool> $curlOptions */
function downloadHttp(string &$url, array $curlOptions = []): string {
syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url);
$url = checkUrl($url);
if ($url == false) {
return '';
}
/** @var CurlHandle $ch */
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
CURLOPT_MAXREDIRS => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '', //Enable all encodings
//CURLOPT_VERBOSE => 1, // To debug sent HTTP headers
]);
FreshRSS_Context::initSystem();
if (FreshRSS_Context::hasSystemConf()) {
curl_setopt_array($ch, FreshRSS_Context::systemConf()->curl_options);
}
curl_setopt_array($ch, $curlOptions);
$response = curl_exec($ch);
if (!is_string($response)) {
$response = '';
}
$info = curl_getinfo($ch);
curl_close($ch);
if (!empty($info['url'])) {
$url2 = checkUrl($info['url']);
if ($url2 != '') {
$url = $url2; //Possible redirect
}
}
return $info['http_code'] == 200 ? $response : '';
}
function searchFavicon(string &$url): string {
$dom = new DOMDocument();
$html = downloadHttp($url);
if ($html == '' || !@$dom->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) {
return '';
}
$xpath = new DOMXPath($dom);
$links = $xpath->query('//link[@href][translate(@rel, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="shortcut icon"'
. ' or translate(@rel, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="icon"]');
if (!($links instanceof DOMNodeList)) {
return '';
}
// Use the base element for relative paths, if there is one
$baseElements = $xpath->query('//base[@href]');
$baseElement = ($baseElements !== false && $baseElements->length > 0) ? $baseElements->item(0) : null;
$baseUrl = ($baseElement instanceof DOMElement) ? $baseElement->getAttribute('href') : $url;
foreach ($links as $link) {
if (!$link instanceof DOMElement) {
continue;
}
$href = trim($link->getAttribute('href'));
$urlParts = parse_url($url);
// Handle protocol-relative URLs by adding the current URL's scheme
if (substr($href, 0, 2) === '//') {
$href = ($urlParts['scheme'] ?? 'https') . ':' . $href;
}
$href = SimplePie_IRI::absolutize($baseUrl, $href);
if ($href == false) {
return '';
}
$iri = $href->get_iri();
$favicon = downloadHttp($iri, array(CURLOPT_REFERER => $url));
if (isImgMime($favicon)) {
return $favicon;
}
}
return '';
}
function download_favicon(string $url, string $dest): bool {
$url = trim($url);
$favicon = searchFavicon($url);
if ($favicon == '') {
$rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url);
if ($rootUrl != $url) {
$url = $rootUrl;
$favicon = searchFavicon($url);
}
if ($favicon == '') {
$link = $rootUrl . 'favicon.ico';
$favicon = downloadHttp($link, array(
CURLOPT_REFERER => $url,
));
if (!isImgMime($favicon)) {
$favicon = '';
}
}
}
return ($favicon != '' && file_put_contents($dest, $favicon) > 0) ||
@copy(DEFAULT_FAVICON, $dest);
}
function contentType(string $ico): string {
$ico_content_type = 'image/x-icon';
if (function_exists('mime_content_type')) {
$ico_content_type = mime_content_type($ico) ?: $ico_content_type;
}
switch ($ico_content_type) {
case 'image/svg':
$ico_content_type = 'image/svg+xml';
break;
}
return $ico_content_type;
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/http-conditional.php'
<?php
declare(strict_types=1);
/*
Enable support for HTTP/1.x conditional requests in PHP.
Goal: Optimisation
- If the client sends a HEAD request, avoid transferring data and return the correct headers.
- If the client already has the same version in its cache, avoid transferring data again (304 Not Modified).
- Possibility to control cache for client and proxies (public or private policy, life time).
- When $feedMode is set to true, in the case of a RSS/ATOM feed,
it puts a timestamp in the global variable $clientCacheDate to allow the sending of only the articles newer than the client’s cache.
- When $compression is set to true, compress the data before sending it to the client and persistent connections are allowed.
- When $session is set to true, automatically checks if $_SESSION has been modified during the last generation the document.
Typical use:
```php
<?php
require_once('http-conditional.php');
//Date of the last modification of the content (Unix Timestamp format).
//Examples: query the database, or last modification of a static file.
$dateLastModification = ...;
if (httpConditional($dateLastModification)) {
... //Close database connections, and other cleaning.
exit(); //No need to send anything
}
//Do not send any text to the client before this line.
... //Rest of the script, just as you would do normally.
?>
```
Version 1.9, 2023-04-08, https://alexandre.alapetite.fr/doc-alex/php-http-304/
------------------------------------------------------------------
Written by Alexandre Alapetite in 2004, https://alexandre.alapetite.fr/cv/
Copyright 2004-2023, Licence: Creative Commons "Attribution-ShareAlike 2.0 France" BY-SA (FR),
https://creativecommons.org/licenses/by-sa/2.0/fr/
https://alexandre.alapetite.fr/divers/apropos/#by-sa
- Attribution. You must give the original author credit
- Share Alike. If you alter, transform, or build upon this work,
you may distribute the resulting work only under a license identical to this one
(Can be included in GPL/LGPL projects)
- The French law is authoritative
- Any of these conditions can be waived if you get permission from Alexandre Alapetite
- Please send to Alexandre Alapetite the modifications you make,
in order to improve this file for the benefit of everybody
If you want to distribute this code, please do it as a link to:
https://alexandre.alapetite.fr/doc-alex/php-http-304/
*/
/**
* In RSS/ATOM feedMode, contains the date of the clients last update.
* Global public variable because PHP4 did not allow conditional arguments by reference
* @var int
*/
$clientCacheDate = 0;
/**
* Global private variable
* @var bool
*/
$_sessionMode = false;
/**
* RFC2616 HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616.html
* RFC1945 HTTP/1.0: https://www.w3.org/Protocols/rfc1945/rfc1945.txt
* Credits: https://alexandre.alapetite.fr/doc-alex/php-http-304/
*
* @param int $UnixTimeStamp: Date of the last modification of the data to send to the client (Unix Timestamp format).
* @param int $cacheSeconds (default 0) Lifetime in seconds of the document. If $cacheSeconds<0, cache is disabled.
* If $cacheSeconds==0, the document will be revalidated each time it is accessed. If $cacheSeconds>0, the document will be cashed and not revalidated against the server for this delay.
* @phpstan-param 0|1|2 $cachePrivacy
* @param int $cachePrivacy (default 0) 0=private, 1=normal (public), 2=forced public. When public, it allows a cashed document ($cacheSeconds>0) to be shared by several users.
* @param bool $feedMode (default false) Special RSS/ATOM feeds.
* When true, it sets $cachePrivacy to 0 (private), does not use the modification time of the script itself, and puts the date of the client’s cache (or a old date from 1980) in the global variable $clientCacheDate.
* @param bool $compression (default false) Enable the compression and allows persistent connections (automatic detection of the capacities of the client).
* @param bool $session (default false) To be turned on when sessions are used. Checks if the data contained in $_SESSION has been modified during the last generation the document.
* @return bool True if the connection can be closed (e.g.: the client has already the latest version), false if the new content has to be send to the client.
*/
function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePrivacy = 0, bool $feedMode = false, bool $compression = false, bool $session = false): bool {
if (headers_sent()) return false;
if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME'];
elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED'];
else return false;
if ((!$feedMode) && (($modifScript = (int)filemtime($scriptName)) > $UnixTimeStamp))
$UnixTimeStamp = $modifScript;
$UnixTimeStamp = (int)min($UnixTimeStamp, time());
$is304 = true;
$is412 = false;
$nbCond = 0;
//rfc2616-sec3.html#sec3.3.1
$dateLastModif = gmdate('D, d M Y H:i:s \G\M\T', $UnixTimeStamp);
$dateCacheClient = 'Thu, 10 Jan 1980 20:30:40 GMT';
//rfc2616-sec14.html#sec14.19 //='"0123456789abcdef0123456789abcdef"'
if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING'];
else $myQuery = '';
if ($session && isset($_SESSION)) {
global $_sessionMode;
$_sessionMode = $session;
$myQuery .= print_r($_SESSION, true) . session_name() . '=' . session_id();
}
$etagServer = '"' . md5($scriptName . $myQuery . '#' . $dateLastModif) . '"';
// @phpstan-ignore booleanNot.alwaysTrue
if ((!$is412) && isset($_SERVER['HTTP_IF_MATCH'])) { //rfc2616-sec14.html#sec14.24
$etagsClient = stripslashes($_SERVER['HTTP_IF_MATCH']);
$etagsClient = str_ireplace('-gzip', '', $etagsClient);
$is412 = (($etagsClient !== '*') && (strpos($etagsClient, $etagServer) === false));
}
// @phpstan-ignore booleanAnd.leftAlwaysTrue
if ($is304 && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt
$nbCond++;
$dateCacheClient = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
$p = strpos($dateCacheClient, ';');
if ($p !== false)
$dateCacheClient = substr($dateCacheClient, 0, $p);
$is304 = ($dateCacheClient == $dateLastModif);
}
if ($is304 && isset($_SERVER['HTTP_IF_NONE_MATCH'])) { //rfc2616-sec14.html#sec14.26
$nbCond++;
$etagClient = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
$etagClient = str_ireplace('-gzip', '', $etagClient);
$is304 = (($etagClient === $etagServer) || ($etagClient === '*'));
}
if ((!$is412) && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.28
$dateCacheClient = $_SERVER['HTTP_IF_UNMODIFIED_SINCE'];
$p = strpos($dateCacheClient, ';');
if ($p !== false)
$dateCacheClient = substr($dateCacheClient, 0, $p);
$is412 = ($dateCacheClient !== $dateLastModif);
}
if ($feedMode) { //Special RSS/ATOM
global $clientCacheDate;
$clientCacheDate = @strtotime($dateCacheClient);
$cachePrivacy = 0;
}
if ($is412) { //rfc2616-sec10.html#sec10.4.13
header('HTTP/1.1 412 Precondition Failed');
header('Cache-Control: private, max-age=0, must-revalidate');
header('Content-Type: text/plain');
echo "HTTP/1.1 Error 412 Precondition Failed: Precondition request failed positive evaluation\n";
return true;
} elseif ($is304 && ($nbCond > 0)) { //rfc2616-sec10.html#sec10.3.5
header('HTTP/1.0 304 Not Modified');
header('Etag: ' . $etagServer);
if ($feedMode) header('Connection: close'); //Comment this line under IIS
return true;
} else { //rfc2616-sec10.html#sec10.2.1
//rfc2616-sec14.html#sec14.3
if ($compression) ob_start('_httpConditionalCallBack'); //Will check HTTP_ACCEPT_ENCODING
//header('HTTP/1.0 200 OK');
if ($cacheSeconds < 0) {
$cache = 'private, no-cache, no-store, must-revalidate';
//header('Expires: 0');
header('Pragma: no-cache');
} else {
if ($cacheSeconds === 0) {
$cache = 'private, must-revalidate, ';
//header('Expires: 0');
} elseif ($cachePrivacy === 0) $cache = 'private, ';
elseif ($cachePrivacy === 2) $cache = 'public, ';
else $cache = '';
$cache .= 'max-age=' . floor($cacheSeconds);
}
//header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T',time()+$cacheSeconds)); //HTTP/1.0 //rfc2616-sec14.html#sec14.21
header('Cache-Control: ' . $cache); //rfc2616-sec14.html#sec14.9
header('Last-Modified: ' . $dateLastModif);
header('Etag: ' . $etagServer);
if ($feedMode) header('Connection: close'); //rfc2616-sec14.html#sec14.10 //Comment this line under IIS
return $_SERVER['REQUEST_METHOD'] === 'HEAD'; //rfc2616-sec9.html#sec9.4
}
}
/**
* Private function automatically called at the end of the script when compression is enabled.
* One can adjust the level of compression with zlib.output_compression_level in php.ini
* Reference rfc2616-sec14.html#sec14.11
*/
function _httpConditionalCallBack(string $buffer, int $mode = 5): string {
if (extension_loaded('zlib') && (ini_get('zlib.output_compression') == false)) {
$buffer2 = ob_gzhandler($buffer, $mode) ?: ''; //Will check HTTP_ACCEPT_ENCODING and put correct headers such as Vary //rfc2616-sec14.html#sec14.44
if (strlen($buffer2) > 1) //When ob_gzhandler succeeded
$buffer = $buffer2;
}
header('Content-Length: ' . strlen($buffer)); //Allows persistent connections //rfc2616-sec14.html#sec14.13
return $buffer;
}
/**
* Update HTTP headers if the content has just been modified by the client’s request.
* See an example on https://alexandre.alapetite.fr/doc-alex/compteur/
*/
function httpConditionalRefresh(int $UnixTimeStamp): void {
if (headers_sent()) return;
if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME'];
elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED'];
else return;
$dateLastModif = gmdate('D, d M Y H:i:s \G\M\T', $UnixTimeStamp);
if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING'];
else $myQuery = '';
global $_sessionMode;
if ($_sessionMode && isset($_SESSION))
$myQuery .= print_r($_SESSION, true) . session_name() . '=' . session_id();
$etagServer = '"' . md5($scriptName . $myQuery . '#' . $dateLastModif) . '"';
header('Last-Modified: ' . $dateLastModif);
header('Etag: ' . $etagServer);
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/lib_date.php'
<?php
declare(strict_types=1);
/**
* Author: Alexandre Alapetite https://alexandre.alapetite.fr
* 2014-06-01
* License: GNU AGPLv3 http://www.gnu.org/licenses/agpl-3.0.html
*
* Parser of ISO 8601 time intervals http://en.wikipedia.org/wiki/ISO_8601#Time_intervals
* Examples: "2014-02/2014-04", "2014-02/04", "2014-06", "P1M"
*/
/*
example('2014-03');
example('201403');
example('2014-03-30');
example('2014-05-30T13');
example('2014-05-30T13:30');
example('2014-02/2014-04');
example('2014-02--2014-04');
example('2014-02/04');
example('2014-02-03/05');
example('2014-02-03T22:00/22:15');
example('2014-02-03T22:00/15');
example('2014-03/');
example('/2014-03');
example('2014-03/P1W');
example('P1W/2014-05-25T23:59:59');
example('P1Y/');
example('P1Y');
example('P2M/');
example('P3W/');
example('P4D/');
example('PT5H/');
example('PT6M/');
example('PT7S/');
example('P1DT1H/');
function example(string $dateInterval) {
$dateIntervalArray = parseDateInterval($dateInterval);
echo $dateInterval, "\t=>\t",
$dateIntervalArray[0] == null ? 'null' : @date('c', $dateIntervalArray[0]), '/',
$dateIntervalArray[1] == null ? 'null' : @date('c', $dateIntervalArray[1]), "\n";
}
*/
function _dateFloor(string $isoDate): string {
$x = explode('T', $isoDate, 2);
$t = isset($x[1]) ? str_pad($x[1], 6, '0') : '000000';
return str_pad($x[0], 8, '01') . 'T' . $t;
}
function _dateCeiling(string $isoDate): string {
$x = explode('T', $isoDate, 2);
$t = isset($x[1]) && strlen($x[1]) > 1 ? str_pad($x[1], 6, '59') : '235959';
switch (strlen($x[0])) {
case 4:
return $x[0] . '1231T' . $t;
case 6:
$d = @strtotime($x[0] . '01');
if ($d != false) {
return $x[0] . date('t', $d) . 'T' . $t;
}
}
return $x[0] . 'T' . $t;
}
/** @phpstan-return ($isoDate is null ? null : ($isoDate is '' ? null : string)) */
function _noDelimit(?string $isoDate): ?string {
return $isoDate === null || $isoDate === '' ? null : str_replace(array('-', ':'), '', $isoDate); //FIXME: Bug with negative time zone
}
function _dateRelative(?string $d1, ?string $d2): ?string {
if ($d2 === null) {
return $d1 !== null && $d1[0] !== 'P' ? $d1 : null;
}
if ($d2 !== '' && $d2[0] != 'P' && $d1 !== null && $d1[0] !== 'P') {
$y2 = substr($d2, 0, 4);
if (strlen($y2) < 4 || !ctype_digit($y2)) { //Does not start by a year
$d2 = _noDelimit($d2);
return substr($d1, 0, -strlen($d2)) . $d2; //Add prefix from $d1
}
}
return _noDelimit($d2);
}
/**
* Parameter $dateInterval is a string containing an ISO 8601 time interval.
* @return array{int|null|false,int|null|false} an array with the minimum and maximum Unix timestamp of this interval,
* or null if open interval, or false if error.
*/
function parseDateInterval(string $dateInterval): array {
$dateInterval = trim($dateInterval);
$dateInterval = str_replace('--', '/', $dateInterval);
$dateInterval = strtoupper($dateInterval);
$min = null;
$max = null;
$x = explode('/', $dateInterval, 2);
$d1 = _noDelimit($x[0]);
$d2 = _dateRelative($d1, count($x) > 1 ? $x[1] : null);
if ($d1 !== null && $d1[0] !== 'P') {
$min = @strtotime(_dateFloor($d1));
}
if ($d2 !== null) {
if ($d2[0] === 'P') {
try {
$di2 = new DateInterval($d2);
$dt1 = @date_create(); //new DateTime() would create an Exception if the default time zone is not defined
if ($dt1 === false) {
$max = false;
} else {
if ($min !== null && $min !== false) {
$dt1->setTimestamp($min);
}
$max = $dt1->add($di2)->getTimestamp() - 1;
}
} catch (Exception $e) {
$max = false;
}
} elseif ($d1 === null || $d1[0] !== 'P') {
$max = @strtotime(_dateCeiling($d2));
} else {
$max = @strtotime($d2);
}
}
if ($d1 !== null && $d1[0] === 'P') {
try {
$di1 = new DateInterval($d1);
$dt2 = @date_create();
if ($dt2 === false) {
$min = false;
} else {
if ($max !== null && $max !== false) {
$dt2->setTimestamp($max);
}
$min = $dt2->sub($di1)->getTimestamp() + 1;
}
} catch (Exception $e) {
$min = false;
}
}
return array($min, $max);
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/lib_install.php'
<?php
declare(strict_types=1);
FreshRSS_SystemConfiguration::register('default_system', join_path(FRESHRSS_PATH, 'config.default.php'));
FreshRSS_UserConfiguration::register('default_user', join_path(FRESHRSS_PATH, 'config-user.default.php'));
/** @return array<string,string> */
function checkRequirements(string $dbType = ''): array {
$php = version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION) >= 0;
$curl = extension_loaded('curl');
$pdo_mysql = extension_loaded('pdo_mysql');
$pdo_sqlite = extension_loaded('pdo_sqlite');
$pdo_pgsql = extension_loaded('pdo_pgsql');
$message = '';
switch ($dbType) {
case 'mysql':
$pdo_sqlite = $pdo_pgsql = true;
$pdo = $pdo_mysql;
break;
case 'sqlite':
$pdo_mysql = $pdo_pgsql = true;
$pdo = $pdo_sqlite;
break;
case 'pgsql':
$pdo_mysql = $pdo_sqlite = true;
$pdo = $pdo_pgsql;
break;
case '':
$pdo = $pdo_mysql || $pdo_sqlite || $pdo_pgsql;
break;
default:
$pdo_mysql = $pdo_sqlite = $pdo_pgsql = true;
$pdo = false;
$message = 'Invalid database type!';
break;
}
$pcre = extension_loaded('pcre');
$ctype = extension_loaded('ctype');
$fileinfo = extension_loaded('fileinfo');
$dom = class_exists('DOMDocument');
$xml = function_exists('xml_parser_create');
$json = function_exists('json_encode');
$mbstring = extension_loaded('mbstring');
$data = is_dir(DATA_PATH) && touch(DATA_PATH . '/index.html'); // is_writable() is not reliable for a folder on NFS
$cache = is_dir(CACHE_PATH) && touch(CACHE_PATH . '/index.html');
$tmp = is_dir(TMP_PATH) && is_writable(TMP_PATH);
$users = is_dir(USERS_PATH) && touch(USERS_PATH . '/index.html');
$favicons = is_dir(DATA_PATH) && touch(DATA_PATH . '/favicons/index.html');
return array(
'php' => $php ? 'ok' : 'ko',
'curl' => $curl ? 'ok' : 'ko',
'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
'pdo-pgsql' => $pdo_pgsql ? 'ok' : 'ko',
'pdo' => $pdo ? 'ok' : 'ko',
'pcre' => $pcre ? 'ok' : 'ko',
'ctype' => $ctype ? 'ok' : 'ko',
'fileinfo' => $fileinfo ? 'ok' : 'ko',
'dom' => $dom ? 'ok' : 'ko',
'xml' => $xml ? 'ok' : 'ko',
'json' => $json ? 'ok' : 'ko',
'mbstring' => $mbstring ? 'ok' : 'ko',
'data' => $data ? 'ok' : 'ko',
'cache' => $cache ? 'ok' : 'ko',
'tmp' => $tmp ? 'ok' : 'ko',
'users' => $users ? 'ok' : 'ko',
'favicons' => $favicons ? 'ok' : 'ko',
'message' => $message ?: '',
'all' => $php && $curl && $pdo && $pcre && $ctype && $dom && $xml &&
$data && $cache && $tmp && $users && $favicons && $message == '' ? 'ok' : 'ko'
);
}
function generateSalt(): string {
return sha1(uniqid('' . mt_rand(), true) . implode('', stat(__FILE__) ?: []));
}
/**
* @throws FreshRSS_Context_Exception
*/
function initDb(): string {
$db = FreshRSS_Context::systemConf()->db;
if (empty($db['pdo_options'])) {
$db['pdo_options'] = [];
}
$db['pdo_options'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
FreshRSS_Context::systemConf()->db = $db; //TODO: Remove this Minz limitation "Indirect modification of overloaded property"
if (empty($db['type'])) {
$db['type'] = 'sqlite';
}
//Attempt to auto-create database if it does not already exist
if ($db['type'] !== 'sqlite') {
Minz_ModelPdo::$usesSharedPdo = false;
$dbBase = $db['base'] ?? '';
//For first connection, use default database for PostgreSQL, empty database for MySQL / MariaDB:
$db['base'] = $db['type'] === 'pgsql' ? 'postgres' : '';
FreshRSS_Context::systemConf()->db = $db;
try {
//First connection without database name to create the database
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
} catch (Exception $ex) {
$databaseDAO = null;
}
//Restore final database parameters for auto-creation and for future connections
$db['base'] = $dbBase;
FreshRSS_Context::systemConf()->db = $db;
if ($databaseDAO != null) {
//Perform database auto-creation
$databaseDAO->create();
}
}
//New connection with the database name
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
Minz_ModelPdo::$usesSharedPdo = true;
return $databaseDAO->testConnection();
}
function setupMigrations(): bool {
$migrations_path = APP_PATH . '/migrations';
$migrations_version_path = DATA_PATH . '/applied_migrations.txt';
$migrator = new Minz_Migrator($migrations_path);
$versions = implode("\n", $migrator->versions());
return @file_put_contents($migrations_version_path, $versions) !== false;
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/lib_rss.php'
<?php
declare(strict_types=1);
if (version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION, '<')) {
die(sprintf('FreshRSS error: FreshRSS requires PHP %s+!', FRESHRSS_MIN_PHP_VERSION));
}
if (!function_exists('array_is_list')) {
/**
* Polyfill for PHP <8.1
* https://php.net/array-is-list#127044
* @param array<mixed> $array
*/
function array_is_list(array $array): bool {
$i = -1;
foreach ($array as $k => $v) {
++$i;
if ($k !== $i) {
return false;
}
}
return true;
}
}
if (!function_exists('mb_strcut')) {
function mb_strcut(string $str, int $start, ?int $length = null, string $encoding = 'UTF-8'): string {
return substr($str, $start, $length) ?: '';
}
}
if (!function_exists('str_starts_with')) {
/** Polyfill for PHP <8.0 */
function str_starts_with(string $haystack, string $needle): bool {
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
if (!function_exists('syslog')) {
if (COPY_SYSLOG_TO_STDERR && !defined('STDERR')) {
define('STDERR', fopen('php://stderr', 'w'));
}
function syslog(int $priority, string $message): bool {
if (COPY_SYSLOG_TO_STDERR && defined('STDERR') && is_resource(STDERR)) {
return fwrite(STDERR, $message . "\n") != false;
}
return false;
}
}
if (function_exists('openlog')) {
if (COPY_SYSLOG_TO_STDERR) {
openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID | LOG_PERROR, LOG_USER);
} else {
openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID, LOG_USER);
}
}
/**
* Build a directory path by concatenating a list of directory names.
*
* @param string ...$path_parts a list of directory names
* @return string corresponding to the final pathname
*/
function join_path(...$path_parts): string {
return join(DIRECTORY_SEPARATOR, $path_parts);
}
//<Auto-loading>
function classAutoloader(string $class): void {
if (strpos($class, 'FreshRSS') === 0) {
$components = explode('_', $class);
switch (count($components)) {
case 1:
include(APP_PATH . '/' . $components[0] . '.php');
return;
case 2:
include(APP_PATH . '/Models/' . $components[1] . '.php');
return;
case 3: //Controllers, Exceptions
include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php');
return;
}
} elseif (strpos($class, 'Minz') === 0) {
include(LIB_PATH . '/' . str_replace('_', '/', $class) . '.php');
} elseif (strpos($class, 'SimplePie') === 0) {
include(LIB_PATH . '/SimplePie/' . str_replace('_', '/', $class) . '.php');
} elseif (str_starts_with($class, 'Gt\\CssXPath\\')) {
$prefix = 'Gt\\CssXPath\\';
$base_dir = LIB_PATH . '/phpgt/cssxpath/src/';
$relative_class_name = substr($class, strlen($prefix));
require $base_dir . str_replace('\\', '/', $relative_class_name) . '.php';
} elseif (str_starts_with($class, 'marienfressinaud\\LibOpml\\')) {
$prefix = 'marienfressinaud\\LibOpml\\';
$base_dir = LIB_PATH . '/marienfressinaud/lib_opml/src/LibOpml/';
$relative_class_name = substr($class, strlen($prefix));
require $base_dir . str_replace('\\', '/', $relative_class_name) . '.php';
} elseif (str_starts_with($class, 'PHPMailer\\PHPMailer\\')) {
$prefix = 'PHPMailer\\PHPMailer\\';
$base_dir = LIB_PATH . '/phpmailer/phpmailer/src/';
$relative_class_name = substr($class, strlen($prefix));
require $base_dir . str_replace('\\', '/', $relative_class_name) . '.php';
}
}
spl_autoload_register('classAutoloader');
//</Auto-loading>
/**
* Memory efficient replacement of `echo json_encode(...)`
* @param array<mixed>|mixed $json
* @param int $optimisationDepth Number of levels for which to perform memory optimisation
* before calling the faster native JSON serialisation.
* Set to negative value for infinite depth.
*/
function echoJson($json, int $optimisationDepth = -1): void {
if ($optimisationDepth === 0 || !is_array($json)) {
echo json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return;
}
$first = true;
if (array_is_list($json)) {
echo '[';
foreach ($json as $item) {
if ($first) {
$first = false;
} else {
echo ',';
}
echoJson($item, $optimisationDepth - 1);
}
echo ']';
} else {
echo '{';
foreach ($json as $key => $value) {
if ($first) {
$first = false;
} else {
echo ',';
}
echo json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), ':';
echoJson($value, $optimisationDepth - 1);
}
echo '}';
}
}
function idn_to_puny(string $url): string {
if (function_exists('idn_to_ascii')) {
$idn = parse_url($url, PHP_URL_HOST);
if (is_string($idn) && $idn != '') {
// https://wiki.php.net/rfc/deprecate-and-remove-intl_idna_variant_2003
if (defined('INTL_IDNA_VARIANT_UTS46')) {
$puny = idn_to_ascii($idn, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
} elseif (defined('INTL_IDNA_VARIANT_2003')) {
$puny = idn_to_ascii($idn, IDNA_DEFAULT, INTL_IDNA_VARIANT_2003);
} else {
$puny = idn_to_ascii($idn);
}
$pos = strpos($url, $idn);
if ($puny != false && $pos !== false) {
$url = substr_replace($url, $puny, $pos, strlen($idn));
}
}
}
return $url;
}
/**
* @return string|false
*/
function checkUrl(string $url, bool $fixScheme = true) {
$url = trim($url);
if ($url == '') {
return '';
}
if ($fixScheme && preg_match('#^https?://#i', $url) !== 1) {
$url = 'https://' . ltrim($url, '/');
}
$url = idn_to_puny($url); //PHP bug #53474 IDN
$urlRelaxed = str_replace('_', 'z', $url); //PHP discussion #64948 Underscore
if (is_string(filter_var($urlRelaxed, FILTER_VALIDATE_URL))) {
return $url;
} else {
return false;
}
}
function safe_ascii(?string $text): string {
return $text === null ? '' : (filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?: '');
}
if (function_exists('mb_convert_encoding')) {
function safe_utf8(string $text): string {
return mb_convert_encoding($text, 'UTF-8', 'UTF-8') ?: '';
}
} elseif (function_exists('iconv')) {
function safe_utf8(string $text): string {
return iconv('UTF-8', 'UTF-8//IGNORE', $text) ?: '';
}
} else {
function safe_utf8(string $text): string {
return $text;
}
}
function escapeToUnicodeAlternative(string $text, bool $extended = true): string {
$text = htmlspecialchars_decode($text, ENT_QUOTES);
//Problematic characters
$problem = array('&', '<', '>');
//Use their fullwidth Unicode form instead:
$replace = array('&', '<', '>');
// https://raw.githubusercontent.com/mihaip/google-reader-api/master/wiki/StreamId.wiki
if ($extended) {
$problem += array("'", '"', '^', '?', '\\', '/', ',', ';');
$replace += array("’", '"', '^', '?', '\', '/', ',', ';');
}
return trim(str_replace($problem, $replace, $text));
}
/** @param int|float $n */
function format_number($n, int $precision = 0): string {
// number_format does not seem to be Unicode-compatible
return str_replace(' ', ' ', // Thin non-breaking space
number_format((float)$n, $precision, '.', ' ')
);
}
function format_bytes(int $bytes, int $precision = 2, string $system = 'IEC'): string {
if ($system === 'IEC') {
$base = 1024;
$units = array('B', 'KiB', 'MiB', 'GiB', 'TiB');
} elseif ($system === 'SI') {
$base = 1000;
$units = array('B', 'KB', 'MB', 'GB', 'TB');
} else {
return format_number($bytes, $precision);
}
$bytes = max(intval($bytes), 0);
$pow = $bytes === 0 ? 0 : floor(log($bytes) / log($base));
$pow = min($pow, count($units) - 1);
$bytes /= pow($base, $pow);
return format_number($bytes, $precision) . ' ' . $units[$pow];
}
function timestamptodate(int $t, bool $hour = true): string {
$month = _t('gen.date.' . date('M', $t));
if ($hour) {
$date = _t('gen.date.format_date_hour', $month);
} else {
$date = _t('gen.date.format_date', $month);
}
return @date($date, $t) ?: '';
}
/**
* Decode HTML entities but preserve XML entities.
*/
function html_only_entity_decode(?string $text): string {
static $htmlEntitiesOnly = null;
if ($htmlEntitiesOnly === null) {
$htmlEntitiesOnly = array_flip(array_diff(
get_html_translation_table(HTML_ENTITIES, ENT_NOQUOTES, 'UTF-8'), //Decode HTML entities
get_html_translation_table(HTML_SPECIALCHARS, ENT_NOQUOTES, 'UTF-8') //Preserve XML entities
));
}
return $text == null ? '' : strtr($text, $htmlEntitiesOnly);
}
/**
* Remove passwords in FreshRSS logs.
* See also ../cli/sensitive-log.sh for Web server logs.
* @param array<string,mixed>|string $log
* @return array<string,mixed>|string
*/
function sensitive_log($log) {
if (is_array($log)) {
foreach ($log as $k => $v) {
if (in_array($k, ['api_key', 'Passwd', 'T'], true)) {
$log[$k] = '██';
} elseif (is_array($v) || is_string($v)) {
$log[$k] = sensitive_log($v);
} else {
return '';
}
}
} elseif (is_string($log)) {
$log = preg_replace([
'/\b(auth=.*?\/)[^&]+/i',
'/\b(Passwd=)[^&]+/i',
'/\b(Authorization)[^&]+/i',
], '$1█', $log) ?? '';
}
return $log;
}
/**
* @param array<string,mixed> $attributes
* @param array<int,mixed> $curl_options
* @throws FreshRSS_Context_Exception
*/
function customSimplePie(array $attributes = [], array $curl_options = []): SimplePie {
$limits = FreshRSS_Context::systemConf()->limits;
$simplePie = new SimplePie();
$simplePie->set_useragent(FRESHRSS_USERAGENT);
$simplePie->set_syslog(FreshRSS_Context::systemConf()->simplepie_syslog_enabled);
$simplePie->set_cache_name_function('sha1');
$simplePie->set_cache_location(CACHE_PATH);
$simplePie->set_cache_duration($limits['cache_duration']);
$simplePie->enable_order_by_date(false);
$feed_timeout = empty($attributes['timeout']) || !is_numeric($attributes['timeout']) ? 0 : (int)$attributes['timeout'];
$simplePie->set_timeout($feed_timeout > 0 ? $feed_timeout : $limits['timeout']);
$curl_options = array_replace(FreshRSS_Context::systemConf()->curl_options, $curl_options);
if (isset($attributes['ssl_verify'])) {
$curl_options[CURLOPT_SSL_VERIFYHOST] = $attributes['ssl_verify'] ? 2 : 0;
$curl_options[CURLOPT_SSL_VERIFYPEER] = (bool)$attributes['ssl_verify'];
if (!$attributes['ssl_verify']) {
$curl_options[CURLOPT_SSL_CIPHER_LIST] = 'DEFAULT@SECLEVEL=1';
}
}
if (!empty($attributes['curl_params']) && is_array($attributes['curl_params'])) {
foreach ($attributes['curl_params'] as $co => $v) {
$curl_options[$co] = $v;
}
}
$simplePie->set_curl_options($curl_options);
$simplePie->strip_comments(true);
$simplePie->strip_htmltags([
'base', 'blink', 'body', 'doctype', 'embed',
'font', 'form', 'frame', 'frameset', 'html',
'link', 'input', 'marquee', 'meta', 'noscript',
'object', 'param', 'plaintext', 'script', 'style',
'svg', //TODO: Support SVG after sanitizing and URL rewriting of xlink:href
]);
$simplePie->rename_attributes(['id', 'class']);
$simplePie->strip_attributes(array_merge($simplePie->strip_attributes, [
'autoplay', 'class', 'onload', 'onunload', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup',
'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur',
'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless', 'sizes', 'srcset']));
$simplePie->add_attributes([
'audio' => ['controls' => 'controls', 'preload' => 'none'],
'iframe' => [
'allow' => 'accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
'sandbox' => 'allow-scripts allow-same-origin',
],
'video' => ['controls' => 'controls', 'preload' => 'none'],
]);
$simplePie->set_url_replacements([
'a' => 'href',
'area' => 'href',
'audio' => 'src',
'blockquote' => 'cite',
'del' => 'cite',
'form' => 'action',
'iframe' => 'src',
'img' => [
'longdesc',
'src'
],
'input' => 'src',
'ins' => 'cite',
'q' => 'cite',
'source' => 'src',
'track' => 'src',
'video' => [
'poster',
'src',
],
]);
$https_domains = [];
$force = @file(FRESHRSS_PATH . '/force-https.default.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (is_array($force)) {
$https_domains = array_merge($https_domains, $force);
}
$force = @file(DATA_PATH . '/force-https.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (is_array($force)) {
$https_domains = array_merge($https_domains, $force);
}
$simplePie->set_https_domains($https_domains);
return $simplePie;
}
function sanitizeHTML(string $data, string $base = '', ?int $maxLength = null): string {
if ($data === '' || ($maxLength !== null && $maxLength <= 0)) {
return '';
}
if ($maxLength !== null) {
$data = mb_strcut($data, 0, $maxLength, 'UTF-8');
}
static $simplePie = null;
if ($simplePie == null) {
$simplePie = customSimplePie();
$simplePie->init();
}
$result = html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_HTML, $base));
if ($maxLength !== null && strlen($result) > $maxLength) {
//Sanitizing has made the result too long so try again shorter
$data = mb_strcut($result, 0, (2 * $maxLength) - strlen($result) - 2, 'UTF-8');
return sanitizeHTML($data, $base, $maxLength);
}
return $result;
}
function cleanCache(int $hours = 720): void {
// N.B.: GLOB_BRACE is not available on all platforms
$files = array_merge(
glob(CACHE_PATH . '/*.html', GLOB_NOSORT) ?: [],
glob(CACHE_PATH . '/*.json', GLOB_NOSORT) ?: [],
glob(CACHE_PATH . '/*.spc', GLOB_NOSORT) ?: [],
glob(CACHE_PATH . '/*.xml', GLOB_NOSORT) ?: []);
foreach ($files as $file) {
if (substr($file, -10) === 'index.html') {
continue;
}
$cacheMtime = @filemtime($file);
if ($cacheMtime !== false && $cacheMtime < time() - (3600 * $hours)) {
unlink($file);
}
}
}
/**
* Remove the charset meta information of an HTML document, e.g.:
* `<meta charset="..." />`
* `<meta http-equiv="Content-Type" content="text/html; charset=...">`
*/
function stripHtmlMetaCharset(string $html): string {
return preg_replace('/<meta\s[^>]*charset\s*=\s*[^>]+>/i', '', $html, 1) ?? '';
}
/**
* Set an XML preamble to enforce the HTML content type charset received by HTTP.
* @param string $html the raw downloaded HTML content
* @param string $contentType an HTTP Content-Type such as 'text/html; charset=utf-8'
* @return string an HTML string with XML encoding information for DOMDocument::loadHTML()
*/
function enforceHttpEncoding(string $html, string $contentType = ''): string {
$httpCharset = preg_match('/\bcharset=([0-9a-z_-]{2,12})$/i', $contentType, $matches) === 1 ? $matches[1] : '';
if ($httpCharset == '') {
// No charset defined by HTTP
if (preg_match('/<meta\s[^>]*charset\s*=[\s\'"]*UTF-?8\b/i', substr($html, 0, 2048))) {
// Detect UTF-8 even if declared too deep in HTML for DOMDocument
$httpCharset = 'UTF-8';
} else {
// Do nothing
return $html;
}
}
$httpCharsetNormalized = SimplePie_Misc::encoding($httpCharset);
if (in_array($httpCharsetNormalized, ['windows-1252', 'US-ASCII'], true)) {
// Default charset for HTTP, do nothing
return $html;
}
if (substr($html, 0, 3) === "\xEF\xBB\xBF" || // UTF-8 BOM
substr($html, 0, 2) === "\xFF\xFE" || // UTF-16 Little Endian BOM
substr($html, 0, 2) === "\xFE\xFF" || // UTF-16 Big Endian BOM
substr($html, 0, 4) === "\xFF\xFE\x00\x00" || // UTF-32 Little Endian BOM
substr($html, 0, 4) === "\x00\x00\xFE\xFF") { // UTF-32 Big Endian BOM
// Existing byte order mark, do nothing
return $html;
}
if (preg_match('/^<[?]xml[^>]+encoding\b/', substr($html, 0, 64))) {
// Existing XML declaration, do nothing
return $html;
}
if ($httpCharsetNormalized !== 'UTF-8') {
// Try to change encoding to UTF-8 using mbstring or iconv or intl
$utf8 = SimplePie_Misc::change_encoding($html, $httpCharsetNormalized, 'UTF-8');
if (is_string($utf8)) {
$html = stripHtmlMetaCharset($utf8);
$httpCharsetNormalized = 'UTF-8';
}
}
if ($httpCharsetNormalized === 'UTF-8') {
// Save encoding information as XML declaration
return '<' . '?xml version="1.0" encoding="' . $httpCharsetNormalized . '" ?' . ">\n" . $html;
}
// Give up
return $html;
}
/**
* @param string $type {html,json,opml,xml}
* @param array<string,mixed> $attributes
* @param array<int,mixed> $curl_options
*/
function httpGet(string $url, string $cachePath, string $type = 'html', array $attributes = [], array $curl_options = []): string {
$limits = FreshRSS_Context::systemConf()->limits;
$feed_timeout = empty($attributes['timeout']) || !is_numeric($attributes['timeout']) ? 0 : intval($attributes['timeout']);
$cacheMtime = @filemtime($cachePath);
if ($cacheMtime !== false && $cacheMtime > time() - intval($limits['cache_duration'])) {
$body = @file_get_contents($cachePath);
if ($body != false) {
syslog(LOG_DEBUG, 'FreshRSS uses cache for ' . SimplePie_Misc::url_remove_credentials($url));
return $body;
}
}
if (mt_rand(0, 30) === 1) { // Remove old entries once in a while
cleanCache(CLEANCACHE_HOURS);
}
if (FreshRSS_Context::systemConf()->simplepie_syslog_enabled) {
syslog(LOG_INFO, 'FreshRSS GET ' . $type . ' ' . SimplePie_Misc::url_remove_credentials($url));
}
$accept = '*/*;q=0.8';
switch ($type) {
case 'json':
$accept = 'application/json,application/feed+json,application/javascript;q=0.9,text/javascript;q=0.8,*/*;q=0.7';
break;
case 'opml':
$accept = 'text/x-opml,text/xml;q=0.9,application/xml;q=0.9,*/*;q=0.8';
break;
case 'xml':
$accept = 'application/xml,application/xhtml+xml,text/xml;q=0.9,*/*;q=0.8';
break;
case 'html':
default:
$accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
break;
}
// TODO: Implement HTTP 1.1 conditional GET If-Modified-Since
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => array('Accept: ' . $accept),
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
CURLOPT_MAXREDIRS => 4,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '', //Enable all encodings
//CURLOPT_VERBOSE => 1, // To debug sent HTTP headers
]);
curl_setopt_array($ch, FreshRSS_Context::systemConf()->curl_options);
if (isset($attributes['curl_params']) && is_array($attributes['curl_params'])) {
curl_setopt_array($ch, $attributes['curl_params']);
}
if (isset($attributes['ssl_verify'])) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (bool)$attributes['ssl_verify']);
if (!$attributes['ssl_verify']) {
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1');
}
}
curl_setopt_array($ch, $curl_options);
$body = curl_exec($ch);
$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$c_content_type = '' . curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$c_error = curl_error($ch);
curl_close($ch);
if ($c_status != 200 || $c_error != '' || $body === false) {
Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
$body = '';
// TODO: Implement HTTP 410 Gone
} elseif (!is_string($body) || strlen($body) === 0) {
$body = '';
} else {
$body = trim($body, " \n\r\t\v"); // Do not trim \x00 to avoid breaking a BOM
if ($type !== 'json') {
$body = enforceHttpEncoding($body, $c_content_type);
}
}
if (file_put_contents($cachePath, $body) === false) {
Minz_Log::warning("Error saving cache $cachePath for $url");
}
return $body;
}
/**
* Validate an email address, supports internationalized addresses.
*
* @param string $email The address to validate
* @return bool true if email is valid, else false
*/
function validateEmailAddress(string $email): bool {
$mailer = new PHPMailer\PHPMailer\PHPMailer();
$mailer->CharSet = 'utf-8';
$punyemail = $mailer->punyencodeAddress($email);
return PHPMailer\PHPMailer\PHPMailer::validateAddress($punyemail, 'html5');
}
/**
* Add support of image lazy loading
* Move content from src attribute to data-original
* @param string $content is the text we want to parse
*/
function lazyimg(string $content): string {
return preg_replace([
'/<((?:img|iframe)[^>]+?)src="([^"]+)"([^>]*)>/i',
"/<((?:img|iframe)[^>]+?)src='([^']+)'([^>]*)>/i",
], [
'<$1src="' . Minz_Url::display('/themes/icons/grey.gif') . '" data-original="$2"$3>',
"<$1src='" . Minz_Url::display('/themes/icons/grey.gif') . "' data-original='$2'$3>",
],
$content
) ?? '';
}
/** @return numeric-string */
function uTimeString(): string {
$t = @gettimeofday();
$result = $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT);
/** @var numeric-string @result */
return $result;
}
function invalidateHttpCache(string $username = ''): bool {
if (!FreshRSS_user_Controller::checkUsername($username)) {
Minz_Session::_param('touch', uTimeString());
$username = Minz_User::name() ?? Minz_User::INTERNAL_USER;
}
return FreshRSS_UserDAO::ctouch($username);
}
/**
* @return array<string>
*/
function listUsers(): array {
$final_list = array();
$base_path = join_path(DATA_PATH, 'users');
$dir_list = array_values(array_diff(
scandir($base_path) ?: [],
['..', '.', Minz_User::INTERNAL_USER]
));
foreach ($dir_list as $file) {
if ($file[0] !== '.' && is_dir(join_path($base_path, $file)) && file_exists(join_path($base_path, $file, 'config.php'))) {
$final_list[] = $file;
}
}
return $final_list;
}
/**
* Return if the maximum number of registrations has been reached.
* Note a max_registrations of 0 means there is no limit.
*
* @return bool true if number of users >= max registrations, false else.
*/
function max_registrations_reached(): bool {
$limit_registrations = FreshRSS_Context::systemConf()->limits['max_registrations'];
$number_accounts = count(listUsers());
return $limit_registrations > 0 && $number_accounts >= $limit_registrations;
}
/**
* Register and return the configuration for a given user.
*
* Note this function has been created to generate temporary configuration
* objects. If you need a long-time configuration, please don't use this function.
*
* @param string $username the name of the user of which we want the configuration.
* @return FreshRSS_UserConfiguration|null object, or null if the configuration cannot be loaded.
* @throws Minz_ConfigurationNamespaceException
*/
function get_user_configuration(string $username): ?FreshRSS_UserConfiguration {
if (!FreshRSS_user_Controller::checkUsername($username)) {
return null;
}
$namespace = 'user_' . $username;
try {
FreshRSS_UserConfiguration::register($namespace,
USERS_PATH . '/' . $username . '/config.php',
FRESHRSS_PATH . '/config-user.default.php');
} catch (Minz_FileNotExistException $e) {
Minz_Log::warning($e->getMessage(), ADMIN_LOG);
return null;
}
$user_conf = FreshRSS_UserConfiguration::get($namespace);
return $user_conf;
}
/**
* Converts an IP (v4 or v6) to a binary representation using inet_pton
*
* @param string $ip the IP to convert
* @return string a binary representation of the specified IP
*/
function ipToBits(string $ip): string {
$binaryip = '';
foreach (str_split(inet_pton($ip) ?: '') as $char) {
$binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
}
/**
* Check if an ip belongs to the provided range (in CIDR format)
*
* @param string $ip the IP that we want to verify (ex: 192.168.16.1)
* @param string $range the range to check against (ex: 192.168.16.0/24)
* @return bool true if the IP is in the range, otherwise false
*/
function checkCIDR(string $ip, string $range): bool {
$binary_ip = ipToBits($ip);
$split = explode('/', $range);
$subnet = $split[0] ?? '';
if ($subnet == '') {
return false;
}
$binary_subnet = ipToBits($subnet);
$mask_bits = $split[1] ?? '';
$mask_bits = (int)$mask_bits;
if ($mask_bits === 0) {
$mask_bits = null;
}
$ip_net_bits = substr($binary_ip, 0, $mask_bits);
$subnet_bits = substr($binary_subnet, 0, $mask_bits);
return $ip_net_bits === $subnet_bits;
}
/**
* Use CONN_REMOTE_ADDR (if available, to be robust even when using Apache mod_remoteip) or REMOTE_ADDR environment variable to determine the connection IP.
*/
function connectionRemoteAddress(): string {
$remoteIp = $_SERVER['CONN_REMOTE_ADDR'] ?? '';
if ($remoteIp == '') {
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? '';
}
if ($remoteIp == 0) {
$remoteIp = '';
}
return $remoteIp;
}
/**
* Check if the client (e.g. last proxy) is allowed to send unsafe headers.
* This uses the `TRUSTED_PROXY` environment variable or the `trusted_sources` configuration option to get an array of the authorized ranges,
* The connection IP is obtained from the `CONN_REMOTE_ADDR` (if available, to be robust even when using Apache mod_remoteip) or `REMOTE_ADDR` environment variables.
* @return bool true if the sender’s IP is in one of the ranges defined in the configuration, else false
*/
function checkTrustedIP(): bool {
if (!FreshRSS_Context::hasSystemConf()) {
return false;
}
$remoteIp = connectionRemoteAddress();
if ($remoteIp === '') {
return false;
}
$trusted = getenv('TRUSTED_PROXY');
if ($trusted != 0 && is_string($trusted)) {
$trusted = preg_split('/\s+/', $trusted, -1, PREG_SPLIT_NO_EMPTY);
}
if (!is_array($trusted) || empty($trusted)) {
$trusted = FreshRSS_Context::systemConf()->trusted_sources;
}
foreach ($trusted as $cidr) {
if (checkCIDR($remoteIp, $cidr)) {
return true;
}
}
return false;
}
function httpAuthUser(bool $onlyTrusted = true): string {
if (!empty($_SERVER['REMOTE_USER'])) {
return $_SERVER['REMOTE_USER'];
}
if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
return $_SERVER['REDIRECT_REMOTE_USER'];
}
if (!$onlyTrusted || checkTrustedIP()) {
if (!empty($_SERVER['HTTP_REMOTE_USER'])) {
return $_SERVER['HTTP_REMOTE_USER'];
}
if (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) {
return $_SERVER['HTTP_X_WEBAUTH_USER'];
}
}
return '';
}
function cryptAvailable(): bool {
$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
return $hash === @crypt('password', $hash);
}
/**
* Check PHP and its extensions are well-installed.
*
* @return array<string,bool> of tested values.
*/
function check_install_php(): array {
$pdo_mysql = extension_loaded('pdo_mysql');
$pdo_pgsql = extension_loaded('pdo_pgsql');
$pdo_sqlite = extension_loaded('pdo_sqlite');
return array(
'php' => version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION) >= 0,
'curl' => extension_loaded('curl'),
'pdo' => $pdo_mysql || $pdo_sqlite || $pdo_pgsql,
'pcre' => extension_loaded('pcre'),
'ctype' => extension_loaded('ctype'),
'fileinfo' => extension_loaded('fileinfo'),
'dom' => class_exists('DOMDocument'),
'json' => extension_loaded('json'),
'mbstring' => extension_loaded('mbstring'),
'zip' => extension_loaded('zip'),
);
}
/**
* Check different data files and directories exist.
* @return array<string,bool> of tested values.
*/
function check_install_files(): array {
return [
'data' => is_dir(DATA_PATH) && touch(DATA_PATH . '/index.html'), // is_writable() is not reliable for a folder on NFS
'cache' => is_dir(CACHE_PATH) && touch(CACHE_PATH . '/index.html'),
'users' => is_dir(USERS_PATH) && touch(USERS_PATH . '/index.html'),
'favicons' => is_dir(DATA_PATH) && touch(DATA_PATH . '/favicons/index.html'),
'tokens' => is_dir(DATA_PATH) && touch(DATA_PATH . '/tokens/index.html'),
];
}
/**
* Check database is well-installed.
*
* @return array<string,bool> of tested values.
*/
function check_install_database(): array {
$status = array(
'connection' => true,
'tables' => false,
'categories' => false,
'feeds' => false,
'entries' => false,
'entrytmp' => false,
'tag' => false,
'entrytag' => false,
);
try {
$dbDAO = FreshRSS_Factory::createDatabaseDAO();
$status['tables'] = $dbDAO->tablesAreCorrect();
$status['categories'] = $dbDAO->categoryIsCorrect();
$status['feeds'] = $dbDAO->feedIsCorrect();
$status['entries'] = $dbDAO->entryIsCorrect();
$status['entrytmp'] = $dbDAO->entrytmpIsCorrect();
$status['tag'] = $dbDAO->tagIsCorrect();
$status['entrytag'] = $dbDAO->entrytagIsCorrect();
} catch (Minz_PDOConnectionException $e) {
$status['connection'] = false;
}
return $status;
}
/**
* Remove a directory recursively.
* From http://php.net/rmdir#110489
*/
function recursive_unlink(string $dir): bool {
if (!is_dir($dir)) {
return true;
}
$files = array_diff(scandir($dir) ?: [], ['.', '..']);
foreach ($files as $filename) {
$filename = $dir . '/' . $filename;
if (is_dir($filename)) {
@chmod($filename, 0777);
recursive_unlink($filename);
} else {
unlink($filename);
}
}
return rmdir($dir);
}
/**
* Remove queries where $get is appearing.
* @param string $get the get attribute which should be removed.
* @param array<int,array<string,string|int>> $queries an array of queries.
* @return array<int,array<string,string|int>> without queries where $get is appearing.
*/
function remove_query_by_get(string $get, array $queries): array {
$final_queries = array();
foreach ($queries as $key => $query) {
if (empty($query['get']) || $query['get'] !== $get) {
$final_queries[$key] = $query;
}
}
return $final_queries;
}
function _i(string $icon, int $type = FreshRSS_Themes::ICON_DEFAULT): string {
return FreshRSS_Themes::icon($icon, $type);
}
const SHORTCUT_KEYS = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Backspace', 'Delete',
'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab',
];
/**
* @param array<string> $shortcuts
* @return array<string>
*/
function getNonStandardShortcuts(array $shortcuts): array {
$standard = strtolower(implode(' ', SHORTCUT_KEYS));
$nonStandard = array_filter($shortcuts, static function (string $shortcut) use ($standard) {
$shortcut = trim($shortcut);
return $shortcut !== '' && stripos($standard, $shortcut) === false;
});
return $nonStandard;
}
function errorMessageInfo(string $errorTitle, string $error = ''): string {
$errorTitle = htmlspecialchars($errorTitle, ENT_NOQUOTES, 'UTF-8');
$message = '';
$details = '';
$error = trim($error);
// Prevent empty tags by checking if error is not empty first
if ($error !== '') {
$error = htmlspecialchars($error, ENT_NOQUOTES, 'UTF-8') . "\n";
// First line is the main message, other lines are the details
list($message, $details) = explode("\n", $error, 2);
$message = "<h2>{$message}</h2>";
$details = "<pre>{$details}</pre>";
}
header("Content-Security-Policy: default-src 'self'");
return <<<MSG
<!DOCTYPE html><html><header><title>HTTP 500: {$errorTitle}</title></header><body>
<h1>HTTP 500: {$errorTitle}</h1>
{$message}
{$details}
<hr />
<small>For help see the documentation: <a href="https://freshrss.github.io/FreshRSS/en/admins/logs_and_errors.html" target="_blank">
https://freshrss.github.io/FreshRSS/en/admins/logs_and_errors.html</a></small>
</body></html>
MSG;
}