]> git.wh0rd.org - tt-rss.git/blame - classes/rssutils.php
rssutils: PDO
[tt-rss.git] / classes / rssutils.php
CommitLineData
2c08214a 1<?php
e6c886bf
AD
2class RSSUtils {
3 static function calculate_article_hash($article, $pluginhost) {
af244f92
AD
4 $tmp = "";
5
6 foreach ($article as $k => $v) {
7 if ($k != "feed" && isset($v)) {
24e6ff5d
AD
8 $x = strip_tags(is_array($v) ? implode(",", $v) : $v);
9
10 //_debug("$k:" . sha1($x) . ":" . htmlspecialchars($x), true);
11
12 $tmp .= sha1("$k:" . sha1($x));
af244f92
AD
13 }
14 }
15
eb16bd9f 16 return sha1(implode(",", $pluginhost->get_plugin_names()) . $tmp);
b1840673
AD
17 }
18
e6c886bf 19 static function update_feedbrowser_cache() {
79178062 20
afcb105f
AD
21 $pdo = Db::pdo();
22
23 $sth = $pdo->query("SELECT feed_url, site_url, title, COUNT(id) AS subscribers
45378752
LD
24 FROM ttrss_feeds WHERE feed_url NOT IN (SELECT feed_url FROM ttrss_feeds
25 WHERE private IS true OR auth_login != '' OR auth_pass != '' OR feed_url LIKE '%:%@%/%')
79178062
AD
26 GROUP BY feed_url, site_url, title ORDER BY subscribers DESC LIMIT 1000");
27
afcb105f 28 $pdo->beginTransaction();
79178062 29
afcb105f 30 $pdo->query("DELETE FROM ttrss_feedbrowser_cache");
79178062
AD
31
32 $count = 0;
33
afcb105f
AD
34 while ($line = $sth->fetch()) {
35
0567016b
AD
36 $subscribers = $line["subscribers"];
37 $feed_url = $line["feed_url"];
38 $title = $line["title"];
39 $site_url = $line["site_url"];
79178062 40
afcb105f
AD
41 $tmph = $pdo->prepare("SELECT subscribers FROM
42 ttrss_feedbrowser_cache WHERE feed_url = ?");
43 $tmph->execute([$feed_url]);
44
45 if (!$tmph->fetch()) {
79178062 46
afcb105f
AD
47 $tmph = $pdo->prepare("INSERT INTO ttrss_feedbrowser_cache
48 (feed_url, site_url, title, subscribers)
49 VALUES
50 (?, ?, ?, ?)");
79178062 51
afcb105f 52 $tmph->execute([$feed_url, $site_url, $title, $subscribers]);
79178062
AD
53
54 ++$count;
55
56 }
57
58 }
59
afcb105f 60 $pdo->commit();
79178062
AD
61
62 return $count;
63
64 }
65
e6c886bf 66 static function update_daemon_common($limit = DAEMON_FEED_LIMIT, $debug = true) {
6322ac79 67 $schema_version = get_schema_version();
857efe49
AD
68
69 if ($schema_version != SCHEMA_VERSION) {
70 die("Schema version is wrong, please upgrade the database.\n");
71 }
72
afcb105f
AD
73 $pdo = Db::pdo();
74
09e8bdfd 75 if (!SINGLE_USER_MODE && DAEMON_UPDATE_LOGIN_LIMIT > 0) {
2c08214a
AD
76 if (DB_TYPE == "pgsql") {
77 $login_thresh_qpart = "AND ttrss_users.last_login >= NOW() - INTERVAL '".DAEMON_UPDATE_LOGIN_LIMIT." days'";
78 } else {
79 $login_thresh_qpart = "AND ttrss_users.last_login >= DATE_SUB(NOW(), INTERVAL ".DAEMON_UPDATE_LOGIN_LIMIT." DAY)";
80 }
81 } else {
82 $login_thresh_qpart = "";
83 }
84
2c08214a
AD
85 if (DB_TYPE == "pgsql") {
86 $update_limit_qpart = "AND ((
87 ttrss_feeds.update_interval = 0
ee0542ce 88 AND ttrss_user_prefs.value != '-1'
2c08214a
AD
89 AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_user_prefs.value || ' minutes') AS INTERVAL)
90 ) OR (
91 ttrss_feeds.update_interval > 0
92 AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_feeds.update_interval || ' minutes') AS INTERVAL)
f08426e3
AD
93 ) OR (ttrss_feeds.last_updated IS NULL
94 AND ttrss_user_prefs.value != '-1')
95 OR (last_updated = '1970-01-01 00:00:00'
96 AND ttrss_user_prefs.value != '-1'))";
2c08214a
AD
97 } else {
98 $update_limit_qpart = "AND ((
99 ttrss_feeds.update_interval = 0
ee0542ce 100 AND ttrss_user_prefs.value != '-1'
2c08214a
AD
101 AND ttrss_feeds.last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(ttrss_user_prefs.value, SIGNED INTEGER) MINUTE)
102 ) OR (
103 ttrss_feeds.update_interval > 0
104 AND ttrss_feeds.last_updated < DATE_SUB(NOW(), INTERVAL ttrss_feeds.update_interval MINUTE)
f08426e3
AD
105 ) OR (ttrss_feeds.last_updated IS NULL
106 AND ttrss_user_prefs.value != '-1')
107 OR (last_updated = '1970-01-01 00:00:00'
108 AND ttrss_user_prefs.value != '-1'))";
2c08214a
AD
109 }
110
111 // Test if feed is currently being updated by another process.
112 if (DB_TYPE == "pgsql") {
566417c4 113 $updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < NOW() - INTERVAL '10 minutes')";
2c08214a 114 } else {
566417c4 115 $updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < DATE_SUB(NOW(), INTERVAL 10 MINUTE))";
2c08214a
AD
116 }
117
93af11cb 118 $query_limit = $limit ? sprintf("LIMIT %d", $limit) : "";
2c08214a 119
98070db0
TK
120 // Update the least recently updated feeds first
121 $query_order = "ORDER BY last_updated";
122 if (DB_TYPE == "pgsql") $query_order .= " NULLS FIRST";
123
fce451a4 124 $query = "SELECT DISTINCT ttrss_feeds.feed_url, ttrss_feeds.last_updated
2c08214a
AD
125 FROM
126 ttrss_feeds, ttrss_users, ttrss_user_prefs
f4ae0f05 127 WHERE
2c08214a 128 ttrss_feeds.owner_uid = ttrss_users.id
f08426e3 129 AND ttrss_user_prefs.profile IS NULL
2c08214a
AD
130 AND ttrss_users.id = ttrss_user_prefs.owner_uid
131 AND ttrss_user_prefs.pref_name = 'DEFAULT_UPDATE_INTERVAL'
132 $login_thresh_qpart $update_limit_qpart
1c4421fc 133 $updstart_thresh_qpart
98070db0 134 $query_order $query_limit";
fce451a4 135
afcb105f 136 $res = $pdo->query($query);
2c08214a 137
2c08214a 138 $feeds_to_update = array();
afcb105f 139 while ($line = $res->fetch()) {
93af11cb 140 array_push($feeds_to_update, $line['feed_url']);
2c08214a
AD
141 }
142
afcb105f
AD
143 if ($debug) _debug(sprintf("Scheduled %d feeds to update...", count($feeds_to_update)));
144
93af11cb
AD
145 // Update last_update_started before actually starting the batch
146 // in order to minimize collision risk for parallel daemon tasks
147 if (count($feeds_to_update) > 0) {
afcb105f 148 $feeds_qmarks = arr_qmarks($feeds_to_update);
1c4421fc 149
afcb105f
AD
150 $tmph = $pdo->prepare("UPDATE ttrss_feeds SET last_update_started = NOW()
151 WHERE feed_url IN ($feeds_qmarks)");
152 $tmph->execute($feeds_to_update);
2c08214a
AD
153 }
154
8292d05b 155 $nf = 0;
2d9c5684 156 $bstarted = microtime(true);
8292d05b 157
5cbd1fe8
AD
158 $batch_owners = array();
159
afcb105f
AD
160 // since we have the data cached, we can deal with other feeds with the same url
161 $usth = $pdo->prepare("SELECT DISTINCT ttrss_feeds.id,last_updated,ttrss_feeds.owner_uid
ee0542ce
AD
162 FROM ttrss_feeds, ttrss_users, ttrss_user_prefs WHERE
163 ttrss_user_prefs.owner_uid = ttrss_feeds.owner_uid AND
164 ttrss_users.id = ttrss_user_prefs.owner_uid AND
165 ttrss_user_prefs.pref_name = 'DEFAULT_UPDATE_INTERVAL' AND
f08426e3 166 ttrss_user_prefs.profile IS NULL AND
afcb105f 167 feed_url = ?
9e84bab4 168 $update_limit_qpart
1c4421fc 169 $login_thresh_qpart
5929a0c1 170 ORDER BY ttrss_feeds.id $query_limit");
1c4421fc 171
afcb105f
AD
172 foreach ($feeds_to_update as $feed) {
173 if($debug) _debug("Base feed: $feed");
174
175 $usth->execute([$feed]);
176 //update_rss_feed($line["id"], true);
177
178 if ($tline = $usth->fetch()) {
179 if ($debug) _debug(" => " . $tline["last_updated"] . ", " . $tline["id"] . " " . $tline["owner_uid"]);
f08426e3 180
afcb105f
AD
181 if (array_search($tline["owner_uid"], $batch_owners) === FALSE)
182 array_push($batch_owners, $tline["owner_uid"]);
5cbd1fe8 183
afcb105f
AD
184 $fstarted = microtime(true);
185 RSSUtils::update_rss_feed($tline["id"], true, false);
186 _debug_suppress(false);
2d9c5684 187
afcb105f 188 _debug(sprintf(" %.4f (sec)", microtime(true) - $fstarted));
2d9c5684 189
afcb105f 190 ++$nf;
1c4421fc 191 }
2c08214a
AD
192 }
193
2d9c5684
AD
194 if ($nf > 0) {
195 _debug(sprintf("Processed %d feeds in %.4f (sec), %.4f (sec/feed avg)", $nf,
196 microtime(true) - $bstarted, (microtime(true) - $bstarted) / $nf));
197 }
198
5cbd1fe8
AD
199 foreach ($batch_owners as $owner_uid) {
200 _debug("Running housekeeping tasks for user $owner_uid...");
201
e6c886bf 202 RSSUtils::housekeeping_user($owner_uid);
5cbd1fe8
AD
203 }
204
2c08214a 205 // Send feed digests by email if needed.
c2f0f24e 206 Digest::send_headlines_digests($debug);
2c08214a 207
8292d05b 208 return $nf;
7b55001e 209 }
2c08214a 210
6022776d 211 // this is used when subscribing
e6c886bf 212 static function set_basic_feed_info($feed) {
6022776d 213
0567016b 214 $pdo = Db::pdo();
6022776d 215
0567016b
AD
216 $sth = $pdo->prepare("SELECT owner_uid,feed_url,auth_pass,auth_login,auth_pass_encrypted
217 FROM ttrss_feeds WHERE id = ?");
218 $sth->execute([$feed]);
6022776d 219
0567016b 220 if ($row = $sth->fetch()) {
bec5ba93 221
0567016b 222 $owner_uid = $row["owner_uid"];
6022776d 223
0567016b 224 $auth_pass_encrypted = sql_bool_to_bool($row["auth_pass_encrypted"]);
6022776d 225
0567016b
AD
226 $auth_login = $row["auth_login"];
227 $auth_pass = $row["auth_pass"];
6022776d 228
0567016b
AD
229 if ($auth_pass_encrypted && function_exists("mcrypt_decrypt")) {
230 require_once "crypt.php";
231 $auth_pass = decrypt_string($auth_pass);
232 }
6022776d 233
0567016b 234 $fetch_url = $row["feed_url"];
6022776d 235
0567016b
AD
236 $pluginhost = new PluginHost();
237 $user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
6022776d 238
0567016b
AD
239 $pluginhost->load(PLUGINS, PluginHost::KIND_ALL);
240 $pluginhost->load($user_plugins, PluginHost::KIND_USER, $owner_uid);
241 $pluginhost->load_data();
242
243 $basic_info = array();
244 foreach ($pluginhost->get_hooks(PluginHost::HOOK_FEED_BASIC_INFO) as $plugin) {
245 $basic_info = $plugin->hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed, $auth_login, $auth_pass);
246 }
6022776d 247
0567016b
AD
248 if (!$basic_info) {
249 $feed_data = fetch_file_contents($fetch_url, false,
250 $auth_login, $auth_pass, false,
251 FEED_FETCH_TIMEOUT,
252 0);
bec5ba93 253
0567016b 254 global $fetch_curl_used;
bec5ba93 255
0567016b
AD
256 if (!$fetch_curl_used) {
257 $tmp = @gzdecode($feed_data);
bec5ba93 258
0567016b
AD
259 if ($tmp) $feed_data = $tmp;
260 }
6022776d 261
0567016b 262 $feed_data = trim($feed_data);
6022776d 263
0567016b
AD
264 $rss = new FeedParser($feed_data);
265 $rss->init();
6022776d 266
0567016b
AD
267 if (!$rss->error()) {
268 $basic_info = array(
269 'title' => mb_substr($rss->get_title(), 0, 199),
270 'site_url' => mb_substr(rewrite_relative_url($fetch_url, $rss->get_link()), 0, 245)
271 );
272 }
3476690c 273 }
6022776d 274
0567016b
AD
275 if ($basic_info && is_array($basic_info)) {
276 $sth = $pdo->prepare("SELECT title, site_url FROM ttrss_feeds WHERE id = ?");
277 $sth->execute([$feed]);
6022776d 278
0567016b 279 if ($row = $sth->fetch()) {
6022776d 280
0567016b
AD
281 $registered_title = $row["title"];
282 $orig_site_url = $row["site_url"];
283
284 if ($basic_info['title'] && (!$registered_title || $registered_title == "[Unknown]")) {
285
286 $sth = $pdo->prepare("UPDATE ttrss_feeds SET
287 title = ? WHERE id = ?");
288 $sth->execute([$basic_info['title'], $feed]);
289 }
6022776d 290
0567016b
AD
291 if ($basic_info['site_url'] && $orig_site_url != $basic_info['site_url']) {
292 $sth = $pdo->prepare("UPDATE ttrss_feeds SET
293 site_url = ? WHERE id = ?");
294 $sth->execute([$basic_info['site_url'], $feed]);
295 }
296
297 }
6022776d
AD
298 }
299 }
300 }
301
7b55001e 302 /**
e6c886bf
AD
303 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
304 */
305 static function update_rss_feed($feed, $no_cache = false) {
2c08214a 306
2c08214a
AD
307 $debug_enabled = defined('DAEMON_EXTENDED_DEBUG') || $_REQUEST['xdebug'];
308
4f71d743 309 _debug_suppress(!$debug_enabled);
68cccafc 310 _debug("start", $debug_enabled);
2c08214a 311
0567016b
AD
312 $pdo = Db::pdo();
313
314 $sth = $pdo->prepare("SELECT title FROM ttrss_feeds WHERE id = ?");
315 $sth->execute([$feed]);
bfe1eb4e 316
0567016b 317 if (!$row = $sth->fetch()) {
bfe1eb4e
AD
318 _debug("feed $feed NOT FOUND/SKIPPED", $debug_enabled);
319 user_error("Attempt to update unknown/invalid feed $feed", E_USER_WARNING);
320 return false;
321 }
322
0567016b 323 $title = $row["title"];
6bb96beb
AD
324
325 // feed was batch-subscribed or something, we need to get basic info
326 // this is not optimal currently as it fetches stuff separately TODO: optimize
327 if ($title == "[Unknown]") {
328 _debug("setting basic feed info for $feed...");
e6c886bf 329 RSSUtils::set_basic_feed_info($feed);
6bb96beb
AD
330 }
331
0567016b 332 $sth = $pdo->prepare("SELECT id,update_interval,auth_login,
5ba1ddd4 333 feed_url,auth_pass,cache_images,
5321e775 334 mark_unread_on_update, owner_uid,
153cb6d3 335 auth_pass_encrypted, feed_language,
e50c8eaa
AD
336 last_modified,
337 ".SUBSTRING_FOR_DATE."(last_unconditional, 1, 19) AS last_unconditional
0567016b
AD
338 FROM ttrss_feeds WHERE id = ?");
339 $sth->execute([$feed]);
340
341 if ($row = $sth->fetch()) {
2c08214a 342
0567016b
AD
343 $owner_uid = $row["owner_uid"];
344 $mark_unread_on_update = sql_bool_to_bool($row["mark_unread_on_update"]);
345 $auth_pass_encrypted = sql_bool_to_bool($row["auth_pass_encrypted"]);
2c08214a 346
0567016b
AD
347 $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_update_started = NOW()
348 WHERE id = ?");
349 $sth->execute([$feed]);
2c08214a 350
0567016b
AD
351 $auth_login = $row["auth_login"];
352 $auth_pass = $row["auth_pass"];
2c08214a 353
0567016b
AD
354 if ($auth_pass_encrypted && function_exists("mcrypt_decrypt")) {
355 require_once "crypt.php";
356 $auth_pass = decrypt_string($auth_pass);
357 }
044cff2d 358
0567016b
AD
359 $stored_last_modified = $row["last_modified"];
360 $last_unconditional = $row["last_unconditional"];
361 $cache_images = sql_bool_to_bool($row["cache_images"]);
362 $fetch_url = $row["feed_url"];
363 $feed_language = mb_strtolower($row["feed_language"]);
364 if (!$feed_language) $feed_language = 'english';
2c08214a 365
0567016b
AD
366 } else {
367 return false;
368 }
2c08214a 369
f074ffe9 370 $date_feed_processed = date('Y-m-d H:i');
2c08214a 371
865a3ed6 372 $cache_filename = CACHE_DIR . "/simplepie/" . sha1($fetch_url) . ".xml";
f074ffe9 373
ee65bef4
AD
374 $pluginhost = new PluginHost();
375 $pluginhost->set_debug($debug_enabled);
376 $user_plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
377
378 $pluginhost->load(PLUGINS, PluginHost::KIND_ALL);
379 $pluginhost->load($user_plugins, PluginHost::KIND_USER, $owner_uid);
380 $pluginhost->load_data();
381
7b55001e 382 $rss_hash = false;
4f9cbdff 383
7b55001e
AD
384 $force_refetch = isset($_REQUEST["force_refetch"]);
385 $feed_data = "";
687a4f59 386
7b55001e
AD
387 foreach ($pluginhost->get_hooks(PluginHost::HOOK_FETCH_FEED) as $plugin) {
388 $feed_data = $plugin->hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, 0, $auth_login, $auth_pass);
389 }
2c08214a 390
7b55001e
AD
391 // try cache
392 if (!$feed_data &&
393 file_exists($cache_filename) &&
394 is_readable($cache_filename) &&
395 !$auth_login && !$auth_pass &&
396 filemtime($cache_filename) > time() - 30) {
be574731 397
7b55001e 398 _debug("using local cache [$cache_filename].", $debug_enabled);
52637d3b 399
7b55001e 400 @$feed_data = file_get_contents($cache_filename);
f074ffe9 401
7b55001e
AD
402 if ($feed_data) {
403 $rss_hash = sha1($feed_data);
88edaa93 404 }
ee65bef4 405
7b55001e
AD
406 } else {
407 _debug("local cache will not be used for this feed", $debug_enabled);
408 }
312742db 409
153cb6d3
AD
410 global $fetch_last_modified;
411
7b55001e
AD
412 // fetch feed from source
413 if (!$feed_data) {
e50c8eaa 414 _debug("last unconditional update request: $last_unconditional");
312742db 415
7b55001e
AD
416 if (ini_get("open_basedir") && function_exists("curl_init")) {
417 _debug("not using CURL due to open_basedir restrictions");
418 }
3f6f0857 419
e50c8eaa
AD
420 if (time() - strtotime($last_unconditional) > MAX_CONDITIONAL_INTERVAL) {
421 _debug("maximum allowed interval for conditional requests exceeded, forcing refetch");
422
423 $force_refetch = true;
424 } else {
425 _debug("stored last modified for conditional request: $stored_last_modified", $debug_enabled);
426 }
153cb6d3 427
e50c8eaa 428 _debug("fetching [$fetch_url] (force_refetch: $force_refetch)...", $debug_enabled);
153cb6d3
AD
429
430 $feed_data = fetch_file_contents([
431 "url" => $fetch_url,
432 "login" => $auth_login,
433 "pass" => $auth_pass,
434 "timeout" => $no_cache ? FEED_FETCH_NO_CACHE_TIMEOUT : FEED_FETCH_TIMEOUT,
435 "last_modified" => $force_refetch ? "" : $stored_last_modified
436 ]);
3f6f0857 437
7b55001e 438 global $fetch_curl_used;
3f6f0857 439
7b55001e
AD
440 if (!$fetch_curl_used) {
441 $tmp = @gzdecode($feed_data);
1367bc3f 442
7b55001e
AD
443 if ($tmp) $feed_data = $tmp;
444 }
017401dd 445
7b55001e 446 $feed_data = trim($feed_data);
fd687300 447
7b55001e 448 _debug("fetch done.", $debug_enabled);
9d930af9 449 _debug("source last modified: " . $fetch_last_modified, $debug_enabled);
153cb6d3
AD
450
451 if ($feed_data && $fetch_last_modified != $stored_last_modified) {
0567016b
AD
452 $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_modified = ? WHERE id = ?");
453 $sth->execute([substr($fetch_last_modified, 0, 245), $feed]);
153cb6d3 454 }
95beaa14 455
7b55001e
AD
456 // cache vanilla feed data for re-use
457 if ($feed_data && !$auth_pass && !$auth_login && is_writable(CACHE_DIR . "/simplepie")) {
458 $new_rss_hash = sha1($feed_data);
459
460 if ($new_rss_hash != $rss_hash) {
461 _debug("saving $cache_filename", $debug_enabled);
462 @file_put_contents($cache_filename, $feed_data);
95beaa14 463 }
4f9cbdff 464 }
7b55001e 465 }
017401dd 466
7b55001e
AD
467 if (!$feed_data) {
468 global $fetch_last_error;
469 global $fetch_last_error_code;
f074ffe9 470
7b55001e 471 _debug("unable to fetch: $fetch_last_error [$fetch_last_error_code]", $debug_enabled);
f074ffe9 472
7b55001e
AD
473 // If-Modified-Since
474 if ($fetch_last_error_code != 304) {
0567016b 475 $error_message = $fetch_last_error;
7b55001e
AD
476 } else {
477 _debug("source claims data not modified, nothing to do.", $debug_enabled);
0567016b 478 $error_message = "";
7b55001e 479 }
4f9cbdff 480
0567016b
AD
481 $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
482 last_updated = NOW() WHERE id = ?");
483 $sth->execute([$error_message, $feed]);
4f9cbdff 484
7b55001e 485 return;
f074ffe9
AD
486 }
487
1ffe3391 488 foreach ($pluginhost->get_hooks(PluginHost::HOOK_FEED_FETCHED) as $plugin) {
6791af0c 489 $feed_data = $plugin->hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed);
017401dd
AD
490 }
491
07d3431e
AD
492 $rss = new FeedParser($feed_data);
493 $rss->init();
2c08214a 494
0567016b 495 $feed = $feed;
2c08214a 496
19b3992b 497 if (!$rss->error()) {
2c08214a 498
d2a421e3 499 // We use local pluginhost here because we need to load different per-user feed plugins
1ffe3391 500 $pluginhost->run_hooks(PluginHost::HOOK_FEED_PARSED, "hook_feed_parsed", $rss);
4412b877 501
df659891 502 _debug("language: $feed_language", $debug_enabled);
68cccafc 503 _debug("processing feed data...", $debug_enabled);
2c08214a 504
382268c6
AD
505 if (DB_TYPE == "pgsql") {
506 $favicon_interval_qpart = "favicon_last_checked < NOW() - INTERVAL '12 hour'";
507 } else {
508 $favicon_interval_qpart = "favicon_last_checked < DATE_SUB(NOW(), INTERVAL 12 HOUR)";
509 }
510
0567016b 511 $sth = $pdo->prepare("SELECT owner_uid,favicon_avg_color,
382268c6
AD
512 (favicon_last_checked IS NULL OR $favicon_interval_qpart) AS
513 favicon_needs_check
0567016b
AD
514 FROM ttrss_feeds WHERE id = ?");
515 $sth->execute([$feed]);
2c08214a 516
0567016b
AD
517 if ($row = $sth->fetch()) {
518 $favicon_needs_check = sql_bool_to_bool($row["favicon_needs_check"]);
519 $favicon_avg_color = $row["favicon_avg_color"];
520 $owner_uid = $row["owner_uid"];
521 } else {
522 return false;
523 }
2c08214a 524
0567016b 525 $site_url = mb_substr(rewrite_relative_url($fetch_url, $rss->get_link()), 0, 245);
2c08214a 526
cd07592c
AD
527 _debug("site_url: $site_url", $debug_enabled);
528 _debug("feed_title: " . $rss->get_title(), $debug_enabled);
529
687a4f59 530 if ($favicon_needs_check || $force_refetch) {
36490f11
AD
531
532 /* terrible hack: if we crash on floicon shit here, we won't check
560cbd8c 533 * the icon avgcolor again (unless the icon got updated) */
36490f11 534
560cbd8c
AD
535 $favicon_file = ICONS_DIR . "/$feed.ico";
536 $favicon_modified = @filemtime($favicon_file);
537
68cccafc 538 _debug("checking favicon...", $debug_enabled);
687a4f59 539
e6c886bf 540 RSSUtils::check_feed_favicon($site_url, $feed);
560cbd8c
AD
541 $favicon_modified_new = @filemtime($favicon_file);
542
543 if ($favicon_modified_new > $favicon_modified)
544 $favicon_avg_color = '';
687a4f59 545
0567016b 546 $favicon_colorstring = "";
6ee0d4b0 547 if (file_exists($favicon_file) && function_exists("imagecreatefromstring") && $favicon_avg_color == '') {
e6c886bf 548 require_once "colors.php";
687a4f59 549
0567016b
AD
550 $sth = $pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = 'fail' WHERE
551 id = ?");
552 $sth->execute([$feed]);
aafd55ba 553
0567016b
AD
554 $favicon_color = calculate_avg_color($favicon_file);
555
556 $favicon_colorstring = ",favicon_avg_color = " . $pdo->quote($favicon_color);
63c323f7 557
36490f11 558 } else if ($favicon_avg_color == 'fail') {
84ceb6bd 559 _debug("floicon failed on this file, not trying to recalculate avg color", $debug_enabled);
6ac722d5 560 }
687a4f59 561
0567016b
AD
562 $sth = $pdo->prepare("UPDATE ttrss_feeds SET favicon_last_checked = NOW()
563 $favicon_colorstring WHERE id = ?");
564 $sth->execute([$feed]);
f2798eb6 565 }
2c08214a 566
68cccafc 567 _debug("loading filters & labels...", $debug_enabled);
2c08214a 568
a42c55f0 569 $filters = load_filters($feed, $owner_uid);
2c08214a 570
02f3992a
AD
571 if ($debug_enabled) {
572 print_r($filters);
573 }
574
68cccafc 575 _debug("" . count($filters) . " filters loaded.", $debug_enabled);
2c08214a 576
19b3992b 577 $items = $rss->get_items();
2c08214a 578
19b3992b 579 if (!is_array($items)) {
68cccafc 580 _debug("no articles found.", $debug_enabled);
2c08214a 581
0567016b
AD
582 $sth = $pdo->prepare("UPDATE ttrss_feeds
583 SET last_updated = NOW(), last_unconditional = NOW(), last_error = '' WHERE id = ?");
584 $sth->execute([$feed]);
2c08214a 585
0567016b 586 return true; // no articles
2c08214a
AD
587 }
588
68cccafc 589 _debug("processing articles...", $debug_enabled);
2c08214a 590
6c9f3d4a
AD
591 $tstart = time();
592
19b3992b 593 foreach ($items as $item) {
5d56d100 594 if ($_REQUEST['xdebug'] == 3) {
2c08214a
AD
595 print_r($item);
596 }
597
6c9f3d4a
AD
598 if (ini_get("max_execution_time") > 0 && time() - $tstart >= ini_get("max_execution_time") * 0.7) {
599 _debug("looks like there's too many articles to process at once, breaking out", $debug_enabled);
600 break;
601 }
602
0567016b
AD
603 $entry_guid = strip_tags($item->get_id());
604 if (!$entry_guid) $entry_guid = strip_tags($item->get_link());
e6c886bf 605 if (!$entry_guid) $entry_guid = RSSUtils::make_guid_from_title($item->get_title());
2c08214a
AD
606 if (!$entry_guid) continue;
607
3a4c8973
AD
608 $entry_guid = "$owner_uid,$entry_guid";
609
0567016b 610 $entry_guid_hashed = 'SHA1:' . sha1($entry_guid);
5e3d5480 611
68cccafc 612 _debug("guid $entry_guid / $entry_guid_hashed", $debug_enabled);
5e3d5480 613
0567016b 614 $entry_timestamp = strip_tags($item->get_date());
04d2f9c8
AD
615
616 _debug("orig date: " . $item->get_date(), $debug_enabled);
2c08214a 617
30123fe6 618 if ($entry_timestamp == -1 || !$entry_timestamp || $entry_timestamp > time()) {
2c08214a 619 $entry_timestamp = time();
2c08214a
AD
620 }
621
622 $entry_timestamp_fmt = strftime("%Y/%m/%d %H:%M:%S", $entry_timestamp);
623
68cccafc 624 _debug("date $entry_timestamp [$entry_timestamp_fmt]", $debug_enabled);
2c08214a 625
0567016b 626 $entry_title = strip_tags($item->get_title());
1b35d30c 627
5d56d100 628 $entry_link = rewrite_relative_url($site_url, $item->get_link());
2c08214a 629
68cccafc
AD
630 _debug("title $entry_title", $debug_enabled);
631 _debug("link $entry_link", $debug_enabled);
2c08214a
AD
632
633 if (!$entry_title) $entry_title = date("Y-m-d H:i:s", $entry_timestamp);;
634
19b3992b
AD
635 $entry_content = $item->get_content();
636 if (!$entry_content) $entry_content = $item->get_description();
2c08214a
AD
637
638 if ($_REQUEST["xdebug"] == 2) {
9ec10352 639 print "content: ";
0bc503ff 640 print htmlspecialchars($entry_content);
3c696512 641 print "\n";
2c08214a
AD
642 }
643
0567016b 644 $entry_comments = mb_substr(strip_tags($item->get_comments_url()), 0, 245);
12ff230b 645 $num_comments = (int) $item->get_comments_count();
2c08214a 646
0567016b
AD
647 $entry_author = strip_tags($item->get_author());
648 $entry_guid = mb_substr($entry_guid, 0, 245);
2c08214a 649
68cccafc
AD
650 _debug("author $entry_author", $debug_enabled);
651 _debug("num_comments: $num_comments", $debug_enabled);
ee78f81c 652 _debug("looking for tags...", $debug_enabled);
2c08214a
AD
653
654 // parse <category> entries into tags
655
656 $additional_tags = array();
657
19b3992b 658 $additional_tags_src = $item->get_categories();
2c08214a 659
19b3992b
AD
660 if (is_array($additional_tags_src)) {
661 foreach ($additional_tags_src as $tobj) {
cd07592c 662 array_push($additional_tags, $tobj);
2c08214a 663 }
19b3992b 664 }
2c08214a 665
fa6fbd36 666 $entry_tags = array_unique($additional_tags);
2c08214a
AD
667
668 for ($i = 0; $i < count($entry_tags); $i++)
669 $entry_tags[$i] = mb_strtolower($entry_tags[$i], 'utf-8');
670
ee78f81c
AD
671 _debug("tags found: " . join(",", $entry_tags), $debug_enabled);
672
68cccafc 673 _debug("done collecting data.", $debug_enabled);
2c08214a 674
0567016b
AD
675 $sth = $pdo->prepare("SELECT id, content_hash, lang FROM ttrss_entries
676 WHERE guid = ? OR guid = ?");
677 $sth->execute([$entry_guid, $entry_guid_hashed]);
b30abdad 678
0567016b
AD
679 if ($row = $sth->fetch()) {
680 $base_entry_id = $row["id"];
681 $entry_stored_hash = $row["content_hash"];
4a0da0e5 682 $article_labels = Article::get_article_labels($base_entry_id, $owner_uid);
0567016b 683 $entry_language = $row["lang"];
a8ac7661 684
2ed0d6c4 685 $existing_tags = Article::get_article_tags($base_entry_id, $owner_uid);
a8ac7661 686 $entry_tags = array_unique(array_merge($entry_tags, $existing_tags));
b30abdad 687 } else {
b1840673
AD
688 $base_entry_id = false;
689 $entry_stored_hash = "";
a29fe121 690 $article_labels = array();
3318d324 691 $entry_language = "";
b30abdad
AD
692 }
693
455b1401 694 $article = array("owner_uid" => $owner_uid, // read only
b30abdad 695 "guid" => $entry_guid, // read only
59e83455 696 "guid_hashed" => $entry_guid_hashed, // read only
19b3992b
AD
697 "title" => $entry_title,
698 "content" => $entry_content,
699 "link" => $entry_link,
a29fe121 700 "labels" => $article_labels, // current limitation: can add labels to article, can't remove them
19b3992b 701 "tags" => $entry_tags,
e02555c1 702 "author" => $entry_author,
c9299c28 703 "force_catchup" => false, // ugly hack for the time being
6de3a1be 704 "score_modifier" => 0, // no previous value, plugin should recalculate score modifier based on content if needed
3318d324 705 "language" => $entry_language,
20d2195f 706 "num_comments" => $num_comments, // read only
f73e03e0
AD
707 "feed" => array("id" => $feed,
708 "fetch_url" => $fetch_url,
babfadbf
J
709 "site_url" => $site_url,
710 "cache_images" => $cache_images)
e6c886bf 711 );
cc85704f 712
b1840673 713 $entry_plugin_data = "";
e6c886bf 714 $entry_current_hash = RSSUtils::calculate_article_hash($article, $pluginhost);
b1840673
AD
715
716 _debug("article hash: $entry_current_hash [stored=$entry_stored_hash]", $debug_enabled);
717
522e8b35 718 if ($entry_current_hash == $entry_stored_hash && !isset($_REQUEST["force_rehash"])) {
b1840673
AD
719 _debug("stored article seems up to date [IID: $base_entry_id], updating timestamp only", $debug_enabled);
720
721 // we keep encountering the entry in feeds, so we need to
722 // update date_updated column so that we don't get horrible
723 // dupes when the entry gets purged and reinserted again e.g.
724 // in the case of SLOW SLOW OMG SLOW updating feeds
725
0567016b
AD
726 $sth = $pdo->prepare("UPDATE ttrss_entries SET date_updated = NOW()
727 WHERE id = ?");
728 $sth->execute([$base_entry_id]);
b1840673 729
5bdcb8fd 730 continue;
b1840673
AD
731 }
732
733 _debug("hash differs, applying plugin filters:", $debug_enabled);
734
1ffe3391 735 foreach ($pluginhost->get_hooks(PluginHost::HOOK_ARTICLE_FILTER) as $plugin) {
b1840673
AD
736 _debug("... " . get_class($plugin), $debug_enabled);
737
738 $start = microtime(true);
19b3992b 739 $article = $plugin->hook_article_filter($article);
0084f0d1 740
b1840673
AD
741 _debug("=== " . sprintf("%.4f (sec)", microtime(true) - $start), $debug_enabled);
742
743 $entry_plugin_data .= mb_strtolower(get_class($plugin)) . ",";
cc85704f
AD
744 }
745
0bc503ff
AD
746 if ($_REQUEST["xdebug"] == 2) {
747 print "processed content: ";
748 print htmlspecialchars($article["content"]);
749 print "\n";
750 }
751
b1840673
AD
752 _debug("plugin data: $entry_plugin_data", $debug_enabled);
753
35c12dc4
AD
754 // Workaround: 4-byte unicode requires utf8mb4 in MySQL. See https://tt-rss.org/forum/viewtopic.php?f=1&t=3377&p=20077#p20077
755 if (DB_TYPE == "mysql") {
756 foreach ($article as $k => $v) {
35c37354
AD
757
758 // i guess we'll have to take the risk of 4byte unicode labels & tags here
dae16f72 759 if (is_string($article[$k])) {
35c37354
AD
760 $article[$k] = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $v);
761 }
35c12dc4
AD
762 }
763 }
764
b8774453
AD
765 /* Collect article tags here so we could filter by them: */
766
557d86fe
AD
767 $matched_rules = array();
768
e6c886bf 769 $article_filters = RSSUtils::get_article_filters($filters, $article["title"],
7b55001e 770 $article["content"], $article["link"], $article["author"],
557d86fe 771 $article["tags"], $matched_rules);
b8774453
AD
772
773 if ($debug_enabled) {
557d86fe
AD
774 _debug("matched filter rules: ", $debug_enabled);
775
776 if (count($matched_rules) != 0) {
777 print_r($matched_rules);
778 }
779
780 _debug("filter actions: ", $debug_enabled);
781
b8774453
AD
782 if (count($article_filters) != 0) {
783 print_r($article_filters);
784 }
785 }
786
e6c886bf 787 $plugin_filter_names = RSSUtils::find_article_filters($article_filters, "plugin");
b8774453
AD
788 $plugin_filter_actions = $pluginhost->get_filter_actions();
789
790 if (count($plugin_filter_names) > 0) {
791 _debug("applying plugin filter actions...", $debug_enabled);
792
793 foreach ($plugin_filter_names as $pfn) {
794 list($pfclass,$pfaction) = explode(":", $pfn["param"]);
795
796 if (isset($plugin_filter_actions[$pfclass])) {
797 $plugin = $pluginhost->get_plugin($pfclass);
798
799 _debug("... $pfclass: $pfaction", $debug_enabled);
800
801 if ($plugin) {
802 $start = microtime(true);
803 $article = $plugin->hook_article_filter_action($article, $pfaction);
804
805 _debug("=== " . sprintf("%.4f (sec)", microtime(true) - $start), $debug_enabled);
806 } else {
807 _debug("??? $pfclass: plugin object not found.");
808 }
809 } else {
810 _debug("??? $pfclass: filter plugin not registered.");
811 }
812 }
813 }
814
19b3992b 815 $entry_tags = $article["tags"];
0567016b
AD
816 $entry_title = strip_tags($article["title"]);
817 $entry_author = mb_substr(strip_tags($article["author"]), 0, 245);
818 $entry_link = strip_tags($article["link"]);
f935d98e 819 $entry_content = $article["content"]; // escaped below
c9299c28 820 $entry_force_catchup = $article["force_catchup"];
a29fe121 821 $article_labels = $article["labels"];
6de3a1be 822 $entry_score_modifier = (int) $article["score_modifier"];
0567016b 823 $entry_language = $article["language"];
a29fe121
AD
824
825 if ($debug_enabled) {
826 _debug("article labels:", $debug_enabled);
557d86fe
AD
827
828 if (count($article_labels) != 0) {
829 print_r($article_labels);
830 }
a29fe121 831 }
c9299c28
AD
832
833 _debug("force catchup: $entry_force_catchup");
f935d98e 834
0a3fd79b 835 if ($cache_images && is_writable(CACHE_DIR . '/images'))
e6c886bf 836 RSSUtils::cache_media($entry_content, $site_url, $debug_enabled);
0a3fd79b 837
0567016b
AD
838 $csth = $pdo->prepare("SELECT id FROM ttrss_entries
839 WHERE guid = ? OR guid = ?");
840 $csth->execute([$entry_guid, $entry_guid_hashed]);
9e222305 841
0567016b 842 if (!$row = $csth->fetch()) {
2c08214a 843
07d3431e 844 _debug("base guid [$entry_guid or $entry_guid_hashed] not found, creating...", $debug_enabled);
2c08214a
AD
845
846 // base post entry does not exist, create it
847
0567016b 848 $usth = $pdo->prepare(
2c08214a 849 "INSERT INTO ttrss_entries
0567016b 850 (title,
2c08214a
AD
851 guid,
852 link,
853 updated,
854 content,
855 content_hash,
856 no_orig_date,
857 date_updated,
858 date_entered,
859 comments,
860 num_comments,
b30abdad 861 plugin_data,
6b461797 862 lang,
2c08214a
AD
863 author)
864 VALUES
0567016b 865 (?, ?, ?, ?, ?, ?,
5ba1ddd4 866 false,
2c08214a 867 NOW(),
0567016b
AD
868 ?, ?, ?, ?, ?, ?)");
869
870 $usth->execute([$entry_title,
871 $entry_guid_hashed,
872 $entry_link,
873 $entry_timestamp_fmt,
874 $entry_content,
875 $entry_current_hash,
876 $date_feed_processed,
877 $entry_comments,
878 $num_comments,
879 $entry_plugin_data,
880 $entry_language,
881 $entry_author]);
e8291805 882
2c08214a
AD
883 }
884
0567016b 885 $csth->execute([$entry_guid, $entry_guid_hashed]);
2c08214a
AD
886
887 $entry_ref_id = 0;
888 $entry_int_id = 0;
889
0567016b 890 if ($row = $csth->fetch()) {
2c08214a 891
68cccafc 892 _debug("base guid found, checking for user record", $debug_enabled);
2c08214a 893
0567016b 894 $ref_id = $row['id'];
2c08214a
AD
895 $entry_ref_id = $ref_id;
896
e6c886bf 897 if (RSSUtils::find_article_filter($article_filters, "filter")) {
2c08214a
AD
898 continue;
899 }
900
e6c886bf 901 $score = RSSUtils::calculate_article_score($article_filters) + $entry_score_modifier;
2c08214a 902
6de3a1be 903 _debug("initial score: $score [including plugin modifier: $entry_score_modifier]", $debug_enabled);
2c08214a 904
4f186b1f
AD
905 // check for user post link to main table
906
0567016b
AD
907 $sth = $pdo->prepare("SELECT ref_id, int_id FROM ttrss_user_entries WHERE
908 ref_id = ? AND owner_uid = ?");
909 $sth->execute([$ref_id, $owner_uid]);
2c08214a
AD
910
911 // okay it doesn't exist - create user entry
0567016b
AD
912 if ($row = $sth->fetch()) {
913 $entry_ref_id = $row["ref_id"];
914 $entry_int_id = $row["int_id"];
2c08214a 915
0567016b
AD
916 _debug("user record FOUND: RID: $entry_ref_id, IID: $entry_int_id", $debug_enabled);
917 } else {
918
68cccafc 919 _debug("user record not found, creating...", $debug_enabled);
2c08214a 920
e6c886bf 921 if ($score >= -500 && !RSSUtils::find_article_filter($article_filters, 'catchup') && !$entry_force_catchup) {
0567016b
AD
922 $unread = 1;
923 $last_read_qpart = null;
2c08214a 924 } else {
0567016b 925 $unread = 0;
2c08214a
AD
926 $last_read_qpart = 'NOW()';
927 }
928
e6c886bf 929 if (RSSUtils::find_article_filter($article_filters, 'mark') || $score > 1000) {
0567016b 930 $marked = 1;
2c08214a 931 } else {
0567016b 932 $marked = 0;
2c08214a
AD
933 }
934
e6c886bf 935 if (RSSUtils::find_article_filter($article_filters, 'publish')) {
0567016b 936 $published = 1;
2c08214a 937 } else {
0567016b 938 $published = 0;
2c08214a
AD
939 }
940
0567016b
AD
941 $last_marked = ($marked == 'true') ? 'NOW()' : null;
942 $last_published = ($published == 'true') ? 'NOW()' : null;
7873d588 943
0567016b 944 $sth = $pdo->prepare(
2c08214a
AD
945 "INSERT INTO ttrss_user_entries
946 (ref_id, owner_uid, feed_id, unread, last_read, marked,
7873d588
AD
947 published, score, tag_cache, label_cache, uuid,
948 last_marked, last_published)
0567016b 949 VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', '', '', ?, ?)");
2c08214a 950
0567016b
AD
951 $sth->execute([$ref_id, $owner_uid, $feed, $unread, $last_read_qpart, $marked,
952 $published, $score, $last_marked, $last_published]);
2c08214a 953
0567016b
AD
954 $sth = $pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
955 ref_id = ? AND owner_uid = ? AND
956 feed_id = ? LIMIT 1");
2c08214a 957
0567016b
AD
958 $sth->execute([$ref_id, $owner_uid, $feed]);
959
960 if ($row = $sth->fetch())
961 $entry_int_id = $row['int_id'];
2c08214a
AD
962 }
963
0567016b 964 _debug("resulting RID: $entry_ref_id, IID: $entry_int_id", $debug_enabled);
2c08214a 965
e854442e 966 if (DB_TYPE == "pgsql") {
0567016b
AD
967 $tsvector_combined = mb_substr($entry_title . ' ' . strip_tags(str_replace('<', ' <', $entry_content)),
968 0, 1000000);
e854442e 969
0567016b 970 $tsvector_qpart = "tsvector_combined = to_tsvector('$feed_language', ".$pdo->quote($tsvector_combined)."),";
e854442e
AD
971
972 } else {
973 $tsvector_qpart = "";
974 }
975
0567016b
AD
976 $sth = $pdo->prepare("UPDATE ttrss_entries
977 SET title = ?,
978 content = ?,
979 content_hash = ?,
980 updated = ?,
e854442e 981 $tsvector_qpart
0567016b
AD
982 num_comments = ?,
983 plugin_data = ?,
984 author = ?,
985 lang = ?
986 WHERE id = ?");
987
988 $sth->execute([$entry_title, $entry_content, $entry_current_hash, $entry_timestamp_fmt,
989 $num_comments, $entry_plugin_data, $entry_author, $entry_language, $ref_id]);
b1840673 990
59e83455 991 // update aux data
0567016b
AD
992 $sth = $pdo->prepare("UPDATE ttrss_user_entries
993 SET score = ? WHERE ref_id = ?");
994 $sth->execute([$score, $ref_id]);
59e83455 995
b1840673 996 if ($mark_unread_on_update) {
24e6ff5d
AD
997 _debug("article updated, marking unread as requested.", $debug_enabled);
998
0567016b
AD
999 $sth = $pdo->prepare("UPDATE ttrss_user_entries
1000 SET last_read = null, unread = true WHERE ref_id = ?");
1001 $sth->execute([$ref_id]);
2c08214a
AD
1002 }
1003 }
1004
a29fe121
AD
1005 _debug("assigning labels [other]...", $debug_enabled);
1006
1007 foreach ($article_labels as $label) {
7c9b5a3f 1008 Labels::add_article($entry_ref_id, $label[1], $owner_uid);
a29fe121
AD
1009 }
1010
1011 _debug("assigning labels [filters]...", $debug_enabled);
2c08214a 1012
e6c886bf 1013 RSSUtils::assign_article_to_label_filters($entry_ref_id, $article_filters,
b24504b1 1014 $owner_uid, $article_labels);
2c08214a 1015
68cccafc 1016 _debug("looking for enclosures...", $debug_enabled);
2c08214a
AD
1017
1018 // enclosures
1019
1020 $enclosures = array();
1021
19b3992b 1022 $encs = $item->get_enclosures();
2c08214a 1023
19b3992b
AD
1024 if (is_array($encs)) {
1025 foreach ($encs as $e) {
1026 $e_item = array(
86e53429
AD
1027 rewrite_relative_url($site_url, $e->link),
1028 $e->type, $e->length, $e->title, $e->width, $e->height);
2c08214a 1029 array_push($enclosures, $e_item);
2c08214a
AD
1030 }
1031 }
1032
388d4dfa 1033 if ($cache_images && is_writable(CACHE_DIR . '/images'))
e6c886bf 1034 RSSUtils::cache_enclosures($enclosures, $site_url, $debug_enabled);
388d4dfa 1035
2c08214a 1036 if ($debug_enabled) {
68cccafc 1037 _debug("article enclosures:", $debug_enabled);
2c08214a
AD
1038 print_r($enclosures);
1039 }
1040
0567016b
AD
1041 $esth = $pdo->prepare("SELECT id FROM ttrss_enclosures
1042 WHERE content_url = ? AND post_id = ?");
2c08214a 1043
0567016b
AD
1044 $usth = $pdo->prepare("INSERT INTO ttrss_enclosures
1045 (content_url, content_type, title, duration, post_id, width, height) VALUES
1046 (?, ?, ?, ?, ?, ?, ?)");
5c54e683 1047
2c08214a 1048 foreach ($enclosures as $enc) {
0567016b
AD
1049 $enc_url = $enc[0];
1050 $enc_type = $enc[1];
1051 $enc_dur = $enc[2];
1052 $enc_title = $enc[3];
523bd90b
FE
1053 $enc_width = intval($enc[4]);
1054 $enc_height = intval($enc[5]);
2c08214a 1055
0567016b 1056 $esth->execute([$enc_url, $entry_ref_id]);
2c08214a 1057
0567016b
AD
1058 if (!$esth->fetch()) {
1059 $usth->execute([$enc_url, $enc_type, (string)$enc_title, $enc_dur, $entry_ref_id, $enc_width, $enc_height]);
2c08214a
AD
1060 }
1061 }
1062
2c08214a
AD
1063 // check for manual tags (we have to do it here since they're loaded from filters)
1064
1065 foreach ($article_filters as $f) {
6aff7845 1066 if ($f["type"] == "tag") {
2c08214a 1067
6aff7845 1068 $manual_tags = trim_array(explode(",", $f["param"]));
2c08214a
AD
1069
1070 foreach ($manual_tags as $tag) {
1071 if (tag_is_valid($tag)) {
1072 array_push($entry_tags, $tag);
1073 }
1074 }
1075 }
1076 }
1077
1078 // Skip boring tags
1079
6322ac79 1080 $boring_tags = trim_array(explode(",", mb_strtolower(get_pref(
2c08214a
AD
1081 'BLACKLISTED_TAGS', $owner_uid, ''), 'utf-8')));
1082
1083 $filtered_tags = array();
1084 $tags_to_cache = array();
1085
1086 if ($entry_tags && is_array($entry_tags)) {
1087 foreach ($entry_tags as $tag) {
1088 if (array_search($tag, $boring_tags) === false) {
1089 array_push($filtered_tags, $tag);
1090 }
1091 }
1092 }
1093
1094 $filtered_tags = array_unique($filtered_tags);
1095
1096 if ($debug_enabled) {
68cccafc 1097 _debug("filtered article tags:", $debug_enabled);
2c08214a
AD
1098 print_r($filtered_tags);
1099 }
1100
1101 // Save article tags in the database
1102
1103 if (count($filtered_tags) > 0) {
1104
0567016b
AD
1105 $tsth = $pdo->prepare("SELECT id FROM ttrss_tags
1106 WHERE tag_name = ? AND post_int_id = ? AND
1107 owner_uid = ? LIMIT 1");
1108
1109 $usth = $pdo->prepare("INSERT INTO ttrss_tags
1110 (owner_uid,tag_name,post_int_id)
1111 VALUES (?, ?, ?)");
2c08214a
AD
1112
1113 foreach ($filtered_tags as $tag) {
1114
1115 $tag = sanitize_tag($tag);
2c08214a
AD
1116
1117 if (!tag_is_valid($tag)) continue;
1118
0567016b 1119 $tsth->execute([$tag, $entry_int_id, $owner_uid]);
2c08214a 1120
0567016b
AD
1121 if (!$tsth->fetch()) {
1122 $usth->execute([$owner_uid, $tag, $entry_int_id]);
e6c886bf 1123 }
2c08214a
AD
1124
1125 array_push($tags_to_cache, $tag);
1126 }
1127
1128 /* update the cache */
1129
1130 $tags_to_cache = array_unique($tags_to_cache);
1131
0567016b 1132 $tags_str = join(",", $tags_to_cache);
2c08214a 1133
0567016b
AD
1134 $tsth = $pdo->prepare("UPDATE ttrss_user_entries
1135 SET tag_cache = ? WHERE ref_id = ?
1136 AND owner_uid = ?");
1137 $tsth->execute([$tags_str, $entry_ref_id, $owner_uid]);
2c08214a
AD
1138 }
1139
68cccafc 1140 _debug("article processed", $debug_enabled);
2c08214a
AD
1141 }
1142
68cccafc 1143 _debug("purging feed...", $debug_enabled);
2c08214a 1144
a42c55f0 1145 purge_feed($feed, 0, $debug_enabled);
2c08214a 1146
0567016b
AD
1147 $sth = $pdo->prepare("UPDATE ttrss_feeds
1148 SET last_updated = NOW(), last_unconditional = NOW(), last_error = '' WHERE id = ?");
1149 $sth->execute([$feed]);
2c08214a
AD
1150
1151 } else {
1152
0567016b 1153 $error_msg = mb_substr($rss->error(), 0, 245);
2c08214a 1154
4ad04ee2
AD
1155 _debug("fetch error: $error_msg", $debug_enabled);
1156
1157 if (count($rss->errors()) > 1) {
1158 foreach ($rss->errors() as $error) {
1159 _debug("+ $error");
1160 }
1161 }
2c08214a 1162
0567016b
AD
1163 $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
1164 last_updated = NOW(), last_unconditional = NOW() WHERE id = ?");
1165 $sth->execute([$error_msg, $feed]);
2c08214a 1166
88edaa93 1167 unset($rss);
0567016b 1168 return false;
88edaa93 1169 }
2c08214a 1170
68cccafc 1171 _debug("done", $debug_enabled);
88edaa93 1172
7b55001e 1173 return true;
2c08214a
AD
1174 }
1175
e6c886bf 1176 static function cache_enclosures($enclosures, $site_url, $debug) {
388d4dfa
AD
1177 foreach ($enclosures as $enc) {
1178
1179 if (preg_match("/(image|audio|video)/", $enc[1])) {
1180
1181 $src = rewrite_relative_url($site_url, $enc[0]);
1182
1183 $local_filename = CACHE_DIR . "/images/" . sha1($src);
1184
1185 if ($debug) _debug("cache_enclosures: downloading: $src to $local_filename");
1186
1187 if (!file_exists($local_filename)) {
1188 $file_content = fetch_file_contents($src);
1189
6fd03996 1190 if ($file_content && strlen($file_content) > MIN_CACHE_FILE_SIZE) {
388d4dfa
AD
1191 file_put_contents($local_filename, $file_content);
1192 }
1193 } else {
1194 touch($local_filename);
1195 }
1196 }
1197 }
1198 }
1199
e6c886bf 1200 static function cache_media($html, $site_url, $debug) {
3c696512
AD
1201 libxml_use_internal_errors(true);
1202
1203 $charset_hack = '<head>
1204 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1205 </head>';
1206
1207 $doc = new DOMDocument();
1208 $doc->loadHTML($charset_hack . $html);
1209 $xpath = new DOMXPath($doc);
1210
388d4dfa 1211 $entries = $xpath->query('(//img[@src])|(//video/source[@src])|(//audio/source[@src])');
3c696512
AD
1212
1213 foreach ($entries as $entry) {
5edd605a 1214 if ($entry->hasAttribute('src') && strpos($entry->getAttribute('src'), "data:") !== 0) {
3c696512
AD
1215 $src = rewrite_relative_url($site_url, $entry->getAttribute('src'));
1216
41bead9b 1217 $local_filename = CACHE_DIR . "/images/" . sha1($src);
3c696512 1218
41bead9b 1219 if ($debug) _debug("cache_media: downloading: $src to $local_filename");
3c696512
AD
1220
1221 if (!file_exists($local_filename)) {
1222 $file_content = fetch_file_contents($src);
1223
6fd03996 1224 if ($file_content && strlen($file_content) > MIN_CACHE_FILE_SIZE) {
3c696512
AD
1225 file_put_contents($local_filename, $file_content);
1226 }
4a27966e
J
1227 } else {
1228 touch($local_filename);
3c696512 1229 }
3c696512
AD
1230 }
1231 }
3c696512
AD
1232 }
1233
e6c886bf 1234 static function expire_error_log($debug) {
e2261e17
AD
1235 if ($debug) _debug("Removing old error log entries...");
1236
0567016b
AD
1237 $pdo = Db::pdo();
1238
e2261e17 1239 if (DB_TYPE == "pgsql") {
0567016b 1240 $pdo->query("DELETE FROM ttrss_error_log
e2261e17
AD
1241 WHERE created_at < NOW() - INTERVAL '7 days'");
1242 } else {
0567016b 1243 $pdo->query("DELETE FROM ttrss_error_log
e2261e17
AD
1244 WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)");
1245 }
e2261e17
AD
1246 }
1247
e6c886bf 1248 static function expire_lock_files($debug) {
65465085 1249 //if ($debug) _debug("Removing old lock files...");
2a91b6ff
AD
1250
1251 $num_deleted = 0;
1252
1253 if (is_writable(LOCK_DIRECTORY)) {
1254 $files = glob(LOCK_DIRECTORY . "/*.lock");
1255
1256 if ($files) {
1257 foreach ($files as $file) {
11344971 1258 if (!file_is_locked(basename($file)) && time() - filemtime($file) > 86400*2) {
2a91b6ff
AD
1259 unlink($file);
1260 ++$num_deleted;
1261 }
1262 }
1263 }
1264 }
1265
65465085 1266 if ($debug) _debug("Removed $num_deleted old lock files.");
2a91b6ff
AD
1267 }
1268
e6c886bf 1269 static function expire_cached_files($debug) {
3306daec 1270 foreach (array("simplepie", "images", "export", "upload") as $dir) {
3c696512 1271 $cache_dir = CACHE_DIR . "/$dir";
2c08214a 1272
65465085 1273// if ($debug) _debug("Expiring $cache_dir");
2c08214a 1274
3c696512
AD
1275 $num_deleted = 0;
1276
1277 if (is_writable($cache_dir)) {
1278 $files = glob("$cache_dir/*");
1279
2a91b6ff 1280 if ($files) {
2ab20c31 1281 foreach ($files as $file) {
6fd03996 1282 if (time() - filemtime($file) > 86400*CACHE_MAX_DAYS) {
2ab20c31 1283 unlink($file);
3c696512 1284
2ab20c31
AD
1285 ++$num_deleted;
1286 }
3c696512
AD
1287 }
1288 }
2a91b6ff 1289 }
3c696512 1290
65465085 1291 if ($debug) _debug("$cache_dir: removed $num_deleted files.");
3c696512
AD
1292 }
1293 }
2c08214a 1294
a3e0bdcf 1295 /**
e6c886bf
AD
1296 * Source: http://www.php.net/manual/en/function.parse-url.php#104527
1297 * Returns the url query as associative array
1298 *
1299 * @param string query
1300 * @return array params
1301 */
1302 static function convertUrlQuery($query) {
a3e0bdcf
AD
1303 $queryParts = explode('&', $query);
1304
1305 $params = array();
1306
1307 foreach ($queryParts as $param) {
1308 $item = explode('=', $param);
1309 $params[$item[0]] = $item[1];
1310 }
1311
1312 return $params;
1313 }
92c14e9d 1314
e6c886bf 1315 static function get_article_filters($filters, $title, $content, $link, $author, $tags, &$matched_rules = false) {
92c14e9d
AD
1316 $matches = array();
1317
1318 foreach ($filters as $filter) {
1319 $match_any_rule = $filter["match_any_rule"];
a3a896a1 1320 $inverse = $filter["inverse"];
92c14e9d
AD
1321 $filter_match = false;
1322
1323 foreach ($filter["rules"] as $rule) {
1324 $match = false;
ffa1bd7b 1325 $reg_exp = str_replace('/', '\/', $rule["reg_exp"]);
a3a896a1 1326 $rule_inverse = $rule["inverse"];
92c14e9d
AD
1327
1328 if (!$reg_exp)
1329 continue;
1330
1331 switch ($rule["type"]) {
e6c886bf
AD
1332 case "title":
1333 $match = @preg_match("/$reg_exp/iu", $title);
1334 break;
1335 case "content":
1336 // we don't need to deal with multiline regexps
1337 $content = preg_replace("/[\r\n\t]/", "", $content);
d03ae73e 1338
e6c886bf
AD
1339 $match = @preg_match("/$reg_exp/iu", $content);
1340 break;
1341 case "both":
1342 // we don't need to deal with multiline regexps
1343 $content = preg_replace("/[\r\n\t]/", "", $content);
d03ae73e 1344
e6c886bf
AD
1345 $match = (@preg_match("/$reg_exp/iu", $title) || @preg_match("/$reg_exp/iu", $content));
1346 break;
1347 case "link":
1348 $match = @preg_match("/$reg_exp/iu", $link);
1349 break;
1350 case "author":
1351 $match = @preg_match("/$reg_exp/iu", $author);
1352 break;
1353 case "tag":
1354 foreach ($tags as $tag) {
1355 if (@preg_match("/$reg_exp/iu", $tag)) {
1356 $match = true;
1357 break;
1358 }
7b80b5e1 1359 }
e6c886bf 1360 break;
92c14e9d
AD
1361 }
1362
a3a896a1
AD
1363 if ($rule_inverse) $match = !$match;
1364
92c14e9d
AD
1365 if ($match_any_rule) {
1366 if ($match) {
1367 $filter_match = true;
1368 break;
1369 }
1370 } else {
1371 $filter_match = $match;
1372 if (!$match) {
1373 break;
1374 }
1375 }
1376 }
1377
a3a896a1
AD
1378 if ($inverse) $filter_match = !$filter_match;
1379
92c14e9d 1380 if ($filter_match) {
557d86fe
AD
1381 if (is_array($matched_rules)) array_push($matched_rules, $rule);
1382
92c14e9d
AD
1383 foreach ($filter["actions"] AS $action) {
1384 array_push($matches, $action);
5e736e45
AD
1385
1386 // if Stop action encountered, perform no further processing
fd3e5e8d 1387 if (isset($action["type"]) && $action["type"] == "stop") return $matches;
92c14e9d
AD
1388 }
1389 }
1390 }
1391
1392 return $matches;
1393 }
1394
e6c886bf 1395 static function find_article_filter($filters, $filter_name) {
92c14e9d
AD
1396 foreach ($filters as $f) {
1397 if ($f["type"] == $filter_name) {
1398 return $f;
1399 };
1400 }
1401 return false;
1402 }
1403
e6c886bf 1404 static function find_article_filters($filters, $filter_name) {
92c14e9d
AD
1405 $results = array();
1406
1407 foreach ($filters as $f) {
1408 if ($f["type"] == $filter_name) {
1409 array_push($results, $f);
1410 };
1411 }
1412 return $results;
1413 }
1414
e6c886bf 1415 static function calculate_article_score($filters) {
92c14e9d
AD
1416 $score = 0;
1417
1418 foreach ($filters as $f) {
1419 if ($f["type"] == "score") {
1420 $score += $f["param"];
1421 };
1422 }
1423 return $score;
1424 }
1425
e6c886bf 1426 static function labels_contains_caption($labels, $caption) {
b24504b1
AD
1427 foreach ($labels as $label) {
1428 if ($label[1] == $caption) {
1429 return true;
1430 }
1431 }
1432
1433 return false;
1434 }
1435
e6c886bf 1436 static function assign_article_to_label_filters($id, $filters, $owner_uid, $article_labels) {
92c14e9d
AD
1437 foreach ($filters as $f) {
1438 if ($f["type"] == "label") {
e6c886bf 1439 if (!RSSUtils::labels_contains_caption($article_labels, $f["param"])) {
7c9b5a3f 1440 Labels::add_article($id, $f["param"], $owner_uid);
b24504b1
AD
1441 }
1442 }
92c14e9d
AD
1443 }
1444 }
87764a50 1445
e6c886bf 1446 static function make_guid_from_title($title) {
87d7e850
AD
1447 return preg_replace("/[ \"\',.:;]/", "-",
1448 mb_strtolower(strip_tags($title), 'utf-8'));
1449 }
1450
e6c886bf 1451 static function cleanup_counters_cache($debug) {
0567016b
AD
1452 $pdo = Db::pdo();
1453
1454 $res = $pdo->query("DELETE FROM ttrss_counters_cache
168cf351
AD
1455 WHERE feed_id > 0 AND
1456 (SELECT COUNT(id) FROM ttrss_feeds WHERE
1457 id = feed_id AND
1458 ttrss_counters_cache.owner_uid = ttrss_feeds.owner_uid) = 0");
168cf351 1459
0567016b
AD
1460 $frows = $res->rowCount();
1461
1462 $res = $pdo->query("DELETE FROM ttrss_cat_counters_cache
168cf351
AD
1463 WHERE feed_id > 0 AND
1464 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
1465 id = feed_id AND
1466 ttrss_cat_counters_cache.owner_uid = ttrss_feed_categories.owner_uid) = 0");
0567016b
AD
1467
1468 $crows = $res->rowCount();
168cf351 1469
7b55001e 1470 if ($debug) _debug("Removed $frows (feeds) $crows (cats) orphaned counter cache entries.");
168cf351
AD
1471 }
1472
e6c886bf 1473 static function housekeeping_user($owner_uid) {
5cbd1fe8
AD
1474 $tmph = new PluginHost();
1475
1476 load_user_plugins($owner_uid, $tmph);
1477
1478 $tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING, "hook_house_keeping", "");
1479 }
1480
e6c886bf
AD
1481 static function housekeeping_common($debug) {
1482 RSSUtils::expire_cached_files($debug);
1483 RSSUtils::expire_lock_files($debug);
1484 RSSUtils::expire_error_log($debug);
e2cf81e2 1485
e6c886bf 1486 $count = RSSUtils::update_feedbrowser_cache();
e2cf81e2
AD
1487 _debug("Feedbrowser updated, $count feeds processed.");
1488
a230bf88 1489 Article::purge_orphans( true);
e6c886bf 1490 RSSUtils::cleanup_counters_cache($debug);
e2cf81e2 1491
9b736a20
AD
1492 //$rc = cleanup_tags( 14, 50000);
1493 //_debug("Cleaned $rc cached tags.");
8e470220 1494
00f22824 1495 PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING, "hook_house_keeping", "");
e2cf81e2 1496 }
ea79a0e0 1497
e6c886bf
AD
1498 static function check_feed_favicon($site_url, $feed) {
1499 # print "FAVICON [$site_url]: $favicon_url\n";
a230bf88
AD
1500
1501 $icon_file = ICONS_DIR . "/$feed.ico";
1502
1503 if (!file_exists($icon_file)) {
1504 $favicon_url = get_favicon_url($site_url);
1505
1506 if ($favicon_url) {
1507 // Limiting to "image" type misses those served with text/plain
1508 $contents = fetch_file_contents($favicon_url); // , "image");
1509
1510 if ($contents) {
1511 // Crude image type matching.
1512 // Patterns gleaned from the file(1) source code.
1513 if (preg_match('/^\x00\x00\x01\x00/', $contents)) {
1514 // 0 string \000\000\001\000 MS Windows icon resource
1515 //error_log("check_feed_favicon: favicon_url=$favicon_url isa MS Windows icon resource");
1516 }
1517 elseif (preg_match('/^GIF8/', $contents)) {
1518 // 0 string GIF8 GIF image data
1519 //error_log("check_feed_favicon: favicon_url=$favicon_url isa GIF image");
1520 }
1521 elseif (preg_match('/^\x89PNG\x0d\x0a\x1a\x0a/', $contents)) {
1522 // 0 string \x89PNG\x0d\x0a\x1a\x0a PNG image data
1523 //error_log("check_feed_favicon: favicon_url=$favicon_url isa PNG image");
1524 }
1525 elseif (preg_match('/^\xff\xd8/', $contents)) {
1526 // 0 beshort 0xffd8 JPEG image data
1527 //error_log("check_feed_favicon: favicon_url=$favicon_url isa JPG image");
1528 }
f9ad33c2
GG
1529 elseif (preg_match('/^BM/', $contents)) {
1530 // 0 string BM PC bitmap (OS2, Windows BMP files)
1531 //error_log("check_feed_favicon, favicon_url=$favicon_url isa BMP image");
1532 }
a230bf88
AD
1533 else {
1534 //error_log("check_feed_favicon: favicon_url=$favicon_url isa UNKNOWN type");
1535 $contents = "";
1536 }
1537 }
1538
1539 if ($contents) {
1540 $fp = @fopen($icon_file, "w");
1541
1542 if ($fp) {
1543 fwrite($fp, $contents);
1544 fclose($fp);
1545 chmod($icon_file, 0644);
1546 }
1547 }
1548 }
1549 return $icon_file;
1550 }
1551 }
e6c886bf
AD
1552
1553
1554
bec5ba93 1555}