<?php
/**
- * @file src/Network/Probe.php
+ * @copyright Copyright (C) 2020, Friendica
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
*/
-namespace Friendica\Network;
-/**
- * @file src/Network/Probe.php
- * @brief Functions for probing URL
- */
+namespace Friendica\Network;
use DOMDocument;
-use Friendica\Core\Cache;
-use Friendica\Core\Config;
+use DomXPath;
+use Friendica\Core\Cache\Duration;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
-use Friendica\Core\System;
use Friendica\Database\DBA;
+use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Profile;
+use Friendica\Protocol\ActivityNamespace;
+use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Email;
use Friendica\Protocol\Feed;
-use Friendica\Protocol\ActivityPub;
use Friendica\Util\Crypto;
-use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use Friendica\Util\Strings;
use Friendica\Util\XML;
-use DomXPath;
-
-require_once 'include/dba.php';
/**
- * @brief This class contain functions for probing URL
- *
+ * This class contain functions for probing URL
*/
class Probe
{
private static $baseurl;
+ private static $istimeout;
+
+ /**
+ * Remove stuff from an URI that doesn't belong there
+ *
+ * @param string $URI
+ * @return string Cleaned URI
+ */
+ public static function cleanURI(string $URI)
+ {
+ // At first remove leading and trailing junk
+ $URI = trim($URI, "@#?:/ \t\n\r\0\x0B");
+
+ $parts = parse_url($URI);
+
+ if (empty($parts['scheme'])) {
+ return $URI;
+ }
+
+ // Remove the URL fragment, since these shouldn't be part of any profile URL
+ unset($parts['fragment']);
+
+ $URI = Network::unparseURL($parts);
+
+ return $URI;
+ }
/**
- * @brief Rearrange the array so that it always has the same order
+ * Rearrange the array so that it always has the same order
*
* @param array $data Unordered data
*
*/
private static function rearrangeData($data)
{
- $fields = ["name", "nick", "guid", "url", "addr", "alias",
- "photo", "community", "keywords", "location", "about",
+ $fields = ["name", "nick", "guid", "url", "addr", "alias", "photo", "account-type",
+ "community", "keywords", "location", "about", "hide",
"batch", "notify", "poll", "request", "confirm", "poco",
+ "following", "followers", "inbox", "outbox", "sharedinbox",
"priority", "network", "pubkey", "baseurl"];
$newdata = [];
}
/**
- * @brief Check if the hostname belongs to the own server
+ * Check if the hostname belongs to the own server
*
* @param string $host The hostname that is to be checked
*
*/
private static function ownHost($host)
{
- $own_host = get_app()->getHostName();
+ $own_host = DI::baseUrl()->getHostname();
$parts = parse_url($host);
}
/**
- * @brief Probes for webfinger path via "host-meta"
+ * Probes for webfinger path via "host-meta"
*
* We have to check if the servers in the future still will offer this.
* It seems as if it was dropped from the standard.
* @param string $host The host part of an url
*
* @return array with template and type of the webfinger template for JSON or XML
+ * @throws HTTPException\InternalServerErrorException
*/
private static function hostMeta($host)
{
$ssl_url = "https://".$host."/.well-known/host-meta";
$url = "http://".$host."/.well-known/host-meta";
- $xrd_timeout = Config::get('system', 'xrd_timeout', 20);
- $redirects = 0;
+ $xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20);
Logger::log("Probing for ".$host, Logger::DEBUG);
$xrd = null;
- $curlResult = Network::curl($ssl_url, false, $redirects, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']);
+ $curlResult = Network::curl($ssl_url, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']);
+ $ssl_connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0);
if ($curlResult->isSuccess()) {
$xml = $curlResult->getBody();
$xrd = XML::parseString($xml, false);
$host_url = 'https://'.$host;
+ } elseif ($curlResult->isTimeout()) {
+ Logger::info('Probing timeout', ['url' => $ssl_url], Logger::DEBUG);
+ self::$istimeout = true;
+ return false;
}
if (!is_object($xrd)) {
- $curlResult = Network::curl($url, false, $redirects, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']);
+ $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']);
+ $connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0);
if ($curlResult->isTimeout()) {
- Logger::log("Probing timeout for " . $url, Logger::DEBUG);
+ Logger::info('Probing timeout', ['url' => $url], Logger::DEBUG);
+ self::$istimeout = true;
+ return false;
+ } elseif ($connection_error && $ssl_connection_error) {
+ self::$istimeout = true;
return false;
}
+
$xml = $curlResult->getBody();
$xrd = XML::parseString($xml, false);
$host_url = 'http://'.$host;
return [];
}
- $lrdd = [];
- // The following webfinger path is defined in RFC 7033 https://tools.ietf.org/html/rfc7033
- // Problem is that Hubzilla currently doesn't provide all data in the JSON webfinger
- // compared to the XML webfinger. So this is commented out by now.
- // $lrdd = array("application/jrd+json" => $host_url.'/.well-known/webfinger?resource={uri}');
+ $lrdd = ['application/jrd+json' => $host_url . '/.well-known/webfinger?resource={uri}'];
foreach ($links["xrd"]["link"] as $value => $link) {
if (!empty($link["@attributes"])) {
continue;
}
- if (($attributes["rel"] == "lrdd") && !empty($attributes["template"])) {
+ if (!empty($attributes["rel"]) && $attributes["rel"] == "lrdd" && !empty($attributes["template"])) {
$type = (empty($attributes["type"]) ? '' : $attributes["type"]);
$lrdd[$type] = $attributes["template"];
}
}
- self::$baseurl = "http://".$host;
+ self::$baseurl = $host_url;
Logger::log("Probing successful for ".$host, Logger::DEBUG);
}
/**
- * @brief Perform Webfinger lookup and return DFRN data
+ * Perform Webfinger lookup and return DFRN data
*
* Given an email style address, perform webfinger lookup and
* return the resulting DFRN profile URL, or if no DFRN profile URL
* @param string $hcard_url Link to the hcard - is returned by reference
*
* @return string profile link
+ * @throws HTTPException\InternalServerErrorException
*/
public static function webfingerDfrn($webbie, &$hcard_url)
{
$links = self::lrdd($webbie);
Logger::log('webfingerDfrn: '.$webbie.':'.print_r($links, true), Logger::DATA);
- if (count($links)) {
+ if (!empty($links) && is_array($links)) {
foreach ($links as $link) {
- if ($link['@attributes']['rel'] === NAMESPACE_DFRN) {
+ if ($link['@attributes']['rel'] === ActivityNamespace::DFRN) {
$profile_link = $link['@attributes']['href'];
}
- if (($link['@attributes']['rel'] === NAMESPACE_OSTATUSSUB) && ($profile_link == "")) {
+ if (($link['@attributes']['rel'] === ActivityNamespace::OSTATUSSUB) && ($profile_link == "")) {
$profile_link = 'stat:'.$link['@attributes']['template'];
}
if ($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard') {
}
/**
- * @brief Check an URI for LRDD data
+ * Get the link for the remote follow page for a given profile link
+ *
+ * @param sting $profile
+ * @return string Remote follow page link
+ */
+ public static function getRemoteFollowLink(string $profile)
+ {
+ $follow_link = '';
+
+ $links = self::lrdd($profile);
+
+ if (!empty($links) && is_array($links)) {
+ foreach ($links as $link) {
+ if ($link['@attributes']['rel'] === ActivityNamespace::OSTATUSSUB) {
+ $follow_link = $link['@attributes']['template'];
+ }
+ }
+ }
+ return $follow_link;
+ }
+
+ /**
+ * Check an URI for LRDD data
*
* this is a replacement for the "lrdd" function.
* It isn't used in this class and has some redundancies in the code.
* @param string $uri Address that should be probed
*
* @return array uri data
+ * @throws HTTPException\InternalServerErrorException
*/
public static function lrdd($uri)
{
}
/**
- * @brief Fetch information (protocol endpoints and user information) about a given uri
+ * Fetch information (protocol endpoints and user information) about a given uri
*
* @param string $uri Address that should be probed
* @param string $network Test for this specific network
* @param boolean $cache Use cached values?
*
* @return array uri data
+ * @throws HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
- public static function uri($uri, $network = "", $uid = -1, $cache = true)
+ public static function uri($uri, $network = '', $uid = -1, $cache = true)
{
if ($cache) {
- $result = Cache::get("Probe::uri:".$network.":".$uri);
+ $result = DI::cache()->get('Probe::uri:' . $network . ':' . $uri);
if (!is_null($result)) {
return $result;
}
$uid = local_user();
}
+ self::$istimeout = false;
+
if ($network != Protocol::ACTIVITYPUB) {
$data = self::detect($uri, $network, $uid);
} else {
$data = null;
}
- $ap_profile = ActivityPub::probeProfile($uri);
+ // When the previous detection process had got a time out
+ // we could falsely detect a Friendica profile as AP profile.
+ if (!self::$istimeout) {
+ $ap_profile = ActivityPub::probeProfile($uri);
- if (!empty($ap_profile) && (defaults($data, 'network', '') != Protocol::DFRN)) {
- $data = $ap_profile;
+ if (empty($data) || (!empty($ap_profile) && empty($network) && (($data['network'] ?? '') != Protocol::DFRN))) {
+ $data = $ap_profile;
+ } elseif (!empty($ap_profile)) {
+ $ap_profile['batch'] = '';
+ $data = array_merge($ap_profile, $data);
+ }
+ } else {
+ Logger::notice('Time out detected. AP will not be probed.', ['uri' => $uri]);
}
- if (!isset($data["url"])) {
- $data["url"] = $uri;
+ if (!isset($data['url'])) {
+ $data['url'] = $uri;
}
- if (x($data, "photo")) {
- $data["baseurl"] = Network::getUrlMatch(Strings::normaliseLink(defaults($data, "baseurl", "")), Strings::normaliseLink($data["photo"]));
- } else {
- $data["photo"] = System::baseUrl().'/images/person-300.jpg';
+ if (!empty($data['photo']) && !empty($data['baseurl'])) {
+ $data['baseurl'] = Network::getUrlMatch(Strings::normaliseLink($data['baseurl']), Strings::normaliseLink($data['photo']));
+ } elseif (empty($data['photo'])) {
+ $data['photo'] = DI::baseUrl() . '/images/person-300.jpg';
}
- if (empty($data["name"])) {
- if (!empty($data["nick"])) {
- $data["name"] = $data["nick"];
+ if (empty($data['name'])) {
+ if (!empty($data['nick'])) {
+ $data['name'] = $data['nick'];
}
- if (!x($data, "name")) {
- $data["name"] = $data["url"];
+ if (empty($data['name'])) {
+ $data['name'] = $data['url'];
}
}
- if (empty($data["nick"])) {
- $data["nick"] = strtolower($data["name"]);
+ if (empty($data['nick'])) {
+ $data['nick'] = strtolower($data['name']);
if (strpos($data['nick'], ' ')) {
$data['nick'] = trim(substr($data['nick'], 0, strpos($data['nick'], ' ')));
}
if (!empty(self::$baseurl)) {
- $data["baseurl"] = self::$baseurl;
+ $data['baseurl'] = self::$baseurl;
}
- if (empty($data["network"])) {
- $data["network"] = Protocol::PHANTOM;
+ if (empty($data['network'])) {
+ $data['network'] = Protocol::PHANTOM;
+ }
+
+ // Ensure that local connections always are DFRN
+ if (($network == '') && ($data['network'] != Protocol::PHANTOM) && (self::ownHost($data['baseurl'] ?? '') || self::ownHost($data['url']))) {
+ $data['network'] = Protocol::DFRN;
+ }
+
+ if (!isset($data['hide']) && in_array($data['network'], Protocol::FEDERATED)) {
+ $data['hide'] = self::getHideStatus($data['url']);
}
$data = self::rearrangeData($data);
// Only store into the cache if the value seems to be valid
if (!in_array($data['network'], [Protocol::PHANTOM, Protocol::MAIL])) {
- Cache::set("Probe::uri:".$network.":".$uri, $data, Cache::DAY);
-
- /// @todo temporary fix - we need a real contact update function that updates only changing fields
- /// The biggest problem is the avatar picture that could have a reduced image size.
- /// It should only be updated if the existing picture isn't existing anymore.
- /// We only update the contact when it is no probing for a specific network.
- if (($data['network'] != Protocol::FEED)
- && ($network == "")
- && $data["name"]
- && $data["nick"]
- && $data["url"]
- && $data["addr"]
- && $data["poll"]
- ) {
- $fields = ['name' => $data['name'],
- 'nick' => $data['nick'],
- 'url' => $data['url'],
- 'addr' => $data['addr'],
- 'photo' => $data['photo'],
- 'keywords' => $data['keywords'],
- 'location' => $data['location'],
- 'about' => $data['about'],
- 'notify' => $data['notify'],
- 'network' => $data['network'],
- 'server_url' => $data['baseurl']];
-
- // This doesn't cover the case when a community isn't a community anymore
- if (!empty($data['community']) && $data['community']) {
- $fields['community'] = $data['community'];
- $fields['contact-type'] = Contact::ACCOUNT_TYPE_COMMUNITY;
- }
-
- $fieldnames = [];
-
- foreach ($fields as $key => $val) {
- if (empty($val)) {
- unset($fields[$key]);
- } else {
- $fieldnames[] = $key;
- }
- }
-
- $fields['updated'] = DateTimeFormat::utcNow();
-
- $condition = ['nurl' => Strings::normaliseLink($data["url"])];
-
- $old_fields = DBA::selectFirst('gcontact', $fieldnames, $condition);
-
- // When the gcontact doesn't exist, the value "true" will trigger an insert.
- // In difference to the public contacts we want to have every contact
- // in the world in our global contacts.
- if (!$old_fields) {
- $old_fields = true;
-
- // These values have to be set only on insert
- $fields['photo'] = $data['photo'];
- $fields['created'] = DateTimeFormat::utcNow();
- }
-
- DBA::update('gcontact', $fields, $condition, $old_fields);
-
- $fields = ['name' => $data['name'],
- 'nick' => $data['nick'],
- 'url' => $data['url'],
- 'addr' => $data['addr'],
- 'alias' => $data['alias'],
- 'keywords' => $data['keywords'],
- 'location' => $data['location'],
- 'about' => $data['about'],
- 'batch' => $data['batch'],
- 'notify' => $data['notify'],
- 'poll' => $data['poll'],
- 'request' => $data['request'],
- 'confirm' => $data['confirm'],
- 'poco' => $data['poco'],
- 'network' => $data['network'],
- 'pubkey' => $data['pubkey'],
- 'priority' => $data['priority'],
- 'writable' => true,
- 'rel' => Contact::SHARING];
-
- $fieldnames = [];
-
- foreach ($fields as $key => $val) {
- if (empty($val)) {
- unset($fields[$key]);
- } else {
- $fieldnames[] = $key;
- }
- }
-
- $condition = ['nurl' => Strings::normaliseLink($data["url"]), 'self' => false, 'uid' => 0];
-
- // "$old_fields" will return a "false" when the contact doesn't exist.
- // This won't trigger an insert. This is intended, since we only need
- // public contacts for everyone we store items from.
- // We don't need to store every contact on the planet.
- $old_fields = DBA::selectFirst('contact', $fieldnames, $condition);
-
- $fields['name-date'] = DateTimeFormat::utcNow();
- $fields['uri-date'] = DateTimeFormat::utcNow();
- $fields['success_update'] = DateTimeFormat::utcNow();
-
- DBA::update('contact', $fields, $condition, $old_fields);
- }
+ DI::cache()->set('Probe::uri:' . $network . ':' . $uri, $data, Duration::DAY);
}
return $data;
}
+
/**
- * @brief Switch the scheme of an url between http and https
+ * Fetches the "hide" status from the profile
*
- * @param string $url URL
+ * @param string $url URL of the profile
*
- * @return string switched URL
+ * @return boolean "hide" status
*/
- private static function switchScheme($url)
+ private static function getHideStatus($url)
{
- $parts = parse_url($url);
+ $curlResult = Network::curl($url);
+ if (!$curlResult->isSuccess()) {
+ return false;
+ }
- if (!isset($parts['scheme'])) {
- return $url;
+ // If the file is too large then exit
+ if (($curlResult->getInfo()['download_content_length'] ?? 0) > 1000000) {
+ return false;
}
- if ($parts['scheme'] == 'http') {
- $url = str_replace('http://', 'https://', $url);
- } elseif ($parts['scheme'] == 'https') {
- $url = str_replace('https://', 'http://', $url);
+ // If it isn't a HTML file then exit
+ if (($curlResult->getContentType() != '') && !strstr(strtolower($curlResult->getContentType()), 'html')) {
+ return false;
+ }
+
+ $body = $curlResult->getBody();
+
+ $doc = new DOMDocument();
+ @$doc->loadHTML($body);
+
+ $xpath = new DOMXPath($doc);
+
+ $list = $xpath->query('//meta[@name]');
+ foreach ($list as $node) {
+ $meta_tag = [];
+ if ($node->attributes->length) {
+ foreach ($node->attributes as $attribute) {
+ $meta_tag[$attribute->name] = $attribute->value;
+ }
+ }
+
+ if (empty($meta_tag['content'])) {
+ continue;
+ }
+
+ $content = strtolower(trim($meta_tag['content']));
+
+ switch (strtolower(trim($meta_tag['name']))) {
+ case 'dfrn-global-visibility':
+ if ($content == 'false') {
+ return true;
+ }
+ break;
+ case 'robots':
+ if (strpos($content, 'noindex') !== false) {
+ return true;
+ }
+ break;
+ }
}
- return $url;
+ return false;
}
/**
- * @brief Checks if a profile url should be OStatus but only provides partial information
+ * Checks if a profile url should be OStatus but only provides partial information
*
* @param array $webfinger Webfinger data
* @param string $lrdd Path template for webfinger request
* @param string $type type
*
* @return array fixed webfinger data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function fixOStatus($webfinger, $lrdd, $type)
{
$has_key = false;
foreach ($webfinger['links'] as $link) {
- if ($link['rel'] == NAMESPACE_OSTATUSSUB) {
+ if ($link['rel'] == ActivityNamespace::OSTATUSSUB) {
$is_ostatus = true;
}
if ($link['rel'] == 'magic-public-key') {
return $webfinger;
}
- $url = self::switchScheme($webfinger['subject']);
+ $url = Network::switchScheme($webfinger['subject']);
$path = str_replace('{uri}', urlencode($url), $lrdd);
$webfinger2 = self::webfinger($path, $type);
}
/**
- * @brief Fetch information (protocol endpoints and user information) about a given uri
+ * Fetch information (protocol endpoints and user information) about a given uri
*
* This function is only called by the "uri" function that adds caching and rearranging of data.
*
* @param integer $uid User ID for the probe (only used for mails)
*
* @return array uri data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function detect($uri, $network, $uid)
{
$parts = parse_url($uri);
- if (!empty($parts["scheme"]) && !empty($parts["host"]) && !empty($parts["path"])) {
+ if (!empty($parts["scheme"]) && !empty($parts["host"])) {
$host = $parts["host"];
if (!empty($parts["port"])) {
$host .= ':'.$parts["port"];
}
if ($host == 'twitter.com') {
- return ["network" => Protocol::TWITTER];
+ return self::twitter($uri);
}
$lrdd = self::hostMeta($host);
return [];
}
- $path_parts = explode("/", trim($parts["path"], "/"));
+ $path_parts = explode("/", trim($parts['path'] ?? '', "/"));
while (!$lrdd && (sizeof($path_parts) > 1)) {
$host .= "/".array_shift($path_parts);
$nick = substr($uri, 0, strpos($uri, '@'));
if (strpos($uri, '@twitter.com')) {
- return ["network" => Protocol::TWITTER];
+ return self::twitter($uri);
}
$lrdd = self::hostMeta($host);
if ((!$result && ($network == "")) || ($network == Protocol::OSTATUS)) {
$result = self::ostatus($webfinger);
}
+ if (in_array($network, ['', Protocol::ZOT])) {
+ $result = self::zot($webfinger, $result);
+ }
if ((!$result && ($network == "")) || ($network == Protocol::PUMPIO)) {
$result = self::pumpio($webfinger, $addr);
}
Logger::log($uri." is ".$result["network"], Logger::DEBUG);
- if (empty($result["baseurl"])) {
+ if (empty($result["baseurl"]) && ($result["network"] != Protocol::PHANTOM)) {
$pos = strpos($result["url"], $host);
if ($pos) {
$result["baseurl"] = substr($result["url"], 0, $pos).$host;
}
/**
- * @brief Perform a webfinger request.
+ * Check for Zot contact
+ *
+ * @param array $webfinger Webfinger data
+ * @param array $data previously probed data
+ *
+ * @return array Zot data
+ * @throws HTTPException\InternalServerErrorException
+ */
+ private static function zot($webfinger, $data)
+ {
+ if (!empty($webfinger["aliases"]) && is_array($webfinger["aliases"])) {
+ foreach ($webfinger["aliases"] as $alias) {
+ if (substr($alias, 0, 5) == 'acct:') {
+ $data["addr"] = substr($alias, 5);
+ }
+ }
+ }
+
+ if (!empty($webfinger["subject"]) && (substr($webfinger["subject"], 0, 5) == "acct:")) {
+ $data["addr"] = substr($webfinger["subject"], 5);
+ }
+
+ $zot_url = '';
+ foreach ($webfinger['links'] as $link) {
+ if (($link['rel'] == 'http://purl.org/zot/protocol') && !empty($link['href'])) {
+ $zot_url = $link['href'];
+ }
+ }
+
+ if (empty($zot_url) && !empty($data['addr']) && !empty(self::$baseurl)) {
+ $condition = ['nurl' => Strings::normaliseLink(self::$baseurl), 'platform' => ['hubzilla']];
+ if (!DBA::exists('gserver', $condition)) {
+ return $data;
+ }
+ $zot_url = self::$baseurl . '/.well-known/zot-info?address=' . $data['addr'];
+ }
+
+ if (empty($zot_url)) {
+ return $data;
+ }
+
+ $data = self::pollZot($zot_url, $data);
+
+ if (!empty($data['url']) && !empty($webfinger['aliases']) && is_array($webfinger['aliases'])) {
+ foreach ($webfinger['aliases'] as $alias) {
+ if (!strstr($alias, '@') && Strings::normaliseLink($alias) != Strings::normaliseLink($data['url'])) {
+ $data['alias'] = $alias;
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ public static function pollZot($url, $data)
+ {
+ $curlResult = Network::curl($url);
+ if ($curlResult->isTimeout()) {
+ return $data;
+ }
+ $content = $curlResult->getBody();
+ if (!$content) {
+ return $data;
+ }
+
+ $json = json_decode($content, true);
+ if (!is_array($json)) {
+ return $data;
+ }
+
+ if (empty($data['network'])) {
+ if (!empty($json['protocols']) && in_array('zot', $json['protocols'])) {
+ $data['network'] = Protocol::ZOT;
+ } elseif (!isset($json['protocols'])) {
+ $data['network'] = Protocol::ZOT;
+ }
+ }
+
+ if (!empty($json['guid']) && empty($data['guid'])) {
+ $data['guid'] = $json['guid'];
+ }
+ if (!empty($json['key']) && empty($data['pubkey'])) {
+ $data['pubkey'] = $json['key'];
+ }
+ if (!empty($json['name'])) {
+ $data['name'] = $json['name'];
+ }
+ if (!empty($json['photo'])) {
+ $data['photo'] = $json['photo'];
+ if (!empty($json['photo_updated'])) {
+ $data['photo'] .= '?rev=' . urlencode($json['photo_updated']);
+ }
+ }
+ if (!empty($json['address'])) {
+ $data['addr'] = $json['address'];
+ }
+ if (!empty($json['url'])) {
+ $data['url'] = $json['url'];
+ }
+ if (!empty($json['connections_url'])) {
+ $data['poco'] = $json['connections_url'];
+ }
+ if (isset($json['searchable'])) {
+ $data['hide'] = !$json['searchable'];
+ }
+ if (!empty($json['public_forum'])) {
+ $data['community'] = $json['public_forum'];
+ $data['account-type'] = Contact::PAGE_COMMUNITY;
+ }
+
+ if (!empty($json['profile'])) {
+ $profile = $json['profile'];
+ if (!empty($profile['description'])) {
+ $data['about'] = $profile['description'];
+ }
+ if (!empty($profile['keywords'])) {
+ $keywords = implode(', ', $profile['keywords']);
+ if (!empty($keywords)) {
+ $data['keywords'] = $keywords;
+ }
+ }
+
+ $loc = [];
+ if (!empty($profile['region'])) {
+ $loc['region'] = $profile['region'];
+ }
+ if (!empty($profile['country'])) {
+ $loc['country-name'] = $profile['country'];
+ }
+ $location = Profile::formatLocation($loc);
+ if (!empty($location)) {
+ $data['location'] = $location;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Perform a webfinger request.
*
* For details see RFC 7033: <https://tools.ietf.org/html/rfc7033>
*
* @param string $type type
*
* @return array webfinger data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function webfinger($url, $type)
{
- $xrd_timeout = Config::get('system', 'xrd_timeout', 20);
- $redirects = 0;
+ $xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20);
- $curlResult = Network::curl($url, false, $redirects, ['timeout' => $xrd_timeout, 'accept_content' => $type]);
+ $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout, 'accept_content' => $type]);
if ($curlResult->isTimeout()) {
+ self::$istimeout = true;
return false;
}
$data = $curlResult->getBody();
}
/**
- * @brief Poll the Friendica specific noscrape page.
+ * Poll the Friendica specific noscrape page.
*
* "noscrape" is a faster alternative to fetch the data from the hcard.
* This functionality was originally created for the directory.
* @param array $data The already fetched data
*
* @return array noscrape data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function pollNoscrape($noscrape_url, $data)
{
$curlResult = Network::curl($noscrape_url);
if ($curlResult->isTimeout()) {
+ self::$istimeout = true;
return false;
}
$content = $curlResult->getBody();
}
if (!empty($json["tags"])) {
- $keywords = implode(" ", $json["tags"]);
+ $keywords = implode(", ", $json["tags"]);
if ($keywords != "") {
$data["keywords"] = $keywords;
}
$data["poll"] = $json["dfrn-poll"];
}
+ if (isset($json["hide"])) {
+ $data["hide"] = (bool)$json["hide"];
+ } else {
+ $data["hide"] = false;
+ }
+
return $data;
}
/**
- * @brief Check for valid DFRN data
+ * Check for valid DFRN data
*
* @param array $data DFRN data
*
}
/**
- * @brief Fetch data from a DFRN profile page and via "noscrape"
+ * Fetch data from a DFRN profile page and via "noscrape"
*
* @param string $profile_link Link to the profile page
*
* @return array profile data
+ * @throws HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
public static function profile($profile_link)
{
if (empty($data["addr"]) || empty($data["nick"])) {
$probe_data = self::uri($profile_link);
- $data["addr"] = defaults($data, "addr", $probe_data["addr"]);
- $data["nick"] = defaults($data, "nick", $probe_data["nick"]);
+ $data["addr"] = ($data["addr"] ?? '') ?: $probe_data["addr"];
+ $data["nick"] = ($data["nick"] ?? '') ?: $probe_data["nick"];
}
$prof_data["addr"] = $data["addr"];
$prof_data["nick"] = $data["nick"];
- $prof_data["dfrn-request"] = defaults($data, 'request', null);
- $prof_data["dfrn-confirm"] = defaults($data, 'confirm', null);
- $prof_data["dfrn-notify"] = defaults($data, 'notify' , null);
- $prof_data["dfrn-poll"] = defaults($data, 'poll' , null);
- $prof_data["photo"] = defaults($data, 'photo' , null);
- $prof_data["fn"] = defaults($data, 'name' , null);
- $prof_data["key"] = defaults($data, 'pubkey' , null);
+ $prof_data["dfrn-request"] = $data['request'] ?? null;
+ $prof_data["dfrn-confirm"] = $data['confirm'] ?? null;
+ $prof_data["dfrn-notify"] = $data['notify'] ?? null;
+ $prof_data["dfrn-poll"] = $data['poll'] ?? null;
+ $prof_data["photo"] = $data['photo'] ?? null;
+ $prof_data["fn"] = $data['name'] ?? null;
+ $prof_data["key"] = $data['pubkey'] ?? null;
Logger::log("Result for profile ".$profile_link.": ".print_r($prof_data, true), Logger::DEBUG);
}
/**
- * @brief Check for DFRN contact
+ * Check for DFRN contact
*
* @param array $webfinger Webfinger data
*
* @return array DFRN data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function dfrn($webfinger)
{
$hcard_url = "";
$data = [];
- foreach ($webfinger["links"] as $link) {
- if (($link["rel"] == NAMESPACE_DFRN) && !empty($link["href"])) {
+ // The array is reversed to take into account the order of preference for same-rel links
+ // See: https://tools.ietf.org/html/rfc7033#section-4.4.4
+ foreach (array_reverse($webfinger["links"]) as $link) {
+ if (($link["rel"] == ActivityNamespace::DFRN) && !empty($link["href"])) {
$data["network"] = Protocol::DFRN;
- } elseif (($link["rel"] == NAMESPACE_FEED) && !empty($link["href"])) {
+ } elseif (($link["rel"] == ActivityNamespace::FEED) && !empty($link["href"])) {
$data["poll"] = $link["href"];
- } elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") && (defaults($link, "type", "") == "text/html") && !empty($link["href"])) {
+ } elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") && (($link["type"] ?? "") == "text/html") && !empty($link["href"])) {
$data["url"] = $link["href"];
} elseif (($link["rel"] == "http://microformats.org/profile/hcard") && !empty($link["href"])) {
$hcard_url = $link["href"];
- } elseif (($link["rel"] == NAMESPACE_POCO) && !empty($link["href"])) {
+ } elseif (($link["rel"] == ActivityNamespace::POCO) && !empty($link["href"])) {
$data["poco"] = $link["href"];
} elseif (($link["rel"] == "http://webfinger.net/rel/avatar") && !empty($link["href"])) {
$data["photo"] = $link["href"];
}
/**
- * @brief Poll the hcard page (Diaspora and Friendica specific)
+ * Poll the hcard page (Diaspora and Friendica specific)
*
* @param string $hcard_url Link to the hcard page
* @param array $data The already fetched data
* @param boolean $dfrn Poll DFRN specific data
*
* @return array hcard data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function pollHcard($hcard_url, $data, $dfrn = false)
{
$curlResult = Network::curl($hcard_url);
if ($curlResult->isTimeout()) {
+ self::$istimeout = true;
return false;
}
$content = $curlResult->getBody();
}
/**
- * @brief Check for Diaspora contact
+ * Check for Diaspora contact
*
* @param array $webfinger Webfinger data
*
* @return array Diaspora data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function diaspora($webfinger)
{
$hcard_url = "";
$data = [];
- foreach ($webfinger["links"] as $link) {
+
+ // The array is reversed to take into account the order of preference for same-rel links
+ // See: https://tools.ietf.org/html/rfc7033#section-4.4.4
+ foreach (array_reverse($webfinger["links"]) as $link) {
if (($link["rel"] == "http://microformats.org/profile/hcard") && !empty($link["href"])) {
$hcard_url = $link["href"];
} elseif (($link["rel"] == "http://joindiaspora.com/seed_location") && !empty($link["href"])) {
$data["baseurl"] = trim($link["href"], '/');
} elseif (($link["rel"] == "http://joindiaspora.com/guid") && !empty($link["href"])) {
$data["guid"] = $link["href"];
- } elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") && (defaults($link, "type", "") == "text/html") && !empty($link["href"])) {
+ } elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") && (($link["type"] ?? "") == "text/html") && !empty($link["href"])) {
$data["url"] = $link["href"];
- } elseif (($link["rel"] == NAMESPACE_FEED) && !empty($link["href"])) {
+ } elseif (($link["rel"] == ActivityNamespace::FEED) && !empty($link["href"])) {
$data["poll"] = $link["href"];
- } elseif (($link["rel"] == NAMESPACE_POCO) && !empty($link["href"])) {
+ } elseif (($link["rel"] == ActivityNamespace::POCO) && !empty($link["href"])) {
$data["poco"] = $link["href"];
} elseif (($link["rel"] == "salmon") && !empty($link["href"])) {
$data["notify"] = $link["href"];
}
}
- if (!isset($data["url"]) || ($hcard_url == "")) {
+ if (empty($data["url"]) || empty($hcard_url)) {
return false;
}
return false;
}
- if (isset($data["url"])
- && isset($data["guid"])
- && isset($data["baseurl"])
- && isset($data["pubkey"])
- && ($hcard_url != "")
+ if (!empty($data["url"])
+ && !empty($data["guid"])
+ && !empty($data["baseurl"])
+ && !empty($data["pubkey"])
+ && !empty($hcard_url)
) {
$data["network"] = Protocol::DIASPORA;
}
/**
- * @brief Check for OStatus contact
+ * Check for OStatus contact
*
* @param array $webfinger Webfinger data
* @param bool $short Short detection mode
*
* @return array|bool OStatus data or "false" on error or "true" on short mode
+ * @throws HTTPException\InternalServerErrorException
*/
private static function ostatus($webfinger, $short = false)
{
$data["addr"] = str_replace('acct:', '', $webfinger["subject"]);
}
- $pubkey = "";
if (is_array($webfinger["links"])) {
- foreach ($webfinger["links"] as $link) {
+ // The array is reversed to take into account the order of preference for same-rel links
+ // See: https://tools.ietf.org/html/rfc7033#section-4.4.4
+ foreach (array_reverse($webfinger["links"]) as $link) {
if (($link["rel"] == "http://webfinger.net/rel/profile-page")
- && (defaults($link, "type", "") == "text/html")
+ && (($link["type"] ?? "") == "text/html")
&& ($link["href"] != "")
) {
$data["url"] = $link["href"];
} elseif (($link["rel"] == "salmon") && !empty($link["href"])) {
$data["notify"] = $link["href"];
- } elseif (($link["rel"] == NAMESPACE_FEED) && !empty($link["href"])) {
+ } elseif (($link["rel"] == ActivityNamespace::FEED) && !empty($link["href"])) {
$data["poll"] = $link["href"];
} elseif (($link["rel"] == "magic-public-key") && !empty($link["href"])) {
$pubkey = $link["href"];
} elseif (Strings::normaliseLink($pubkey) == 'http://') {
$curlResult = Network::curl($pubkey);
if ($curlResult->isTimeout()) {
+ self::$istimeout = true;
return false;
}
- $pubkey = $curlResult['body'];
+ $pubkey = $curlResult->getBody();
}
$key = explode(".", $pubkey);
// Fetch all additional data from the feed
$curlResult = Network::curl($data["poll"]);
if ($curlResult->isTimeout()) {
+ self::$istimeout = true;
return false;
}
$feed = $curlResult->getBody();
- $dummy1 = null;
- $dummy2 = null;
- $dummy2 = null;
- $feed_data = Feed::import($feed, $dummy1, $dummy2, $dummy3, true);
+ $feed_data = Feed::import($feed);
if (!$feed_data) {
return false;
}
}
/**
- * @brief Fetch data from a pump.io profile page
+ * Fetch data from a pump.io profile page
*
* @param string $profile_link Link to the profile page
*
*/
private static function pumpioProfileData($profile_link)
{
+ $curlResult = Network::curl($profile_link);
+ if (!$curlResult->isSuccess()) {
+ return false;
+ }
+
$doc = new DOMDocument();
- if (!@$doc->loadHTMLFile($profile_link)) {
+ if (!@$doc->loadHTML($curlResult->getBody())) {
return false;
}
}
/**
- * @brief Check for pump.io contact
- *
- * @param array $webfinger Webfinger data
+ * Check for pump.io contact
*
+ * @param array $webfinger Webfinger data
+ * @param string $addr
* @return array pump.io data
*/
private static function pumpio($webfinger, $addr)
{
$data = [];
- foreach ($webfinger["links"] as $link) {
+ // The array is reversed to take into account the order of preference for same-rel links
+ // See: https://tools.ietf.org/html/rfc7033#section-4.4.4
+ foreach (array_reverse($webfinger["links"]) as $link) {
if (($link["rel"] == "http://webfinger.net/rel/profile-page")
- && (defaults($link, "type", "") == "text/html")
+ && (($link["type"] ?? "") == "text/html")
&& ($link["href"] != "")
) {
$data["url"] = $link["href"];
}
/**
- * @brief Check page for feed link
+ * Check for twitter contact
+ *
+ * @param string $uri
+ *
+ * @return array twitter data
+ */
+ private static function twitter($uri)
+ {
+ if (preg_match('=(.*)@twitter.com=i', $uri, $matches)) {
+ $nick = $matches[1];
+ } elseif (preg_match('=https?://twitter.com/(.*)=i', $uri, $matches)) {
+ $nick = $matches[1];
+ } else {
+ return [];
+ }
+
+ $data = [];
+ $data['url'] = 'https://twitter.com/' . $nick;
+ $data['addr'] = $nick . '@twitter.com';
+ $data['nick'] = $data['name'] = $nick;
+ $data['network'] = Protocol::TWITTER;
+ $data['baseurl'] = 'https://twitter.com';
+
+ $curlResult = Network::curl($data['url'], false);
+ if (!$curlResult->isSuccess()) {
+ return [];
+ }
+
+ $body = $curlResult->getBody();
+ $doc = new DOMDocument();
+ @$doc->loadHTML($body);
+ $xpath = new DOMXPath($doc);
+
+ $list = $xpath->query('//img[@class]');
+ foreach ($list as $node) {
+ $img_attr = [];
+ if ($node->attributes->length) {
+ foreach ($node->attributes as $attribute) {
+ $img_attr[$attribute->name] = $attribute->value;
+ }
+ }
+
+ if (empty($img_attr['class'])) {
+ continue;
+ }
+
+ if (strpos($img_attr['class'], 'ProfileAvatar-image') !== false) {
+ if (!empty($img_attr['src'])) {
+ $data['photo'] = $img_attr['src'];
+ }
+ if (!empty($img_attr['alt'])) {
+ $data['name'] = $img_attr['alt'];
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Check page for feed link
*
* @param string $url Page link
*
*/
private static function getFeedLink($url)
{
- $doc = new DOMDocument();
+ $curlResult = Network::curl($url);
+ if (!$curlResult->isSuccess()) {
+ return false;
+ }
- if (!@$doc->loadHTMLFile($url)) {
+ $doc = new DOMDocument();
+ if (!@$doc->loadHTML($curlResult->getBody())) {
return false;
}
$attr[$attribute->name] = trim($attribute->value);
}
- if ($feed_url == "") {
+ if (empty($feed_url) && !empty($attr['href'])) {
$feed_url = $attr["href"];
}
}
}
/**
- * @brief Check for feed contact
+ * Check for feed contact
*
* @param string $url Profile link
* @param boolean $probe Do a probe if the page contains a feed link
*
* @return array feed data
+ * @throws HTTPException\InternalServerErrorException
*/
private static function feed($url, $probe = true)
{
$curlResult = Network::curl($url);
if ($curlResult->isTimeout()) {
+ self::$istimeout = true;
return false;
}
$feed = $curlResult->getBody();
- $dummy1 = $dummy2 = $dummy3 = null;
- $feed_data = Feed::import($feed, $dummy1, $dummy2, $dummy3, true);
+ $feed_data = Feed::import($feed);
if (!$feed_data) {
if (!$probe) {
}
/**
- * @brief Check for mail contact
+ * Check for mail contact
*
* @param string $uri Profile link
* @param integer $uid User ID
*
* @return array mail data
+ * @throws \Exception
*/
private static function mail($uri, $uid)
{
}
/**
- * @brief Mix two paths together to possibly fix missing parts
+ * Mix two paths together to possibly fix missing parts
*
* @param string $avatar Path to the avatar
* @param string $base Another path that is hopefully complete
*
* @return string fixed avatar path
+ * @throws \Exception
*/
public static function fixAvatar($avatar, $base)
{