+ /**
+ * Set the last date that the contact had posted something
+ *
+ * @param string $data Probing result
+ * @param bool $force force updating
+ */
+ public static function setLastUpdate(array $data, bool $force = false)
+ {
+ // Fetch the global contact
+ $gcontact = DBA::selectFirst('gcontact', ['created', 'updated', 'last_contact', 'last_failure'],
+ ['nurl' => Strings::normaliseLink($data['url'])]);
+ if (!DBA::isResult($gcontact)) {
+ return;
+ }
+
+ if (!$force && !PortableContact::updateNeeded($gcontact['created'], $gcontact['updated'], $gcontact['last_failure'], $gcontact['last_contact'])) {
+ Logger::info("Don't update profile", ['url' => $data['url'], 'updated' => $gcontact['updated']]);
+ return;
+ }
+
+ if (self::updateFromNoScrape($data)) {
+ return;
+ }
+
+ // When the profile doesn't have got a feed, then we exit here
+ if (empty($data['poll'])) {
+ return;
+ }
+
+ if ($data['network'] == Protocol::ACTIVITYPUB) {
+ self::updateFromOutbox($data['poll'], $data);
+ } else {
+ self::updateFromFeed($data);
+ }
+ }
+
+ /**
+ * Update a global contact via the "noscrape" endpoint
+ *
+ * @param string $data Probing result
+ *
+ * @return bool 'true' if update was successful or the server was unreachable
+ */
+ private static function updateFromNoScrape(array $data)
+ {
+ // Check the 'noscrape' endpoint when it is a Friendica server
+ $gserver = DBA::selectFirst('gserver', ['noscrape'], ["`nurl` = ? AND `noscrape` != ''",
+ Strings::normaliseLink($data['baseurl'])]);
+ if (!DBA::isResult($gserver)) {
+ return false;
+ }
+
+ $curlResult = Network::curl($gserver['noscrape'] . '/' . $data['nick']);
+
+ if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
+ $noscrape = json_decode($curlResult->getBody(), true);
+ if (!empty($noscrape)) {
+ $noscrape['updated'] = DateTimeFormat::utc($noscrape['updated'], DateTimeFormat::MYSQL);
+ $fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $noscrape['updated']];
+ DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
+ return true;
+ }
+ } elseif ($curlResult->isTimeout()) {
+ // On a timeout return the existing value, but mark the contact as failure
+ $fields = ['last_failure' => DateTimeFormat::utcNow()];
+ DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update a global contact via an ActivityPub Outbox
+ *
+ * @param string $data Probing result
+ */
+ private static function updateFromOutbox(string $feed, array $data)
+ {
+ $outbox = ActivityPub::fetchContent($feed);
+ if (empty($outbox)) {
+ return;
+ }
+
+ if (!empty($outbox['orderedItems'])) {
+ $items = $outbox['orderedItems'];
+ } elseif (!empty($outbox['first']['orderedItems'])) {
+ $items = $outbox['first']['orderedItems'];
+ } elseif (!empty($outbox['first'])) {
+ self::updateFromOutbox($outbox['first'], $data);
+ return;
+ } else {
+ $items = [];
+ }
+
+ $last_updated = '';
+
+ foreach ($items as $activity) {
+ if ($last_updated < $activity['published']) {
+ $last_updated = $activity['published'];
+ }
+ }
+
+ if (empty($last_updated)) {
+ return;
+ }
+
+ $fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $last_updated];
+ DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
+ }
+
+ /**
+ * Update a global contact via an XML feed
+ *
+ * @param string $data Probing result
+ */
+ private static function updateFromFeed(array $data)
+ {
+ // Search for the newest entry in the feed
+ $curlResult = Network::curl($data['poll']);
+ if (!$curlResult->isSuccess()) {
+ $fields = ['last_failure' => DateTimeFormat::utcNow()];
+ DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
+
+ Logger::info("Profile wasn't reachable (no feed)", ['url' => $data['url']]);
+ return;
+ }
+
+ $doc = new DOMDocument();
+ @$doc->loadXML($curlResult->getBody());
+
+ $xpath = new DOMXPath($doc);
+ $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
+
+ $entries = $xpath->query('/atom:feed/atom:entry');
+
+ $last_updated = '';
+
+ foreach ($entries as $entry) {
+ $published_item = $xpath->query('atom:published/text()', $entry)->item(0);
+ $updated_item = $xpath->query('atom:updated/text()' , $entry)->item(0);
+ $published = !empty($published_item->nodeValue) ? DateTimeFormat::utc($published_item->nodeValue) : null;
+ $updated = !empty($updated_item->nodeValue) ? DateTimeFormat::utc($updated_item->nodeValue) : null;
+
+ if (empty($published) || empty($updated)) {
+ Logger::notice('Invalid entry for XPath.', ['entry' => $entry, 'url' => $data['url']]);
+ continue;
+ }
+
+ if ($last_updated < $published) {
+ $last_updated = $published;
+ }
+
+ if ($last_updated < $updated) {
+ $last_updated = $updated;
+ }
+ }
+
+ if (empty($last_updated)) {
+ return;
+ }
+
+ $fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $last_updated];
+ DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
+ }