Passed
Pull Request — master (#4)
by
unknown
04:42
created

Feeds::format_headlines_list()   F

Complexity

Conditions 62

Size

Total Lines 345
Code Lines 208

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 62
eloc 208
c 1
b 0
f 0
nop 11
dl 0
loc 345
rs 3.3333

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
require_once "colors.php";
3
4
class Feeds extends Handler_Protected {
5
	const NEVER_GROUP_FEEDS = [ -6, 0];
6
	const NEVER_GROUP_BY_DATE = [ -2, -1, -3];
7
8
	private $params;
9
10
	function csrf_ignore($method) {
11
		$csrf_ignored = array("index", "quickaddfeed", "search");
12
13
		return array_search($method, $csrf_ignored) !== false;
14
	}
15
16
	private function format_headline_subtoolbar($feed_site_url, $feed_title,
17
			$feed_id, $is_cat, $search,
18
			$error, $feed_last_updated) {
19
20
		if ($is_cat) {
21
			$cat_q = "&is_cat=$is_cat";
22
		}
23
24
		if ($search) {
25
			$search_q = "&q=$search";
26
		} else {
27
			$search_q = "";
28
		}
29
30
		$reply = "";
31
32
		$rss_link = htmlspecialchars(get_self_url_prefix().
33
			"/public.php?op=rss&id=$feed_id$cat_q$search_q");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cat_q does not seem to be defined for all execution paths leading up to this point.
Loading history...
34
35
		$reply .= "<span class='left'>";
36
37
		$reply .= "<a href=\"#\"
38
				title=\"".__("Show as feed")."\"
39
				onclick=\"App.displayDlg('".__("Show as feed")."','generatedFeed', '$feed_id:$is_cat:$rss_link')\">
40
				<i class='icon-syndicate material-icons'>rss_feed</i></a>";
41
42
		$reply .= "<span id='feed_title'>";
43
44
		if ($feed_site_url) {
45
			$last_updated = T_sprintf("Last updated: %s", $feed_last_updated);
46
47
			$reply .= "<a title=\"$last_updated\" target='_blank' href=\"$feed_site_url\">".
48
				truncate_string(strip_tags($feed_title), 30)."</a>";
49
		} else {
50
			$reply .= strip_tags($feed_title);
51
		}
52
53
		if ($error)
54
			$reply .= " <i title=\"".htmlspecialchars($error)."\" class='material-icons icon-error'>error</i>";
55
56
		$reply .= "</span>";
57
		$reply .= "<span id='feed_current_unread' style='display: none'></span>";
58
		$reply .= "</span>";
59
60
		$reply .= "<span class=\"right\">";
61
		$reply .= "<span id='selected_prompt'></span>";
62
		$reply .= "&nbsp;";
63
		$reply .= "<select dojoType=\"fox.form.Select\"
64
			onchange=\"Headlines.onActionChanged(this)\">";
65
66
		$reply .= "<option value=\"0\" disabled='1'>".__('Select...')."</option>";
67
68
		$reply .= "<option value=\"Headlines.select('all')\">".__('All')."</option>";
69
		$reply .= "<option value=\"Headlines.select('unread')\">".__('Unread')."</option>";
70
		$reply .= "<option value=\"Headlines.select('invert')\">".__('Invert')."</option>";
71
		$reply .= "<option value=\"Headlines.select('none')\">".__('None')."</option>";
72
73
		$reply .= "<option value=\"0\" disabled=\"1\">".__('Selection toggle:')."</option>";
74
75
		$reply .= "<option value=\"Headlines.selectionToggleUnread()\">".__('Unread')."</option>
76
			<option value=\"Headlines.selectionToggleMarked()\">".__('Starred')."</option>
77
			<option value=\"Headlines.selectionTogglePublished()\">".__('Published')."</option>";
78
79
		$reply .= "<option value=\"0\" disabled=\"1\">".__('Selection:')."</option>";
80
81
		$reply .= "<option value=\"Headlines.catchupSelection()\">".__('Mark as read')."</option>";
82
		$reply .= "<option value=\"Article.selectionSetScore()\">".__('Set score')."</option>";
83
84
		if ($feed_id == 0 && !$is_cat) {
85
			$reply .= "<option value=\"Headlines.archiveSelection()\">".__('Move back')."</option>";
86
			$reply .= "<option value=\"Headlines.deleteSelection()\">".__('Delete')."</option>";
87
		} else {
88
			$reply .= "<option value=\"Headlines.archiveSelection()\">".__('Archive')."</option>";
89
		}
90
91
		if (PluginHost::getInstance()->get_plugin("mail")) {
92
			$reply .= "<option value=\"Plugins.Mail.send()\">".__('Forward by email').
93
				"</option>";
94
		}
95
96
		if (PluginHost::getInstance()->get_plugin("mailto")) {
97
			$reply .= "<option value=\"Plugins.Mailto.send()\">".__('Forward by email').
98
				"</option>";
99
		}
100
101
		$reply .= "<option value=\"0\" disabled=\"1\">".__('Feed:')."</option>";
102
103
		//$reply .= "<option value=\"catchupPage()\">".__('Mark as read')."</option>";
104
105
		$reply .= "<option value=\"App.displayDlg('".__("Show as feed")."','generatedFeed', '$feed_id:$is_cat:$rss_link')\">".
106
			__('Show as feed')."</option>";
107
108
		$reply .= "</select>";
109
110
		//$reply .= "</h2";
111
112
		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON) as $p) {
113
			 $reply .= $p->hook_headline_toolbar_button($feed_id, $is_cat);
114
		}
115
116
		$reply .= "</span>";
117
118
		return $reply;
119
	}
120
121
	private function format_headlines_list($feed, $method, $view_mode, $limit, $cat_view,
122
					$offset, $override_order = false, $include_children = false, $check_first_id = false,
123
					$skip_first_id_check = false, $order_by = false) {
124
125
		$disable_cache = false;
126
127
		$reply = array();
128
129
		$rgba_cache = array();
130
		$topmost_article_ids = array();
131
132
		if (!$offset) {
133
			$offset = 0;
134
		}
135
		if ($method == "undefined") {
136
			$method = "";
137
		}
138
139
		$method_split = explode(":", $method);
140
141
		if ($method == "ForceUpdate" && $feed > 0 && is_numeric($feed)) {
142
			$sth = $this->pdo->prepare("UPDATE ttrss_feeds
143
                            SET last_updated = '1970-01-01', last_update_started = '1970-01-01'
144
                            WHERE id = ?");
145
			$sth->execute([$feed]);
146
		}
147
148
		if ($method_split[0] == "MarkAllReadGR") {
149
			$this->catchup_feed($method_split[1], false);
150
		}
151
152
		// FIXME: might break tag display?
153
154
		if (is_numeric($feed) && $feed > 0 && !$cat_view) {
155
			$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? LIMIT 1");
156
			$sth->execute([$feed]);
157
158
			if (!$sth->fetch()) {
159
				$reply['content'] = "<div align='center'>".__('Feed not found.')."</div>";
160
			}
161
		}
162
163
		@$search = $_REQUEST["query"];
164
		@$search_language = $_REQUEST["search_language"]; // PGSQL only
165
166
		if ($search) {
167
			$disable_cache = true;
168
		}
169
170
		if (!$cat_view && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) {
171
			$handler = PluginHost::getInstance()->get_feed_handler(
172
				PluginHost::feed_to_pfeed_id($feed));
173
174
			if ($handler) {
175
				$options = array(
176
					"limit" => $limit,
177
					"view_mode" => $view_mode,
178
					"cat_view" => $cat_view,
179
					"search" => $search,
180
					"override_order" => $override_order,
181
					"offset" => $offset,
182
					"owner_uid" => $_SESSION["uid"],
183
					"filter" => false,
184
					"since_id" => 0,
185
					"include_children" => $include_children,
186
					"order_by" => $order_by);
187
188
				$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed),
189
					$options);
190
			}
191
192
		} else {
193
194
			$params = array(
195
				"feed" => $feed,
196
				"limit" => $limit,
197
				"view_mode" => $view_mode,
198
				"cat_view" => $cat_view,
199
				"search" => $search,
200
				"search_language" => $search_language,
201
				"override_order" => $override_order,
202
				"offset" => $offset,
203
				"include_children" => $include_children,
204
				"check_first_id" => $check_first_id,
205
				"skip_first_id_check" => $skip_first_id_check,
206
				"order_by" => $order_by
207
			);
208
209
			$qfh_ret = $this->queryFeedHeadlines($params);
210
		}
211
212
		$vfeed_group_enabled = get_pref("VFEED_GROUP_BY_FEED") &&
213
			!(in_array($feed, Feeds::NEVER_GROUP_FEEDS) && !$cat_view);
214
215
		$result = $qfh_ret[0]; // this could be either a PDO query result or a -1 if first id changed
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $qfh_ret does not seem to be defined for all execution paths leading up to this point.
Loading history...
216
		$feed_title = $qfh_ret[1];
217
		$feed_site_url = $qfh_ret[2];
218
		$last_error = $qfh_ret[3];
219
		$last_updated = strpos($qfh_ret[4], '1970-') === FALSE ?
220
			make_local_datetime($qfh_ret[4], false) : __("Never");
221
		$highlight_words = $qfh_ret[5];
222
		$reply['first_id'] = $qfh_ret[6];
223
		$reply['is_vfeed'] = $qfh_ret[7];
224
		$query_error_override = $qfh_ret[8];
225
226
		$reply['search_query'] = [$search, $search_language];
227
		$reply['vfeed_group_enabled'] = $vfeed_group_enabled;
228
229
		$reply['toolbar'] = $this->format_headline_subtoolbar($feed_site_url,
230
			$feed_title,
231
			$feed, $cat_view, $search,
232
			$last_error, $last_updated);
233
234
		if ($offset == 0) {
235
			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HEADLINES_BEFORE) as $p) {
236
				 $reply['content'] .= $p->hook_headlines_before($feed, $cat_view, $qfh_ret);
237
			}
238
		}
239
240
		$reply['content'] = [];
241
242
		$headlines_count = 0;
