lib.php 21.7 KB
Newer Older
1
2
<?php
/**
Francois Marier's avatar
Francois Marier committed
3
 * Mahara: Electronic portfolio, weblog, resume builder and social networking
4
5
 * Copyright (C) 2006-2009 Catalyst IT Ltd and others; see:
 *                         http://wiki.mahara.org/Contributors
6
 *
Francois Marier's avatar
Francois Marier committed
7
8
9
10
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
11
 *
Francois Marier's avatar
Francois Marier committed
12
13
14
15
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
16
 *
Francois Marier's avatar
Francois Marier committed
17
18
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21
 *
 * @package    mahara
 * @subpackage blocktype-externalfeed
22
 * @author     Catalyst IT Ltd
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
24
 * @copyright  (C) 2006-2009 Catalyst IT Ltd http://catalyst.net.nz
25
26
27
28
29
 *
 */

defined('INTERNAL') || die();

30
31
require_once('XML/Feed/Parser.php');

32
33
34
35
36
37
38
39
40
41
42
class PluginBlocktypeExternalfeed extends SystemBlocktype {

    public static function get_title() {
        return get_string('title', 'blocktype.externalfeed');
    }

    public static function get_description() {
        return get_string('description', 'blocktype.externalfeed');
    }

    public static function get_categories() {
43
        return array('external');
44
    }
45
46
47
    public static function postinst($prevversion) {
        if ($prevversion == 0) {
            if (is_postgres()) {
48
49
50
51
                $table = new XMLDBTable('blocktype_externalfeed_data');
                $index = new XMLDBIndex('urlautautix');
                $index->setAttributes(XMLDB_INDEX_NOTUNIQUE, array('url', 'authuser', 'authpassword'));
                add_index($table, $index);
52
53
            }
            else if (is_mysql()) {
54
55
56
                // MySQL needs size limits when indexing text fields
                execute_sql('ALTER TABLE {blocktype_externalfeed_data} ADD INDEX
                               {blocextedata_urlautaut_ix} (url(255), authuser(255), authpassword(255))');
57
            }
58
59
60
        }
    }

61
    public static function render_instance(BlockInstance $instance, $editing=false) {
62
        $configdata = $instance->get('configdata');
63
        if (!empty($configdata['feedid'])) {
64
65

            $data = $instance->get_data('feed', $configdata['feedid']);
66

67
68
69
70
71
72
73
74
75
            if (isset($data) && is_string($data->content)) {
                $data->content = unserialize($data->content);
            }
            if (isset($data) && (is_string($data->image) || is_array($data->image))) {
                $data->image = @unserialize($data->image);
            }
            else {
                $data->image = null;
            }
76

77
            // only keep the number of entries the user asked for
78
79
80
81
            if (count($data->content)) {
                $chunks = array_chunk($data->content, isset($configdata['count']) ? $configdata['count'] : 10);
                $data->content = $chunks[0];
            }
82

83
84
85
86
            foreach ($data->content as $k => $c) {
                $data->content[$k]->link =  sanitize_url($c->link);
            }

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
            // Attempt to fix relative URLs in the feeds
            if (!empty($data->image['link'])) {
                $data->description = preg_replace(
                    '/src="(\/[^"]+)"/',
                    'src="' . $data->image['link'] . '$1"',
                    $data->description
                );
                foreach ($data->content as &$entry) {
                    $entry->description = preg_replace(
                        '/src="(\/[^"]+)"/',
                        'src="' . $data->image['link'] . '$1"',
                        $entry->description
                    );
                }
            }

103
104
105
            $smarty = smarty_core();
            $smarty->assign('title', $data->title);
            $smarty->assign('description', $data->description);
106
            $smarty->assign('url', $data->url);
107
108
            // 'full' won't be set for feeds created before 'full' support was added
            $smarty->assign('full', isset($configdata['full']) ? $configdata['full'] : false); 
109
            $smarty->assign('link', sanitize_url($data->link));
110
111
            $smarty->assign('entries', $data->content);
            $smarty->assign('feedimage', self::make_feed_image_tag($data->image));
112
            $smarty->assign('lastupdated', get_string('lastupdatedon', 'blocktype.externalfeed', format_date($data->lastupdate)));
113
114
115
116
117
            return $smarty->fetch('blocktype:externalfeed:feed.tpl');
        }
        return '';
    }

118
119
120
121
    // Called by $instance->get_data('feed', ...).
    public static function get_instance_feed($id) {
        return get_record(
            'blocktype_externalfeed_data', 'id', $id, null, null, null, null,
122
            'id,url,link,title,description,content,authuser,authpassword,insecuresslmode,' . db_format_tsfield('lastupdate') . ',image'
123
124
125
        );
    }

126
127
128
129
130
131
132
    public static function has_instance_config() {
        return true;
    }

    public static function instance_config_form($instance) {
        $configdata = $instance->get('configdata');

133
        if (!empty($configdata['feedid'])) {
134
135
            $instancedata = $instance->get_data('feed', $configdata['feedid']);
            $url = $instancedata->url;
136
            $insecuresslmode = $instancedata->insecuresslmode;
137
138
            $authuser = $instancedata->authuser;
            $authpassword = $instancedata->authpassword;
139
140
141
        }
        else {
            $url = '';
142
            $insecuresslmode = 0;
143
144
            $authuser = '';
            $authpassword = '';
145
146
        }

147
148
149
150
151
152
153
        if (isset($configdata['full'])) {
            $full = $configdata['full'];
        }
        else {
            $full = false;
        }

154
155
156
157
158
159
160
161
        return array(
            'url' => array(
                'type'  => 'text',
                'title' => get_string('feedlocation', 'blocktype.externalfeed'),
                'description' => get_string('feedlocationdesc', 'blocktype.externalfeed'),
                'width' => '90%',
                'defaultvalue' => $url,
                'rules' => array(
162
                    'required' => true,
163
                    'maxlength' => 2048, // See install.xml for this plugin - MySQL can only safely handle up to 255 chars
164
165
                ),
            ),
166
167
168
169
170
171
            'insecuresslmode' => array(
                'type'  => 'checkbox',
                'title' => get_string('insecuresslmode', 'blocktype.externalfeed'),
                'description' => get_string('insecuresslmodedesc', 'blocktype.externalfeed'),
                'defaultvalue' => (bool)$insecuresslmode,
            ),
172
173
174
175
176
177
178
179
180
181
182
183
184
185
            'authuser' => array(
                'type' => 'text',
                'title' => get_string('authuser', 'blocktype.externalfeed'),
                'description' => get_string('authuserdesc', 'blocktype.externalfeed'),
                'width' => '90%',
                'defaultvalue' => $authuser,
            ),
            'authpassword' => array(
                'type' => 'text',
                'title' => get_string('authpassword', 'blocktype.externalfeed'),
                'description' => get_string('authpassworddesc', 'blocktype.externalfeed'),
                'width' => '90%',
                'defaultvalue' => $authpassword,
            ),
186
187
188
189
190
191
192
193
194
            'count' => array(
                'type' => 'text',
                'title' => get_string('itemstoshow', 'blocktype.externalfeed'),
                'description' => get_string('itemstoshowdescription', 'blocktype.externalfeed'),
                'defaultvalue' => isset($configdata['count']) ? $configdata['count'] : 10,
                'size' => 3,
                'minvalue' => 1,
                'maxvalue' => 20,
            ),
195
196
197
198
199
200
            'full' => array(
                'type'         => 'checkbox',
                'title'        => get_string('showfeeditemsinfull', 'blocktype.externalfeed'),
                'description'  => get_string('showfeeditemsinfulldesc', 'blocktype.externalfeed'),
                'defaultvalue' => (bool)$full,
            ),
201
202
203
204
205
206
207
208
209
210
211
        );
    }

    /**
     * Optional method. If exists, allows this class to decide the title for 
     * all blockinstances of this type
     */
    public static function get_instance_title(BlockInstance $bi) {
        $configdata = $bi->get('configdata');

        if (!empty($configdata['feedid'])) {
212
            if ($title = $bi->get_data('feed', $configdata['feedid'])->title) {
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
                return $title;
            }
        }
        return '';
    }

    public static function instance_config_validate(Pieform $form, $values) {
        if (strpos($values['url'], '://') == false) {
            // try add on http://
            $values['url'] = 'http://' . $values['url'];
        }
        else {
            $proto = substr($values['url'], 0, strpos($values['url'], '://'));
            if (!in_array($proto, array('http', 'https'))) {
                $form->set_error('url', get_string('invalidurl', 'blocktype.externalfeed'));
            }
        }
230
231
        if (!$form->get_error('url') && !record_exists('blocktype_externalfeed_data', 'url', $values['url'])) {
            try {
232
                self::parse_feed($values['url'], $values['insecuresslmode'], $values['authuser'], $values['authpassword']);
233
234
235
236
237
                return;
            }
            catch (XML_Feed_Parser_Exception $e) {
                $form->set_error('url', get_string('invalidfeed', 'blocktype.externalfeed',  $e->getMessage()));
            }
238
239
240
241
242
243
244
245
246
        }
    }

    public static function instance_config_save($values) {
        // we need to turn the feed url into an id in the feed_data table..
        if (strpos($values['url'], '://') == false) {
            // try add on http://
            $values['url'] = 'http://' . $values['url'];
        }
247
        // We know this is safe because self::parse_feed caches its result and
248
        // the validate method would have failed if the feed was invalid
249
        $data = self::parse_feed($values['url'], $values['insecuresslmode'], $values['authuser'], $values['authpassword']);
250
        $data->content  = serialize($data->content);
251
        $data->image    = serialize($data->image);
252
        $data->lastupdate = db_format_timestamp(time());
253
254
        $wheredata = array('url' => $values['url'], 'authuser' => $values['authuser'], 'authpassword' => $values['authpassword']);
        $id = ensure_record_exists('blocktype_externalfeed_data', $wheredata, $data, 'id', true);
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
        $values['feedid'] = $id;
        unset($values['url']);
        return $values;

    }

    public static function get_cron() {
        $refresh = new StdClass;
        $refresh->callfunction = 'refresh_feeds';
        $refresh->hour = '*';
        $refresh->minute = '0';

        $cleanup = new StdClass;
        $cleanup->callfunction = 'cleanup_feeds';
        $cleanup->hour = '3';
270
        $cleanup->minute = '30';
271
272
273
274
275
276
277

        return array($refresh, $cleanup);

    }

    public static function refresh_feeds() {
        if (!$feeds = get_records_select_array('blocktype_externalfeed_data', 
278
            'lastupdate < ?', array(db_format_timestamp(strtotime('-30 minutes'))),
279
            '', 'id,url,authuser,authpassword,insecuresslmode,' . db_format_tsfield('lastupdate', 'tslastupdate'))) {
280
281
            return;
        }
282
        $yesterday = time() - 60*60*24;
283
        foreach ($feeds as $feed) {
284
285
286
287
288
289
290
291
292
            // Hack to stop the updating of dead feeds from delaying other
            // more important stuff that runs on cron.
            if (defined('CRON') && $feed->tslastupdate < $yesterday) {
                // We've been trying for 24 hours already, so waste less
                // time on this one and just try it once a day
                if (mt_rand(0, 24) != 0) {
                    continue;
                }
            }
293
            try {
294
                $data = self::parse_feed($feed->url, $feed->insecuresslmode, $feed->authuser, $feed->authpassword);
295
296
297
298
299
300
            }
            catch (XML_Feed_Parser_Exception $e) {
                // The feed must have changed in such a way as to become 
                // invalid since it was added. We ignore this case in the hope 
                // the feed will become valid some time later
            }
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
            if (isset($data) && $data instanceof XML_Feed_Parser_Exception) {
                continue;
            }
            else if (isset($data)) {
                if (!isset($data->image)) {
                    $data->image = null;
                }
                try {
                    $data->content = $data->content ? serialize($data->content) : '' ;
                    $data->image = $data->image ? serialize($data->image) : '';
                    $data->id = $feed->id;
                    $data->lastupdate = db_format_timestamp(time());
                    update_record('blocktype_externalfeed_data', $data);
                }
                catch (XML_Feed_Parser_Exception $e) {
                    // We tried to add the newly parsed data
                }
            }
319
320
321
322
323
324
325
        }
    }

    public static function cleanup_feeds() {
        $ids = array();
        if ($instances = get_records_array('block_instance', 'blocktype', 'externalfeed')) {
            foreach ($instances as $block) {
326
327
328
329
330
                if (is_string($block->configdata) && strlen($block->configdata)) {
                    $data = unserialize($block->configdata);
                    if (isset($data['feedid']) && $data['feedid']) {
                        $ids[$data['feedid']] = true;
                    }
331
                }
332
333
334
335
336
337
338
339
340
341
            }
        }
        if (count($ids) == 0) {
            delete_records('blocktype_externalfeed_data'); // just delete it all 
            return;
        }
        $usedids = implode(', ', array_map('db_quote', array_keys($ids)));
        delete_records_select('blocktype_externalfeed_data', 'id NOT IN ( ' . $usedids . ' )');
    }

342
343
344
345
346
    /**
     * Parses the RSS feed given by $source. Throws an exception if the feed 
     * isn't able to be parsed
     *
     * @param string $source The URI for the feed
347
     * @param bool $insecuresslmode Skip certificate checking
348
349
     * @param string $authuser HTTP basic auth username to use
     * @param string $authpassword HTTP basic auth password to use
350
351
     * @throws XML_Feed_Parser_Exception
     */
352
    public static function parse_feed($source, $insecuresslmode=false, $authuser='', $authpassword='') {
353
354
355
356
357
358
359
360
361

        static $cache;
        if (empty($cache)) {
            $cache = array();
        }
        if (array_key_exists($source, $cache)) {
            return $cache[$source];
        }

362
363
364
        $config = array(
            CURLOPT_URL => $source,
            CURLOPT_TIMEOUT => 15,
365
            CURLOPT_USERAGENT => '',
366
        );
367

368
369
370
371
        if (!empty($authuser) || !empty($authpassword)) {
            $config[CURLOPT_USERPWD] = $authuser . ':' . $authpassword;
        }

372
373
374
375
        if ($insecuresslmode) {
            $config[CURLOPT_SSL_VERIFYPEER] = false;
        }

376
        $result = mahara_http_request($config, true);
377

378
379
        if ($result->error) {
            throw new XML_Feed_Parser_Exception($result->error);
380
381
        }

382
383
384
385
        if (empty($result->data)) {
            throw new XML_Feed_Parser_Exception('Feed url returned no data');
        }

386
        try {
387
            $feed = new XML_Feed_Parser($result->data, false, true, false);
388
389
        }
        catch (XML_Feed_Parser_Exception $e) {
390
391
392
393
            $cache[$source] = $e;
            throw $e;
            // Don't catch other exceptions, they're an indication something 
            // really bad happened
394
        }
395

396
397
398
        $data = new StdClass;
        $data->title = $feed->title;
        $data->url = $source;
399
400
        $data->authuser = $authuser;
        $data->authpassword = $authpassword;
401
        $data->insecuresslmode = (int)$insecuresslmode;
402
403
        $data->link = $feed->link;
        $data->description = $feed->description;
404
405
406
407
408
409
410
411

        // Work out the icon for the feed depending on whether it's RSS or ATOM
        $data->image = $feed->image;
        if (!$data->image) {
            // ATOM feed. These are simple strings
            $data->image = $feed->logo ? $feed->logo : null;
        }

412
413
        $data->content = array();
        foreach ($feed as $count => $item) {
414
            if ($count == 20) {
415
416
                break;
            }
417
            $description = $item->content ? $item->content : ($item->description ? $item->description : ($item->summary ? $item->summary : null));
418
419
420
421
422
423
424
425
426
427
428
            if (!$item->title) {
                if (!empty($description)) {
                    $item->title = substr($description, 0, 60);
                }
                else if ($item->link) {
                    $item->title = $item->link;
                }
                else {
                    $item->title = get_string('notitle', 'view');
                }
            }
429
            $data->content[] = (object)array('title' => $item->title, 'link' => $item->link, 'description' => $description);
430
431
432
433
        }
        $cache[$source] = $data;
        return $data;
    }
434
435
436
437
438
439
440
441

    /**
     * Returns the HTML for the feed icon (not the little RSS one, but the 
     * actual logo associated with the feed)
     */
    private static function make_feed_image_tag($image) {
        $result = '';

442
443
444
445
        if ($image['url']) {
            $image['url'] = sanitize_url($image['url']);
        }

446
447
448
449
        if (!$image['url']) {
            return '';
        }

450
        if (is_string($image)) {
451
452
453
454
            if (is_https() and stripos($image, 'http://') !== false) {
                // HTTPS sites should not display HTTP images
                return '';
            }
455
456
457
            return '<img src="' . hsc($image) . '">';
        }

458
459
460
461
        if ($image['link']) {
            $image['link'] = sanitize_url($image['link']);
        }

462
        if (!empty($image['link'])) {
463
            $result .= '<a href="' . $image['link'] . '">';
464
465
466
467
468
469
470
471
472
473
        }

        $url = $image['url'];
        // Try and fix URLs that aren't absolute. The standards all say URLs 
        // are supposed to be absolute in RSS feeds, yet still some people 
        // can't even get the basics right...
        if (substr($url, 0, 1) == '/' && !empty($image['link'])) {
            $url = $image['link'] . $image['url'];
        }

474
475
476
477
478
        if (is_https() and stripos($url, 'http://') !== false) {
            // HTTPS sites should not display HTTP images
            return '';
        }

479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
        $result .= '<img src="' . hsc($url) . '"';
        // Required by the specification, but we can't count on it...
        if (!empty($image['title'])) {
            $result .= ' alt="' . hsc($image['title']) . '"';
        }

        if (!empty($image['width']) || !empty($image['height'])) {
            $result .= ' style="';
            if (!empty($image['width'])) {
                $result .= 'width: ' . hsc($image['width']) . 'px;"';
            }
            if (!empty($image['height'])) {
                $result .= 'height: ' . hsc($image['height']) . 'px;"';
            }
            $result .= '"';
        }

        $result .= '>';

        if (!empty($image['link'])) {
            $result .= '</a>';
        }

        return $result;
    }
504
505
506
507
508

    public static function default_copy_type() {
        return 'full';
    }

509
510
511
512
513
    /**
     * The URL is not stored in the configdata, so we need to get it separately
     */
    public static function export_blockinstance_config(BlockInstance $bi) {
        $config = $bi->get('configdata');
514
515

        $url = $authuser = $authpassword = '';
516
        $insecuresslmode = false;
517
518
519
520
        if (!empty($config['feedid']) and $record = get_record('blocktype_externalfeed_data', 'id', $config['feedid'])) {
            $url =  $record->url;
            $authuser = $record->authuser;
            $authpassword = $record->authpassword;
521
            $insecuresslmode = (bool)$record->insecuresslmode;
522
523
        }

524
525
        return array(
            'url' => $url,
526
527
            'authuser' => $authuser,
            'authpassword' => $authpassword,
528
            'insecuresslmode' => $insecuresslmode ? 1 : 0,
529
530
531
532
533
534
535
536
537
538
539
540
            'full' => isset($config['full']) ? ($config['full'] ? 1 : 0) : 0,
        );
    }

    /**
     * Overrides default import to trigger retrieving the feed.
     */
    public static function import_create_blockinstance(array $config) {
        // Trigger retrieving the feed
        // Note: may have to re-think this at some point - we'll end up retrieving all the 
        // RSS feeds for this user at import time, which could easily be quite 
        // slow. This plugin will need a bit of re-working for this to be possible
541
        if (!empty($config['config']['url'])) {
542
            try {
543
544
545
                $urloptions = array('url' => $config['config']['url'],
                                    'authuser' => !empty($config['config']['authuser']) ? $config['config']['authuser'] : '',
                                    'authpassword' => !empty($config['config']['authpassword']) ? $config['config']['authpassword'] : '',
546
                                    'insecuresslmode' => !empty($config['config']['insecuresslmode']) ? (bool)$config['config']['insecuresslmode'] : false,
547
548
                                    );
                $values = self::instance_config_save($urloptions);
549
550
551
552
553
            }
            catch (XML_Feed_Parser_Exception $e) {
                log_info("Note: was unable to parse RSS feed for new blockinstance. URL was {$config['config']['url']}");
                $values = array();
            }
554
555
556
557
558
559
560
        }

        $bi = new BlockInstance(0,
            array(
                'blocktype'  => 'externalfeed',
                'configdata' => array(
                    'feedid' => (isset($values['feedid'])) ? $values['feedid'] : '',
561
                    'full'   => (isset($config['config']['full']))   ? $config['config']['full']   : '',
562
563
564
565
566
567
568
                ),
            )
        );

        return $bi;
    }

569
}