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.
 
 
 
 
 
 

1006 lines
31 KiB

  1. <?php
  2. /**
  3. * @file mod/network.php
  4. */
  5. use Friendica\App;
  6. use Friendica\Content\Feature;
  7. use Friendica\Content\ForumManager;
  8. use Friendica\Content\Nav;
  9. use Friendica\Content\Pager;
  10. use Friendica\Content\Widget;
  11. use Friendica\Content\Text\HTML;
  12. use Friendica\Core\ACL;
  13. use Friendica\Core\Config;
  14. use Friendica\Core\Hook;
  15. use Friendica\Core\L10n;
  16. use Friendica\Core\Logger;
  17. use Friendica\Core\Protocol;
  18. use Friendica\Core\Renderer;
  19. use Friendica\Core\Session;
  20. use Friendica\Database\DBA;
  21. use Friendica\DI;
  22. use Friendica\Model\Contact;
  23. use Friendica\Model\Group;
  24. use Friendica\Model\Item;
  25. use Friendica\Model\Profile;
  26. use Friendica\Model\Term;
  27. use Friendica\Module\Security\Login;
  28. use Friendica\Util\DateTimeFormat;
  29. use Friendica\Util\Proxy as ProxyUtils;
  30. use Friendica\Util\Strings;
  31. function network_init(App $a)
  32. {
  33. if (!local_user()) {
  34. notice(L10n::t('Permission denied.') . EOL);
  35. return;
  36. }
  37. Hook::add('head', __FILE__, 'network_infinite_scroll_head');
  38. $is_a_date_query = false;
  39. $group_id = (($a->argc > 1 && is_numeric($a->argv[1])) ? intval($a->argv[1]) : 0);
  40. $cid = 0;
  41. if (!empty($_GET['cid'])) {
  42. $cid = $_GET['cid'];
  43. $_GET['nets'] = '';
  44. $group_id = 0;
  45. }
  46. if ($a->argc > 1) {
  47. for ($x = 1; $x < $a->argc; $x ++) {
  48. if (DI::dtFormat()->isYearMonthDay($a->argv[$x])) {
  49. $is_a_date_query = true;
  50. break;
  51. }
  52. }
  53. }
  54. // convert query string to array. remove friendica args
  55. $query_array = [];
  56. parse_str(parse_url(DI::args()->getQueryString(), PHP_URL_QUERY), $query_array);
  57. // fetch last used network view and redirect if needed
  58. if (!$is_a_date_query) {
  59. $sel_nets = $_GET['nets'] ?? '';
  60. $sel_tabs = network_query_get_sel_tab($a);
  61. $sel_groups = network_query_get_sel_group($a);
  62. $last_sel_tabs = DI::pConfig()->get(local_user(), 'network.view', 'tab.selected');
  63. $remember_tab = ($sel_tabs[0] === 'active' && is_array($last_sel_tabs) && $last_sel_tabs[0] !== 'active');
  64. $net_baseurl = '/network';
  65. $net_args = [];
  66. if ($sel_groups !== false) {
  67. $net_baseurl .= '/' . $sel_groups;
  68. }
  69. if ($remember_tab) {
  70. // redirect if current selected tab is '/network' and
  71. // last selected tab is _not_ '/network?order=activity'.
  72. // and this isn't a date query
  73. $tab_args = [
  74. 'order=activity', //all
  75. 'order=post', //postord
  76. 'conv=1', //conv
  77. 'new=1', //new
  78. 'star=1', //starred
  79. 'bmark=1', //bookmarked
  80. ];
  81. $k = array_search('active', $last_sel_tabs);
  82. if ($k != 3) {
  83. // parse out tab queries
  84. $dest_qa = [];
  85. $dest_qs = $tab_args[$k];
  86. parse_str($dest_qs, $dest_qa);
  87. $net_args = array_merge($net_args, $dest_qa);
  88. } else {
  89. $remember_tab = false;
  90. }
  91. }
  92. if ($sel_nets) {
  93. $net_args['nets'] = $sel_nets;
  94. }
  95. if ($remember_tab) {
  96. $net_args = array_merge($query_array, $net_args);
  97. $net_queries = http_build_query($net_args);
  98. $redir_url = ($net_queries ? $net_baseurl . '?' . $net_queries : $net_baseurl);
  99. DI::baseUrl()->redirect($redir_url);
  100. }
  101. }
  102. if (empty(DI::page()['aside'])) {
  103. DI::page()['aside'] = '';
  104. }
  105. DI::page()['aside'] .= Group::sidebarWidget('network/0', 'network', 'standard', $group_id);
  106. DI::page()['aside'] .= ForumManager::widget(local_user(), $cid);
  107. DI::page()['aside'] .= Widget::postedByYear('network', local_user(), false);
  108. DI::page()['aside'] .= Widget::networks('network', $_GET['nets'] ?? '');
  109. DI::page()['aside'] .= Widget\SavedSearches::getHTML(DI::args()->getQueryString());
  110. DI::page()['aside'] .= Widget::fileAs('network', $_GET['file'] ?? '');
  111. }
  112. /**
  113. * Return selected tab from query
  114. *
  115. * urls -> returns
  116. * '/network' => $no_active = 'active'
  117. * '/network?order=activity' => $activity_active = 'active'
  118. * '/network?order=post' => $postord_active = 'active'
  119. * '/network?conv=1', => $conv_active = 'active'
  120. * '/network?new=1', => $new_active = 'active'
  121. * '/network?star=1', => $starred_active = 'active'
  122. * '/network?bmark=1', => $bookmarked_active = 'active'
  123. *
  124. * @param App $a
  125. * @return array ($no_active, $activity_active, $postord_active, $conv_active, $new_active, $starred_active, $bookmarked_active);
  126. */
  127. function network_query_get_sel_tab(App $a)
  128. {
  129. $no_active = '';
  130. $starred_active = '';
  131. $new_active = '';
  132. $bookmarked_active = '';
  133. $all_active = '';
  134. $conv_active = '';
  135. $postord_active = '';
  136. if (!empty($_GET['new'])) {
  137. $new_active = 'active';
  138. }
  139. if (!empty($_GET['star'])) {
  140. $starred_active = 'active';
  141. }
  142. if (!empty($_GET['bmark'])) {
  143. $bookmarked_active = 'active';
  144. }
  145. if (!empty($_GET['conv'])) {
  146. $conv_active = 'active';
  147. }
  148. if (($new_active == '') && ($starred_active == '') && ($bookmarked_active == '') && ($conv_active == '')) {
  149. $no_active = 'active';
  150. }
  151. if ($no_active == 'active' && !empty($_GET['order'])) {
  152. switch($_GET['order']) {
  153. case 'post' : $postord_active = 'active'; $no_active=''; break;
  154. case 'activity' : $all_active = 'active'; $no_active=''; break;
  155. }
  156. }
  157. return [$no_active, $all_active, $postord_active, $conv_active, $new_active, $starred_active, $bookmarked_active];
  158. }
  159. function network_query_get_sel_group(App $a)
  160. {
  161. $group = false;
  162. if ($a->argc >= 2 && is_numeric($a->argv[1])) {
  163. $group = $a->argv[1];
  164. }
  165. return $group;
  166. }
  167. /**
  168. * @brief Sets the pager data and returns SQL
  169. *
  170. * @param App $a The global App
  171. * @param Pager $pager
  172. * @param integer $update Used for the automatic reloading
  173. * @return string SQL with the appropriate LIMIT clause
  174. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  175. */
  176. function networkPager(App $a, Pager $pager, $update)
  177. {
  178. if ($update) {
  179. // only setup pagination on initial page view
  180. return ' LIMIT 100';
  181. }
  182. // check if we serve a mobile device and get the user settings
  183. // accordingly
  184. if (DI::mode()->isMobile()) {
  185. $itemspage_network = DI::pConfig()->get(local_user(), 'system', 'itemspage_mobile_network');
  186. $itemspage_network = ((intval($itemspage_network)) ? $itemspage_network : 20);
  187. } else {
  188. $itemspage_network = DI::pConfig()->get(local_user(), 'system', 'itemspage_network');
  189. $itemspage_network = ((intval($itemspage_network)) ? $itemspage_network : 40);
  190. }
  191. // now that we have the user settings, see if the theme forces
  192. // a maximum item number which is lower then the user choice
  193. if (($a->force_max_items > 0) && ($a->force_max_items < $itemspage_network)) {
  194. $itemspage_network = $a->force_max_items;
  195. }
  196. $pager->setItemsPerPage($itemspage_network);
  197. return sprintf(" LIMIT %d, %d ", $pager->getStart(), $pager->getItemsPerPage());
  198. }
  199. /**
  200. * @brief Sets items as seen
  201. *
  202. * @param array $condition The array with the SQL condition
  203. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  204. */
  205. function networkSetSeen($condition)
  206. {
  207. if (empty($condition)) {
  208. return;
  209. }
  210. $unseen = Item::exists($condition);
  211. if ($unseen) {
  212. Item::update(['unseen' => false], $condition);
  213. }
  214. }
  215. /**
  216. * @brief Create the conversation HTML
  217. *
  218. * @param App $a The global App
  219. * @param array $items Items of the conversation
  220. * @param Pager $pager
  221. * @param string $mode Display mode for the conversation
  222. * @param integer $update Used for the automatic reloading
  223. * @param string $ordering
  224. * @return string HTML of the conversation
  225. * @throws ImagickException
  226. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  227. */
  228. function networkConversation(App $a, $items, Pager $pager, $mode, $update, $ordering = '')
  229. {
  230. // Set this so that the conversation function can find out contact info for our wall-wall items
  231. $a->page_contact = $a->contact;
  232. if (!is_array($items)) {
  233. Logger::log("Expecting items to be an array. Got " . print_r($items, true));
  234. $items = [];
  235. }
  236. $o = conversation($a, $items, $pager, $mode, $update, false, $ordering, local_user());
  237. if (!$update) {
  238. if (DI::pConfig()->get(local_user(), 'system', 'infinite_scroll')) {
  239. $o .= HTML::scrollLoader();
  240. } else {
  241. $o .= $pager->renderMinimal(count($items));
  242. }
  243. }
  244. return $o;
  245. }
  246. function network_content(App $a, $update = 0, $parent = 0)
  247. {
  248. if (!local_user()) {
  249. return Login::form();
  250. }
  251. /// @TODO Is this really necessary? $a is already available to hooks
  252. $arr = ['query' => DI::args()->getQueryString()];
  253. Hook::callAll('network_content_init', $arr);
  254. if (!empty($_GET['new']) || !empty($_GET['file'])) {
  255. $o = networkFlatView($a, $update);
  256. } else {
  257. $o = networkThreadedView($a, $update, $parent);
  258. }
  259. if ($o === '') {
  260. info("No items found");
  261. }
  262. return $o;
  263. }
  264. /**
  265. * @brief Get the network content in flat view
  266. *
  267. * @param App $a The global App
  268. * @param integer $update Used for the automatic reloading
  269. * @return string HTML of the network content in flat view
  270. * @throws ImagickException
  271. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  272. * @global Pager $pager
  273. */
  274. function networkFlatView(App $a, $update = 0)
  275. {
  276. global $pager;
  277. // Rawmode is used for fetching new content at the end of the page
  278. $rawmode = (isset($_GET['mode']) && ($_GET['mode'] == 'raw'));
  279. $o = '';
  280. $file = $_GET['file'] ?? '';
  281. if (!$update && !$rawmode) {
  282. $tabs = network_tabs($a);
  283. $o .= $tabs;
  284. Nav::setSelected('network');
  285. $x = [
  286. 'is_owner' => true,
  287. 'allow_location' => $a->user['allow_location'],
  288. 'default_location' => $a->user['default-location'],
  289. 'nickname' => $a->user['nickname'],
  290. 'lockstate' => (is_array($a->user) &&
  291. (strlen($a->user['allow_cid']) || strlen($a->user['allow_gid']) ||
  292. strlen($a->user['deny_cid']) || strlen($a->user['deny_gid'])) ? 'lock' : 'unlock'),
  293. 'default_perms' => ACL::getDefaultUserPermissions($a->user),
  294. 'acl' => ACL::getFullSelectorHTML(DI::page(), $a->user, true),
  295. 'bang' => '',
  296. 'visitor' => 'block',
  297. 'profile_uid' => local_user(),
  298. 'content' => '',
  299. ];
  300. $o .= status_editor($a, $x);
  301. if (!Config::get('theme', 'hide_eventlist')) {
  302. $o .= Profile::getBirthdays();
  303. $o .= Profile::getEventsReminderHTML();
  304. }
  305. }
  306. $pager = new Pager(DI::args()->getQueryString());
  307. networkPager($a, $pager, $update);
  308. $item_params = ['order' => ['id' => true]];
  309. if (strlen($file)) {
  310. $term_condition = ["`term` = ? AND `otype` = ? AND `type` = ? AND `uid` = ?",
  311. $file, Term::OBJECT_TYPE_POST, Term::FILE, local_user()];
  312. $term_params = ['order' => ['tid' => true], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
  313. $result = DBA::select('term', ['oid'], $term_condition, $term_params);
  314. $posts = [];
  315. while ($term = DBA::fetch($result)) {
  316. $posts[] = $term['oid'];
  317. }
  318. DBA::close($result);
  319. if (count($posts) == 0) {
  320. return '';
  321. }
  322. $item_condition = ['uid' => local_user(), 'id' => $posts];
  323. } else {
  324. $item_condition = ['uid' => local_user()];
  325. $item_params['limit'] = [$pager->getStart(), $pager->getItemsPerPage()];
  326. networkSetSeen(['unseen' => true, 'uid' => local_user()]);
  327. }
  328. $result = Item::selectForUser(local_user(), [], $item_condition, $item_params);
  329. $items = Item::inArray($result);
  330. $o .= networkConversation($a, $items, $pager, 'network-new', $update);
  331. return $o;
  332. }
  333. /**
  334. * @brief Get the network content in threaded view
  335. *
  336. * @param App $a The global App
  337. * @param integer $update Used for the automatic reloading
  338. * @param integer $parent
  339. * @return string HTML of the network content in flat view
  340. * @throws ImagickException
  341. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  342. * @global Pager $pager
  343. */
  344. function networkThreadedView(App $a, $update, $parent)
  345. {
  346. /// @TODO this will have to be converted to a static property of the converted Module\Network class
  347. global $pager;
  348. // Rawmode is used for fetching new content at the end of the page
  349. $rawmode = (isset($_GET['mode']) AND ( $_GET['mode'] == 'raw'));
  350. if (isset($_GET['last_received']) && isset($_GET['last_commented']) && isset($_GET['last_created']) && isset($_GET['last_id'])) {
  351. $last_received = DateTimeFormat::utc($_GET['last_received']);
  352. $last_commented = DateTimeFormat::utc($_GET['last_commented']);
  353. $last_created = DateTimeFormat::utc($_GET['last_created']);
  354. $last_id = intval($_GET['last_id']);
  355. } else {
  356. $last_received = '';
  357. $last_commented = '';
  358. $last_created = '';
  359. $last_id = 0;
  360. }
  361. $datequery = $datequery2 = '';
  362. $gid = 0;
  363. $default_permissions = [];
  364. if ($a->argc > 1) {
  365. for ($x = 1; $x < $a->argc; $x ++) {
  366. if (DI::dtFormat()->isYearMonthDay($a->argv[$x])) {
  367. if ($datequery) {
  368. $datequery2 = Strings::escapeHtml($a->argv[$x]);
  369. } else {
  370. $datequery = Strings::escapeHtml($a->argv[$x]);
  371. $_GET['order'] = 'post';
  372. }
  373. } elseif (intval($a->argv[$x])) {
  374. $gid = intval($a->argv[$x]);
  375. $default_permissions['allow_gid'] = [$gid];
  376. }
  377. }
  378. }
  379. $o = '';
  380. $cid = intval($_GET['cid'] ?? 0);
  381. $star = intval($_GET['star'] ?? 0);
  382. $bmark = intval($_GET['bmark'] ?? 0);
  383. $conv = intval($_GET['conv'] ?? 0);
  384. $order = Strings::escapeTags(($_GET['order'] ?? '') ?: 'activity');
  385. $nets = $_GET['nets'] ?? '';
  386. $allowedCids = [];
  387. if ($cid) {
  388. $allowedCids[] = (int) $cid;
  389. } elseif ($nets) {
  390. $condition = [
  391. 'uid' => local_user(),
  392. 'network' => $nets,
  393. 'self' => false,
  394. 'blocked' => false,
  395. 'pending' => false,
  396. 'archive' => false,
  397. 'rel' => [Contact::SHARING, Contact::FRIEND],
  398. ];
  399. $contactStmt = DBA::select('contact', ['id'], $condition);
  400. while ($contact = DBA::fetch($contactStmt)) {
  401. $allowedCids[] = (int) $contact['id'];
  402. }
  403. DBA::close($contactStmt);
  404. }
  405. if (count($allowedCids)) {
  406. $default_permissions['allow_cid'] = $allowedCids;
  407. }
  408. if (!$update && !$rawmode) {
  409. $tabs = network_tabs($a);
  410. $o .= $tabs;
  411. if ($gid && ($t = Contact::getOStatusCountByGroupId($gid)) && !DI::pConfig()->get(local_user(), 'system', 'nowarn_insecure')) {
  412. notice(L10n::tt("Warning: This group contains %s member from a network that doesn't allow non public messages.",
  413. "Warning: This group contains %s members from a network that doesn't allow non public messages.",
  414. $t) . EOL);
  415. notice(L10n::t("Messages in this group won't be send to these receivers.").EOL);
  416. }
  417. Nav::setSelected('network');
  418. $content = '';
  419. if ($cid) {
  420. // If $cid belongs to a communitity forum or a privat goup,.add a mention to the status editor
  421. $condition = ["`id` = ? AND (`forum` OR `prv`)", $cid];
  422. $contact = DBA::selectFirst('contact', ['addr', 'nick'], $condition);
  423. if (DBA::isResult($contact)) {
  424. if ($contact['addr'] != '') {
  425. $content = '!' . $contact['addr'];
  426. } else {
  427. $content = '!' . $contact['nick'] . '+' . $cid;
  428. }
  429. }
  430. }
  431. $x = [
  432. 'is_owner' => true,
  433. 'allow_location' => $a->user['allow_location'],
  434. 'default_location' => $a->user['default-location'],
  435. 'nickname' => $a->user['nickname'],
  436. 'lockstate' => ($gid || $cid || $nets || (is_array($a->user) &&
  437. (strlen($a->user['allow_cid']) || strlen($a->user['allow_gid']) ||
  438. strlen($a->user['deny_cid']) || strlen($a->user['deny_gid']))) ? 'lock' : 'unlock'),
  439. 'default_perms' => ACL::getDefaultUserPermissions($a->user),
  440. 'acl' => ACL::getFullSelectorHTML(DI::page(), $a->user, true, $default_permissions),
  441. 'bang' => (($gid || $cid || $nets) ? '!' : ''),
  442. 'visitor' => 'block',
  443. 'profile_uid' => local_user(),
  444. 'content' => $content,
  445. ];
  446. $o .= status_editor($a, $x);
  447. }
  448. // We don't have to deal with ACLs on this page. You're looking at everything
  449. // that belongs to you, hence you can see all of it. We will filter by group if
  450. // desired.
  451. $sql_post_table = '';
  452. $sql_options = ($star ? " AND `thread`.`starred` " : '');
  453. $sql_options .= ($bmark ? sprintf(" AND `thread`.`post-type` = %d ", Item::PT_PAGE) : '');
  454. $sql_extra = $sql_options;
  455. $sql_extra2 = '';
  456. $sql_extra3 = '';
  457. $sql_table = '`thread`';
  458. $sql_parent = '`iid`';
  459. if ($update) {
  460. $sql_table = '`item`';
  461. $sql_parent = '`parent`';
  462. $sql_post_table = " INNER JOIN `thread` ON `thread`.`iid` = `item`.`parent`";
  463. }
  464. $sql_nets = (($nets) ? sprintf(" AND $sql_table.`network` = '%s' ", DBA::escape($nets)) : '');
  465. $sql_tag_nets = (($nets) ? sprintf(" AND `item`.`network` = '%s' ", DBA::escape($nets)) : '');
  466. if ($gid) {
  467. $group = DBA::selectFirst('group', ['name'], ['id' => $gid, 'uid' => local_user()]);
  468. if (!DBA::isResult($group)) {
  469. if ($update) {
  470. exit();
  471. }
  472. notice(L10n::t('No such group') . EOL);
  473. DI::baseUrl()->redirect('network/0');
  474. // NOTREACHED
  475. }
  476. $contacts = Group::expand(local_user(), [$gid]);
  477. if ((is_array($contacts)) && count($contacts)) {
  478. $contact_str_self = '';
  479. $contact_str = implode(',', $contacts);
  480. $self = DBA::selectFirst('contact', ['id'], ['uid' => local_user(), 'self' => true]);
  481. if (DBA::isResult($self)) {
  482. $contact_str_self = $self['id'];
  483. }
  484. $sql_post_table .= " INNER JOIN `item` AS `temp1` ON `temp1`.`id` = " . $sql_table . "." . $sql_parent;
  485. $sql_extra3 .= " AND (`thread`.`contact-id` IN ($contact_str) ";
  486. $sql_extra3 .= " OR (`thread`.`contact-id` = '$contact_str_self' AND `temp1`.`allow_gid` LIKE '" . Strings::protectSprintf('%<' . intval($gid) . '>%') . "' AND `temp1`.`private`))";
  487. } else {
  488. $sql_extra3 .= " AND false ";
  489. info(L10n::t('Group is empty'));
  490. }
  491. $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [
  492. '$title' => L10n::t('Group: %s', $group['name'])
  493. ]) . $o;
  494. } elseif ($cid) {
  495. $fields = ['id', 'name', 'network', 'writable', 'nurl',
  496. 'forum', 'prv', 'contact-type', 'addr', 'thumb', 'location'];
  497. $condition = ["`id` = ? AND (NOT `blocked` OR `pending`)", $cid];
  498. $contact = DBA::selectFirst('contact', $fields, $condition);
  499. if (DBA::isResult($contact)) {
  500. $sql_extra = " AND " . $sql_table . ".`contact-id` = " . intval($cid);
  501. $entries[0] = [
  502. 'id' => 'network',
  503. 'name' => $contact['name'],
  504. 'itemurl' => ($contact['addr'] ?? '') ?: $contact['nurl'],
  505. 'thumb' => ProxyUtils::proxifyUrl($contact['thumb'], false, ProxyUtils::SIZE_THUMB),
  506. 'details' => $contact['location'],
  507. ];
  508. $entries[0]['account_type'] = Contact::getAccountType($contact);
  509. $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('viewcontact_template.tpl'), [
  510. 'contacts' => $entries,
  511. 'id' => 'network',
  512. ]) . $o;
  513. if ($contact['network'] === Protocol::OSTATUS && $contact['writable'] && !DI::pConfig()->get(local_user(),'system','nowarn_insecure')) {
  514. notice(L10n::t('Private messages to this person are at risk of public disclosure.') . EOL);
  515. }
  516. } else {
  517. notice(L10n::t('Invalid contact.') . EOL);
  518. DI::baseUrl()->redirect('network');
  519. // NOTREACHED
  520. }
  521. }
  522. if (!$gid && !$cid && !$update && !Config::get('theme', 'hide_eventlist')) {
  523. $o .= Profile::getBirthdays();
  524. $o .= Profile::getEventsReminderHTML();
  525. }
  526. if ($datequery) {
  527. $sql_extra3 .= Strings::protectSprintf(sprintf(" AND $sql_table.received <= '%s' ",
  528. DBA::escape(DateTimeFormat::convert($datequery, 'UTC', date_default_timezone_get()))));
  529. }
  530. if ($datequery2) {
  531. $sql_extra3 .= Strings::protectSprintf(sprintf(" AND $sql_table.received >= '%s' ",
  532. DBA::escape(DateTimeFormat::convert($datequery2, 'UTC', date_default_timezone_get()))));
  533. }
  534. if ($conv) {
  535. $sql_extra3 .= " AND $sql_table.`mention`";
  536. }
  537. // Normal conversation view
  538. if ($order === 'post') {
  539. $ordering = '`received`';
  540. $order_mode = 'received';
  541. } else {
  542. $ordering = '`commented`';
  543. $order_mode = 'commented';
  544. }
  545. $sql_order = "$sql_table.$ordering";
  546. if (!empty($_GET['offset'])) {
  547. $sql_range = sprintf(" AND $sql_order <= '%s'", DBA::escape($_GET['offset']));
  548. } else {
  549. $sql_range = '';
  550. }
  551. $pager = new Pager(DI::args()->getQueryString());
  552. $pager_sql = networkPager($a, $pager, $update);
  553. $last_date = '';
  554. switch ($order_mode) {
  555. case 'received':
  556. if ($last_received != '') {
  557. $last_date = $last_received;
  558. $sql_range .= sprintf(" AND $sql_table.`received` < '%s'", DBA::escape($last_received));
  559. $pager->setPage(1);
  560. $pager_sql = sprintf(" LIMIT %d, %d ", $pager->getStart(), $pager->getItemsPerPage());
  561. }
  562. break;
  563. case 'commented':
  564. if ($last_commented != '') {
  565. $last_date = $last_commented;
  566. $sql_range .= sprintf(" AND $sql_table.`commented` < '%s'", DBA::escape($last_commented));
  567. $pager->setPage(1);
  568. $pager_sql = sprintf(" LIMIT %d, %d ", $pager->getStart(), $pager->getItemsPerPage());
  569. }
  570. break;
  571. case 'created':
  572. if ($last_created != '') {
  573. $last_date = $last_created;
  574. $sql_range .= sprintf(" AND $sql_table.`created` < '%s'", DBA::escape($last_created));
  575. $pager->setPage(1);
  576. $pager_sql = sprintf(" LIMIT %d, %d ", $pager->getStart(), $pager->getItemsPerPage());
  577. }
  578. break;
  579. case 'id':
  580. if (($last_id > 0) && ($sql_table == '`thread`')) {
  581. $sql_range .= sprintf(" AND $sql_table.`iid` < '%s'", DBA::escape($last_id));
  582. $pager->setPage(1);
  583. $pager_sql = sprintf(" LIMIT %d, %d ", $pager->getStart(), $pager->getItemsPerPage());
  584. }
  585. break;
  586. }
  587. // Fetch a page full of parent items for this page
  588. if ($update) {
  589. if (!empty($parent)) {
  590. // Load only a single thread
  591. $sql_extra4 = "`item`.`id` = ".intval($parent);
  592. } else {
  593. // Load all unseen items
  594. $sql_extra4 = "`item`.`unseen`";
  595. if (Config::get("system", "like_no_comment")) {
  596. $sql_extra4 .= " AND `item`.`gravity` IN (" . GRAVITY_PARENT . "," . GRAVITY_COMMENT . ")";
  597. }
  598. if ($order === 'post') {
  599. // Only show toplevel posts when updating posts in this order mode
  600. $sql_extra4 .= " AND `item`.`id` = `item`.`parent`";
  601. }
  602. }
  603. $r = q("SELECT `item`.`parent-uri` AS `uri`, `item`.`parent` AS `item_id`, $sql_order AS `order_date`
  604. FROM `item` $sql_post_table
  605. STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id`
  606. AND (NOT `contact`.`blocked` OR `contact`.`pending`)
  607. AND (`item`.`gravity` != %d
  608. OR `contact`.`uid` = `item`.`uid` AND `contact`.`self`
  609. OR `contact`.`rel` IN (%d, %d) AND NOT `contact`.`readonly`)
  610. LEFT JOIN `user-item` ON `user-item`.`iid` = `item`.`id` AND `user-item`.`uid` = %d
  611. WHERE `item`.`uid` = %d AND `item`.`visible` AND NOT `item`.`deleted`
  612. AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`)
  613. AND NOT `item`.`moderated` AND $sql_extra4
  614. $sql_extra3 $sql_extra $sql_range $sql_nets
  615. ORDER BY `order_date` DESC LIMIT 100",
  616. intval(GRAVITY_PARENT),
  617. intval(Contact::SHARING),
  618. intval(Contact::FRIEND),
  619. intval(local_user()),
  620. intval(local_user())
  621. );
  622. } else {
  623. $r = q("SELECT `item`.`uri`, `thread`.`iid` AS `item_id`, $sql_order AS `order_date`
  624. FROM `thread` $sql_post_table
  625. STRAIGHT_JOIN `contact` ON `contact`.`id` = `thread`.`contact-id`
  626. AND (NOT `contact`.`blocked` OR `contact`.`pending`)
  627. STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid`
  628. AND (`item`.`gravity` != %d
  629. OR `contact`.`uid` = `item`.`uid` AND `contact`.`self`
  630. OR `contact`.`rel` IN (%d, %d) AND NOT `contact`.`readonly`)
  631. LEFT JOIN `user-item` ON `user-item`.`iid` = `item`.`id` AND `user-item`.`uid` = %d
  632. WHERE `thread`.`uid` = %d AND `thread`.`visible` AND NOT `thread`.`deleted`
  633. AND NOT `thread`.`moderated`
  634. AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`)
  635. $sql_extra2 $sql_extra3 $sql_range $sql_extra $sql_nets
  636. ORDER BY `order_date` DESC $pager_sql",
  637. intval(GRAVITY_PARENT),
  638. intval(Contact::SHARING),
  639. intval(Contact::FRIEND),
  640. intval(local_user()),
  641. intval(local_user())
  642. );
  643. }
  644. // Only show it when unfiltered (no groups, no networks, ...)
  645. if (in_array($nets, ['', Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]) && (strlen($sql_extra . $sql_extra2 . $sql_extra3) == 0)) {
  646. if (DBA::isResult($r)) {
  647. $top_limit = current($r)['order_date'];
  648. $bottom_limit = end($r)['order_date'];
  649. if (empty($_SESSION['network_last_top_limit']) || ($_SESSION['network_last_top_limit'] < $top_limit)) {
  650. $_SESSION['network_last_top_limit'] = $top_limit;
  651. }
  652. } else {
  653. $top_limit = $bottom_limit = DateTimeFormat::utcNow();
  654. }
  655. // When checking for updates we need to fetch from the newest date to the newest date before
  656. // Only do this, when the last stored date isn't too long ago (10 times the update interval)
  657. $browser_update = DI::pConfig()->get(local_user(), 'system', 'update_interval', 40000) / 1000;
  658. if (($browser_update > 0) && $update && !empty($_SESSION['network_last_date']) &&
  659. (($bottom_limit < $_SESSION['network_last_date']) || ($top_limit == $bottom_limit)) &&
  660. ((time() - $_SESSION['network_last_date_timestamp']) < ($browser_update * 10))) {
  661. $bottom_limit = $_SESSION['network_last_date'];
  662. }
  663. $_SESSION['network_last_date'] = Session::get('network_last_top_limit', $top_limit);
  664. $_SESSION['network_last_date_timestamp'] = time();
  665. if ($last_date > $top_limit) {
  666. $top_limit = $last_date;
  667. } elseif ($pager->getPage() == 1) {
  668. // Highest possible top limit when we are on the first page
  669. $top_limit = DateTimeFormat::utcNow();
  670. }
  671. $items = DBA::p("SELECT `item`.`parent-uri` AS `uri`, 0 AS `item_id`, `item`.$ordering AS `order_date`, `author`.`url` AS `author-link` FROM `item`
  672. STRAIGHT_JOIN (SELECT `oid` FROM `term` WHERE `term` IN
  673. (SELECT SUBSTR(`term`, 2) FROM `search` WHERE `uid` = ? AND `term` LIKE '#%') AND `otype` = ? AND `type` = ? AND `uid` = 0) AS `term`
  674. ON `item`.`id` = `term`.`oid`
  675. STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `item`.`author-id`
  676. WHERE `item`.`uid` = 0 AND `item`.$ordering < ? AND `item`.$ordering > ? AND `item`.`gravity` = ?
  677. AND NOT `author`.`hidden` AND NOT `author`.`blocked`" . $sql_tag_nets,
  678. local_user(), TERM_OBJ_POST, TERM_HASHTAG,
  679. $top_limit, $bottom_limit, GRAVITY_PARENT);
  680. $data = DBA::toArray($items);
  681. if (count($data) > 0) {
  682. $tag_top_limit = current($data)['order_date'];
  683. if ($_SESSION['network_last_date'] < $tag_top_limit) {
  684. $_SESSION['network_last_date'] = $tag_top_limit;
  685. }
  686. Logger::log('Tagged items: ' . count($data) . ' - ' . $bottom_limit . ' - ' . $top_limit . ' - ' . local_user().' - '.(int)$update);
  687. $s = [];
  688. foreach ($r as $item) {
  689. $s[$item['uri']] = $item;
  690. }
  691. foreach ($data as $item) {
  692. // Don't show hash tag posts from blocked or ignored contacts
  693. $condition = ["`nurl` = ? AND `uid` = ? AND (`blocked` OR `readonly`)",
  694. Strings::normaliseLink($item['author-link']), local_user()];
  695. if (!DBA::exists('contact', $condition)) {
  696. $s[$item['uri']] = $item;
  697. }
  698. }
  699. $r = $s;
  700. }
  701. }
  702. $parents_str = '';
  703. $date_offset = '';
  704. $items = $r;
  705. if (DBA::isResult($items)) {
  706. $parents_arr = [];
  707. foreach ($items as $item) {
  708. if ($date_offset < $item['order_date']) {
  709. $date_offset = $item['order_date'];
  710. }
  711. if (!in_array($item['item_id'], $parents_arr) && ($item['item_id'] > 0)) {
  712. $parents_arr[] = $item['item_id'];
  713. }
  714. }
  715. $parents_str = implode(', ', $parents_arr);
  716. }
  717. if (!empty($_GET['offset'])) {
  718. $date_offset = $_GET['offset'];
  719. }
  720. $query_string = DI::args()->getQueryString();
  721. if ($date_offset && !preg_match('/[?&].offset=/', $query_string)) {
  722. $query_string .= '&offset=' . urlencode($date_offset);
  723. }
  724. $pager->setQueryString($query_string);
  725. // We aren't going to try and figure out at the item, group, and page
  726. // level which items you've seen and which you haven't. If you're looking
  727. // at the top level network page just mark everything seen.
  728. if (!$gid && !$cid && !$star) {
  729. $condition = ['unseen' => true, 'uid' => local_user()];
  730. networkSetSeen($condition);
  731. } elseif ($parents_str) {
  732. $condition = ["`uid` = ? AND `unseen` AND `parent` IN (" . DBA::escape($parents_str) . ")", local_user()];
  733. networkSetSeen($condition);
  734. }
  735. $mode = 'network';
  736. $o .= networkConversation($a, $items, $pager, $mode, $update, $ordering);
  737. return $o;
  738. }
  739. /**
  740. * @brief Get the network tabs menu
  741. *
  742. * @param App $a The global App
  743. * @return string Html of the networktab
  744. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  745. */
  746. function network_tabs(App $a)
  747. {
  748. // item filter tabs
  749. /// @TODO fix this logic, reduce duplication
  750. /// $a->page['content'] .= '<div class="tabs-wrapper">';
  751. list($no_active, $all_active, $post_active, $conv_active, $new_active, $starred_active, $bookmarked_active) = network_query_get_sel_tab($a);
  752. // if no tabs are selected, defaults to activitys
  753. if ($no_active == 'active') {
  754. $all_active = 'active';
  755. }
  756. $cmd = DI::args()->getCommand();
  757. $def_param = [];
  758. if (!empty($_GET['cid'])) {
  759. $def_param['cid'] = $_GET['cid'];
  760. }
  761. // tabs
  762. $tabs = [
  763. [
  764. 'label' => L10n::t('Latest Activity'),
  765. 'url' => $cmd . '?' . http_build_query(array_merge($def_param, ['order' => 'activity'])),
  766. 'sel' => $all_active,
  767. 'title' => L10n::t('Sort by latest activity'),
  768. 'id' => 'activity-order-tab',
  769. 'accesskey' => 'e',
  770. ],
  771. [
  772. 'label' => L10n::t('Latest Posts'),
  773. 'url' => $cmd . '?' . http_build_query(array_merge($def_param, ['order' => 'post'])),
  774. 'sel' => $post_active,
  775. 'title' => L10n::t('Sort by post received date'),
  776. 'id' => 'post-order-tab',
  777. 'accesskey' => 't',
  778. ],
  779. ];
  780. $tabs[] = [
  781. 'label' => L10n::t('Personal'),
  782. 'url' => $cmd . '?' . http_build_query(array_merge($def_param, ['conv' => true])),
  783. 'sel' => $conv_active,
  784. 'title' => L10n::t('Posts that mention or involve you'),
  785. 'id' => 'personal-tab',
  786. 'accesskey' => 'r',
  787. ];
  788. if (Feature::isEnabled(local_user(), 'new_tab')) {
  789. $tabs[] = [
  790. 'label' => L10n::t('New'),
  791. 'url' => $cmd . '?' . http_build_query(array_merge($def_param, ['new' => true])),
  792. 'sel' => $new_active,
  793. 'title' => L10n::t('Activity Stream - by date'),
  794. 'id' => 'activitiy-by-date-tab',
  795. 'accesskey' => 'w',
  796. ];
  797. }
  798. if (Feature::isEnabled(local_user(), 'link_tab')) {
  799. $tabs[] = [
  800. 'label' => L10n::t('Shared Links'),
  801. 'url' => $cmd . '?' . http_build_query(array_merge($def_param, ['bmark' => true])),
  802. 'sel' => $bookmarked_active,
  803. 'title' => L10n::t('Interesting Links'),
  804. 'id' => 'shared-links-tab',
  805. 'accesskey' => 'b',
  806. ];
  807. }
  808. $tabs[] = [
  809. 'label' => L10n::t('Starred'),
  810. 'url' => $cmd . '?' . http_build_query(array_merge($def_param, ['star' => true])),
  811. 'sel' => $starred_active,
  812. 'title' => L10n::t('Favourite Posts'),
  813. 'id' => 'starred-posts-tab',
  814. 'accesskey' => 'm',
  815. ];
  816. // save selected tab, but only if not in file mode
  817. if (empty($_GET['file'])) {
  818. DI::pConfig()->set(local_user(), 'network.view', 'tab.selected', [
  819. $all_active, $post_active, $conv_active, $new_active, $starred_active, $bookmarked_active
  820. ]);
  821. }
  822. $arr = ['tabs' => $tabs];
  823. Hook::callAll('network_tabs', $arr);
  824. $tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
  825. return Renderer::replaceMacros($tpl, ['$tabs' => $arr['tabs']]);
  826. // --- end item filter tabs
  827. }
  828. /**
  829. * Network hook into the HTML head to enable infinite scroll.
  830. *
  831. * Since the HTML head is built after the module content has been generated, we need to retrieve the base query string
  832. * of the page to make the correct asynchronous call. This is obtained through the Pager that was instantiated in
  833. * networkThreadedView or networkFlatView.
  834. *
  835. * @param App $a
  836. * @param string $htmlhead The head tag HTML string
  837. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  838. * @global Pager $pager
  839. */
  840. function network_infinite_scroll_head(App $a, &$htmlhead)
  841. {
  842. /// @TODO this will have to be converted to a static property of the converted Module\Network class
  843. /**
  844. * @var $pager Pager
  845. */
  846. global $pager;
  847. if (DI::pConfig()->get(local_user(), 'system', 'infinite_scroll')
  848. && ($_GET['mode'] ?? '') != 'minimal'
  849. ) {
  850. $tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
  851. $htmlhead .= Renderer::replaceMacros($tpl, [
  852. '$pageno' => $pager->getPage(),
  853. '$reload_uri' => $pager->getBaseQueryString()
  854. ]);
  855. }
  856. }