block_rss_client.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Contains block_rss_client
  18. * @package block_rss_client
  19. * @copyright Daryl Hawes
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL
  21. */
  22. /**
  23. * A block which displays Remote feeds
  24. *
  25. * @package block_rss_client
  26. * @copyright Daryl Hawes
  27. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL
  28. */
  29. class block_rss_client extends block_base {
  30. /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
  31. const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
  32. function init() {
  33. $this->title = get_string('pluginname', 'block_rss_client');
  34. }
  35. function applicable_formats() {
  36. return array('all' => true, 'tag' => false); // Needs work to make it work on tags MDL-11960
  37. }
  38. function specialization() {
  39. // After the block has been loaded we customize the block's title display
  40. if (!empty($this->config) && !empty($this->config->title)) {
  41. // There is a customized block title, display it
  42. $this->title = $this->config->title;
  43. } else {
  44. // No customized block title, use localized remote news feed string
  45. $this->title = get_string('remotenewsfeed', 'block_rss_client');
  46. }
  47. }
  48. /**
  49. * Gets the footer, which is the channel link of the last feed in our list of feeds
  50. *
  51. * @param array $feedrecords The feed records from the database.
  52. * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed.
  53. */
  54. protected function get_footer($feedrecords) {
  55. $footer = null;
  56. if ($this->config->block_rss_client_show_channel_link) {
  57. global $CFG;
  58. require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
  59. $feedrecord = array_pop($feedrecords);
  60. $feed = new moodle_simplepie($feedrecord->url);
  61. $channellink = new moodle_url($feed->get_link());
  62. if (!empty($channellink)) {
  63. $footer = new block_rss_client\output\footer($channellink);
  64. }
  65. }
  66. return $footer;
  67. }
  68. function get_content() {
  69. global $CFG, $DB;
  70. if ($this->content !== NULL) {
  71. return $this->content;
  72. }
  73. // initalise block content object
  74. $this->content = new stdClass;
  75. $this->content->text = '';
  76. $this->content->footer = '';
  77. if (empty($this->instance)) {
  78. return $this->content;
  79. }
  80. if (!isset($this->config)) {
  81. // The block has yet to be configured - just display configure message in
  82. // the block if user has permission to configure it
  83. if (has_capability('block/rss_client:manageanyfeeds', $this->context)) {
  84. $this->content->text = get_string('feedsconfigurenewinstance2', 'block_rss_client');
  85. }
  86. return $this->content;
  87. }
  88. // How many feed items should we display?
  89. $maxentries = 5;
  90. if ( !empty($this->config->shownumentries) ) {
  91. $maxentries = intval($this->config->shownumentries);
  92. }elseif( isset($CFG->block_rss_client_num_entries) ) {
  93. $maxentries = intval($CFG->block_rss_client_num_entries);
  94. }
  95. /* ---------------------------------
  96. * Begin Normal Display of Block Content
  97. * --------------------------------- */
  98. $renderer = $this->page->get_renderer('block_rss_client');
  99. $block = new \block_rss_client\output\block();
  100. if (!empty($this->config->rssid)) {
  101. list($rssidssql, $params) = $DB->get_in_or_equal($this->config->rssid);
  102. $rssfeeds = $DB->get_records_select('block_rss_client', "id $rssidssql", $params);
  103. if (!empty($rssfeeds)) {
  104. $showtitle = false;
  105. if (count($rssfeeds) > 1) {
  106. // When many feeds show the title for each feed.
  107. $showtitle = true;
  108. }
  109. foreach ($rssfeeds as $feed) {
  110. if ($renderablefeed = $this->get_feed($feed, $maxentries, $showtitle)) {
  111. $block->add_feed($renderablefeed);
  112. }
  113. }
  114. $footer = $this->get_footer($rssfeeds);
  115. }
  116. }
  117. $this->content->text = $renderer->render_block($block);
  118. if (isset($footer)) {
  119. $this->content->footer = $renderer->render_footer($footer);
  120. }
  121. return $this->content;
  122. }
  123. function instance_allow_multiple() {
  124. return true;
  125. }
  126. function has_config() {
  127. return true;
  128. }
  129. function instance_allow_config() {
  130. return true;
  131. }
  132. /**
  133. * Returns the html of a feed to be displaed in the block
  134. *
  135. * @param mixed feedrecord The feed record from the database
  136. * @param int maxentries The maximum number of entries to be displayed
  137. * @param boolean showtitle Should the feed title be displayed in html
  138. * @return block_rss_client\output\feed|null The renderable feed or null of there is an error
  139. */
  140. public function get_feed($feedrecord, $maxentries, $showtitle) {
  141. global $CFG;
  142. require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
  143. $simplepiefeed = new moodle_simplepie($feedrecord->url);
  144. if(isset($CFG->block_rss_client_timeout)){
  145. $simplepiefeed->set_cache_duration($CFG->block_rss_client_timeout * 60);
  146. }
  147. if ($simplepiefeed->error()) {
  148. debugging($feedrecord->url .' Failed with code: '.$simplepiefeed->error());
  149. return null;
  150. }
  151. if(empty($feedrecord->preferredtitle)){
  152. $feedtitle = $this->format_title($simplepiefeed->get_title());
  153. }else{
  154. $feedtitle = $this->format_title($feedrecord->preferredtitle);
  155. }
  156. if (empty($this->config->title)){
  157. //NOTE: this means the 'last feed' displayed wins the block title - but
  158. //this is exiting behaviour..
  159. $this->title = strip_tags($feedtitle);
  160. }
  161. $feed = new \block_rss_client\output\feed($feedtitle, $showtitle, $this->config->block_rss_client_show_channel_image);
  162. if ($simplepieitems = $simplepiefeed->get_items(0, $maxentries)) {
  163. foreach ($simplepieitems as $simplepieitem) {
  164. try {
  165. $item = new \block_rss_client\output\item(
  166. $simplepieitem->get_id(),
  167. new moodle_url($simplepieitem->get_link()),
  168. $simplepieitem->get_title(),
  169. $simplepieitem->get_description(),
  170. new moodle_url($simplepieitem->get_permalink()),
  171. $simplepieitem->get_date('U'),
  172. $this->config->display_description
  173. );
  174. $feed->add_item($item);
  175. } catch (moodle_exception $e) {
  176. // If there is an error with the RSS item, we don't
  177. // want to crash the page. Specifically, moodle_url can
  178. // throw an exception of the param is an extremely
  179. // malformed url.
  180. debugging($e->getMessage());
  181. }
  182. }
  183. }
  184. // Feed image.
  185. if ($imageurl = $simplepiefeed->get_image_url()) {
  186. try {
  187. $image = new \block_rss_client\output\channel_image(
  188. new moodle_url($imageurl),
  189. $simplepiefeed->get_image_title(),
  190. new moodle_url($simplepiefeed->get_image_link())
  191. );
  192. $feed->set_image($image);
  193. } catch (moodle_exception $e) {
  194. // If there is an error with the RSS image, we don'twant to
  195. // crash the page. Specifically, moodle_url can throw an
  196. // exception if the param is an extremely malformed url.
  197. debugging($e->getMessage());
  198. }
  199. }
  200. return $feed;
  201. }
  202. /**
  203. * Strips a large title to size and adds ... if title too long
  204. *
  205. * @param string title to shorten
  206. * @param int max character length of title
  207. * @return string title s() quoted and shortened if necessary
  208. */
  209. function format_title($title,$max=64) {
  210. if (core_text::strlen($title) <= $max) {
  211. return s($title);
  212. } else {
  213. return s(core_text::substr($title,0,$max-3).'...');
  214. }
  215. }
  216. /**
  217. * cron - goes through all the feeds. If the feed has a skipuntil value
  218. * that is less than the current time cron will attempt to retrieve it
  219. * with the cache duration set to 0 in order to force the retrieval of
  220. * the item and refresh the cache.
  221. *
  222. * If a feed fails then the skipuntil time of that feed is set to be
  223. * later than the next expected cron time. The amount of time will
  224. * increase each time the fetch fails until the maximum is reached.
  225. *
  226. * If a feed that has been failing is successfully retrieved it will
  227. * go back to being handled as though it had never failed.
  228. *
  229. * CRON should therefor process requests for permanently broken RSS
  230. * feeds infrequently, and temporarily unavailable feeds will be tried
  231. * less often until they become available again.
  232. *
  233. * @return boolean Always returns true
  234. */
  235. function cron() {
  236. global $CFG, $DB;
  237. require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
  238. // Get the legacy cron time, strangely the cron property of block_base
  239. // does not seem to get set. This means we must retrive it here.
  240. $this->cron = $DB->get_field('block', 'cron', array('name' => 'rss_client'));
  241. // We are going to measure execution times
  242. $starttime = microtime();
  243. $starttimesec = time();
  244. // Fetch all site feeds.
  245. $rs = $DB->get_recordset('block_rss_client');
  246. $counter = 0;
  247. mtrace('');
  248. foreach ($rs as $rec) {
  249. mtrace(' ' . $rec->url . ' ', '');
  250. // Skip feed if it failed recently.
  251. if ($starttimesec < $rec->skipuntil) {
  252. mtrace('skipping until ' . userdate($rec->skipuntil));
  253. continue;
  254. }
  255. // Fetch the rss feed, using standard simplepie caching
  256. // so feeds will be renewed only if cache has expired
  257. core_php_time_limit::raise(60);
  258. $feed = new moodle_simplepie();
  259. // set timeout for longer than normal to be agressive at
  260. // fetching feeds if possible..
  261. $feed->set_timeout(40);
  262. $feed->set_cache_duration(0);
  263. $feed->set_feed_url($rec->url);
  264. $feed->init();
  265. if ($feed->error()) {
  266. // Skip this feed (for an ever-increasing time if it keeps failing).
  267. $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
  268. $rec->skipuntil = time() + $rec->skiptime;
  269. $DB->update_record('block_rss_client', $rec);
  270. mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
  271. } else {
  272. mtrace ('ok');
  273. // It worked this time, so reset the skiptime.
  274. if ($rec->skiptime > 0) {
  275. $rec->skiptime = 0;
  276. $rec->skipuntil = 0;
  277. $DB->update_record('block_rss_client', $rec);
  278. }
  279. // Only increase the counter when a feed is sucesfully refreshed.
  280. $counter ++;
  281. }
  282. }
  283. $rs->close();
  284. // Show times
  285. mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
  286. return true;
  287. }
  288. /**
  289. * Calculates a new skip time for a record based on the current skip time.
  290. *
  291. * @param int $currentskip The curreent skip time of a record.
  292. * @return int A new skip time that should be set.
  293. */
  294. protected function calculate_skiptime($currentskip) {
  295. // The default time to skiptime.
  296. $newskiptime = $this->cron * 1.1;
  297. if ($currentskip > 0) {
  298. // Double the last time.
  299. $newskiptime = $currentskip * 2;
  300. }
  301. if ($newskiptime > self::CLIENT_MAX_SKIPTIME) {
  302. // Do not allow the skip time to increase indefinatly.
  303. $newskiptime = self::CLIENT_MAX_SKIPTIME;
  304. }
  305. return $newskiptime;
  306. }
  307. }