243
244
		if (is_object($result)) {
245
			while ($line = $result->fetch(PDO::FETCH_ASSOC)) {
246
247
				++$headlines_count;
248
249
				if (!get_pref('SHOW_CONTENT_PREVIEW')) {
250
					$line["content_preview"] = "";
251
				} else {
252
					$line["content_preview"] = "&mdash; ".truncate_string(strip_tags($line["content"]), 250);
253
254
					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
255
						$line = $p->hook_query_headlines($line, 250, false);
256
					}
257
				}
258
259
				$id = $line["id"];
260
261
				// frontend doesn't expect pdo returning booleans as strings on mysql
262
				if (DB_TYPE == "mysql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
263
					foreach (["unread", "marked", "published"] as $k) {
264
						$line[$k] = $line[$k] === "1";
265
					}
266
				}
267
268
				// normalize archived feed
269
				if ($line['feed_id'] === null) {
270
					$line['feed_id'] = 0;
271
					$line["feed_title"] = __("Archived articles");
272
				}
273
274
				$feed_id = $line["feed_id"];
275
276
				$label_cache = $line["label_cache"];
277
				$labels = false;
278
279
				if ($label_cache) {
280
					$label_cache = json_decode($label_cache, true);
281
282
					if ($label_cache) {
283
						if ($label_cache["no-labels"] == 1) {
284
													$labels = array();
285
						} else {
286
													$labels = $label_cache;
287
						}
288
					}
289
				}
290
291
				if (!is_array($labels)) {
292
					$labels = Article::get_article_labels($id);
293
				}
294
295
				$labels_str = "<span class=\"HLLCTR-$id\">";
296
				$labels_str .= Article::format_article_labels($labels);
297
				$labels_str .= "</span>";
298
299
				$line["labels"] = $labels_str;
300
301
				if (count($topmost_article_ids) < 3) {
302
					array_push($topmost_article_ids, $id);
303
				}
304
305
				if (!$line["feed_title"]) {
306
					$line["feed_title"] = "";
307
				}
308
309
				$line["buttons_left"] = "";
310
				foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
311
					$line["buttons_left"] .= $p->hook_article_left_button($line);
312
				}
313
314
				$line["buttons"] = "";
315
				foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
316
					$line["buttons"] .= $p->hook_article_button($line);
317
				}
318
319
				$line["content"] = sanitize($line["content"],
320
					$line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
321
322
				foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
323
					$line = $p->hook_render_article_cdm($line);
324
				}
325
326
				$line['content'] = DiskCache::rewriteUrls($line['content']);
327
328
				if ($line['note'])
329
					$line['note'] = Article::format_article_note($id, $line['note']);
330
				else
331
					$line['note'] = "";
332
333
				if (!get_pref("CDM_EXPANDED")) {
334
					$line["cdm_excerpt"] = "<span class='collapse'>
335
                        <i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
336
                            title=\"" . __("Collapse article") . "\">remove_circle</i></span>";
337
338
					if (get_pref('SHOW_CONTENT_PREVIEW')) {
339
						$line["cdm_excerpt"] .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
340
					}
341
				}
342
343
				$line["enclosures"] = Article::format_article_enclosures($id, $line["always_display_enclosures"],
344
					$line["content"], $line["hide_images"]);
345
346
				if ($line["orig_feed_id"]) {
347
348
					$ofgh = $this->pdo->prepare("SELECT * FROM ttrss_archived_feeds
349
                    WHERE id = ? AND owner_uid = ?");
350
					$ofgh->execute([$line["orig_feed_id"], $_SESSION['uid']]);
351
352
					if ($tmp_line = $ofgh->fetch()) {
353
						$line["orig_feed"] = [ $tmp_line["title"], $tmp_line["site_url"], $tmp_line["feed_url"] ];
354
					}
355
				}
356
357
				$line["updated_long"] = make_local_datetime($line["updated"],true);
358
				$line["updated"] = make_local_datetime($line["updated"], false, false, false, true);
359
360
361
				$line['imported'] = T_sprintf("Imported at %s",
362
					make_local_datetime($line["date_entered"], false));
363
364
				if ($line["tag_cache"]) {
365
									$tags = explode(",", $line["tag_cache"]);
366
				} else {
367
									$tags = false;
368
				}
369
370
				$line["tags_str"] = Article::format_tags_string($tags);
371
372
				if (feeds::feedHasIcon($feed_id)) {
373
					$line['feed_icon'] = "<img class=\"icon\" src=\"".ICONS_URL."/$feed_id.ico\" alt=\"\">";
0 ignored issues
show
Bug introduced by
The constant ICONS_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
374
				} else {
375
					$line['feed_icon'] = "<i class='icon-no-feed material-icons'>rss_feed</i>";
376
				}
377
378
				//setting feed headline background color, needs to change text color based on dark/light
379
				$fav_color = $line['favicon_avg_color'];
380
381
				require_once "colors.php";
382
383
				if (!isset($rgba_cache[$feed_id])) {
384
					if ($fav_color && $fav_color != 'fail') {
385
						$rgba_cache[$feed_id] = _color_unpack($fav_color);
386
					} else {
387
						$rgba_cache[$feed_id] = _color_unpack($this->color_of($line['feed_title']));
388
					}
389
				}
390
391
				if (isset($rgba_cache[$feed_id])) {
392
					$line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)';
393
				}
394
395
				/* we don't need those */
396
397
				foreach (["date_entered", "guid", "last_published", "last_marked", "tag_cache", "favicon_avg_color",
398
							 "uuid", "label_cache", "yyiw"] as $k)
399
					unset($line[$k]);
400
401
				array_push($reply['content'], $line);
402
			}
403
		}
