drupal/core/modules/aggregator/aggregator.admin.inc

555 lines
20 KiB
PHP

<?php
/**
* @file
* Administration page callbacks for the Aggregator module.
*/
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Drupal\aggregator\Plugin\FetcherManager;
use Drupal\aggregator\Plugin\Core\Entity\Feed;
/**
* Page callback: Displays the aggregator administration page.
*
* @return string
* A HTML-formatted string with administration page content.
*
* @see aggregator_menu()
*/
function aggregator_admin_overview() {
return aggregator_view();
}
/**
* Displays the aggregator administration page.
*
* @return
* The page HTML.
*/
function aggregator_view() {
$result = db_query('SELECT f.fid, f.title, f.url, f.refresh, f.checked, f.link, f.description, f.hash, f.etag, f.modified, f.image, f.block, COUNT(i.iid) AS items FROM {aggregator_feed} f LEFT JOIN {aggregator_item} i ON f.fid = i.fid GROUP BY f.fid, f.title, f.url, f.refresh, f.checked, f.link, f.description, f.hash, f.etag, f.modified, f.image, f.block ORDER BY f.title');
$output = '<h3>' . t('Feed overview') . '</h3>';
$header = array(t('Title'), t('Items'), t('Last update'), t('Next update'), t('Operations'));
$rows = array();
foreach ($result as $feed) {
$row = array();
$row[] = l($feed->title, "aggregator/sources/$feed->fid");
$row[] = format_plural($feed->items, '1 item', '@count items');
$row[] = ($feed->checked ? t('@time ago', array('@time' => format_interval(REQUEST_TIME - $feed->checked))) : t('never'));
$row[] = ($feed->checked && $feed->refresh ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - REQUEST_TIME))) : t('never'));
$links = array();
$links['edit'] = array(
'title' => t('Edit'),
'href' => "admin/config/services/aggregator/edit/feed/$feed->fid",
);
$links['remove'] = array(
'title' => t('Remove items'),
'href' => "admin/config/services/aggregator/remove/$feed->fid",
);
$links['update'] = array(
'title' => t('Update items'),
'href' => "admin/config/services/aggregator/update/$feed->fid",
'query' => array('token' => drupal_get_token("aggregator/update/$feed->fid")),
);
$row[] = array(
'data' => array(
'#type' => 'operations',
'#links' => $links,
),
);
$rows[] = $row;
}
$output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No feeds available. <a href="@link">Add feed</a>.', array('@link' => url('admin/config/services/aggregator/add/feed')))));
$result = db_query('SELECT c.cid, c.title, COUNT(ci.iid) as items FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid GROUP BY c.cid, c.title ORDER BY title');
$output .= '<h3>' . t('Category overview') . '</h3>';
$header = array(t('Title'), t('Items'), t('Operations'));
$rows = array();
foreach ($result as $category) {
$row = array();
$row[] = l($category->title, "aggregator/categories/$category->cid");
$row[] = format_plural($category->items, '1 item', '@count items');
$links = array();
$links['edit'] = array(
'title' => t('Edit'),
'href' => "admin/config/services/aggregator/edit/category/$category->cid",
);
$row[] = array(
'data' => array(
'#type' => 'operations',
'#links' => $links,
),
);
$rows[] = $row;
}
$output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No categories available. <a href="@link">Add category</a>.', array('@link' => url('admin/config/services/aggregator/add/category')))));
return $output;
}
/**
* Page callback: Presents the aggregator feed creation form.
*
* @return array
* A form array as expected by drupal_render().
*
* @see aggregator_menu()
*/
function aggregator_feed_add() {
$feed = entity_create('aggregator_feed', array(
'refresh' => 3600,
'block' => 5,
));
return entity_get_form($feed);
}
/**
* Page callback: Deletes a feed.
*
* @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed
* A feed object describing the feed to be cleared.
*
* @see aggregator_admin_remove_feed_submit()
* @see aggregator_menu()
*/
function aggregator_admin_remove_feed($form, $form_state, Feed $feed) {
return confirm_form(
array(
'feed' => array(
'#type' => 'value',
'#value' => $feed,
),
),
t('Are you sure you want to remove all items from the feed %feed?', array('%feed' => $feed->label())),
'admin/config/services/aggregator',
t('This action cannot be undone.'),
t('Remove items'),
t('Cancel')
);
}
/**
* Form submission handler for aggregator_admin_remove_feed().
*
* Removes all items from a feed and redirects to the overview page.
*/
function aggregator_admin_remove_feed_submit($form, &$form_state) {
aggregator_remove($form_state['values']['feed']);
$form_state['redirect'] = 'admin/config/services/aggregator';
}
/**
* Form constructor for importing feeds from OPML.
*
* @ingroup forms
* @see aggregator_menu()
* @see aggregator_form_opml_validate()
* @see aggregator_form_opml_submit()
*/
function aggregator_form_opml($form, &$form_state) {
$period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
$form['upload'] = array(
'#type' => 'file',
'#title' => t('OPML File'),
'#description' => t('Upload an OPML file containing a list of feeds to be imported.'),
);
$form['remote'] = array(
'#type' => 'url',
'#title' => t('OPML Remote URL'),
'#maxlength' => 1024,
'#description' => t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'),
);
$form['refresh'] = array(
'#type' => 'select',
'#title' => t('Update interval'),
'#default_value' => 3600,
'#options' => $period,
'#description' => t('The length of time between feed updates. Requires a correctly configured <a href="@cron">cron maintenance task</a>.', array('@cron' => url('admin/reports/status'))),
);
$form['block'] = array('#type' => 'select',
'#title' => t('News items in block'),
'#default_value' => 5,
'#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)),
'#description' => t("Drupal can make a block with the most recent news items of a feed. You can <a href=\"@block-admin\">configure blocks</a> to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in a feed's block. If you choose '0' these feeds' blocks will be disabled.", array('@block-admin' => url('admin/structure/block'))),
);
// Handling of categories.
$options = array_map('check_plain', db_query("SELECT cid, title FROM {aggregator_category} ORDER BY title")->fetchAllKeyed());
if ($options) {
$form['category'] = array(
'#type' => 'checkboxes',
'#title' => t('Categorize news items'),
'#options' => $options,
'#description' => t('New feed items are automatically filed in the checked categories.'),
);
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Import')
);
return $form;
}
/**
* Form validation handler for aggregator_form_opml().
*
* @see aggregator_form_opml_submit()
*/
function aggregator_form_opml_validate($form, &$form_state) {
// If both fields are empty or filled, cancel.
if (empty($form_state['values']['remote']) == empty($_FILES['files']['name']['upload'])) {
form_set_error('remote', t('You must <em>either</em> upload a file or enter a URL.'));
}
}
/**
* Form submission handler for aggregator_form_opml().
*
* @see aggregator_form_opml_validate()
*/
function aggregator_form_opml_submit($form, &$form_state) {
$data = '';
$validators = array('file_validate_extensions' => array('opml xml'));
if ($file = file_save_upload('upload', $validators)) {
$data = file_get_contents($file->uri);
}
else {
$response = drupal_http_request($form_state['values']['remote']);
if (!isset($response->error)) {
$data = $response->data;
}
else {
watchdog('aggregator', 'HTTP request to @url failed with error: @error', array('@url' => $form_state['values']['remote'], '@error' => $response->error));
}
}
$feeds = _aggregator_parse_opml($data);
if (empty($feeds)) {
drupal_set_message(t('No new feed has been added.'));
return;
}
foreach ($feeds as $feed) {
// Ensure URL is valid.
if (!valid_url($feed['url'], TRUE)) {
drupal_set_message(t('The URL %url is invalid.', array('%url' => $feed['url'])), 'warning');
continue;
}
// Check for duplicate titles or URLs.
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = :title OR url = :url", array(':title' => $feed['title'], ':url' => $feed['url']));
foreach ($result as $old) {
if (strcasecmp($old->title, $feed['title']) == 0) {
drupal_set_message(t('A feed named %title already exists.', array('%title' => $old->title)), 'warning');
continue 2;
}
if (strcasecmp($old->url, $feed['url']) == 0) {
drupal_set_message(t('A feed with the URL %url already exists.', array('%url' => $old->url)), 'warning');
continue 2;
}
}
$new_feed = entity_create('aggregator_feed', array(
'title' => $feed['title'],
'url' => $feed['url'],
'refresh' => $form_state['values']['refresh'],
'block' => $form_state['values']['block'],
));
$new_feed->categories = $form_state['values']['category'];
$new_feed->save();
}
$form_state['redirect'] = 'admin/config/services/aggregator';
}
/**
* Parses an OPML file.
*
* Feeds are recognized as <outline> elements with the attributes "text" and
* "xmlurl" set.
*
* @param $opml
* The complete contents of an OPML document.
*
* @return
* An array of feeds, each an associative array with a "title" and a "url"
* element, or NULL if the OPML document failed to be parsed. An empty array
* will be returned if the document is valid but contains no feeds, as some
* OPML documents do.
*/
function _aggregator_parse_opml($opml) {
$feeds = array();
$xml_parser = drupal_xml_parser_create($opml);
if (xml_parse_into_struct($xml_parser, $opml, $values)) {
foreach ($values as $entry) {
if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) {
$item = $entry['attributes'];
if (!empty($item['XMLURL']) && !empty($item['TEXT'])) {
$feeds[] = array('title' => $item['TEXT'], 'url' => $item['XMLURL']);
}
}
}
}
xml_parser_free($xml_parser);
return $feeds;
}
/**
* Page callback: Refreshes a feed, then redirects to the overview page.
*
* @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed
* An object describing the feed to be refreshed.
*
* @see aggregator_menu()
*/
function aggregator_admin_refresh_feed(Feed $feed) {
// @todo CSRF tokens are validated in page callbacks rather than access
// callbacks, because access callbacks are also invoked during menu link
// generation. Add token support to routing: http://drupal.org/node/755584.
$token = drupal_container()->get('request')->query->get('token');
if (!isset($token) || !drupal_valid_token($token, 'aggregator/update/' . $feed->id())) {
throw new AccessDeniedHttpException();
}
aggregator_refresh($feed);
drupal_goto('admin/config/services/aggregator');
}
/**
* Form constructor for the aggregator system settings.
*
* @see aggregator_menu()
* @see aggregator_admin_form_submit()
* @ingroup forms
*/
function aggregator_admin_form($form, $form_state) {
// Global aggregator settings.
$form['aggregator_allowed_html_tags'] = array(
'#type' => 'textfield',
'#title' => t('Allowed HTML tags'),
'#size' => 80,
'#maxlength' => 255,
'#default_value' => config('aggregator.settings')->get('items.allowed_html'),
'#description' => t('A space-separated list of HTML tags allowed in the content of feed items. Disallowed tags are stripped from the content.'),
);
// Make sure configuration is sane.
aggregator_sanitize_configuration();
// Get all available fetchers.
$fetcher_manager = new FetcherManager();
$fetchers = array();
foreach ($fetcher_manager->getDefinitions() as $id => $definition) {
$label = $definition['title'] . ' <span class="description">' . $definition['description'] . '</span>';
$fetchers[$id] = $label;
}
// Get all available parsers.
$parsers = module_implements('aggregator_parse');
foreach ($parsers as $k => $module) {
if ($info = module_invoke($module, 'aggregator_parse_info')) {
$label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>';
}
else {
$label = $module;
}
unset($parsers[$k]);
$parsers[$module] = $label;
}
// Get all available processors.
$processors = module_implements('aggregator_process');
foreach ($processors as $k => $module) {
if ($info = module_invoke($module, 'aggregator_process_info')) {
$label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>';
}
else {
$label = $module;
}
unset($processors[$k]);
$processors[$module] = $label;
}
// Only show basic configuration if there are actually options.
$basic_conf = array();
if (count($fetchers) > 1) {
$basic_conf['aggregator_fetcher'] = array(
'#type' => 'radios',
'#title' => t('Fetcher'),
'#description' => t('Fetchers download data from an external source. Choose a fetcher suitable for the external source you would like to download from.'),
'#options' => $fetchers,
'#default_value' => config('aggregator.settings')->get('fetcher'),
);
}
if (count($parsers) > 1) {
$basic_conf['aggregator_parser'] = array(
'#type' => 'radios',
'#title' => t('Parser'),
'#description' => t('Parsers transform downloaded data into standard structures. Choose a parser suitable for the type of feeds you would like to aggregate.'),
'#options' => $parsers,
'#default_value' => config('aggregator.settings')->get('parser'),
);
}
if (count($processors) > 1) {
$basic_conf['aggregator_processors'] = array(
'#type' => 'checkboxes',
'#title' => t('Processors'),
'#description' => t('Processors act on parsed feed data, for example they store feed items. Choose the processors suitable for your task.'),
'#options' => $processors,
'#default_value' => config('aggregator.settings')->get('processors'),
);
}
if (count($basic_conf)) {
$form['basic_conf'] = array(
'#type' => 'details',
'#title' => t('Basic configuration'),
'#description' => t('For most aggregation tasks, the default settings are fine.'),
'#collapsed' => FALSE,
);
$form['basic_conf'] += $basic_conf;
}
// Implementing modules will expect an array at $form['modules'].
$form['modules'] = array();
return system_config_form($form, $form_state);
}
/**
* Form submission handler for aggregator_admin_form().
*/
function aggregator_admin_form_submit($form, &$form_state) {
$config = config('aggregator.settings');
$config
->set('items.allowed_html', $form_state['values']['aggregator_allowed_html_tags'])
->set('items.expire', $form_state['values']['aggregator_clear'])
->set('items.teaser_length', $form_state['values']['aggregator_teaser_length'])
->set('source.list_max', $form_state['values']['aggregator_summary_items'])
->set('source.category_selector', $form_state['values']['aggregator_category_selector']);
if (isset($form_state['values']['aggregator_fetcher'])) {
$config->set('fetcher', $form_state['values']['aggregator_fetcher']);
}
if (isset($form_state['values']['aggregator_parser'])) {
$config->set('parser', $form_state['values']['aggregator_parser']);
}
if (isset($form_state['values']['aggregator_processors'])) {
$config->set('processors', array_filter($form_state['values']['aggregator_processors']));
}
$config->save();
}
/**
* Form constructor to add/edit/delete aggregator categories.
*
* @param $edit
* An object containing:
* - title: A string to use for the category title.
* - description: A string to use for the category description.
* - cid: The category ID.
*
* @ingroup forms
* @see aggregator_menu()
* @see aggregator_form_category_validate()
* @see aggregator_form_category_submit()
*/
function aggregator_form_category($form, &$form_state, $edit = NULL) {
$form['title'] = array('#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => isset($edit->title) ? $edit->title : '',
'#maxlength' => 64,
'#required' => TRUE,
);
$form['description'] = array('#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => isset($edit->description) ? $edit->description : '',
);
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
if (!empty($edit->cid)) {
$form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
$form['cid'] = array('#type' => 'hidden', '#value' => $edit->cid);
}
return $form;
}
/**
* Form validation handler for aggregator_form_category().
*
* @see aggregator_form_category_submit()
*/
function aggregator_form_category_validate($form, &$form_state) {
if ($form_state['values']['op'] == t('Save')) {
// Check for duplicate titles
if (isset($form_state['values']['cid'])) {
$category = db_query("SELECT cid FROM {aggregator_category} WHERE title = :title AND cid <> :cid", array(':title' => $form_state['values']['title'], ':cid' => $form_state['values']['cid']))->fetchObject();
}
else {
$category = db_query("SELECT cid FROM {aggregator_category} WHERE title = :title", array(':title' => $form_state['values']['title']))->fetchObject();
}
if ($category) {
form_set_error('title', t('A category named %category already exists. Enter a unique title.', array('%category' => $form_state['values']['title'])));
}
}
}
/**
* Form submission handler for aggregator_form_category().
*
* @see aggregator_form_category_validate()
*
* @todo Add delete confirmation dialog.
*/
function aggregator_form_category_submit($form, &$form_state) {
// @todo Replicate this cache invalidation when these ops are separated.
// Invalidate the block cache to update aggregator category-based derivatives.
if (module_exists('block')) {
drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
}
if ($form_state['values']['op'] == t('Delete')) {
$title = $form_state['values']['title'];
// Unset the title.
unset($form_state['values']['title']);
}
aggregator_save_category($form_state['values']);
if (isset($form_state['values']['cid'])) {
if (isset($form_state['values']['title'])) {
drupal_set_message(t('The category %category has been updated.', array('%category' => $form_state['values']['title'])));
if (arg(0) == 'admin') {
$form_state['redirect'] = 'admin/config/services/aggregator/';
return;
}
else {
$form_state['redirect'] = 'aggregator/categories/' . $form_state['values']['cid'];
return;
}
}
else {
watchdog('aggregator', 'Category %category deleted.', array('%category' => $title));
drupal_set_message(t('The category %category has been deleted.', array('%category' => $title)));
if (arg(0) == 'admin') {
$form_state['redirect'] = 'admin/config/services/aggregator/';
return;
}
else {
$form_state['redirect'] = 'aggregator/categories/';
return;
}
}
}
else {
watchdog('aggregator', 'Category %category added.', array('%category' => $form_state['values']['title']), WATCHDOG_NOTICE, l(t('view'), 'admin/config/services/aggregator'));
drupal_set_message(t('The category %category has been added.', array('%category' => $form_state['values']['title'])));
}
}