aggregator.module

Used to aggregate syndicated content (RSS, RDF, and Atom).

File

drupal/core/modules/aggregator/aggregator.module
View source
  1. <?php
  2. /**
  3. * @file
  4. * Used to aggregate syndicated content (RSS, RDF, and Atom).
  5. */
  6. use Drupal\aggregator\Plugin\Core\Entity\Feed;
  7. use Drupal\Component\Plugin\Exception\PluginException;
  8. /**
  9. * Denotes that a feed's items should never expire.
  10. */
  11. const AGGREGATOR_CLEAR_NEVER = 0;
  12. /**
  13. * Implements hook_help().
  14. */
  15. function aggregator_help($path, $arg) {
  16. switch ($path) {
  17. case 'admin/help#aggregator':
  18. $output = '';
  19. $output .= '<h3>' . t('About') . '</h3>';
  20. $output .= '<p>' . t('The Aggregator module is an on-site syndicator and news reader that gathers and displays fresh content from RSS-, RDF-, and Atom-based feeds made available across the web. Thousands of sites (particularly news sites and blogs) publish their latest headlines in feeds, using a number of standardized XML-based formats. For more information, see the online handbook entry for <a href="@aggregator-module">Aggregator module</a>.', array('@aggregator-module' => 'http://drupal.org/documentation/modules/aggregator', '@aggregator' => url('aggregator'))) . '</p>';
  21. $output .= '<h3>' . t('Uses') . '</h3>';
  22. $output .= '<dl>';
  23. $output .= '<dt>' . t('Viewing feeds') . '</dt>';
  24. $output .= '<dd>' . t('Feeds contain published content, and may be grouped in categories, generally by topic. Users view feed content in the <a href="@aggregator">main aggregator display</a>, or by <a href="@aggregator-sources">their source</a> (usually via an RSS feed reader). The most recent content in a feed or category can be displayed as a block through the <a href="@admin-block">Blocks administration page</a>.', array('@aggregator' => url('aggregator'), '@aggregator-sources' => url('aggregator/sources'), '@admin-block' => url('admin/structure/block'))) . '</a></dd>';
  25. $output .= '<dt>' . t('Adding, editing, and deleting feeds') . '</dt>';
  26. $output .= '<dd>' . t('Administrators can add, edit, and delete feeds, and choose how often to check each feed for newly updated items on the <a href="@feededit">Feed aggregator administration page</a>.', array('@feededit' => url('admin/config/services/aggregator'))) . '</dd>';
  27. $output .= '<dt>' . t('OPML integration') . '</dt>';
  28. $output .= '<dd>' . t('A <a href="@aggregator-opml">machine-readable OPML file</a> of all feeds is available. OPML is an XML-based file format used to share outline-structured information such as a list of RSS feeds. Feeds can also be <a href="@import-opml">imported via an OPML file</a>.', array('@aggregator-opml' => url('aggregator/opml'), '@import-opml' => url('admin/config/services/aggregator'))) . '</dd>';
  29. $output .= '<dt>' . t('Configuring cron') . '</dt>';
  30. $output .= '<dd>' . t('A correctly configured <a href="@cron">cron maintenance task</a> is required to update feeds automatically.', array('@cron' => 'http://drupal.org/cron')) . '</dd>';
  31. $output .= '</dl>';
  32. return $output;
  33. case 'admin/config/services/aggregator':
  34. $output = '<p>' . t('Thousands of sites (particularly news sites and blogs) publish their latest headlines and posts in feeds, using a number of standardized XML-based formats. Formats supported by the aggregator include <a href="@rss">RSS</a>, <a href="@rdf">RDF</a>, and <a href="@atom">Atom</a>.', array('@rss' => 'http://cyber.law.harvard.edu/rss/', '@rdf' => 'http://www.w3.org/RDF/', '@atom' => 'http://www.atomenabled.org')) . '</p>';
  35. $output .= '<p>' . t('Current feeds are listed below, and <a href="@addfeed">new feeds may be added</a>. For each feed or feed category, the <em>latest items</em> block may be enabled at the <a href="@block">blocks administration page</a>.', array('@addfeed' => url('admin/config/services/aggregator/add/feed'), '@block' => url('admin/structure/block'))) . '</p>';
  36. return $output;
  37. case 'admin/config/services/aggregator/add/feed':
  38. return '<p>' . t('Add a feed in RSS, RDF or Atom format. A feed may only have one entry.') . '</p>';
  39. case 'admin/config/services/aggregator/add/category':
  40. return '<p>' . t('Categories allow feed items from different feeds to be grouped together. For example, several sport-related feeds may belong to a category named <em>Sports</em>. Feed items may be grouped automatically (by selecting a category when creating or editing a feed) or manually (via the <em>Categorize</em> page available from feed item listings). Each category provides its own feed page and block.') . '</p>';
  41. case 'admin/config/services/aggregator/add/opml':
  42. return '<p>' . t('<acronym title="Outline Processor Markup Language">OPML</acronym> is an XML format used to exchange multiple feeds between aggregators. A single OPML document may contain a collection of many feeds. Drupal can parse such a file and import all feeds at once, saving you the effort of adding them manually. You may either upload a local file from your computer or enter a URL where Drupal can download it.') . '</p>';
  43. }
  44. }
  45. /**
  46. * Implements hook_theme().
  47. */
  48. function aggregator_theme() {
  49. return array(
  50. 'aggregator_feed_source' => array(
  51. 'variables' => array('aggregator_feed' => NULL, 'view_mode' => NULL),
  52. 'file' => 'aggregator.pages.inc',
  53. 'template' => 'aggregator-feed-source',
  54. ),
  55. 'aggregator_block_item' => array(
  56. 'variables' => array('item' => NULL, 'feed' => 0),
  57. ),
  58. 'aggregator_summary_items' => array(
  59. 'variables' => array('summary_items' => NULL, 'source' => NULL),
  60. 'file' => 'aggregator.pages.inc',
  61. 'template' => 'aggregator-summary-items',
  62. ),
  63. 'aggregator_summary_item' => array(
  64. 'variables' => array('aggregator_item' => NULL, 'view_mode' => NULL),
  65. 'file' => 'aggregator.pages.inc',
  66. ),
  67. 'aggregator_item' => array(
  68. 'variables' => array('aggregator_item' => NULL, 'view_mode' => NULL),
  69. 'file' => 'aggregator.pages.inc',
  70. 'template' => 'aggregator-item',
  71. ),
  72. 'aggregator_page_opml' => array(
  73. 'variables' => array('feeds' => NULL),
  74. 'file' => 'aggregator.pages.inc',
  75. ),
  76. 'aggregator_page_rss' => array(
  77. 'variables' => array('feeds' => NULL, 'category' => NULL),
  78. 'file' => 'aggregator.pages.inc',
  79. ),
  80. );
  81. }
  82. /**
  83. * Implements hook_menu().
  84. */
  85. function aggregator_menu() {
  86. $items['admin/config/services/aggregator'] = array(
  87. 'title' => 'Feed aggregator',
  88. 'description' => "Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized.",
  89. 'route_name' => 'aggregator_admin_overview',
  90. 'weight' => 10,
  91. );
  92. $items['admin/config/services/aggregator/add/feed'] = array(
  93. 'title' => 'Add feed',
  94. 'route_name' => 'aggregator_feed_add',
  95. 'type' => MENU_LOCAL_ACTION,
  96. );
  97. $items['admin/config/services/aggregator/add/category'] = array(
  98. 'title' => 'Add category',
  99. 'page callback' => 'drupal_get_form',
  100. 'page arguments' => array('aggregator_form_category'),
  101. 'access arguments' => array('administer news feeds'),
  102. 'type' => MENU_LOCAL_ACTION,
  103. 'file' => 'aggregator.admin.inc',
  104. );
  105. $items['admin/config/services/aggregator/add/opml'] = array(
  106. 'title' => 'Import OPML',
  107. 'type' => MENU_LOCAL_ACTION,
  108. 'route_name' => 'aggregator_opml_add',
  109. );
  110. $items['admin/config/services/aggregator/remove/%aggregator_feed'] = array(
  111. 'title' => 'Remove items',
  112. 'route_name' => 'aggregator_feed_items_delete',
  113. );
  114. $items['admin/config/services/aggregator/update/%aggregator_feed'] = array(
  115. 'title' => 'Update items',
  116. 'route_name' => 'aggregator_feed_refresh',
  117. );
  118. $items['admin/config/services/aggregator/list'] = array(
  119. 'title' => 'List',
  120. 'type' => MENU_DEFAULT_LOCAL_TASK,
  121. );
  122. $items['admin/config/services/aggregator/settings'] = array(
  123. 'title' => 'Settings',
  124. 'description' => 'Configure the behavior of the feed aggregator, including when to discard feed items and how to present feed items and categories.',
  125. 'route_name' => 'aggregator_admin_settings',
  126. 'type' => MENU_LOCAL_TASK,
  127. 'weight' => 100,
  128. );
  129. $items['aggregator'] = array(
  130. 'title' => 'Feed aggregator',
  131. 'weight' => 5,
  132. 'route_name' => 'aggregator_page_last',
  133. );
  134. $items['aggregator/sources'] = array(
  135. 'title' => 'Sources',
  136. 'route_name' => 'aggregator_sources',
  137. );
  138. $items['aggregator/categories'] = array(
  139. 'title' => 'Categories',
  140. 'page callback' => 'aggregator_page_categories',
  141. 'access callback' => '_aggregator_has_categories',
  142. 'file' => 'aggregator.pages.inc',
  143. );
  144. $items['aggregator/rss'] = array(
  145. 'title' => 'RSS feed',
  146. 'page callback' => 'aggregator_page_rss',
  147. 'access arguments' => array('access news feeds'),
  148. 'type' => MENU_CALLBACK,
  149. 'file' => 'aggregator.pages.inc',
  150. );
  151. $items['aggregator/opml'] = array(
  152. 'title' => 'OPML feed',
  153. 'page callback' => 'aggregator_page_opml',
  154. 'access arguments' => array('access news feeds'),
  155. 'type' => MENU_CALLBACK,
  156. 'file' => 'aggregator.pages.inc',
  157. );
  158. $items['aggregator/categories/%aggregator_category'] = array(
  159. 'title callback' => '_aggregator_category_title',
  160. 'title arguments' => array(2),
  161. 'page callback' => 'aggregator_page_category',
  162. 'page arguments' => array(2),
  163. 'access arguments' => array('access news feeds'),
  164. 'file' => 'aggregator.pages.inc',
  165. );
  166. $items['aggregator/categories/%aggregator_category/view'] = array(
  167. 'title' => 'View',
  168. 'type' => MENU_DEFAULT_LOCAL_TASK,
  169. );
  170. $items['aggregator/categories/%aggregator_category/categorize'] = array(
  171. 'title' => 'Categorize',
  172. 'page callback' => 'drupal_get_form',
  173. 'page arguments' => array('aggregator_page_category_form', 2),
  174. 'access arguments' => array('administer news feeds'),
  175. 'type' => MENU_LOCAL_TASK,
  176. 'file' => 'aggregator.pages.inc',
  177. );
  178. $items['aggregator/categories/%aggregator_category/configure'] = array(
  179. 'title' => 'Configure',
  180. 'page callback' => 'drupal_get_form',
  181. 'page arguments' => array('aggregator_form_category', 2),
  182. 'access arguments' => array('administer news feeds'),
  183. 'type' => MENU_LOCAL_TASK,
  184. 'weight' => 10,
  185. 'file' => 'aggregator.admin.inc',
  186. );
  187. $items['aggregator/sources/%aggregator_feed'] = array(
  188. 'title callback' => 'entity_page_label',
  189. 'title arguments' => array(2),
  190. 'page callback' => 'aggregator_page_source',
  191. 'page arguments' => array(2),
  192. 'access arguments' => array('access news feeds'),
  193. 'file' => 'aggregator.pages.inc',
  194. );
  195. $items['aggregator/sources/%aggregator_feed/view'] = array(
  196. 'title' => 'View',
  197. 'type' => MENU_DEFAULT_LOCAL_TASK,
  198. );
  199. $items['aggregator/sources/%aggregator_feed/categorize'] = array(
  200. 'title' => 'Categorize',
  201. 'page callback' => 'drupal_get_form',
  202. 'page arguments' => array('aggregator_page_source_form', 2),
  203. 'access arguments' => array('administer news feeds'),
  204. 'type' => MENU_LOCAL_TASK,
  205. 'file' => 'aggregator.pages.inc',
  206. );
  207. $items['aggregator/sources/%aggregator_feed/configure'] = array(
  208. 'title' => 'Configure',
  209. 'page callback' => 'entity_get_form',
  210. 'page arguments' => array(2),
  211. 'access arguments' => array('administer news feeds'),
  212. 'type' => MENU_LOCAL_TASK,
  213. 'weight' => 10,
  214. 'file' => 'aggregator.admin.inc',
  215. );
  216. $items['admin/config/services/aggregator/edit/feed/%aggregator_feed'] = array(
  217. 'title' => 'Edit feed',
  218. 'page callback' => 'entity_get_form',
  219. 'page arguments' => array(6),
  220. 'access arguments' => array('administer news feeds'),
  221. 'file' => 'aggregator.admin.inc',
  222. );
  223. $items['admin/config/services/aggregator/delete/feed/%aggregator_feed'] = array(
  224. 'title' => 'Delete feed',
  225. 'route_name' => 'aggregator_feed_delete',
  226. );
  227. $items['admin/config/services/aggregator/edit/category/%aggregator_category'] = array(
  228. 'title' => 'Edit category',
  229. 'page callback' => 'drupal_get_form',
  230. 'page arguments' => array('aggregator_form_category', 6),
  231. 'access arguments' => array('administer news feeds'),
  232. 'file' => 'aggregator.admin.inc',
  233. );
  234. return $items;
  235. }
  236. /**
  237. * Title callback: Returns a title for aggregator category pages.
  238. *
  239. * @param $category
  240. * An aggregator category.
  241. *
  242. * @return
  243. * A string with the aggregator category title.
  244. */
  245. function _aggregator_category_title($category) {
  246. return $category->title;
  247. }
  248. /**
  249. * Access callback: Determines whether there are any aggregator categories.
  250. *
  251. * @return
  252. * TRUE if there is at least one category and the user has access to them;
  253. * FALSE otherwise.
  254. */
  255. function _aggregator_has_categories() {
  256. return user_access('access news feeds') && (bool) db_query_range('SELECT 1 FROM {aggregator_category}', 0, 1)->fetchField();
  257. }
  258. /**
  259. * Implements hook_permission().
  260. */
  261. function aggregator_permission() {
  262. return array(
  263. 'administer news feeds' => array(
  264. 'title' => t('Administer news feeds'),
  265. ),
  266. 'access news feeds' => array(
  267. 'title' => t('View news feeds'),
  268. ),
  269. );
  270. }
  271. /**
  272. * Implements hook_cron().
  273. *
  274. * Queues news feeds for updates once their refresh interval has elapsed.
  275. */
  276. function aggregator_cron() {
  277. $result = db_query('SELECT fid FROM {aggregator_feed} WHERE queued = 0 AND checked + refresh < :time AND refresh <> :never', array(
  278. ':time' => REQUEST_TIME,
  279. ':never' => AGGREGATOR_CLEAR_NEVER
  280. ));
  281. $queue = Drupal::queue('aggregator_feeds');
  282. foreach ($result->fetchCol() as $fid) {
  283. $feed = aggregator_feed_load($fid);
  284. if ($queue->createItem($feed)) {
  285. // Add timestamp to avoid queueing item more than once.
  286. $feed->queued->value = REQUEST_TIME;
  287. $feed->save();
  288. }
  289. }
  290. // Remove queued timestamp after 6 hours assuming the update has failed.
  291. db_update('aggregator_feed')
  292. ->fields(array('queued' => 0))
  293. ->condition('queued', REQUEST_TIME - (3600 * 6), '<')
  294. ->execute();
  295. }
  296. /**
  297. * Implements hook_queue_info().
  298. */
  299. function aggregator_queue_info() {
  300. $queues['aggregator_feeds'] = array(
  301. 'title' => t('Aggregator refresh'),
  302. 'worker callback' => 'aggregator_refresh',
  303. 'cron' => array(
  304. 'time' => 60,
  305. ),
  306. );
  307. return $queues;
  308. }
  309. /**
  310. * Adds/edits/deletes aggregator categories.
  311. *
  312. * @param $edit
  313. * An associative array describing the category to be added/edited/deleted.
  314. */
  315. function aggregator_save_category($edit) {
  316. $link_path = 'aggregator/categories/';
  317. if (!empty($edit['cid'])) {
  318. $link_path .= $edit['cid'];
  319. if (!empty($edit['title'])) {
  320. db_merge('aggregator_category')
  321. ->key(array('cid' => $edit['cid']))
  322. ->fields(array(
  323. 'title' => $edit['title'],
  324. 'description' => $edit['description'],
  325. ))
  326. ->execute();
  327. $op = 'update';
  328. }
  329. else {
  330. db_delete('aggregator_category')
  331. ->condition('cid', $edit['cid'])
  332. ->execute();
  333. // Make sure there is no active block for this category.
  334. if (module_exists('block')) {
  335. foreach (entity_load_multiple_by_properties('block', array('plugin' => 'aggregator_category_block:' . $edit['cid'])) as $block) {
  336. $block->delete();
  337. }
  338. }
  339. $edit['title'] = '';
  340. $op = 'delete';
  341. }
  342. }
  343. elseif (!empty($edit['title'])) {
  344. // A single unique id for bundles and feeds, to use in blocks.
  345. $link_path .= db_insert('aggregator_category')
  346. ->fields(array(
  347. 'title' => $edit['title'],
  348. 'description' => $edit['description'],
  349. 'block' => 5,
  350. ))
  351. ->execute();
  352. $op = 'insert';
  353. }
  354. if (isset($op) && module_exists('menu_link')) {
  355. menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
  356. }
  357. }
  358. /**
  359. * Removes all items from a feed.
  360. *
  361. * @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed
  362. * An object describing the feed to be cleared.
  363. */
  364. function aggregator_remove(Feed $feed) {
  365. // Call \Drupal\aggregator\Plugin\ProcessorInterface::remove() on all
  366. // processors.
  367. $manager = Drupal::service('plugin.manager.aggregator.processor');
  368. foreach ($manager->getDefinitions() as $id => $definition) {
  369. $manager->createInstance($id)->remove($feed);
  370. }
  371. // Reset feed.
  372. $feed->checked->value = 0;
  373. $feed->hash->value = '';
  374. $feed->etag->value = '';
  375. $feed->modified->value = 0;
  376. $feed->save();
  377. }
  378. /**
  379. * Checks a news feed for new items.
  380. *
  381. * @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed
  382. * An object describing the feed to be refreshed.
  383. */
  384. function aggregator_refresh(Feed $feed) {
  385. // Store feed URL to track changes.
  386. $feed_url = $feed->url->value;
  387. $config = config('aggregator.settings');
  388. // Fetch the feed.
  389. $fetcher_manager = Drupal::service('plugin.manager.aggregator.fetcher');
  390. try {
  391. $success = $fetcher_manager->createInstance($config->get('fetcher'))->fetch($feed);
  392. }
  393. catch (PluginException $e) {
  394. $success = FALSE;
  395. watchdog_exception('aggregator', $e);
  396. }
  397. // Retrieve processor manager now.
  398. $processor_manager = Drupal::service('plugin.manager.aggregator.processor');
  399. // Store instances in an array so we dont have to instantiate new objects.
  400. $processor_instances = array();
  401. foreach ($config->get('processors') as $processor) {
  402. try {
  403. $processor_instances[$processor] = $processor_manager->createInstance($processor);
  404. }
  405. catch (PluginException $e) {
  406. watchdog_exception('aggregator', $e);
  407. }
  408. }
  409. // We store the hash of feed data in the database. When refreshing a
  410. // feed we compare stored hash and new hash calculated from downloaded
  411. // data. If both are equal we say that feed is not updated.
  412. $hash = hash('sha256', $feed->source_string);
  413. if ($success && ($feed->hash->value != $hash)) {
  414. // Parse the feed.
  415. $parser_manager = Drupal::service('plugin.manager.aggregator.parser');
  416. try {
  417. if ($parser_manager->createInstance($config->get('parser'))->parse($feed)) {
  418. if (empty($feed->link->value)) {
  419. $feed->link->value = $feed->url->value;
  420. }
  421. $feed->hash->value = $hash;
  422. // Update feed with parsed data.
  423. $feed->save();
  424. // Log if feed URL has changed.
  425. if ($feed->url->value != $feed_url) {
  426. watchdog('aggregator', 'Updated URL for feed %title to %url.', array('%title' => $feed->label(), '%url' => $feed->url->value));
  427. }
  428. watchdog('aggregator', 'There is new syndicated content from %site.', array('%site' => $feed->label()));
  429. drupal_set_message(t('There is new syndicated content from %site.', array('%site' => $feed->label())));
  430. // If there are items on the feed, let enabled processors process them.
  431. if (!empty($feed->items)) {
  432. foreach ($processor_instances as $instance) {
  433. $instance->process($feed);
  434. }
  435. }
  436. }
  437. }
  438. catch (PluginException $e) {
  439. watchdog_exception('aggregator', $e);
  440. }
  441. }
  442. else {
  443. drupal_set_message(t('There is no new syndicated content from %site.', array('%site' => $feed->label())));
  444. }
  445. // Regardless of successful or not, indicate that this feed has been checked.
  446. $feed->checked->value = REQUEST_TIME;
  447. $feed->queued->value = 0;
  448. $feed->save();
  449. // Processing is done, call postProcess on enabled processors.
  450. foreach ($processor_instances as $instance) {
  451. $instance->postProcess($feed);
  452. }
  453. }
  454. /**
  455. * Loads an aggregator feed.
  456. *
  457. * @param int $fid
  458. * The feed id.
  459. *
  460. * @return \Drupal\aggregator\Plugin\Core\Entity\Feed
  461. * An object describing the feed.
  462. */
  463. function aggregator_feed_load($fid) {
  464. return entity_load('aggregator_feed', $fid);
  465. }
  466. /**
  467. * Loads an aggregator category.
  468. *
  469. * @param $cid
  470. * The category id.
  471. *
  472. * @return
  473. * An associative array describing the category.
  474. */
  475. function aggregator_category_load($cid) {
  476. $categories = &drupal_static(__FUNCTION__);
  477. if (!isset($categories[$cid])) {
  478. $categories[$cid] = db_query('SELECT * FROM {aggregator_category} WHERE cid = :cid', array(':cid' => $cid))->fetchObject();
  479. }
  480. return $categories[$cid];
  481. }
  482. /**
  483. * Returns HTML for an individual feed item for display in the block.
  484. *
  485. * @param $variables
  486. * An associative array containing:
  487. * - item: The item to be displayed.
  488. * - feed: Not used.
  489. *
  490. * @ingroup themeable
  491. */
  492. function theme_aggregator_block_item($variables) {
  493. // Display the external link to the item.
  494. return '<a href="' . check_url($variables['item']->link) . '">' . check_plain($variables['item']->title) . "</a>\n";
  495. }
  496. /**
  497. * Renders the HTML content safely, as allowed.
  498. *
  499. * @param $value
  500. * The content to be filtered.
  501. *
  502. * @return
  503. * The filtered content.
  504. */
  505. function aggregator_filter_xss($value) {
  506. return filter_xss($value, preg_split('/\s+|<|>/', config('aggregator.settings')->get('items.allowed_html'), -1, PREG_SPLIT_NO_EMPTY));
  507. }
  508. /**
  509. * Implements hook_preprocess_HOOK() for block.html.twig.
  510. */
  511. function aggregator_preprocess_block(&$variables) {
  512. if ($variables['configuration']['module'] == 'aggregator') {
  513. $variables['attributes']['role'] = 'complementary';
  514. }
  515. }

Functions

Namesort descending Description
aggregator_category_load Loads an aggregator category.
aggregator_cron Implements hook_cron().
aggregator_feed_load Loads an aggregator feed.
aggregator_filter_xss Renders the HTML content safely, as allowed.
aggregator_help Implements hook_help().
aggregator_menu Implements hook_menu().
aggregator_permission Implements hook_permission().
aggregator_preprocess_block Implements hook_preprocess_HOOK() for block.html.twig.
aggregator_queue_info Implements hook_queue_info().
aggregator_refresh Checks a news feed for new items.
aggregator_remove Removes all items from a feed.
aggregator_save_category Adds/edits/deletes aggregator categories.
aggregator_theme Implements hook_theme().
theme_aggregator_block_item Returns HTML for an individual feed item for display in the block.
_aggregator_category_title Title callback: Returns a title for aggregator category pages.
_aggregator_has_categories Access callback: Determines whether there are any aggregator categories.

Constants

Namesort descending Description
AGGREGATOR_CLEAR_NEVER Denotes that a feed's items should never expire.