404
405
		if (!$headlines_count) {
406
407
			if (is_object($result)) {
408
409
				if ($query_error_override) {
410
					$message = $query_error_override;
411
				} else {
412
					switch ($view_mode) {
413
					case "unread":
414
						$message = __("No unread articles found to display.");
415
						break;
416
					case "updated":
417
						$message = __("No updated articles found to display.");
418
						break;
419
					case "marked":
420
						$message = __("No starred articles found to display.");
421
						break;
422
					default:
423
						if ($feed < LABEL_BASE_INDEX) {
424
							$message = __("No articles found to display. You can assign articles to labels manually from article header context menu (applies to all selected articles) or use a filter.");
425
						} else {
426
							$message = __("No articles found to display.");
427
						}
428
					}
429
				}
430
431
				if (!$offset && $message) {
432
					$reply['content'] = "<div class='whiteBox'>$message";
433
434
					$reply['content'] .= "<p><span class=\"text-muted\">";
435
436
					$sth = $this->pdo->prepare("SELECT ".SUBSTRING_FOR_DATE."(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds
437
                        WHERE owner_uid = ?");
438
					$sth->execute([$_SESSION['uid']]);
439
					$row = $sth->fetch();
440
441
					$last_updated = make_local_datetime($row["last_updated"], false);
442
443
					$reply['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated);
444
445
					$sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors
446
                        FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
447
					$sth->execute([$_SESSION['uid']]);
448
					$row = $sth->fetch();
449
450
					$num_errors = $row["num_errors"];
451
452
					if ($num_errors > 0) {
453
						$reply['content'] .= "<br/>";
454
						$reply['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">".
455
							__('Some feeds have update errors (click for details)')."</a>";
456
					}
457
					$reply['content'] .= "</span></p></div>";
458
459
				}
460
			} else if (is_numeric($result) && $result == -1) {
461
				$reply['first_id_changed'] = true;
462
			}
463
		}
464
465
		return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply);
466
	}
467
468
	public function catchupAll() {
469
		$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
470
						last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?");
471
		$sth->execute([$_SESSION['uid']]);
472
473
		CCache::zero_all($_SESSION["uid"]);
474
	}
475
476
	public function view() {
477
		$reply = array();
478
479
		$feed = $_REQUEST["feed"];
480
		$method = $_REQUEST["m"];
481
		$view_mode = $_REQUEST["view_mode"];
482
		$limit = 30;
483
		@$cat_view = $_REQUEST["cat"] == "true";
484
		@$next_unread_feed = $_REQUEST["nuf"];
485
		@$offset = $_REQUEST["skip"];
486
		$order_by = $_REQUEST["order_by"];
487
		$check_first_id = $_REQUEST["fid"];
488
489
		if (is_numeric($feed)) {
490
			$feed = (int) $feed;
491
		}
492
493
		/* Feed -5 is a special case: it is used to display auxiliary information
494
		 * when there's nothing to load - e.g. no stuff in fresh feed */
495
496
		if ($feed == -5) {
497
			print json_encode($this->generate_dashboard_feed());
498
			return;
499
		}
500
501
		$sth = false;
502
		if ($feed < LABEL_BASE_INDEX) {
503
504
			$label_feed = Labels::feed_to_label_id($feed);
505
506
			$sth = $this->pdo->prepare("SELECT id FROM ttrss_labels2 WHERE
507
							id = ? AND owner_uid = ?");
508
			$sth->execute([$label_feed, $_SESSION['uid']]);
509
510
		} else if (!$cat_view && is_numeric($feed) && $feed > 0) {
511
512
			$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
513
							id = ? AND owner_uid = ?");
514
			$sth->execute([$feed, $_SESSION['uid']]);
515
516
		} else if ($cat_view && is_numeric($feed) && $feed > 0) {
517
518
			$sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE
519
							id = ? AND owner_uid = ?");
520
521
			$sth->execute([$feed, $_SESSION['uid']]);
522
		}
523
524
		if ($sth && !$sth->fetch()) {
525
			print json_encode($this->generate_error_feed(__("Feed not found.")));
526
			return;
527
		}
528
529
		/* Updating a label ccache means recalculating all of the caches
530
		 * so for performance reasons we don't do that here */
531
532
		if ($feed >= 0) {
533
			CCache::update($feed, $_SESSION["uid"], $cat_view);
534
		}
535
536
		set_pref("_DEFAULT_VIEW_MODE", $view_mode);
537
		set_pref("_DEFAULT_VIEW_ORDER_BY", $order_by);
538
539
		/* bump login timestamp if needed */
540
		if (time() - $_SESSION["last_login_update"] > 3600) {
541
			$sth = $this->pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
542
			$sth->execute([$_SESSION['uid']]);
543
544
			$_SESSION["last_login_update"] = time();
545
		}
546
547
		if (!$cat_view && is_numeric($feed) && $feed > 0) {
548
			$sth = $this->pdo->prepare("UPDATE ttrss_feeds SET last_viewed = NOW()
549
							WHERE id = ? AND owner_uid = ?");
550
			$sth->execute([$feed, $_SESSION['uid']]);
551
		}
552
553
		$reply['headlines'] = [];
554
555
		$override_order = false;
556
		$skip_first_id_check = false;
557
558
		switch ($order_by) {
559
		case "title":
560
			$override_order = "ttrss_entries.title, date_entered, updated";
561
			break;
562
		case "date_reverse":
563
			$override_order = "score DESC, date_entered, updated";
564
			$skip_first_id_check = true;
565
			break;
566
		case "feed_dates":
567
			$override_order = "updated DESC";
568
			break;
569
		}
570
571
		$ret = $this->format_headlines_list($feed, $method,
572
			$view_mode, $limit, $cat_view, $offset,
573
			$override_order, true, $check_first_id, $skip_first_id_check, $order_by);
574
575
		$headlines_count = $ret[1];
576
		$disable_cache = $ret[3];
577
		$reply['headlines'] = $ret[4];
578
579
		if (!$next_unread_feed) {
580
					$reply['headlines']['id'] = $feed;
581
		} else {
582
					$reply['headlines']['id'] = $next_unread_feed;
583
		}
584
585
		$reply['headlines']['is_cat'] = (bool) $cat_view;
586
587
		$reply['headlines-info'] = ["count" => (int) $headlines_count,
588
									"disable_cache" => (bool) $disable_cache];
589
590
		// this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc
591
		$reply['runtime-info'] = make_runtime_info();
592
593
		$reply_json = json_encode($reply);
594
595
		if (!$reply_json) {
596
			$reply_json = json_encode(["error" => ["code" => 15,
597
				"message" => json_last_error_msg()]]);
598
		}
599
600
		print $reply_json;
601
602
	}
603
604
	private function generate_dashboard_feed() {
605
		$reply = array();
606
607
		$reply['headlines']['id'] = -5;
608
		$reply['headlines']['is_cat'] = false;
609
610
		$reply['headlines']['toolbar'] = '';
611
612
		$reply['headlines']['content'] = "<div class='whiteBox'>".__('No feed selected.');
613
614
		$reply['headlines']['content'] .= "<p><span class=\"text-muted\">";
615
616
		$sth = $this->pdo->prepare("SELECT ".SUBSTRING_FOR_DATE."(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds
617
			WHERE owner_uid = ?");
618
		$sth->execute([$_SESSION['uid']]);
619
		$row = $sth->fetch();
620
621
		$last_updated = make_local_datetime($row["last_updated"], false);
622
623
		$reply['headlines']['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated);
624
625
		$sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors
626
			FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
627
		$sth->execute([$_SESSION['uid']]);
628
		$row = $sth->fetch();
629
630
		$num_errors = $row["num_errors"];
631
632
		if ($num_errors > 0) {
633
			$reply['headlines']['content'] .= "<br/>";
634
			$reply['headlines']['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">".
635
				__('Some feeds have update errors (click for details)')."</a>";
636
		}
637
		$reply['headlines']['content'] .= "</span></p>";
638
639
		$reply['headlines-info'] = array("count" => 0,
640
			"unread" => 0,
641
			"disable_cache" => true);
642
643
		return $reply;
644
	}
645
646
	private function generate_error_feed($error) {
647
		$reply = array();
648
649
		$reply['headlines']['id'] = -7;
650
		$reply['headlines']['is_cat'] = false;
651
652
		$reply['headlines']['toolbar'] = '';
653
		$reply['headlines']['content'] = "<div class='whiteBox'>".$error."</div>";
654
655
		$reply['headlines-info'] = array("count" => 0,
656
			"unread" => 0,
657
			"disable_cache" => true);
658
659
		return $reply;
660
	}
661
662
	public function quickAddFeed() {
663
		print "<form onsubmit='return false'>";
664
665
		print_hidden("op", "rpc");
666
		print_hidden("method", "addfeed");
667
668
		print "<div id='fadd_error_message' style='display : none' class='alert alert-danger'></div>";
669
670
		print "<div id='fadd_multiple_notify' style='display : none'>";
671
		print_notice("Provided URL is a HTML page referencing multiple feeds, please select required feed from the dropdown menu below.");
672
		print "<p></div>";
673
674
		print "<section>";
675
676
		print "<fieldset>";
677
		print "<div style='float : right'><img style='display : none' id='feed_add_spinner' src='images/indicator_white.gif'></div>";
678
		print "<input style='font-size : 16px; width : 500px;'
679
			placeHolder=\"".__("Feed or site URL")."\"
680
			dojoType='dijit.form.ValidationTextBox' required='1' name='feed' id='feedDlg_feedUrl'>";
681
682
		print "</fieldset>";
683
684
		print "<fieldset>";
685
686
		if (get_pref('ENABLE_FEED_CATS')) {
687
			print "<label class='inline'>".__('Place in category:')."</label> ";
688
			print_feed_cat_select("cat", false, 'dojoType="fox.form.Select"');
689
		}
690
691
		print "</fieldset>";
692
693
		print "</section>";
694
695
		print '<div id="feedDlg_feedsContainer" style="display : none">
696
				<header>' . __('Available feeds').'</header>
697
				<section>
698
					<fieldset>
699
						<select id="feedDlg_feedContainerSelect"
700
							dojoType="fox.form.Select" size="3">
701
							<script type="dojo/method" event="onChange" args="value">
702
								dijit.byId("feedDlg_feedUrl").attr("value", value);
703
							</script>
704
						</select>
705
					</fieldset>
706
				</section>
707
			</div>';
708
709
		print "<div id='feedDlg_loginContainer' style='display : none'>
710
				<section>
711
				<fieldset>
712
					<input dojoType=\"dijit.form.TextBox\" name='login'\"
713
						placeHolder=\"".__("Login")."\"
714
						autocomplete=\"new-password\"
715
						style=\"width : 10em;\">
716
					<input
717
						placeHolder=\"".__("Password")."\"
718
						dojoType=\"dijit.form.TextBox\" type='password'
719
						autocomplete=\"new-password\"
720
						style=\"width : 10em;\" name='pass'\">
721
				</fieldset>
722
				</section>
723
			</div>";
724
725
		print "<section>";
726
		print "<label>
727
			<label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox' id='feedDlg_loginCheck'
728
					onclick='displayIfChecked(this, \"feedDlg_loginContainer\")'>
729
				".__('This feed requires authentication.')."</label>";
730
		print "</section>";
731
732
		print "<footer>";
733
		print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit'
734
				onclick=\"return dijit.byId('feedAddDlg').execute()\">".__('Subscribe')."</button>";
735
736
		print "<button dojoType='dijit.form.Button' onclick=\"return dijit.byId('feedAddDlg').hide()\">".__('Cancel')."</button>";
737
		print "</footer>";
738
739
		print "</form>";
740
	}
741
742
	public function search() {
743
		$this->params = explode(":", $_REQUEST["param"], 2);
744
745
		$active_feed_id = sprintf("%d", $this->params[0]);
746
		$is_cat = $this->params[1] != "false";
747
748
		print "<form onsubmit='return false;'>";
749
750
		print "<section>";
751
752
		print "<fieldset>";
753
		print "<input dojoType='dijit.form.ValidationTextBox' id='search_query'
754
			style='font-size : 16px; width : 540px;'
755
			placeHolder=\"".T_sprintf("Search %s...", $this->getFeedTitle($active_feed_id, $is_cat))."\"
756
			name='query' type='search' value=''>";
757
		print "</fieldset>";
758
759
		if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
760
			print "<fieldset>";
761
			print "<label class='inline'>".__("Language:")."</label>";
762
			print_select("search_language", get_pref('DEFAULT_SEARCH_LANGUAGE'), Pref_Feeds::get_ts_languages(),
763
				"dojoType='fox.form.Select' title=\"".__('Used for word stemming')."\"");
764
			print "</fieldset>";
765
		}
766
767
		print "</section>";
768
769
		print "<footer>";
770
771
		if (count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0) {
772
			print "<button dojoType='dijit.form.Button' style='float : left' class='alt-info' onclick='window.open(\"https://tt-rss.org/wiki/SearchSyntax\")'>
773
				<i class='material-icons'>help</i> ".__("Search syntax")."</button>";
774
		}
775
776
		print "<button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick=\"dijit.byId('searchDlg').execute()\">".__('Search')."</button>
777
			<button dojoType='dijit.form.Button' onclick=\"dijit.byId('searchDlg').hide()\">".__('Cancel')."</button>";
778
779
		print "</footer>";
780
781
		print "</form>";
782
	}
783
784
	public function update_debugger() {
785
		header("Content-type: text/html");
786
787
		Debug::set_enabled(true);
788
		Debug::set_loglevel($_REQUEST["xdebug"]);
789
790
		$feed_id = (int) $_REQUEST["feed_id"];
791
		@$do_update = $_REQUEST["action"] == "do_update";
792
		$csrf_token = $_REQUEST["csrf_token"];
793
794
		$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
795
		$sth->execute([$feed_id, $_SESSION['uid']]);
796
797
		if (!$sth->fetch()) {
798
			print "Access denied.";
799
			return;
800
		}
801
802
		$refetch_checked = isset($_REQUEST["force_refetch"]) ? "checked" : "";
803
		$rehash_checked = isset($_REQUEST["force_rehash"]) ? "checked" : "";
804
805
		?>
806
		<!DOCTYPE html>
807
		<html>
808
		<head>
809
			<?php echo stylesheet_tag("css/default.css") ?>
810
			<title>Feed Debugger</title>
811
			<?php
812
				echo stylesheet_tag("css/default.css");
813
				echo javascript_tag("lib/prototype.js");
814
				echo javascript_tag("lib/dojo/dojo.js");
815
				echo javascript_tag("lib/dojo/tt-rss-layer.js");
816
			?>
817
		</head>
818
		<body class="flat ttrss_utility feed_debugger">
819
		<script type="text/javascript">
820
			require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
821
				'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
822
				ready(function() {
823
					parser.parse();
824
				});
825
			});
826
		</script>
827
828
			<div class="container">
829
				<h1>Feed Debugger: <?php echo "$feed_id: ".$this->getFeedTitle($feed_id) ?></h1>
830
				<div class="content">
831
					<form method="GET" action="">
832
						<input type="hidden" name="op" value="feeds">
833
						<input type="hidden" name="method" value="update_debugger">
834
						<input type="hidden" name="xdebug" value="1">
835
						<input type="hidden" name="csrf_token" value="<?php echo $csrf_token ?>">
836
						<input type="hidden" name="action" value="do_update">
837
						<input type="hidden" name="feed_id" value="<?php echo $feed_id ?>">
838
839
						<fieldset class="narrow">
840
							<label class="checkbox"><input dojoType="dijit.form.CheckBox" type="checkbox" name="force_refetch" value="1" <?php echo $refetch_checked ?>> Force refetch</label>
841
						</fieldset>
842
843
						<fieldset class="narrow">
844
							<label class="checkbox"><input dojoType="dijit.form.CheckBox" type="checkbox" name="force_rehash" value="1" <?php echo $rehash_checked ?>> Force rehash</label>
845
						</fieldset>
846
847
						<button type="submit" dojoType="dijit.form.Button" class="alt-primary">Continue</button>
848
					</form>
849
850
					<hr>
851
852
					<pre><?php
853
854
					if ($do_update) {
855
						RSSUtils::update_rss_feed($feed_id, true);
856
					}
857
858
					?></pre>
859
				</div>
860
			</div>
861
		</body>
862
		</html>
863
		<?php
864
865
	}
866
867
	public static function catchup_feed($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) {
868
869
		if (!$owner_uid) {
870
			$owner_uid = $_SESSION['uid'];
871
		}
872
873
		$pdo = Db::pdo();
874
875
		if (is_array($search) && $search[0]) {
876
			$search_qpart = "";
877
878
			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH) as $plugin) {
879
				list($search_qpart, $search_words) = $plugin->hook_search($search[0]);
880
				break;
881
			}
882
883
			// fall back in case of no plugins
884
			if (!$search_qpart) {
885
				list($search_qpart, $search_words) = Feeds::search_to_sql($search[0], $search[1]);
886
			}
887
		} else {
888
			$search_qpart = "true";
889
		}
890
891
		// TODO: all this interval stuff needs some generic generator function
892
893
		switch ($mode) {
894
		case "1day":
895
			if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
896
				$date_qpart = "date_entered < NOW() - INTERVAL '1 day' ";
897
			} else {
898
				$date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 DAY) ";
899
			}
900
			break;
901
		case "1week":
902
			if (DB_TYPE == "pgsql") {
903
				$date_qpart = "date_entered < NOW() - INTERVAL '1 week' ";
904
			} else {
905
				$date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 WEEK) ";
906
			}
