use Friendica\Network\Probe;
use Friendica\Object\Image;
use Friendica\Util\XML;
-
-require_once 'library/slinky.php';
+use DOMDocument;
+use DomXPath;
class Network
{
/**
- * @brief Curl wrapper
+ * Curl wrapper
*
* If binary flag is true, return binary results.
* Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt")
* to preserve cookies from one request to the next.
*
+ * @brief Curl wrapper
* @param string $url URL to fetch
* @param boolean $binary default false
* TRUE if asked to return binary results (file download)
*
* @return string The fetched content
*/
- public static function fetchURL($url, $binary = false, &$redirects = 0, $timeout = 0, $accept_content = null, $cookiejar = 0)
+ public static function fetchUrl($url, $binary = false, &$redirects = 0, $timeout = 0, $accept_content = null, $cookiejar = 0)
{
- $ret = self::zFetchURL(
+ $ret = self::fetchUrlFull($url, $binary, $redirects, $timeout, $accept_content, $cookiejar);
+
+ return $ret['body'];
+ }
+
+ /**
+ * Curl wrapper with array of return values.
+ *
+ * Inner workings and parameters are the same as @ref fetchUrl but returns an array with
+ * all the information collected during the fetch.
+ *
+ * @brief Curl wrapper with array of return values.
+ * @param string $url URL to fetch
+ * @param boolean $binary default false
+ * TRUE if asked to return binary results (file download)
+ * @param integer $redirects The recursion counter for internal use - default 0
+ * @param integer $timeout Timeout in seconds, default system config value or 60 seconds
+ * @param string $accept_content supply Accept: header with 'accept_content' as the value
+ * @param string $cookiejar Path to cookie jar file
+ *
+ * @return array With all relevant information, 'body' contains the actual fetched content.
+ */
+ public static function fetchUrlFull($url, $binary = false, &$redirects = 0, $timeout = 0, $accept_content = null, $cookiejar = 0)
+ {
+ return self::curl(
$url,
$binary,
$redirects,
'cookiejar'=>$cookiejar
]
);
-
- return($ret['body']);
}
/**
* string 'header' => HTTP headers
* string 'body' => fetched content
*/
- public static function zFetchURL($url, $binary = false, &$redirects = 0, $opts = [])
+ public static function curl($url, $binary = false, &$redirects = 0, $opts = [])
{
$ret = ['return_code' => 0, 'success' => false, 'header' => '', 'info' => '', 'body' => ''];
$a = get_app();
- if (self::blockedURL($url)) {
- logger('z_fetch_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
+ $parts = parse_url($url);
+ $path_parts = explode('/', defaults($parts, 'path', ''));
+ foreach ($path_parts as $part) {
+ if (strlen($part) <> mb_strlen($part)) {
+ $parts2[] = rawurlencode($part);
+ } else {
+ $parts2[] = $part;
+ }
+ }
+ $parts['path'] = implode('/', $parts2);
+ $url = self::unparseURL($parts);
+
+ if (self::isUrlBlocked($url)) {
+ logger('domain of ' . $url . ' is blocked', LOGGER_DATA);
return $ret;
}
}
if (curl_errno($ch) !== CURLE_OK) {
- logger('fetch_url error fetching ' . $url . ': ' . curl_error($ch), LOGGER_NORMAL);
+ logger('error fetching ' . $url . ': ' . curl_error($ch), LOGGER_INFO);
}
$ret['errno'] = curl_errno($ch);
$http_code = $curl_info['http_code'];
- logger('fetch_url ' . $url . ': ' . $http_code . " " . $s, LOGGER_DATA);
+ logger($url . ': ' . $http_code . " " . $s, LOGGER_DATA);
$header = '';
// Pull out multiple headers, e.g. proxy and continuation headers
$newurl = $curl_info['redirect_url'];
- if (($new_location_info['path'] == '') && ( $new_location_info['host'] != '')) {
+ if (empty($new_location_info['path']) && !empty($new_location_info['host'])) {
$newurl = $new_location_info['scheme'] . '://' . $new_location_info['host'] . $old_location_info['path'];
}
if (strpos($newurl, '/') === 0) {
$newurl = $old_location_info["scheme"]."://".$old_location_info["host"].$newurl;
}
+ $old_location_query = @parse_url($url, PHP_URL_QUERY);
+
+ if ($old_location_query != '') {
+ $newurl .= '?' . $old_location_query;
+ }
if (filter_var($newurl, FILTER_VALIDATE_URL)) {
$redirects++;
@curl_close($ch);
- return self::zFetchURL($newurl, $binary, $redirects, $opts);
+ return self::curl($newurl, $binary, $redirects, $opts);
}
}
if (!$ret['success']) {
$ret['error'] = curl_error($ch);
$ret['debug'] = $curl_info;
- logger('z_fetch_url: error: '.$url.': '.$ret['return_code'].' - '.$ret['error'], LOGGER_DEBUG);
- logger('z_fetch_url: debug: '.print_r($curl_info, true), LOGGER_DATA);
+ logger('error: '.$url.': '.$ret['return_code'].' - '.$ret['error'], LOGGER_DEBUG);
+ logger('debug: '.print_r($curl_info, true), LOGGER_DATA);
}
$ret['body'] = substr($s, strlen($header));
*
* @return string The content
*/
- public static function postURL($url, $params, $headers = null, &$redirects = 0, $timeout = 0)
+ public static function post($url, $params, $headers = null, &$redirects = 0, $timeout = 0)
{
$stamp1 = microtime(true);
- if (self::blockedURL($url)) {
+ if (self::isUrlBlocked($url)) {
logger('post_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
return false;
}
if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
$matches = [];
+ $new_location_info = @parse_url($curl_info['redirect_url']);
+ $old_location_info = @parse_url($curl_info['url']);
+
preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches);
$newurl = trim(array_pop($matches));
if (filter_var($newurl, FILTER_VALIDATE_URL)) {
$redirects++;
logger('post_url: redirect ' . $url . ' to ' . $newurl);
- return self::postURL($newurl, $params, $headers, $redirects, $timeout);
+ return self::post($newurl, $params, $headers, $redirects, $timeout);
}
}
}
/**
- * Generic XML return
- * Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable
- * of $st and an optional text <message> of $message and terminates the current process.
- */
- public static function xmlStatus($st, $message = '')
- {
- $result = ['status' => $st];
-
- if ($message != '') {
- $result['message'] = $message;
- }
-
- if ($st) {
- logger('xml_status returning non_zero: ' . $st . " message=" . $message);
- }
-
- header("Content-type: text/xml");
-
- $xmldata = ["result" => $result];
-
- echo XML::fromArray($xmldata, $xml);
-
- killme();
- }
-
- /**
- * @brief Send HTTP status header and exit.
- *
- * @param integer $val HTTP status result value
- * @param array $description optional message
- * 'title' => header title
- * 'description' => optional message
- */
- public static function httpStatusExit($val, $description = [])
- {
- $err = '';
- if ($val >= 400) {
- $err = 'Error';
- if (!isset($description["title"])) {
- $description["title"] = $err." ".$val;
- }
- }
-
- if ($val >= 200 && $val < 300) {
- $err = 'OK';
- }
-
- logger('http_status_exit ' . $val);
- header($_SERVER["SERVER_PROTOCOL"] . ' ' . $val . ' ' . $err);
-
- if (isset($description["title"])) {
- $tpl = get_markup_template('http_status.tpl');
- echo replace_macros(
- $tpl,
- [
- '$title' => $description["title"],
- '$description' => $description["description"]]
- );
- }
-
- killme();
- }
-
- /**
- * @brief Check URL to se if ts's real
+ * @brief Check URL to see if it's real
*
* Take a URL from the wild, prepend http:// if necessary
* and check DNS to see if it's real (or check if is a valid IP address)
* @param string $url The URL to be validated
* @return string|boolean The actual working URL, false else
*/
- public static function validateURL($url)
+ public static function isUrlValid($url)
{
if (Config::get('system', 'disable_url_validation')) {
return $url;
/// @TODO Really suppress function outcomes? Why not find them + debug them?
$h = @parse_url($url);
- if ((is_array($h)) && (@dns_get_record($h['host'], DNS_A + DNS_CNAME + DNS_PTR) || filter_var($h['host'], FILTER_VALIDATE_IP) )) {
+ if ((is_array($h)) && (@dns_get_record($h['host'], DNS_A + DNS_CNAME) || filter_var($h['host'], FILTER_VALIDATE_IP) )) {
return $url;
}
* @param string $addr The email address
* @return boolean True if it's a valid email address, false if it's not
*/
- public static function validateEmail($addr)
+ public static function isEmailDomainValid($addr)
{
if (Config::get('system', 'disable_email_validation')) {
return true;
$h = substr($addr, strpos($addr, '@') + 1);
- if (($h) && (dns_get_record($h, DNS_A + DNS_CNAME + DNS_PTR + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP) )) {
+ // Concerning the @ see here: https://stackoverflow.com/questions/36280957/dns-get-record-a-temporary-server-error-occurred
+ if ($h && (@dns_get_record($h, DNS_A + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP) )) {
+ return true;
+ }
+ if ($h && @dns_get_record($h, DNS_CNAME + DNS_MX)) {
return true;
}
return false;
* @param string $url URL which get tested
* @return boolean True if url is allowed otherwise return false
*/
- public static function allowedURL($url)
+ public static function isUrlAllowed($url)
{
$h = @parse_url($url);
*
* @return boolean
*/
- public static function blockedURL($url)
+ public static function isUrlBlocked($url)
{
- $h = @parse_url($url);
-
- if (! $h) {
- return true;
+ $host = @parse_url($url, PHP_URL_HOST);
+ if (!$host) {
+ return false;
}
$domain_blocklist = Config::get('system', 'blocklist', []);
- if (! $domain_blocklist) {
+ if (!$domain_blocklist) {
return false;
}
- $host = strtolower($h['host']);
-
foreach ($domain_blocklist as $domain_block) {
- if (strtolower($domain_block['domain']) == $host) {
+ if (strcasecmp($domain_block['domain'], $host) === 0) {
return true;
}
}
* @return boolean False if not allowed, true if allowed
* or if allowed list is not configured
*/
- public static function allowedEmail($email)
+ public static function isEmailDomainAllowed($email)
{
$domain = strtolower(substr($email, strpos($email, '@') + 1));
if (!$domain) {
$allowed = explode(',', $str_allowed);
- return self::allowedDomain($domain, $allowed);
+ return self::isDomainAllowed($domain, $allowed);
}
/**
* @param array $domain_list
* @return boolean
*/
- public static function allowedDomain($domain, array $domain_list)
+ public static function isDomainAllowed($domain, array $domain_list)
{
$found = false;
return $found;
}
- public static function avatarImg($email)
+ public static function lookupAvatarByEmail($email)
{
$avatar['size'] = 175;
$avatar['email'] = $email;
return $avatar['url'];
}
- public static function parseXmlString($s, $strict = true)
- {
- // the "strict" parameter is deactivated
-
- /// @todo Move this function to the xml class
- libxml_use_internal_errors(true);
-
- $x = @simplexml_load_string($s);
- if (!$x) {
- logger('libxml: parse: error: ' . $s, LOGGER_DATA);
- foreach (libxml_get_errors() as $err) {
- logger('libxml: parse: ' . $err->code." at ".$err->line.":".$err->column." : ".$err->message, LOGGER_DATA);
- }
- libxml_clear_errors();
- }
- return $x;
- }
-
- public static function scaleExternalImages($srctext, $include_link = true, $scale_replace = false)
- {
- // Suppress "view full size"
- if (intval(Config::get('system', 'no_view_full_size'))) {
- $include_link = false;
- }
-
- // Picture addresses can contain special characters
- $s = htmlspecialchars_decode($srctext);
-
- $matches = null;
- $c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER);
- if ($c) {
- foreach ($matches as $mtch) {
- logger('scale_external_image: ' . $mtch[1]);
-
- $hostname = str_replace('www.', '', substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3));
- if (stristr($mtch[1], $hostname)) {
- continue;
- }
-
- // $scale_replace, if passed, is an array of two elements. The
- // first is the name of the full-size image. The second is the
- // name of a remote, scaled-down version of the full size image.
- // This allows Friendica to display the smaller remote image if
- // one exists, while still linking to the full-size image
- if ($scale_replace) {
- $scaled = str_replace($scale_replace[0], $scale_replace[1], $mtch[1]);
- } else {
- $scaled = $mtch[1];
- }
- $i = self::fetchURL($scaled);
- if (! $i) {
- return $srctext;
- }
-
- // guess mimetype from headers or filename
- $type = Image::guessType($mtch[1], true);
-
- if ($i) {
- $Image = new Image($i, $type);
- if ($Image->isValid()) {
- $orig_width = $Image->getWidth();
- $orig_height = $Image->getHeight();
-
- if ($orig_width > 640 || $orig_height > 640) {
- $Image->scaleDown(640);
- $new_width = $Image->getWidth();
- $new_height = $Image->getHeight();
- logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG);
- $s = str_replace(
- $mtch[0],
- '[img=' . $new_width . 'x' . $new_height. ']' . $scaled . '[/img]'
- . "\n" . (($include_link)
- ? '[url=' . $mtch[1] . ']' . L10n::t('view full size') . '[/url]' . "\n"
- : ''),
- $s
- );
- logger('scale_external_images: new string: ' . $s, LOGGER_DEBUG);
- }
- }
- }
- }
- }
-
- // replace the special char encoding
- $s = htmlspecialchars($s, ENT_NOQUOTES, 'UTF-8');
- return $s;
- }
-
- public static function fixContactSslPolicy(&$contact, $new_policy)
- {
- $ssl_changed = false;
- if ((intval($new_policy) == SSL_POLICY_SELFSIGN || $new_policy === 'self') && strstr($contact['url'], 'https:')) {
- $ssl_changed = true;
- $contact['url'] = str_replace('https:', 'http:', $contact['url']);
- $contact['request'] = str_replace('https:', 'http:', $contact['request']);
- $contact['notify'] = str_replace('https:', 'http:', $contact['notify']);
- $contact['poll'] = str_replace('https:', 'http:', $contact['poll']);
- $contact['confirm'] = str_replace('https:', 'http:', $contact['confirm']);
- $contact['poco'] = str_replace('https:', 'http:', $contact['poco']);
- }
-
- if ((intval($new_policy) == SSL_POLICY_FULL || $new_policy === 'full') && strstr($contact['url'], 'http:')) {
- $ssl_changed = true;
- $contact['url'] = str_replace('http:', 'https:', $contact['url']);
- $contact['request'] = str_replace('http:', 'https:', $contact['request']);
- $contact['notify'] = str_replace('http:', 'https:', $contact['notify']);
- $contact['poll'] = str_replace('http:', 'https:', $contact['poll']);
- $contact['confirm'] = str_replace('http:', 'https:', $contact['confirm']);
- $contact['poco'] = str_replace('http:', 'https:', $contact['poco']);
- }
-
- if ($ssl_changed) {
- $fields = ['url' => $contact['url'], 'request' => $contact['request'],
- 'notify' => $contact['notify'], 'poll' => $contact['poll'],
- 'confirm' => $contact['confirm'], 'poco' => $contact['poco']];
- dba::update('contact', $fields, ['id' => $contact['id']]);
- }
- }
-
/**
* @brief Remove Google Analytics and other tracking platforms params from URL
*
public static function stripTrackingQueryParams($url)
{
$urldata = parse_url($url);
- if (is_string($urldata["query"])) {
+ if (!empty($urldata["query"])) {
$query = $urldata["query"];
parse_str($query, $querydata);
* @param bool $fetchbody Wether to fetch the body or not after the HEAD requests
* @return string A canonical URL
*/
- public static function originalURL($url, $depth = 1, $fetchbody = false)
+ public static function finalUrl($url, $depth = 1, $fetchbody = false)
{
$a = get_app();
- $url = strip_tracking_query_params($url);
+ $url = self::stripTrackingQueryParams($url);
if ($depth > 10) {
- return($url);
+ return $url;
}
$url = trim($url, "'");
$a->save_timestamp($stamp1, "network");
if ($http_code == 0) {
- return($url);
+ return $url;
}
- if ((($curl_info['http_code'] == "301") || ($curl_info['http_code'] == "302"))
- && (($curl_info['redirect_url'] != "") || ($curl_info['location'] != ""))
- ) {
- if ($curl_info['redirect_url'] != "") {
- return(original_url($curl_info['redirect_url'], ++$depth, $fetchbody));
- } else {
- return(original_url($curl_info['location'], ++$depth, $fetchbody));
+ if (in_array($http_code, ['301', '302'])) {
+ if (!empty($curl_info['redirect_url'])) {
+ return self::finalUrl($curl_info['redirect_url'], ++$depth, $fetchbody);
+ } elseif (!empty($curl_info['location'])) {
+ return self::finalUrl($curl_info['location'], ++$depth, $fetchbody);
}
}
// Check for redirects in the meta elements of the body if there are no redirects in the header.
if (!$fetchbody) {
- return(original_url($url, ++$depth, true));
+ return(self::finalUrl($url, ++$depth, true));
}
// if the file is too large then exit
if ($curl_info["download_content_length"] > 1000000) {
- return($url);
+ return $url;
}
// if it isn't a HTML file then exit
- if (($curl_info["content_type"] != "") && !strstr(strtolower($curl_info["content_type"]), "html")) {
- return($url);
+ if (!empty($curl_info["content_type"]) && !strstr(strtolower($curl_info["content_type"]), "html")) {
+ return $url;
}
$stamp1 = microtime(true);
$a->save_timestamp($stamp1, "network");
if (trim($body) == "") {
- return($url);
+ return $url;
}
// Check for redirect in meta elements
$pathinfo = explode(";", $path);
foreach ($pathinfo as $value) {
if (substr(strtolower($value), 0, 4) == "url=") {
- return(original_url(substr($value, 4), ++$depth));
+ return self::finalUrl(substr($value, 4), ++$depth);
}
}
}
return $url;
}
- public static function shortLink($url)
- {
- $slinky = new Slinky($url);
- $yourls_url = Config::get('yourls', 'url1');
- if ($yourls_url) {
- $yourls_username = Config::get('yourls', 'username1');
- $yourls_password = Config::get('yourls', 'password1');
- $yourls_ssl = Config::get('yourls', 'ssl1');
- $yourls = new Slinky_YourLS();
- $yourls->set('username', $yourls_username);
- $yourls->set('password', $yourls_password);
- $yourls->set('ssl', $yourls_ssl);
- $yourls->set('yourls-url', $yourls_url);
- $slinky->set_cascade([$yourls, new Slinky_Ur1ca(), new Slinky_TinyURL()]);
- } else {
- // setup a cascade of shortening services
- // try to get a short link from these services
- // in the order ur1.ca, tinyurl
- $slinky->set_cascade([new Slinky_Ur1ca(), new Slinky_TinyURL()]);
- }
- return $slinky->short();
- }
-
- /**
- * @brief Encodes content to json
- *
- * This function encodes an array to json format
- * and adds an application/json HTTP header to the output.
- * After finishing the process is getting killed.
- *
- * @param array $x The input content
- */
- public static function jsonReturnAndDie($x)
- {
- header("content-type: application/json");
- echo json_encode($x);
- killme();
- }
-
/**
* @brief Find the matching part between two url
*
* @param string $url2
* @return string The matching part
*/
- public static function matchingURL($url1, $url2)
+ public static function getUrlMatch($url1, $url2)
{
if (($url1 == "") || ($url2 == "")) {
return "";
return "";
}
+ if (empty($parts1["scheme"])) {
+ $parts1["scheme"] = '';
+ }
+ if (empty($parts2["scheme"])) {
+ $parts2["scheme"] = '';
+ }
+
if ($parts1["scheme"] != $parts2["scheme"]) {
return "";
}
+ if (empty($parts1["host"])) {
+ $parts1["host"] = '';
+ }
+ if (empty($parts2["host"])) {
+ $parts2["host"] = '';
+ }
+
if ($parts1["host"] != $parts2["host"]) {
return "";
}
+ if (empty($parts1["port"])) {
+ $parts1["port"] = '';
+ }
+ if (empty($parts2["port"])) {
+ $parts2["port"] = '';
+ }
+
if ($parts1["port"] != $parts2["port"]) {
return "";
}
$match .= ":".$parts1["port"];
}
+ if (empty($parts1["path"])) {
+ $parts1["path"] = '';
+ }
+ if (empty($parts2["path"])) {
+ $parts2["path"] = '';
+ }
+
$pathparts1 = explode("/", $parts1["path"]);
$pathparts2 = explode("/", $parts2["path"]);
$i = 0;
$path = "";
do {
- $path1 = $pathparts1[$i];
- $path2 = $pathparts2[$i];
+ $path1 = defaults($pathparts1, $i, '');
+ $path2 = defaults($pathparts2, $i, '');
if ($path1 == $path2) {
$path .= $path1."/";
*
* @return string The glued URL
*/
- public static function unParseURL($parsed)
+ public static function unparseURL($parsed)
{
$get = function ($key) use ($parsed) {
return isset($parsed[$key]) ? $parsed[$key] : null;