]> git.wh0rd.org - tt-rss.git/blob - plugins/instances/init.php
make pluginhost a singleton
[tt-rss.git] / plugins / instances / init.php
1 <?php
2 class Instances extends Plugin implements IHandler {
3 private $host;
4
5 private $status_codes = array(
6 0 => "Connection failed",
7 1 => "Success",
8 2 => "Invalid object received",
9 16 => "Access denied" );
10
11 function about() {
12 return array(1.0,
13 "Support for linking tt-rss instances together and sharing popular feeds.",
14 "fox",
15 true);
16 }
17
18 function init($host) {
19 $this->host = $host;
20
21 $host->add_hook($host::HOOK_PREFS_TABS, $this);
22 $host->add_handler("pref-instances", "*", $this);
23 $host->add_handler("public", "fbexport", $this);
24 $host->add_command("get-feeds", "receive popular feeds from linked instances", $this);
25 $host->add_hook($host::HOOK_UPDATE_TASK, $this);
26 }
27
28 function hook_update_task($args) {
29 _debug("Get linked feeds...");
30 $this->get_linked_feeds();
31 }
32
33 // Status codes:
34 // -1 - never connected
35 // 0 - no data received
36 // 1 - data received successfully
37 // 2 - did not receive valid data
38 // >10 - server error, code + 10 (e.g. 16 means server error 6)
39
40 function get_linked_feeds($instance_id = false) {
41 if ($instance_id)
42 $instance_qpart = "id = '$instance_id' AND ";
43 else
44 $instance_qpart = "";
45
46 if (DB_TYPE == "pgsql") {
47 $date_qpart = "last_connected < NOW() - INTERVAL '6 hours'";
48 } else {
49 $date_qpart = "last_connected < DATE_SUB(NOW(), INTERVAL 6 HOUR)";
50 }
51
52 $result = db_query("SELECT id, access_key, access_url FROM ttrss_linked_instances
53 WHERE $instance_qpart $date_qpart ORDER BY last_connected");
54
55 while ($line = db_fetch_assoc($result)) {
56 $id = $line['id'];
57
58 _debug("Updating: " . $line['access_url'] . " ($id)");
59
60 $fetch_url = $line['access_url'] . '/public.php?op=fbexport';
61 $post_query = 'key=' . $line['access_key'];
62
63 $feeds = fetch_file_contents($fetch_url, false, false, false, $post_query);
64
65 // try doing it the old way
66 if (!$feeds) {
67 $fetch_url = $line['access_url'] . '/backend.php?op=fbexport';
68 $feeds = fetch_file_contents($fetch_url, false, false, false, $post_query);
69 }
70
71 if ($feeds) {
72 $feeds = json_decode($feeds, true);
73
74 if ($feeds) {
75 if ($feeds['error']) {
76 $status = $feeds['error']['code'] + 10;
77
78 // access denied
79 if ($status == 16) {
80 db_query("DELETE FROM ttrss_linked_feeds
81 WHERE instance_id = '$id'");
82 }
83 } else {
84 $status = 1;
85
86 if (count($feeds['feeds']) > 0) {
87
88 db_query("DELETE FROM ttrss_linked_feeds
89 WHERE instance_id = '$id'");
90
91 foreach ($feeds['feeds'] as $feed) {
92 $feed_url = db_escape_string($feed['feed_url']);
93 $title = db_escape_string($feed['title']);
94 $subscribers = db_escape_string($feed['subscribers']);
95 $site_url = db_escape_string($feed['site_url']);
96
97 db_query("INSERT INTO ttrss_linked_feeds
98 (feed_url, site_url, title, subscribers, instance_id, created, updated)
99 VALUES
100 ('$feed_url', '$site_url', '$title', '$subscribers', '$id', NOW(), NOW())");
101 }
102 } else {
103 // received 0 feeds, this might indicate that
104 // the instance on the other hand is rebuilding feedbrowser cache
105 // we will try again later
106
107 // TODO: maybe perform expiration based on updated here?
108 }
109
110 _debug("Processed " . count($feeds['feeds']) . " feeds.");
111 }
112 } else {
113 $status = 2;
114 }
115
116 } else {
117 $status = 0;
118 }
119
120 _debug("Status: $status");
121
122 db_query("UPDATE ttrss_linked_instances SET
123 last_status_out = '$status', last_connected = NOW() WHERE id = '$id'");
124
125 }
126 }
127
128
129 function get_feeds() {
130 $this->get_linked_feeds(false);
131 }
132
133 function get_prefs_js() {
134 return file_get_contents(dirname(__FILE__) . "/instances.js");
135 }
136
137 function hook_prefs_tabs($args) {
138 if ($_SESSION["access_level"] >= 10 || SINGLE_USER_MODE) {
139 ?><div id="instanceConfigTab" dojoType="dijit.layout.ContentPane"
140 href="backend.php?op=pref-instances"
141 title="<?php echo __('Linked') ?>"></div><?php
142 }
143 }
144
145 function csrf_ignore($method) {
146 $csrf_ignored = array("index", "edit");
147
148 return array_search($method, $csrf_ignored) !== false;
149 }
150
151 function before($method) {
152 if ($_SESSION["uid"]) {
153 if ($_SESSION["access_level"] < 10) {
154 print __("Your access level is insufficient to open this tab.");
155 return false;
156 }
157 return true;
158 }
159 return false;
160 }
161
162 function after() {
163 return true;
164 }
165
166 function remove() {
167 $ids = db_escape_string($_REQUEST['ids']);
168
169 db_query("DELETE FROM ttrss_linked_instances WHERE
170 id IN ($ids)");
171 }
172
173 function add() {
174 $id = db_escape_string($_REQUEST["id"]);
175 $access_url = db_escape_string($_REQUEST["access_url"]);
176 $access_key = db_escape_string($_REQUEST["access_key"]);
177
178 db_query("BEGIN");
179
180 $result = db_query("SELECT id FROM ttrss_linked_instances
181 WHERE access_url = '$access_url'");
182
183 if (db_num_rows($result) == 0) {
184 db_query("INSERT INTO ttrss_linked_instances
185 (access_url, access_key, last_connected, last_status_in, last_status_out)
186 VALUES
187 ('$access_url', '$access_key', '1970-01-01', -1, -1)");
188
189 }
190
191 db_query("COMMIT");
192 }
193
194 function edit() {
195 $id = db_escape_string($_REQUEST["id"]);
196
197 $result = db_query("SELECT * FROM ttrss_linked_instances WHERE
198 id = '$id'");
199
200 print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"id\" value=\"$id\">";
201 print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"pref-instances\">";
202 print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"editSave\">";
203
204 print "<div class=\"dlgSec\">".__("Instance")."</div>";
205
206 print "<div class=\"dlgSecCont\">";
207
208 /* URL */
209
210 $access_url = htmlspecialchars(db_fetch_result($result, 0, "access_url"));
211
212 print __("URL:") . " ";
213
214 print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"1\"
215 placeHolder=\"".__("Instance URL")."\"
216 regExp='^(http|https)://.*'
217 style=\"font-size : 16px; width: 20em\" name=\"access_url\"
218 value=\"$access_url\">";
219
220 print "<hr/>";
221
222 $access_key = htmlspecialchars(db_fetch_result($result, 0, "access_key"));
223
224 /* Access key */
225
226 print __("Access key:") . " ";
227
228 print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"1\"
229 placeHolder=\"".__("Access key")."\" regExp='\w{40}'
230 style=\"width: 20em\" name=\"access_key\" id=\"instance_edit_key\"
231 value=\"$access_key\">";
232
233 print "<p class='insensitive'>" . __("Use one access key for both linked instances.");
234
235 print "</div>";
236
237 print "<div class=\"dlgButtons\">
238 <div style='float : left'>
239 <button dojoType=\"dijit.form.Button\"
240 onclick=\"return dijit.byId('instanceEditDlg').regenKey()\">".
241 __('Generate new key')."</button>
242 </div>
243 <button dojoType=\"dijit.form.Button\"
244 onclick=\"return dijit.byId('instanceEditDlg').execute()\">".
245 __('Save')."</button>
246 <button dojoType=\"dijit.form.Button\"
247 onclick=\"return dijit.byId('instanceEditDlg').hide()\"\">".
248 __('Cancel')."</button></div>";
249
250 }
251
252 function editSave() {
253 $id = db_escape_string($_REQUEST["id"]);
254 $access_url = db_escape_string($_REQUEST["access_url"]);
255 $access_key = db_escape_string($_REQUEST["access_key"]);
256
257 db_query("UPDATE ttrss_linked_instances SET
258 access_key = '$access_key', access_url = '$access_url',
259 last_connected = '1970-01-01'
260 WHERE id = '$id'");
261
262 }
263
264 function index() {
265
266 if (!function_exists('curl_init')) {
267 print "<div style='padding : 1em'>";
268 print_error("This functionality requires CURL functions. Please enable CURL in your PHP configuration (you might also want to disable open_basedir in php.ini) and reload this page.");
269 print "</div>";
270 }
271
272 print "<div id=\"pref-instance-wrap\" dojoType=\"dijit.layout.BorderContainer\" gutters=\"false\">";
273 print "<div id=\"pref-instance-header\" dojoType=\"dijit.layout.ContentPane\" region=\"top\">";
274
275 print "<div id=\"pref-instance-toolbar\" dojoType=\"dijit.Toolbar\">";
276
277 $sort = db_escape_string($_REQUEST["sort"]);
278
279 if (!$sort || $sort == "undefined") {
280 $sort = "access_url";
281 }
282
283 print "<div dojoType=\"dijit.form.DropDownButton\">".
284 "<span>" . __('Select')."</span>";
285 print "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
286 print "<div onclick=\"selectTableRows('prefInstanceList', 'all')\"
287 dojoType=\"dijit.MenuItem\">".__('All')."</div>";
288 print "<div onclick=\"selectTableRows('prefInstanceList', 'none')\"
289 dojoType=\"dijit.MenuItem\">".__('None')."</div>";
290 print "</div></div>";
291
292 print "<button dojoType=\"dijit.form.Button\" onclick=\"addInstance()\">".__('Link instance')."</button>";
293 print "<button dojoType=\"dijit.form.Button\" onclick=\"editSelectedInstance()\">".__('Edit')."</button>";
294 print "<button dojoType=\"dijit.form.Button\" onclick=\"removeSelectedInstances()\">".__('Remove')."</button>";
295
296 print "</div>"; #toolbar
297
298 $result = db_query("SELECT *,
299 (SELECT COUNT(*) FROM ttrss_linked_feeds
300 WHERE instance_id = ttrss_linked_instances.id) AS num_feeds
301 FROM ttrss_linked_instances
302 ORDER BY $sort");
303
304 print "<p class=\"insensitive\" style='margin-left : 1em;'>" . __("You can connect other instances of Tiny Tiny RSS to this one to share Popular feeds. Link to this instance of Tiny Tiny RSS by using this URL:");
305
306 print " <a href=\"#\" onclick=\"alert('".htmlspecialchars(get_self_url_prefix())."')\">(display url)</a>";
307
308 print "<p><table width='100%' id='prefInstanceList' class='prefInstanceList' cellspacing='0'>";
309
310 print "<tr class=\"title\">
311 <td align='center' width=\"5%\">&nbsp;</td>
312 <td width=''><a href=\"#\" onclick=\"updateInstanceList('access_url')\">".__('Instance URL')."</a></td>
313 <td width='20%'><a href=\"#\" onclick=\"updateInstanceList('access_key')\">".__('Access key')."</a></td>
314 <td width='10%'><a href=\"#\" onclick=\"updateUsersList('last_connected')\">".__('Last connected')."</a></td>
315 <td width='10%'><a href=\"#\" onclick=\"updateUsersList('last_status_out')\">".__('Status')."</a></td>
316 <td width='10%'><a href=\"#\" onclick=\"updateUsersList('num_feeds')\">".__('Stored feeds')."</a></td>
317 </tr>";
318
319 $lnum = 0;
320
321 while ($line = db_fetch_assoc($result)) {
322 $class = ($lnum % 2) ? "even" : "odd";
323
324 $id = $line['id'];
325 $this_row_id = "id=\"LIRR-$id\"";
326
327 $line["last_connected"] = make_local_datetime($line["last_connected"], false);
328
329 print "<tr class=\"$class\" $this_row_id>";
330
331 print "<td align='center'><input onclick='toggleSelectRow(this);'
332 type=\"checkbox\" id=\"LICHK-$id\"></td>";
333
334 $onclick = "onclick='editInstance($id, event)' title='".__('Click to edit')."'";
335
336 $access_key = mb_substr($line['access_key'], 0, 4) . '...' .
337 mb_substr($line['access_key'], -4);
338
339 print "<td $onclick>" . htmlspecialchars($line['access_url']) . "</td>";
340 print "<td $onclick>" . htmlspecialchars($access_key) . "</td>";
341 print "<td $onclick>" . htmlspecialchars($line['last_connected']) . "</td>";
342 print "<td $onclick>" . $this->status_codes[$line['last_status_out']] . "</td>";
343 print "<td $onclick>" . htmlspecialchars($line['num_feeds']) . "</td>";
344
345 print "</tr>";
346
347 ++$lnum;
348 }
349
350 print "</table>";
351
352 print "</div>"; #pane
353
354 PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB,
355 "hook_prefs_tab", "prefInstances");
356
357 print "</div>"; #container
358
359 }
360
361 function fbexport() {
362
363 $access_key = db_escape_string($_POST["key"]);
364
365 // TODO: rate limit checking using last_connected
366 $result = db_query("SELECT id FROM ttrss_linked_instances
367 WHERE access_key = '$access_key'");
368
369 if (db_num_rows($result) == 1) {
370
371 $instance_id = db_fetch_result($result, 0, "id");
372
373 $result = db_query("SELECT feed_url, site_url, title, subscribers
374 FROM ttrss_feedbrowser_cache ORDER BY subscribers DESC LIMIT 100");
375
376 $feeds = array();
377
378 while ($line = db_fetch_assoc($result)) {
379 array_push($feeds, $line);
380 }
381
382 db_query("UPDATE ttrss_linked_instances SET
383 last_status_in = 1 WHERE id = '$instance_id'");
384
385 print json_encode(array("feeds" => $feeds));
386 } else {
387 print json_encode(array("error" => array("code" => 6)));
388 }
389 }
390
391 function addInstance() {
392 print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"pref-instances\">";
393 print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"add\">";
394
395 print "<div class=\"dlgSec\">".__("Instance")."</div>";
396
397 print "<div class=\"dlgSecCont\">";
398
399 /* URL */
400
401 print __("URL:") . " ";
402
403 print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"1\"
404 placeHolder=\"".__("Instance URL")."\"
405 regExp='^(http|https)://.*'
406 style=\"font-size : 16px; width: 20em\" name=\"access_url\">";
407
408 print "<hr/>";
409
410 $access_key = sha1(uniqid(rand(), true));
411
412 /* Access key */
413
414 print __("Access key:") . " ";
415
416 print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"1\"
417 placeHolder=\"".__("Access key")."\" regExp='\w{40}'
418 style=\"width: 20em\" name=\"access_key\" id=\"instance_add_key\"
419 value=\"$access_key\">";
420
421 print "<p class='insensitive'>" . __("Use one access key for both linked instances.");
422
423 print "</div>";
424
425 print "<div class=\"dlgButtons\">
426 <div style='float : left'>
427 <button dojoType=\"dijit.form.Button\"
428 onclick=\"return dijit.byId('instanceAddDlg').regenKey()\">".
429 __('Generate new key')."</button>
430 </div>
431 <button dojoType=\"dijit.form.Button\"
432 onclick=\"return dijit.byId('instanceAddDlg').execute()\">".
433 __('Create link')."</button>
434 <button dojoType=\"dijit.form.Button\"
435 onclick=\"return dijit.byId('instanceAddDlg').hide()\"\">".
436 __('Cancel')."</button></div>";
437
438 return;
439 }
440
441 function genHash() {
442 $hash = sha1(uniqid(rand(), true));
443
444 print json_encode(array("hash" => $hash));
445 }
446
447
448 }
449 ?>