907
			break;
908
		case "2week":
909
			if (DB_TYPE == "pgsql") {
910
				$date_qpart = "date_entered < NOW() - INTERVAL '2 week' ";
911
			} else {
912
				$date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 2 WEEK) ";
913
			}
914
			break;
915
		default:
916
			$date_qpart = "true";
917
		}
918
919
		if (is_numeric($feed)) {
920
			if ($cat_view) {
921
922
				if ($feed >= 0) {
923
924
					if ($feed > 0) {
925
						$children = Feeds::getChildCategories($feed, $owner_uid);
926
						array_push($children, $feed);
927
						$children = array_map("intval", $children);
928
929
						$children = join(",", $children);
930
931
						$cat_qpart = "cat_id IN ($children)";
932
					} else {
933
						$cat_qpart = "cat_id IS NULL";
934
					}
935
936
					$sth = $pdo->prepare("UPDATE ttrss_user_entries
937
						SET unread = false, last_read = NOW() WHERE ref_id IN
938
							(SELECT id FROM
939
								(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
940
									AND owner_uid = ? AND unread = true AND feed_id IN
941
										(SELECT id FROM ttrss_feeds WHERE $cat_qpart) AND $date_qpart AND $search_qpart) as tmp)");
942
					$sth->execute([$owner_uid]);
943
944
				} else if ($feed == -2) {
945
946
					$sth = $pdo->prepare("UPDATE ttrss_user_entries
947
						SET unread = false,last_read = NOW() WHERE (SELECT COUNT(*)
948
							FROM ttrss_user_labels2, ttrss_entries WHERE article_id = ref_id AND id = ref_id AND $date_qpart AND $search_qpart) > 0
949
							AND unread = true AND owner_uid = ?");
950
					$sth->execute([$owner_uid]);
951
				}
952
953
			} else if ($feed > 0) {
954
955
				$sth = $pdo->prepare("UPDATE ttrss_user_entries
956
					SET unread = false, last_read = NOW() WHERE ref_id IN
957
						(SELECT id FROM
958
							(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
959
								AND owner_uid = ? AND unread = true AND feed_id = ? AND $date_qpart AND $search_qpart) as tmp)");
960
				$sth->execute([$owner_uid, $feed]);
961
962
			} else if ($feed < 0 && $feed > LABEL_BASE_INDEX) { // special, like starred
963
964
				if ($feed == -1) {
965
					$sth = $pdo->prepare("UPDATE ttrss_user_entries
966
						SET unread = false, last_read = NOW() WHERE ref_id IN
967
							(SELECT id FROM
968
								(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
969
									AND owner_uid = ? AND unread = true AND marked = true AND $date_qpart AND $search_qpart) as tmp)");
970
					$sth->execute([$owner_uid]);
971
				}
972
973
				if ($feed == -2) {
974
					$sth = $pdo->prepare("UPDATE ttrss_user_entries
975
						SET unread = false, last_read = NOW() WHERE ref_id IN
976
							(SELECT id FROM
977
								(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
978
									AND owner_uid = ? AND unread = true AND published = true AND $date_qpart AND $search_qpart) as tmp)");
979
					$sth->execute([$owner_uid]);
980
				}
981
982
				if ($feed == -3) {
983
984
					$intl = (int) get_pref("FRESH_ARTICLE_MAX_AGE");
985
986
					if (DB_TYPE == "pgsql") {
987
						$match_part = "date_entered > NOW() - INTERVAL '$intl hour' ";
988
					} else {
989
						$match_part = "date_entered > DATE_SUB(NOW(),
990
							INTERVAL $intl HOUR) ";
991
					}
992
993
					$sth = $pdo->prepare("UPDATE ttrss_user_entries
994
						SET unread = false, last_read = NOW() WHERE ref_id IN
995
							(SELECT id FROM
996
								(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
997
									AND owner_uid = ? AND score >= 0 AND unread = true AND $date_qpart AND $match_part AND $search_qpart) as tmp)");
998
					$sth->execute([$owner_uid]);
999
				}
1000
1001
				if ($feed == -4) {
1002
					$sth = $pdo->prepare("UPDATE ttrss_user_entries
1003
						SET unread = false, last_read = NOW() WHERE ref_id IN
1004
							(SELECT id FROM
1005
								(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
1006
									AND owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
1007
					$sth->execute([$owner_uid]);
1008
				}
1009
1010
			} else if ($feed < LABEL_BASE_INDEX) { // label
1011
1012
				$label_id = Labels::feed_to_label_id($feed);
1013
1014
				$sth = $pdo->prepare("UPDATE ttrss_user_entries
1015
					SET unread = false, last_read = NOW() WHERE ref_id IN
1016
						(SELECT id FROM
1017
							(SELECT DISTINCT ttrss_entries.id FROM ttrss_entries, ttrss_user_entries, ttrss_user_labels2 WHERE ref_id = id
1018
								AND label_id = ? AND ref_id = article_id
1019
								AND owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
1020
				$sth->execute([$label_id, $owner_uid]);
1021
1022
			}
1023
1024
			CCache::update($feed, $owner_uid, $cat_view);
1025
1026
		} else { // tag
1027
			$sth = $pdo->prepare("UPDATE ttrss_user_entries
1028
				SET unread = false, last_read = NOW() WHERE ref_id IN
1029
					(SELECT id FROM
1030
						(SELECT DISTINCT ttrss_entries.id FROM ttrss_entries, ttrss_user_entries, ttrss_tags WHERE ref_id = ttrss_entries.id
1031
							AND post_int_id = int_id AND tag_name = ?
1032
							AND ttrss_user_entries.owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
1033
			$sth->execute([$feed, $owner_uid]);
1034
1035
		}
1036
	}
1037
1038
	public static function getFeedArticles($feed, $is_cat = false, $unread_only = false,
1039
							 $owner_uid = false) {
1040
1041
		$n_feed = (int) $feed;
1042
		$need_entries = false;
1043
1044
		$pdo = Db::pdo();
1045
1046
		if (!$owner_uid) {
1047
			$owner_uid = $_SESSION["uid"];
1048
		}
1049
1050
		if ($unread_only) {
1051
			$unread_qpart = "unread = true";
1052
		} else {
1053
			$unread_qpart = "true";
1054
		}
1055
1056
		$match_part = "";
1057
1058
		if ($is_cat) {
1059
			return Feeds::getCategoryUnread($n_feed, $owner_uid);
1060
		} else if ($n_feed == -6) {
1061
			return 0;
1062
		} else if ($feed != "0" && $n_feed == 0) {
1063
1064
			$sth = $pdo->prepare("SELECT SUM((SELECT COUNT(int_id)
1065
				FROM ttrss_user_entries,ttrss_entries WHERE int_id = post_int_id
1066
					AND ref_id = id AND $unread_qpart)) AS count FROM ttrss_tags
1067
				WHERE owner_uid = ? AND tag_name = ?");
1068
1069
			$sth->execute([$owner_uid, $feed]);
1070
			$row = $sth->fetch();
1071
1072
			return $row["count"];
1073
1074
		} else if ($n_feed == -1) {
1075
			$match_part = "marked = true";
1076
		} else if ($n_feed == -2) {
1077
			$match_part = "published = true";
1078
		} else if ($n_feed == -3) {
1079
			$match_part = "unread = true AND score >= 0";
1080
1081
			$intl = (int) get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid);
1082
1083
			if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1084
				$match_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' ";
1085
			} else {
1086
				$match_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) ";
1087
			}
1088
1089
			$need_entries = true;
1090
1091
		} else if ($n_feed == -4) {
1092
			$match_part = "true";
1093
		} else if ($n_feed >= 0) {
1094
1095
			if ($n_feed != 0) {
1096
				$match_part = "feed_id = ".(int) $n_feed;
1097
			} else {
1098
				$match_part = "feed_id IS NULL";
1099
			}
1100
1101
		} else if ($feed < LABEL_BASE_INDEX) {
1102
1103
			$label_id = Labels::feed_to_label_id($feed);
1104
1105
			return Feeds::getLabelUnread($label_id, $owner_uid);
1106
		}
1107
1108
		if ($match_part) {
1109
1110
			if ($need_entries) {
1111
				$from_qpart = "ttrss_user_entries,ttrss_entries";
1112
				$from_where = "ttrss_entries.id = ttrss_user_entries.ref_id AND";
1113
			} else {
1114
				$from_qpart = "ttrss_user_entries";
1115
				$from_where = "";
1116
			}
1117
1118
			$sth = $pdo->prepare("SELECT count(int_id) AS unread
1119
				FROM $from_qpart WHERE
1120
				$unread_qpart AND $from_where ($match_part) AND ttrss_user_entries.owner_uid = ?");
1121
			$sth->execute([$owner_uid]);
1122
			$row = $sth->fetch();
1123
1124
			return $row["unread"];
1125
1126
		} else {
1127
1128
			$sth = $pdo->prepare("SELECT COUNT(post_int_id) AS unread
1129
				FROM ttrss_tags,ttrss_user_entries,ttrss_entries
1130
				WHERE tag_name = ? AND post_int_id = int_id AND ref_id = ttrss_entries.id
1131
				AND $unread_qpart AND ttrss_tags.owner_uid = ,");
1132
1133
			$sth->execute([$feed, $owner_uid]);
1134
			$row = $sth->fetch();
1135
1136
			return $row["unread"];
1137
		}
1138
	}
1139
1140
	/**
1141
	 * @return array (code => Status code, message => error message if available)
1142
	 *
1143
	 *                 0 - OK, Feed already exists
1144
	 *                 1 - OK, Feed added
1145
	 *                 2 - Invalid URL
1146
	 *                 3 - URL content is HTML, no feeds available
1147
	 *                 4 - URL content is HTML which contains multiple feeds.
1148
	 *                     Here you should call extractfeedurls in rpc-backend
1149
	 *                     to get all possible feeds.
1150
	 *                 5 - Couldn't download the URL content.
1151
	 *                 6 - Content is an invalid XML.
1152
	 */
1153
	public static function subscribe_to_feed($url, $cat_id = 0,
1154
							   $auth_login = '', $auth_pass = '') {
1155
1156
		global $fetch_last_error;
1157
		global $fetch_last_error_content;
1158
		global $fetch_last_content_type;
1159
1160
		$pdo = Db::pdo();
1161
1162
		$url = Feeds::fix_url($url);
1163
1164
		if (!$url || !Feeds::validate_feed_url($url)) {
1165
			return array("code" => 2);
1166
		}
1167
1168
		$contents = @fetch_file_contents($url, false, $auth_login, $auth_pass);
1169
1170
		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SUBSCRIBE_FEED) as $plugin) {
1171
			$contents = $plugin->hook_subscribe_feed($contents, $url, $auth_login, $auth_pass);
1172
		}
1173
1174
		if (!$contents) {
1175
			if (preg_match("/cloudflare\.com/", $fetch_last_error_content)) {
1176
				$fetch_last_error .= " (feed behind Cloudflare)";
1177
			}
1178
1179
			return array("code" => 5, "message" => $fetch_last_error);
1180
		}
1181
1182
		if (mb_strpos($fetch_last_content_type, "html") !== FALSE && Feeds::is_html($contents)) {
1183
			$feedUrls = Feeds::get_feeds_from_html($url, $contents);
1184
1185
			if (count($feedUrls) == 0) {
1186
				return array("code" => 3);
1187
			} else if (count($feedUrls) > 1) {
1188
				return array("code" => 4, "feeds" => $feedUrls);
1189
			}
1190
			//use feed url as new URL
1191
			$url = key($feedUrls);
1192
		}
1193
1194
		if (!$cat_id) {
1195
			$cat_id = null;
1196
		}
1197
1198
		$sth = $pdo->prepare("SELECT id FROM ttrss_feeds
1199
			WHERE feed_url = ? AND owner_uid = ?");
1200
		$sth->execute([$url, $_SESSION['uid']]);
1201
1202
		if ($row = $sth->fetch()) {
1203
			return array("code" => 0, "feed_id" => (int) $row["id"]);
1204
		} else {
1205
			$sth = $pdo->prepare(
1206
				"INSERT INTO ttrss_feeds
1207
					(owner_uid,feed_url,title,cat_id, auth_login,auth_pass,update_method,auth_pass_encrypted)
1208
				VALUES (?, ?, ?, ?, ?, ?, 0, false)");
1209
1210
			$sth->execute([$_SESSION['uid'], $url, "[Unknown]", $cat_id, (string) $auth_login, (string) $auth_pass]);
1211
1212
			$sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE feed_url = ?
1213
					AND owner_uid = ?");
1214
			$sth->execute([$url, $_SESSION['uid']]);
1215
			$row = $sth->fetch();
1216
1217
			$feed_id = $row["id"];
1218
1219
			if ($feed_id) {
1220
				RSSUtils::set_basic_feed_info($feed_id);
1221
			}
1222
1223
			return array("code" => 1, "feed_id" => (int) $feed_id);
1224
1225
		}
1226
	}
1227
1228
	public static function getIconFile($feed_id) {
1229
		return ICONS_DIR."/$feed_id.ico";
0 ignored issues
show
Bug introduced by
The constant ICONS_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1230
	}
1231
1232
	public static function feedHasIcon($id) {
1233
		return is_file(ICONS_DIR."/$id.ico") && filesize(ICONS_DIR."/$id.ico") > 0;
0 ignored issues
show
Bug introduced by
The constant ICONS_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1234
	}
1235
1236
	public static function getFeedIcon($id) {
1237
		switch ($id) {
1238
			case 0:
1239
				return "archive";
1240
			case -1:
1241
				return "star";
1242
			case -2:
1243
				return "rss_feed";
1244
			case -3:
1245
				return "whatshot";
1246
			case -4:
1247
				return "inbox";
1248
			case -6:
1249
				return "restore";
1250
			default:
1251
				if ($id < LABEL_BASE_INDEX) {
1252
					return "label";
1253
				} else {
1254
					$icon = self::getIconFile($id);
1255
1256
					if ($icon && file_exists($icon)) {
1257
						return ICONS_URL . "/" . basename($icon) . "?" . filemtime($icon);
0 ignored issues
show
Bug introduced by
The constant ICONS_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1258
					}
1259
				}
1260
		}
1261
1262
		return false;
1263
	}
1264
1265
	public static function getFeedTitle($id, $cat = false) {
1266
		$pdo = Db::pdo();
1267
1268
		if ($cat) {
1269
			return Feeds::getCategoryTitle($id);
1270
		} else if ($id == -1) {
1271
			return __("Starred articles");
1272
		} else if ($id == -2) {
1273
			return __("Published articles");
1274
		} else if ($id == -3) {
1275
			return __("Fresh articles");
1276
		} else if ($id == -4) {
1277
			return __("All articles");
1278
		} else if ($id === 0 || $id === "0") {
1279
			return __("Archived articles");
1280
		} else if ($id == -6) {
1281
			return __("Recently read");
1282
		} else if ($id < LABEL_BASE_INDEX) {
1283
1284
			$label_id = Labels::feed_to_label_id($id);
1285
1286
			$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2 WHERE id = ?");
1287
			$sth->execute([$label_id]);
1288
1289
			if ($row = $sth->fetch()) {
1290
				return $row["caption"];
1291
			} else {
1292
				return "Unknown label ($label_id)";
1293
			}
1294
1295
		} else if (is_numeric($id) && $id > 0) {
1296
1297
			$sth = $pdo->prepare("SELECT title FROM ttrss_feeds WHERE id = ?");
1298
			$sth->execute([$id]);
1299
1300
			if ($row = $sth->fetch()) {
1301
				return $row["title"];
1302
			} else {
1303
				return "Unknown feed ($id)";
1304
			}
1305
1306
		} else {
1307
			return $id;
1308
		}
1309
	}
1310
1311
	public static function getCategoryUnread($cat, $owner_uid = false) {
1312
1313
		if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1314
1315
		$pdo = Db::pdo();
1316
1317
		if ($cat >= 0) {
1318
1319
			if (!$cat) $cat = null;
1320
1321
			$sth = $pdo->prepare("SELECT id FROM ttrss_feeds
1322
                    WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL))
1323
					AND owner_uid = :uid");
1324
1325
			$sth->execute([":cat" => $cat, ":uid" => $owner_uid]);
1326
1327
			$cat_feeds = array();
1328
			while ($line = $sth->fetch()) {
1329
				array_push($cat_feeds, "feed_id = ".(int) $line["id"]);
1330
			}
1331
1332
			if (count($cat_feeds) == 0) {
1333
				return 0;
1334
			}
1335
1336
			$match_part = implode(" OR ", $cat_feeds);
1337
1338
			$sth = $pdo->prepare("SELECT COUNT(int_id) AS unread
1339
				FROM ttrss_user_entries
1340
				WHERE	unread = true AND ($match_part)
1341
				AND owner_uid = ?");
1342
			$sth->execute([$owner_uid]);
1343
1344
			$unread = 0;
1345
1346
			# this needs to be rewritten
1347
			while ($line = $sth->fetch()) {
1348
				$unread += $line["unread"];
1349
			}
1350
1351
			return $unread;
1352
		} else if ($cat == -1) {
1353
			return getFeedUnread(-1) + getFeedUnread(-2) + getFeedUnread(-3) + getFeedUnread(0);
1354
		} else if ($cat == -2) {
1355
1356
			$sth = $pdo->prepare("SELECT COUNT(unread) AS unread FROM
1357
					ttrss_user_entries, ttrss_user_labels2
1358
				WHERE article_id = ref_id AND unread = true
1359
					AND ttrss_user_entries.owner_uid = ?");
1360
			$sth->execute([$owner_uid]);
1361
			$row = $sth->fetch();
1362
1363
			return $row["unread"];
1364
		}
1365
	}
1366
1367
	// only accepts real cats (>= 0)
1368
	public static function getCategoryChildrenUnread($cat, $owner_uid = false) {
1369
		if (!$owner_uid) {
1370
			$owner_uid = $_SESSION["uid"];
1371
		}
1372
1373
		$pdo = Db::pdo();
1374
1375
		$sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE parent_cat = ?
1376
				AND owner_uid = ?");
1377
		$sth->execute([$cat, $owner_uid]);
1378
1379
		$unread = 0;
1380
1381
		while ($line = $sth->fetch()) {
1382
			$unread += Feeds::getCategoryUnread($line["id"], $owner_uid);
1383
			$unread += Feeds::getCategoryChildrenUnread($line["id"], $owner_uid);
1384
		}
1385
1386
		return $unread;
1387
	}
1388
1389
	public static function getGlobalUnread($user_id = false) {
1390
1391
		if (!$user_id) {
1392
			$user_id = $_SESSION["uid"];
1393
		}
1394
1395
		$pdo = Db::pdo();
1396
1397
		$sth = $pdo->prepare("SELECT SUM(value) AS c_id FROM ttrss_counters_cache
1398
			WHERE owner_uid = ? AND feed_id > 0");
1399
		$sth->execute([$user_id]);
1400
		$row = $sth->fetch();
1401
1402
		return $row["c_id"];
1403
	}
1404
1405
	public static function getCategoryTitle($cat_id) {
1406
1407
		if ($cat_id == -1) {
1408
			return __("Special");
1409
		} else if ($cat_id == -2) {
1410
			return __("Labels");
1411
		} else {
1412
1413
			$pdo = Db::pdo();
1414
1415
			$sth = $pdo->prepare("SELECT title FROM ttrss_feed_categories WHERE
1416
				id = ?");
1417
			$sth->execute([$cat_id]);
1418
1419
			if ($row = $sth->fetch()) {
1420
				return $row["title"];
1421
			} else {
1422
				return __("Uncategorized");
1423
			}
1424
		}
1425
	}
1426
1427
	public static function getLabelUnread($label_id, $owner_uid = false) {
1428
		if (!$owner_uid) {
1429
			$owner_uid = $_SESSION["uid"];
1430
		}
1431
1432
		$pdo = Db::pdo();
1433
1434
		$sth = $pdo->prepare("SELECT COUNT(ref_id) AS unread FROM ttrss_user_entries, ttrss_user_labels2
1435
			WHERE owner_uid = ? AND unread = true AND label_id = ? AND article_id = ref_id");
1436
1437
		$sth->execute([$owner_uid, $label_id]);
1438
1439
		if ($row = $sth->fetch()) {
1440
			return $row["unread"];
1441
		} else {
1442
			return 0;
1443
		}
1444
	}
1445
1446
	public static function queryFeedHeadlines($params) {
1447
1448
		$pdo = Db::pdo();
1449
1450
		// WARNING: due to highly dynamic nature of this query its going to quote parameters
1451
		// right before adding them to SQL part
1452
1453
		$feed = $params["feed"];
1454
		$limit = isset($params["limit"]) ? $params["limit"] : 30;
1455
		$view_mode = $params["view_mode"];
1456
		$cat_view = isset($params["cat_view"]) ? $params["cat_view"] : false;
1457
		$search = isset($params["search"]) ? $params["search"] : false;
1458
		$search_language = isset($params["search_language"]) ? $params["search_language"] : "";
1459
		$override_order = isset($params["override_order"]) ? $params["override_order"] : false;
1460
		$offset = isset($params["offset"]) ? $params["offset"] : 0;
1461
		$owner_uid = isset($params["owner_uid"]) ? $params["owner_uid"] : $_SESSION["uid"];
1462
		$since_id = isset($params["since_id"]) ? $params["since_id"] : 0;
1463
		$include_children = isset($params["include_children"]) ? $params["include_children"] : false;
1464
		$ignore_vfeed_group = isset($params["ignore_vfeed_group"]) ? $params["ignore_vfeed_group"] : false;
1465
		$override_strategy = isset($params["override_strategy"]) ? $params["override_strategy"] : false;
1466
		$override_vfeed = isset($params["override_vfeed"]) ? $params["override_vfeed"] : false;
1467
		$start_ts = isset($params["start_ts"]) ? $params["start_ts"] : false;
1468
		$check_first_id = isset($params["check_first_id"]) ? $params["check_first_id"] : false;
1469
		$skip_first_id_check = isset($params["skip_first_id_check"]) ? $params["skip_first_id_check"] : false;
1470
		//$order_by = isset($params["order_by"]) ? $params["order_by"] : false;
1471
1472
		$ext_tables_part = "";
1473
		$limit_query_part = "";
1474
		$query_error_override = "";
1475
1476
		$search_words = [];
1477
1478
		if ($search) {
1479
			$search_query_part = "";
1480
1481
			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH) as $plugin) {
1482
				list($search_query_part, $search_words) = $plugin->hook_search($search);
1483
				break;
1484
			}
1485
1486
			// fall back in case of no plugins
1487
			if (!$search_query_part) {
1488
				list($search_query_part, $search_words) = Feeds::search_to_sql($search, $search_language);
1489
			}
1490
1491
			if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1492
				$test_sth = $pdo->prepare("select $search_query_part
1493
					FROM ttrss_entries, ttrss_user_entries WHERE id = ref_id limit 1");
1494
1495
				try {
1496
					$test_sth->execute();
1497
				} catch (PDOException $e) {
1498
					// looks like tsquery syntax is invalid
1499
					$search_query_part = "false";
1500
1501
					$query_error_override = T_sprintf("Incorrect search syntax: %s.", implode(" ", $search_words));
1502
				}
1503
			}
1504
1505
			$search_query_part .= " AND ";
1506
		} else {
1507
			$search_query_part = "";
1508
		}
1509
1510
		if ($since_id) {
1511
			$since_id_part = "ttrss_entries.id > ".$pdo->quote($since_id)." AND ";
1512
		} else {
1513
			$since_id_part = "";
1514
		}
1515
1516
		$view_query_part = "";
1517
1518
		if ($view_mode == "adaptive") {
1519
			if ($search) {
1520
				$view_query_part = " ";
1521
			} else if ($feed != -1) {
1522
1523
				$unread = getFeedUnread($feed, $cat_view);
1524
1525
				if ($cat_view && $feed > 0 && $include_children) {
1526
									$unread += Feeds::getCategoryChildrenUnread($feed);
1527
				}
1528
1529
				if ($unread > 0) {
1530
					$view_query_part = " unread = true AND ";
1531
				}
1532
			}
1533
		}
1534
1535
		if ($view_mode == "marked") {
1536
			$view_query_part = " marked = true AND ";
1537
		}
1538
1539
		if ($view_mode == "has_note") {
1540
			$view_query_part = " (note IS NOT NULL AND note != '') AND ";
1541
		}
1542
1543
		if ($view_mode == "published") {
1544
			$view_query_part = " published = true AND ";
1545
		}
1546
1547
		if ($view_mode == "unread" && $feed != -6) {
1548
			$view_query_part = " unread = true AND ";
1549
		}
1550
1551
		if ($limit > 0) {
1552
			$limit_query_part = "LIMIT ".(int) $limit;
1553
		}
1554
1555
		$allow_archived = false;
1556
1557
		$vfeed_query_part = "";
1558
1559
		/* tags */
1560
		if (!is_numeric($feed)) {
1561
			$query_strategy_part = "true";
1562
			$vfeed_query_part = "(SELECT title FROM ttrss_feeds WHERE
1563
					id = feed_id) as feed_title,";
1564
		} else if ($feed > 0) {
1565
1566
			if ($cat_view) {
1567
1568
				if ($feed > 0) {
1569
					if ($include_children) {
1570
						# sub-cats
1571
						$subcats = Feeds::getChildCategories($feed, $owner_uid);
1572
						array_push($subcats, $feed);
1573
						$subcats = array_map("intval", $subcats);
1574
1575
						$query_strategy_part = "cat_id IN (".
1576
							implode(",", $subcats).")";
1577
1578
					} else {
1579
						$query_strategy_part = "cat_id = ".$pdo->quote($feed);
1580
					}
1581
1582
				} else {
1583
					$query_strategy_part = "cat_id IS NULL";
1584
				}
1585
1586
				$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1587
1588
			} else {
1589
				$query_strategy_part = "feed_id = ".$pdo->quote($feed);
1590
			}
1591
		} else if ($feed == 0 && !$cat_view) { // archive virtual feed
1592
			$query_strategy_part = "feed_id IS NULL";
1593
			$allow_archived = true;
1594
		} else if ($feed == 0 && $cat_view) { // uncategorized
1595
			$query_strategy_part = "cat_id IS NULL AND feed_id IS NOT NULL";
1596
			$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1597
		} else if ($feed == -1) { // starred virtual feed
1598
			$query_strategy_part = "marked = true";
1599
			$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1600
			$allow_archived = true;
1601
1602
			if (!$override_order) {
1603
				$override_order = "last_marked DESC, date_entered DESC, updated DESC";
1604
			}
1605
1606
		} else if ($feed == -2) { // published virtual feed OR labels category
1607
1608
			if (!$cat_view) {
1609
				$query_strategy_part = "published = true";
1610
				$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1611
				$allow_archived = true;
1612
1613
				if (!$override_order) {
1614
					$override_order = "last_published DESC, date_entered DESC, updated DESC";
1615
				}
1616
1617
			} else {
1618
				$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1619
1620
				$ext_tables_part = "ttrss_labels2,ttrss_user_labels2,";
1621
1622
				$query_strategy_part = "ttrss_labels2.id = ttrss_user_labels2.label_id AND
1623
						ttrss_user_labels2.article_id = ref_id";
1624
1625
			}
1626
		} else if ($feed == -6) { // recently read
1627
			$query_strategy_part = "unread = false AND last_read IS NOT NULL";
1628
1629
			if (DB_TYPE == "pgsql") {
1630
				$query_strategy_part .= " AND last_read > NOW() - INTERVAL '1 DAY' ";
1631
			} else {
1632
				$query_strategy_part .= " AND last_read > DATE_SUB(NOW(), INTERVAL 1 DAY) ";
1633
			}
1634
1635
			$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1636
			$allow_archived = true;
1637
			$ignore_vfeed_group = true;
1638
1639
			if (!$override_order) {
1640
				$override_order = "last_read DESC";
1641
			}
1642
1643
		} else if ($feed == -3) { // fresh virtual feed
1644
			$query_strategy_part = "unread = true AND score >= 0";
1645
1646
			$intl = (int) get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid);
1647
1648
			if (DB_TYPE == "pgsql") {
1649
				$query_strategy_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' ";
1650
			} else {
1651
				$query_strategy_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) ";
1652
			}
1653
1654
			$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1655
		} else if ($feed == -4) { // all articles virtual feed
1656
			$allow_archived = true;
1657
			$query_strategy_part = "true";
1658
			$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1659
		} else if ($feed <= LABEL_BASE_INDEX) { // labels
1660
			$label_id = Labels::feed_to_label_id($feed);
1661
1662
			$query_strategy_part = "label_id = ".$pdo->quote($label_id)." AND
1663
					ttrss_labels2.id = ttrss_user_labels2.label_id AND
1664
					ttrss_user_labels2.article_id = ref_id";
1665
1666
			$vfeed_query_part = "ttrss_feeds.title AS feed_title,";
1667
			$ext_tables_part = "ttrss_labels2,ttrss_user_labels2,";
1668
			$allow_archived = true;
1669
1670
		} else {
1671
			$query_strategy_part = "true";
1672
		}
1673
1674
		$order_by = "score DESC, date_entered DESC, updated DESC";
1675
1676
		if ($override_order) {
1677
			$order_by = $override_order;
1678
		}
1679
1680
		if ($override_strategy) {
1681
			$query_strategy_part = $override_strategy;
1682
		}
1683
1684
		if ($override_vfeed) {
1685
			$vfeed_query_part = $override_vfeed;
1686
		}
1687
1688
		if ($search) {
1689
			$feed_title = T_sprintf("Search results: %s", $search);
1690
		} else {
1691
			if ($cat_view) {
1692
				$feed_title = Feeds::getCategoryTitle($feed);
1693
			} else {
1694
				if (is_numeric($feed) && $feed > 0) {
1695
					$ssth = $pdo->prepare("SELECT title,site_url,last_error,last_updated
1696
							FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
1697
					$ssth->execute([$feed, $owner_uid]);
1698
					$row = $ssth->fetch();
1699
1700
					$feed_title = $row["title"];
1701
					$feed_site_url = $row["site_url"];
1702
					$last_error = $row["last_error"];
1703
					$last_updated = $row["last_updated"];
1704
				} else {
1705
					$feed_title = Feeds::getFeedTitle($feed);
1706
				}
1707
			}
1708
		}
1709
1710
		$content_query_part = "content, ";
1711
1712
		if ($limit_query_part) {
1713
			$offset_query_part = "OFFSET ".(int) $offset;
1714
		} else {
1715
			$offset_query_part = "";
1716
		}
1717
1718
		if ($start_ts) {
1719
			$start_ts_formatted = date("Y/m/d H:i:s", strtotime($start_ts));
1720
			$start_ts_query_part = "date_entered >= '$start_ts_formatted' AND";
1721
		} else {
1722
			$start_ts_query_part = "";
1723
		}
1724
1725
		if (is_numeric($feed)) {
1726
			// proper override_order applied above
1727
			if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) {
1728
1729
				if (!(in_array($feed, Feeds::NEVER_GROUP_BY_DATE) && !$cat_view)) {
1730
					$yyiw_desc = $order_by == "date_reverse" ? "" : "desc";
1731
					$yyiw_order_qpart = "yyiw $yyiw_desc, ";
1732
				} else {
1733
					$yyiw_order_qpart = "";
1734
				}
1735
1736
				if (!$override_order) {
1737
					$order_by = "$yyiw_order_qpart ttrss_feeds.title, $order_by";
1738
				} else {
1739
					$order_by = "$yyiw_order_qpart ttrss_feeds.title, $override_order";
1740
				}
1741
			}
1742
1743
			if (!$allow_archived) {
1744
				$from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id),ttrss_feeds";
1745
				$feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND";
1746
1747
			} else {
1748
				$from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id)
1749
						LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)";
1750
			}
1751
1752
			if ($vfeed_query_part) {
1753
				$vfeed_query_part .= "favicon_avg_color,";
1754
			}
1755
1756
			$first_id = 0;
1757
			$first_id_query_strategy_part = $query_strategy_part;
1758
1759
			if ($feed == -3) {
1760
							$first_id_query_strategy_part = "true";
1761
			}
1762
1763
			if (DB_TYPE == "pgsql") {
1764
				$sanity_interval_qpart = "date_entered >= NOW() - INTERVAL '1 hour' AND";
1765
				$yyiw_qpart = "to_char(date_entered, 'IYYY-IW') AS yyiw";
1766
			} else {
1767
				$sanity_interval_qpart = "date_entered >= DATE_SUB(NOW(), INTERVAL 1 hour) AND";
1768
				$yyiw_qpart = "date_format(date_entered, '%Y-%u') AS yyiw";
1769
			}
1770
1771
			if (!$search && !$skip_first_id_check) {
1772
				// if previous topmost article id changed that means our current pagination is no longer valid
1773
				$query = "SELECT DISTINCT
1774
							ttrss_feeds.title,
1775
							date_entered,
1776
                            $yyiw_qpart,
1777
							guid,
1778
							ttrss_entries.id,
1779
							ttrss_entries.title,
1780
							updated,
1781
							score,
1782
							marked,
1783
							published,
1784
							last_marked,
1785
							last_published,
1786
							last_read
1787
						FROM
1788
							$from_qpart
1789
						WHERE
1790
						$feed_check_qpart
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $feed_check_qpart does not seem to be defined for all execution paths leading up to this point.
Loading history...
1791
						ttrss_user_entries.owner_uid = ".$pdo->quote($owner_uid)." AND
1792
						$search_query_part
1793
						$start_ts_query_part
1794
						$since_id_part
1795
						$sanity_interval_qpart
1796
						$first_id_query_strategy_part ORDER BY $order_by LIMIT 1";
1797
1798
				/*if ($_REQUEST["debug"]) {
1799
					print $query;
1800
				}*/
1801
1802
				$res = $pdo->query($query);
1803
1804
				if ($row = $res->fetch()) {
1805
					$first_id = (int) $row["id"];
1806
1807
					if ($offset > 0 && $first_id && $check_first_id && $first_id != $check_first_id) {
1808
						return array(-1, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id, $vfeed_query_part != "", $query_error_override);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $feed_site_url does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $last_updated does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $last_error does not seem to be defined for all execution paths leading up to this point.
Loading history...
1809
					}
1810
				}
1811
			}
1812
1813
			$query = "SELECT DISTINCT
1814
						date_entered,
1815
                        $yyiw_qpart,
1816
						guid,
1817
						ttrss_entries.id,ttrss_entries.title,
1818
						updated,
1819
						label_cache,
1820
						tag_cache,
1821
						always_display_enclosures,
1822
						site_url,
1823
						note,
1824
						num_comments,
1825
						comments,
1826
						int_id,
1827
						uuid,
1828
						lang,
1829
						hide_images,
1830
						unread,feed_id,marked,published,link,last_read,orig_feed_id,
1831
						last_marked, last_published,
1832
						$vfeed_query_part
1833
						$content_query_part
1834
						author,score
1835
					FROM
1836
						$from_qpart
1837
					WHERE
1838
					$feed_check_qpart
1839
					ttrss_user_entries.owner_uid = ".$pdo->quote($owner_uid)." AND
1840
					$search_query_part
1841
					$start_ts_query_part
1842
					$view_query_part
1843
					$since_id_part
1844
					$query_strategy_part ORDER BY $order_by
1845
					$limit_query_part $offset_query_part";
1846
1847
			//if ($_REQUEST["debug"]) print $query;
1848
1849
			$res = $pdo->query($query);
1850
1851
		} else {
1852
			// browsing by tag
1853
1854
			$query = "SELECT DISTINCT
1855
							date_entered,
1856
							guid,
1857
							note,
1858
							ttrss_entries.id as id,
1859
							title,
1860
							updated,
1861
							unread,
1862
							feed_id,
1863
							orig_feed_id,
1864
							marked,
1865
							published,
1866
							num_comments,
1867
							comments,
1868
							int_id,
1869
							tag_cache,
1870
							label_cache,
1871
							link,
1872
							lang,
1873
							uuid,
1874
							last_read,
1875
							(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images,
1876
							last_marked, last_published,
1877
							$since_id_part
1878
							$vfeed_query_part
1879
							$content_query_part
1880
							author, score
1881
						FROM ttrss_entries, ttrss_user_entries, ttrss_tags
1882
						WHERE
1883
							ref_id = ttrss_entries.id AND
1884
							ttrss_user_entries.owner_uid = ".$pdo->quote($owner_uid)." AND
1885
							post_int_id = int_id AND
1886
							tag_name = ".$pdo->quote($feed)." AND
1887
							$view_query_part
1888
							$search_query_part
1889
							$start_ts_query_part
1890
							$query_strategy_part ORDER BY $order_by
1891
							$limit_query_part $offset_query_part";
1892
1893
			if ($_REQUEST["debug"]) {
1894
				print $query;
1895
			}
1896
1897
			$res = $pdo->query($query);
1898
		}
1899
1900
		return array($res, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id, $vfeed_query_part != "", $query_error_override);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $first_id does not seem to be defined for all execution paths leading up to this point.
Loading history...
1901
1902
	}
1903
1904
	public static function getParentCategories($cat, $owner_uid) {
1905
		$rv = array();
1906
1907
		$pdo = Db::pdo();
1908
1909
		$sth = $pdo->prepare("SELECT parent_cat FROM ttrss_feed_categories
1910
			WHERE id = ? AND parent_cat IS NOT NULL AND owner_uid = ?");
1911
		$sth->execute([$cat, $owner_uid]);
1912
1913
		while ($line = $sth->fetch()) {
1914
			array_push($rv, $line["parent_cat"]);
1915
			$rv = array_merge($rv, Feeds::getParentCategories($line["parent_cat"], $owner_uid));
1916
		}
1917
1918
		return $rv;
1919
	}
1920
1921
	public static function getChildCategories($cat, $owner_uid) {
1922
		$rv = array();
1923
1924
		$pdo = Db::pdo();
1925
1926
		$sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1927
			WHERE parent_cat = ? AND owner_uid = ?");
1928
		$sth->execute([$cat, $owner_uid]);
1929
1930
		while ($line = $sth->fetch()) {
1931
			array_push($rv, $line["id"]);
1932
			$rv = array_merge($rv, Feeds::getChildCategories($line["id"], $owner_uid));
1933
		}
1934
1935
		return $rv;
1936
	}
1937
1938
	public static function getFeedCategory($feed) {
1939
		$pdo = Db::pdo();
1940
1941
		$sth = $pdo->prepare("SELECT cat_id FROM ttrss_feeds
1942
				WHERE id = ?");
1943
		$sth->execute([$feed]);
1944
1945
		if ($row = $sth->fetch()) {
1946
			return $row["cat_id"];
1947
		} else {
1948
			return false;
1949
		}
1950
1951
	}
1952
1953
	function color_of($name) {
1954
		$colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b",
1955
			"#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416",
1956
			"#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ];
1957
1958
		$sum = 0;
1959
1960
		for ($i = 0; $i < strlen($name); $i++) {
1961
			$sum += ord($name[$i]);
1962
		}
1963
1964
		$sum %= count($colormap);
1965
1966
		return $colormap[$sum];
1967
	}
1968
1969
	public static function get_feeds_from_html($url, $content) {
1970
		$url     = Feeds::fix_url($url);
1971
		$baseUrl = substr($url, 0, strrpos($url, '/') + 1);
1972
1973
		$feedUrls = [];
1974
1975
		$doc = new DOMDocument();
1976
		if ($doc->loadHTML($content)) {
1977
			$xpath = new DOMXPath($doc);
1978
			$entries = $xpath->query('/html/head/link[@rel="alternate" and '.
1979
				'(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
1980
1981
			foreach ($entries as $entry) {
1982
				if ($entry->hasAttribute('href')) {
1983
					$title = $entry->getAttribute('title');
1984
					if ($title == '') {
1985
						$title = $entry->getAttribute('type');
1986
					}
1987
					$feedUrl = rewrite_relative_url(
1988
						$baseUrl, $entry->getAttribute('href')
1989
					);
1990
					$feedUrls[$feedUrl] = $title;
1991
				}
1992
			}
1993
		}
1994
		return $feedUrls;
1995
	}
1996
1997
	public static function is_html($content) {
1998
		return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 8192)) !== 0;
1999
	}
2000
2001
	public static function validate_feed_url($url) {
2002
		$parts = parse_url($url);
2003
2004
		return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2005
	}
2006
2007
	/**
2008
	 * Fixes incomplete URLs by prepending "http://".
2009
	 * Also replaces feed:// with http://, and
2010
	 * prepends a trailing slash if the url is a domain name only.
2011
	 *
2012
	 * @param string $url Possibly incomplete URL
2013
	 *
2014
	 * @return string Fixed URL.
2015
	 */
2016
	public static function fix_url($url) {
2017
2018
		// support schema-less urls
2019
		if (strpos($url, '//') === 0) {
2020
			$url = 'https:'.$url;
2021
		}
2022
2023
		if (strpos($url, '://') === false) {
2024
			$url = 'http://'.$url;
2025
		} else if (substr($url, 0, 5) == 'feed:') {
2026
			$url = 'http:'.substr($url, 5);
2027
		}
2028
2029
		//prepend slash if the URL has no slash in it
2030
		// "http://www.example" -> "http://www.example/"
2031
		if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2032
			$url .= '/';
2033
		}
2034
2035
		//convert IDNA hostname to punycode if possible
2036
		if (function_exists("idn_to_ascii")) {
2037
			$parts = parse_url($url);
2038
			if (mb_detect_encoding($parts['host']) != 'ASCII')
2039
			{
2040
				$parts['host'] = idn_to_ascii($parts['host']);
2041
				$url = build_url($parts);
2042
			}
2043
		}
2044
2045
		if ($url != "http:///") {
2046
					return $url;
2047
		} else {
2048
					return '';
2049
		}
2050
	}
2051
2052
	public static function add_feed_category($feed_cat, $parent_cat_id = false, $order_id = 0) {
2053
2054
		if (!$feed_cat) {
2055
			return false;
2056
		}
2057
2058
		$feed_cat = mb_substr($feed_cat, 0, 250);
2059
		if (!$parent_cat_id) {
2060
			$parent_cat_id = null;
2061
		}
2062
2063
		$pdo = Db::pdo();
2064
		$tr_in_progress = false;
2065
2066
		try {
2067
			$pdo->beginTransaction();
2068
		} catch (Exception $e) {
2069
			$tr_in_progress = true;
2070
		}
2071
2072
		$sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2073
				WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2074
				AND title = :title AND owner_uid = :uid");
2075
		$sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2076
2077
		if (!$sth->fetch()) {
2078
2079
			$sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat,order_id)
2080
					VALUES (?, ?, ?, ?)");
2081
			$sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id, (int) $order_id]);
2082
2083
			if (!$tr_in_progress) {
2084
				$pdo->commit();
2085
			}
2086
2087
			return true;
2088
		}
2089
2090
		$pdo->commit();
2091
2092
		return false;
2093
	}
2094
2095
	public static function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2096
2097
		if (!$owner_uid) {
2098
			$owner_uid = $_SESSION["uid"];
2099
		}
2100
2101
		$is_cat = bool_to_sql_bool($is_cat);
2102
2103
		$pdo = Db::pdo();
2104
2105
		$sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2106
				WHERE feed_id = ? AND is_cat = ?
2107
				AND owner_uid = ?");
2108
		$sth->execute([$feed_id, $is_cat, $owner_uid]);
2109
2110
		if ($row = $sth->fetch()) {
2111
			return $row["access_key"];
2112
		} else {
2113
			$key = uniqid_short();
2114
2115
			$sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2116
					(access_key, feed_id, is_cat, owner_uid)
2117
					VALUES (?, ?, ?, ?)");
2118
2119
			$sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
2120
2121
			return $key;
2122
		}
2123
	}
2124
2125
	/**
2126
	 * Purge a feed old posts.
2127
	 *
2128
	 * @param mixed $link A database connection.
2129
	 * @param mixed $feed_id The id of the purged feed.
2130
	 * @param mixed $purge_interval Olderness of purged posts.
2131
	 * @param boolean $debug Set to True to enable the debug. False by default.
2132
	 * @access public
2133
	 * @return void
2134
	 */
2135
	public static function purge_feed($feed_id, $purge_interval) {
2136
2137
		if (!$purge_interval) {
2138
			$purge_interval = Feeds::feed_purge_interval($feed_id);
2139
		}
2140
2141
		$pdo = Db::pdo();
2142
2143
		$sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
2144
		$sth->execute([$feed_id]);
2145
2146
		$owner_uid = false;
2147
2148
		if ($row = $sth->fetch()) {
2149
			$owner_uid = $row["owner_uid"];
2150
		}
2151
2152
		if ($purge_interval == -1 || !$purge_interval) {
2153
			if ($owner_uid) {
2154
				CCache::update($feed_id, $owner_uid);
2155
			}
2156
			return;
2157
		}
2158
2159
		if (!$owner_uid) {
2160
			return;
2161
		}
2162
2163
		if (FORCE_ARTICLE_PURGE == 0) {
0 ignored issues
show
Bug introduced by
The constant FORCE_ARTICLE_PURGE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
2164
			$purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
2165
				$owner_uid, false);
2166
		} else {
2167
			$purge_unread = true;
2168
			$purge_interval = FORCE_ARTICLE_PURGE;
2169
		}
2170
2171
		if (!$purge_unread) {
2172
					$query_limit = " unread = false AND ";
2173
		} else {
2174
					$query_limit = "";
2175
		}
2176
2177
		$purge_interval = (int) $purge_interval;
2178
2179
		if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
2180
			$sth = $pdo->prepare("DELETE FROM ttrss_user_entries
2181
				USING ttrss_entries
2182
				WHERE ttrss_entries.id = ref_id AND
2183
				marked = false AND
2184
				feed_id = ? AND
2185
				$query_limit
2186
				ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
2187
			$sth->execute([$feed_id]);
2188
2189
		} else {
2190
			$sth = $pdo->prepare("DELETE FROM ttrss_user_entries
2191
				USING ttrss_user_entries, ttrss_entries
2192
				WHERE ttrss_entries.id = ref_id AND
2193
				marked = false AND
2194
				feed_id = ? AND
2195
				$query_limit
2196
				ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
2197
			$sth->execute([$feed_id]);
2198
2199
		}
2200
2201
		$rows = $sth->rowCount();
2202
2203
		CCache::update($feed_id, $owner_uid);
2204
2205
		Debug::log("Purged feed $feed_id ($purge_interval): deleted $rows articles");
2206
2207
		return $rows;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $rows returns the type integer which is incompatible with the documented return type void.
Loading history...
2208
	}
2209
2210
	public static function feed_purge_interval($feed_id) {
2211
2212
		$pdo = DB::pdo();
2213
2214
		$sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
2215
			WHERE id = ?");
2216
		$sth->execute([$feed_id]);
2217
2218
		if ($row = $sth->fetch()) {
2219
			$purge_interval = $row["purge_interval"];
2220
			$owner_uid = $row["owner_uid"];
2221
2222
			if ($purge_interval == 0) {
2223
				$purge_interval = get_pref(
2224
				'PURGE_OLD_DAYS', $owner_uid);
2225
			}
2226
2227
			return $purge_interval;
2228
2229
		} else {
2230
			return -1;
2231
		}
2232
	}
2233
2234
	public static function search_to_sql($search, $search_language) {
2235
2236
		$keywords = str_getcsv(trim($search), " ");
2237
		$query_keywords = array();
2238
		$search_words = array();
2239
		$search_query_leftover = array();
2240
2241
		$pdo = Db::pdo();
2242
2243
		if ($search_language) {
2244
					$search_language = $pdo->quote(mb_strtolower($search_language));
2245
		} else {
2246
					$search_language = $pdo->quote("english");
2247
		}
2248
2249
		foreach ($keywords as $k) {
2250
			if (strpos($k, "-") === 0) {
2251
				$k = substr($k, 1);
2252
				$not = "NOT";
2253
			} else {
2254
				$not = "";
2255
			}
2256
2257
			$commandpair = explode(":", mb_strtolower($k), 2);
2258
2259
			switch ($commandpair[0]) {
2260
			case "title":
2261
				if ($commandpair[1]) {
2262
					array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
2263
						$pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
2264
				} else {
2265
					array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
2266
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2267
					array_push($search_words, $k);
2268
				}
2269
				break;
2270
			case "author":
2271
				if ($commandpair[1]) {
2272
					array_push($query_keywords, "($not (LOWER(author) LIKE ".
2273
						$pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
2274
				} else {
2275
					array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
2276
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2277
					array_push($search_words, $k);
2278
				}
2279
				break;
2280
			case "note":
2281
				if ($commandpair[1]) {
2282
					if ($commandpair[1] == "true")
2283
						array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
2284
					else if ($commandpair[1] == "false")
2285
						array_push($query_keywords, "($not (note IS NULL OR note = ''))");
2286
					else
2287
						array_push($query_keywords, "($not (LOWER(note) LIKE ".
2288
							$pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
2289
				} else {
2290
					array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
2291
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2292
					if (!$not) array_push($search_words, $k);
2293
				}
2294
				break;
2295
			case "star":
2296
2297
				if ($commandpair[1]) {
2298
					if ($commandpair[1] == "true")
2299
						array_push($query_keywords, "($not (marked = true))");
2300
					else
2301
						array_push($query_keywords, "($not (marked = false))");
2302
				} else {
2303
					array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
2304
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2305
					if (!$not) array_push($search_words, $k);
2306
				}
2307
				break;
2308
			case "pub":
2309
				if ($commandpair[1]) {
2310
					if ($commandpair[1] == "true")
2311
						array_push($query_keywords, "($not (published = true))");
2312
					else
2313
						array_push($query_keywords, "($not (published = false))");
2314
2315
				} else {
2316
					array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
2317
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2318
					if (!$not) array_push($search_words, $k);
2319
				}
2320
				break;
2321
			case "unread":
2322
				if ($commandpair[1]) {
2323
					if ($commandpair[1] == "true")
2324
						array_push($query_keywords, "($not (unread = true))");
2325
					else
2326
						array_push($query_keywords, "($not (unread = false))");
2327
2328
				} else {
2329
					array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
2330
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2331
					if (!$not) array_push($search_words, $k);
2332
				}
2333
				break;
2334
			default:
2335
				if (strpos($k, "@") === 0) {
2336
2337
					$user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
2338
					$orig_ts = strtotime(substr($k, 1));
2339
					$k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
2340
2341
					//$k = date("Y-m-d", strtotime(substr($k, 1)));
2342
2343
					array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
2344
				} else {
2345
2346
					if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
2347
						$k = mb_strtolower($k);
2348
						array_push($search_query_leftover, $not ? "!$k" : $k);
2349
					} else {
2350
						array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
2351
								OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
2352
					}
2353
2354
					if (!$not) array_push($search_words, $k);
2355
				}
2356
			}
2357
		}
2358
2359
		if (count($search_query_leftover) > 0) {
2360
2361
			if (DB_TYPE == "pgsql") {
2362
2363
				// if there's no joiners consider this a "simple" search and
2364
				// concatenate everything with &, otherwise don't try to mess with tsquery syntax
2365
				if (preg_match("/[&|]/", implode(" ", $search_query_leftover))) {
2366
					$tsquery = $pdo->quote(implode(" ", $search_query_leftover));
2367
				} else {
2368
					$tsquery = $pdo->quote(implode(" & ", $search_query_leftover));
2369
				}
2370
2371
				array_push($query_keywords,
2372
					"(tsvector_combined @@ to_tsquery($search_language, $tsquery))");
2373
			}
2374
2375
		}
2376
2377
		$search_query_part = implode("AND", $query_keywords);
2378
2379
		return array($search_query_part, $search_words);
2380
	}
2381
}
2382
2383