|
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); |
|
|
|
|
|
|
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
|
|
|
|