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.

3163 lines
98 KiB

4 years ago
  1. <?php
  2. /**
  3. * @file include/dfrn.php
  4. * @brief The implementation of the dfrn protocol
  5. *
  6. * @see https://github.com/friendica/friendica/wiki/Protocol and
  7. * https://github.com/friendica/friendica/blob/master/spec/dfrn2.pdf
  8. */
  9. namespace Friendica\Protocol;
  10. use Friendica\App;
  11. use Friendica\Content\OEmbed;
  12. use Friendica\Core\Addon;
  13. use Friendica\Core\Config;
  14. use Friendica\Core\L10n;
  15. use Friendica\Core\System;
  16. use Friendica\Core\Worker;
  17. use Friendica\Database\DBM;
  18. use Friendica\Model\Contact;
  19. use Friendica\Model\GContact;
  20. use Friendica\Model\Group;
  21. use Friendica\Model\Item;
  22. use Friendica\Model\Profile;
  23. use Friendica\Model\Term;
  24. use Friendica\Model\User;
  25. use Friendica\Object\Image;
  26. use Friendica\Protocol\OStatus;
  27. use Friendica\Util\Crypto;
  28. use Friendica\Util\Network;
  29. use Friendica\Util\XML;
  30. use dba;
  31. use DOMDocument;
  32. use DOMXPath;
  33. use HTMLPurifier;
  34. use HTMLPurifier_Config;
  35. require_once 'boot.php';
  36. require_once 'include/dba.php';
  37. require_once "include/enotify.php";
  38. require_once "include/threads.php";
  39. require_once "include/items.php";
  40. require_once "include/tags.php";
  41. require_once "include/event.php";
  42. require_once "include/text.php";
  43. require_once "include/html2bbcode.php";
  44. require_once "include/bbcode.php";
  45. /**
  46. * @brief This class contain functions to create and send DFRN XML files
  47. */
  48. class DFRN
  49. {
  50. const DFRN_TOP_LEVEL = 0; // Top level posting
  51. const DFRN_REPLY = 1; // Regular reply that is stored locally
  52. const DFRN_REPLY_RC = 2; // Reply that will be relayed
  53. /**
  54. * @brief Generates the atom entries for delivery.php
  55. *
  56. * This function is used whenever content is transmitted via DFRN.
  57. *
  58. * @param array $items Item elements
  59. * @param array $owner Owner record
  60. *
  61. * @return string DFRN entries
  62. * @todo Add type-hints
  63. */
  64. public static function entries($items, $owner)
  65. {
  66. $doc = new DOMDocument('1.0', 'utf-8');
  67. $doc->formatOutput = true;
  68. $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
  69. if (! count($items)) {
  70. return trim($doc->saveXML());
  71. }
  72. foreach ($items as $item) {
  73. $entry = self::entry($doc, "text", $item, $owner, $item["entry:comment-allow"], $item["entry:cid"]);
  74. $root->appendChild($entry);
  75. }
  76. return(trim($doc->saveXML()));
  77. }
  78. /**
  79. * @brief Generate an atom feed for the given user
  80. *
  81. * This function is called when another server is pulling data from the user feed.
  82. *
  83. * @param string $dfrn_id DFRN ID from the requesting party
  84. * @param string $owner_nick Owner nick name
  85. * @param string $last_update Date of the last update
  86. * @param int $direction Can be -1, 0 or 1.
  87. * @param boolean $onlyheader Output only the header without content? (Default is "no")
  88. *
  89. * @return string DFRN feed entries
  90. */
  91. public static function feed($dfrn_id, $owner_nick, $last_update, $direction = 0, $onlyheader = false)
  92. {
  93. $a = get_app();
  94. $sitefeed = ((strlen($owner_nick)) ? false : true); // not yet implemented, need to rewrite huge chunks of following logic
  95. $public_feed = (($dfrn_id) ? false : true);
  96. $starred = false; // not yet implemented, possible security issues
  97. $converse = false;
  98. if ($public_feed && $a->argc > 2) {
  99. for ($x = 2; $x < $a->argc; $x++) {
  100. if ($a->argv[$x] == 'converse') {
  101. $converse = true;
  102. }
  103. if ($a->argv[$x] == 'starred') {
  104. $starred = true;
  105. }
  106. if ($a->argv[$x] == 'category' && $a->argc > ($x + 1) && strlen($a->argv[$x+1])) {
  107. $category = $a->argv[$x+1];
  108. }
  109. }
  110. }
  111. // default permissions - anonymous user
  112. $sql_extra = " AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = '' ";
  113. $r = q(
  114. "SELECT `contact`.*, `user`.`nickname`, `user`.`timezone`, `user`.`page-flags`, `user`.`account-type`
  115. FROM `contact` INNER JOIN `user` ON `user`.`uid` = `contact`.`uid`
  116. WHERE `contact`.`self` AND `user`.`nickname` = '%s' LIMIT 1",
  117. dbesc($owner_nick)
  118. );
  119. if (! DBM::is_result($r)) {
  120. killme();
  121. }
  122. $owner = $r[0];
  123. $owner_id = $owner['uid'];
  124. $owner_nick = $owner['nickname'];
  125. $sql_post_table = "";
  126. if (! $public_feed) {
  127. $sql_extra = '';
  128. switch ($direction) {
  129. case (-1):
  130. $sql_extra = sprintf(" AND `issued-id` = '%s' ", dbesc($dfrn_id));
  131. $my_id = $dfrn_id;
  132. break;
  133. case 0:
  134. $sql_extra = sprintf(" AND `issued-id` = '%s' AND `duplex` = 1 ", dbesc($dfrn_id));
  135. $my_id = '1:' . $dfrn_id;
  136. break;
  137. case 1:
  138. $sql_extra = sprintf(" AND `dfrn-id` = '%s' AND `duplex` = 1 ", dbesc($dfrn_id));
  139. $my_id = '0:' . $dfrn_id;
  140. break;
  141. default:
  142. return false;
  143. break; // NOTREACHED
  144. }
  145. $r = q(
  146. "SELECT * FROM `contact` WHERE NOT `blocked` AND `contact`.`uid` = %d $sql_extra LIMIT 1",
  147. intval($owner_id)
  148. );
  149. if (! DBM::is_result($r)) {
  150. killme();
  151. }
  152. $contact = $r[0];
  153. include_once 'include/security.php';
  154. $groups = Group::getIdsByContactId($contact['id']);
  155. if (count($groups)) {
  156. for ($x = 0; $x < count($groups); $x ++)
  157. $groups[$x] = '<' . intval($groups[$x]) . '>' ;
  158. $gs = implode('|', $groups);
  159. } else {
  160. $gs = '<<>>' ; // Impossible to match
  161. }
  162. $sql_extra = sprintf(
  163. "
  164. AND ( `allow_cid` = '' OR `allow_cid` REGEXP '<%d>' )
  165. AND ( `deny_cid` = '' OR NOT `deny_cid` REGEXP '<%d>' )
  166. AND ( `allow_gid` = '' OR `allow_gid` REGEXP '%s' )
  167. AND ( `deny_gid` = '' OR NOT `deny_gid` REGEXP '%s')
  168. ",
  169. intval($contact['id']),
  170. intval($contact['id']),
  171. dbesc($gs),
  172. dbesc($gs)
  173. );
  174. }
  175. if ($public_feed) {
  176. $sort = 'DESC';
  177. } else {
  178. $sort = 'ASC';
  179. }
  180. if (! strlen($last_update)) {
  181. $last_update = 'now -30 days';
  182. }
  183. if (isset($category)) {
  184. $sql_post_table = sprintf(
  185. "INNER JOIN (SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d ORDER BY `tid` DESC) AS `term` ON `item`.`id` = `term`.`oid` ",
  186. dbesc(protect_sprintf($category)),
  187. intval(TERM_OBJ_POST),
  188. intval(TERM_CATEGORY),
  189. intval($owner_id)
  190. );
  191. //$sql_extra .= file_tag_file_query('item',$category,'category');
  192. }
  193. if ($public_feed) {
  194. if (! $converse) {
  195. $sql_extra .= " AND `contact`.`self` = 1 ";
  196. }
  197. }
  198. $check_date = datetime_convert('UTC', 'UTC', $last_update, 'Y-m-d H:i:s');
  199. $r = q(
  200. "SELECT `item`.*, `item`.`id` AS `item_id`,
  201. `contact`.`name`, `contact`.`network`, `contact`.`photo`, `contact`.`url`,
  202. `contact`.`name-date`, `contact`.`uri-date`, `contact`.`avatar-date`,
  203. `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
  204. `sign`.`signed_text`, `sign`.`signature`, `sign`.`signer`
  205. FROM `item` USE INDEX (`uid_wall_changed`) $sql_post_table
  206. STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id`
  207. AND (NOT `contact`.`blocked` OR `contact`.`pending`)
  208. LEFT JOIN `sign` ON `sign`.`iid` = `item`.`id`
  209. WHERE `item`.`uid` = %d AND `item`.`visible` AND NOT `item`.`moderated` AND `item`.`parent` != 0
  210. AND `item`.`wall` AND `item`.`changed` > '%s'
  211. $sql_extra
  212. ORDER BY `item`.`parent` ".$sort.", `item`.`created` ASC LIMIT 0, 300",
  213. intval($owner_id),
  214. dbesc($check_date),
  215. dbesc($sort)
  216. );
  217. /*
  218. * Will check further below if this actually returned results.
  219. * We will provide an empty feed if that is the case.
  220. */
  221. $items = $r;
  222. $doc = new DOMDocument('1.0', 'utf-8');
  223. $doc->formatOutput = true;
  224. $alternatelink = $owner['url'];
  225. if (isset($category)) {
  226. $alternatelink .= "/category/".$category;
  227. }
  228. if ($public_feed) {
  229. $author = "dfrn:owner";
  230. } else {
  231. $author = "author";
  232. }
  233. $root = self::addHeader($doc, $owner, $author, $alternatelink, true);
  234. /// @TODO This hook can't work anymore
  235. // Addon::callHooks('atom_feed', $atom);
  236. if (!DBM::is_result($items) || $onlyheader) {
  237. $atom = trim($doc->saveXML());
  238. Addon::callHooks('atom_feed_end', $atom);
  239. return $atom;
  240. }
  241. foreach ($items as $item) {
  242. // prevent private email from leaking.
  243. if ($item['network'] == NETWORK_MAIL) {
  244. continue;
  245. }
  246. // public feeds get html, our own nodes use bbcode
  247. if ($public_feed) {
  248. $type = 'html';
  249. // catch any email that's in a public conversation and make sure it doesn't leak
  250. if ($item['private']) {
  251. continue;
  252. }
  253. } else {
  254. $type = 'text';
  255. }
  256. $entry = self::entry($doc, $type, $item, $owner, true);
  257. $root->appendChild($entry);
  258. }
  259. $atom = trim($doc->saveXML());
  260. Addon::callHooks('atom_feed_end', $atom);
  261. return $atom;
  262. }
  263. /**
  264. * @brief Generate an atom entry for a given item id
  265. *
  266. * @param int $item_id The item id
  267. * @param boolean $conversation Show the conversation. If false show the single post.
  268. *
  269. * @return string DFRN feed entry
  270. */
  271. public static function itemFeed($item_id, $conversation = false)
  272. {
  273. if ($conversation) {
  274. $condition = '`item`.`parent`';
  275. } else {
  276. $condition = '`item`.`id`';
  277. }
  278. $r = q(
  279. "SELECT `item`.*, `item`.`id` AS `item_id`,
  280. `contact`.`name`, `contact`.`network`, `contact`.`photo`, `contact`.`url`,
  281. `contact`.`name-date`, `contact`.`uri-date`, `contact`.`avatar-date`,
  282. `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
  283. `sign`.`signed_text`, `sign`.`signature`, `sign`.`signer`
  284. FROM `item`
  285. STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id`
  286. AND (NOT `contact`.`blocked` OR `contact`.`pending`)
  287. LEFT JOIN `sign` ON `sign`.`iid` = `item`.`id`
  288. WHERE %s = %d AND `item`.`visible` AND NOT `item`.`moderated` AND `item`.`parent` != 0
  289. AND NOT `item`.`private`",
  290. $condition,
  291. intval($item_id)
  292. );
  293. if (!DBM::is_result($r)) {
  294. killme();
  295. }
  296. $items = $r;
  297. $item = $r[0];
  298. if ($item['uid'] != 0) {
  299. $owner = User::getOwnerDataById($item['uid']);
  300. if (!$owner) {
  301. killme();
  302. }
  303. } else {
  304. $owner = ['uid' => 0, 'nick' => 'feed-item'];
  305. }
  306. $doc = new DOMDocument('1.0', 'utf-8');
  307. $doc->formatOutput = true;
  308. $type = 'html';
  309. if ($conversation) {
  310. $root = $doc->createElementNS(NAMESPACE_ATOM1, 'feed');
  311. $doc->appendChild($root);
  312. $root->setAttribute("xmlns:thr", NAMESPACE_THREAD);
  313. $root->setAttribute("xmlns:at", NAMESPACE_TOMB);
  314. $root->setAttribute("xmlns:media", NAMESPACE_MEDIA);
  315. $root->setAttribute("xmlns:dfrn", NAMESPACE_DFRN);
  316. $root->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY);
  317. $root->setAttribute("xmlns:georss", NAMESPACE_GEORSS);
  318. $root->setAttribute("xmlns:poco", NAMESPACE_POCO);
  319. $root->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS);
  320. $root->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET);
  321. //$root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
  322. foreach ($items as $item) {
  323. $entry = self::entry($doc, $type, $item, $owner, true, 0);
  324. $root->appendChild($entry);
  325. }
  326. } else {
  327. $root = self::entry($doc, $type, $item, $owner, true, 0, true);
  328. }
  329. $atom = trim($doc->saveXML());
  330. return $atom;
  331. }
  332. /**
  333. * @brief Create XML text for DFRN mails
  334. *
  335. * @param array $item message elements
  336. * @param array $owner Owner record
  337. *
  338. * @return string DFRN mail
  339. * @todo Add type-hints
  340. */
  341. public static function mail($item, $owner)
  342. {
  343. $doc = new DOMDocument('1.0', 'utf-8');
  344. $doc->formatOutput = true;
  345. $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
  346. $mail = $doc->createElement("dfrn:mail");
  347. $sender = $doc->createElement("dfrn:sender");
  348. XML::addElement($doc, $sender, "dfrn:name", $owner['name']);
  349. XML::addElement($doc, $sender, "dfrn:uri", $owner['url']);
  350. XML::addElement($doc, $sender, "dfrn:avatar", $owner['thumb']);
  351. $mail->appendChild($sender);
  352. XML::addElement($doc, $mail, "dfrn:id", $item['uri']);
  353. XML::addElement($doc, $mail, "dfrn:in-reply-to", $item['parent-uri']);
  354. XML::addElement($doc, $mail, "dfrn:sentdate", datetime_convert('UTC', 'UTC', $item['created'] . '+00:00', ATOM_TIME));
  355. XML::addElement($doc, $mail, "dfrn:subject", $item['title']);
  356. XML::addElement($doc, $mail, "dfrn:content", $item['body']);
  357. $root->appendChild($mail);
  358. return(trim($doc->saveXML()));
  359. }
  360. /**
  361. * @brief Create XML text for DFRN friend suggestions
  362. *
  363. * @param array $item suggestion elements
  364. * @param array $owner Owner record
  365. *
  366. * @return string DFRN suggestions
  367. * @todo Add type-hints
  368. */
  369. public static function fsuggest($item, $owner)
  370. {
  371. $doc = new DOMDocument('1.0', 'utf-8');
  372. $doc->formatOutput = true;
  373. $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
  374. $suggest = $doc->createElement("dfrn:suggest");
  375. XML::addElement($doc, $suggest, "dfrn:url", $item['url']);
  376. XML::addElement($doc, $suggest, "dfrn:name", $item['name']);
  377. XML::addElement($doc, $suggest, "dfrn:photo", $item['photo']);
  378. XML::addElement($doc, $suggest, "dfrn:request", $item['request']);
  379. XML::addElement($doc, $suggest, "dfrn:note", $item['note']);
  380. $root->appendChild($suggest);
  381. return(trim($doc->saveXML()));
  382. }
  383. /**
  384. * @brief Create XML text for DFRN relocations
  385. *
  386. * @param array $owner Owner record
  387. * @param int $uid User ID
  388. *
  389. * @return string DFRN relocations
  390. * @todo Add type-hints
  391. */
  392. public static function relocate($owner, $uid)
  393. {
  394. /* get site pubkey. this could be a new installation with no site keys*/
  395. $pubkey = Config::get('system', 'site_pubkey');
  396. if (! $pubkey) {
  397. $res = Crypto::newKeypair(1024);
  398. Config::set('system', 'site_prvkey', $res['prvkey']);
  399. Config::set('system', 'site_pubkey', $res['pubkey']);
  400. }
  401. $rp = q(
  402. "SELECT `resource-id` , `scale`, type FROM `photo`
  403. WHERE `profile` = 1 AND `uid` = %d ORDER BY scale;",
  404. $uid
  405. );
  406. $photos = [];
  407. $ext = Image::supportedTypes();
  408. foreach ($rp as $p) {
  409. $photos[$p['scale']] = System::baseUrl().'/photo/'.$p['resource-id'].'-'.$p['scale'].'.'.$ext[$p['type']];
  410. }
  411. unset($rp, $ext);
  412. $doc = new DOMDocument('1.0', 'utf-8');
  413. $doc->formatOutput = true;
  414. $root = self::addHeader($doc, $owner, "dfrn:owner", "", false);
  415. $relocate = $doc->createElement("dfrn:relocate");
  416. XML::addElement($doc, $relocate, "dfrn:url", $owner['url']);
  417. XML::addElement($doc, $relocate, "dfrn:name", $owner['name']);
  418. XML::addElement($doc, $relocate, "dfrn:addr", $owner['addr']);
  419. XML::addElement($doc, $relocate, "dfrn:avatar", $owner['avatar']);
  420. XML::addElement($doc, $relocate, "dfrn:photo", $photos[4]);
  421. XML::addElement($doc, $relocate, "dfrn:thumb", $photos[5]);
  422. XML::addElement($doc, $relocate, "dfrn:micro", $photos[6]);
  423. XML::addElement($doc, $relocate, "dfrn:request", $owner['request']);
  424. XML::addElement($doc, $relocate, "dfrn:confirm", $owner['confirm']);
  425. XML::addElement($doc, $relocate, "dfrn:notify", $owner['notify']);
  426. XML::addElement($doc, $relocate, "dfrn:poll", $owner['poll']);
  427. XML::addElement($doc, $relocate, "dfrn:sitepubkey", Config::get('system', 'site_pubkey'));
  428. $root->appendChild($relocate);
  429. return(trim($doc->saveXML()));
  430. }
  431. /**
  432. * @brief Adds the header elements for the DFRN protocol
  433. *
  434. * @param object $doc XML document
  435. * @param array $owner Owner record
  436. * @param string $authorelement Element name for the author
  437. * @param string $alternatelink link to profile or category
  438. * @param bool $public Is it a header for public posts?
  439. *
  440. * @return object XML root object
  441. * @todo Add type-hints
  442. */
  443. private static function addHeader($doc, $owner, $authorelement, $alternatelink = "", $public = false)
  444. {
  445. if ($alternatelink == "") {
  446. $alternatelink = $owner['url'];
  447. }
  448. $root = $doc->createElementNS(NAMESPACE_ATOM1, 'feed');
  449. $doc->appendChild($root);
  450. $root->setAttribute("xmlns:thr", NAMESPACE_THREAD);
  451. $root->setAttribute("xmlns:at", NAMESPACE_TOMB);
  452. $root->setAttribute("xmlns:media", NAMESPACE_MEDIA);
  453. $root->setAttribute("xmlns:dfrn", NAMESPACE_DFRN);
  454. $root->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY);
  455. $root->setAttribute("xmlns:georss", NAMESPACE_GEORSS);
  456. $root->setAttribute("xmlns:poco", NAMESPACE_POCO);
  457. $root->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS);
  458. $root->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET);
  459. XML::addElement($doc, $root, "id", System::baseUrl()."/profile/".$owner["nick"]);
  460. XML::addElement($doc, $root, "title", $owner["name"]);
  461. $attributes = ["uri" => "https://friendi.ca", "version" => FRIENDICA_VERSION."-".DB_UPDATE_VERSION];
  462. XML::addElement($doc, $root, "generator", FRIENDICA_PLATFORM, $attributes);
  463. $attributes = ["rel" => "license", "href" => "http://creativecommons.org/licenses/by/3.0/"];
  464. XML::addElement($doc, $root, "link", "", $attributes);
  465. $attributes = ["rel" => "alternate", "type" => "text/html", "href" => $alternatelink];
  466. XML::addElement($doc, $root, "link", "", $attributes);
  467. if ($public) {
  468. // DFRN itself doesn't uses this. But maybe someone else wants to subscribe to the public feed.
  469. OStatus::hublinks($doc, $root, $owner["nick"]);
  470. $attributes = ["rel" => "salmon", "href" => System::baseUrl().&