feedmanager.module 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113
  1. <?php
  2. /**
  3. * Feed Manager
  4. * @description: Provides a standard interface to manage RSS feeds
  5. *
  6. * @author: Mike Carter <mike @ www.ixis.co.uk/contact>
  7. *
  8. * @todo: link feedmanager_opml() sources page
  9. */
  10. /**
  11. * Implementation of hook_help().
  12. */
  13. function feedmanager_help($section) {
  14. switch ($section) {
  15. case 'admin/help#aggregator':
  16. $output = '<p>'. t('The news aggregator is a powerful on-site RSS syndicator/news reader that can gather fresh content from news sites and weblogs around the web.') .'</p>';
  17. $output .= '<p>'. t('Users can view the latest news chronologically in the <a href="%aggregator">main news aggregator display</a> or by <a href="%aggregator-sources">source</a>. Administrators can add, edit and delete feeds and choose how often to check for newly updated news for each individual feed. Administrators can also tag individual feeds with categories, offering selective grouping of some feeds into separate displays. Listings of the latest news for individual sources or categorized sources can be enabled as blocks for display in the sidebar through the <a href="%admin-block">block administration page</a>. The news aggregator requires cron to check for the latest news from the sites to which you have subscribed. Drupal also provides a <a href="%aggregator-opml">machine-readable OPML file</a> of all of your subscribed feeds.', array('%aggregator' => url('aggregator'), '%aggregator-sources' => url('aggregator/sources'), '%admin-block' => url('admin/block'), '%aggregator-opml' => url('aggregator/opml'))) .'</p>';
  18. $output .= t('<p>You can</p>
  19. <ul>
  20. <li>administer your list of news feeds <a href="%admin-aggregator">administer &gt;&gt; aggregator</a>.</li>
  21. <li>add a new feed <a href="!admin-aggregator-add-feed">administer &gt;&gt; aggregator &gt;&gt; add feed</a>.</li>
  22. <li>add a new category <a href="!admin-aggregator-add-category">administer &gt;&gt; aggregator &gt;&gt; add category</a>.</li>
  23. <li>configure global settings for the news aggregator <a href="%admin-settings-aggregator">administer &gt;&gt; settings &gt;&gt; aggregator</a>.</li>
  24. <li>control access to the aggregator module through access permissions <a href="!admin-access">administer &gt;&gt; access control &gt;&gt; permissions</a>.</li>
  25. <li>set permissions to access new feeds for user roles such as anonymous users at <a href="!admin-access">administer &gt;&gt; access control</a>.</li>
  26. <li>view the <a href="%aggregator">aggregator page</a>.</li>
  27. </ul>
  28. ', array('!admin-aggregator' => url('admin/content/aggregator'), '!admin-aggregator-add-feed' => url('admin/content/aggregator/add/feed'), '!admin-aggregator-add-category' => url('admin/content/aggregator/add/category'), '!admin-settings-aggregator' => url('admin/settings/feedmanager'), '!admin-access' => url('admin/access'), '%aggregator' => url('aggregator')));
  29. $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="!aggregator">Aggregator page</a>.', array('!aggregator' => 'http://drupal.org/handbook/modules/aggregator/')) .'</p>';
  30. return $output;
  31. case 'admin/aggregator':
  32. return t('<p>Thousands of sites (particularly news sites and weblogs) publish their latest headlines and/or stories in a machine-readable format so that other sites can easily link to them. This content is usually in the form of an <a href="http://blogs.law.harvard.edu/tech/rss">RSS</a> feed (which is an XML-based syndication standard). To display the feed or category in a block you must decide how many items to show by editing the feed or block and turning on the <a href="%block">feed\'s block</a>.</p>', array('%block' => url('admin/block')));
  33. case 'admin/content/aggregator/add/feed':
  34. return t('<p>Add a site that has an RSS/RDF/Atom feed. The URL is the full path to the feed file. For the feed to update automatically you must run "cron.php" on a regular basis. If you already have a feed with the URL you are planning to use, the system will not accept another feed with the same URL.</p>');
  35. case 'admin/content/aggregator/add/category':
  36. return t('<p>Categories provide a way to group items from different news feeds together. Each news category has its own feed page and block. For example, you could tag various sport-related feeds as belonging to a category called <em>Sports</em>. News items can be added to a category automatically by setting a feed to automatically place its item into that category, or by using the categorize items link in any listing of news items.</p>');
  37. }
  38. }
  39. /**
  40. * Implementation of hook_menu().
  41. */
  42. function feedmanager_menu($may_cache) {
  43. $items = array();
  44. $edit = user_access('administer news feeds');
  45. $view = user_access('access news feeds');
  46. if ($may_cache) {
  47. $items[] = array('path' => 'aggregator/sources',
  48. 'title' => t('sources'),
  49. 'callback' => 'feedmanager_page_sources',
  50. 'access' => $view);
  51. $items[] = array('path' => 'admin/content/aggregator',
  52. 'title' => t('News aggregator'),
  53. 'description' => t("Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized."),
  54. 'callback' => 'feedmanager_view',
  55. 'access' => $edit);
  56. $items[] = array('path' => 'admin/content/aggregator/add/feed',
  57. 'title' => t('Add'),
  58. 'callback' => 'feedmanager_form_feed',
  59. 'access' => $edit,
  60. 'type' => MENU_LOCAL_TASK);
  61. $items[] = array('path' => 'admin/content/aggregator/remove',
  62. 'title' => t('Remove'),
  63. 'callback' => 'feedmanager_remove',
  64. 'access' => $edit,
  65. 'type' => MENU_CALLBACK);
  66. $items[] = array('path' => 'admin/content/aggregator/update',
  67. 'title' => t('Update'),
  68. 'callback' => 'feedmanager_admin_refresh_feed',
  69. 'access' => $edit,
  70. 'type' => MENU_CALLBACK);
  71. $items[] = array('path' => 'admin/content/aggregator/list',
  72. 'title' => t('List'),
  73. 'type' => MENU_DEFAULT_LOCAL_TASK,
  74. 'weight' => -10);
  75. $items[] = array('path' => 'admin/settings/feedmanager',
  76. 'title' => t('Feed Manager'),
  77. 'description' => 'Provides a management interface for RSS/RDF/Atom feeds.',
  78. 'callback' => 'drupal_get_form',
  79. 'callback arguments' => 'feedmanager_admin_settings_form',
  80. 'access' => user_access('administer site configuration'),
  81. 'type' => MENU_NORMAL_ITEM,
  82. );
  83. }
  84. else if (arg(2) == 'aggregator' && is_numeric(arg(5))) {
  85. if (arg(4) == 'feed') {
  86. $feed = feedmanager_get_feed(arg(5));
  87. if ($feed) {
  88. $items[] = array('path' => 'admin/content/aggregator/edit/feed/'. $feed['fid'],
  89. 'title' => t('Edit'),
  90. 'callback' => 'feedmanager_form_feed',
  91. 'callback arguments' => array($feed),
  92. 'access' => $edit,
  93. 'type' => MENU_CALLBACK);
  94. }
  95. }
  96. }
  97. return $items;
  98. }
  99. /**
  100. * Implementation of hook_requirements().
  101. */
  102. function feedmanager_requirements($phase) {
  103. $requirements = array();
  104. if ($phase == 'runtime') {
  105. // Raise warning if no processors are installed
  106. $processors = module_invoke_all('feedapi', $feed, 'processor_name');
  107. if (count($processors) < 1) {
  108. $requirements['feedman ager'] = array(
  109. 'title' => t('Feed Manager'),
  110. 'description' => t('At least one feed processor needs to be installed.'),
  111. 'severity' => REQUIREMENT_ERROR,
  112. 'value' => t('Not configured'),
  113. );
  114. }
  115. }
  116. return $requirements;
  117. }
  118. function feedmanager_admin_settings_form() {
  119. $form = array();
  120. $form['feedprocessing'] = array(
  121. '#type' => 'fieldset',
  122. '#title' => t('Feed Parsing')
  123. );
  124. $feedmanager_count['0'] = t('None');
  125. $feedmanager_count += drupal_map_assoc(array(1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 100));
  126. $feedmanager_count['9999999'] = t('All');
  127. $form['feedprocessing']['feedmanager_cron_count'] = array (
  128. '#type' => 'select',
  129. '#title' => t('Update count'),
  130. '#default_value' => variable_get('feedmanager_cron_count', 5),
  131. '#options' => $feedmanager_count,
  132. '#description' => t('Select how many feeds can be updated in one cron run.')
  133. );
  134. $form['feedprocessing']['feedmanager_timeout'] = array(
  135. '#type' => 'select',
  136. '#options' => drupal_map_assoc(array(10,20,30,40,50,60)),
  137. '#title' => t('Feed Timeout'),
  138. '#description' => t('Define how many seconds to wait before giving up when attempting to load a feed.'),
  139. '#default_value' => variable_get('feedmanager_timeout', 20)
  140. );
  141. $vocabularies = taxonomy_get_vocabularies();
  142. $vocabularies_list = array(-1 => t('None'));
  143. foreach($vocabularies as $vocabulary) {
  144. $vocabularies_list[$vocabulary->vid] = check_plain($vocabulary->name);
  145. }
  146. $form['feedprocessing']['feedmanager_vocabulary'] = array(
  147. '#type' => 'select',
  148. '#multiple' => FALSE,
  149. '#title' => t('Vocabulary'),
  150. '#default_value' => variable_get('feedmanager_vocabulary', -1),
  151. '#description' => t('Used to store taxonomy terms in for feeds.'),
  152. '#options' => $vocabularies_list,
  153. );
  154. $form['feedsources'] = array(
  155. '#type' => 'fieldset',
  156. '#title' => t('Feed Sources'),
  157. '#description' => t('Control the layout of feed items on the !url pages.', array('!url' => l('aggregator sources', 'aggregator/sources')))
  158. );
  159. $items = array(0 => t('none')) + drupal_map_assoc(array(3, 5, 10, 15, 20, 25), '_feedmanager_items');
  160. $form['feedsources']['aggregator_summary_items'] = array(
  161. '#type' => 'select',
  162. '#title' => t('Items shown in sources and categories pages') ,
  163. '#default_value' => variable_get('aggregator_summary_items', 3),
  164. '#options' => $items,
  165. '#description' => t('The number of items which will be shown with each feed or category in the feed and category summary pages.')
  166. );
  167. $form['fullstory'] = array(
  168. '#type' => 'fieldset',
  169. '#title' => t('Full Story links')
  170. );
  171. $form['fullstory']['feedmanager_showfullstory'] = array(
  172. '#type' => 'checkbox',
  173. '#title' => t('Show a full story link'),
  174. '#description' => t('When available in the feed item, a link back to the original post will be displayed at the end of a node.'),
  175. '#default_value' => variable_get('feedmanager_showfullstory', true),
  176. );
  177. $options = array(
  178. 'default' => t('Default (no target attribute)'),
  179. '_top' => t('Open link in window root'),
  180. '_blank' => t('Open link in new window'),
  181. );
  182. $form['fullstory']['feedmanager_target'] = array(
  183. '#type' => 'radios',
  184. '#title' => t('Link Target'),
  185. '#default_value' => variable_get('feedmanager_target', 'default'),
  186. '#options' => $options,
  187. );
  188. $form['fullstory']['feedmanager_rel'] = array(
  189. '#type' => 'checkbox',
  190. '#return_value' => 'nofollow',
  191. '#prefix' => '<div class="form-item"><label>Nofollow Value: </label>',
  192. '#suffix' => '</div>',
  193. '#title' => t('Add rel=&quot;nofollow&quot; Attribute'),
  194. '#description' => t('The <a href="http://en.wikipedia.org/wiki/Nofollow#rel.3D.22nofollow.22">rel=&quot;nofollow&quot; attribute</a> prevents some search engines from spidering entered links.'),
  195. '#default_value' => variable_get('feedmanager_rel', false),
  196. );
  197. return system_settings_form($form);
  198. }
  199. /**
  200. * Implementation of hook_perm().
  201. */
  202. function feedmanager_perm() {
  203. return array('administer news feeds', 'access news feeds');
  204. }
  205. /**
  206. * Generate a form to add/edit feed sources.
  207. */
  208. function feedmanager_form_feed($edit = array()) {
  209. $op = arg(3);
  210. switch($op) {
  211. case 'edit':
  212. $form = drupal_get_form('feedmanager_form_feed_edit', $edit);
  213. break;
  214. case 'add':
  215. $form = drupal_get_form('feedmanager_form_feed_add');
  216. break;
  217. }
  218. return $form;
  219. }
  220. function feedmanager_form_feed_add() {
  221. $form = array();
  222. $feed = FALSE;
  223. $form['url'] = array('#type' => 'textfield',
  224. '#title' => t('URL'),
  225. '#default_value' => $edit['url'],
  226. '#size' => 100,
  227. '#description' => t('The fully-qualified URL of the feed.'),
  228. '#required' => TRUE,
  229. );
  230. // We set feeds to not update until they have been edited
  231. $form['refresh'] = array('#type' => 'value', '#value' => -1);
  232. $processors = module_invoke_all('feedapi', $feed, 'processor_name');
  233. $form['processor'] = array(
  234. '#type' => 'select',
  235. '#title' => t('Processor'),
  236. '#description' => t("Each feed can have it's items generated by a different processor. This allows you to generate different types of content per feed - such as nodes or traditional aggregator items. Re-edit the feed to see processor specific settings."),
  237. '#options' => $processors,
  238. '#default_value' => $edit['processor'],
  239. '#size' => 1,
  240. '#required' => TRUE,
  241. );
  242. $form['submit'] = array(
  243. '#type' => 'submit',
  244. '#value' => t('Submit'),
  245. '#weight' => 35,
  246. );
  247. return $form;
  248. }
  249. function feedmanager_form_feed_edit($edit = array()) {
  250. $form = array();
  251. $form['title'] = array('#type' => 'textfield',
  252. '#title' => t('Title'),
  253. '#default_value' => $edit['title'],
  254. '#maxlength' => 64,
  255. '#description' => t('The name of the feed; typically the name of the web site you syndicate content from.'),
  256. '#required' => TRUE,
  257. );
  258. $form['url'] = array('#type' => 'textfield',
  259. '#title' => t('URL'),
  260. '#default_value' => $edit['url'],
  261. '#size' => 100,
  262. '#description' => t('The fully-qualified URL of the feed.'),
  263. '#attributes' => array('readonly' => 'readonly')
  264. );
  265. $form['description'] = array('#type' => 'textarea',
  266. '#title' => t('Description'),
  267. '#default_value' => $edit['description'],
  268. '#rows' => 5,
  269. '#description' => t('As supplied by the RSS feed.'),
  270. );
  271. $period = array(0 => t('never')) + drupal_map_assoc(array(300, 900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
  272. if ($edit['refresh'] == -1) {
  273. $edit['refresh'] = 3600;
  274. }
  275. $form['refresh'] = array('#type' => 'select',
  276. '#title' => t('Update interval'),
  277. '#default_value' => $edit['refresh'],
  278. '#options' => $period,
  279. '#description' => t('The refresh interval indicating how often you want to update this feed. Requires !cron.', array('!cron' => l('crontab', 'admin/help/system') )),
  280. );
  281. $period = array(0 => t('never')) + drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
  282. $form['expires'] = array(
  283. '#type' => 'select',
  284. '#title' => t('Discard news items older than'),
  285. '#default_value' => $edit['expires'],
  286. '#options' => $period,
  287. '#description' => t('Older news items will be automatically discarded. Requires !cron.', array('!cron' => l('crontab', 'admin/help/system') ))
  288. );
  289. // Handling of categories:
  290. $form += feedmanager_freetag($edit);
  291. $form['advanced'] = array(
  292. '#type' => 'fieldset',
  293. '#title' => t('Advanced'),
  294. '#description' => t('These settings are specific to the type of feed processor you have selected.'),
  295. '#collapsible' => TRUE,
  296. '#collapsed' => FALSE,
  297. '#tree' => TRUE
  298. );
  299. $form['advanced']['update_items'] = array(
  300. '#type' => 'checkbox',
  301. '#title' => t('Update news items'),
  302. '#description' => t('If the same news item is found in the feed on the next update the contents will overwrite the previous version stored here.'),
  303. '#default_value' => $edit['update_items'] ? $edit['update_items'] : FALSE,
  304. );
  305. $form['advanced']['processor'] = array('#type' => 'value', '#value' => $edit['processor']);
  306. $form['submit'] = array(
  307. '#type' => 'submit',
  308. '#value' => t('Submit'),
  309. '#weight' => 35,
  310. );
  311. if ($edit['fid']) {
  312. $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'), '#weight' => 19);
  313. $form['fid'] = array('#type' => 'value', '#value' => $edit['fid']);
  314. }
  315. return $form;
  316. }
  317. /**
  318. * Provides a freetagging form element.
  319. *
  320. * Code lifted from the taxonomy.module hook_form_alter()
  321. */
  322. function feedmanager_freetag(&$feed) {
  323. $form = array();
  324. $terms = $feed['taxonomy'];
  325. // Find the vocabulary (if any) to use
  326. $vid = variable_get('feedmanager_vocabulary', -1);
  327. if($vid && $vocabulary = taxonomy_get_vocabulary($vid)) {
  328. if($terms) {
  329. $typed_terms = array();
  330. foreach ($terms as $term) {
  331. // Extract terms belonging to the vocabulary in question.
  332. if ($term->vid == $vocabulary->vid) {
  333. // Commas and quotes in terms are special cases, so encode 'em.
  334. if (preg_match('/,/', $term->name) || preg_match('/"/', $term->name)) {
  335. $term->name = '"'.preg_replace('/"/', '""', $term->name).'"';
  336. }
  337. $typed_terms[] = $term->name;
  338. }
  339. }
  340. $typed_string = implode(', ', $typed_terms) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL);
  341. }
  342. if ($vocabulary->help) {
  343. $help = $vocabulary->help;
  344. }
  345. else {
  346. $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".');
  347. }
  348. $form['taxonomy']['tags'] = array('#type' => 'textfield',
  349. '#title' => $vocabulary->name,
  350. '#description' => $help,
  351. '#required' => $vocabulary->required,
  352. '#default_value' => $typed_string,
  353. '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid,
  354. '#weight' => $vocabulary->weight,
  355. '#maxlength' => 255,
  356. );
  357. }
  358. else
  359. {
  360. $form['taxonomy_help'] = array(
  361. '#value' => t('You can define detault tags which will be added to every new feed item. To do this you need to !url to store feed tags.', array('!url' => l(t('assign a vocabulary'), 'admin/settings/feedmanager', NULL, 'destination=admin/content/aggregator/edit/feed/'.$feed['fid']))),
  362. );
  363. }
  364. return $form;
  365. }
  366. /**
  367. * Validate feedmanager_form_feed form submissions.
  368. */
  369. function feedmanager_form_feed_add_validate($form_id, $form_values) {
  370. // Check for duplicate feed urls
  371. if (isset($form_values['fid'])) {
  372. $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE (title = '%s' OR url='%s') AND fid != %d", $form_values['title'], $form_values['url'], $form_values['fid']);
  373. }
  374. else {
  375. $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = '%s' OR url='%s'", $form_values['title'], $form_values['url']);
  376. }
  377. while ($feed = db_fetch_array($result)) {
  378. if (strcasecmp($feed['url'], $form_values['url']) == 0) {
  379. form_set_error('url', t('A feed with this URL %url already exists. Please enter a unique URL.', array('%url' => $form_values['url'])));
  380. }
  381. }
  382. }
  383. function feedmanager_form_feed_add_submit($form_id, $form_values) {
  384. $feed = feedmanager_query_feed($form_values['url']);
  385. // Create a Feed title if none is available
  386. if(!$feed_title = $feed->get_feed_title()) {
  387. $urlinfo = parse_url($form_values['url']);
  388. $feed_title = $urlinfo['host'];
  389. }
  390. $form_values['title'] = $feed_title;
  391. $form_values['description'] = $feed->get_feed_description();
  392. $form_values['advanced']['processor'] = $form_values['processor'];
  393. feedmanager_save_feed($form_values);
  394. // Allow editing of further details
  395. drupal_goto('admin/content/aggregator/edit/feed/'.$form_values['fid']);
  396. }
  397. /**
  398. * Process feedmanager_form_feed form submissions.
  399. * @todo Add delete confirmation dialog.
  400. */
  401. function feedmanager_form_feed_edit_submit($form_id, $form_values) {
  402. if ($_POST['op'] == t('Delete')) {
  403. $title = $form_values['title'];
  404. // Unset the title:
  405. unset($form_values['title']);
  406. }
  407. feedmanager_save_feed($form_values);
  408. menu_rebuild();
  409. if (isset($form_values['fid'])) {
  410. if (isset($form_values['title'])) {
  411. drupal_set_message(t('The feed %feed has been updated.', array('%feed' => $form_values['title'])));
  412. if (arg(0) == 'admin') {
  413. return 'admin/content/aggregator/';
  414. }
  415. else {
  416. return 'aggregator/sources/'. $form_values['fid'];
  417. }
  418. }
  419. else {
  420. watchdog('aggregator', t('Feed %feed deleted.', array('%feed' => theme('placeholder', $title))));
  421. drupal_set_message(t('The feed %feed has been deleted.', array('%feed' => theme('placeholder', $title))));
  422. module_invoke($form_values['processor'], 'delete_feed', $form_values);
  423. if (arg(0) == 'admin') {
  424. return 'admin/content/aggregator';
  425. }
  426. else {
  427. return 'aggregator/sources';
  428. }
  429. }
  430. }
  431. else {
  432. watchdog('aggregator', t('Feed %feed added.', array('%feed' => $form_values['title'])), WATCHDOG_NOTICE, l(t('view'), 'admin/content/aggregator'));
  433. drupal_set_message(t('The feed %feed has been added.', array('%feed' => $form_values['title'])));
  434. }
  435. }
  436. /**
  437. * Add/edit/delete an aggregator feed.
  438. */
  439. function feedmanager_save_feed(&$edit) {
  440. if ($edit['fid']) {
  441. // An existing feed is being modified, delete the existing category listings.
  442. db_query('DELETE FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
  443. }
  444. $data = serialize($edit['advanced']);
  445. if ($edit['fid'] && $edit['title']) {
  446. db_query("UPDATE {aggregator_feed} SET title = '%s', url = '%s', refresh = %d, description = '%s', expires = %d, data = '%s' WHERE fid = %d", $edit['title'], $edit['url'], $edit['refresh'], $edit['description'], $edit['expires'], $data, $edit['fid']);
  447. }
  448. else if ($edit['fid']) {
  449. // Delete all feed category items
  450. $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
  451. while ($item = db_fetch_object($result)) {
  452. $items[] = "iid = $item->iid";
  453. }
  454. if ($items) {
  455. db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
  456. }
  457. db_query('DELETE FROM {aggregator_feed} WHERE fid = %d', $edit['fid']);
  458. db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
  459. }
  460. else if ($edit['title']) {
  461. // A single unique id for bundles and feeds, to use in blocks.
  462. $edit['fid'] = db_next_id('{aggregator_feed}_fid');
  463. db_query("INSERT INTO {aggregator_feed} (fid, title, url, refresh, description, expires, data) VALUES (%d, '%s', '%s', %d, '%s', %d, '%s')", $edit['fid'], $edit['title'], $edit['url'], $edit['refresh'], $edit['description'], $edit['expires'], $data);
  464. }
  465. if ($edit['title']) {
  466. // The feed is being saved, save the categories as well.
  467. $vid = variable_get('feedmanager_vocabulary', FALSE);
  468. if ($edit['tags'] && $vid) {
  469. $vid_value = $edit['tags'];
  470. // This regexp allows the following types of user input:
  471. // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
  472. $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
  473. preg_match_all($regexp, $vid_value, $matches);
  474. $typed_terms = array_unique($matches[1]);
  475. $inserted = array();
  476. foreach ($typed_terms as $typed_term) {
  477. // If a user has escaped a term (to demonstrate that it is a group,
  478. // or includes a comma or quote character), we remove the escape
  479. // formatting so to save the term into the DB as the user intends.
  480. $typed_term = str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $typed_term));
  481. $typed_term = trim($typed_term);
  482. if ($typed_term == "") { continue; }
  483. // See if the term exists in the chosen vocabulary
  484. // and return the tid, otherwise, add a new record.
  485. $possibilities = taxonomy_get_term_by_name($typed_term);
  486. $typed_term_tid = NULL; // tid match if any.
  487. foreach ($possibilities as $possibility) {
  488. if ($possibility->vid == $vid) {
  489. $typed_term_tid = $possibility->tid;
  490. }
  491. }
  492. // if the term does not already exist, add it to the feedmanager vocabulary
  493. if (!$typed_term_tid) {
  494. $term = array('vid' => $vid, 'name' => $typed_term);
  495. $status = taxonomy_save_term($term);
  496. $typed_term_tid = $term['tid'];
  497. }
  498. // defend against duplicate, different cased tags
  499. if (!isset($inserted[$typed_term_tid])) {
  500. db_query('INSERT INTO {aggregator_category_feed} (fid, cid) VALUES (%d, %d)', $edit['fid'], $typed_term_tid);
  501. $inserted[$typed_term_tid] = TRUE;
  502. }
  503. }
  504. }
  505. } else {
  506. drupal_set_message(t('Unable to add the feed due to incomplete details being provided.'));
  507. }
  508. }
  509. function feedmanager_remove() {
  510. $feed_id = (integer)arg(4);
  511. $feed = feedmanager_get_feed($feed_id);
  512. $output = drupal_get_form('feedmanager_remove_confirm', $feed);
  513. return $output;
  514. }
  515. /*
  516. feedmanager_confirm_remove
  517. */
  518. function feedmanager_remove_confirm($feed) {
  519. $form['feed_id'] = array('#type' => 'hidden', '#value' => $feed['fid']);
  520. $form['operation'] = array('#type' => 'hidden', '#value' => 'remove');
  521. $counter = module_invoke($feed['processor'], 'feedapi', $feed, 'item_count');
  522. return confirm_form($form,
  523. t('Delete Aggregator Feed Items'),
  524. 'admin/content/node',
  525. t('Are you sure you want to delete all @count?', array('@count' => format_plural($counter['item_count'], '1 item', '@count items'))) . t('This action cannot be undone.'),
  526. t('Delete all'), t('Cancel'),
  527. 'feedmanager_remove_confirm');
  528. }
  529. /**
  530. * Removes all items (and associated data) from a feed
  531. */
  532. function feedmanager_remove_confirm_submit($form_id, $form_values) {
  533. if ($form_values['operation'] == 'remove') {
  534. $feed = feedmanager_get_feed($form_values['feed_id']);
  535. module_invoke_all('feedapi', $feed, 'remove');
  536. db_query("UPDATE {aggregator_feed} SET checked = 0, etag = '', modified = 0 WHERE fid = %d", $feed['fid']);
  537. drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed['title'])));
  538. }
  539. return 'admin/content/aggregator';
  540. }
  541. function feedmanager_query_feed($url) {
  542. include_once 'simplepie.inc';
  543. $pie_feed = new SimplePie();
  544. $pie_feed->enable_caching(false);
  545. $pie_feed->timeout = variable_get('feedmanager_timeout', 20);
  546. $pie_feed->cache_max_minutes(240); // configuration option
  547. $pie_feed->feed_url($url); // process the feed URL
  548. $pie_feed->init(); // process the feed
  549. $pie_feed->handle_content_type(); // set content type and character encoding
  550. // Check if there's any problems loading the feed
  551. $feed_error = $pie_feed->error;
  552. if (isset($feed_error)) {
  553. watchdog('aggregator', t('The RSS-feed from %site seems to be broken, due to "%error".', array('%site' => theme('placeholder', $pie_feed->get_feed_title()), '%error' => $feed_error)), WATCHDOG_WARNING);
  554. drupal_set_message(t('The RSS-feed from %site seems to be broken, because of error "%error".', array('%site' => theme('placeholder', $pie_feed->get_feed_title()), '%error' => $feed_error)));
  555. return FALSE;
  556. }
  557. return $pie_feed;
  558. }
  559. /**
  560. * Checks a news feed for new items.
  561. */
  562. function feedmanager_refresh($feed) {
  563. // Load The Feed
  564. $pie_feed = feedmanager_query_feed($feed['url']);
  565. if(!$pie_feed) return FALSE;
  566. if (!$feed['data'] = $pie_feed->data) return FALSE;
  567. $pie_feed->strip_ads($feed['stripads']); // configuration option
  568. $feed['items'] = $pie_feed->get_items();
  569. $feed_links = $pie_feed->get_feed_links();
  570. $feed['link'] = array();
  571. $feed['link']['self'] = $feed_links['self'] ? $feed_links['self'][0] : $feed['url'];
  572. $feed['link']['alternate'] = $feed_links['alternate'] ? $feed_links['alternate'][0] : $feed['link']['self'];
  573. $feed['description'] = $pie_feed->get_feed_description();
  574. $feed['expires'] = 0;
  575. $feed['modified'] = 0;
  576. $feed['etag'] = 0;
  577. // end simplepie specific
  578. if ($pie_feed->get_image_link() && $pie_feed->get_image_url()) { // FIXME: use theme_image
  579. $feed['image_html'] = '<a href="'. $pie_feed->get_image_link() .'" class="feed-image"><img src="'. check_url($pie_feed->get_image_url()) .'" alt="'. check_plain($pie_feed->get_image_title()) .'" /></a>';
  580. }
  581. else {
  582. $feed['image_html'] = NULL;
  583. }
  584. // update feed information in database
  585. db_query("UPDATE {aggregator_feed} SET url = '%s', checked = %d, link = '%s', description = '%s', image = '%s', etag = '%s', modified = %d WHERE fid = %d", $feed['link']['self'], time(), $feed['link']['alternate'], $feed['description'], $feed['image_html'], $feed['etag'], $feed['modified'], $feed['fid']);
  586. // process all feed items
  587. $processed_items = 0;
  588. foreach($feed['items'] as $item) {
  589. $item->data['iid'] = db_next_id('{feedparser}_iid');
  590. $r = module_invoke_all('feedapi', $feed, 'item_save', $item);
  591. // if an item is saved, increase counter
  592. if($r['item_save']) {
  593. $processed_items++;
  594. }
  595. }
  596. // Log how many items were added and/or updated from a feed
  597. if($processed_items > 0) {
  598. watchdog('aggregator', t('There is %items new syndicated items from %site.', array('%items' => $processed_items, '%site' => $feed['title']) ));
  599. }
  600. // Clear out any old feed items
  601. if($feed['expires'] != 0) {
  602. module_invoke($feed['processor'], 'feedapi', $feed, 'expire_items');
  603. }
  604. }
  605. /**
  606. * Gets information about a feed, either from the dbase or a temp cache
  607. */
  608. function feedmanager_get_feed($fid) {
  609. static $feeds;
  610. if(!$feeds[$fid] && $fid) {
  611. $feed = db_fetch_array(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', $fid));
  612. $data = unserialize($feed['data']);
  613. $feed += (array)$data;
  614. unset($feed['data']);
  615. $feed['taxonomy'] = array();
  616. $terms = db_query('SELECT cid, fid FROM {aggregator_category_feed} WHERE fid = %d', $feed['fid']);
  617. while ($term = db_fetch_object($terms)) {
  618. if ($term->fid) $feed['taxonomy'][] = taxonomy_get_term($term->cid);
  619. }
  620. $feeds[$fid] = $feed;
  621. }
  622. return $feeds[$fid];
  623. }
  624. function feedmanager_view() {
  625. $output .= '<h3>'. t('Feed overview') .'</h3>';
  626. $header = array(t('Title'), t('Items'), t('Last update'), t('Next update'), array('data' => t('Operations'), 'colspan' => '3'));
  627. $rows = array();
  628. $result = db_query('SELECT fid FROM {aggregator_feed} ORDER BY title');
  629. while ($feed = db_fetch_array($result)) {
  630. $feed = feedmanager_get_feed($feed['fid']);
  631. $item_count = module_invoke($feed['processor'], 'feedapi', $feed, 'item_count');
  632. // If A Feed Processor Implements Listing of Items Provide A Link
  633. if(module_hook($feed['processor'], 'list_items')) {
  634. $item_list_link = l($feed['title'], "aggregator/sources/{$feed['fid']}");
  635. } else {
  636. $item_list_link = check_plain($feed['title']);
  637. }
  638. $rows[] = array(
  639. $item_list_link,
  640. format_plural($item_count['item_count'], '1 item', '@count items'),
  641. ($feed['checked'] ? t('%time ago', array('%time' => format_interval(time() - $feed['checked']))) : t('never')),
  642. ($feed['checked'] ? t('%time left', array('%time' => format_interval($feed['checked'] + $feed['refresh'] - time()))) : t('never')),
  643. l(t('edit'), "admin/content/aggregator/edit/feed/{$feed['fid']}"),
  644. l(t('remove items'), "admin/content/aggregator/remove/{$feed['fid']}"),
  645. l(t('update items'), "admin/content/aggregator/update/{$feed['fid']}"));
  646. }
  647. $output .= theme('table', $header, $rows);
  648. return $output;
  649. }
  650. /**
  651. * Menu callback; removes all items from a feed, then redirects to the overview page.
  652. */
  653. function feedmanager_admin_remove_feed($feed) {
  654. feedmanager_remove(feedmanager_get_feed($feed));
  655. drupal_goto('admin/content/aggregator');
  656. }
  657. /**
  658. * Menu callback; refreshes a feed, then redirects to the overview page.
  659. */
  660. function feedmanager_admin_refresh_feed($feed) {
  661. feedmanager_refresh(feedmanager_get_feed($feed));
  662. drupal_goto('admin/content/aggregator');
  663. }
  664. /**
  665. * Display items from a feed source
  666. */
  667. function feedmanager_page_sources() {
  668. $fid = arg(2);
  669. $output = '<div id="aggregator">';
  670. // If A feed id is provided only show items from that feed
  671. if(is_numeric($fid)) {
  672. if($feed = feedmanager_get_feed($fid)) {
  673. drupal_set_breadcrumb(array( l(t('Home'), NULL), l('Aggregator','aggregator/sources'), l($feed['title'], 'aggregator/sources/'.$feed['fid'] ) ));
  674. $output .= theme('feedmanager_feed', $feed);
  675. $output .= module_invoke($feed['processor'], 'list_items', $feed);
  676. }
  677. } else {
  678. $result = db_query('SELECT fid FROM {aggregator_feed} ORDER BY title');
  679. while ($fid = db_fetch_array($result)) {
  680. $feed = feedmanager_get_feed($fid['fid']);
  681. if(module_hook($feed['processor'], 'list_items')) {
  682. $output .= '<h2>'. check_plain($feed['title']) ."</h2>\n";
  683. $output .= module_invoke($feed['processor'], 'list_items', $feed, variable_get('aggregator_summary_items', 3));
  684. //add more link to full page
  685. }
  686. }
  687. // Add OPML XML link
  688. }
  689. $output .= '</div>';
  690. return $output;
  691. }
  692. /**
  693. * Generates an OPML representation of all feeds.
  694. */
  695. function feedmanager_opml() {
  696. $result = db_query(db_rewrite_sql('SELECT title, description, url FROM {aggregator_feed} ORDER BY title ASC'));
  697. $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  698. $output .= "<opml version=\"1.1\">\n";
  699. $output .= "<head>\n";
  700. $output .= '<title>'. variable_get('site_name', 'drupal') .' - '. variable_get('site_slogan', '') ."</title>\n";
  701. $output .= '<dateModified>'. gmdate('r') ."</dateModified>\n";
  702. $output .= "</head>\n";
  703. $output .= "<body>\n";
  704. while ($feed = db_fetch_object($result)) {
  705. $output .= '<outline title="'. check_plain($feed->title) .'" text="'. check_plain($feed->description) .'" xmlUrl="'. check_plain($feed->url) .'" />'."\n";
  706. }
  707. $output .= "</body>\n";
  708. $output .= "</opml>\n";
  709. drupal_set_header('Content-Type: text/xml; charset=utf-8');
  710. print $output;
  711. }
  712. /**
  713. * Implementation of hook_cron().
  714. *
  715. * Checks news feeds for updates once their refresh interval has elapsed.
  716. */
  717. function feedmanager_cron() {
  718. // check how many feeds we can update at a time
  719. $limit = variable_get('feedmanager_cron_count', 5);
  720. if (is_numeric($limit) && $limit > -1) {
  721. $limit = 'LIMIT '. $limit;
  722. }
  723. else {
  724. $limit = '';
  725. }
  726. $result = db_query('SELECT fid FROM {aggregator_feed} WHERE refresh != 0 AND checked + refresh < %d ORDER BY checked ASC '. $limit, time());
  727. while ($fid = db_fetch_array($result)) {
  728. $feed = feedmanager_get_feed($fid['fid']);
  729. feedmanager_refresh($feed);
  730. }
  731. }
  732. /**
  733. * Helper function for drupal_map_assoc.
  734. */
  735. function _feedmanager_items($count) {
  736. return format_plural($count, '1 item', '@count items');
  737. }
  738. /**
  739. * Safely render HTML content, as allowed.
  740. */
  741. function feedmanager_filter_xss($value) {
  742. return filter_xss($value, preg_split('/\s+|<|>/', variable_get("feedmanager_allowed_html_tags", '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'), -1, PREG_SPLIT_NO_EMPTY));
  743. }
  744. /**
  745. * Private function;
  746. * from: http://uk2.php.net/manual/en/function.html-entity-decode.php#51055
  747. * Used as callback function for preg_replace_all() to decode numeric entities to UTF-8 chars
  748. *
  749. * @param $ord Number
  750. * @return UTF-8 string
  751. */
  752. function _parse_num_entity($ord) {
  753. $ord = $ord[1];
  754. if (preg_match('/^x([0-9a-f]+)$/i', $ord, $match)) {
  755. $ord = hexdec($match[1]);
  756. }
  757. else {
  758. $ord = intval($ord);
  759. }
  760. $no_bytes = 0;
  761. $byte = array();
  762. if ($ord == 128) {
  763. return chr(226).chr(130).chr(172);
  764. }
  765. else if($ord == 129) {
  766. return chr(239).chr(191).chr(189);
  767. }
  768. else if($ord == 130) {
  769. return chr(226).chr(128).chr(154);
  770. }
  771. else if($ord == 131) {
  772. return chr(198).chr(146);
  773. }
  774. else if($ord == 132) {
  775. return chr(226).chr(128).chr(158);
  776. }
  777. else if($ord == 133) {
  778. return chr(226).chr(128).chr(166);
  779. }
  780. else if($ord == 134) {
  781. return chr(226).chr(128).chr(160);
  782. }
  783. else if($ord == 135) {
  784. return chr(226).chr(128).chr(161);
  785. }
  786. else if($ord == 136) {
  787. return chr(203).chr(134);
  788. }
  789. else if($ord == 137) {
  790. return chr(226).chr(128).chr(176);
  791. }
  792. else if($ord == 138) {
  793. return chr(197).chr(160);
  794. }
  795. else if($ord == 139) {
  796. return chr(226).chr(128).chr(185);
  797. }
  798. else if($ord == 140) {
  799. return chr(197).chr(146);
  800. }
  801. else if($ord == 141) {
  802. return chr(239).chr(191).chr(189);
  803. }
  804. else if($ord == 142) {
  805. return chr(197).chr(189);
  806. }
  807. else if($ord == 143) {
  808. return chr(239).chr(191).chr(189);
  809. }
  810. else if($ord == 144) {
  811. return chr(239).chr(191).chr(189);
  812. }
  813. else if($ord == 145) {
  814. return chr(226).chr(128).chr(152);
  815. }
  816. else if($ord == 146) {
  817. return chr(226).chr(128).chr(153);
  818. }
  819. else if($ord == 147) {
  820. return chr(226).chr(128).chr(156);
  821. }
  822. else if($ord == 148) {
  823. return chr(226).chr(128).chr(157);
  824. }
  825. else if($ord == 149) {
  826. return chr(226).chr(128).chr(162);
  827. }
  828. else if($ord == 150) {
  829. return chr(226).chr(128).chr(147);
  830. }
  831. else if($ord == 151) {
  832. return chr(226).chr(128).chr(148);
  833. }
  834. else if($ord == 152) {
  835. return chr(203).chr(156);
  836. }
  837. else if($ord == 153) {
  838. return chr(226).chr(132).chr(162);
  839. }
  840. else if($ord == 154) {
  841. return chr(197).chr(161);
  842. }
  843. else if($ord == 155) {
  844. return chr(226).chr(128).chr(186);
  845. }
  846. else if($ord == 156) {
  847. return chr(197).chr(147);
  848. }
  849. else if($ord == 157) {
  850. return chr(239).chr(191).chr(189);
  851. }
  852. else if($ord == 158) {
  853. return chr(197).chr(190);
  854. }
  855. else if($ord == 159) {
  856. return chr(197).chr(184);
  857. }
  858. else if($ord == 160) {
  859. return chr(194).chr(160);
  860. }
  861. if ($ord < 128) {
  862. return chr($ord);
  863. }
  864. else if ($ord < 2048) {
  865. $no_bytes = 2;
  866. }
  867. else if ($ord < 65536) {
  868. $no_bytes = 3;
  869. }
  870. else if ($ord < 1114112) {
  871. $no_bytes = 4;
  872. }
  873. else {
  874. return;
  875. }
  876. switch ($no_bytes) {
  877. case 2:
  878. $prefix = array(31, 192);
  879. break;
  880. case 3:
  881. $prefix = array(15, 224);
  882. break;
  883. case 4:
  884. $prefix = array(7, 240);
  885. break;
  886. }
  887. for ($i = 0; $i < $no_bytes; $i++) {
  888. $byte[$no_bytes - $i - 1] = (($ord & (63 * pow(2, 6 * $i))) / pow(2, 6 * $i)) & 63 | 128;
  889. }
  890. $byte[0] = ($byte[0] & $prefix[0]) | $prefix[1];
  891. $ret = '';
  892. for ($i = 0; $i < $no_bytes; $i++) {
  893. $ret .= chr($byte[$i]);
  894. }
  895. return $ret;
  896. }
  897. /**
  898. * Private function; Convert named entities to UTF-8 characters
  899. * from: http://pl2.php.net/manual/en/function.html-entity-decode.php#51722
  900. */
  901. function _parse_name_entities(&$data) {
  902. static $ttr;
  903. if (!$ttr) {
  904. $trans_tbl = get_html_translation_table(HTML_ENTITIES);
  905. foreach ($trans_tbl as $k => $v) {
  906. $ttr[$v] = utf8_encode($k);
  907. }
  908. $ttr['&apos;'] = "'";
  909. }
  910. return strtr($data, $ttr);
  911. }
  912. /**
  913. * Private function; Convert all entities to UTF-8 characters
  914. */
  915. function parse_entities(&$data) {
  916. $result = _parse_name_entities($data);
  917. return preg_replace_callback('/&#([0-9a-fx]+);/mi', '_parse_num_entity', $result);
  918. }
  919. /**
  920. * Format a news feed.
  921. *
  922. * @ingroup themeable
  923. */
  924. function theme_feedmanager_feed($feed) {
  925. $output = '<div class="feed-source">';
  926. $output .= theme('feed_icon', $feed['url']) ."\n";
  927. $output .= $feed['image'];
  928. $output .= '<div class="feed-description">'. feedmanager_filter_xss($feed['description']) ."</div>\n";
  929. $output .= '<div class="feed-url"><em>'. t('URL:') .'</em> '. l($feed['link'], $feed['link'], array(), NULL, NULL, TRUE) ."</div>\n";
  930. $updated = $feed['checked'] ? t('@time ago', array('@time' => format_interval(time() - $feed['checked']))) : t('never');
  931. if (user_access('administer news feeds')) {
  932. $updated = l($updated, "admin/content/aggregator/update/{$feed['fid']}", array('title' => t('Update this feed')), "destination=aggregator/sources/{$feed['fid']}", FALSE, TRUE);
  933. }
  934. $output .= '<div class="feed-updated">'. t('Updated:') . ' ' . $updated . "</div>";
  935. $output .= "</div>\n";
  936. return $output;
  937. }