Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1056 lines
32 KiB

3 years ago
3 years ago
Cleanups: isResult() more used, readability improved (#5608) * [diaspora]: Maybe SimpleXMLElement is the right type-hint? * Changes proposed + pre-renaming: - pre-renamed $db -> $connection - added TODOs for not allowing bad method invocations (there is a BadMethodCallException in SPL) * If no record is found, below $r[0] will fail with a E_NOTICE and the code doesn't behave as expected. * Ops, one more left ... * Continued: - added documentation for Contact::updateSslPolicy() method - added type-hint for $contact of same method - empty lines added + TODO where the bug origins that $item has no element 'body' * Added empty lines for better readability * Cleaned up: - no more x() (deprecated) usage but empty() instead - fixed mixing of space/tab indending - merged else/if block goether in elseif() (lesser nested code blocks) * Re-fixed DBM -> DBA switch * Fixes/rewrites: - use empty()/isset() instead of deprecated x() - merged 2 nested if() blocks into one - avoided nested if() block inside else block by rewriting it to elseif() - $contact_id is an integer, let's test on > 0 here - added a lot spaces and some empty lines for better readability * Rewrite: - moved all CONTACT_* constants from boot.php to Contact class * CR request: - renamed Contact::CONTACT_IS_* -> Contact::* ;-) * Rewrites: - moved PAGE_* to Friendica\Model\Profile class - fixed mixure with "Contact::* rewrite" * Ops, one still there (return is no function) * Rewrite to Proxy class: - introduced new Friendica\Network\Proxy class for in exchange of proxy_*() functions - moved also all PROXY_* constants there as Proxy::* - removed now no longer needed mod/proxy.php loading as composer's auto-load will do this for us - renamed those proxy_*() functions to better names: + proxy_init() -> Proxy::init() (public) + proxy_url() -> Proxy::proxifyUrl() (public) + proxy_parse_html() -> Proxy::proxifyHtml() (public) + proxy_is_local_image() -> Proxy::isLocalImage() (private) + proxy_parse_query() -> Proxy::parseQuery() (private) + proxy_img_cb() -> Proxy::replaceUrl() (private) * CR request: - moved all PAGE_* constants to Friendica\Model\Contact class - fixed all references of both classes * Ops, need to set $a here ... * CR request: - moved Proxy class to Friendica\Module - extended BaseModule * Ops, no need for own instance of $a when self::getApp() is around. * Proxy-rewrite: - proxy_url() and proxy_parse_html() are both non-module functions (now methods) - so they must be splitted into a seperate class - also the SIZE_* and DEFAULT_TIME constants are both not relevant to module * No instances from utility classes * Fixed error: - proxify*() is now located in `Friendica\Util\ProxyUtils` * Moved back to original place, ops? How did they move here? Well, it was not intended by me. * Removed duplicate (left-over from split) constants and static array. Thank to MrPetovan finding it. * Renamed ProxyUtils -> Proxy and aliased it back to ProxyUtils. * Rewrite: - stopped using deprecated NETWORK_* constants, now Protocol::* should be used - still left them intact for slow/lazy developers ... * Ops, was added accidentally ... * Ops, why these wrong moves? * Ops, one to much (thanks to MrPetovan) * Ops, wrong moving ... * moved back to original place ... * spaces added * empty lines add for better readability. * convertered spaces -> tab for code indenting. * CR request: Add space between if and brace. * CR requests fixed + move reverted - ops, src/Module/*.php has been moved to src/Network/ accidentally - reverted some parts in src/Database/DBA.php as pointed out by Annando - removed internal TODO items - added some spaces for better readability
3 years ago
Cleanups: isResult() more used, readability improved (#5608) * [diaspora]: Maybe SimpleXMLElement is the right type-hint? * Changes proposed + pre-renaming: - pre-renamed $db -> $connection - added TODOs for not allowing bad method invocations (there is a BadMethodCallException in SPL) * If no record is found, below $r[0] will fail with a E_NOTICE and the code doesn't behave as expected. * Ops, one more left ... * Continued: - added documentation for Contact::updateSslPolicy() method - added type-hint for $contact of same method - empty lines added + TODO where the bug origins that $item has no element 'body' * Added empty lines for better readability * Cleaned up: - no more x() (deprecated) usage but empty() instead - fixed mixing of space/tab indending - merged else/if block goether in elseif() (lesser nested code blocks) * Re-fixed DBM -> DBA switch * Fixes/rewrites: - use empty()/isset() instead of deprecated x() - merged 2 nested if() blocks into one - avoided nested if() block inside else block by rewriting it to elseif() - $contact_id is an integer, let's test on > 0 here - added a lot spaces and some empty lines for better readability * Rewrite: - moved all CONTACT_* constants from boot.php to Contact class * CR request: - renamed Contact::CONTACT_IS_* -> Contact::* ;-) * Rewrites: - moved PAGE_* to Friendica\Model\Profile class - fixed mixure with "Contact::* rewrite" * Ops, one still there (return is no function) * Rewrite to Proxy class: - introduced new Friendica\Network\Proxy class for in exchange of proxy_*() functions - moved also all PROXY_* constants there as Proxy::* - removed now no longer needed mod/proxy.php loading as composer's auto-load will do this for us - renamed those proxy_*() functions to better names: + proxy_init() -> Proxy::init() (public) + proxy_url() -> Proxy::proxifyUrl() (public) + proxy_parse_html() -> Proxy::proxifyHtml() (public) + proxy_is_local_image() -> Proxy::isLocalImage() (private) + proxy_parse_query() -> Proxy::parseQuery() (private) + proxy_img_cb() -> Proxy::replaceUrl() (private) * CR request: - moved all PAGE_* constants to Friendica\Model\Contact class - fixed all references of both classes * Ops, need to set $a here ... * CR request: - moved Proxy class to Friendica\Module - extended BaseModule * Ops, no need for own instance of $a when self::getApp() is around. * Proxy-rewrite: - proxy_url() and proxy_parse_html() are both non-module functions (now methods) - so they must be splitted into a seperate class - also the SIZE_* and DEFAULT_TIME constants are both not relevant to module * No instances from utility classes * Fixed error: - proxify*() is now located in `Friendica\Util\ProxyUtils` * Moved back to original place, ops? How did they move here? Well, it was not intended by me. * Removed duplicate (left-over from split) constants and static array. Thank to MrPetovan finding it. * Renamed ProxyUtils -> Proxy and aliased it back to ProxyUtils. * Rewrite: - stopped using deprecated NETWORK_* constants, now Protocol::* should be used - still left them intact for slow/lazy developers ... * Ops, was added accidentally ... * Ops, why these wrong moves? * Ops, one to much (thanks to MrPetovan) * Ops, wrong moving ... * moved back to original place ... * spaces added * empty lines add for better readability. * convertered spaces -> tab for code indenting. * CR request: Add space between if and brace. * CR requests fixed + move reverted - ops, src/Module/*.php has been moved to src/Network/ accidentally - reverted some parts in src/Database/DBA.php as pointed out by Annando - removed internal TODO items - added some spaces for better readability
3 years ago
  1. <?php
  2. /**
  3. * @file src/Model/GlobalContact.php
  4. * @brief This file includes the GlobalContact class with directory related functions
  5. */
  6. namespace Friendica\Model;
  7. use Exception;
  8. use Friendica\Core\Config;
  9. use Friendica\Core\Logger;
  10. use Friendica\Core\Protocol;
  11. use Friendica\Core\System;
  12. use Friendica\Core\Worker;
  13. use Friendica\Database\DBA;
  14. use Friendica\Network\Probe;
  15. use Friendica\Protocol\PortableContact;
  16. use Friendica\Util\DateTimeFormat;
  17. use Friendica\Util\Network;
  18. use Friendica\Util\Strings;
  19. require_once 'include/dba.php';
  20. /**
  21. * @brief This class handles GlobalContact related functions
  22. */
  23. class GContact
  24. {
  25. /**
  26. * @brief Search global contact table by nick or name
  27. *
  28. * @param string $search Name or nick
  29. * @param string $mode Search mode (e.g. "community")
  30. *
  31. * @return array with search results
  32. */
  33. public static function searchByName($search, $mode = '')
  34. {
  35. if (empty($search)) {
  36. return [];
  37. }
  38. // check supported networks
  39. if (Config::get('system', 'diaspora_enabled')) {
  40. $diaspora = Protocol::DIASPORA;
  41. } else {
  42. $diaspora = Protocol::DFRN;
  43. }
  44. if (!Config::get('system', 'ostatus_disabled')) {
  45. $ostatus = Protocol::OSTATUS;
  46. } else {
  47. $ostatus = Protocol::DFRN;
  48. }
  49. // check if we search only communities or every contact
  50. if ($mode === "community") {
  51. $extra_sql = " AND `community`";
  52. } else {
  53. $extra_sql = "";
  54. }
  55. $search .= "%";
  56. $results = DBA::p("SELECT `nurl` FROM `gcontact`
  57. WHERE NOT `hide` AND `network` IN (?, ?, ?, ?) AND
  58. ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND
  59. (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql
  60. GROUP BY `nurl` ORDER BY `nurl` DESC LIMIT 1000",
  61. Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, $search, $search, $search
  62. );
  63. $gcontacts = [];
  64. while ($result = DBA::fetch($results)) {
  65. $urlparts = parse_url($result["nurl"]);
  66. // Ignore results that look strange.
  67. // For historic reasons the gcontact table does contain some garbage.
  68. if (!empty($urlparts['query']) || !empty($urlparts['fragment'])) {
  69. continue;
  70. }
  71. $gcontacts[] = Contact::getDetailsByURL($result["nurl"], local_user());
  72. }
  73. return $gcontacts;
  74. }
  75. /**
  76. * @brief Link the gcontact entry with user, contact and global contact
  77. *
  78. * @param integer $gcid Global contact ID
  79. * @param integer $uid User ID
  80. * @param integer $cid Contact ID
  81. * @param integer $zcid Global Contact ID
  82. * @return void
  83. */
  84. public static function link($gcid, $uid = 0, $cid = 0, $zcid = 0)
  85. {
  86. if ($gcid <= 0) {
  87. return;
  88. }
  89. $condition = ['cid' => $cid, 'uid' => $uid, 'gcid' => $gcid, 'zcid' => $zcid];
  90. DBA::update('glink', ['updated' => DateTimeFormat::utcNow()], $condition, true);
  91. }
  92. /**
  93. * @brief Sanitize the given gcontact data
  94. *
  95. * @param array $gcontact array with gcontact data
  96. * @throw Exception
  97. *
  98. * Generation:
  99. * 0: No definition
  100. * 1: Profiles on this server
  101. * 2: Contacts of profiles on this server
  102. * 3: Contacts of contacts of profiles on this server
  103. * 4: ...
  104. * @return array $gcontact
  105. */
  106. public static function sanitize($gcontact)
  107. {
  108. if ($gcontact['url'] == "") {
  109. throw new Exception('URL is empty');
  110. }
  111. $urlparts = parse_url($gcontact['url']);
  112. if (!isset($urlparts["scheme"])) {
  113. throw new Exception("This (".$gcontact['url'].") doesn't seem to be an url.");
  114. }
  115. if (in_array($urlparts["host"], ["twitter.com", "identi.ca"])) {
  116. throw new Exception('Contact from a non federated network ignored. ('.$gcontact['url'].')');
  117. }
  118. // Don't store the statusnet connector as network
  119. // We can't simply set this to Protocol::OSTATUS since the connector could have fetched posts from friendica as well
  120. if ($gcontact['network'] == Protocol::STATUSNET) {
  121. $gcontact['network'] = "";
  122. }
  123. // Assure that there are no parameter fragments in the profile url
  124. if (in_array($gcontact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
  125. $gcontact['url'] = self::cleanContactUrl($gcontact['url']);
  126. }
  127. $alternate = PortableContact::alternateOStatusUrl($gcontact['url']);
  128. // The global contacts should contain the original picture, not the cached one
  129. if (($gcontact['generation'] != 1) && stristr(Strings::normaliseLink($gcontact['photo']), Strings::normaliseLink(System::baseUrl()."/photo/"))) {
  130. $gcontact['photo'] = "";
  131. }
  132. if (!isset($gcontact['network'])) {
  133. $condition = ["`uid` = 0 AND `nurl` = ? AND `network` != '' AND `network` != ?",
  134. Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
  135. $contact = DBA::selectFirst('contact', ['network'], $condition);
  136. if (DBA::isResult($contact)) {
  137. $gcontact['network'] = $contact["network"];
  138. }
  139. if (($gcontact['network'] == "") || ($gcontact['network'] == Protocol::OSTATUS)) {
  140. $condition = ["`uid` = 0 AND `alias` IN (?, ?) AND `network` != '' AND `network` != ?",
  141. $gcontact['url'], Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
  142. $contact = DBA::selectFirst('contact', ['network'], $condition);
  143. if (DBA::isResult($contact)) {
  144. $gcontact['network'] = $contact["network"];
  145. }
  146. }
  147. }
  148. $gcontact['server_url'] = '';
  149. $gcontact['network'] = '';
  150. $fields = ['network', 'updated', 'server_url', 'url', 'addr'];
  151. $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($gcontact['url'])]);
  152. if (DBA::isResult($gcnt)) {
  153. if (!isset($gcontact['network']) && ($gcnt["network"] != Protocol::STATUSNET)) {
  154. $gcontact['network'] = $gcnt["network"];
  155. }
  156. if ($gcontact['updated'] <= DBA::NULL_DATETIME) {
  157. $gcontact['updated'] = $gcnt["updated"];
  158. }
  159. if (!isset($gcontact['server_url']) && (Strings::normaliseLink($gcnt["server_url"]) != Strings::normaliseLink($gcnt["url"]))) {
  160. $gcontact['server_url'] = $gcnt["server_url"];
  161. }
  162. if (!isset($gcontact['addr'])) {
  163. $gcontact['addr'] = $gcnt["addr"];
  164. }
  165. }
  166. if ((!isset($gcontact['network']) || !isset($gcontact['name']) || !isset($gcontact['addr']) || !isset($gcontact['photo']) || !isset($gcontact['server_url']) || $alternate)
  167. && PortableContact::reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)
  168. ) {
  169. $data = Probe::uri($gcontact['url']);
  170. if ($data["network"] == Protocol::PHANTOM) {
  171. throw new Exception('Probing for URL '.$gcontact['url'].' failed');
  172. }
  173. $orig_profile = $gcontact['url'];
  174. $gcontact["server_url"] = $data["baseurl"];
  175. $gcontact = array_merge($gcontact, $data);
  176. if ($alternate && ($gcontact['network'] == Protocol::OSTATUS)) {
  177. // Delete the old entry - if it exists
  178. if (DBA::exists('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)])) {
  179. DBA::delete('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)]);
  180. }
  181. }
  182. }
  183. if (!isset($gcontact['name']) || !isset($gcontact['photo'])) {
  184. throw new Exception('No name and photo for URL '.$gcontact['url']);
  185. }
  186. if (!in_array($gcontact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
  187. throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
  188. }
  189. if (!isset($gcontact['server_url'])) {
  190. // We check the server url to be sure that it is a real one
  191. $server_url = PortableContact::detectServer($gcontact['url']);
  192. // We are now sure that it is a correct URL. So we use it in the future
  193. if ($server_url != "") {
  194. $gcontact['server_url'] = $server_url;
  195. }
  196. }
  197. // The server URL doesn't seem to be valid, so we don't store it.
  198. if (!PortableContact::checkServer($gcontact['server_url'], $gcontact['network'])) {
  199. $gcontact['server_url'] = "";
  200. }
  201. return $gcontact;
  202. }
  203. /**
  204. * @param integer $uid id
  205. * @param integer $cid id
  206. * @return integer
  207. */
  208. public static function countCommonFriends($uid, $cid)
  209. {
  210. $r = q(
  211. "SELECT count(*) as `total`
  212. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  213. WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
  214. ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR
  215. (`gcontact`.`updated` >= `gcontact`.`last_failure`))
  216. AND `gcontact`.`nurl` IN (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 and id != %d ) ",
  217. intval($cid),
  218. intval($uid),
  219. intval($uid),
  220. intval($cid)
  221. );
  222. // Logger::log("countCommonFriends: $uid $cid {$r[0]['total']}");
  223. if (DBA::isResult($r)) {
  224. return $r[0]['total'];
  225. }
  226. return 0;
  227. }
  228. /**
  229. * @param integer $uid id
  230. * @param integer $zcid zcid
  231. * @return integer
  232. */
  233. public static function countCommonFriendsZcid($uid, $zcid)
  234. {
  235. $r = q(
  236. "SELECT count(*) as `total`
  237. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  238. where `glink`.`zcid` = %d
  239. and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 ) ",
  240. intval($zcid),
  241. intval($uid)
  242. );
  243. if (DBA::isResult($r)) {
  244. return $r[0]['total'];
  245. }
  246. return 0;
  247. }
  248. /**
  249. * @param integer $uid user
  250. * @param integer $cid cid
  251. * @param integer $start optional, default 0
  252. * @param integer $limit optional, default 9999
  253. * @param boolean $shuffle optional, default false
  254. * @return object
  255. */
  256. public static function commonFriends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false)
  257. {
  258. if ($shuffle) {
  259. $sql_extra = " order by rand() ";
  260. } else {
  261. $sql_extra = " order by `gcontact`.`name` asc ";
  262. }
  263. $r = q(
  264. "SELECT `gcontact`.*, `contact`.`id` AS `cid`
  265. FROM `glink`
  266. INNER JOIN `gcontact` ON `glink`.`gcid` = `gcontact`.`id`
  267. INNER JOIN `contact` ON `gcontact`.`nurl` = `contact`.`nurl`
  268. WHERE `glink`.`cid` = %d and `glink`.`uid` = %d
  269. AND `contact`.`uid` = %d AND `contact`.`self` = 0 AND `contact`.`blocked` = 0
  270. AND `contact`.`hidden` = 0 AND `contact`.`id` != %d
  271. AND ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
  272. $sql_extra LIMIT %d, %d",
  273. intval($cid),
  274. intval($uid),
  275. intval($uid),
  276. intval($cid),
  277. intval($start),
  278. intval($limit)
  279. );
  280. /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
  281. return $r;
  282. }
  283. /**
  284. * @param integer $uid user
  285. * @param integer $zcid zcid
  286. * @param integer $start optional, default 0
  287. * @param integer $limit optional, default 9999
  288. * @param boolean $shuffle optional, default false
  289. * @return object
  290. */
  291. public static function commonFriendsZcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false)
  292. {
  293. if ($shuffle) {
  294. $sql_extra = " order by rand() ";
  295. } else {
  296. $sql_extra = " order by `gcontact`.`name` asc ";
  297. }
  298. $r = q(
  299. "SELECT `gcontact`.*
  300. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  301. where `glink`.`zcid` = %d
  302. and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 )
  303. $sql_extra limit %d, %d",
  304. intval($zcid),
  305. intval($uid),
  306. intval($start),
  307. intval($limit)
  308. );
  309. /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
  310. return $r;
  311. }
  312. /**
  313. * @param integer $uid user
  314. * @param integer $cid cid
  315. * @return integer
  316. */
  317. public static function countAllFriends($uid, $cid)
  318. {
  319. $r = q(
  320. "SELECT count(*) as `total`
  321. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  322. where `glink`.`cid` = %d and `glink`.`uid` = %d AND
  323. ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))",
  324. intval($cid),
  325. intval($uid)
  326. );
  327. if (DBA::isResult($r)) {
  328. return $r[0]['total'];
  329. }
  330. return 0;
  331. }
  332. /**
  333. * @param integer $uid user
  334. * @param integer $cid cid
  335. * @param integer $start optional, default 0
  336. * @param integer $limit optional, default 80
  337. * @return array
  338. */
  339. public static function allFriends($uid, $cid, $start = 0, $limit = 80)
  340. {
  341. $r = q(
  342. "SELECT `gcontact`.*, `contact`.`id` AS `cid`
  343. FROM `glink`
  344. INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  345. LEFT JOIN `contact` ON `contact`.`nurl` = `gcontact`.`nurl` AND `contact`.`uid` = %d
  346. WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
  347. ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
  348. ORDER BY `gcontact`.`name` ASC LIMIT %d, %d ",
  349. intval($uid),
  350. intval($cid),
  351. intval($uid),
  352. intval($start),
  353. intval($limit)
  354. );
  355. /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
  356. return $r;
  357. }
  358. /**
  359. * @param object $uid user
  360. * @param integer $start optional, default 0
  361. * @param integer $limit optional, default 80
  362. * @return array
  363. */
  364. public static function suggestionQuery($uid, $start = 0, $limit = 80)
  365. {
  366. if (!$uid) {
  367. return [];
  368. }
  369. /*
  370. * Uncommented because the result of the queries are to big to store it in the cache.
  371. * We need to decide if we want to change the db column type or if we want to delete it.
  372. */
  373. //$list = Cache::get("suggestion_query:".$uid.":".$start.":".$limit);
  374. //if (!is_null($list)) {
  375. // return $list;
  376. //}
  377. $network = [Protocol::DFRN];
  378. if (Config::get('system', 'diaspora_enabled')) {
  379. $network[] = Protocol::DIASPORA;
  380. }
  381. if (!Config::get('system', 'ostatus_disabled')) {
  382. $network[] = Protocol::OSTATUS;
  383. }
  384. $sql_network = implode("', '", $network);
  385. $sql_network = "'".$sql_network."'";
  386. /// @todo This query is really slow
  387. // By now we cache the data for five minutes
  388. $r = q(
  389. "SELECT count(glink.gcid) as `total`, gcontact.* from gcontact
  390. INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
  391. where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d )
  392. AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
  393. AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
  394. AND `gcontact`.`updated` >= '%s'
  395. AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
  396. AND `gcontact`.`network` IN (%s)
  397. GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d",
  398. intval($uid),
  399. intval($uid),
  400. intval($uid),
  401. intval($uid),
  402. DBA::NULL_DATETIME,
  403. $sql_network,
  404. intval($start),
  405. intval($limit)
  406. );
  407. if (DBA::isResult($r) && count($r) >= ($limit -1)) {
  408. /*
  409. * Uncommented because the result of the queries are to big to store it in the cache.
  410. * We need to decide if we want to change the db column type or if we want to delete it.
  411. */
  412. //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $r, Cache::FIVE_MINUTES);
  413. return $r;
  414. }
  415. $r2 = q(
  416. "SELECT gcontact.* FROM gcontact
  417. INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
  418. WHERE `glink`.`uid` = 0 AND `glink`.`cid` = 0 AND `glink`.`zcid` = 0 AND NOT `gcontact`.`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = %d)
  419. AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
  420. AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
  421. AND `gcontact`.`updated` >= '%s'
  422. AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
  423. AND `gcontact`.`network` IN (%s)
  424. ORDER BY rand() LIMIT %d, %d",
  425. intval($uid),
  426. intval($uid),
  427. intval($uid),
  428. DBA::NULL_DATETIME,
  429. $sql_network,
  430. intval($start),
  431. intval($limit)
  432. );
  433. $list = [];
  434. foreach ($r2 as $suggestion) {
  435. $list[$suggestion["nurl"]] = $suggestion;
  436. }
  437. foreach ($r as $suggestion) {
  438. $list[$suggestion["nurl"]] = $suggestion;
  439. }
  440. while (sizeof($list) > ($limit)) {
  441. array_pop($list);
  442. }
  443. /*
  444. * Uncommented because the result of the queries are to big to store it in the cache.
  445. * We need to decide if we want to change the db column type or if we want to delete it.
  446. */
  447. //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $list, Cache::FIVE_MINUTES);
  448. return $list;
  449. }
  450. /**
  451. * @return void
  452. */
  453. public static function updateSuggestions()
  454. {
  455. $a = get_app();
  456. $done = [];
  457. /// @TODO Check if it is really neccessary to poll the own server
  458. PortableContact::loadWorker(0, 0, 0, System::baseUrl() . '/poco');
  459. $done[] = System::baseUrl() . '/poco';
  460. if (strlen(Config::get('system', 'directory'))) {
  461. $x = Network::fetchUrl(get_server()."/pubsites");
  462. if (!empty($x)) {
  463. $j = json_decode($x);
  464. if (!empty($j->entries)) {
  465. foreach ($j->entries as $entry) {
  466. PortableContact::checkServer($entry->url);
  467. $url = $entry->url . '/poco';
  468. if (!in_array($url, $done)) {
  469. PortableContact::loadWorker(0, 0, 0, $url);
  470. $done[] = $url;
  471. }
  472. }
  473. }
  474. }
  475. }
  476. // Query your contacts from Friendica and Redmatrix/Hubzilla for their contacts
  477. $r = q(
  478. "SELECT DISTINCT(`poco`) AS `poco` FROM `contact` WHERE `network` IN ('%s', '%s')",
  479. DBA::escape(Protocol::DFRN),
  480. DBA::escape(Protocol::DIASPORA)
  481. );
  482. if (DBA::isResult($r)) {
  483. foreach ($r as $rr) {
  484. $base = substr($rr['poco'], 0, strrpos($rr['poco'], '/'));
  485. if (! in_array($base, $done)) {
  486. PortableContact::loadWorker(0, 0, 0, $base);
  487. }
  488. }
  489. }
  490. }
  491. /**
  492. * @brief Removes unwanted parts from a contact url
  493. *
  494. * @param string $url Contact url
  495. *
  496. * @return string Contact url with the wanted parts
  497. */
  498. public static function cleanContactUrl($url)
  499. {
  500. $parts = parse_url($url);
  501. if (!isset($parts["scheme"]) || !isset($parts["host"])) {
  502. return $url;
  503. }
  504. $new_url = $parts["scheme"]."://".$parts["host"];
  505. if (isset($parts["port"])) {
  506. $new_url .= ":".$parts["port"];
  507. }
  508. if (isset($parts["path"])) {
  509. $new_url .= $parts["path"];
  510. }
  511. if ($new_url != $url) {
  512. Logger::log("Cleaned contact url ".$url." to ".$new_url." - Called by: ".System::callstack(), Logger::DEBUG);
  513. }
  514. return $new_url;
  515. }
  516. /**
  517. * @brief Replace alternate OStatus user format with the primary one
  518. *
  519. * @param array $contact contact array (called by reference)
  520. * @return void
  521. */
  522. public static function fixAlternateContactAddress(&$contact)
  523. {
  524. if (($contact["network"] == Protocol::OSTATUS) && PortableContact::alternateOStatusUrl($contact["url"])) {
  525. $data = Probe::uri($contact["url"]);
  526. if ($contact["network"] == Protocol::OSTATUS) {
  527. Logger::log("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  528. $contact["url"] = $data["url"];
  529. $contact["addr"] = $data["addr"];
  530. $contact["alias"] = $data["alias"];
  531. $contact["server_url"] = $data["baseurl"];
  532. }
  533. }
  534. }
  535. /**
  536. * @brief Fetch the gcontact id, add an entry if not existed
  537. *
  538. * @param array $contact contact array
  539. *
  540. * @return bool|int Returns false if not found, integer if contact was found
  541. */
  542. public static function getId($contact)
  543. {
  544. $gcontact_id = 0;
  545. $doprobing = false;
  546. $last_failure_str = '';
  547. $last_contact_str = '';
  548. if (empty($contact["network"])) {
  549. Logger::log("Empty network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  550. return false;
  551. }
  552. if (in_array($contact["network"], [Protocol::PHANTOM])) {
  553. Logger::log("Invalid network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  554. return false;
  555. }
  556. if ($contact["network"] == Protocol::STATUSNET) {
  557. $contact["network"] = Protocol::OSTATUS;
  558. }
  559. // All new contacts are hidden by default
  560. if (!isset($contact["hide"])) {
  561. $contact["hide"] = true;
  562. }
  563. // Replace alternate OStatus user format with the primary one
  564. self::fixAlternateContactAddress($contact);
  565. // Remove unwanted parts from the contact url (e.g. "?zrl=...")
  566. if (in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
  567. $contact["url"] = self::cleanContactUrl($contact["url"]);
  568. }
  569. DBA::lock('gcontact');
  570. $fields = ['id', 'last_contact', 'last_failure', 'network'];
  571. $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($contact["url"])]);
  572. if (DBA::isResult($gcnt)) {
  573. $gcontact_id = $gcnt["id"];
  574. // Update every 90 days
  575. if (in_array($gcnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
  576. $last_failure_str = $gcnt["last_failure"];
  577. $last_failure = strtotime($gcnt["last_failure"]);
  578. $last_contact_str = $gcnt["last_contact"];
  579. $last_contact = strtotime($gcnt["last_contact"]);
  580. $doprobing = (((time() - $last_contact) > (90 * 86400)) && ((time() - $last_failure) > (90 * 86400)));
  581. }
  582. } else {
  583. $contact['location'] = defaults($contact, 'location', '');
  584. $contact['about'] = defaults($contact, 'about', '');
  585. $contact['generation'] = defaults($contact, 'generation', 0);
  586. q(
  587. "INSERT INTO `gcontact` (`name`, `nick`, `addr` , `network`, `url`, `nurl`, `photo`, `created`, `updated`, `location`, `about`, `hide`, `generation`)
  588. VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d)",
  589. DBA::escape($contact["name"]),
  590. DBA::escape($contact["nick"]),
  591. DBA::escape($contact["addr"]),
  592. DBA::escape($contact["network"]),
  593. DBA::escape($contact["url"]),
  594. DBA::escape(Strings::normaliseLink($contact["url"])),
  595. DBA::escape($contact["photo"]),
  596. DBA::escape(DateTimeFormat::utcNow()),
  597. DBA::escape(DateTimeFormat::utcNow()),
  598. DBA::escape($contact["location"]),
  599. DBA::escape($contact["about"]),
  600. intval($contact["hide"]),
  601. intval($contact["generation"])
  602. );
  603. $condition = ['nurl' => Strings::normaliseLink($contact["url"])];
  604. $cnt = DBA::selectFirst('gcontact', ['id', 'network'], $condition, ['order' => ['id']]);
  605. if (DBA::isResult($cnt)) {
  606. $gcontact_id = $cnt["id"];
  607. $doprobing = in_array($cnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""]);
  608. }
  609. }
  610. DBA::unlock();
  611. if ($doprobing) {
  612. Logger::log("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], Logger::DEBUG);
  613. Worker::add(PRIORITY_LOW, 'GProbe', $contact["url"]);
  614. }
  615. return $gcontact_id;
  616. }
  617. /**
  618. * @brief Updates the gcontact table from a given array
  619. *
  620. * @param array $contact contact array
  621. *
  622. * @return bool|int Returns false if not found, integer if contact was found
  623. */
  624. public static function update($contact)
  625. {
  626. // Check for invalid "contact-type" value
  627. if (isset($contact['contact-type']) && (intval($contact['contact-type']) < 0)) {
  628. $contact['contact-type'] = 0;
  629. }
  630. /// @todo update contact table as well
  631. $gcontact_id = self::getId($contact);
  632. if (!$gcontact_id) {
  633. return false;
  634. }
  635. $public_contact = q(
  636. "SELECT `name`, `nick`, `photo`, `location`, `about`, `addr`, `generation`, `birthday`, `gender`, `keywords`,
  637. `contact-type`, `hide`, `nsfw`, `network`, `alias`, `notify`, `server_url`, `connect`, `updated`, `url`
  638. FROM `gcontact` WHERE `id` = %d LIMIT 1",
  639. intval($gcontact_id)
  640. );
  641. // Get all field names
  642. $fields = [];
  643. foreach ($public_contact[0] as $field => $data) {
  644. $fields[$field] = $data;
  645. }
  646. unset($fields["url"]);
  647. unset($fields["updated"]);
  648. unset($fields["hide"]);
  649. // Bugfix: We had an error in the storing of keywords which lead to the "0"
  650. // This value is still transmitted via poco.
  651. if (!empty($contact["keywords"]) && ($contact["keywords"] == "0")) {
  652. unset($contact["keywords"]);
  653. }
  654. if (!empty($public_contact[0]["keywords"]) && ($public_contact[0]["keywords"] == "0")) {
  655. $public_contact[0]["keywords"] = "";
  656. }
  657. // assign all unassigned fields from the database entry
  658. foreach ($fields as $field => $data) {
  659. if (!isset($contact[$field]) || ($contact[$field] == "")) {
  660. $contact[$field] = $public_contact[0][$field];
  661. }
  662. }
  663. if (!isset($contact["hide"])) {
  664. $contact["hide"] = $public_contact[0]["hide"];
  665. }
  666. $fields["hide"] = $public_contact[0]["hide"];
  667. if ($contact["network"] == Protocol::STATUSNET) {
  668. $contact["network"] = Protocol::OSTATUS;
  669. }
  670. // Replace alternate OStatus user format with the primary one
  671. self::fixAlternateContactAddress($contact);
  672. if (!isset($contact["updated"])) {
  673. $contact["updated"] = DateTimeFormat::utcNow();
  674. }
  675. if ($contact["network"] == Protocol::TWITTER) {
  676. $contact["server_url"] = 'http://twitter.com';
  677. }
  678. if ($contact["server_url"] == "") {
  679. $data = Probe::uri($contact["url"]);
  680. if ($data["network"] != Protocol::PHANTOM) {
  681. $contact["server_url"] = $data['baseurl'];
  682. }
  683. } else {
  684. $contact["server_url"] = Strings::normaliseLink($contact["server_url"]);
  685. }
  686. if (($contact["addr"] == "") && ($contact["server_url"] != "") && ($contact["nick"] != "")) {
  687. $hostname = str_replace("http://", "", $contact["server_url"]);
  688. $contact["addr"] = $contact["nick"]."@".$hostname;
  689. }
  690. // Check if any field changed
  691. $update = false;
  692. unset($fields["generation"]);
  693. if ((($contact["generation"] > 0) && ($contact["generation"] <= $public_contact[0]["generation"])) || ($public_contact[0]["generation"] == 0)) {
  694. foreach ($fields as $field => $data) {
  695. if ($contact[$field] != $public_contact[0][$field]) {
  696. Logger::log("Difference for contact ".$contact["url"]." in field '".$field."'. New value: '".$contact[$field]."', old value '".$public_contact[0][$field]."'", Logger::DEBUG);
  697. $update = true;
  698. }
  699. }
  700. if ($contact["generation"] < $public_contact[0]["generation"]) {
  701. Logger::log("Difference for contact ".$contact["url"]." in field 'generation'. new value: '".$contact["generation"]."', old value '".$public_contact[0]["generation"]."'", Logger::DEBUG);
  702. $update = true;
  703. }
  704. }
  705. if ($update) {
  706. Logger::log("Update gcontact for ".$contact["url"], Logger::DEBUG);
  707. $condition = ['`nurl` = ? AND (`generation` = 0 OR `generation` >= ?)',
  708. Strings::normaliseLink($contact["url"]), $contact["generation"]];
  709. $contact["updated"] = DateTimeFormat::utc($contact["updated"]);
  710. $updated = ['photo' => $contact['photo'], 'name' => $contact['name'],
  711. 'nick' => $contact['nick'], 'addr' => $contact['addr'],
  712. 'network' => $contact['network'], 'birthday' => $contact['birthday'],
  713. 'gender' => $contact['gender'], 'keywords' => $contact['keywords'],
  714. 'hide' => $contact['hide'], 'nsfw' => $contact['nsfw'],
  715. 'contact-type' => $contact['contact-type'], 'alias' => $contact['alias'],
  716. 'notify' => $contact['notify'], 'url' => $contact['url'],
  717. 'location' => $contact['location'], 'about' => $contact['about'],
  718. 'generation' => $contact['generation'], 'updated' => $contact['updated'],
  719. 'server_url' => $contact['server_url'], 'connect' => $contact['connect']];
  720. DBA::update('gcontact', $updated, $condition, $fields);
  721. // Now update the contact entry with the user id "0" as well.
  722. // This is used for the shadow copies of public items.
  723. /// @todo Check if we really should do this.
  724. // The quality of the gcontact table is mostly lower than the public contact
  725. $public_contact = DBA::selectFirst('contact', ['id'], ['nurl' => Strings::normaliseLink($contact["url"]), 'uid' => 0]);
  726. if (DBA::isResult($public_contact)) {
  727. Logger::log("Update public contact ".$public_contact["id"], Logger::DEBUG);
  728. Contact::updateAvatar($contact["photo"], 0, $public_contact["id"]);
  729. $fields = ['name', 'nick', 'addr',
  730. 'network', 'bd', 'gender',
  731. 'keywords', 'alias', 'contact-type',
  732. 'url', 'location', 'about'];
  733. $old_contact = DBA::selectFirst('contact', $fields, ['id' => $public_contact["id"]]);
  734. // Update it with the current values
  735. $fields = ['name' => $contact['name'], 'nick' => $contact['nick'],
  736. 'addr' => $contact['addr'], 'network' => $contact['network'],
  737. 'bd' => $contact['birthday'], 'gender' => $contact['gender'],
  738. 'keywords' => $contact['keywords'], 'alias' => $contact['alias'],
  739. 'contact-type' => $contact['contact-type'], 'url' => $contact['url'],
  740. 'location' => $contact['location'], 'about' => $contact['about']];
  741. // Don't update the birthday field if not set or invalid
  742. if (empty($contact['birthday']) || ($contact['birthday'] < '0001-01-01')) {
  743. unset($fields['bd']);
  744. }
  745. DBA::update('contact', $fields, ['id' => $public_contact["id"]], $old_contact);
  746. }
  747. }
  748. return $gcontact_id;
  749. }
  750. /**
  751. * @brief Updates the gcontact entry from probe
  752. *
  753. * @param string $url profile link
  754. * @return void
  755. */
  756. public static function updateFromProbe($url)
  757. {
  758. $data = Probe::uri($url);
  759. if (in_array($data["network"], [Protocol::PHANTOM])) {
  760. Logger::log("Invalid network for contact url ".$data["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  761. return;
  762. }
  763. $data["server_url"] = $data["baseurl"];
  764. self::update($data);
  765. }
  766. /**
  767. * @brief Update the gcontact entry for a given user id
  768. *
  769. * @param int $uid User ID
  770. * @return void
  771. */
  772. public static function updateForUser($uid)
  773. {
  774. $r = q(
  775. "SELECT `profile`.`locality`, `profile`.`region`, `profile`.`country-name`,
  776. `profile`.`name`, `profile`.`about`, `profile`.`gender`,
  777. `profile`.`pub_keywords`, `profile`.`dob`, `profile`.`photo`,
  778. `profile`.`net-publish`, `user`.`nickname`, `user`.`hidewall`,
  779. `contact`.`notify`, `contact`.`url`, `contact`.`addr`
  780. FROM `profile`
  781. INNER JOIN `user` ON `user`.`uid` = `profile`.`uid`
  782. INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid`
  783. WHERE `profile`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self`",
  784. intval($uid)
  785. );
  786. if (!DBA::isResult($r)) {
  787. Logger::log('Cannot find user with uid=' . $uid, Logger::INFO);
  788. return false;
  789. }
  790. $location = Profile::formatLocation(
  791. ["locality" => $r[0]["locality"], "region" => $r[0]["region"], "country-name" => $r[0]["country-name"]]
  792. );
  793. // The "addr" field was added in 3.4.3 so it can be empty for older users
  794. if ($r[0]["addr"] != "") {
  795. $addr = $r[0]["nickname"].'@'.str_replace(["http://", "https://"], "", System::baseUrl());
  796. } else {
  797. $addr = $r[0]["addr"];
  798. }
  799. $gcontact = ["name" => $r[0]["name"], "location" => $location, "about" => $r[0]["about"],
  800. "gender" => $r[0]["gender"], "keywords" => $r[0]["pub_keywords"],
  801. "birthday" => $r[0]["dob"], "photo" => $r[0]["photo"],
  802. "notify" => $r[0]["notify"], "url" => $r[0]["url"],
  803. "hide" => ($r[0]["hidewall"] || !$r[0]["net-publish"]),
  804. "nick" => $r[0]["nickname"], "addr" => $addr,
  805. "connect" => $addr, "server_url" => System::baseUrl(),
  806. "generation" => 1, "network" => Protocol::DFRN];
  807. self::update($gcontact);
  808. }
  809. /**
  810. * @brief Fetches users of given GNU Social server
  811. *
  812. * If the "Statistics" addon is enabled (See http://gstools.org/ for details) we query user data with this.
  813. *
  814. * @param string $server Server address
  815. * @return void
  816. */
  817. public static function fetchGsUsers($server)
  818. {
  819. Logger::log("Fetching users from GNU Social server ".$server, Logger::DEBUG);
  820. $url = $server."/main/statistics";
  821. $curlResult = Network::curl($url);
  822. if (!$curlResult->isSuccess()) {
  823. return false;
  824. }
  825. $statistics = json_decode($curlResult->getBody());
  826. if (!empty($statistics->config)) {
  827. if ($statistics->config->instance_with_ssl) {
  828. $server = "https://";
  829. } else {
  830. $server = "http://";
  831. }
  832. $server .= $statistics->config->instance_address;
  833. $hostname = $statistics->config->instance_address;
  834. } elseif (!empty($statistics)) {
  835. if ($statistics->instance_with_ssl) {
  836. $server = "https://";
  837. } else {
  838. $server = "http://";
  839. }
  840. $server .= $statistics->instance_address;
  841. $hostname = $statistics->instance_address;
  842. }
  843. if (!empty($statistics->users)) {
  844. foreach ($statistics->users as $nick => $user) {
  845. $profile_url = $server."/".$user->nickname;
  846. $contact = ["url" => $profile_url,
  847. "name" => $user->fullname,
  848. "addr" => $user->nickname."@".$hostname,
  849. "nick" => $user->nickname,
  850. "network" => Protocol::OSTATUS,
  851. "photo" => System::baseUrl()."/images/person-300.jpg"];
  852. if (isset($user->bio)) {
  853. $contact["about"] = $user->bio;
  854. }
  855. self::getId($contact);
  856. }
  857. }
  858. }
  859. /**
  860. * @brief Asking GNU Social server on a regular base for their user data
  861. * @return void
  862. */
  863. public static function discoverGsUsers()
  864. {
  865. $requery_days = intval(Config::get("system", "poco_requery_days"));
  866. $last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
  867. $r = q(
  868. "SELECT `nurl`, `url` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `network` = '%s' AND `last_poco_query` < '%s' ORDER BY RAND() LIMIT 5",
  869. DBA::escape(Protocol::OSTATUS),
  870. DBA::escape($last_update)
  871. );
  872. if (!DBA::isResult($r)) {
  873. return;
  874. }
  875. foreach ($r as $server) {
  876. self::fetchGsUsers($server["url"]);
  877. q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", DBA::escape(DateTimeFormat::utcNow()), DBA::escape($server["nurl"]));
  878. }
  879. }
  880. /**
  881. * @return string
  882. */
  883. public static function getRandomUrl()
  884. {
  885. $r = q(
  886. "SELECT `url` FROM `gcontact` WHERE `network` = '%s'
  887. AND `last_contact` >= `last_failure`
  888. AND `updated` > UTC_TIMESTAMP - INTERVAL 1 MONTH
  889. ORDER BY rand() LIMIT 1",
  890. DBA::escape(Protocol::DFRN)
  891. );
  892. if (DBA::isResult($r)) {
  893. return dirname($r[0]['url']);
  894. }
  895. return '';
  896. }
  897. }