1: <?php
2:
3: /**
4: * Feeds modules
5: * @package modules
6: * @subpackage feeds
7: */
8:
9: if (!defined('DEBUG_MODE')) { die(); }
10:
11: require APP_PATH.'modules/feeds/hm-feed.php';
12:
13: /**
14: * @subpackage feeds/handler
15: */
16: class Hm_Handler_feed_list_type extends Hm_Handler_Module {
17: public function process() {
18: if (array_key_exists('list_path', $this->request->get)) {
19: $path = $this->request->get['list_path'];
20: if ($path == 'feeds') {
21: $this->out('list_path', 'feeds', false);
22: $this->out('mailbox_list_title', array('All Feeds'));
23: $this->out('message_list_since', $this->user_config->get('feed_since_setting', DEFAULT_FEED_SINCE));
24: $this->out('per_source_limit', $this->user_config->get('feed_limit_setting', DEFAULT_FEED_LIMIT));
25: }
26: elseif (preg_match("/^feeds_.+$/", $path)) {
27: $this->out('message_list_since', $this->user_config->get('feed_since_setting', DEFAULT_FEED_SINCE));
28: $this->out('per_source_limit', $this->user_config->get('feed_limit_setting', DEFAULT_FEED_LIMIT));
29: $this->out('list_path', $path, false);
30: $this->out('custom_list_controls', ' ');
31: $parts = explode('_', $path, 2);
32: $details = Hm_Feed_List::dump($parts[1]);
33: if (!empty($details)) {
34: $this->out('mailbox_list_title', array('Feeds', $details['name']));
35: }
36: }
37: }
38: }
39: }
40:
41: /**
42: * @subpackage feeds/handler
43: */
44: class Hm_Handler_process_feed_limit_setting extends Hm_Handler_Module {
45: public function process() {
46: process_site_setting('feed_limit', $this, 'max_source_setting_callback', DEFAULT_FEED_LIMIT);
47: }
48: }
49:
50: /**
51: * @subpackage feeds/handler
52: */
53: class Hm_Handler_process_feed_since_setting extends Hm_Handler_Module {
54: public function process() {
55: process_site_setting('feed_since', $this, 'since_setting_callback', DEFAULT_FEED_SINCE);
56: }
57: }
58:
59: /**
60: * @subpackage feeds/handler
61: */
62: class Hm_Handler_process_unread_feeds_setting extends Hm_Handler_Module {
63: public function process() {
64: function unread_feed_setting_callback($val) { return $val; }
65: process_site_setting('unread_exclude_feeds', $this, 'unread_feed_setting_callback', DEFAULT_UNREAD_EXCLUDE_FEEDS, true);
66: }
67: }
68:
69: /**
70: * @subpackage feeds/handler
71: */
72: class Hm_Handler_feed_connect extends Hm_Handler_Module {
73: public function process() {
74: $failed = true;
75: if (isset($this->request->post['feed_connect'])) {
76: list($success, $form) = $this->process_form(array('feed_id'));
77: if ($success) {
78: $feed_data = Hm_Feed_List::dump($form['feed_id']);
79: if ($feed_data) {
80: $feed = is_news_feed($feed_data['server']);
81: if ($feed) {
82: $failed = false;
83: Hm_Msgs::add("Successfully connected to the feed", "info");
84: }
85: }
86: }
87: if ($failed) {
88: Hm_Msgs::add("Failed to connect to the feed", "warning");
89: }
90: }
91: }
92: }
93:
94: /**
95: * @subpackage feeds/handler
96: */
97: class Hm_Handler_delete_feed extends Hm_Handler_Module {
98: public function process() {
99: if (isset($this->request->post['delete_feed'])) {
100: list($success, $form) = $this->process_form(array('feed_id'));
101: if ($success) {
102: $res = Hm_Feed_List::del($form['feed_id']);
103: if ($res) {
104: $this->out('deleted_server_id', $form['feed_id']);
105: $this->out('reload_folders', true);
106: Hm_Msgs::add('Feed deleted');
107: $this->session->record_unsaved('Feed deleted');
108: }
109: }
110: else {
111: $this->out('old_form', $form);
112: }
113: }
114: }
115: }
116:
117: /**
118: * @subpackage feeds/handler
119: */
120: class Hm_Handler_feed_status extends Hm_Handler_Module {
121: public function process() {
122: list($success, $form) = $this->process_form(array('feed_server_ids'));
123: if ($success) {
124: $ids = explode(',', $form['feed_server_ids']);
125: foreach ($ids as $id) {
126: $start_time = microtime(true);
127: $feed_data = Hm_Feed_List::dump($id);
128: if ($feed_data) {
129: $feed = is_news_feed($feed_data['server']);
130: if ($feed && $feed->parsed_data) {
131: $this->out('feed_connect_time', microtime(true) - $start_time);
132: $this->out('feed_connect_status', 'Connected');
133: $this->out('feed_status_server_id', $id);
134: }
135: }
136: }
137: }
138: }
139: }
140:
141: /**
142: * @subpackage feeds/handler
143: */
144: class Hm_Handler_feed_message_action extends Hm_Handler_Module {
145: public function process() {
146:
147: list($success, $form) = $this->process_form(array('action_type', 'message_ids'));
148: if ($success) {
149: $id_list = explode(',', $form['message_ids']);
150: foreach ($id_list as $msg_id) {
151: if (preg_match("/^feeds_(\d)+_.+$/", $msg_id)) {
152: $parts = explode('_', $msg_id, 3);
153: $guid = $parts[2];
154: switch($form['action_type']) {
155: case 'unread':
156: Hm_Feed_Uid_Cache::unread($guid);
157: break;
158: case 'read':
159: Hm_Feed_Uid_Cache::read($guid);
160: break;
161: }
162: }
163: }
164: }
165: }
166: }
167:
168: /**
169: * @subpackage feeds/handler
170: * @todo this is pretty ugly, try to break up
171: */
172: class Hm_Handler_feed_list_content extends Hm_Handler_Module {
173: public function process() {
174: list($success, $form) = $this->process_form(array('feed_server_ids'));
175: $dataSources = array_map(fn ($feed) => $feed['id'], Hm_Feed_List::dump());
176: if (!$success && empty($dataSources)) {
177: return;
178: }
179: $cache = false;
180: $terms = $this->request->get['search_terms'] ?? false;
181: $since = $this->request->get['search_since'] ?? DEFAULT_SEARCH_SINCE;
182: $fld = $this->request->get['search_fld'] ?? 'TEXT';
183: $ids = isset($form['feed_server_ids']) ? explode(',', $form['feed_server_ids']): $dataSources;
184: $res = array();
185: $unread_only = false;
186: $login_time = $this->session->get('login_time', false);
187: if ($login_time) {
188: $this->out('login_time', $login_time);
189: }
190: if ($this->get('list_path') == 'unread') {
191: $limit = $this->user_config->get('unread_per_source_setting', DEFAULT_UNREAD_PER_SOURCE);
192: $date = process_since_argument($this->user_config->get('unread_since_setting', DEFAULT_UNREAD_SINCE));
193: $unread_only = true;
194: $cutoff_timestamp = strtotime($date);
195: if ($login_time && $login_time > $cutoff_timestamp) {
196: $cutoff_timestamp = $login_time;
197: }
198: }
199: elseif ($this->get('list_path') == 'combined_inbox') {
200: $limit = $this->user_config->get('all_per_source_setting', DEFAULT_ALL_PER_SOURCE);
201: $date = process_since_argument($this->user_config->get('all_since_setting', DEFAULT_ALL_SINCE));
202: $cutoff_timestamp = strtotime($date);
203: }
204: else {
205: $limit = $this->user_config->get('feed_limit_setting', DEFAULT_FEED_LIMIT);
206: $date = process_since_argument($this->user_config->get('feed_since_setting', DEFAULT_FEED_SINCE));
207: $cutoff_timestamp = strtotime($date);
208: }
209: foreach($ids as $id) {
210: $feed_data = Hm_Feed_List::dump($id);
211: if ($feed_data) {
212: if (! $terms) {
213: $cache = feed_memcached_fetch($this, $feed_data);
214: }
215: $data = false;
216: if (is_array($cache) && count($cache) > 0) {
217: $data = $cache;
218: }
219: else {
220: $feed = is_news_feed($feed_data['server'], $limit);
221: if ($feed && $feed->parsed_data) {
222: $data = $feed->parsed_data;
223: $cache = false;
224: }
225: }
226: if (is_array($data)) {
227: foreach ($data as $item) {
228: if (array_key_exists('id', $item) && !array_key_exists('guid', $item)) {
229: $item['guid'] = $item['id'];
230: }
231: elseif (array_key_exists('link', $item) && !array_key_exists('guid', $item)) {
232: $item['guid'] = $item['link'];
233: }
234: if (array_key_exists('link_self', $item) || !array_key_exists('guid', $item)) {
235: continue;
236: }
237: if (!Hm_Feed_Uid_Cache::is_unread(md5($item['guid']))) {
238: if (isset($item['pubdate']) && strtotime($item['pubdate']) < $cutoff_timestamp) {
239: continue;
240: }
241: elseif (isset($item['dc:date']) && strtotime($item['dc:date']) < $cutoff_timestamp) {
242: continue;
243: }
244: if (isset($item['guid']) && $unread_only && Hm_Feed_Uid_Cache::is_read(md5($item['guid']))) {
245: continue;
246: }
247: }
248: if ($terms && !search_feed_item($item, $terms, $since, $fld)) {
249: continue;
250: }
251: else {
252: $item['server_id'] = $id;
253: $item['server_name'] = $feed_data['name'];
254: $res[] = $item;
255: }
256: }
257: }
258: }
259: }
260: if (! $cache && ! $terms) {
261: # TODO: fix potential warning about feed_data
262: feed_memcached_save($this, $feed_data, $res);
263: }
264:
265: usort($res, function($a, $b) {
266: if (isset($a['pubdate']) && isset($b['pubdate'])) {
267: return strtotime($b['pubdate']) - strtotime($a['pubdate']);
268: }
269: elseif (isset($a['dc:date']) && isset($b['dc:date'])) {
270: return strtotime($b['dc:date']) - strtotime($a['dc:date']);
271: }
272: return 0;
273: });
274:
275: $this->out('feed_list_data', $res);
276: if (isset($this->request->get['list_path'])) {
277: $this->out('feed_list_parent', $this->request->get['list_path']);
278: }
279: elseif (isset($this->request->get['page']) && $this->request->get['page'] == 'search') {
280: $this->out('feed_list_parent', 'search');
281: }
282: $this->out('feed_server_ids', $form['feed_server_ids']);
283: }
284: }
285:
286: /**
287: * @subpackage feeds/handler
288: */
289: class Hm_Handler_feed_item_content extends Hm_Handler_Module {
290: public function process() {
291: $content = '';
292: $headers = array();
293: list($success, $form) = $this->process_form(array('feed_uid', 'feed_list_path'));
294: if ($success) {
295: $path = explode('_', $form['feed_list_path']);
296: $id = $path[1];
297: $feed_items = array();
298: $feed_data = Hm_Feed_List::dump($id);
299: if ($feed_data) {
300: $cache = feed_memcached_fetch($this, $feed_data);
301: if (is_array($cache) && count($cache) > 0) {
302: $feed_items = $cache;
303: }
304: else {
305: $feed = is_news_feed($feed_data['server']);
306: if ($feed && $feed->parsed_data) {
307: $feed_items = $feed->parsed_data;
308: $cache = false;
309: }
310: }
311: }
312: $title = false;
313: foreach ($feed_items as $item) {
314: if (isset($item['id']) && !isset($item['guid'])) {
315: $item['guid'] = $item['id'];
316: unset($item['id']);
317: }
318: elseif (array_key_exists('link', $item) && !array_key_exists('guid', $item)) {
319: $item['guid'] = $item['link'];
320: }
321: elseif (isset($item['title']) && !isset($item['guid'])) {
322: $item['guid'] = md5($item['title']);
323: }
324: if (isset($item['guid']) && md5($item['guid']) == $form['feed_uid']) {
325: if (isset($item['description'])) {
326: $content = $item['description'];
327: unset($item['description']);
328: }
329: elseif (isset($item['content'])) {
330: $content = $item['content'];
331: unset($item['content']);
332: }
333: elseif (isset($item['summary'])) {
334: $content = $item['summary'];
335: unset($item['summary']);
336: }
337: if (array_key_exists('title', $item)) {
338: $title = $item['title'];
339: }
340: $headers = $item;
341: unset($headers['title']);
342: $headers = array_merge(array('title' => $title), $headers);
343: break;
344: }
345: }
346: if ($content) {
347: feed_memcached_save($this, $feed_data, $feed_items);
348: Hm_Feed_Uid_Cache::read($form['feed_uid']);
349: $this->out('feed_message_content', $content);
350: $this->out('feed_message_headers', $headers);
351: }
352: }
353: }
354: }
355:
356: /**
357: * @subpackage feeds/handler
358: */
359: class Hm_Handler_process_add_feed extends Hm_Handler_Module {
360: public function process() {
361: if (isset($this->request->post['submit_feed'])) {
362: $found = false;
363: list($success, $form) = $this->process_form(array('new_feed_name', 'new_feed_address'));
364: if ($success) {
365: if (feed_exists($form['new_feed_address'])) {
366: Hm_Msgs::add(sprintf('Feed url %s already exists', $form['new_feed_address']), 'warning');
367: } else {
368: $connection_test = address_from_url($form['new_feed_address']);
369: if ($con = @fsockopen($connection_test, 80, $errno, $errstr, 2)) {
370: $feed = is_news_feed($form['new_feed_address']);
371: if (!$feed) {
372: $feed = new Hm_Feed();
373: $homepage = $feed->get_feed_data($form['new_feed_address']);
374: if (trim($homepage)) {
375: list($type, $href) = search_for_feeds($homepage);
376: if ($type && $href) {
377: Hm_Msgs::add('Discovered a feed at that address');
378: $found = true;
379: }
380: else {
381: Hm_Msgs::add('Could not find an RSS or ATOM feed at that address', 'warning');
382: }
383: }
384: else {
385: Hm_Msgs::add('Could not find a feed at that address', 'warning');
386: }
387: }
388: else {
389: Hm_Msgs::add('Successfully connected to feed');
390: $found = true;
391: if (mb_stristr('<feed', $feed->xml_data)) {
392: $type = 'application/atom+xml';
393: }
394: else {
395: $type = 'application/rss+xml';
396: }
397: $href = $form['new_feed_address'];
398: }
399: }
400: else {
401: Hm_Msgs::add(sprintf('Could not add feed: %s', $errstr), 'danger');
402: }
403: }
404: }
405: else {
406: Hm_Msgs::add('Feed Name and Address are required', 'warning');
407: }
408: if ($found) {
409: $this->out('reload_folders', true);
410: Hm_Feed_List::add(array(
411: 'name' => $form['new_feed_name'],
412: 'server' => $href,
413: 'tls' => false,
414: 'port' => 80
415: ));
416: $this->session->record_unsaved('Feed added');
417: }
418: }
419: }
420: }
421:
422: /**
423: * @subpackage feeds/handler
424: */
425: class Hm_Handler_load_feeds_from_config extends Hm_Handler_Module {
426: public function process() {
427: Hm_Feed_List::init($this->user_config, $this->session);
428: Hm_Feed_Uid_Cache::load($this->cache->get('feed_read_uids', array(), true));
429: }
430: }
431:
432: /**
433: * @subpackage feeds/handler
434: */
435: class Hm_Handler_add_feeds_to_page_data extends Hm_Handler_Module {
436: public function process() {
437: $excluded = false;
438: if ($this->get('list_path') == 'unread') {
439: $excluded = $this->user_config->get('unread_exclude_feeds_setting', DEFAULT_UNREAD_EXCLUDE_FEEDS);
440: }
441: if ($excluded) {
442: return;
443: }
444: $feeds = Hm_Feed_List::dump();
445: if (!empty($feeds)) {
446: $this->out('feeds', $feeds);
447: }
448: }
449: }
450:
451: /**
452: * @subpackage feeds/handler
453: */
454: class Hm_Handler_load_feeds_for_search extends Hm_Handler_Module {
455: public function process() {
456: foreach (Hm_Feed_List::dump() as $index => $vals) {
457: $this->append('data_sources', array('feeds_search_page_content', 'type' => 'feeds', 'name' => $vals['name'], 'id' => $vals['id']));
458: }
459:
460: }
461: }
462:
463: /**
464: * @subpackage feeds/handler
465: */
466: class Hm_Handler_load_feeds_for_message_list extends Hm_Handler_Module {
467: public function process() {
468: $server_id = false;
469: if (array_key_exists('list_path', $this->request->get)) {
470: $path = $this->request->get['list_path'];
471: if (preg_match("/^feeds_(.+)$/", $path, $matches)) {
472: $server_id = $matches[1];
473: }
474: }
475:
476: foreach (Hm_Feed_List::dump() as $index => $vals) {
477: if ($server_id !== false && $index != $server_id) {
478: continue;
479: }
480: $this->append('data_sources', array('type' => 'feeds', 'name' => $vals['name'], 'id' => $vals['id']));
481: }
482: }
483: }
484:
485: /**
486: * @subpackage feeds/handler
487: */
488: class Hm_Handler_save_feeds extends Hm_Handler_Module {
489: public function process() {
490: Hm_Feed_List::save();
491: $this->cache->set('feed_read_uids', Hm_Feed_Uid_Cache::dump(), 0, true);
492: }
493: }
494:
495: /**
496: * @subpackage feeds/handler
497: */
498: class Hm_Handler_load_feed_folders extends Hm_Handler_Module {
499: public function process() {
500: $feeds = Hm_Feed_List::dump();
501: $folders = array();
502: if (!empty($feeds)) {
503: foreach ($feeds as $id => $feed) {
504: $folders[$id] = $feed['name'];
505: }
506: }
507: $this->out('feed_folders', $folders);
508: }
509: }
510:
511: /**
512: * @subpackage feeds/output
513: */
514: class Hm_Output_add_feed_dialog extends Hm_Output_Module {
515: protected function output() {
516: if ($this->format == 'HTML5') {
517: $count = count($this->get('feeds', array()));
518: $count = sprintf($this->trans('%d configured'), $count);
519:
520: return '<div class="feed_server_setup">
521: <div data-target=".feeds_section" class="server_section border-bottom cursor-pointer px-1 py-3 pe-auto">
522: <a href="#" class="pe-auto">
523: <i class="bi bi-rss-fill me-3"></i>
524: <b> '.$this->trans('Feeds').'</b>
525: </a>
526: <div class="server_count">'.$count.'</div>
527: </div>
528:
529: <div class="feeds_section px-4 pt-3 me-0">
530: <div class="row">
531: <div class="col-12 col-lg-4 mb-4">
532: <form class="add_server me-0" method="POST">
533: <input type="hidden" name="hm_page_key" value="'.$this->html_safe(Hm_Request_Key::generate()).'" />
534:
535: <div class="subtitle mt-4">'.$this->trans('Add an RSS/ATOM Feed').'</div>
536:
537: <div class="form-floating mb-3">
538: <input required type="text" id="new_feed_name" name="new_feed_name" class="txt_fld form-control" value="" placeholder="'.$this->trans('Feed name').'">
539: <label class="" for="new_feed_name">'.$this->trans('Feed name').'</label>
540: </div>
541:
542: <div class="form-floating mb-3">
543: <input required type="url" id="new_feed_address" name="new_feed_address" class="txt_fld form-control" placeholder="'.$this->trans('Site address or feed URL').'" value="">
544: <label for="new_feed_address" class="">'.$this->trans('Site address or feed URL').'</label>
545: </div>
546:
547: <input type="submit" class="btn btn-primary px-5" value="'.$this->trans('Add').'" name="submit_feed" />
548: </form>
549: </div></div>';
550: }
551: }
552: }
553:
554:
555: /**
556: * @subpackage feeds/output
557: */
558: class Hm_Output_display_configured_feeds extends Hm_Output_Module {
559: protected function output() {
560: $res = '';
561: if ($this->format == 'HTML5') {
562: foreach ($this->get('feeds', array()) as $index => $vals) {
563: $res .= '<div class="configured_server col-12 col-lg-4 mb-2"><div class="card card-body">';
564: $res .= sprintf('<div class="server_title"><b>%s</b></div><div title="%s" class="server_subtitle">%s</div>',
565: $this->html_safe($vals['name']), $this->html_safe($vals['server']), $this->html_safe($vals['server']));
566: $res .= '<form class="feed_connect d-flex gap-2" method="POST">';
567: $res .= '<input type="hidden" name="feed_id" value="'.$this->html_safe($index).'" />';
568: $res .= '<input type="submit" value="'.$this->trans('Test').'" class="test_feed_connect btn btn-primary btn-sm" />';
569: $res .= '<input type="submit" value="'.$this->trans('Delete').'" class="feed_delete btn btn-outline-danger btn-sm" />';
570: $res .= '<input type="hidden" value="ajax_feed_debug" name="hm_ajax_hook" />';
571: $res .= '</form></div></div>';
572: }
573: $res .= '<br class="clear_float" /></div></div>';
574: }
575: return $res;
576: }
577: }
578:
579: /**
580: * @subpackage feeds/output
581: */
582: class Hm_Output_feed_ids extends Hm_Output_Module {
583: protected function output() {
584: return '<input type="hidden" class="feed_server_ids" value="'.$this->html_safe(implode(',', array_keys($this->get('feeds', array())))).'" />';
585: }
586: }
587:
588: /**
589: * @subpackage feeds/output
590: */
591: class Hm_Output_filter_feed_item_content extends Hm_Output_Module {
592: protected function output() {
593: /* TODO: show "cannot find feed item if feed_message_headers is not present */
594: if ($this->get('feed_message_headers')) {
595: $header_str = '<div class="msg_headers d-flex flex-column border-bottom border-2 border-secondary-subtle pb-3 mb-3">';
596: foreach ($this->get('feed_message_headers', array()) as $name => $value) {
597: if (in_array($name, array('server_id', 'server_name', 'guid', 'id', 'content'), true)) {
598: continue;
599: }
600: if ($name != 'link' && $name != 'link_alternate' && !mb_strstr($value, ' ') && mb_strlen($value) > 75) {
601: $value = mb_substr($value, 0, 75).'...';
602: }
603: if ($name == 'title') {
604: $header_str .= '<div class="header_subject d-flex justify-content-center"><h4 class="text-center mb-0 fw-bold">'.$this->html_safe($value).'</h4></div>';
605: }
606: elseif ($name == 'link' || $name == 'link_alternate') {
607: $header_str .= '<div class="header_'.$name.' d-flex align-items-center py-1"><span class="fw-semibold me-2 text-nowrap">'.$this->trans($name).':</span><a class="text-break" data-external="true" href="'.$this->html_safe($value).'">'.$this->html_safe($value).'</a></div>';
608: }
609: elseif ($name == 'author' || $name == 'dc:creator' || $name == 'name') {
610: $header_str .= '<div class="header_from d-flex align-items-center py-1"><span class="fw-semibold me-2 text-nowrap">'.$this->trans($name).':</span><span class="text-break">'.$this->html_safe($value).'</span></div>';
611: }
612: elseif ($name == 'pubdate' || $name == 'dc:date') {
613: $header_str .= '<div class="header_date d-flex align-items-center py-1"><span class="fw-semibold me-2 text-nowrap">'.$this->trans($name).':</span><span class="text-break">'.$this->html_safe($value).'</span></div>';
614: }
615: else {
616: $header_str .= '<div class="d-flex align-items-center py-1"><span class="fw-semibold me-2 text-nowrap">'.$this->trans($name).':</span><span class="text-break">'.$this->html_safe($value).'</span></div>';
617: }
618: }
619: $header_str .= '</div>';
620: $this->out('feed_message_content', str_replace(array('<', '>', '&ldquo;'), array(' <', '> ', ' &ldquo;'), $this->get('feed_message_content')));
621: $txt = '<div class="msg_text_inner">'.format_msg_html($this->get('feed_message_content')).'</div>';
622: $this->out('feed_msg_text', $txt);
623: $this->out('feed_msg_headers', $header_str);
624: }
625: }
626: }
627:
628: /**
629: * @subpackage feeds/output
630: */
631: class Hm_Output_filter_feed_list_data extends Hm_Output_Module {
632: public static function formatMessageList($mod) {
633: $res = array();
634: $login_time = false;
635: if ($mod->get('login_time')) {
636: $login_time = $mod->get('login_time');
637: }
638: if ($mod->get('feed_list_parent') == 'feeds' ||
639: $mod->get('feed_list_parent') == 'search' ||
640: $mod->get('feed_list_parent') == 'combined_inbox') {
641: $src_callback = 'feed_source_callback';
642: }
643: else {
644: $src_callback = 'safe_output_callback';
645: }
646: $show_icons = $mod->get('msg_list_icons');
647:
648: foreach ($mod->get('feed_list_data', array()) as $item) {
649: $row_style = 'feeds';
650: if (isset($item['id']) && !isset($item['guid'])) {
651: $item['guid'] = $item['id'];
652: unset($item['id']);
653: }
654: elseif (isset($item['title']) && !isset($item['guid'])) {
655: $item['guid'] = md5($item['title']);
656: }
657: if (isset($item['guid'])) {
658: if (!array_key_exists('title', $item) || !trim($item['title'])) {
659: $item['title'] = $mod->trans('[No Subject]');
660: }
661: $icon = 'rss';
662: $id = sprintf("feeds_%s_%s", $item['server_id'], md5($item['guid']));
663: if (isset($item['dc:date'])) {
664: $date = display_value('dc:date', $item, 'date');
665: $timestamp = display_value('dc:date', $item, 'time');
666: }
667: elseif (isset($item['pubdate'])) {
668: $date = display_value('pubdate', $item, 'date');
669: $timestamp = display_value('pubdate', $item, 'time');
670: }
671: else {
672: $date = '';
673: $timestamp = 0;
674: }
675: if ($date) {
676: $date = translate_time_str($date, $mod);
677: }
678: $url = '?page=message&uid='.urlencode(md5($item['guid'])).'&list_path=feeds_'.$item['server_id'];
679: if ($mod->in('feed_list_parent', array('combined_inbox', 'unread', 'feeds', 'search'))) {
680: $url .= '&list_parent='.$mod->html_safe($mod->get('feed_list_parent', ''));
681: }
682: else {
683: $url .= '&list_parent=feeds_'.$item['server_id'];
684: }
685: if ($mod->get('news_list_style')) {
686: $style = 'news';
687: }
688: else {
689: $style = 'email';
690: }
691: if ($mod->get('is_mobile')) {
692: $style = 'news';
693: }
694: if (Hm_Feed_Uid_Cache::is_read(md5($item['guid']))) {
695: $flags = array();
696: }
697: elseif (Hm_Feed_Uid_Cache::is_unread(md5($item['guid']))) {
698: $icon = 'rss_alt';
699: $flags = array('unseen');
700: $row_style .= ' unseen';
701: }
702: elseif ($timestamp && $login_time && $timestamp <= $login_time) {
703: $flags = array();
704: }
705: else {
706: $icon = 'rss_alt';
707: $flags = array('unseen');
708: if (mb_strpos($row_style, 'unseen') === false) {
709: $row_style .= ' unseen';
710: }
711: }
712: $nofrom = '';
713: if (isset($item['author'])) {
714: $from = display_value('author', $item, 'from');
715: }
716: elseif (isset($item['name'])) {
717: $from = display_value('name', $item, 'from');
718: }
719: elseif (isset($item['dc:creator'])) {
720: $from = display_value('dc:creator', $item, 'from');
721: }
722: elseif ($style == 'email') {
723: $from = $mod->trans('[No From]');
724: $nofrom = ' nofrom';
725: }
726: else {
727: $from = '';
728: }
729: if (!$show_icons) {
730: $icon = false;
731: }
732: $sorting_date = $item['dc:date'] ?? $item['pubdate'] ?? '';
733: $row_style .= ' '.str_replace(' ', '_', $item['server_name']);
734: if ($style == 'news') {
735: $res[$id] = message_list_row(array(
736: array('checkbox_callback', $id),
737: array('icon_callback', $flags),
738: array('subject_callback', strip_tags($item['title']), $url, $flags, $icon),
739: array('safe_output_callback', 'source', $item['server_name']),
740: array('safe_output_callback', 'from'.$nofrom, $from),
741: array('date_callback', $date, $timestamp),
742: array('dates_holders_callback', $sorting_date, $sorting_date),
743: ),
744: $id,
745: $style,
746: $mod,
747: $row_style
748: );
749: }
750: else {
751: $res[$id] = message_list_row(array(
752: array('checkbox_callback', $id),
753: array($src_callback, 'source', $item['server_name'], $icon, $item['server_id']),
754: array('safe_output_callback', 'from'.$nofrom, $from),
755: array('subject_callback', strip_tags($item['title']), $url, $flags),
756: array('date_callback', $date, $timestamp),
757: array('icon_callback', $flags),
758: array('dates_holders_callback', $sorting_date, $sorting_date),
759: ),
760: $id,
761: $style,
762: $mod,
763: $row_style
764: );
765: }
766: }
767: }
768:
769: return $res;
770: }
771:
772: protected function output() {
773: $this->out('formatted_message_list', self::formatMessageList($this));
774: }
775: }
776:
777: /**
778: * @subpackage feeds/output
779: */
780: class Hm_Output_filter_feed_folders extends Hm_Output_Module {
781: protected function output() {
782: $res = '';
783: $folders = $this->get('feed_folders', array());
784: if (is_array($folders) && !empty($folders)) {
785: if(count($this->get('feeds', array())) > 1) {
786: $res .= '<li class="menu_feeds"><a class="unread_link" href="?page=message_list&amp;list_path=feeds">';
787: if (!$this->get('hide_folder_icons')) {
788: $res .= '<i class="bi bi-rss-fill menu-icon"></i>';
789: }
790: $res .= $this->trans('All');
791: $res .= '</a> <span class="unread_feed_count"></span></li>';
792: }
793: foreach ($this->get('feed_folders') as $id => $folder) {
794: $res .= '<li class="feeds_'.$this->html_safe($id).'">'.
795: '<a data-id="feeds_'.$this->html_safe($id).'" href="?page=message_list&list_path=feeds_'.$this->html_safe($id).'">';
796: if (!$this->get('hide_folder_icons')) {
797: $res .= '<i class="bi bi-rss menu-icon"></i>';
798: }
799: $res .= $this->html_safe($folder).'</a></li>';
800: }
801: }
802: $res .= '<li class="feeds_add_new"><a href="?page=servers#feeds_section">';
803: if (!$this->get('hide_folder_icons')) {
804: $res .= '<i class="bi bi-plus-square menu-icon"></i>';
805: }
806: $res .= $this->trans('Add a feed').'</a></li>';
807: if ($res) {
808: $this->append('folder_sources', array('feeds_folders', $res));
809: }
810: return '';
811: }
812: }
813:
814: /**
815: * @subpackage feeds/output
816: */
817: class Hm_Output_display_feeds_status extends Hm_Output_Module {
818: protected function output() {
819: $res = '';
820: foreach ($this->get('feeds', array()) as $index => $vals) {
821: $res .= '<tr><td>'.$this->trans('FEED').'</td><td>'.$this->html_safe($vals['name']).'</td><td class="feeds_status_'.$index.'"></td>'.
822: '<td class="feeds_detail_'.$index.'"></td></tr>';
823: }
824: return $res;
825: }
826: }
827:
828: /**
829: * @subpackage feeds/output
830: */
831: class Hm_Output_unread_feeds_included extends Hm_Output_Module {
832: protected function output() {
833: $settings = $this->get('user_settings');
834: if (array_key_exists('unread_exclude_feeds', $settings) && $settings['unread_exclude_feeds']) {
835: $checked = ' checked="checked"';
836: $reset = '<span class="tooltip_restore" restore_aria_label="Restore default value"><i class="bi bi-arrow-counterclockwise refresh_list reset_default_value_checkbox"></i></span>';
837: }
838: else {
839: $checked = '';
840: $reset = '';
841: }
842: return '<tr class="unread_setting"><td><label class="form-check-label" for="unread_exclude_feeds">'.$this->trans('Exclude unread feed items').'</label></td>'.
843: '<td><input class="form-check-input" type="checkbox" '.$checked.' value="1" id="unread_exclude_feeds" name="unread_exclude_feeds" />'.$reset.'</td></tr>';
844: }
845: }
846:
847: /**
848: * @subpackage feeds/output
849: */
850: class Hm_Output_filter_feed_status_data extends Hm_Output_Module {
851: protected function output() {
852: if ($this->get('feed_connect_status') == 'Connected') {
853: $this->out('feed_status_display', '<span class="online">'.
854: $this->trans(ucwords($this->get('feed_connect_status'))).'</span> in '.round($this->get('feed_connect_time'), 3));
855: }
856: else {
857: $this->out('feed_status_display', '<span class="down">'.$this->trans('Down').'</span>');
858: }
859: }
860: }
861:
862: /**
863: * @subpackage feeds/output
864: */
865: class Hm_Output_start_feed_settings extends Hm_Output_Module {
866: protected function output() {
867: return '<tr><td colspan="2" data-target=".feeds_setting" class="settings_subtitle cursor-pointer border-bottom p-2">'.
868: '<i class="bi bi-rss-fill menu-icon"></i>'.$this->trans('Feed Settings').'</td></tr>';
869: }
870: }
871:
872: /**
873: * @subpackage feeds/output
874: */
875: class Hm_Output_feed_since_setting extends Hm_Output_Module {
876: protected function output() {
877: $since = DEFAULT_FEED_SINCE;
878: $settings = $this->get('user_settings');
879: if (array_key_exists('feed_since', $settings)) {
880: $since = $settings['feed_since'];
881: }
882: return '<tr class="feeds_setting"><td><label for="feed_since">'.$this->trans('Show feed items received since').'</label></td>'.
883: '<td>'.message_since_dropdown($since, 'feed_since', $this, DEFAULT_FEED_SINCE).'</td></tr>';
884: }
885: }
886:
887: /**
888: * @subpackage feeds/output
889: */
890: class Hm_Output_feed_limit_setting extends Hm_Output_Module {
891: protected function output() {
892: $limit = DEFAULT_FEED_LIMIT;
893: $settings = $this->get('user_settings');
894: $reset = '';
895: if (array_key_exists('feed_limit', $settings)) {
896: $limit = $settings['feed_limit'];
897: }
898: if ($limit != DEFAULT_FEED_LIMIT) {
899: $reset = '<span class="tooltip_restore" restore_aria_label="Restore default value"><i class="bi bi-arrow-counterclockwise refresh_list reset_default_value_input"></i></span>';
900: }
901: return '<tr class="feeds_setting"><td><label for="feed_limit">'.$this->trans('Max feed items to display').'</label></td>'.
902: '<td class="d-flex"><input class="form-control form-control-sm w-auto" type="text" id="feed_limit" name="feed_limit" size="2" value="'.$this->html_safe($limit).'" data-default-value="'.DEFAULT_FEED_LIMIT.'" />'.$reset.'</td></tr>';
903: }
904: }
905:
906: /**
907: * @subpackage feeds/functions
908: */
909: if (!hm_exists('feed_source_callback')) {
910: function feed_source_callback($vals, $style, $output_mod) {
911: if ($vals[2]) {
912: $img = '<i class="bi bi-rss-fill"></i>';
913: }
914: else {
915: $img = '';
916: }
917: if ($style == 'email') {
918: return sprintf('<td class="%s" title="%s"><a href="?page=message_list&list_path=feeds_%s">%s%s</td>',
919: $output_mod->html_safe($vals[0]), $output_mod->html_safe($vals[1]), $output_mod->html_safe($vals[3]),
920: $img, $output_mod->html_safe($vals[1]));
921: }
922: elseif ($style == 'news') {
923: return sprintf('<div class="%s" title="%s"><a href="?page=message_list&list_path=feeds_%s">%s%s</div>',
924: $output_mod->html_safe($vals[0]), $output_mod->html_safe($vals[1]), $output_mod->html_safe($vals[3]),
925: $img, $output_mod->html_safe($vals[1]));
926: }
927: }}
928:
929: /**
930: * @subpackage feeds/functions
931: */
932: if (!hm_exists('address_from_url')) {
933: function address_from_url($str) {
934: $res = $str;
935: $url_bits = parse_url($str);
936: if (isset($url_bits['scheme']) && isset($url_bits['host'])) {
937: $res = $url_bits['host'];
938: }
939: return $res;
940: }}
941:
942: /**
943: * @subpackage feeds/functions
944: */
945: if (!hm_exists('is_news_feed')) {
946: function is_news_feed($url, $limit=20) {
947: $feed = new Hm_Feed();
948: $feed->limit = $limit;
949: $feed->parse_feed($url);
950: $feed_data = array_filter($feed->parsed_data);
951: if (empty($feed_data)) {
952: return false;
953: }
954: else {
955: return $feed;
956: }
957: }}
958:
959: /**
960: * @subpackage feeds/functions
961: */
962: if (!hm_exists('search_for_feeds')) {
963: function search_for_feeds($html) {
964: $type = false;
965: $href = false;
966: if (preg_match_all("/<link.+>/U", $html, $matches)) {
967: foreach ($matches[0] as $link_tag) {
968: if (mb_stristr($link_tag, 'alternate')) {
969: if (preg_match("/type=(\"|'|)(.+)(\"|'|\>| )/U", $link_tag, $types)) {
970: $type = trim($types[2]);
971: }
972: if (preg_match("/href=(\"|'|)(.+)(\"|'|\>| )/U", $link_tag, $hrefs)) {
973: $href = trim($hrefs[2]);
974: }
975: }
976: }
977: }
978: return array($type, $href);
979: }}
980:
981: /**
982: * @subpackage feeds/functions
983: */
984: if (!hm_exists('search_feed_item')) {
985: function search_feed_item($item, $terms, $since, $fld) {
986: if (array_key_exists('pubdate', $item)) {
987: if (strtotime($item['pubdate']) < strtotime($since)) {
988: return false;
989: }
990: }
991: if (array_key_exists('dc:date', $item)) {
992: if (strtotime($item['dc:date']) < strtotime($since)) {
993: return false;
994: }
995: }
996: switch ($fld) {
997: case 'BODY':
998: $flds = array('description');
999: break;
1000: case 'FROM':
1001: $flds = array('dc:creator');
1002: break;
1003: case 'SUBJECT':
1004: $flds = array('title');
1005: break;
1006: case 'TEXT':
1007: default:
1008: $flds = array('description', 'title', 'dc:creator', 'guid');
1009: break;
1010: }
1011: foreach ($flds as $fld) {
1012: if (array_key_exists($fld, $item)) {
1013: if (mb_stristr($item[$fld], $terms) !== false) {
1014: return true;
1015: }
1016: }
1017: }
1018: return false;
1019: }}
1020:
1021: /**
1022: * @subpackage feeds/functions
1023: */
1024: if (!hm_exists('feed_memcached_save')) {
1025: function feed_memcached_save($hmod, $feed_data, $data) {
1026: $key = sprintf('%s%s%s', $feed_data['server'], $feed_data['tls'], $feed_data['port']);
1027: $hmod->cache->set($key, $data, 500);
1028: }}
1029:
1030: /**
1031: * @subpackage feeds/functions
1032: */
1033: if (!hm_exists('feed_memcached_fetch')) {
1034: function feed_memcached_fetch($hmod, $feed_data) {
1035: $key = sprintf('%s%s%s', $feed_data['server'], $feed_data['tls'], $feed_data['port']);
1036: return $hmod->cache->get($key);
1037: }}
1038:
1039: /**
1040: * @subpackage feeds/functions
1041: */
1042: if (!hm_exists('feed_exists')) {
1043: function feed_exists($server) {
1044: $list = Hm_Feed_List::dump();
1045: foreach ($list as $feed) {
1046: if ($feed['server'] == $server) {
1047: return true;
1048: }
1049: }
1050: return false;
1051: }}
1052: