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