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.

1062 lines
32KB

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